@agent-native/dispatch 0.20.4 → 0.22.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 (87) hide show
  1. package/dist/actions/get-agent-thread-debug.d.ts +2 -0
  2. package/dist/actions/provider-api-register.d.ts +7 -7
  3. package/dist/actions/search-agent-threads.js +2 -2
  4. package/dist/actions/search-agent-threads.js.map +1 -1
  5. package/dist/actions/start-workspace-app-creation.js +1 -1
  6. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  7. package/dist/components/create-app-popover.d.ts.map +1 -1
  8. package/dist/components/create-app-popover.js +5 -71
  9. package/dist/components/create-app-popover.js.map +1 -1
  10. package/dist/components/layout/HeaderActions.d.ts +1 -17
  11. package/dist/components/layout/HeaderActions.d.ts.map +1 -1
  12. package/dist/components/layout/HeaderActions.js +1 -51
  13. package/dist/components/layout/HeaderActions.js.map +1 -1
  14. package/dist/components/layout/Layout.d.ts +23 -1
  15. package/dist/components/layout/Layout.d.ts.map +1 -1
  16. package/dist/components/layout/Layout.js +707 -18
  17. package/dist/components/layout/Layout.js.map +1 -1
  18. package/dist/components/ui/dialog.d.ts +1 -19
  19. package/dist/components/ui/dialog.d.ts.map +1 -1
  20. package/dist/components/ui/dialog.js +1 -22
  21. package/dist/components/ui/dialog.js.map +1 -1
  22. package/dist/components/ui/dropdown-menu.d.ts +1 -27
  23. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  24. package/dist/components/ui/dropdown-menu.js +1 -35
  25. package/dist/components/ui/dropdown-menu.js.map +1 -1
  26. package/dist/components/ui/popover.d.ts +2 -1
  27. package/dist/components/ui/popover.d.ts.map +1 -1
  28. package/dist/components/ui/popover.js +2 -1
  29. package/dist/components/ui/popover.js.map +1 -1
  30. package/dist/components/ui/select.d.ts +1 -13
  31. package/dist/components/ui/select.d.ts.map +1 -1
  32. package/dist/components/ui/select.js +1 -26
  33. package/dist/components/ui/select.js.map +1 -1
  34. package/dist/hooks/use-mobile.d.ts +1 -1
  35. package/dist/hooks/use-mobile.d.ts.map +1 -1
  36. package/dist/hooks/use-mobile.js +1 -15
  37. package/dist/hooks/use-mobile.js.map +1 -1
  38. package/dist/lib/automation-display.js +1 -1
  39. package/dist/lib/automation-display.js.map +1 -1
  40. package/dist/lib/workspace-apps.d.ts +6 -0
  41. package/dist/lib/workspace-apps.d.ts.map +1 -1
  42. package/dist/lib/workspace-apps.js +21 -0
  43. package/dist/lib/workspace-apps.js.map +1 -1
  44. package/dist/routes/pages/settings.d.ts.map +1 -1
  45. package/dist/routes/pages/settings.js +21 -1
  46. package/dist/routes/pages/settings.js.map +1 -1
  47. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  48. package/dist/routes/pages/thread-debug.js +288 -118
  49. package/dist/routes/pages/thread-debug.js.map +1 -1
  50. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  51. package/dist/server/lib/app-creation-store.js +13 -0
  52. package/dist/server/lib/app-creation-store.js.map +1 -1
  53. package/dist/server/lib/thread-debug-store.d.ts +2 -0
  54. package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
  55. package/dist/server/lib/thread-debug-store.js +6 -2
  56. package/dist/server/lib/thread-debug-store.js.map +1 -1
  57. package/dist/server/plugins/agent-chat.js +1 -0
  58. package/dist/server/plugins/agent-chat.js.map +1 -1
  59. package/dist/server/plugins/integrations.js +1 -1
  60. package/dist/server/plugins/integrations.js.map +1 -1
  61. package/package.json +3 -3
  62. package/src/actions/search-agent-threads.ts +2 -2
  63. package/src/actions/start-workspace-app-creation.ts +1 -1
  64. package/src/components/create-app-popover.spec.tsx +22 -0
  65. package/src/components/create-app-popover.tsx +30 -94
  66. package/src/components/layout/HeaderActions.tsx +1 -84
  67. package/src/components/layout/Layout.spec.tsx +35 -10
  68. package/src/components/layout/Layout.tsx +1394 -74
  69. package/src/components/ui/dialog.tsx +1 -120
  70. package/src/components/ui/dropdown-menu.tsx +1 -198
  71. package/src/components/ui/popover.tsx +3 -1
  72. package/src/components/ui/select.tsx +1 -158
  73. package/src/hooks/use-mobile.tsx +1 -21
  74. package/src/lib/automation-display.spec.ts +1 -1
  75. package/src/lib/automation-display.ts +1 -1
  76. package/src/lib/workspace-apps.ts +24 -0
  77. package/src/routes/pages/settings.tsx +72 -0
  78. package/src/routes/pages/thread-debug.spec.tsx +103 -5
  79. package/src/routes/pages/thread-debug.tsx +1051 -451
  80. package/src/server/lib/app-creation-store.spec.ts +13 -0
  81. package/src/server/lib/app-creation-store.ts +13 -0
  82. package/src/server/lib/mcp-gateway.spec.ts +18 -0
  83. package/src/server/lib/thread-debug-store.spec.ts +17 -0
  84. package/src/server/lib/thread-debug-store.ts +9 -2
  85. package/src/server/plugins/agent-chat.ts +1 -0
  86. package/src/server/plugins/integrations.ts +1 -1
  87. package/src/styles/dispatch.css +29 -0
@@ -1,13 +1,65 @@
1
1
  import {
2
+ CHAT_FIRST_MODE_CHANGED_EVENT,
3
+ AgentChatSurface,
4
+ chatFirstSurfaceTabId,
2
5
  AgentSidebar,
6
+ ChatFirstSurfacePanelToggle,
7
+ closeChatFirstSessionWatch,
8
+ emitChatFirstSessionWatch,
9
+ getChatFirstSurfaceTabsStore,
3
10
  focusAgentChat,
4
11
  navigateWithAgentChatViewTransition,
12
+ readChatFirstAppLayout,
13
+ readChatFirstMode,
14
+ resolveChatFirstBrowserTarget,
15
+ resolveChatFirstAppTarget,
16
+ subscribeChatFirstOpenBrowser,
17
+ subscribeChatFirstOpenApp,
18
+ writeChatFirstAppLayout,
19
+ useChatFirstSessionWatch,
20
+ useChatFirstSurfacePanel,
21
+ useChatFirstSurfaceResize,
22
+ useChatFirstSurfaceTabs,
23
+ type ChatFirstAppRegistration,
24
+ type ChatFirstAppLayoutPreference,
25
+ type ChatFirstAppResolution,
26
+ type ChatFirstOpenBrowserDetail,
27
+ type ChatFirstOpenAppDetail,
28
+ type ChatFirstSessionReference,
29
+ type ChatFirstSurfaceTab,
30
+ type ChatFirstSurfaceKind,
5
31
  useAgentChatHomeHandoff,
6
32
  useAgentChatHomeHandoffLinks,
7
33
  useChatThreads,
8
34
  type ChatThreadSummary,
9
35
  } from "@agent-native/core/client/agent-chat";
10
36
  import { appBasePath, appPath } from "@agent-native/core/client/api-path";
37
+ import {
38
+ readClientAppState,
39
+ writeClientAppState,
40
+ } from "@agent-native/core/client/application-state";
41
+ import {
42
+ ChatFirstAgentsPane,
43
+ ChatFirstAppPane,
44
+ ChatFirstAppsRail,
45
+ ChatFirstBrowserPane,
46
+ ChatFirstChatHistory,
47
+ ChatFirstPrimaryNavigation,
48
+ ChatFirstSessionWatchPane,
49
+ ChatFirstSurfacePanel,
50
+ ChatFirstSurfaceContent,
51
+ ChatFirstSurfaceTabs,
52
+ defaultChatFirstCopy,
53
+ type ChatFirstAgentActivity,
54
+ type ChatFirstAppItem,
55
+ type ChatFirstCopy,
56
+ type ChatFirstEmbedTarget,
57
+ } from "@agent-native/core/client/chat-first";
58
+ import { writeClipboardText } from "@agent-native/core/client/clipboard";
59
+ import {
60
+ useActionMutation,
61
+ useActionQuery,
62
+ } from "@agent-native/core/client/hooks";
11
63
  import { useT } from "@agent-native/core/client/i18n";
12
64
  import { openCommandMenu } from "@agent-native/core/client/navigation";
13
65
  import { InvitationBanner, OrgSwitcher } from "@agent-native/core/client/org";
