@agent-native/dispatch 0.22.1 → 0.23.1

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 (61) hide show
  1. package/dist/actions/provider-api-register.d.ts +1 -1
  2. package/dist/components/app-icon.d.ts +2 -1
  3. package/dist/components/app-icon.d.ts.map +1 -1
  4. package/dist/components/app-icon.js +51 -29
  5. package/dist/components/app-icon.js.map +1 -1
  6. package/dist/components/app-list-row.d.ts +1 -1
  7. package/dist/components/app-list-row.d.ts.map +1 -1
  8. package/dist/components/app-list-row.js +1 -1
  9. package/dist/components/app-list-row.js.map +1 -1
  10. package/dist/components/app-open-actions.d.ts +2 -1
  11. package/dist/components/app-open-actions.d.ts.map +1 -1
  12. package/dist/components/app-open-actions.js +2 -2
  13. package/dist/components/app-open-actions.js.map +1 -1
  14. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  15. package/dist/components/dispatch-control-plane.js +16 -17
  16. package/dist/components/dispatch-control-plane.js.map +1 -1
  17. package/dist/components/layout/Layout.d.ts.map +1 -1
  18. package/dist/components/layout/Layout.js +137 -66
  19. package/dist/components/layout/Layout.js.map +1 -1
  20. package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
  21. package/dist/components/layout/workspace-apps-rail.js +1 -1
  22. package/dist/components/layout/workspace-apps-rail.js.map +1 -1
  23. package/dist/components/other-apps-section.d.ts +3 -1
  24. package/dist/components/other-apps-section.d.ts.map +1 -1
  25. package/dist/components/other-apps-section.js +9 -6
  26. package/dist/components/other-apps-section.js.map +1 -1
  27. package/dist/components/workspace-app-card.d.ts.map +1 -1
  28. package/dist/components/workspace-app-card.js +7 -2
  29. package/dist/components/workspace-app-card.js.map +1 -1
  30. package/dist/components/workspace-template-card.d.ts.map +1 -1
  31. package/dist/components/workspace-template-card.js +2 -5
  32. package/dist/components/workspace-template-card.js.map +1 -1
  33. package/dist/lib/overview-chat.d.ts.map +1 -1
  34. package/dist/lib/overview-chat.js +0 -8
  35. package/dist/lib/overview-chat.js.map +1 -1
  36. package/dist/routes/pages/chat.js +1 -1
  37. package/dist/routes/pages/chat.js.map +1 -1
  38. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  39. package/dist/server/lib/mcp-gateway.js +14 -1
  40. package/dist/server/lib/mcp-gateway.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/components/app-icon.tsx +71 -43
  43. package/src/components/app-list-row.tsx +1 -1
  44. package/src/components/app-open-actions.tsx +4 -2
  45. package/src/components/dispatch-control-plane.spec.tsx +45 -7
  46. package/src/components/dispatch-control-plane.tsx +56 -75
  47. package/src/components/layout/Layout.spec.tsx +53 -0
  48. package/src/components/layout/Layout.tsx +227 -99
  49. package/src/components/layout/workspace-apps-rail.tsx +1 -0
  50. package/src/components/other-apps-section.tsx +61 -40
  51. package/src/components/workspace-app-card.spec.tsx +63 -66
  52. package/src/components/workspace-app-card.tsx +10 -3
  53. package/src/components/workspace-template-card.spec.tsx +7 -8
  54. package/src/components/workspace-template-card.tsx +21 -11
  55. package/src/lib/overview-chat.spec.ts +0 -19
  56. package/src/lib/overview-chat.ts +0 -9
  57. package/src/routes/pages/chat.tsx +1 -1
  58. package/src/server/lib/mcp-gateway.spec.ts +64 -2
  59. package/src/server/lib/mcp-gateway.ts +19 -1
  60. package/src/styles/dispatch-css.spec.ts +11 -0
  61. package/src/styles/dispatch.css +2 -2
