@agent-native/dispatch 0.29.2 → 0.29.4

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 (41) hide show
  1. package/dist/actions/list-workspace-apps.d.ts +1 -0
  2. package/dist/actions/list-workspace-apps.js +3 -0
  3. package/dist/actions/list-workspace-apps.js.map +1 -1
  4. package/dist/actions/provider-api-register.d.ts +14 -14
  5. package/dist/components/layout/Layout.d.ts.map +1 -1
  6. package/dist/components/layout/Layout.js +19 -4
  7. package/dist/components/layout/Layout.js.map +1 -1
  8. package/dist/components/workspace-app-card.d.ts.map +1 -1
  9. package/dist/components/workspace-app-card.js +9 -6
  10. package/dist/components/workspace-app-card.js.map +1 -1
  11. package/dist/components/workspace-app-host.d.ts.map +1 -1
  12. package/dist/components/workspace-app-host.js +65 -13
  13. package/dist/components/workspace-app-host.js.map +1 -1
  14. package/dist/lib/workspace-apps.d.ts +16 -0
  15. package/dist/lib/workspace-apps.d.ts.map +1 -1
  16. package/dist/lib/workspace-apps.js +71 -1
  17. package/dist/lib/workspace-apps.js.map +1 -1
  18. package/dist/server/lib/app-creation-store.d.ts +2 -0
  19. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  20. package/dist/server/lib/app-creation-store.js +9 -3
  21. package/dist/server/lib/app-creation-store.js.map +1 -1
  22. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  23. package/dist/server/lib/mcp-gateway.js +5 -42
  24. package/dist/server/lib/mcp-gateway.js.map +1 -1
  25. package/dist/shared/workspace-sso.d.ts +12 -0
  26. package/dist/shared/workspace-sso.d.ts.map +1 -1
  27. package/dist/shared/workspace-sso.js +43 -0
  28. package/dist/shared/workspace-sso.js.map +1 -1
  29. package/package.json +2 -2
  30. package/src/actions/list-workspace-apps.ts +3 -0
  31. package/src/components/layout/Layout.tsx +27 -5
  32. package/src/components/workspace-app-card.spec.tsx +60 -0
  33. package/src/components/workspace-app-card.tsx +17 -4
  34. package/src/components/workspace-app-host.spec.tsx +208 -41
  35. package/src/components/workspace-app-host.tsx +85 -10
  36. package/src/lib/workspace-apps.spec.ts +80 -0
  37. package/src/lib/workspace-apps.ts +95 -1
  38. package/src/server/lib/app-creation-store.spec.ts +43 -0
  39. package/src/server/lib/app-creation-store.ts +11 -3
  40. package/src/server/lib/mcp-gateway.ts +5 -48
  41. package/src/shared/workspace-sso.ts +55 -0
