@agent-native/dispatch 0.40.30 → 0.40.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +9 -13
  2. package/dist/actions/list-connected-agents.js +7 -3
  3. package/dist/actions/list-connected-agents.js.map +1 -1
  4. package/dist/actions/list-workspace-connections.d.ts.map +1 -1
  5. package/dist/actions/list-workspace-connections.js +13 -18
  6. package/dist/actions/list-workspace-connections.js.map +1 -1
  7. package/dist/components/action-query-error.d.ts +2 -1
  8. package/dist/components/action-query-error.d.ts.map +1 -1
  9. package/dist/components/action-query-error.js +25 -2
  10. package/dist/components/action-query-error.js.map +1 -1
  11. package/dist/components/chat-home-apps.d.ts.map +1 -1
  12. package/dist/components/chat-home-apps.js +2 -2
  13. package/dist/components/chat-home-apps.js.map +1 -1
  14. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  15. package/dist/components/dispatch-control-plane.js +1 -1
  16. package/dist/components/dispatch-control-plane.js.map +1 -1
  17. package/dist/components/layout/Layout.d.ts +1 -0
  18. package/dist/components/layout/Layout.d.ts.map +1 -1
  19. package/dist/components/layout/Layout.js +7 -0
  20. package/dist/components/layout/Layout.js.map +1 -1
  21. package/dist/routes/pages/$appId.d.ts.map +1 -1
  22. package/dist/routes/pages/$appId.js +1 -1
  23. package/dist/routes/pages/$appId.js.map +1 -1
  24. package/dist/routes/pages/apps.js +1 -1
  25. package/dist/routes/pages/apps.js.map +1 -1
  26. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  27. package/dist/server/lib/app-creation-store.js +122 -54
  28. package/dist/server/lib/app-creation-store.js.map +1 -1
  29. package/package.json +3 -3
  30. package/src/actions/list-connected-agents.spec.ts +17 -1
  31. package/src/actions/list-connected-agents.ts +7 -2
  32. package/src/actions/list-workspace-connections.spec.ts +14 -3
  33. package/src/actions/list-workspace-connections.ts +13 -17
  34. package/src/components/action-query-error.spec.tsx +20 -0
  35. package/src/components/action-query-error.tsx +40 -0
  36. package/src/components/chat-home-apps.spec.tsx +32 -1
  37. package/src/components/chat-home-apps.tsx +6 -1
  38. package/src/components/dispatch-control-plane.tsx +1 -0
  39. package/src/components/layout/Layout.tsx +9 -0
  40. package/src/routes/pages/$appId.tsx +1 -0
  41. package/src/routes/pages/apps.spec.tsx +26 -2
  42. package/src/routes/pages/apps.tsx +2 -0
  43. package/src/server/lib/app-creation-store.spec.ts +281 -5
  44. package/src/server/lib/app-creation-store.ts +172 -60
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.40.30",
3
+ "version": "0.40.31",
4
4
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -90,7 +90,7 @@
90
90
  "tailwind-merge": "^3.5.0",
91
91
  "vaul": "^1.1.2",
92
92
  "zod": "^4.3.6",
93
- "@agent-native/toolkit": "0.212.0"
93
+ "@agent-native/toolkit": "0.212.1"
94
94
  },
95
95
  "devDependencies": {
96
96
  "@react-router/dev": "^8.1.0",
@@ -104,7 +104,7 @@
104
104
  "typescript-7": "npm:typescript@^7.0.2",
105
105
  "vite": "8.1.0",
106
106
  "vitest": "^4.1.5",
107
- "@agent-native/core": "0.212.0"
107
+ "@agent-native/core": "0.212.1"
108
108
  },
