@agent-native/dispatch 0.24.5 → 0.25.0

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 (151) hide show
  1. package/README.md +12 -1
  2. package/dist/actions/connect-external-agent.d.ts +18 -0
  3. package/dist/actions/connect-external-agent.d.ts.map +1 -0
  4. package/dist/actions/connect-external-agent.js +68 -0
  5. package/dist/actions/connect-external-agent.js.map +1 -0
  6. package/dist/actions/create-workspace-resource.d.ts +14 -14
  7. package/dist/actions/create-workspace-resource.js +9 -2
  8. package/dist/actions/create-workspace-resource.js.map +1 -1
  9. package/dist/actions/create_workspace_app_embed_session.d.ts +13 -0
  10. package/dist/actions/create_workspace_app_embed_session.d.ts.map +1 -0
  11. package/dist/actions/create_workspace_app_embed_session.js +30 -0
  12. package/dist/actions/create_workspace_app_embed_session.js.map +1 -0
  13. package/dist/actions/import-agent-pack.d.ts +96 -0
  14. package/dist/actions/import-agent-pack.d.ts.map +1 -0
  15. package/dist/actions/import-agent-pack.js +116 -0
  16. package/dist/actions/import-agent-pack.js.map +1 -0
  17. package/dist/actions/import-agent.d.ts +60 -0
  18. package/dist/actions/import-agent.d.ts.map +1 -0
  19. package/dist/actions/import-agent.js +82 -0
  20. package/dist/actions/import-agent.js.map +1 -0
  21. package/dist/actions/index.d.ts.map +1 -1
  22. package/dist/actions/index.js +10 -0
  23. package/dist/actions/index.js.map +1 -1
  24. package/dist/actions/list-agent-pack.d.ts +36 -0
  25. package/dist/actions/list-agent-pack.d.ts.map +1 -0
  26. package/dist/actions/list-agent-pack.js +32 -0
  27. package/dist/actions/list-agent-pack.js.map +1 -0
  28. package/dist/actions/list-workspace-resource-options.d.ts +1 -1
  29. package/dist/actions/list-workspace-resource-options.js +8 -1
  30. package/dist/actions/list-workspace-resource-options.js.map +1 -1
  31. package/dist/actions/list-workspace-resources.d.ts +1 -1
  32. package/dist/actions/list-workspace-resources.js +8 -1
  33. package/dist/actions/list-workspace-resources.js.map +1 -1
  34. package/dist/actions/navigate.js +1 -1
  35. package/dist/actions/navigate.js.map +1 -1
  36. package/dist/actions/provider-api-register.d.ts +10 -10
  37. package/dist/actions/update-workspace-resource.d.ts +13 -13
  38. package/dist/actions/view-screen.js +18 -1
  39. package/dist/actions/view-screen.js.map +1 -1
  40. package/dist/components/index.d.ts +1 -0
  41. package/dist/components/index.d.ts.map +1 -1
  42. package/dist/components/index.js +1 -0
  43. package/dist/components/index.js.map +1 -1
  44. package/dist/components/layout/Layout.d.ts +1 -8
  45. package/dist/components/layout/Layout.d.ts.map +1 -1
  46. package/dist/components/layout/Layout.js +33 -106
  47. package/dist/components/layout/Layout.js.map +1 -1
  48. package/dist/components/simple-agents-panel.d.ts +9 -0
  49. package/dist/components/simple-agents-panel.d.ts.map +1 -0
  50. package/dist/components/simple-agents-panel.js +405 -0
  51. package/dist/components/simple-agents-panel.js.map +1 -0
  52. package/dist/components/workspace-app-host.d.ts +29 -0
  53. package/dist/components/workspace-app-host.d.ts.map +1 -0
  54. package/dist/components/workspace-app-host.js +145 -0
  55. package/dist/components/workspace-app-host.js.map +1 -0
  56. package/dist/db/migrations.d.ts +2 -4
  57. package/dist/db/migrations.d.ts.map +1 -1
  58. package/dist/db/migrations.js +25 -0
  59. package/dist/db/migrations.js.map +1 -1
  60. package/dist/db/schema.js +1 -1
  61. package/dist/db/schema.js.map +1 -1
  62. package/dist/hooks/use-navigation-state.d.ts +1 -0
  63. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  64. package/dist/hooks/use-navigation-state.js +10 -0
  65. package/dist/hooks/use-navigation-state.js.map +1 -1
  66. package/dist/lib/agent-pack.d.ts +31 -0
  67. package/dist/lib/agent-pack.d.ts.map +1 -0
  68. package/dist/lib/agent-pack.js +149 -0
  69. package/dist/lib/agent-pack.js.map +1 -0
  70. package/dist/lib/simple-agent-profile.d.ts +36 -0
  71. package/dist/lib/simple-agent-profile.d.ts.map +1 -0
  72. package/dist/lib/simple-agent-profile.js +183 -0
  73. package/dist/lib/simple-agent-profile.js.map +1 -0
  74. package/dist/routes/pages/agents.d.ts.map +1 -1
  75. package/dist/routes/pages/agents.js +11 -1
  76. package/dist/routes/pages/agents.js.map +1 -1
  77. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  78. package/dist/routes/pages/apps.$appId.js +4 -95
  79. package/dist/routes/pages/apps.$appId.js.map +1 -1
  80. package/dist/routes/pages/chat.d.ts.map +1 -1
  81. package/dist/routes/pages/chat.js +62 -10
  82. package/dist/routes/pages/chat.js.map +1 -1
  83. package/dist/server/lib/agent-pack-store.d.ts +24 -0
  84. package/dist/server/lib/agent-pack-store.d.ts.map +1 -0
  85. package/dist/server/lib/agent-pack-store.js +22 -0
  86. package/dist/server/lib/agent-pack-store.js.map +1 -0
  87. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  88. package/dist/server/lib/dispatch-store.js +4 -0
  89. package/dist/server/lib/dispatch-store.js.map +1 -1
  90. package/dist/server/lib/mcp-gateway.d.ts +11 -0
  91. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  92. package/dist/server/lib/mcp-gateway.js +155 -9
  93. package/dist/server/lib/mcp-gateway.js.map +1 -1
  94. package/dist/server/lib/workspace-resources-store.d.ts +15 -1
  95. package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
  96. package/dist/server/lib/workspace-resources-store.js +11 -2
  97. package/dist/server/lib/workspace-resources-store.js.map +1 -1
  98. package/dist/server/plugins/agent-chat.js +13 -0
  99. package/dist/server/plugins/agent-chat.js.map +1 -1
  100. package/dist/shared/feature-flags.d.ts +7 -0
  101. package/dist/shared/feature-flags.d.ts.map +1 -0
  102. package/dist/shared/feature-flags.js +12 -0
  103. package/dist/shared/feature-flags.js.map +1 -0
  104. package/dist/shared/workspace-sso.d.ts +36 -0
  105. package/dist/shared/workspace-sso.d.ts.map +1 -0
  106. package/dist/shared/workspace-sso.js +99 -0
  107. package/dist/shared/workspace-sso.js.map +1 -0
  108. package/package.json +5 -3
  109. package/src/actions/connect-external-agent.ts +92 -0
  110. package/src/actions/create-workspace-resource.ts +9 -2
  111. package/src/actions/create_workspace_app_embed_session.ts +32 -0
  112. package/src/actions/import-agent-pack.ts +146 -0
  113. package/src/actions/import-agent.ts +102 -0
  114. package/src/actions/index.spec.ts +13 -0
  115. package/src/actions/index.ts +10 -0
  116. package/src/actions/list-agent-pack.ts +43 -0
  117. package/src/actions/list-workspace-resource-options.ts +8 -1
  118. package/src/actions/list-workspace-resources.ts +8 -1
  119. package/src/actions/navigate.ts +1 -1
  120. package/src/actions/view-screen.spec.ts +29 -1
  121. package/src/actions/view-screen.ts +19 -1
  122. package/src/components/app-icon.spec.tsx +10 -0
  123. package/src/components/index.ts +1 -0
  124. package/src/components/layout/Layout.spec.tsx +2 -0
  125. package/src/components/layout/Layout.tsx +59 -171
  126. package/src/components/simple-agents-panel.spec.tsx +73 -0
  127. package/src/components/simple-agents-panel.tsx +1226 -0
  128. package/src/components/workspace-app-host.spec.tsx +204 -0
  129. package/src/components/workspace-app-host.tsx +359 -0
  130. package/src/db/migrations.spec.ts +5 -1
  131. package/src/db/migrations.ts +28 -1
  132. package/src/db/schema.ts +1 -1
  133. package/src/hooks/use-navigation-state.spec.ts +25 -0
  134. package/src/hooks/use-navigation-state.ts +11 -0
  135. package/src/lib/agent-pack.spec.ts +47 -0
  136. package/src/lib/agent-pack.ts +226 -0
  137. package/src/lib/simple-agent-profile.spec.ts +77 -0
  138. package/src/lib/simple-agent-profile.ts +247 -0
  139. package/src/routes/index.spec.ts +1 -0
  140. package/src/routes/pages/agents.tsx +21 -1
  141. package/src/routes/pages/apps.$appId.tsx +3 -240
  142. package/src/routes/pages/chat.spec.tsx +62 -0
  143. package/src/routes/pages/chat.tsx +117 -9
  144. package/src/server/lib/agent-pack-store.ts +35 -0
  145. package/src/server/lib/dispatch-store.ts +8 -0
  146. package/src/server/lib/mcp-gateway.spec.ts +141 -0
  147. package/src/server/lib/mcp-gateway.ts +227 -15
  148. package/src/server/lib/workspace-resources-store.ts +9 -2
  149. package/src/server/plugins/agent-chat.ts +13 -0
  150. package/src/shared/feature-flags.ts +13 -0
  151. package/src/shared/workspace-sso.ts +118 -0