@@ -21,6 +73,8 @@ import {
21
73
  IconApps,
22
74
  IconBrandSlack,
23
75
  IconBrandTelegram,
76
+ IconCopy,
77
+ IconEye,
24
78
  IconMessageQuestion,
25
79
  IconBroadcast,
26
80
  IconLayoutSidebarLeftCollapse,
@@ -30,6 +84,7 @@ import {
30
84
  IconSearch,
31
85
  IconWorld,
32
86
  IconDeviceDesktop,
87
+ IconPlus,
33
88
  } from "@tabler/icons-react";
34
89
  import {
35
90
  createContext,
@@ -38,12 +93,20 @@ import {
38
93
  useRef,
39
94
  useState,
40
95
  useContext,
96
+ useCallback,
41
97
  type ComponentType,
42
98
  type ReactNode,
43
99
  } from "react";
44
100
  import { Link, useLocation, useNavigate } from "react-router";
101
+ import { toast } from "sonner";
45
102
 
46
103
  import { cn } from "../../lib/utils";
104
+ import {
105
+ mergeChatFirstWorkspaceApps,
106
+ type WorkspaceAppSummary,
107
+ } from "../../lib/workspace-apps";
108
+ import { AppIcon } from "../app-icon";
109
+ import { CreateAppPopover } from "../create-app-popover";
47
110
  import {
48
111
  DropdownMenu,
49
112
  DropdownMenuContent,
@@ -55,7 +118,6 @@ import { Skeleton } from "../ui/skeleton";
55
118
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
56
119
  import { Header } from "./Header";
57
120
  import { HeaderActionsProvider } from "./HeaderActions";
58
- import { WorkspaceAppsRail } from "./workspace-apps-rail";
59
121
 
60
122
  export type DispatchNavSection = "primary" | "operations";
61
123
 
@@ -80,6 +142,8 @@ export interface DispatchNavItem {
80
142
  }
81
143
 
82
144
  export interface DispatchExtensionConfig {
145
+ /** Opt into the Codex/T3-like chat-first shell for chat routes. */
146
+ chatFirst?: boolean;
83
147
  /** Extra sidebar tabs supplied by the generated workspace. */
84
148
  navItems?: readonly DispatchNavItem[];
85
149
  /** Extra React Query keys to invalidate when Dispatch receives DB sync events. */
@@ -131,6 +195,43 @@ const DISPATCH_SIDEBAR_LABEL = "Dispatch";
131
195
  const CHROMELESS_PATHS = ["/approval", "/browser-chat", "/browser-connect"];
132
196
  const SIDEBAR_COLLAPSE_KEY = "dispatch.sidebar.collapsed";
133
197
  const CHAT_HISTORY_SOURCE_KEY = "dispatch.chat-history.source";
198
+ const CHAT_FIRST_PANE_STATE_KEY = "chat-first-pane";
199
+
200
+ interface DispatchChatFirstPane {
201
+ appId: string;
202
+ path?: string;
203
+ view?: string;
204
+ }
205
+
206
+ interface ChatFirstEmbedSessionResult {
207
+ startUrl: string;
208
+ }
209
+
210
+ interface ChatFirstEmbedSessionInput {
211
+ app?: string;
212
+ path?: string;
213
+ url?: string;
214
+ chrome: "minimal";
215
+ }
216
+
217
+ export function buildChatFirstEmbedSessionInput(
218
+ appId: string,
219
+ path: string,
220
+ ): ChatFirstEmbedSessionInput {
221
+ return { app: appId, path, chrome: "minimal" };
222
+ }
223
+
224
+ interface DispatchAgentThreadSummary {
225
+ id: string;
226
+ title: string;
227
+ preview?: string;
228
+ snippet?: string;
229
+ updatedAt?: number;
230
+ }
231
+
232
+ interface SearchAgentThreadsResult {
233
+ threads: DispatchAgentThreadSummary[];
234
+ }
134
235
 
135
236
  const DispatchExtensionsContext = createContext<
136
237
  DispatchExtensionConfig | undefined
@@ -199,8 +300,160 @@ function dispatchNavLinkTarget(path: string): string {
199
300
  return routerHasBasename ? path : appPath(path);
200
301
  }
201
302
 
202
- function chatThreadPath(threadId: string): string {
203
- return `/chat/${encodeURIComponent(threadId)}`;
303
+ function chatFirstResolutionMessage(
304
+ reason: Exclude<ChatFirstAppResolution, { status: "ready" }>["reason"],
305
+ ): string {
306
+ switch (reason) {
307
+ case "empty-detail":
308
+ return "The agent did not provide an app target to open.";
309
+ case "invalid-url":
310
+ return "The requested app route was not registered for this workspace app.";
311
+ case "unknown-app":
312
+ return "That app is not available in this workspace.";
313
+ }
314
+ return "The requested app could not be opened.";
315
+ }
316
+
317
+ function chatFirstBrowserResolutionMessage(
318
+ reason: "empty-detail" | "invalid-url",
319
+ ): string {
320
+ return reason === "empty-detail"
321
+ ? "The agent did not provide a browser URL to open."
322
+ : "The requested browser URL is not a safe HTTP(S) address.";
323
+ }
324
+
325
+ const DISPATCH_CHAT_FIRST_COPY_KEYS: Record<string, string> = {
326
+ workspaceApps: "chatFirstWorkspaceApps",
327
+ createWorkspaceApp: "chatFirstCreateWorkspaceApp",
328
+ openApp: "chatFirstOpenApp",
329
+ appsLoadError: "chatFirstAppsLoadError",
330
+ noWorkspaceApps: "chatFirstNoWorkspaceApps",
331
+ createApp: "chatFirstCreateApp",
332
+ retry: "chatFirstRetry",
333
+ dismiss: "chatFirstDismiss",
334
+ unpinApp: "chatFirstUnpinApp",
335
+ pinApp: "chatFirstPinApp",
336
+ removePinned: "chatFirstRemovePinned",
337
+ pinTop: "chatFirstPinTop",
338
+ openSideSurfaces: "chatFirstOpenSideSurfaces",
339
+ closeTab: "chatFirstCloseTab",
340
+ close: "chatFirstClose",
341
+ closeOthers: "chatFirstCloseOthers",
342
+ closeToRight: "chatFirstCloseToRight",
343
+ closeAll: "chatFirstCloseAll",
344
+ unavailable: "chatFirstUnavailable",
345
+ openActivity: "chatFirstOpenActivity",
346
+ deferred: "chatFirstDeferred",
347
+ browserBack: "chatFirstBrowserBack",
348
+ browserForward: "chatFirstBrowserForward",
349
+ browserReload: "chatFirstBrowserReload",
350
+ browserAddress: "chatFirstBrowserAddress",
351
+ browserOpenExternal: "chatFirstBrowserOpenExternal",
352
+ browserClose: "chatFirstBrowserClose",
353
+ browserPage: "chatFirstBrowserPage",
354
+ browserInvalidUrl: "chatFirstBrowserInvalidUrl",
355
+ browserPreviewStarting: "chatFirstBrowserPreviewStarting",
356
+ browserPreviewError: "chatFirstBrowserPreviewError",
357
+ appUnavailable: "chatFirstAppUnavailable",
358
+ appLoading: "chatFirstLoadingApp",
359
+ agentActivityEyebrow: "chatFirstAgentActivityEyebrow",
360
+ agentActivityTitle: "chatFirstAgentActivityTitle",
361
+ agentActivityDescription: "chatFirstAgentActivityDescription",
362
+ refreshAgentActivity: "chatFirstRefreshAgentActivity",
363
+ noAgentSessions: "chatFirstNoAgentSessions",
364
+ startAgentSession: "chatFirstStartAgentSession",
365
+ watchSession: "chatFirstWatchSession",
366
+ copySessionId: "chatFirstCopySessionId",
367
+ copySessionIdFor: "chatFirstCopySessionIdFor",
368
+ sessionIdCopied: "chatFirstSessionIdCopied",
369
+ sessionIdShort: "chatFirstSessionIdShort",
370
+ copied: "chatFirstCopied",
371
+ agentActivityStatusQueued: "chatFirstAgentActivityStatusQueued",
372
+ agentActivityStatusRunning: "chatFirstAgentActivityStatusRunning",
373
+ agentActivityStatusPaused: "chatFirstAgentActivityStatusPaused",
374
+ agentActivityStatusNeedsApproval: "chatFirstAgentActivityStatusNeedsApproval",
375
+ agentActivityStatusCompleted: "chatFirstAgentActivityStatusCompleted",
376
+ agentActivityStatusErrored: "chatFirstAgentActivityStatusErrored",
377
+ agentActivityStatusRecent: "chatFirstAgentActivityStatusRecent",
378
+ agentActivityStatusUnknown: "chatFirstAgentActivityStatusUnknown",
379
+ watchingSession: "chatFirstWatchingSession",
380
+ session: "chatFirstSession",
381
+ stopWatchingSession: "chatFirstStopWatchingSession",
382
+ stopWatching: "chatFirstStopWatching",
383
+ watchedSession: "chatFirstWatchedSession",
384
+ agentsTitle: "chatFirstAgentsTitle",
385
+ };
386
+
387
+ const DISPATCH_CHAT_FIRST_SURFACE_COPY_KEYS: Record<
388
+ string,
389
+ { label: string; reason: string }
390
+ > = {
391
+ browser: {
392
+ label: "chatFirstSurfaceBrowserLabel",
393
+ reason: "chatFirstSurfaceBrowserReason",
394
+ },
395
+ terminal: {
396
+ label: "chatFirstSurfaceTerminalLabel",
397
+ reason: "chatFirstSurfaceTerminalReason",
398
+ },
399
+ files: {
400
+ label: "chatFirstSurfaceFilesLabel",
401
+ reason: "chatFirstSurfaceFilesReason",
402
+ },
403
+ diff: {
404
+ label: "chatFirstSurfaceDiffLabel",
405
+ reason: "chatFirstSurfaceDiffReason",
406
+ },
407
+ "side-chat": {
408
+ label: "chatFirstSurfaceSideChatLabel",
409
+ reason: "chatFirstSurfaceSideChatReason",
410
+ },
411
+ agents: {
412
+ label: "chatFirstSurfaceAgentsLabel",
413
+ reason: "chatFirstSurfaceAgentsReason",
414
+ },
415
+ };
416
+
417
+ function createDispatchChatFirstCopy(
418
+ t: (key: string, options?: Record<string, unknown>) => string,
419
+ ): ChatFirstCopy {
420
+ return (key, values) => {
421
+ if (key.startsWith("surface.")) {
422
+ const [, kind, field] = key.split(".");
423
+ const surface = DISPATCH_CHAT_FIRST_SURFACE_COPY_KEYS[kind];
424
+ const translationKey =
425
+ surface && (field === "label" || field === "reason")
426
+ ? surface[field]
427
+ : undefined;
428
+ if (translationKey) {
429
+ return t(`dispatch.pages.${translationKey}`, {
430
+ ...(values ?? {}),
431
+ defaultValue: defaultChatFirstCopy(key, values),
432
+ });
433
+ }
434
+ }
435
+ const translationKey = DISPATCH_CHAT_FIRST_COPY_KEYS[key];
436
+ if (!translationKey) return defaultChatFirstCopy(key, values);
437
+ return t(`dispatch.pages.${translationKey}`, {
438
+ ...(values ?? {}),
439
+ defaultValue: defaultChatFirstCopy(key, values),
440
+ });
441
+ };
442
+ }
443
+
444
+ function chatThreadPath(threadId: string | null): string {
445
+ return threadId ? `/chat/${encodeURIComponent(threadId)}` : "/chat";
446
+ }
447
+
448
+ function persistedChatFirstPane(value: unknown): DispatchChatFirstPane | null {
449
+ if (!value || typeof value !== "object") return null;
450
+ const record = value as Record<string, unknown>;
451
+ if (typeof record.appId !== "string" || !record.appId.trim()) return null;
452
+ return {
453
+ appId: record.appId,
454
+ ...(typeof record.path === "string" ? { path: record.path } : {}),
455
+ ...(typeof record.view === "string" ? { view: record.view } : {}),
456
+ };
204
457
  }
205
458
 
206
459
  function threadIdFromPath(pathname: string): string | null {
@@ -261,8 +514,26 @@ function threadUpdatedAt(thread: ChatThreadSummary) {
261
514
  : 0;
262
515
  }
263
516
 
264
- function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
517
+ function DispatchChatsSection({
518
+ onNavigate,
519
+ showNewChat = false,
520
+ prelude,
521
+ chatFirstMode = false,
522
+ chatFirstEmbedded = false,
523
+ chatFirstNavigation,
524
+ }: {
525
+ onNavigate?: () => void;
526
+ showNewChat?: boolean;
527
+ prelude?: ReactNode;
528
+ chatFirstMode?: boolean;
529
+ chatFirstEmbedded?: boolean;
530
+ chatFirstNavigation?: {
531
+ onOpenIntegrations: () => void;
532
+ onOpenScheduled: () => void;
533
+ };
534
+ }) {
265
535
  const t = useT();
536
+ const chatFirstCopy = useMemo(() => createDispatchChatFirstCopy(t), [t]);
266
537
  const navigate = useNavigate();
267
538
  const location = useLocation();
268
539
  const [includeExternal, setIncludeExternal] = useState(false);
@@ -390,45 +661,65 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
390
661
  }
391
662
 
392
663
  return (
393
- <div className="ms-4 min-w-0 space-y-0.5">
394
- <div className="flex justify-end px-2 pt-0.5">
395
- <Tooltip>
396
- <TooltipTrigger asChild>
397
- <button
398
- type="button"
399
- data-dispatch-chat-source-toggle
400
- aria-pressed={includeExternal}
401
- aria-label={
402
- includeExternal
403
- ? t("dispatch.sidebar.showLocalChats", {
404
- defaultValue: "Show local chats",
405
- })
406
- : t("dispatch.sidebar.showAllChats", {
407
- defaultValue: "Show all chats",
408
- })
409
- }
410
- onClick={() => setIncludeExternal((current) => !current)}
411
- className="flex size-6 items-center justify-center rounded-md text-sidebar-foreground/55 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
412
- >
413
- {includeExternal ? (
414
- <IconWorld size={14} aria-hidden="true" />
415
- ) : (
416
- <IconDeviceDesktop size={14} aria-hidden="true" />
417
- )}
418
- </button>
419
- </TooltipTrigger>
420
- <TooltipContent side="right">
421
- {includeExternal
422
- ? t("dispatch.sidebar.showLocalChats", {
423
- defaultValue: "Show local chats",
424
- })
425
- : t("dispatch.sidebar.showAllChats", {
426
- defaultValue: "Show all chats",
427
- })}
428
- </TooltipContent>
429
- </Tooltip>
430
- </div>
431
- {chatsLoading &&
664
+ <div
665
+ className={cn(
666
+ "ms-4 min-w-0 space-y-0.5",
667
+ showNewChat && "dispatch-chat-first-chats",
668
+ )}
669
+ >
670
+ {showNewChat && chatFirstNavigation ? (
671
+ <nav className="space-y-0.5 px-2 py-2">
672
+ <ChatFirstPrimaryNavigation
673
+ copy={chatFirstCopy}
674
+ onNewChat={() => void handleNewChat()}
675
+ onOpenIntegrations={chatFirstNavigation.onOpenIntegrations}
676
+ onOpenScheduled={chatFirstNavigation.onOpenScheduled}
677
+ onSearch={openCommandMenu}
678
+ />
679
+ </nav>
680
+ ) : null}
681
+ {prelude}
682
+ {!chatFirstMode ? (
683
+ <div className="flex justify-end px-2 pt-0.5">
684
+ <Tooltip>
685
+ <TooltipTrigger asChild>
686
+ <button
687
+ type="button"
688
+ data-dispatch-chat-source-toggle
689
+ aria-pressed={includeExternal}
690
+ aria-label={
691
+ includeExternal
692
+ ? t("dispatch.sidebar.showLocalChats", {
693
+ defaultValue: "Show local chats",
694
+ })
695
+ : t("dispatch.sidebar.showAllChats", {
696
+ defaultValue: "Show all chats",
697
+ })
698
+ }
699
+ onClick={() => setIncludeExternal((current) => !current)}
700
+ className="flex size-6 items-center justify-center rounded-md text-sidebar-foreground/55 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
701
+ >
702
+ {includeExternal ? (
703
+ <IconWorld size={14} aria-hidden="true" />
704
+ ) : (
705
+ <IconDeviceDesktop size={14} aria-hidden="true" />
706
+ )}
707
+ </button>
708
+ </TooltipTrigger>
709
+ <TooltipContent side="right">
710
+ {includeExternal
711
+ ? t("dispatch.sidebar.showLocalChats", {
712
+ defaultValue: "Show local chats",
713
+ })
714
+ : t("dispatch.sidebar.showAllChats", {
715
+ defaultValue: "Show all chats",
716
+ })}
717
+ </TooltipContent>
718
+ </Tooltip>
719
+ </div>
720
+ ) : null}
721
+ {!chatFirstMode &&
722
+ chatsLoading &&
432
723
  visibleThreads.length === 0 &&
433
724
  Array.from({ length: 3 }).map((_, index) => (
434
725
  <div
@@ -439,31 +730,147 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
439
730
  <Skeleton className="h-3 w-3/4 rounded" />
440
731
  </div>
441
732
  ))}
442
- <ChatHistoryRail
443
- items={chatItems}
444
- activeId={displayedActiveThreadId}
445
- onSelect={(threadId) => openThread(threadId)}
446
- onNewChat={() => void handleNewChat()}
447
- railLabels={{
448
- newChat: t("dispatch.sidebar.newChat"),
449
- showMore: t("dispatch.sidebar.chats"),
450
- showLess: t("dispatch.sidebar.chats"),
451
- }}
452
- renameMaxLength={160}
453
- onRename={(threadId, title) => void renameThread(threadId, title)}
454
- labels={{
455
- options: (item) =>
456
- t("dispatch.sidebar.chatOptions", {
457
- title: item.titleText ?? "",
458
- }),
459
- renameInput: (item) =>
460
- t("dispatch.sidebar.renameThread", {
461
- title: item.titleText ?? "",
462
- }),
463
- rename: t("dispatch.sidebar.renameChat"),
464
- }}
465
- className="min-w-0"
466
- />
733
+ {chatFirstMode ? (
734
+ <ChatFirstChatHistory
735
+ items={chatItems}
736
+ activeId={displayedActiveThreadId}
737
+ loading={chatsLoading && visibleThreads.length === 0}
738
+ loadingLabel={
739
+ <div className="space-y-1 px-2 py-1">
740
+ {[0, 1, 2].map((index) => (
741
+ <div key={index} className="flex items-center gap-2 px-1 py-1">
742
+ <Skeleton className="size-3.5 shrink-0 rounded-sm" />
743
+ <Skeleton className="h-3 w-3/4 rounded" />
744
+ </div>
745
+ ))}
746
+ </div>
747
+ }
748
+ label={t("dispatch.sidebar.chats", { defaultValue: "Chats" })}
749
+ onSelect={(threadId) => openThread(threadId)}
750
+ renameMaxLength={160}
751
+ onRename={(threadId, title) => void renameThread(threadId, title)}
752
+ labels={{
753
+ options: (item) =>
754
+ t("dispatch.sidebar.chatOptions", {
755
+ title: item.titleText ?? "",
756
+ }),
757
+ renameInput: (item) =>
758
+ t("dispatch.sidebar.renameThread", {
759
+ title: item.titleText ?? "",
760
+ }),
761
+ rename: t("dispatch.sidebar.renameChat"),
762
+ }}
763
+ renderAdditionalRowActions={(item, closeMenu) => (
764
+ <>
765
+ <button
766
+ type="button"
767
+ role="menuitem"
768
+ className="an-chat-history-row__menu-item"
769
+ onClick={() => {
770
+ closeMenu();
771
+ void writeClipboardText(item.id).then((copied) => {
772
+ toast[copied ? "success" : "error"](
773
+ copied
774
+ ? "Session ID copied"
775
+ : "Could not copy the session ID",
776
+ );
777
+ });
778
+ }}
779
+ >
780
+ <IconCopy size={13} strokeWidth={1.8} />
781
+ <span>Copy session ID</span>
782
+ </button>
783
+ {!chatFirstEmbedded ? (
784
+ <button
785
+ type="button"
786
+ role="menuitem"
787
+ className="an-chat-history-row__menu-item"
788
+ onClick={() => {
789
+ closeMenu();
790
+ emitChatFirstSessionWatch({
791
+ sessionId: item.id,
792
+ title: item.titleText,
793
+ kind: "agent-chat",
794
+ sourceSessionId: displayedActiveThreadId ?? undefined,
795
+ });
796
+ }}
797
+ >
798
+ <IconEye size={13} strokeWidth={1.8} />
799
+ <span>Watch and message session</span>
800
+ </button>
801
+ ) : null}
802
+ </>
803
+ )}
804
+ className="min-w-0"
805
+ />
806
+ ) : (
807
+ <ChatHistoryRail
808
+ items={chatItems}
809
+ activeId={displayedActiveThreadId}
810
+ onSelect={(threadId) => openThread(threadId)}
811
+ onNewChat={() => void handleNewChat()}
812
+ railLabels={{
813
+ newChat: t("dispatch.sidebar.newChat"),
814
+ showMore: t("dispatch.sidebar.chats"),
815
+ showLess: t("dispatch.sidebar.chats"),
816
+ }}
817
+ renameMaxLength={160}
818
+ onRename={(threadId, title) => void renameThread(threadId, title)}
819
+ labels={{
820
+ options: (item) =>
821
+ t("dispatch.sidebar.chatOptions", {
822
+ title: item.titleText ?? "",
823
+ }),
824
+ renameInput: (item) =>
825
+ t("dispatch.sidebar.renameThread", {
826
+ title: item.titleText ?? "",
827
+ }),
828
+ rename: t("dispatch.sidebar.renameChat"),
829
+ }}
830
+ renderAdditionalRowActions={(item, closeMenu) => (
831
+ <>
832
+ <button
833
+ type="button"
834
+ role="menuitem"
835
+ className="an-chat-history-row__menu-item"
836
+ onClick={() => {
837
+ closeMenu();
838
+ void writeClipboardText(item.id).then((copied) => {
839
+ toast[copied ? "success" : "error"](
840
+ copied
841
+ ? "Session ID copied"
842
+ : "Could not copy the session ID",
843
+ );
844
+ });
845
+ }}
846
+ >
847
+ <IconCopy size={13} strokeWidth={1.8} />
848
+ <span>Copy session ID</span>
849
+ </button>
850
+ {chatFirstMode && !chatFirstEmbedded ? (
851
+ <button
852
+ type="button"
853
+ role="menuitem"
854
+ className="an-chat-history-row__menu-item"
855
+ onClick={() => {
856
+ closeMenu();
857
+ emitChatFirstSessionWatch({
858
+ sessionId: item.id,
859
+ title: item.titleText,
860
+ kind: "agent-chat",
861
+ sourceSessionId: displayedActiveThreadId ?? undefined,
862
+ });
863
+ }}
864
+ >
865
+ <IconEye size={13} strokeWidth={1.8} />
866
+ <span>Watch and message session</span>
867
+ </button>
868
+ ) : null}
869
+ </>
870
+ )}
871
+ className="min-w-0"
872
+ />
873
+ )}
467
874
  </div>
468
875
  );
469
876
  }
@@ -471,17 +878,38 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
471
878
  export function NavContent({
472
879
  onNavigate,
473
880
  extensions,
881
+ chatFirstMode = false,
882
+ chatFirstEmbedded = false,
474
883
  collapsed = false,
475
884
  collapsible = false,
476
885
  onCollapsedChange,
886
+ chatFirstAppLayout,
887
+ onChatFirstAppLayoutChange,
888
+ chatFirstApps = [],
889
+ chatFirstAppsLoading = false,
890
+ chatFirstAppsError,
891
+ chatFirstActiveAppId,
892
+ onChatFirstAppOpen,
893
+ onChatFirstAppsRetry,
477
894
  }: {
478
895
  onNavigate?: () => void;
479
896
  extensions?: DispatchExtensionConfig;
897
+ chatFirstMode?: boolean;
898
+ chatFirstEmbedded?: boolean;
480
899
  collapsed?: boolean;
481
900
  collapsible?: boolean;
482
901
  onCollapsedChange?: (collapsed: boolean) => void;
902
+ chatFirstAppLayout?: ChatFirstAppLayoutPreference;
903
+ onChatFirstAppLayoutChange?: (layout: ChatFirstAppLayoutPreference) => void;
904
+ chatFirstApps?: readonly ChatFirstAppItem[];
905
+ chatFirstAppsLoading?: boolean;
906
+ chatFirstAppsError?: string | null;
907
+ chatFirstActiveAppId?: string;
908
+ onChatFirstAppOpen?: (app: ChatFirstAppItem) => void;
909
+ onChatFirstAppsRetry?: () => void;
483
910
  }) {
484
911
  const t = useT();
912
+ const chatFirstCopy = useMemo(() => createDispatchChatFirstCopy(t), [t]);
485
913
  const location = useLocation();
486
914
  const navigate = useNavigate();
487
915
  const extensionNavItems = extensions?.navItems ?? EMPTY_NAV_ITEMS;
@@ -547,6 +975,48 @@ export function NavContent({
547
975
  className={collapsed ? "size-8" : "min-w-0"}
548
976
  />
549
977
  );
978
+ const chatFirstCreateAppTrigger = (
979
+ <CreateAppPopover
980
+ align="start"
981
+ trigger={
982
+ <button
983
+ type="button"
984
+ className="flex size-6 items-center justify-center rounded text-sidebar-foreground/55 hover:bg-sidebar-accent hover:text-sidebar-foreground"
985
+ aria-label={chatFirstCopy("createWorkspaceApp")}
986
+ title={chatFirstCopy("createWorkspaceApp")}
987
+ >
988
+ <IconPlus size={14} aria-hidden="true" />
989
+ </button>
990
+ }
991
+ />
992
+ );
993
+
994
+ const chatFirstAppsRail = (
995
+ <ChatFirstAppsRail
996
+ apps={chatFirstApps}
997
+ activeAppId={chatFirstActiveAppId}
998
+ collapsed={collapsed}
999
+ loading={chatFirstAppsLoading}
1000
+ error={chatFirstAppsError}
1001
+ layout={chatFirstAppLayout}
1002
+ onLayoutChange={onChatFirstAppLayoutChange}
1003
+ onRetry={onChatFirstAppsRetry}
1004
+ onOpenApp={(app) => {
1005
+ onChatFirstAppOpen?.(app);
1006
+ onNavigate?.();
1007
+ }}
1008
+ createAppTrigger={chatFirstCreateAppTrigger}
1009
+ renderIcon={(app) => (
1010
+ <AppIcon
1011
+ id={app.id}
1012
+ name={app.name}
1013
+ size="sm"
1014
+ className="size-5 rounded-md"
1015
+ />
1016
+ )}
1017
+ copy={chatFirstCopy}
1018
+ />
1019
+ );
550
1020
 
551
1021
  const renderNavItem = (item: DispatchNavItem) => {
552
1022
  const Icon = item.icon;
@@ -579,6 +1049,7 @@ export function NavContent({
579
1049
  onNavigate?.();
580
1050
  }}
581
1051
  aria-label={label}
1052
+ aria-current={itemMatchesLocalPath ? "page" : undefined}
582
1053
  className={cn(
583
1054
  "flex size-9 items-center justify-center rounded-md text-sm",
584
1055
  itemMatchesLocalPath
@@ -617,6 +1088,7 @@ export function NavContent({
617
1088
  }
618
1089
  onNavigate?.();
619
1090
  }}
1091
+ aria-current={itemMatchesLocalPath ? "page" : undefined}
620
1092
  className={cn(
621
1093
  "flex h-8 w-full items-center gap-2 rounded-md px-2 text-sm",
622
1094
  itemMatchesLocalPath
@@ -633,7 +1105,11 @@ export function NavContent({
633
1105
  </Link>
634
1106
  )}
635
1107
  {!collapsed && item.id === "chat" && itemMatchesLocalPath ? (
636
- <DispatchChatsSection onNavigate={onNavigate} />
1108
+ <DispatchChatsSection
1109
+ onNavigate={onNavigate}
1110
+ chatFirstMode={chatFirstMode}
1111
+ chatFirstEmbedded={chatFirstEmbedded}
1112
+ />
637
1113
  ) : null}
638
1114
  </li>
639
1115
  );
@@ -688,7 +1164,37 @@ export function NavContent({
688
1164
  </div>
689
1165
  </div>
690
1166
 
691
- <div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
1167
+ {chatFirstMode ? (
1168
+ <div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
1169
+ {!collapsed ? (
1170
+ <DispatchChatsSection
1171
+ onNavigate={onNavigate}
1172
+ showNewChat
1173
+ chatFirstMode
1174
+ chatFirstEmbedded={chatFirstEmbedded}
1175
+ chatFirstNavigation={{
1176
+ onOpenIntegrations: () => {
1177
+ navigate(dispatchNavLinkTarget("/admin/integrations"));
1178
+ onNavigate?.();
1179
+ },
1180
+ onOpenScheduled: () => {
1181
+ navigate(dispatchNavLinkTarget("/admin/automations"));
1182
+ onNavigate?.();
1183
+ },
1184
+ }}
1185
+ prelude={chatFirstAppsRail}
1186
+ />
1187
+ ) : (
1188
+ chatFirstAppsRail
1189
+ )}
1190
+ </div>
1191
+ ) : null}
1192
+ <div
1193
+ className={cn(
1194
+ "flex min-h-0 flex-1 flex-col overflow-y-auto",
1195
+ chatFirstMode && "hidden",
1196
+ )}
1197
+ >
692
1198
  <nav className={cn("py-2", collapsed ? "px-1.5" : "px-2")}>
693
1199
  <ul
694
1200
  className={cn(
@@ -697,8 +1203,6 @@ export function NavContent({
697
1203
  >
698
1204
  {primaryNavItems.map(renderNavItem)}
699
1205
  </ul>
700
-
701
- <WorkspaceAppsRail collapsed={collapsed} onNavigate={onNavigate} />
702
1206
  </nav>
703
1207
 
704
1208
  <div className="mt-auto shrink-0">
@@ -727,6 +1231,16 @@ export function NavContent({
727
1231
  collapse={collapseButton}
728
1232
  />
729
1233
  </div>
1234
+ {chatFirstMode && collapsed ? (
1235
+ <div className="mt-auto shrink-0">
1236
+ <SidebarFooterActions
1237
+ collapsed
1238
+ feedback={feedbackButton}
1239
+ search={searchButton}
1240
+ collapse={collapseButton}
1241
+ />
1242
+ </div>
1243
+ ) : null}
730
1244
  </>
731
1245
  );
732
1246
  }
@@ -755,7 +1269,275 @@ export function Layout({
755
1269
  const localPathname = localDispatchPath(location.pathname);
756
1270
  const isChatRoute =
757
1271
  localPathname === "/chat" || localPathname.startsWith("/chat/");
1272
+ const chatFirstSurfaceScope = threadIdFromPath(localPathname) ?? "new";
758
1273
  const isWorkspaceAppHostRoute = localPathname.startsWith("/apps/");
1274
+ const [chatFirstPreference, setChatFirstPreference] = useState(() =>
1275
+ readChatFirstMode(),
1276
+ );
1277
+ const chatFirstEmbedded =
1278
+ typeof window !== "undefined" &&
1279
+ new URLSearchParams(window.location.search).get("chatFirst") === "1";
1280
+ const chatFirstMode =
1281
+ extensions?.chatFirst === true || chatFirstPreference || chatFirstEmbedded;
1282
+ const [chatFirstAppLayout, setChatFirstAppLayout] =
1283
+ useState<ChatFirstAppLayoutPreference>(() => readChatFirstAppLayout());
1284
+ const chatFirstAppLayoutHydratedRef = useRef(false);
1285
+ const chatFirstAppsQuery = useActionQuery<WorkspaceAppSummary[]>(
1286
+ "list-workspace-apps",
1287
+ { includeAgentCards: false },
1288
+ { enabled: chatFirstMode && isChatRoute },
1289
+ );
1290
+ const chatFirstWorkspaceApps = useMemo(
1291
+ () => mergeChatFirstWorkspaceApps(chatFirstAppsQuery.data),
1292
+ [chatFirstAppsQuery.data],
1293
+ );
1294
+ const chatFirstAppRegistrations = useMemo<ChatFirstAppRegistration[]>(
1295
+ () =>
1296
+ chatFirstWorkspaceApps.map((app) => ({
1297
+ id: app.id,
1298
+ name: app.name,
1299
+ path: app.path,
1300
+ url: app.url,
1301
+ enabled: app.status !== "pending" && app.archived !== true,
1302
+ })),
1303
+ [chatFirstWorkspaceApps],
1304
+ );
1305
+ const chatFirstAppItems = useMemo<ChatFirstAppItem[]>(
1306
+ () =>
1307
+ chatFirstWorkspaceApps.map((app) => ({
1308
+ id: app.id,
1309
+ name: app.name,
1310
+ })),
1311
+ [chatFirstWorkspaceApps],
1312
+ );
1313
+ const chatFirstCopy = useMemo(() => createDispatchChatFirstCopy(t), [t]);
1314
+ const createChatFirstEmbedSession = useActionMutation<
1315
+ ChatFirstEmbedSessionResult,
1316
+ ChatFirstEmbedSessionInput
1317
+ >("create_embed_session", { skipActionQueryInvalidation: true });
1318
+ const [chatFirstPane, setChatFirstPane] =
1319
+ useState<DispatchChatFirstPane | null>(null);
1320
+ const [chatFirstEmbedUrl, setChatFirstEmbedUrl] = useState<string | null>(
1321
+ null,
1322
+ );
1323
+ const [chatFirstEmbedError, setChatFirstEmbedError] = useState<string | null>(
1324
+ null,
1325
+ );
1326
+ const [chatFirstEmbedAttempt, setChatFirstEmbedAttempt] = useState(0);
1327
+ const [chatFirstNotice, setChatFirstNotice] = useState<string | null>(null);
1328
+ const chatFirstSessionWatch = useChatFirstSessionWatch();
1329
+ const chatFirstSurfaceTabs = useChatFirstSurfaceTabs(chatFirstSurfaceScope);
1330
+ const chatFirstSurfaceTabsStore = getChatFirstSurfaceTabsStore(
1331
+ chatFirstSurfaceScope,
1332
+ );
1333
+ const chatFirstAgentsQuery = useActionQuery<SearchAgentThreadsResult>(
1334
+ "search-agent-threads",
1335
+ { sourceId: "current", limit: 12 },
1336
+ {
1337
+ enabled:
1338
+ chatFirstMode &&
1339
+ isChatRoute &&
1340
+ chatFirstSurfaceTabs.activeTabId?.startsWith("agents:") === true,
1341
+ },
1342
+ );
1343
+ const chatFirstSurfacePanel = useChatFirstSurfacePanel(chatFirstSurfaceScope);
1344
+ const { setOpen: setChatFirstSurfacePanelOpen } = chatFirstSurfacePanel;
1345
+ const chatFirstSurfaceResize = useChatFirstSurfaceResize(
1346
+ chatFirstSurfaceScope,
1347
+ );
1348
+ const chatFirstPaneHydratedRef = useRef(false);
1349
+ const pendingChatFirstOpenAppRef = useRef<ChatFirstOpenAppDetail | null>(
1350
+ null,
1351
+ );
1352
+ const previousChatFirstSurfaceScopeRef = useRef(chatFirstSurfaceScope);
1353
+ const previousChatFirstSurfaceTabCountRef = useRef<number | null>(null);
1354
+ const activeChatFirstSurfaceTab = useMemo(
1355
+ () =>
1356
+ chatFirstSurfaceTabs.tabs.find(
1357
+ (tab) => tab.id === chatFirstSurfaceTabs.activeTabId,
1358
+ ) ?? null,
1359
+ [chatFirstSurfaceTabs],
1360
+ );
1361
+ const activeChatFirstApp = useMemo(
1362
+ () =>
1363
+ activeChatFirstSurfaceTab?.kind === "app" &&
1364
+ activeChatFirstSurfaceTab.appId
1365
+ ? (chatFirstWorkspaceApps.find(
1366
+ (app) => app.id === activeChatFirstSurfaceTab.appId,
1367
+ ) ?? null)
1368
+ : null,
1369
+ [activeChatFirstSurfaceTab, chatFirstWorkspaceApps],
1370
+ );
1371
+ const chatFirstAgentActivities = useMemo<ChatFirstAgentActivity[]>(
1372
+ () =>
1373
+ (chatFirstAgentsQuery.data?.threads ?? []).map((thread) => ({
1374
+ sessionId: thread.id,
1375
+ title:
1376
+ thread.title || chatFirstCopy("agentsTitle", { name: thread.id }),
1377
+ subtitle: thread.snippet || thread.preview || chatFirstCopy("session"),
1378
+ status: "recent",
1379
+ ...(thread.updatedAt ? { updatedAt: thread.updatedAt } : {}),
1380
+ })),
1381
+ [chatFirstAgentsQuery.data, chatFirstCopy],
1382
+ );
1383
+ const chatFirstEmbedPath =
1384
+ activeChatFirstSurfaceTab?.kind === "app"
1385
+ ? (activeChatFirstSurfaceTab.path ??
1386
+ chatFirstPane?.path ??
1387
+ activeChatFirstApp?.path)
1388
+ : null;
1389
+ useEffect(() => {
1390
+ if (!chatFirstMode || !isChatRoute || !activeChatFirstApp) {
1391
+ setChatFirstEmbedUrl(null);
1392
+ setChatFirstEmbedError(null);
1393
+ return;
1394
+ }
1395
+ if (!chatFirstEmbedPath?.startsWith("/")) {
1396
+ setChatFirstEmbedUrl(null);
1397
+ setChatFirstEmbedError(chatFirstCopy("appUnavailable"));
1398
+ return;
1399
+ }
1400
+ let cancelled = false;
1401
+ setChatFirstEmbedUrl(null);
1402
+ setChatFirstEmbedError(null);
1403
+ void createChatFirstEmbedSession
1404
+ .mutateAsync(
1405
+ buildChatFirstEmbedSessionInput(
1406
+ activeChatFirstApp.id,
1407
+ chatFirstEmbedPath,
1408
+ ),
1409
+ )
1410
+ .then((result) => {
1411
+ if (!cancelled) setChatFirstEmbedUrl(result.startUrl);
1412
+ })
1413
+ .catch((cause: unknown) => {
1414
+ if (!cancelled) {
1415
+ setChatFirstEmbedError(
1416
+ cause instanceof Error
1417
+ ? cause.message
1418
+ : chatFirstCopy("appUnavailable"),
1419
+ );
1420
+ }
1421
+ });
1422
+ return () => {
1423
+ cancelled = true;
1424
+ };
1425
+ }, [
1426
+ activeChatFirstApp,
1427
+ chatFirstCopy,
1428
+ chatFirstEmbedAttempt,
1429
+ chatFirstEmbedPath,
1430
+ chatFirstMode,
1431
+ createChatFirstEmbedSession.mutateAsync,
1432
+ isChatRoute,
1433
+ ]);
1434
+ const persistChatFirstPane = useCallback(
1435
+ (pane: DispatchChatFirstPane | null) => {
1436
+ setChatFirstPane(pane);
1437
+ void writeClientAppState(CHAT_FIRST_PANE_STATE_KEY, pane).catch(() => {
1438
+ setChatFirstNotice(
1439
+ "The app pane opened locally, but its focus could not be synced.",
1440
+ );
1441
+ });
1442
+ },
1443
+ [],
1444
+ );
1445
+ const openChatFirstPane = useCallback(
1446
+ (pane: DispatchChatFirstPane) => {
1447
+ setChatFirstNotice(null);
1448
+ closeChatFirstSessionWatch();
1449
+ const app = chatFirstAppRegistrations.find(
1450
+ (candidate) => candidate.id === pane.appId,
1451
+ );
1452
+ chatFirstSurfaceTabsStore.open({
1453
+ id: chatFirstSurfaceTabId(
1454
+ "app",
1455
+ `${pane.appId}:${pane.path ?? pane.view ?? "/"}`,
1456
+ ),
1457
+ kind: "app",
1458
+ title: app?.name ?? pane.appId,
1459
+ appId: pane.appId,
1460
+ ...(pane.path ? { path: pane.path } : {}),
1461
+ ...(pane.view ? { view: pane.view } : {}),
1462
+ });
1463
+ persistChatFirstPane(pane);
1464
+ },
1465
+ [
1466
+ chatFirstAppRegistrations,
1467
+ chatFirstSurfaceTabsStore,
1468
+ persistChatFirstPane,
1469
+ ],
1470
+ );
1471
+ const openChatFirstSurface = useCallback(
1472
+ (kind: ChatFirstSurfaceKind) => {
1473
+ if (kind !== "agents") return;
1474
+ persistChatFirstPane(null);
1475
+ closeChatFirstSessionWatch();
1476
+ chatFirstSurfaceTabsStore.open({
1477
+ id: chatFirstSurfaceTabId("agents", "activity"),
1478
+ kind: "agents",
1479
+ title: "Agents",
1480
+ });
1481
+ },
1482
+ [chatFirstSurfaceTabsStore, persistChatFirstPane],
1483
+ );
1484
+ const resolveChatFirstOpenBrowser = useCallback(
1485
+ (detail: ChatFirstOpenBrowserDetail) => {
1486
+ const resolution = resolveChatFirstBrowserTarget(detail);
1487
+ if (resolution.status === "unresolved") {
1488
+ setChatFirstNotice(
1489
+ chatFirstBrowserResolutionMessage(resolution.reason),
1490
+ );
1491
+ return;
1492
+ }
1493
+ persistChatFirstPane(null);
1494
+ closeChatFirstSessionWatch();
1495
+ setChatFirstNotice(null);
1496
+ chatFirstSurfaceTabsStore.open({
1497
+ id: chatFirstSurfaceTabId("browser", resolution.target.url),
1498
+ kind: "browser",
1499
+ title: resolution.target.title ?? "Browser",
1500
+ url: resolution.target.url,
1501
+ });
1502
+ },
1503
+ [chatFirstSurfaceTabsStore, persistChatFirstPane],
1504
+ );
1505
+ const resolveChatFirstOpenApp = useCallback(
1506
+ (detail: ChatFirstOpenAppDetail) => {
1507
+ if (chatFirstAppsQuery.isLoading) {
1508
+ pendingChatFirstOpenAppRef.current = detail;
1509
+ return;
1510
+ }
1511
+ if (chatFirstAppsQuery.isError) {
1512
+ pendingChatFirstOpenAppRef.current = null;
1513
+ setChatFirstNotice(
1514
+ "Workspace apps could not be loaded, so the requested app was not opened.",
1515
+ );
1516
+ return;
1517
+ }
1518
+
1519
+ const resolution = resolveChatFirstAppTarget(
1520
+ detail,
1521
+ chatFirstAppRegistrations,
1522
+ {
1523
+ currentOrigin:
1524
+ typeof window === "undefined" ? undefined : window.location.origin,
1525
+ },
1526
+ );
1527
+ pendingChatFirstOpenAppRef.current = null;
1528
+ if (resolution.status === "unresolved") {
1529
+ setChatFirstNotice(chatFirstResolutionMessage(resolution.reason));
1530
+ return;
1531
+ }
1532
+ openChatFirstPane(resolution.target);
1533
+ },
1534
+ [
1535
+ chatFirstAppRegistrations,
1536
+ chatFirstAppsQuery.isError,
1537
+ chatFirstAppsQuery.isLoading,
1538
+ openChatFirstPane,
1539
+ ],
1540
+ );
759
1541
  const sidebarBeforeAppRef = useRef<boolean | null>(null);
760
1542
  const sidebarAutoCollapsedRef = useRef(false);
761
1543
  const chatHomeHandoffActive = useAgentChatHomeHandoff({
@@ -771,6 +1553,227 @@ export function Layout({
771
1553
  };
772
1554
  useAgentChatHomeHandoffLinks(chatHandoffLinkOptions);
773
1555
 
1556
+ useEffect(() => {
1557
+ const handleModeChange = (event: Event) => {
1558
+ const enabled = (event as CustomEvent<{ enabled?: unknown }>).detail
1559
+ ?.enabled;
1560
+ setChatFirstPreference(enabled === true);
1561
+ };
1562
+ window.addEventListener(CHAT_FIRST_MODE_CHANGED_EVENT, handleModeChange);
1563
+ return () =>
1564
+ window.removeEventListener(
1565
+ CHAT_FIRST_MODE_CHANGED_EVENT,
1566
+ handleModeChange,
1567
+ );
1568
+ }, []);
1569
+
1570
+ useEffect(() => {
1571
+ if (
1572
+ !chatFirstMode ||
1573
+ !isChatRoute ||
1574
+ chatFirstAppLayoutHydratedRef.current
1575
+ ) {
1576
+ return;
1577
+ }
1578
+ chatFirstAppLayoutHydratedRef.current = true;
1579
+ void readClientAppState<unknown>("chat-first-app-layout")
1580
+ .then((value) => {
1581
+ if (!value || typeof value !== "object") return;
1582
+ const candidate = value as Partial<ChatFirstAppLayoutPreference>;
1583
+ const ids = (input: unknown) =>
1584
+ Array.isArray(input)
1585
+ ? input.filter(
1586
+ (id): id is string =>
1587
+ typeof id === "string" && id.trim().length > 0,
1588
+ )
1589
+ : [];
1590
+ setChatFirstAppLayout({
1591
+ pinnedIds: [...new Set(ids(candidate.pinnedIds))],
1592
+ orderedIds: [...new Set(ids(candidate.orderedIds))],
1593
+ });
1594
+ })
1595
+ .catch(() => {
1596
+ // Device-local layout remains the fallback when workspace state is unavailable.
1597
+ });
1598
+ }, [chatFirstMode, isChatRoute]);
1599
+
1600
+ const persistChatFirstAppLayout = useCallback(
1601
+ (layout: ChatFirstAppLayoutPreference) => {
1602
+ setChatFirstAppLayout(layout);
1603
+ void writeClientAppState("chat-first-app-layout", layout).catch(() => {
1604
+ setChatFirstNotice(
1605
+ "App order changed locally, but workspace state could not be synced.",
1606
+ );
1607
+ });
1608
+ },
1609
+ [],
1610
+ );
1611
+
1612
+ useEffect(() => {
1613
+ if (!chatFirstMode || !isChatRoute) {
1614
+ setChatFirstPane(null);
1615
+ closeChatFirstSessionWatch();
1616
+ chatFirstSurfaceTabsStore.closeAll();
1617
+ setChatFirstNotice(null);
1618
+ pendingChatFirstOpenAppRef.current = null;
1619
+ chatFirstPaneHydratedRef.current = false;
1620
+ return;
1621
+ }
1622
+ const unsubscribeApp = subscribeChatFirstOpenApp(resolveChatFirstOpenApp);
1623
+ const unsubscribeBrowser = subscribeChatFirstOpenBrowser(
1624
+ resolveChatFirstOpenBrowser,
1625
+ );
1626
+ return () => {
1627
+ unsubscribeApp();
1628
+ unsubscribeBrowser();
1629
+ };
1630
+ }, [
1631
+ chatFirstMode,
1632
+ isChatRoute,
1633
+ chatFirstSurfaceTabsStore,
1634
+ resolveChatFirstOpenApp,
1635
+ resolveChatFirstOpenBrowser,
1636
+ ]);
1637
+
1638
+ useEffect(() => {
1639
+ if (
1640
+ !chatFirstMode ||
1641
+ !isChatRoute ||
1642
+ chatFirstPaneHydratedRef.current ||
1643
+ chatFirstAppsQuery.isLoading ||
1644
+ chatFirstAppsQuery.isError
1645
+ ) {
1646
+ return;
1647
+ }
1648
+ chatFirstPaneHydratedRef.current = true;
1649
+ if (chatFirstPane) return;
1650
+ let active = true;
1651
+ void readClientAppState<unknown>(CHAT_FIRST_PANE_STATE_KEY)
1652
+ .then((value) => {
1653
+ if (!active) return;
1654
+ const persisted = persistedChatFirstPane(value);
1655
+ if (!persisted) return;
1656
+ const resolution = resolveChatFirstAppTarget(
1657
+ {
1658
+ app: persisted.appId,
1659
+ path: persisted.path,
1660
+ view: persisted.view,
1661
+ },
1662
+ chatFirstAppRegistrations,
1663
+ {
1664
+ currentOrigin:
1665
+ typeof window === "undefined"
1666
+ ? undefined
1667
+ : window.location.origin,
1668
+ },
1669
+ );
1670
+ if (resolution.status === "ready") openChatFirstPane(resolution.target);
1671
+ })
1672
+ .catch(() => {
1673
+ if (active) {
1674
+ setChatFirstNotice(
1675
+ "The last app pane could not be restored from workspace state.",
1676
+ );
1677
+ }
1678
+ });
1679
+ return () => {
1680
+ active = false;
1681
+ };
1682
+ }, [
1683
+ chatFirstAppsQuery.isError,
1684
+ chatFirstAppsQuery.isLoading,
1685
+ chatFirstAppRegistrations,
1686
+ chatFirstMode,
1687
+ chatFirstPane,
1688
+ isChatRoute,
1689
+ openChatFirstPane,
1690
+ ]);
1691
+
1692
+ useEffect(() => {
1693
+ if (previousChatFirstSurfaceScopeRef.current === chatFirstSurfaceScope) {
1694
+ return;
1695
+ }
1696
+ previousChatFirstSurfaceScopeRef.current = chatFirstSurfaceScope;
1697
+ persistChatFirstPane(null);
1698
+ setChatFirstEmbedUrl(null);
1699
+ setChatFirstEmbedError(null);
1700
+ setChatFirstNotice(null);
1701
+ pendingChatFirstOpenAppRef.current = null;
1702
+ chatFirstPaneHydratedRef.current = true;
1703
+ chatFirstSurfaceTabsStore.closeAll();
1704
+ setChatFirstSurfacePanelOpen(false);
1705
+ closeChatFirstSessionWatch();
1706
+ }, [
1707
+ chatFirstSurfaceScope,
1708
+ chatFirstSurfaceTabsStore,
1709
+ persistChatFirstPane,
1710
+ setChatFirstSurfacePanelOpen,
1711
+ ]);
1712
+
1713
+ useEffect(() => {
1714
+ const tabCount = chatFirstSurfaceTabs.tabs.length;
1715
+ const previousTabCount = previousChatFirstSurfaceTabCountRef.current;
1716
+ if (!chatFirstMode || !isChatRoute) {
1717
+ setChatFirstSurfacePanelOpen(false);
1718
+ } else if (
1719
+ tabCount > 0 &&
1720
+ (previousTabCount === null || previousTabCount === 0)
1721
+ ) {
1722
+ setChatFirstSurfacePanelOpen(true);
1723
+ } else if (
1724
+ tabCount === 0 &&
1725
+ previousTabCount !== null &&
1726
+ previousTabCount > 0
1727
+ ) {
1728
+ setChatFirstSurfacePanelOpen(false);
1729
+ }
1730
+ previousChatFirstSurfaceTabCountRef.current = tabCount;
1731
+ }, [
1732
+ chatFirstMode,
1733
+ setChatFirstSurfacePanelOpen,
1734
+ chatFirstSurfaceTabs.tabs.length,
1735
+ isChatRoute,
1736
+ ]);
1737
+
1738
+ useEffect(() => {
1739
+ const activeTab = activeChatFirstSurfaceTab;
1740
+ if (
1741
+ activeTab?.kind === "side-chat" &&
1742
+ activeTab.session &&
1743
+ !chatFirstSessionWatch.target
1744
+ ) {
1745
+ emitChatFirstSessionWatch(activeTab.session);
1746
+ }
1747
+ }, [activeChatFirstSurfaceTab, chatFirstSessionWatch.target]);
1748
+
1749
+ useEffect(() => {
1750
+ const pending = pendingChatFirstOpenAppRef.current;
1751
+ if (!pending || chatFirstAppsQuery.isLoading) return;
1752
+ resolveChatFirstOpenApp(pending);
1753
+ }, [
1754
+ chatFirstAppsQuery.data,
1755
+ chatFirstAppsQuery.isError,
1756
+ chatFirstAppsQuery.isLoading,
1757
+ resolveChatFirstOpenApp,
1758
+ ]);
1759
+
1760
+ useEffect(() => {
1761
+ const target = chatFirstSessionWatch.target;
1762
+ if (!target || !chatFirstMode || !isChatRoute) return;
1763
+ setChatFirstPane(null);
1764
+ chatFirstSurfaceTabsStore.open({
1765
+ id: chatFirstSurfaceTabId("side-chat", target.sessionId),
1766
+ kind: "side-chat",
1767
+ title: target.title ? `Watch · ${target.title}` : "Watched session",
1768
+ session: target,
1769
+ });
1770
+ }, [
1771
+ chatFirstMode,
1772
+ chatFirstSessionWatch.target,
1773
+ chatFirstSurfaceTabsStore,
1774
+ isChatRoute,
1775
+ ]);
1776
+
774
1777
  useEffect(() => {
775
1778
  if (isWorkspaceAppHostRoute) {
776
1779
  if (!sidebarAutoCollapsedRef.current) {
@@ -805,10 +1808,222 @@ export function Layout({
805
1808
  }
806
1809
  }, [isWorkspaceAppHostRoute, sidebarCollapsed]);
807
1810
 
1811
+ const activateChatFirstSurfaceTab = useCallback(
1812
+ (tab: (typeof chatFirstSurfaceTabs.tabs)[number]) => {
1813
+ chatFirstSurfaceTabsStore.activate(tab.id);
1814
+ if (tab.kind === "app" && tab.appId) {
1815
+ closeChatFirstSessionWatch();
1816
+ persistChatFirstPane({
1817
+ appId: tab.appId,
1818
+ ...(tab.path ? { path: tab.path } : {}),
1819
+ ...(tab.view ? { view: tab.view } : {}),
1820
+ });
1821
+ return;
1822
+ }
1823
+ if (tab.kind === "browser" && tab.url) {
1824
+ persistChatFirstPane(null);
1825
+ closeChatFirstSessionWatch();
1826
+ return;
1827
+ }
1828
+ if (tab.kind === "side-chat" && tab.session) {
1829
+ persistChatFirstPane(null);
1830
+ emitChatFirstSessionWatch(tab.session);
1831
+ return;
1832
+ }
1833
+ if (tab.kind === "agents") {
1834
+ persistChatFirstPane(null);
1835
+ closeChatFirstSessionWatch();
1836
+ }
1837
+ },
1838
+ [
1839
+ chatFirstSurfaceTabs.tabs,
1840
+ chatFirstSurfaceTabsStore,
1841
+ persistChatFirstPane,
1842
+ ],
1843
+ );
1844
+
1845
+ const closeChatFirstSurfaceTab = useCallback(
1846
+ (tab: (typeof chatFirstSurfaceTabs.tabs)[number]) => {
1847
+ const isActive = chatFirstSurfaceTabs.activeTabId === tab.id;
1848
+ if (tab.kind === "app" && isActive) persistChatFirstPane(null);
1849
+ if (tab.kind === "side-chat" && isActive) {
1850
+ closeChatFirstSessionWatch();
1851
+ }
1852
+ chatFirstSurfaceTabsStore.close(tab.id);
1853
+ },
1854
+ [chatFirstSurfaceTabs, chatFirstSurfaceTabsStore, persistChatFirstPane],
1855
+ );
1856
+
1857
+ const closeAllChatFirstSurfaceTabs = useCallback(() => {
1858
+ persistChatFirstPane(null);
1859
+ closeChatFirstSessionWatch();
1860
+ chatFirstSurfaceTabsStore.closeAll();
1861
+ }, [chatFirstSurfaceTabsStore, persistChatFirstPane]);
1862
+
1863
+ const renderChatFirstWatchChat = useCallback(
1864
+ (target: ChatFirstSessionReference) => (
1865
+ <AgentChatSurface
1866
+ mode="page"
1867
+ className="h-full min-h-0 w-full"
1868
+ defaultMode="chat"
1869
+ storageKey={`dispatch-session-watch:${target.sessionId}`}
1870
+ restoreActiveThread={false}
1871
+ threadUrlSync={{
1872
+ routeThreadId: target.sessionId,
1873
+ getPath: chatThreadPath,
1874
+ navigate: () => undefined,
1875
+ }}
1876
+ showHeader={false}
1877
+ showTabBar={false}
1878
+ showPageNewChatButton={false}
1879
+ chatOnly
1880
+ dynamicSuggestions={false}
1881
+ suggestions={[]}
1882
+ emptyStateDisplay="hidden"
1883
+ composerPlaceholder={t(
1884
+ "dispatch.pages.chatFirstSessionMessagePlaceholder",
1885
+ )}
1886
+ />
1887
+ ),
1888
+ [t],
1889
+ );
1890
+
1891
+ const renderChatFirstSurfaceTab = useCallback(
1892
+ (tab: ChatFirstSurfaceTab) => {
1893
+ if (tab.kind === "app") {
1894
+ if (tab.id !== chatFirstSurfaceTabs.activeTabId) return null;
1895
+ const app = tab.appId
1896
+ ? (chatFirstWorkspaceApps.find(
1897
+ (candidate) => candidate.id === tab.appId,
1898
+ ) ?? null)
1899
+ : null;
1900
+ return (
1901
+ <ChatFirstAppPane
1902
+ app={app}
1903
+ status={
1904
+ chatFirstEmbedError
1905
+ ? "error"
1906
+ : chatFirstEmbedUrl
1907
+ ? "ready"
1908
+ : chatFirstAppsQuery.isLoading
1909
+ ? "loading"
1910
+ : app
1911
+ ? "loading"
1912
+ : "unresolved"
1913
+ }
1914
+ embedUrl={chatFirstEmbedUrl}
1915
+ errorMessage={chatFirstEmbedError}
1916
+ onRetry={() => setChatFirstEmbedAttempt((value) => value + 1)}
1917
+ renderEmbed={({ url, title }: ChatFirstEmbedTarget) => (
1918
+ <iframe
1919
+ data-dispatch-chat-first-app-frame
1920
+ src={url}
1921
+ title={title ?? chatFirstCopy("appUnavailable")}
1922
+ referrerPolicy="no-referrer"
1923
+ allow="clipboard-read; clipboard-write"
1924
+ className="h-full w-full border-0 bg-background"
1925
+ />
1926
+ )}
1927
+ copy={chatFirstCopy}
1928
+ />
1929
+ );
1930
+ }
1931
+ if (tab.kind === "browser" && tab.url) {
1932
+ return (
1933
+ <ChatFirstBrowserPane
1934
+ url={tab.url}
1935
+ title={tab.title}
1936
+ onClose={() => closeChatFirstSurfaceTab(tab)}
1937
+ renderEmbed={({ url, title, key }: ChatFirstEmbedTarget) => (
1938
+ <iframe
1939
+ key={key}
1940
+ src={url}
1941
+ title={title ?? chatFirstCopy("browserPage")}
1942
+ referrerPolicy="no-referrer"
1943
+ allow="clipboard-read; clipboard-write"
1944
+ className="h-full w-full border-0 bg-background"
1945
+ />
1946
+ )}
1947
+ copy={chatFirstCopy}
1948
+ />
1949
+ );
1950
+ }
1951
+ if (tab.kind === "side-chat") {
1952
+ const target =
1953
+ tab.session ??
1954
+ (tab.id === chatFirstSurfaceTabs.activeTabId
1955
+ ? chatFirstSessionWatch.target
1956
+ : null);
1957
+ return target ? (
1958
+ <ChatFirstSessionWatchPane
1959
+ target={target}
1960
+ onClose={() => closeChatFirstSurfaceTab(tab)}
1961
+ renderChat={renderChatFirstWatchChat}
1962
+ copy={chatFirstCopy}
1963
+ />
1964
+ ) : null;
1965
+ }
1966
+ if (tab.kind === "agents") {
1967
+ return (
1968
+ <ChatFirstAgentsPane
1969
+ activities={chatFirstAgentActivities}
1970
+ loading={chatFirstAgentsQuery.isLoading}
1971
+ error={
1972
+ chatFirstAgentsQuery.isError
1973
+ ? chatFirstAgentsQuery.error.message
1974
+ : null
1975
+ }
1976
+ onRefresh={() => void chatFirstAgentsQuery.refetch()}
1977
+ onWatch={(activity) =>
1978
+ emitChatFirstSessionWatch({
1979
+ sessionId: activity.sessionId,
1980
+ title: activity.title,
1981
+ kind: "agent-chat",
1982
+ ...(activity.goalId ? { goalId: activity.goalId } : {}),
1983
+ })
1984
+ }
1985
+ copy={chatFirstCopy}
1986
+ />
1987
+ );
1988
+ }
1989
+ return null;
1990
+ },
1991
+ [
1992
+ chatFirstAgentActivities,
1993
+ chatFirstAgentsQuery.error,
1994
+ chatFirstAgentsQuery.isError,
1995
+ chatFirstAgentsQuery.isLoading,
1996
+ chatFirstAppsQuery.isLoading,
1997
+ chatFirstCopy,
1998
+ chatFirstEmbedError,
1999
+ chatFirstEmbedUrl,
2000
+ chatFirstSessionWatch.target,
2001
+ chatFirstSurfaceTabs.activeTabId,
2002
+ chatFirstWorkspaceApps,
2003
+ closeChatFirstSurfaceTab,
2004
+ renderChatFirstWatchChat,
2005
+ ],
2006
+ );
2007
+
808
2008
  if (CHROMELESS_PATHS.some((path) => localPathname === path)) {
809
2009
  return <>{children}</>;
810
2010
  }
811
2011
 
2012
+ if (isWorkspaceAppHostRoute) {
2013
+ return (
2014
+ <DispatchExtensionsContext.Provider value={extensions}>
2015
+ <HeaderActionsProvider>
2016
+ <div
2017
+ data-dispatch-workspace-app-chrome-less
2018
+ className="h-screen w-full overflow-hidden bg-background"
2019
+ >
2020
+ {children}
2021
+ </div>
2022
+ </HeaderActionsProvider>
2023
+ </DispatchExtensionsContext.Provider>
2024
+ );
2025
+ }
2026
+
812
2027
  const showHeader = !isChatRoute && !pageOwnsToolbar(localPathname);
813
2028
  function openAskAgentFullscreen() {
814
2029
  focusAgentChat();
@@ -823,9 +2038,32 @@ export function Layout({
823
2038
  t("dispatch.sidebar.suggestionGrantKey"),
824
2039
  ];
825
2040
  const appContent = (
826
- <div className="flex h-full min-w-0 flex-1 flex-col overflow-hidden">
2041
+ <div className="relative flex h-full min-w-0 flex-1 flex-col overflow-hidden">
827
2042
  {showHeader ? <Header onOpenMobile={() => setMobileOpen(true)} /> : null}
828
2043
  <InvitationBanner />
2044
+ {isChatRoute && chatFirstMode ? (
2045
+ <ChatFirstSurfacePanelToggle
2046
+ open={chatFirstSurfacePanel.open}
2047
+ onToggle={chatFirstSurfacePanel.toggle}
2048
+ className="absolute right-3 top-2 z-[4]"
2049
+ />
2050
+ ) : null}
2051
+ {isChatRoute && chatFirstMode && chatFirstNotice ? (
2052
+ <div
2053
+ className="dispatch-chat-first-notice"
2054
+ role="status"
2055
+ aria-live="polite"
2056
+ >
2057
+ <span>{chatFirstNotice}</span>
2058
+ <button
2059
+ type="button"
2060
+ onClick={() => setChatFirstNotice(null)}
2061
+ aria-label="Dismiss app notice"
2062
+ >
2063
+ Dismiss
2064
+ </button>
2065
+ </div>
2066
+ ) : null}
829
2067
  <main
830
2068
  className={cn(
831
2069
  "flex-1",
@@ -845,8 +2083,52 @@ export function Layout({
845
2083
  </div>
846
2084
  );
847
2085
  const content = isChatRoute ? (
848
- <div className="agent-layout-main-surface flex min-w-0 flex-1 overflow-hidden">
2086
+ <div
2087
+ className={cn(
2088
+ "agent-layout-main-surface flex min-w-0 flex-1 overflow-hidden",
2089
+ chatFirstMode && "dispatch-chat-first-surface",
2090
+ )}
2091
+ >
849
2092
  {appContent}
2093
+ {chatFirstMode && chatFirstSurfacePanel.open ? (
2094
+ <ChatFirstSurfacePanel
2095
+ width={chatFirstSurfaceResize.width}
2096
+ onResizePointerDown={chatFirstSurfaceResize.onPointerDown}
2097
+ copy={chatFirstCopy}
2098
+ >
2099
+ <ChatFirstSurfaceTabs
2100
+ tabs={chatFirstSurfaceTabs.tabs}
2101
+ activeTabId={chatFirstSurfaceTabs.activeTabId}
2102
+ onActivate={activateChatFirstSurfaceTab}
2103
+ onClose={closeChatFirstSurfaceTab}
2104
+ onCloseOthers={(tab) => {
2105
+ activateChatFirstSurfaceTab(tab);
2106
+ chatFirstSurfaceTabsStore.closeOthers(tab.id);
2107
+ }}
2108
+ onCloseToRight={(tab) => {
2109
+ const targetIndex = chatFirstSurfaceTabs.tabs.findIndex(
2110
+ (candidate) => candidate.id === tab.id,
2111
+ );
2112
+ const activeIndex = chatFirstSurfaceTabs.tabs.findIndex(
2113
+ (candidate) =>
2114
+ candidate.id === chatFirstSurfaceTabs.activeTabId,
2115
+ );
2116
+ if (activeIndex > targetIndex) activateChatFirstSurfaceTab(tab);
2117
+ chatFirstSurfaceTabsStore.closeToRight(tab.id);
2118
+ }}
2119
+ onCloseAll={closeAllChatFirstSurfaceTabs}
2120
+ onOpenSurface={openChatFirstSurface}
2121
+ copy={chatFirstCopy}
2122
+ />
2123
+ {chatFirstSurfaceTabs.tabs.length > 0 ? (
2124
+ <ChatFirstSurfaceContent
2125
+ tabs={chatFirstSurfaceTabs.tabs}
2126
+ activeTabId={chatFirstSurfaceTabs.activeTabId}
2127
+ renderTab={renderChatFirstSurfaceTab}
2128
+ />
2129
+ ) : null}
2130
+ </ChatFirstSurfacePanel>
2131
+ ) : null}
850
2132
  </div>
851
2133
  ) : (
852
2134
  <AgentSidebar
@@ -877,7 +2159,25 @@ export function Layout({
877
2159
  >
878
2160
  <NavContent
879
2161
  extensions={extensions}
2162
+ chatFirstMode={chatFirstMode}
2163
+ chatFirstEmbedded={chatFirstEmbedded}
880
2164
  collapsed={sidebarCollapsed}
2165
+ chatFirstAppLayout={chatFirstAppLayout}
2166
+ onChatFirstAppLayoutChange={persistChatFirstAppLayout}
2167
+ chatFirstApps={chatFirstAppItems}
2168
+ chatFirstAppsLoading={chatFirstAppsQuery.isLoading}
2169
+ chatFirstAppsError={
2170
+ chatFirstAppsQuery.isError
2171
+ ? chatFirstCopy("appsLoadError")
2172
+ : null
2173
+ }
2174
+ chatFirstActiveAppId={
2175
+ activeChatFirstSurfaceTab?.kind === "app"
2176
+ ? activeChatFirstSurfaceTab.appId
2177
+ : undefined
2178
+ }
2179
+ onChatFirstAppOpen={(app) => openChatFirstPane({ appId: app.id })}
2180
+ onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
881
2181
  collapsible
882
2182
  onCollapsedChange={setSidebarCollapsed}
883
2183
  />
@@ -897,7 +2197,27 @@ export function Layout({
897
2197
  <div className="flex h-full w-full flex-col">
898
2198
  <NavContent
899
2199
  extensions={extensions}
2200
+ chatFirstMode={chatFirstMode}
2201
+ chatFirstEmbedded={chatFirstEmbedded}
900
2202
  collapsed={false}
2203
+ chatFirstAppLayout={chatFirstAppLayout}
2204
+ onChatFirstAppLayoutChange={persistChatFirstAppLayout}
2205
+ chatFirstApps={chatFirstAppItems}
2206
+ chatFirstAppsLoading={chatFirstAppsQuery.isLoading}
2207
+ chatFirstAppsError={
2208
+ chatFirstAppsQuery.isError
2209
+ ? chatFirstCopy("appsLoadError")
2210
+ : null
2211
+ }
2212
+ chatFirstActiveAppId={
2213
+ activeChatFirstSurfaceTab?.kind === "app"
2214
+ ? activeChatFirstSurfaceTab.appId
2215
+ : undefined
2216
+ }
2217
+ onChatFirstAppOpen={(app) =>
2218
+ openChatFirstPane({ appId: app.id })
2219
+ }
2220
+ onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
901
2221
  onNavigate={() => setMobileOpen(false)}
902
2222
  />
903
2223
  </div>