109
109
  "peerDependencies": {
110
110
  "@agent-native/core": ">=0.209.1",
@@ -3,7 +3,9 @@ import { describe, expect, it, vi } from "vitest";
3
3
  const mocks = vi.hoisted(() => ({
4
4
  discoverAgents: vi.fn(),
5
5
  getBuiltinAgents: vi.fn(),
6
- getRequestUserEmail: vi.fn(() => "owner@example.test"),
6
+ getRequestUserEmail: vi.fn<() => string | undefined>(
7
+ () => "owner@example.test",
8
+ ),
7
9
  resourceGet: vi.fn(),
8
10
  resourceListAccessible: vi.fn(async (): Promise<unknown[]> => []),
9
11
  parseRemoteAgentManifest: vi.fn(),
@@ -141,4 +143,18 @@ describe("list-connected-agents", () => {
141
143
  expect(entry).toMatchObject({ id: "mail", source: "workspace" });
142
144
  expect(entry).not.toHaveProperty("homeUrl");
143
145
  });
146
+
147
+ it("returns a typed authentication error when the session has no user email", async () => {
148
+ mocks.discoverAgents.mockResolvedValue([]);
149
+ mocks.getBuiltinAgents.mockReturnValue([]);
150
+ mocks.loadWorkspaceAppsManifest.mockResolvedValue(null);
151
+ mocks.getRequestUserEmail.mockReturnValueOnce(undefined);
152
+
153
+ const { default: action } = await import("./list-connected-agents.js");
154
+
155
+ await expect(action.run({})).rejects.toMatchObject({
156
+ statusCode: 401,
157
+ errorCode: "dispatch_user_identity_missing",
158
+ });
159
+ });
144
160
  });
@@ -1,4 +1,4 @@
1
- import { defineAction } from "@agent-native/core/action";
1
+ import { defineAction, fail } from "@agent-native/core/action";
2
2
  import {
3
3
  REMOTE_AGENT_RESOURCE_PREFIXES,
4
4
  parseRemoteAgentManifest,
@@ -45,7 +45,12 @@ export default defineAction({
45
45
  builtins.map((agent) => [agent.id, agent.url]),
46
46
  );
47
47
  const ownerEmail = getRequestUserEmail();
48
- if (!ownerEmail) throw new Error("no authenticated user");
48
+ if (!ownerEmail) {
49
+ fail("Sign in to list connected agents.", {
50
+ errorCode: "dispatch_user_identity_missing",
51
+ statusCode: 401,
52
+ });
53
+ }
49
54
  const resources: Array<{ id: string; path: string; owner: string }> = [];
50
55
  for (const prefix of [...REMOTE_AGENT_RESOURCE_PREFIXES].reverse()) {
51
56
  resources.push(...(await resourceListAccessible(ownerEmail, prefix)));
@@ -10,6 +10,9 @@ const mocks = vi.hoisted(() => ({
10
10
  hasWorkspaceProviderOAuthCredentials: vi.fn(),
11
11
  isProviderApiId: vi.fn(() => false),
12
12
  listProviderApiCatalog: vi.fn(() => []),
13
+ listWorkspaceApps: vi.fn(async () => [
14
+ { id: "dispatch", name: "Dispatch", status: "ready" },
15
+ ]),
13
16
  listWorkspaceConnectionGrants: vi.fn(),
14
17
  listWorkspaceConnectionProviders: vi.fn(),
15
18
  listWorkspaceConnectionsForUser: vi.fn(),
@@ -54,9 +57,7 @@ vi.mock("@agent-native/core/workspace-connections", () => ({
54
57
  vi.mock("@agent-native/dispatch/actions", () => ({
55
58
  dispatchActions: {
56
59
  "list-workspace-apps": {
57
- run: vi.fn(async () => [
58
- { id: "dispatch", name: "Dispatch", status: "ready" },
59
- ]),
60
+ run: mocks.listWorkspaceApps,
60
61
  },
61
62
  },
62
63
  }));
@@ -123,4 +124,14 @@ describe("list-workspace-connections", () => {
123
124
  expect(result.providers.map((provider) => provider.id)).toEqual(["github"]);
124
125
  expect(result.counts.providers).toBe(1);
125
126
  });
127
+
128
+ it("preserves workspace app lookup failures instead of returning partial grants", async () => {
129
+ const error = Object.assign(new Error("Workspace registry unavailable"), {
130
+ statusCode: 503,
131
+ errorCode: "workspace_gateway_org_a2a_secret_unavailable",
132
+ });
133
+ mocks.listWorkspaceApps.mockRejectedValueOnce(error);
134
+
135
+ await expect(action.run({})).rejects.toBe(error);
136
+ });
126
137
  });
@@ -79,23 +79,19 @@ async function listGrantApps(): Promise<GrantApp[]> {
79
79
  const listWorkspaceApps = dispatchActions["list-workspace-apps"];
80
80
  if (!listWorkspaceApps) return [{ id: "dispatch", label: "Dispatch" }];
81
81
 
82
- try {
83
- const apps = (await listWorkspaceApps.run({
84
- includeAgentCards: false,
85
- audience: "all",
86
- } as any)) as WorkspaceApp[];
87
- const grantApps = apps
88
- .filter((app) => !app.archived && app.status !== "pending")
89
- .map((app) => ({
90
- id: app.id,
91
- label: app.name || humanizeAppId(app.id),
92
- }));
93
- return grantApps.length > 0
94
- ? grantApps
95
- : [{ id: "dispatch", label: "Dispatch" }];
96
- } catch {
97
- return [{ id: "dispatch", label: "Dispatch" }];
98
- }
82
+ const apps = (await listWorkspaceApps.run({
83
+ includeAgentCards: false,
84
+ audience: "all",
85
+ } as any)) as WorkspaceApp[];
86
+ const grantApps = apps
87
+ .filter((app) => !app.archived && app.status !== "pending")
88
+ .map((app) => ({
89
+ id: app.id,
90
+ label: app.name || humanizeAppId(app.id),
91
+ }));
92
+ return grantApps.length > 0
93
+ ? grantApps
94
+ : [{ id: "dispatch", label: "Dispatch" }];
99
95
  }
100
96
 
101
97
  export default defineAction({
@@ -53,4 +53,24 @@ describe("ActionQueryError", () => {
53
53
  });
54
54
  expect(onRetry).toHaveBeenCalledOnce();
55
55
  });
56
+
57
+ it("shows the query and safe HTTP diagnostic without exposing the message", () => {
58
+ act(() => {
59
+ root.render(
60
+ <ActionQueryError
61
+ error={Object.assign(new Error("secret database detail"), {
62
+ status: 401,
63
+ errorCode: "workspace_gateway_rejected",
64
+ })}
65
+ queryName="list-workspace-apps"
66
+ onRetry={() => {}}
67
+ />,
68
+ );
69
+ });
70
+
71
+ expect(container.textContent).toContain(
72
+ "list-workspace-apps · HTTP 401 · workspace_gateway_rejected",
73
+ );
74
+ expect(container.textContent).not.toContain("secret database detail");
75
+ });
56
76
  });
@@ -8,13 +8,48 @@ interface ActionQueryErrorProps {
8
8
  error?: unknown;
9
9
  onRetry: () => unknown;
10
10
  className?: string;
11
+ queryName?: string;
12
+ }
13
+
14
+ function actionQueryDiagnostic(
15
+ error: unknown,
16
+ queryName: string | undefined,
17
+ ): string | null {
18
+ const parts: string[] = [];
19
+ if (queryName) parts.push(queryName);
20
+
21
+ if (error && typeof error === "object") {
22
+ const details = error as Record<string, unknown>;
23
+ const status =
24
+ typeof details.statusCode === "number"
25
+ ? details.statusCode
26
+ : details.status;
27
+ if (
28
+ typeof status === "number" &&
29
+ Number.isInteger(status) &&
30
+ status >= 100 &&
31
+ status <= 599
32
+ ) {
33
+ parts.push(`HTTP ${status}`);
34
+ }
35
+
36
+ const code = details.errorCode ?? details.code;
37
+ if (typeof code === "string" && /^[a-zA-Z0-9_.-]{1,80}$/.test(code)) {
38
+ parts.push(code);
39
+ }
40
+ }
41
+
42
+ return parts.length > 0 ? parts.join(" · ") : null;
11
43
  }
12
44
 
13
45
  export function ActionQueryError({
46
+ error,
14
47
  onRetry,
15
48
  className,
49
+ queryName,
16
50
  }: ActionQueryErrorProps) {
17
51
  const t = useT();
52
+ const diagnostic = actionQueryDiagnostic(error, queryName);
18
53
 
19
54
  return (
20
55
  <Alert variant="destructive" className={className}>
@@ -22,6 +57,11 @@ export function ActionQueryError({
22
57
  <AlertTitle>{t("dispatch.pages.dataLoadFailed")}</AlertTitle>
23
58
  <AlertDescription className="flex flex-wrap items-center justify-between gap-3">
24
59
  <span>{t("dispatch.pages.dataLoadFailedDescription")}</span>
60
+ {diagnostic ? (
61
+ <code className="w-full break-all text-xs text-muted-foreground">
62
+ {diagnostic}
63
+ </code>
64
+ ) : null}
25
65
  <Button type="button" variant="outline" size="sm" onClick={onRetry}>
26
66
  <IconRefresh className="size-4" />
27
67
  {t("dispatch.pages.tryAgain")}
@@ -25,7 +25,11 @@ const launcherState = vi.hoisted(() => ({
25
25
  },
26
26
  ],
27
27
  isLoading: false,
28
- error: undefined,
28
+ error: undefined as unknown,
29
+ errorQueryName: undefined as
30
+ | "list-workspace-apps"
31
+ | "list-connected-agents"
32
+ | undefined,
29
33
  openApp: vi.fn(),
30
34
  retry: vi.fn(),
31
35
  },
@@ -89,6 +93,8 @@ describe("DispatchChatHomeApps", () => {
89
93
  ];
90
94
  launcherState.value.openApp.mockReset();
91
95
  launcherState.value.retry.mockReset();
96
+ launcherState.value.error = undefined;
97
+ launcherState.value.errorQueryName = undefined;
92
98
  container = document.createElement("div");
93
99
  document.body.appendChild(container);
94
100
  root = createRoot(container);
@@ -194,4 +200,29 @@ describe("DispatchChatHomeApps", () => {
194
200
  expect(container.textContent).toContain("Localized Calendar description");
195
201
  expect(container.textContent).not.toContain("Default English text");
196
202
  });
203
+
204
+ it.each([
205
+ ["list-workspace-apps", "Workspace apps failed"],
206
+ ["list-connected-agents", "Connected agents failed"],
207
+ ] as const)("identifies the failed %s query", async (queryName, message) => {
208
+ launcherState.value.workspaceApps = [];
209
+ launcherState.value.error = Object.assign(new Error(message), {
210
+ statusCode: 503,
211
+ errorCode: "workspace_gateway_unavailable",
212
+ });
213
+ launcherState.value.errorQueryName = queryName;
214
+
215
+ await act(async () => {
216
+ root.render(
217
+ <MemoryRouter initialEntries={["/chat"]}>
218
+ <DispatchChatHomeApps />
219
+ </MemoryRouter>,
220
+ );
221
+ });
222
+
223
+ expect(container.textContent).toContain(
224
+ `${queryName} · HTTP 503 · workspace_gateway_unavailable`,
225
+ );
226
+ expect(container.textContent).not.toContain(message);
227
+ });
197
228
  });
@@ -48,7 +48,11 @@ export function DispatchChatHomeApps() {
48
48
  if (launcher.error && launcher.workspaceApps.length === 0) {
49
49
  return (
50
50
  <div className="mx-auto w-full max-w-[1000px]">
51
- <ActionQueryError error={launcher.error} onRetry={launcher.retry} />
51
+ <ActionQueryError
52
+ error={launcher.error}
53
+ onRetry={launcher.retry}
54
+ queryName={launcher.errorQueryName}
55
+ />
52
56
  </div>
53
57
  );
54
58
  }
@@ -61,6 +65,7 @@ export function DispatchChatHomeApps() {
61
65
  error={launcher.error}
62
66
  onRetry={launcher.retry}
63
67
  className="mb-3"
68
+ queryName={launcher.errorQueryName}
64
69
  />
65
70
  ) : null}
66
71
  <section aria-label={t("dispatch.nav.apps")}>
@@ -360,6 +360,7 @@ export function DispatchControlPlane() {
360
360
  <ActionQueryError
361
361
  error={appsQuery.error}
362
362
  onRetry={() => void appsQuery.refetch()}
363
+ queryName="list-workspace-apps"
363
364
  />
364
365
  ) : (
365
366
  <AppsPanel apps={workspaceApps ?? []} isLoading={appsLoading} />
@@ -299,6 +299,7 @@ interface DispatchWorkspaceAppLauncher {
299
299
  workspaceApps: readonly WorkspaceAppSummary[];
300
300
  isLoading: boolean;
301
301
  error?: unknown;
302
+ errorQueryName?: "list-workspace-apps" | "list-connected-agents";
302
303
  openApp: (app: ChatFirstAppItem) => void;
303
304
  retry: () => void;
304
305
  }
@@ -1487,6 +1488,12 @@ function DispatchLayout({
1487
1488
  const chatFirstAppsFailed = chatFirstAppsQuery.isError;
1488
1489
  const chatFirstAppsError =
1489
1490
  chatFirstAppsQuery.error ?? chatFirstConnectedAppsQuery.error;
1491
+ const chatFirstAppsErrorQueryName =
1492
+ chatFirstAppsQuery.error != null
1493
+ ? "list-workspace-apps"
1494
+ : chatFirstConnectedAppsQuery.error != null
1495
+ ? "list-connected-agents"
1496
+ : undefined;
1490
1497
  const chatFirstResolvableApps = useMemo(
1491
1498
  () =>
1492
1499
  mergeChatFirstWorkspaceApps(
@@ -1574,6 +1581,7 @@ function DispatchLayout({
1574
1581
  workspaceApps: chatFirstWorkspaceApps,
1575
1582
  isLoading: chatFirstAppsLoading,
1576
1583
  error: chatFirstAppsError,
1584
+ errorQueryName: chatFirstAppsErrorQueryName,
1577
1585
  openApp: openChatFirstApp,
1578
1586
  retry: () => {
1579
1587
  void chatFirstAppsQuery.refetch();
@@ -1582,6 +1590,7 @@ function DispatchLayout({
1582
1590
  }),
1583
1591
  [
1584
1592
  chatFirstAppsError,
1593
+ chatFirstAppsErrorQueryName,
1585
1594
  chatFirstAppsLoading,
1586
1595
  chatFirstAppsQuery.refetch,
1587
1596
  chatFirstConnectedAppsQuery.refetch,
@@ -98,6 +98,7 @@ export default function WorkspaceAppCatchAllRoute() {
98
98
  <ActionQueryError
99
99
  error={appsQuery.error}
100
100
  onRetry={() => void appsQuery.refetch()}
101
+ queryName="list-workspace-apps"
101
102
  />
102
103
  </DispatchShell>
103
104
  );
@@ -80,8 +80,16 @@ vi.mock("@agent-native/core/client/i18n", () => ({
80
80
  }));
81
81
 
82
82
  vi.mock("../../components/action-query-error", () => ({
83
- ActionQueryError: ({ error }: { error: Error }) => (
84
- <div role="alert">{error.message}</div>
83
+ ActionQueryError: ({
84
+ error,
85
+ queryName,
86
+ }: {
87
+ error: Error;
88
+ queryName?: string;
89
+ }) => (
90
+ <div role="alert">
91
+ {queryName}: {error.message}
92
+ </div>
85
93
  ),
86
94
  }));
87
95
 
@@ -167,6 +175,22 @@ describe("Dispatch apps route", () => {
167
175
  expect(container.textContent).toContain("No workspace apps");
168
176
  });
169
177
 
178
+ it("identifies each app discovery query when both fail", async () => {
179
+ queryState.workspaceAppsError = new Error("Workspace app list failed");
180
+ queryState.connectedAppsError = new Error("Connected agent list failed");
181
+
182
+ await renderAppsRoute();
183
+
184
+ const errors = Array.from(
185
+ container.querySelectorAll('[role="alert"]'),
186
+ (alert) => alert.textContent,
187
+ );
188
+ expect(errors).toContain("list-workspace-apps: Workspace app list failed");
189
+ expect(errors).toContain(
190
+ "list-connected-agents: Connected agent list failed",
191
+ );
192
+ });
193
+
170
194
  it("hides community apps while the Lab is off", async () => {
171
195
  await renderAppsRoute();
172
196
 
@@ -185,12 +185,14 @@ function AppsRoute() {
185
185
  <ActionQueryError
186
186
  error={appsQuery.error}
187
187
  onRetry={() => void appsQuery.refetch()}
188
+ queryName="list-workspace-apps"
188
189
  />
189
190
  ) : null}
190
191
  {connectedAppsQuery.isError ? (
191
192
  <ActionQueryError
192
193
  error={connectedAppsQuery.error}
193
194
  onRetry={() => void connectedAppsQuery.refetch()}
195
+ queryName="list-connected-agents"
194
196
  />
195
197
  ) : null}
196
198
  {showAppSkeletons ? (