@@ -93,6 +93,19 @@ export default defineAction({
93
93
  purpose:
94
94
  "Create apps, manage workspace resources, route work to connected agents, and continue Dispatch conversations.",
95
95
  };
96
+ const agentPath =
97
+ typeof navigation.agentPath === "string"
98
+ ? navigation.agentPath.trim()
99
+ : "";
100
+ if (agentPath) {
101
+ const agents = await listWorkspaceResourceOptions({ kind: "agent" });
102
+ const agent = agents.find((resource) => resource.path === agentPath);
103
+ screen.chatSurface = {
104
+ ...(screen.chatSurface as Record<string, unknown>),
105
+ agentPath,
106
+ ...(agent ? { agent } : {}),
107
+ };
108
+ }
96
109
  }
97
110
  if (navigation?.view === "overview") {
98
111
  screen.recentAudit = overview.recentAudit.slice(0, 5);
@@ -101,7 +114,7 @@ export default defineAction({
101
114
  if (navigation?.view === "destinations") {
102
115
  screen.recentDestinations = overview.recentDestinations;
103
116
  }
104
- if (navigation?.view === "agents") {
117
+ if (navigation?.view === "connected-agents") {
105
118
  const [connectedAgents, mcpAccess] = await Promise.all([
106
119
  runLocalDispatchAction("list-connected-agents", {}),
107
120
  runLocalDispatchAction("list-mcp-app-access", {}),
@@ -109,6 +122,11 @@ export default defineAction({
109
122
  screen.connectedAgents = connectedAgents;
110
123
  screen.mcpAppAccess = mcpAccess;
111
124
  }
125
+ if (navigation?.view === "agents") {
126
+ screen.simpleAgents = await listWorkspaceResourceOptions({
127
+ kind: "agent",
128
+ });
129
+ }
112
130
  if (navigation?.view === "operations") {
113
131
  const nav = navigation as { operationsView?: string };
114
132
  screen.operatorConsole = {
@@ -31,4 +31,14 @@ describe("AppIcon", () => {
31
31
  );
32
32
  expect(icon.style.color).toBe("rgb(var(--dispatch-app-icon-color-rgb))");
33
33
  });
34
+
35
+ it("removes the colored background for monochrome rail icons", () => {
36
+ act(() => {
37
+ root.render(<AppIcon id="plan" name="Plan" monochrome />);
38
+ });
39
+
40
+ const icon = container.firstElementChild as HTMLElement;
41
+ expect(icon.className).toContain("bg-transparent");
42
+ expect(icon.style.cssText).not.toContain("background-color");
43
+ });
34
44
  });
@@ -17,3 +17,4 @@ export type {
17
17
  export { CreateAppPopover, CreateAppFlow } from "./create-app-popover.js";
18
18
  export { AppKeysPopover } from "./app-keys-popover.js";
19
19
  export { ActionQueryError } from "./action-query-error.js";
20
+ export { SimpleAgentsPanel } from "./simple-agents-panel.js";
@@ -61,6 +61,7 @@ vi.mock("@agent-native/core/client/i18n", () => ({
61
61
  "dispatch.nav.chat": "Chat",
62
62
  "dispatch.nav.overview": "Overview",
63
63
  "dispatch.nav.apps": "Apps",
64
+ "dispatch.nav.agents": "Agents",
64
65
  "dispatch.pages.workspaceApps": "Workspace apps",
65
66
  "dispatch.nav.operate": "Operate",
66
67
  "dispatch.nav.advanced": "Advanced",
@@ -196,6 +197,7 @@ describe("Dispatch NavContent", () => {
196
197
  expect(primaryLabels.indexOf("Overview")).toBeLessThan(
197
198
  primaryLabels.indexOf("Chat"),
198
199
  );
200
+ expect(primaryLabels).toContain("Agents");
199
201
  });
200
202
 
201
203
  it("keeps collapsed navigation compact and preserves section spacing", async () => {
@@ -77,6 +77,7 @@ import {
77
77
  IconBrandTelegram,
78
78
  IconCopy,
79
79
  IconEye,
80
+ IconHierarchy2,
80
81
  IconMessageQuestion,
81
82
  IconBroadcast,
82
83
  IconLayoutSidebarLeftCollapse,
@@ -102,13 +103,11 @@ import {
102
103
  import { Link, useLocation, useNavigate } from "react-router";
103
104
  import { toast } from "sonner";
104
105
 
105
- import { isEmbedSessionExpiredMessage } from "../../lib/embed-session-recovery";
106
106
  import { cn } from "../../lib/utils";
107
107
  import {
108
108
  isDispatchWorkspaceAppId,
109
109
  isWorkspaceAppVisibleInDefaultLaunchers,
110
110
  mergeChatFirstWorkspaceApps,
111
- workspaceAppDirectHref,
112
111
  workspaceAppIdFromRoute,
113
112
  workspaceAppRoute,
114
113
  type WorkspaceAppSummary,
@@ -124,9 +123,15 @@ import {
124
123
  import { Sheet, SheetContent, SheetDescription, SheetTitle } from "../ui/sheet";
125
124
  import { Skeleton } from "../ui/skeleton";
126
125
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
126
+ import {
127
+ WorkspaceAppFrame,
128
+ WorkspaceAppKeepAlive,
129
+ } from "../workspace-app-host";
127
130
  import { Header } from "./Header";
128
131
  import { HeaderActionsProvider } from "./HeaderActions";
129
132
 
133
+ export { buildChatFirstEmbedSessionInput } from "../workspace-app-host";
134
+
130
135
  export type DispatchNavSection = "primary" | "operations";
131
136
 
132
137
  export type DispatchNavIcon = ComponentType<{
@@ -180,6 +185,13 @@ const PRIMARY_NAV_ITEMS = [
180
185
  icon: IconApps,
181
186
  section: "primary",
182
187
  },
188
+ {
189
+ id: "agents",
190
+ to: "/agents",
191
+ label: "Agents",
192
+ icon: IconHierarchy2,
193
+ section: "primary",
194
+ },
183
195
  ] as const satisfies readonly DispatchNavItem[];
184
196
 
185
197
  const BOTTOM_NAV_ITEMS = [
@@ -212,17 +224,6 @@ interface DispatchChatFirstPane {
212
224
  view?: string;
213
225
  }
214
226
 
215
- interface ChatFirstEmbedSessionResult {
216
- startUrl: string;
217
- }
218
-
219
- interface ChatFirstEmbedSessionInput {
220
- app?: string;
221
- path?: string;
222
- url?: string;
223
- chrome: "minimal";
224
- }
225
-
226
227
  interface ChatFirstGrantedAppSummary {
227
228
  id: string;
228
229
  name: string;
@@ -233,13 +234,6 @@ interface ChatFirstGrantedAppsResult {
233
234
  apps: ChatFirstGrantedAppSummary[];
234
235
  }
235
236
 
236
- export function buildChatFirstEmbedSessionInput(
237
- appId: string,
238
- path: string,
239
- ): ChatFirstEmbedSessionInput {
240
- return { app: appId, path, chrome: "minimal" };
241
- }
242
-
243
237
  interface DispatchAgentThreadSummary {
244
238
  id: string;
245
239
  title: string;
@@ -1088,11 +1082,12 @@ export function NavContent({
1088
1082
  onNavigate?.();
1089
1083
  }}
1090
1084
  createAppTrigger={chatFirstCreateAppTrigger}
1091
- renderIcon={(app) => (
1085
+ renderIcon={(app, options) => (
1092
1086
  <AppIcon
1093
1087
  id={app.id}
1094
1088
  name={app.name}
1095
1089
  size="sm"
1090
+ monochrome={options?.isInactive}
1096
1091
  className="size-7 rounded-lg"
1097
1092
  />
1098
1093
  )}
@@ -1369,6 +1364,10 @@ export function Layout({
1369
1364
  const isWorkspaceAppRoute = shouldAutoCollapseDispatchSidebar(
1370
1365
  location.pathname,
1371
1366
  );
1367
+ const workspaceAppId = isWorkspaceAppRoute
1368
+ ? workspaceAppIdFromRoute(localPathname)
1369
+ : null;
1370
+ const workspaceAppRouteActive = Boolean(workspaceAppId);
1372
1371
  const isWorkspaceAppHostRoute =
1373
1372
  isWorkspaceAppRoute &&
1374
1373
  typeof window !== "undefined" &&
@@ -1446,20 +1445,8 @@ export function Layout({
1446
1445
  [chatFirstAppRegistrations],
1447
1446
  );
1448
1447
  const chatFirstCopy = useMemo(() => createDispatchChatFirstCopy(t), [t]);
1449
- const createChatFirstEmbedSession = useActionMutation<
1450
- ChatFirstEmbedSessionResult,
1451
- ChatFirstEmbedSessionInput
1452
- >("create_embed_session", { skipActionQueryInvalidation: true });
1453
1448
  const [chatFirstPane, setChatFirstPane] =
1454
1449
  useState<DispatchChatFirstPane | null>(null);
1455
- const [chatFirstEmbedUrl, setChatFirstEmbedUrl] = useState<string | null>(
1456
- null,
1457
- );
1458
- const [chatFirstEmbedError, setChatFirstEmbedError] = useState<string | null>(
1459
- null,
1460
- );
1461
- const [chatFirstEmbedAttempt, setChatFirstEmbedAttempt] = useState(0);
1462
- const chatFirstAppFrameRef = useRef<HTMLIFrameElement>(null);
1463
1450
  const [chatFirstNotice, setChatFirstNotice] = useState<string | null>(null);
1464
1451
  const chatFirstSessionWatch = useChatFirstSessionWatch();
1465
1452
  const chatFirstSurfaceTabs = useChatFirstSurfaceTabs(chatFirstSurfaceScope);
@@ -1512,28 +1499,6 @@ export function Layout({
1512
1499
  : chatFirstAppSelected && activeChatFirstSurfaceTab?.kind === "app"
1513
1500
  ? activeChatFirstSurfaceTab.appId
1514
1501
  : undefined;
1515
- const activeChatFirstAppRegistration = useMemo(
1516
- () =>
1517
- activeChatFirstSurfaceTab?.kind === "app" &&
1518
- activeChatFirstSurfaceTab.appId
1519
- ? (chatFirstAppRegistrations.find(
1520
- (app) => app.id === activeChatFirstSurfaceTab.appId,
1521
- ) ?? null)
1522
- : null,
1523
- [activeChatFirstSurfaceTab, chatFirstAppRegistrations],
1524
- );
1525
- const activeChatFirstApp = useMemo(
1526
- () =>
1527
- activeChatFirstAppRegistration
1528
- ? {
1529
- id: activeChatFirstAppRegistration.id,
1530
- name:
1531
- activeChatFirstAppRegistration.name ??
1532
- activeChatFirstAppRegistration.id,
1533
- }
1534
- : null,
1535
- [activeChatFirstAppRegistration],
1536
- );
1537
1502
  const chatFirstAgentActivities = useMemo<ChatFirstAgentActivity[]>(
1538
1503
  () =>
1539
1504
  (chatFirstAgentsQuery.data?.threads ?? []).map((thread) => ({
@@ -1546,82 +1511,6 @@ export function Layout({
1546
1511
  })),
1547
1512
  [chatFirstAgentsQuery.data, chatFirstCopy],
1548
1513
  );
1549
- const chatFirstEmbedPath =
1550
- activeChatFirstSurfaceTab?.kind === "app"
1551
- ? (activeChatFirstSurfaceTab.path ??
1552
- chatFirstPane?.path ??
1553
- activeChatFirstAppRegistration?.path ??
1554
- "/")
1555
- : null;
1556
- useEffect(() => {
1557
- if (!chatFirstMode || !isChatRoute || !activeChatFirstApp) {
1558
- setChatFirstEmbedUrl(null);
1559
- setChatFirstEmbedError(null);
1560
- return;
1561
- }
1562
- if (!chatFirstEmbedPath?.startsWith("/")) {
1563
- setChatFirstEmbedUrl(null);
1564
- setChatFirstEmbedError(chatFirstCopy("appUnavailable"));
1565
- return;
1566
- }
1567
- let cancelled = false;
1568
- setChatFirstEmbedUrl(null);
1569
- setChatFirstEmbedError(null);
1570
- void createChatFirstEmbedSession
1571
- .mutateAsync(
1572
- buildChatFirstEmbedSessionInput(
1573
- activeChatFirstApp.id,
1574
- chatFirstEmbedPath,
1575
- ),
1576
- )
1577
- .then((result) => {
1578
- if (!cancelled) setChatFirstEmbedUrl(result.startUrl);
1579
- })
1580
- .catch((cause: unknown) => {
1581
- if (!cancelled) {
1582
- const directAppUrl = activeChatFirstAppRegistration
1583
- ? workspaceAppDirectHref(
1584
- activeChatFirstAppRegistration,
1585
- chatFirstEmbedPath,
1586
- )
1587
- : null;
1588
- if (directAppUrl) setChatFirstEmbedUrl(directAppUrl);
1589
- setChatFirstEmbedError(
1590
- cause instanceof Error
1591
- ? cause.message
1592
- : chatFirstCopy("appUnavailable"),
1593
- );
1594
- }
1595
- });
1596
- return () => {
1597
- cancelled = true;
1598
- };
1599
- }, [
1600
- activeChatFirstApp,
1601
- chatFirstCopy,
1602
- chatFirstEmbedAttempt,
1603
- chatFirstEmbedPath,
1604
- chatFirstMode,
1605
- createChatFirstEmbedSession.mutateAsync,
1606
- isChatRoute,
1607
- ]);
1608
-
1609
- useEffect(() => {
1610
- if (!chatFirstMode || !isChatRoute || !activeChatFirstApp) return;
1611
-
1612
- const handleEmbedSessionExpired = (event: MessageEvent) => {
1613
- if (
1614
- !isEmbedSessionExpiredMessage(event, chatFirstAppFrameRef.current, null)
1615
- )
1616
- return;
1617
- setChatFirstEmbedAttempt((attempt) => attempt + 1);
1618
- };
1619
-
1620
- window.addEventListener("message", handleEmbedSessionExpired);
1621
- return () =>
1622
- window.removeEventListener("message", handleEmbedSessionExpired);
1623
- }, [activeChatFirstApp, chatFirstMode, isChatRoute]);
1624
-
1625
1514
  const persistChatFirstPane = useCallback(
1626
1515
  (pane: DispatchChatFirstPane | null) => {
1627
1516
  setChatFirstPane(pane);
@@ -1962,8 +1851,6 @@ export function Layout({
1962
1851
  }
1963
1852
  previousChatFirstSurfaceScopeRef.current = chatFirstSurfaceScope;
1964
1853
  persistChatFirstPane(null);
1965
- setChatFirstEmbedUrl(null);
1966
- setChatFirstEmbedError(null);
1967
1854
  setChatFirstNotice(null);
1968
1855
  pendingChatFirstOpenAppRef.current = null;
1969
1856
  chatFirstPaneHydratedRef.current = true;
@@ -2151,47 +2038,45 @@ export function Layout({
2151
2038
  const renderChatFirstSurfaceTab = useCallback(
2152
2039
  (tab: ChatFirstSurfaceTab) => {
2153
2040
  if (tab.kind === "app") {
2154
- if (tab.id !== chatFirstSurfaceTabs.activeTabId) return null;
2155
2041
  const registration = tab.appId
2156
2042
  ? (chatFirstAppRegistrations.find(
2157
2043
  (candidate) => candidate.id === tab.appId,
2158
2044
  ) ?? null)
2159
2045
  : null;
2160
- const app = registration
2161
- ? {
2046
+ if (!registration) {
2047
+ return (
2048
+ <ChatFirstAppPane
2049
+ app={null}
2050
+ status={chatFirstAppsQuery.isLoading ? "loading" : "unresolved"}
2051
+ renderEmbed={() => null}
2052
+ copy={chatFirstCopy}
2053
+ />
2054
+ );
2055
+ }
2056
+ const embedPath = tab.path ?? registration.path ?? "/";
2057
+ if (!embedPath.startsWith("/")) {
2058
+ return (
2059
+ <ChatFirstAppPane
2060
+ app={{
2061
+ id: registration.id,
2062
+ name: registration.name ?? registration.id,
2063
+ }}
2064
+ status="error"
2065
+ errorMessage={chatFirstCopy("appUnavailable")}
2066
+ renderEmbed={() => null}
2067
+ copy={chatFirstCopy}
2068
+ />
2069
+ );
2070
+ }
2071
+ return (
2072
+ <WorkspaceAppFrame
2073
+ app={{
2162
2074
  id: registration.id,
2163
2075
  name: registration.name ?? registration.id,
2164
- }
2165
- : null;
2166
- return (
2167
- <ChatFirstAppPane
2168
- app={app}
2169
- status={
2170
- chatFirstEmbedUrl
2171
- ? "ready"
2172
- : chatFirstEmbedError
2173
- ? "error"
2174
- : chatFirstAppsQuery.isLoading
2175
- ? "loading"
2176
- : app
2177
- ? "loading"
2178
- : "unresolved"
2179
- }
2180
- embedUrl={chatFirstEmbedUrl}
2181
- errorMessage={chatFirstEmbedError}
2182
- onRetry={() => setChatFirstEmbedAttempt((value) => value + 1)}
2183
- renderEmbed={({ url, title }: ChatFirstEmbedTarget) => (
2184
- <iframe
2185
- key={`${url}:${chatFirstEmbedAttempt}`}
2186
- data-dispatch-chat-first-app-frame
2187
- src={url}
2188
- title={title ?? chatFirstCopy("appUnavailable")}
2189
- ref={chatFirstAppFrameRef}
2190
- referrerPolicy="no-referrer"
2191
- allow="clipboard-read; clipboard-write"
2192
- className="h-full w-full border-0 bg-background"
2193
- />
2194
- )}
2076
+ path: registration.path,
2077
+ url: registration.url,
2078
+ }}
2079
+ embedPath={embedPath}
2195
2080
  copy={chatFirstCopy}
2196
2081
  />
2197
2082
  );
@@ -2263,8 +2148,6 @@ export function Layout({
2263
2148
  chatFirstAgentsQuery.isLoading,
2264
2149
  chatFirstAppsQuery.isLoading,
2265
2150
  chatFirstCopy,
2266
- chatFirstEmbedError,
2267
- chatFirstEmbedUrl,
2268
2151
  chatFirstSessionWatch.target,
2269
2152
  chatFirstSurfaceTabs.activeTabId,
2270
2153
  chatFirstAppRegistrations,
@@ -2449,7 +2332,7 @@ export function Layout({
2449
2332
  </>
2450
2333
  )}
2451
2334
  </div>
2452
- ) : isWorkspaceAppRoute ? (
2335
+ ) : workspaceAppRouteActive ? null : isWorkspaceAppRoute ? (
2453
2336
  appContent
2454
2337
  ) : (
2455
2338
  <AgentSidebar
@@ -2553,7 +2436,12 @@ export function Layout({
2553
2436
  </SheetContent>
2554
2437
  </Sheet>
2555
2438
 
2556
- {content}
2439
+ <div className="relative min-w-0 flex-1 overflow-hidden">
2440
+ {content}
2441
+ <WorkspaceAppKeepAlive
2442
+ activeAppId={workspaceAppRouteActive ? workspaceAppId : null}
2443
+ />
2444
+ </div>
2557
2445
  </div>
2558
2446
  </HeaderActionsProvider>
2559
2447
  </DispatchExtensionsContext.Provider>
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ handleAgentPackMutationSuccess,
5
+ isPendingWorkspaceResourceApproval,
6
+ } from "./simple-agents-panel";
7
+
8
+ describe("agent pack resource mutations", () => {
9
+ it("recognizes pending workspace-resource approvals", () => {
10
+ expect(
11
+ isPendingWorkspaceResourceApproval({
12
+ status: "pending",
13
+ changeType: "workspace-resource.update",
14
+ }),
15
+ ).toBe(true);
16
+ expect(
17
+ isPendingWorkspaceResourceApproval({
18
+ status: "pending",
19
+ changeType: "workspace-resource.create",
20
+ }),
21
+ ).toBe(true);
22
+ });
23
+
24
+ it("does not treat applied resources or unrelated pending results as approvals", () => {
25
+ expect(
26
+ isPendingWorkspaceResourceApproval({
27
+ id: "resource_1",
28
+ status: "applied",
29
+ changeType: "workspace-resource.update",
30
+ }),
31
+ ).toBe(false);
32
+ expect(
33
+ isPendingWorkspaceResourceApproval({
34
+ status: "pending",
35
+ changeType: "destination.upsert",
36
+ }),
37
+ ).toBe(false);
38
+ expect(isPendingWorkspaceResourceApproval(null)).toBe(false);
39
+ });
40
+
41
+ it("reports queued approval without running applied-only refresh work", () => {
42
+ const notifications: string[] = [];
43
+ const onApplied = () => notifications.push("refreshed");
44
+
45
+ handleAgentPackMutationSuccess(
46
+ { status: "pending", changeType: "workspace-resource.update" },
47
+ {
48
+ appliedMessage: "Pack file updated",
49
+ approvalMessage: "Pack file update queued for approval",
50
+ onApplied,
51
+ notify: (message) => notifications.push(message),
52
+ },
53
+ );
54
+
55
+ expect(notifications).toEqual(["Pack file update queued for approval"]);
56
+ });
57
+
58
+ it("reports applied and runs refresh work for an applied resource", () => {
59
+ const notifications: string[] = [];
60
+
61
+ handleAgentPackMutationSuccess(
62
+ { id: "resource_1" },
63
+ {
64
+ appliedMessage: "Pack file added",
65
+ approvalMessage: "Pack file addition queued for approval",
66
+ onApplied: () => notifications.push("refreshed"),
67
+ notify: (message) => notifications.push(message),
68
+ },
69
+ );
70
+
71
+ expect(notifications).toEqual(["Pack file added", "refreshed"]);
72
+ });
73
+ });