@@ -23,6 +23,7 @@ import {
23
23
  type ChatFirstAppRegistration,
24
24
  type ChatFirstAppLayoutPreference,
25
25
  type ChatFirstAppResolution,
26
+ type ChatFirstAppSurfacePlacement,
26
27
  type ChatFirstOpenBrowserDetail,
27
28
  type ChatFirstOpenAppDetail,
28
29
  type ChatFirstSessionReference,
@@ -199,6 +200,7 @@ const CHAT_FIRST_PANE_STATE_KEY = "chat-first-pane";
199
200
 
200
201
  interface DispatchChatFirstPane {
201
202
  appId: string;
203
+ placement?: ChatFirstAppSurfacePlacement;
202
204
  path?: string;
203
205
  view?: string;
204
206
  }
@@ -214,6 +216,16 @@ interface ChatFirstEmbedSessionInput {
214
216
  chrome: "minimal";
215
217
  }
216
218
 
219
+ interface ChatFirstGrantedAppSummary {
220
+ id: string;
221
+ name: string;
222
+ url?: string | null;
223
+ }
224
+
225
+ interface ChatFirstGrantedAppsResult {
226
+ apps: ChatFirstGrantedAppSummary[];
227
+ }
228
+
217
229
  export function buildChatFirstEmbedSessionInput(
218
230
  appId: string,
219
231
  path: string,
@@ -451,6 +463,9 @@ function persistedChatFirstPane(value: unknown): DispatchChatFirstPane | null {
451
463
  if (typeof record.appId !== "string" || !record.appId.trim()) return null;
452
464
  return {
453
465
  appId: record.appId,
466
+ ...(record.placement === "main" || record.placement === "side"
467
+ ? { placement: record.placement }
468
+ : {}),
454
469
  ...(typeof record.path === "string" ? { path: record.path } : {}),
455
470
  ...(typeof record.view === "string" ? { view: record.view } : {}),
456
471
  };
@@ -663,12 +678,12 @@ function DispatchChatsSection({
663
678
  return (
664
679
  <div
665
680
  className={cn(
666
- "ms-4 min-w-0 space-y-0.5",
681
+ "min-w-0 space-y-0.5",
667
682
  showNewChat && "dispatch-chat-first-chats",
668
683
  )}
669
684
  >
670
685
  {showNewChat && chatFirstNavigation ? (
671
- <nav className="space-y-0.5 px-2 py-2">
686
+ <nav className="space-y-0.5 px-0 py-2">
672
687
  <ChatFirstPrimaryNavigation
673
688
  copy={chatFirstCopy}
674
689
  onNewChat={() => void handleNewChat()}
@@ -730,7 +745,7 @@ function DispatchChatsSection({
730
745
  <Skeleton className="h-3 w-3/4 rounded" />
731
746
  </div>
732
747
  ))}
733
- {chatFirstMode ? (
748
+ {chatFirstMode && (chatsLoading || visibleThreads.length > 0) ? (
734
749
  <ChatFirstChatHistory
735
750
  items={chatItems}
736
751
  activeId={displayedActiveThreadId}
@@ -1015,7 +1030,7 @@ export function NavContent({
1015
1030
  id={app.id}
1016
1031
  name={app.name}
1017
1032
  size="sm"
1018
- className="size-5 rounded-md"
1033
+ className="size-7 rounded-lg"
1019
1034
  />
1020
1035
  )}
1021
1036
  copy={chatFirstCopy}
@@ -1246,7 +1261,7 @@ export function NavContent({
1246
1261
  <div className="mt-auto shrink-0">
1247
1262
  {bottomNavigation}
1248
1263
  {organizationPicker}
1249
- {collapsed ? sidebarFooterActions : null}
1264
+ {sidebarFooterActions}
1250
1265
  </div>
1251
1266
  ) : null}
1252
1267
  </>
@@ -1278,7 +1293,11 @@ export function Layout({
1278
1293
  const isChatRoute =
1279
1294
  localPathname === "/chat" || localPathname.startsWith("/chat/");
1280
1295
  const chatFirstSurfaceScope = threadIdFromPath(localPathname) ?? "new";
1281
- const isWorkspaceAppHostRoute = localPathname.startsWith("/apps/");
1296
+ const isWorkspaceAppRoute = localPathname.startsWith("/apps/");
1297
+ const isWorkspaceAppHostRoute =
1298
+ isWorkspaceAppRoute &&
1299
+ typeof window !== "undefined" &&
1300
+ new URLSearchParams(window.location.search).get("embedded") === "1";
1282
1301
  const [chatFirstPreference, setChatFirstPreference] = useState(() =>
1283
1302
  readChatFirstMode(),
1284
1303
  );
@@ -1287,36 +1306,56 @@ export function Layout({
1287
1306
  new URLSearchParams(window.location.search).get("chatFirst") === "1";
1288
1307
  const chatFirstMode =
1289
1308
  extensions?.chatFirst === true || chatFirstPreference || chatFirstEmbedded;
1309
+ const chatFirstHasActiveChat = chatFirstSurfaceScope !== "new";
1290
1310
  const [chatFirstAppLayout, setChatFirstAppLayout] =
1291
1311
  useState<ChatFirstAppLayoutPreference>(() => readChatFirstAppLayout());
1292
1312
  const chatFirstAppLayoutHydratedRef = useRef(false);
1293
1313
  const chatFirstAppsQuery = useActionQuery<WorkspaceAppSummary[]>(
1294
1314
  "list-workspace-apps",
1295
1315
  { includeAgentCards: false },
1296
- { enabled: chatFirstMode && isChatRoute },
1316
+ { enabled: chatFirstMode },
1317
+ );
1318
+ const chatFirstGrantedAppsQuery = useActionQuery<ChatFirstGrantedAppsResult>(
1319
+ "list_apps",
1320
+ {},
1321
+ { enabled: chatFirstMode },
1297
1322
  );
1298
1323
  const chatFirstWorkspaceApps = useMemo(
1299
1324
  () => mergeChatFirstWorkspaceApps(chatFirstAppsQuery.data),
1300
1325
  [chatFirstAppsQuery.data],
1301
1326
  );
1302
- const chatFirstAppRegistrations = useMemo<ChatFirstAppRegistration[]>(
1303
- () =>
1304
- chatFirstWorkspaceApps.map((app) => ({
1327
+ const chatFirstAppRegistrations = useMemo<ChatFirstAppRegistration[]>(() => {
1328
+ const registrations = new Map<string, ChatFirstAppRegistration>();
1329
+ for (const app of chatFirstWorkspaceApps) {
1330
+ registrations.set(app.id.toLowerCase(), {
1305
1331
  id: app.id,
1306
1332
  name: app.name,
1307
1333
  path: app.path,
1308
1334
  url: app.url,
1309
1335
  enabled: app.status !== "pending" && app.archived !== true,
1310
- })),
1311
- [chatFirstWorkspaceApps],
1312
- );
1336
+ });
1337
+ }
1338
+ for (const app of chatFirstGrantedAppsQuery.data?.apps ?? []) {
1339
+ const id = app.id.trim();
1340
+ if (!id || registrations.has(id.toLowerCase())) continue;
1341
+ registrations.set(id.toLowerCase(), {
1342
+ id,
1343
+ name: app.name,
1344
+ url: app.url,
1345
+ enabled: true,
1346
+ });
1347
+ }
1348
+ return [...registrations.values()];
1349
+ }, [chatFirstGrantedAppsQuery.data?.apps, chatFirstWorkspaceApps]);
1313
1350
  const chatFirstAppItems = useMemo<ChatFirstAppItem[]>(
1314
1351
  () =>
1315
- chatFirstWorkspaceApps.map((app) => ({
1316
- id: app.id,
1317
- name: app.name,
1318
- })),
1319
- [chatFirstWorkspaceApps],
1352
+ chatFirstAppRegistrations
1353
+ .filter((app) => app.enabled)
1354
+ .map((app) => ({
1355
+ id: app.id,
1356
+ name: app.name ?? app.id,
1357
+ })),
1358
+ [chatFirstAppRegistrations],
1320
1359
  );
1321
1360
  const chatFirstCopy = useMemo(() => createDispatchChatFirstCopy(t), [t]);
1322
1361
  const createChatFirstEmbedSession = useActionMutation<
@@ -1366,15 +1405,32 @@ export function Layout({
1366
1405
  ) ?? null,
1367
1406
  [chatFirstSurfaceTabs],
1368
1407
  );
1369
- const activeChatFirstApp = useMemo(
1408
+ const chatFirstAppTakesMain =
1409
+ chatFirstMode &&
1410
+ isChatRoute &&
1411
+ activeChatFirstSurfaceTab?.kind === "app" &&
1412
+ activeChatFirstSurfaceTab.placement === "main";
1413
+ const activeChatFirstAppRegistration = useMemo(
1370
1414
  () =>
1371
1415
  activeChatFirstSurfaceTab?.kind === "app" &&
1372
1416
  activeChatFirstSurfaceTab.appId
1373
- ? (chatFirstWorkspaceApps.find(
1417
+ ? (chatFirstAppRegistrations.find(
1374
1418
  (app) => app.id === activeChatFirstSurfaceTab.appId,
1375
1419
  ) ?? null)
1376
1420
  : null,
1377
- [activeChatFirstSurfaceTab, chatFirstWorkspaceApps],
1421
+ [activeChatFirstSurfaceTab, chatFirstAppRegistrations],
1422
+ );
1423
+ const activeChatFirstApp = useMemo(
1424
+ () =>
1425
+ activeChatFirstAppRegistration
1426
+ ? {
1427
+ id: activeChatFirstAppRegistration.id,
1428
+ name:
1429
+ activeChatFirstAppRegistration.name ??
1430
+ activeChatFirstAppRegistration.id,
1431
+ }
1432
+ : null,
1433
+ [activeChatFirstAppRegistration],
1378
1434
  );
1379
1435
  const chatFirstAgentActivities = useMemo<ChatFirstAgentActivity[]>(
1380
1436
  () =>
@@ -1392,7 +1448,8 @@ export function Layout({
1392
1448
  activeChatFirstSurfaceTab?.kind === "app"
1393
1449
  ? (activeChatFirstSurfaceTab.path ??
1394
1450
  chatFirstPane?.path ??
1395
- activeChatFirstApp?.path)
1451
+ activeChatFirstAppRegistration?.path ??
1452
+ "/")
1396
1453
  : null;
1397
1454
  useEffect(() => {
1398
1455
  if (!chatFirstMode || !isChatRoute || !activeChatFirstApp) {
@@ -1451,9 +1508,19 @@ export function Layout({
1451
1508
  [],
1452
1509
  );
1453
1510
  const openChatFirstPane = useCallback(
1454
- (pane: DispatchChatFirstPane) => {
1511
+ (
1512
+ pane: DispatchChatFirstPane,
1513
+ placement: ChatFirstAppSurfacePlacement = "side",
1514
+ ) => {
1515
+ if (placement === "side" && !chatFirstHasActiveChat) return;
1455
1516
  setChatFirstNotice(null);
1456
1517
  closeChatFirstSessionWatch();
1518
+ if (!isChatRoute) {
1519
+ navigateWithAgentChatViewTransition(
1520
+ navigate,
1521
+ dispatchNavLinkTarget("/chat"),
1522
+ );
1523
+ }
1457
1524
  const app = chatFirstAppRegistrations.find(
1458
1525
  (candidate) => candidate.id === pane.appId,
1459
1526
  );
@@ -1465,19 +1532,36 @@ export function Layout({
1465
1532
  kind: "app",
1466
1533
  title: app?.name ?? pane.appId,
1467
1534
  appId: pane.appId,
1535
+ placement,
1468
1536
  ...(pane.path ? { path: pane.path } : {}),
1469
1537
  ...(pane.view ? { view: pane.view } : {}),
1470
1538
  });
1471
- persistChatFirstPane(pane);
1539
+ setChatFirstSurfacePanelOpen(true);
1540
+ persistChatFirstPane({ ...pane, placement });
1472
1541
  },
1473
1542
  [
1474
1543
  chatFirstAppRegistrations,
1475
1544
  chatFirstSurfaceTabsStore,
1545
+ chatFirstHasActiveChat,
1476
1546
  persistChatFirstPane,
1547
+ isChatRoute,
1548
+ navigate,
1549
+ setChatFirstSurfacePanelOpen,
1477
1550
  ],
1478
1551
  );
1479
1552
  const openChatFirstSurface = useCallback(
1480
1553
  (kind: ChatFirstSurfaceKind) => {
1554
+ if (kind === "browser") {
1555
+ persistChatFirstPane(null);
1556
+ closeChatFirstSessionWatch();
1557
+ chatFirstSurfaceTabsStore.open({
1558
+ id: chatFirstSurfaceTabId("browser", "homepage"),
1559
+ kind: "browser",
1560
+ title: "Browser",
1561
+ url: "https://www.google.com/",
1562
+ });
1563
+ return;
1564
+ }
1481
1565
  if (kind !== "agents") return;
1482
1566
  persistChatFirstPane(null);
1483
1567
  closeChatFirstSessionWatch();
@@ -1498,6 +1582,24 @@ export function Layout({
1498
1582
  );
1499
1583
  return;
1500
1584
  }
1585
+ if (resolution.target.openExternally && typeof window !== "undefined") {
1586
+ try {
1587
+ // Builder's Visual Editor rejects iframe ancestors with CSP/X-Frame-
1588
+ // Options. Prefer the real browser tab; the browser pane below is a
1589
+ // visible fallback when popup policy blocks this non-click event.
1590
+ if (
1591
+ window.open(resolution.target.url, "_blank", "noopener,noreferrer")
1592
+ ) {
1593
+ return;
1594
+ }
1595
+ } catch (error) {
1596
+ console.warn(
1597
+ "[chat-first] external browser open failed; keeping link fallback",
1598
+ error,
1599
+ );
1600
+ // Fall through to the non-iframe browser pane below.
1601
+ }
1602
+ }
1501
1603
  persistChatFirstPane(null);
1502
1604
  closeChatFirstSessionWatch();
1503
1605
  setChatFirstNotice(null);
@@ -1512,7 +1614,7 @@ export function Layout({
1512
1614
  );
1513
1615
  const resolveChatFirstOpenApp = useCallback(
1514
1616
  (detail: ChatFirstOpenAppDetail) => {
1515
- if (chatFirstAppsQuery.isLoading) {
1617
+ if (chatFirstAppsQuery.isLoading || chatFirstGrantedAppsQuery.isLoading) {
1516
1618
  pendingChatFirstOpenAppRef.current = detail;
1517
1619
  return;
1518
1620
  }
@@ -1543,11 +1645,10 @@ export function Layout({
1543
1645
  chatFirstAppRegistrations,
1544
1646
  chatFirstAppsQuery.isError,
1545
1647
  chatFirstAppsQuery.isLoading,
1648
+ chatFirstGrantedAppsQuery.isLoading,
1546
1649
  openChatFirstPane,
1547
1650
  ],
1548
1651
  );
1549
- const sidebarBeforeAppRef = useRef<boolean | null>(null);
1550
- const sidebarAutoCollapsedRef = useRef(false);
1551
1652
  const chatHomeHandoffActive = useAgentChatHomeHandoff({
1552
1653
  storageKey: "dispatch",
1553
1654
  activePath: localPathname,
@@ -1618,7 +1719,7 @@ export function Layout({
1618
1719
  );
1619
1720
 
1620
1721
  useEffect(() => {
1621
- if (!chatFirstMode || !isChatRoute) {
1722
+ if (!chatFirstMode) {
1622
1723
  setChatFirstPane(null);
1623
1724
  closeChatFirstSessionWatch();
1624
1725
  chatFirstSurfaceTabsStore.closeAll();
@@ -1675,7 +1776,9 @@ export function Layout({
1675
1776
  : window.location.origin,
1676
1777
  },
1677
1778
  );
1678
- if (resolution.status === "ready") openChatFirstPane(resolution.target);
1779
+ if (resolution.status === "ready") {
1780
+ openChatFirstPane(resolution.target, persisted.placement);
1781
+ }
1679
1782
  })
1680
1783
  .catch(() => {
1681
1784
  if (active) {
@@ -1721,7 +1824,7 @@ export function Layout({
1721
1824
  useEffect(() => {
1722
1825
  const tabCount = chatFirstSurfaceTabs.tabs.length;
1723
1826
  const previousTabCount = previousChatFirstSurfaceTabCountRef.current;
1724
- if (!chatFirstMode || !isChatRoute) {
1827
+ if (!chatFirstMode || !chatFirstHasActiveChat) {
1725
1828
  setChatFirstSurfacePanelOpen(false);
1726
1829
  } else if (
1727
1830
  tabCount > 0 &&
@@ -1738,6 +1841,7 @@ export function Layout({
1738
1841
  previousChatFirstSurfaceTabCountRef.current = tabCount;
1739
1842
  }, [
1740
1843
  chatFirstMode,
1844
+ chatFirstHasActiveChat,
1741
1845
  setChatFirstSurfacePanelOpen,
1742
1846
  chatFirstSurfaceTabs.tabs.length,
1743
1847
  isChatRoute,
@@ -1756,12 +1860,19 @@ export function Layout({
1756
1860
 
1757
1861
  useEffect(() => {
1758
1862
  const pending = pendingChatFirstOpenAppRef.current;
1759
- if (!pending || chatFirstAppsQuery.isLoading) return;
1863
+ if (
1864
+ !pending ||
1865
+ chatFirstAppsQuery.isLoading ||
1866
+ chatFirstGrantedAppsQuery.isLoading
1867
+ )
1868
+ return;
1760
1869
  resolveChatFirstOpenApp(pending);
1761
1870
  }, [
1762
1871
  chatFirstAppsQuery.data,
1763
1872
  chatFirstAppsQuery.isError,
1764
1873
  chatFirstAppsQuery.isLoading,
1874
+ chatFirstGrantedAppsQuery.data,
1875
+ chatFirstGrantedAppsQuery.isLoading,
1765
1876
  resolveChatFirstOpenApp,
1766
1877
  ]);
1767
1878
 
@@ -1782,28 +1893,6 @@ export function Layout({
1782
1893
  isChatRoute,
1783
1894
  ]);
1784
1895
 
1785
- useEffect(() => {
1786
- if (isWorkspaceAppHostRoute) {
1787
- if (!sidebarAutoCollapsedRef.current) {
1788
- sidebarAutoCollapsedRef.current = true;
1789
- sidebarBeforeAppRef.current = sidebarCollapsed;
1790
- }
1791
- if (!sidebarCollapsed) setSidebarCollapsed(true);
1792
- return;
1793
- }
1794
-
1795
- if (!sidebarAutoCollapsedRef.current) return;
1796
- sidebarAutoCollapsedRef.current = false;
1797
- const previousSidebarState = sidebarBeforeAppRef.current;
1798
- sidebarBeforeAppRef.current = null;
1799
- if (
1800
- previousSidebarState !== null &&
1801
- previousSidebarState !== sidebarCollapsed
1802
- ) {
1803
- setSidebarCollapsed(previousSidebarState);
1804
- }
1805
- }, [isWorkspaceAppHostRoute, sidebarCollapsed]);
1806
-
1807
1896
  useEffect(() => {
1808
1897
  if (typeof window === "undefined" || isWorkspaceAppHostRoute) return;
1809
1898
  try {
@@ -1823,6 +1912,7 @@ export function Layout({
1823
1912
  closeChatFirstSessionWatch();
1824
1913
  persistChatFirstPane({
1825
1914
  appId: tab.appId,
1915
+ placement: tab.placement,
1826
1916
  ...(tab.path ? { path: tab.path } : {}),
1827
1917
  ...(tab.view ? { view: tab.view } : {}),
1828
1918
  });
@@ -1900,11 +1990,17 @@ export function Layout({
1900
1990
  (tab: ChatFirstSurfaceTab) => {
1901
1991
  if (tab.kind === "app") {
1902
1992
  if (tab.id !== chatFirstSurfaceTabs.activeTabId) return null;
1903
- const app = tab.appId
1904
- ? (chatFirstWorkspaceApps.find(
1993
+ const registration = tab.appId
1994
+ ? (chatFirstAppRegistrations.find(
1905
1995
  (candidate) => candidate.id === tab.appId,
1906
1996
  ) ?? null)
1907
1997
  : null;
1998
+ const app = registration
1999
+ ? {
2000
+ id: registration.id,
2001
+ name: registration.name ?? registration.id,
2002
+ }
2003
+ : null;
1908
2004
  return (
1909
2005
  <ChatFirstAppPane
1910
2006
  app={app}
@@ -2007,7 +2103,7 @@ export function Layout({
2007
2103
  chatFirstEmbedUrl,
2008
2104
  chatFirstSessionWatch.target,
2009
2105
  chatFirstSurfaceTabs.activeTabId,
2010
- chatFirstWorkspaceApps,
2106
+ chatFirstAppRegistrations,
2011
2107
  closeChatFirstSurfaceTab,
2012
2108
  renderChatFirstWatchChat,
2013
2109
  ],
@@ -2049,7 +2145,7 @@ export function Layout({
2049
2145
  <div className="relative flex h-full min-w-0 flex-1 flex-col overflow-hidden">
2050
2146
  {showHeader ? <Header onOpenMobile={() => setMobileOpen(true)} /> : null}
2051
2147
  <InvitationBanner />
2052
- {isChatRoute && chatFirstMode ? (
2148
+ {isChatRoute && chatFirstMode && chatFirstHasActiveChat ? (
2053
2149
  <ChatFirstSurfacePanelToggle
2054
2150
  open={chatFirstSurfacePanel.open}
2055
2151
  onToggle={chatFirstSurfacePanel.toggle}
@@ -2075,7 +2171,7 @@ export function Layout({
2075
2171
  <main
2076
2172
  className={cn(
2077
2173
  "flex-1",
2078
- isChatRoute || isWorkspaceAppHostRoute
2174
+ isChatRoute || isWorkspaceAppRoute
2079
2175
  ? "min-h-0 overflow-hidden"
2080
2176
  : "overflow-y-auto",
2081
2177
  )}
@@ -2090,6 +2186,50 @@ export function Layout({
2090
2186
  </main>
2091
2187
  </div>
2092
2188
  );
2189
+ const chatFirstSurfaceTabsBar =
2190
+ activeChatFirstSurfaceTab?.kind === "app" ? null : (
2191
+ <ChatFirstSurfaceTabs
2192
+ tabs={chatFirstSurfaceTabs.tabs}
2193
+ activeTabId={chatFirstSurfaceTabs.activeTabId}
2194
+ onActivate={activateChatFirstSurfaceTab}
2195
+ onClose={closeChatFirstSurfaceTab}
2196
+ onCloseOthers={(tab) => {
2197
+ activateChatFirstSurfaceTab(tab);
2198
+ chatFirstSurfaceTabsStore.closeOthers(tab.id);
2199
+ }}
2200
+ onCloseToRight={(tab) => {
2201
+ const targetIndex = chatFirstSurfaceTabs.tabs.findIndex(
2202
+ (candidate) => candidate.id === tab.id,
2203
+ );
2204
+ const activeIndex = chatFirstSurfaceTabs.tabs.findIndex(
2205
+ (candidate) => candidate.id === chatFirstSurfaceTabs.activeTabId,
2206
+ );
2207
+ if (activeIndex > targetIndex) activateChatFirstSurfaceTab(tab);
2208
+ chatFirstSurfaceTabsStore.closeToRight(tab.id);
2209
+ }}
2210
+ onCloseAll={closeAllChatFirstSurfaceTabs}
2211
+ onOpenSurface={openChatFirstSurface}
2212
+ apps={chatFirstAppItems}
2213
+ onOpenApp={(app) => openChatFirstPane({ appId: app.id }, "side")}
2214
+ renderAppIcon={(app) => (
2215
+ <AppIcon
2216
+ id={app.id}
2217
+ name={app.name}
2218
+ size="sm"
2219
+ className="size-7 rounded-lg"
2220
+ />
2221
+ )}
2222
+ copy={chatFirstCopy}
2223
+ />
2224
+ );
2225
+ const chatFirstSurfaceContent =
2226
+ chatFirstSurfaceTabs.tabs.length > 0 ? (
2227
+ <ChatFirstSurfaceContent
2228
+ tabs={chatFirstSurfaceTabs.tabs}
2229
+ activeTabId={chatFirstSurfaceTabs.activeTabId}
2230
+ renderTab={renderChatFirstSurfaceTab}
2231
+ />
2232
+ ) : null;
2093
2233
  const content = isChatRoute ? (
2094
2234
  <div
2095
2235
  className={cn(
@@ -2097,47 +2237,33 @@ export function Layout({
2097
2237
  chatFirstMode && "dispatch-chat-first-surface",
2098
2238
  )}
2099
2239
  >
2100
- {appContent}
2101
- {chatFirstMode && chatFirstSurfacePanel.open ? (
2102
- <ChatFirstSurfacePanel
2103
- width={chatFirstSurfaceResize.width}
2104
- onResizePointerDown={chatFirstSurfaceResize.onPointerDown}
2105
- copy={chatFirstCopy}
2240
+ {chatFirstAppTakesMain ? (
2241
+ <div
2242
+ className="flex min-w-0 flex-1 flex-col overflow-hidden"
2243
+ data-dispatch-chat-first-main-app
2106
2244
  >
2107
- <ChatFirstSurfaceTabs
2108
- tabs={chatFirstSurfaceTabs.tabs}
2109
- activeTabId={chatFirstSurfaceTabs.activeTabId}
2110
- onActivate={activateChatFirstSurfaceTab}
2111
- onClose={closeChatFirstSurfaceTab}
2112
- onCloseOthers={(tab) => {
2113
- activateChatFirstSurfaceTab(tab);
2114
- chatFirstSurfaceTabsStore.closeOthers(tab.id);
2115
- }}
2116
- onCloseToRight={(tab) => {
2117
- const targetIndex = chatFirstSurfaceTabs.tabs.findIndex(
2118
- (candidate) => candidate.id === tab.id,
2119
- );
2120
- const activeIndex = chatFirstSurfaceTabs.tabs.findIndex(
2121
- (candidate) =>
2122
- candidate.id === chatFirstSurfaceTabs.activeTabId,
2123
- );
2124
- if (activeIndex > targetIndex) activateChatFirstSurfaceTab(tab);
2125
- chatFirstSurfaceTabsStore.closeToRight(tab.id);
2126
- }}
2127
- onCloseAll={closeAllChatFirstSurfaceTabs}
2128
- onOpenSurface={openChatFirstSurface}
2129
- copy={chatFirstCopy}
2130
- />
2131
- {chatFirstSurfaceTabs.tabs.length > 0 ? (
2132
- <ChatFirstSurfaceContent
2133
- tabs={chatFirstSurfaceTabs.tabs}
2134
- activeTabId={chatFirstSurfaceTabs.activeTabId}
2135
- renderTab={renderChatFirstSurfaceTab}
2136
- />
2245
+ {chatFirstSurfaceContent}
2246
+ </div>
2247
+ ) : (
2248
+ <>
2249
+ {appContent}
2250
+ {chatFirstMode &&
2251
+ chatFirstHasActiveChat &&
2252
+ chatFirstSurfacePanel.open ? (
2253
+ <ChatFirstSurfacePanel
2254
+ width={chatFirstSurfaceResize.width}
2255
+ onResizePointerDown={chatFirstSurfaceResize.onPointerDown}
2256
+ copy={chatFirstCopy}
2257
+ >
2258
+ {chatFirstSurfaceTabsBar}
2259
+ {chatFirstSurfaceContent}
2260
+ </ChatFirstSurfacePanel>
2137
2261
  ) : null}
2138
- </ChatFirstSurfacePanel>
2139
- ) : null}
2262
+ </>
2263
+ )}
2140
2264
  </div>
2265
+ ) : isWorkspaceAppRoute ? (
2266
+ appContent
2141
2267
  ) : (
2142
2268
  <AgentSidebar
2143
2269
  position="right"
@@ -2184,7 +2310,9 @@ export function Layout({
2184
2310
  ? activeChatFirstSurfaceTab.appId
2185
2311
  : undefined
2186
2312
  }
2187
- onChatFirstAppOpen={(app) => openChatFirstPane({ appId: app.id })}
2313
+ onChatFirstAppOpen={(app) =>
2314
+ openChatFirstPane({ appId: app.id }, "main")
2315
+ }
2188
2316
  onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
2189
2317
  collapsible
2190
2318
  onCollapsedChange={setSidebarCollapsed}
@@ -2194,7 +2322,7 @@ export function Layout({
2194
2322
  <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
2195
2323
  <SheetContent
2196
2324
  side="left"
2197
- className="w-72 p-0 bg-sidebar text-sidebar-foreground [&>button]:hidden"
2325
+ className="w-72 bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
2198
2326
  >
2199
2327
  <SheetTitle className="sr-only">
2200
2328
  {t("dispatch.nav.navigation")}
@@ -2223,7 +2351,7 @@ export function Layout({
2223
2351
  : undefined
2224
2352
  }
2225
2353
  onChatFirstAppOpen={(app) =>
2226
- openChatFirstPane({ appId: app.id })
2354
+ openChatFirstPane({ appId: app.id }, "main")
2227
2355
  }
2228
2356
  onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
2229
2357
  onNavigate={() => setMobileOpen(false)}
@@ -97,6 +97,7 @@ export function WorkspaceAppsRail({
97
97
  id={app.id}
98
98
  name={label}
99
99
  size="sm"
100
+ monochrome
100
101
  className={cn("size-5 rounded-md", active && "ring-1 ring-ring/30")}
101
102
  />
102
103
  {!collapsed ? <span className="truncate">{label}</span> : null}