@@ -20,6 +20,9 @@ export default defineAction({
20
20
  .describe(
21
21
  "Fetch each ready app's /.well-known/agent-card.json with a short non-throwing timeout and include agentCardUrl, agentCardReachable, a2aEndpointUrl, agentName, and agentSkillsCount. Defaults to true for agent calls; UI polling should pass false. Pending Builder apps are not probed.",
22
22
  ),
23
+ includeArchived: httpBoolean
24
+ .default(false)
25
+ .describe("Include archived workspace apps in the result."),
23
26
  audience: z
24
27
  .enum(["all", "internal", "public"])
25
28
  .default("all")
@@ -106,9 +106,13 @@ import { toast } from "sonner";
106
106
  import { cn } from "../../lib/utils";
107
107
  import {
108
108
  isDispatchWorkspaceAppId,
109
+ isPathMountedWorkspaceApp,
109
110
  isWorkspaceAppVisibleInDefaultLaunchers,
111
+ isWorkspaceSsoApp,
110
112
  mergeChatFirstWorkspaceApps,
113
+ navigateToWorkspaceApp,
111
114
  workspaceAppIdFromRoute,
115
+ workspaceAppDirectHref,
112
116
  workspaceAppRoute,
113
117
  type WorkspaceAppSummary,
114
118
  } from "../../lib/workspace-apps";
@@ -1403,7 +1407,7 @@ export function Layout({
1403
1407
  const chatFirstAppLayoutHydratedRef = useRef(false);
1404
1408
  const chatFirstAppsQuery = useActionQuery<WorkspaceAppSummary[]>(
1405
1409
  "list-workspace-apps",
1406
- { includeAgentCards: false },
1410
+ { includeAgentCards: false, includeArchived: true },
1407
1411
  { enabled: chatFirstMode },
1408
1412
  );
1409
1413
  const chatFirstGrantedAppsQuery = useActionQuery<ChatFirstGrantedAppsResult>(
@@ -1450,6 +1454,26 @@ export function Layout({
1450
1454
  })),
1451
1455
  [chatFirstAppRegistrations],
1452
1456
  );
1457
+ const openChatFirstApp = useCallback(
1458
+ (app: ChatFirstAppItem) => {
1459
+ if (isDispatchWorkspaceAppId(app.id)) return;
1460
+ const registration = chatFirstAppRegistrations.find(
1461
+ (candidate) => candidate.id.toLowerCase() === app.id.toLowerCase(),
1462
+ );
1463
+ const directHref =
1464
+ registration &&
1465
+ !isWorkspaceSsoApp(registration) &&
1466
+ isPathMountedWorkspaceApp(registration)
1467
+ ? workspaceAppDirectHref(registration, "/")
1468
+ : null;
1469
+ if (directHref) {
1470
+ navigateToWorkspaceApp(directHref);
1471
+ return;
1472
+ }
1473
+ navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)));
1474
+ },
1475
+ [chatFirstAppRegistrations, navigate],
1476
+ );
1453
1477
  const chatFirstCopy = useMemo(() => createDispatchChatFirstCopy(t), [t]);
1454
1478
  const [chatFirstPane, setChatFirstPane] =
1455
1479
  useState<DispatchChatFirstPane | null>(null);
@@ -2448,7 +2472,7 @@ export function Layout({
2448
2472
  onChatFirstAppOpen={(app) => {
2449
2473
  if (isDispatchWorkspaceAppId(app.id)) return;
2450
2474
  setSidebarCollapsed(true);
2451
- navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)));
2475
+ openChatFirstApp(app);
2452
2476
  }}
2453
2477
  onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
2454
2478
  collapsible
@@ -2489,9 +2513,7 @@ export function Layout({
2489
2513
  chatFirstSurfaceTabsStore.closeAll();
2490
2514
  setChatFirstSurfacePanelOpen(false);
2491
2515
  }}
2492
- onChatFirstAppOpen={(app) =>
2493
- navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)))
2494
- }
2516
+ onChatFirstAppOpen={openChatFirstApp}
2495
2517
  onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
2496
2518
  onNavigate={() => setMobileOpen(false)}
2497
2519
  />
@@ -53,6 +53,7 @@ describe("WorkspaceAppCard", () => {
53
53
  id: "analytics",
54
54
  name: "Analytics",
55
55
  path: "/analytics",
56
+ url: "https://analytics.agent-native.com",
56
57
  description: "Explore product and growth performance.",
57
58
  status: "ready",
58
59
  }}
@@ -116,6 +117,65 @@ describe("WorkspaceAppCard", () => {
116
117
  ).toBeNull();
117
118
  });
118
119
 
120
+ it("opens mounted workspace apps at their published URL", async () => {
121
+ const originalParent = window.parent;
122
+ const originalTop = window.top;
123
+ const topWindow = { location: { href: "" } } as unknown as Window;
124
+ Object.defineProperty(window, "parent", {
125
+ configurable: true,
126
+ value: {},
127
+ });
128
+ Object.defineProperty(window, "top", {
129
+ configurable: true,
130
+ value: topWindow,
131
+ });
132
+
133
+ try {
134
+ await act(async () => {
135
+ root.render(
136
+ <MemoryRouter>
137
+ <TooltipProvider>
138
+ <WorkspaceAppCard
139
+ app={{
140
+ id: "feedback-leaderboard",
141
+ name: "Feedback leaderboard",
142
+ path: "/feedback-leaderboard",
143
+ url: "https://agent-workspace.builder.io/feedback-leaderboard/leaderboard",
144
+ status: "ready",
145
+ }}
146
+ />
147
+ </TooltipProvider>
148
+ </MemoryRouter>,
149
+ );
150
+ });
151
+
152
+ const openButton = container.querySelector<HTMLButtonElement>(
153
+ ".app-open-actions__primary",
154
+ );
155
+ expect(openButton).not.toBeNull();
156
+ expect(openButton?.textContent).toContain("Open app");
157
+ expect(
158
+ container.querySelector(
159
+ 'a[href="https://agent-workspace.builder.io/feedback-leaderboard/leaderboard"]',
160
+ ),
161
+ ).toBeNull();
162
+
163
+ await act(async () => openButton?.click());
164
+ expect(topWindow.location.href).toBe(
165
+ "https://agent-workspace.builder.io/feedback-leaderboard/leaderboard",
166
+ );
167
+ } finally {
168
+ Object.defineProperty(window, "parent", {
169
+ configurable: true,
170
+ value: originalParent,
171
+ });
172
+ Object.defineProperty(window, "top", {
173
+ configurable: true,
174
+ value: originalTop,
175
+ });
176
+ }
177
+ });
178
+
119
179
  it("keeps pinning in the app open menu", async () => {
120
180
  const onTogglePinned = vi.fn();
121
181
  await act(async () => {
@@ -26,6 +26,10 @@ import { toast } from "sonner";
26
26
 
27
27
  import { cn } from "../lib/utils";
28
28
  import {
29
+ isPathMountedWorkspaceApp,
30
+ isWorkspaceSsoApp,
31
+ navigateToWorkspaceApp,
32
+ workspaceAppDirectHref,
29
33
  workspaceAppHref,
30
34
  workspaceAppRoute,
31
35
  type WorkspaceAppSummary,
@@ -75,6 +79,12 @@ export function WorkspaceAppCard({
75
79
  }) {
76
80
  const t = useT();
77
81
  const href = workspaceAppHref(app);
82
+ const directHref =
83
+ app.status !== "pending" &&
84
+ !isWorkspaceSsoApp(app) &&
85
+ isPathMountedWorkspaceApp(app)
86
+ ? workspaceAppDirectHref(app, "/")
87
+ : null;
78
88
  const isPending = app.status === "pending";
79
89
  const pendingLabel = app.statusLabel || "Builder branch";
80
90
  const pendingOpenLabel = t("dispatch.pages.openBuilderBranch", {
@@ -193,7 +203,8 @@ export function WorkspaceAppCard({
193
203
  <div className="flex shrink-0 items-center gap-2">
194
204
  <WorkspaceAppOpenActions
195
205
  app={app}
196
- href={href}
206
+ href={directHref ?? href}
207
+ openDirectly={Boolean(directHref)}
197
208
  isPinned={isPinned}
198
209
  pinLabel={pinLabel}
199
210
  pendingOpenLabel={pendingOpenLabel}
@@ -285,6 +296,7 @@ export function WorkspaceAppCard({
285
296
  function WorkspaceAppOpenActions({
286
297
  app,
287
298
  href,
299
+ openDirectly,
288
300
  isPinned,
289
301
  pinLabel,
290
302
  pendingOpenLabel,
@@ -292,6 +304,7 @@ function WorkspaceAppOpenActions({
292
304
  }: {
293
305
  app: WorkspaceAppSummary;
294
306
  href: string | null;
307
+ openDirectly: boolean;
295
308
  isPinned: boolean;
296
309
  pinLabel: string;
297
310
  pendingOpenLabel: string;
@@ -328,9 +341,9 @@ function WorkspaceAppOpenActions({
328
341
  name={app.name}
329
342
  href={href}
330
343
  showNewTabOption
331
- onOpen={() => {
332
- navigate(appRoute);
333
- }}
344
+ onOpen={() =>
345
+ openDirectly ? navigateToWorkspaceApp(href) : navigate(appRoute)
346
+ }
334
347
  menuItems={
335
348
  onTogglePinned
336
349
  ? [
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React, { act } from "react";
4
4
  import { createRoot, type Root } from "react-dom/client";
5
+ import { MemoryRouter } from "react-router";
5
6
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
7
 
7
8
  const clientState = vi.hoisted(() => {
@@ -13,8 +14,19 @@ const clientState = vi.hoisted(() => {
13
14
  }));
14
15
  const legacyErrorMutateAsync = vi.fn();
15
16
  const actionNames: string[] = [];
17
+ const actionQueryParams: Array<{
18
+ name: string;
19
+ params?: unknown;
20
+ }> = [];
21
+ const actionQueryOptions: Array<{
22
+ name: string;
23
+ options?: Record<string, unknown>;
24
+ }> = [];
16
25
  return {
17
26
  actionNames,
27
+ actionQueryParams,
28
+ actionQueryOptions,
29
+ workspaceApps: null as Array<Record<string, unknown>> | null,
18
30
  grantedApps: [
19
31
  {
20
32
  id: "analytics.agent-native.com",
@@ -98,44 +110,54 @@ vi.mock("@agent-native/core/client/hooks", () => ({
98
110
  : clientState.legacyMutateAsync,
99
111
  };
100
112
  },
101
- useActionQuery: (name: string) => ({
102
- data:
103
- name === "list_apps"
104
- ? { apps: clientState.grantedApps }
105
- : [
106
- {
107
- id: "mail",
108
- name: "Mail",
109
- path: "/mail",
110
- url: null,
111
- status: "ready",
112
- },
113
- {
114
- id: "calendar",
115
- name: "Calendar",
116
- path: "/calendar",
117
- url: null,
118
- status: "ready",
119
- },
120
- {
121
- id: "documents",
122
- name: "Documents",
123
- path: "/documents",
124
- url: null,
125
- status: "ready",
126
- },
127
- {
128
- id: "settings",
129
- name: "Settings",
130
- path: "/settings",
131
- url: null,
132
- status: "ready",
133
- },
134
- ],
135
- isError: false,
136
- isLoading: false,
137
- refetch: vi.fn(),
138
- }),
113
+ useActionQuery: (name: string, params?: unknown, options?: unknown) => {
114
+ clientState.actionQueryParams.push({ name, params });
115
+ clientState.actionQueryOptions.push({
116
+ name,
117
+ options:
118
+ options && typeof options === "object"
119
+ ? (options as Record<string, unknown>)
120
+ : undefined,
121
+ });
122
+ return {
123
+ data:
124
+ name === "list_apps"
125
+ ? { apps: clientState.grantedApps }
126
+ : (clientState.workspaceApps ?? [
127
+ {
128
+ id: "mail",
129
+ name: "Mail",
130
+ path: "/mail",
131
+ url: null,
132
+ status: "ready",
133
+ },
134
+ {
135
+ id: "calendar",
136
+ name: "Calendar",
137
+ path: "/calendar",
138
+ url: null,
139
+ status: "ready",
140
+ },
141
+ {
142
+ id: "documents",
143
+ name: "Documents",
144
+ path: "/documents",
145
+ url: null,
146
+ status: "ready",
147
+ },
148
+ {
149
+ id: "settings",
150
+ name: "Settings",
151
+ path: "/settings",
152
+ url: null,
153
+ status: "ready",
154
+ },
155
+ ]),
156
+ isError: false,
157
+ isLoading: false,
158
+ refetch: vi.fn(),
159
+ };
160
+ },
139
161
  }));
140
162
 
141
163
  vi.mock("@agent-native/core/client/i18n", () => ({
@@ -158,6 +180,9 @@ describe("WorkspaceAppKeepAlive", () => {
158
180
  document.body.appendChild(container);
159
181
  root = createRoot(container);
160
182
  clientState.actionNames.length = 0;
183
+ clientState.actionQueryParams.length = 0;
184
+ clientState.actionQueryOptions.length = 0;
185
+ clientState.workspaceApps = null;
161
186
  clientState.legacyMutateAsync.mockClear();
162
187
  clientState.legacyMutateError = null;
163
188
  clientState.legacyErrorMutateAsync.mockReset();
@@ -212,6 +237,15 @@ describe("WorkspaceAppKeepAlive", () => {
212
237
  expect(calendarEntry?.classList.contains("hidden")).toBe(false);
213
238
  expect(calendarEntry?.querySelector("iframe")).not.toBeNull();
214
239
  expect(container.querySelectorAll("iframe")).toHaveLength(2);
240
+ expect(
241
+ clientState.actionQueryParams.find(
242
+ (query) => query.name === "list-workspace-apps",
243
+ )?.params,
244
+ ).toEqual({ includeAgentCards: false, includeArchived: true });
245
+ expect(
246
+ clientState.actionQueryOptions.find((query) => query.name === "list_apps")
247
+ ?.options?.enabled,
248
+ ).toBe(false);
215
249
  });
216
250
 
217
251
  it("resolves a granted external app instead of showing app not found", async () => {
@@ -236,6 +270,38 @@ describe("WorkspaceAppKeepAlive", () => {
236
270
  url: "https://analytics.agent-native.com",
237
271
  chrome: "minimal",
238
272
  });
273
+ expect(
274
+ clientState.actionQueryOptions.find((query) => query.name === "list_apps")
275
+ ?.options?.enabled,
276
+ ).toBe(true);
277
+ });
278
+
279
+ it("does not resolve archived workspace apps from granted discovery", async () => {
280
+ clientState.workspaceApps = [
281
+ {
282
+ id: "analytics.agent-native.com",
283
+ name: "Hidden app",
284
+ path: "/hidden-app",
285
+ url: null,
286
+ status: "ready",
287
+ archived: true,
288
+ },
289
+ ];
290
+
291
+ await act(async () => {
292
+ root.render(
293
+ <MemoryRouter>
294
+ <WorkspaceAppKeepAlive activeAppId="analytics.agent-native.com" />
295
+ </MemoryRouter>,
296
+ );
297
+ await Promise.resolve();
298
+ await Promise.resolve();
299
+ });
300
+
301
+ expect(
302
+ container.querySelector('[data-chat-first-app-status="ready"]'),
303
+ ).toBeNull();
304
+ expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
239
305
  });
240
306
 
241
307
  it("uses the app-scoped workspace session action when the rollout is enabled", async () => {
@@ -243,7 +309,14 @@ describe("WorkspaceAppKeepAlive", () => {
243
309
 
244
310
  await act(async () => {
245
311
  root.render(
246
- <WorkspaceAppFrame app={{ id: "mail", name: "Mail", path: "/mail" }} />,
312
+ <WorkspaceAppFrame
313
+ app={{
314
+ id: "mail",
315
+ name: "Mail",
316
+ path: "/mail",
317
+ url: "https://mail.agent-native.com",
318
+ }}
319
+ />,
247
320
  );
248
321
  await Promise.resolve();
249
322
  await Promise.resolve();
@@ -251,12 +324,99 @@ describe("WorkspaceAppKeepAlive", () => {
251
324
 
252
325
  expect(clientState.workspaceSsoMutateAsync).toHaveBeenCalledWith({
253
326
  app: "mail",
254
- path: "/mail",
327
+ url: "https://mail.agent-native.com",
255
328
  chrome: "minimal",
256
329
  });
257
330
  expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
258
331
  });
259
332
 
333
+ it("uses the granted-app session when a mounted app reuses a canonical id", async () => {
334
+ clientState.workspaceSsoEnabled = true;
335
+
336
+ await act(async () => {
337
+ root.render(
338
+ <WorkspaceAppFrame
339
+ app={{
340
+ id: "mail",
341
+ name: "Internal Mail",
342
+ path: "/mail",
343
+ url: "https://agent-workspace.builder.io/mail",
344
+ }}
345
+ />,
346
+ );
347
+ await Promise.resolve();
348
+ await Promise.resolve();
349
+ });
350
+
351
+ expect(clientState.legacyMutateAsync).toHaveBeenCalledWith({
352
+ app: "mail",
353
+ url: "https://agent-workspace.builder.io/mail",
354
+ chrome: "minimal",
355
+ });
356
+ expect(clientState.workspaceSsoMutateAsync).not.toHaveBeenCalled();
357
+ });
358
+
359
+ it("uses the granted-app session action for mounted apps outside the SSO registry", async () => {
360
+ clientState.workspaceSsoEnabled = true;
361
+
362
+ await act(async () => {
363
+ root.render(
364
+ <WorkspaceAppFrame
365
+ app={{
366
+ id: "feedback-leaderboard",
367
+ name: "Feedback leaderboard",
368
+ path: "/feedback-leaderboard",
369
+ url: "https://agent-workspace.builder.io/feedback-leaderboard/leaderboard",
370
+ }}
371
+ />,
372
+ );
373
+ await Promise.resolve();
374
+ await Promise.resolve();
375
+ });
376
+
377
+ expect(clientState.legacyMutateAsync).toHaveBeenCalledWith({
378
+ app: "feedback-leaderboard",
379
+ url: "https://agent-workspace.builder.io/feedback-leaderboard/leaderboard",
380
+ chrome: "minimal",
381
+ });
382
+ expect(clientState.workspaceSsoMutateAsync).not.toHaveBeenCalled();
383
+ });
384
+
385
+ it("mints custom SSO before navigating an embedded Dispatch surface", async () => {
386
+ clientState.workspaceSsoEnabled = true;
387
+ const navigateToTopWindow = vi.fn(() => true);
388
+ Object.defineProperty(window, "parent", {
389
+ configurable: true,
390
+ value: {},
391
+ });
392
+
393
+ await act(async () => {
394
+ root.render(
395
+ <WorkspaceAppFrame
396
+ app={{
397
+ id: "custom-sso",
398
+ name: "Custom SSO",
399
+ path: "/custom-sso",
400
+ url: "https://custom.example/custom-sso",
401
+ workspaceSso: true,
402
+ }}
403
+ navigateToTopWindow={navigateToTopWindow}
404
+ />,
405
+ );
406
+ await Promise.resolve();
407
+ await Promise.resolve();
408
+ });
409
+
410
+ expect(clientState.workspaceSsoMutateAsync).toHaveBeenCalledWith({
411
+ app: "custom-sso",
412
+ url: "https://custom.example/custom-sso",
413
+ chrome: "minimal",
414
+ });
415
+ expect(navigateToTopWindow).toHaveBeenCalledWith("about:blank");
416
+ expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
417
+ expect(container.querySelector("iframe")).toBeNull();
418
+ });
419
+
260
420
  it("sends the parent theme on iframe load and when the parent changes", async () => {
261
421
  await act(async () => {
262
422
  root.render(
@@ -354,7 +514,14 @@ describe("WorkspaceAppKeepAlive", () => {
354
514
 
355
515
  await act(async () => {
356
516
  root.render(
357
- <WorkspaceAppFrame app={{ id: "mail", name: "Mail", path: "/mail" }} />,
517
+ <WorkspaceAppFrame
518
+ app={{
519
+ id: "mail",
520
+ name: "Mail",
521
+ path: "/mail",
522
+ url: "https://mail.agent-native.com",
523
+ }}
524
+ />,
358
525
  );
359
526
  await Promise.resolve();
360
527
  await Promise.resolve();