@agent-native/dispatch 0.41.1-nightly-20261104110058 → 0.42.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 (195) hide show
  1. package/CHANGELOG.md +26 -55
  2. package/dist/actions/get-hosted-apps-settings.d.ts +5 -0
  3. package/dist/actions/get-hosted-apps-settings.d.ts.map +1 -0
  4. package/dist/actions/get-hosted-apps-settings.js +25 -0
  5. package/dist/actions/get-hosted-apps-settings.js.map +1 -0
  6. package/dist/actions/index.d.ts.map +1 -1
  7. package/dist/actions/index.js +4 -0
  8. package/dist/actions/index.js.map +1 -1
  9. package/dist/actions/list-connected-agents.d.ts +1 -0
  10. package/dist/actions/list-connected-agents.js +11 -10
  11. package/dist/actions/list-connected-agents.js.map +1 -1
  12. package/dist/actions/list_apps.js +3 -2
  13. package/dist/actions/list_apps.js.map +1 -1
  14. package/dist/actions/provider-api-register.d.ts +10 -10
  15. package/dist/actions/set-hosted-apps-settings.d.ts +7 -0
  16. package/dist/actions/set-hosted-apps-settings.d.ts.map +1 -0
  17. package/dist/actions/set-hosted-apps-settings.js +35 -0
  18. package/dist/actions/set-hosted-apps-settings.js.map +1 -0
  19. package/dist/components/app-card-tiers.d.ts +13 -0
  20. package/dist/components/app-card-tiers.d.ts.map +1 -0
  21. package/dist/components/app-card-tiers.js +22 -0
  22. package/dist/components/app-card-tiers.js.map +1 -0
  23. package/dist/components/app-list-row.d.ts +12 -4
  24. package/dist/components/app-list-row.d.ts.map +1 -1
  25. package/dist/components/app-list-row.js +19 -4
  26. package/dist/components/app-list-row.js.map +1 -1
  27. package/dist/components/app-open-actions.d.ts +4 -0
  28. package/dist/components/app-open-actions.d.ts.map +1 -1
  29. package/dist/components/app-open-actions.js +9 -0
  30. package/dist/components/app-open-actions.js.map +1 -1
  31. package/dist/components/app-pin-button.d.ts +9 -0
  32. package/dist/components/app-pin-button.d.ts.map +1 -0
  33. package/dist/components/app-pin-button.js +29 -0
  34. package/dist/components/app-pin-button.js.map +1 -0
  35. package/dist/components/app-tier-sections.d.ts +27 -0
  36. package/dist/components/app-tier-sections.d.ts.map +1 -0
  37. package/dist/components/app-tier-sections.js +44 -0
  38. package/dist/components/app-tier-sections.js.map +1 -0
  39. package/dist/components/chat-home-apps.d.ts.map +1 -1
  40. package/dist/components/chat-home-apps.js +19 -26
  41. package/dist/components/chat-home-apps.js.map +1 -1
  42. package/dist/components/connected-app-card.d.ts +3 -1
  43. package/dist/components/connected-app-card.d.ts.map +1 -1
  44. package/dist/components/connected-app-card.js +6 -3
  45. package/dist/components/connected-app-card.js.map +1 -1
  46. package/dist/components/create-app-popover.d.ts +4 -0
  47. package/dist/components/create-app-popover.d.ts.map +1 -1
  48. package/dist/components/create-app-popover.js +6 -0
  49. package/dist/components/create-app-popover.js.map +1 -1
  50. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  51. package/dist/components/dispatch-control-plane.js +25 -35
  52. package/dist/components/dispatch-control-plane.js.map +1 -1
  53. package/dist/components/hosted-apps-settings-group.d.ts +6 -0
  54. package/dist/components/hosted-apps-settings-group.d.ts.map +1 -0
  55. package/dist/components/hosted-apps-settings-group.js +76 -0
  56. package/dist/components/hosted-apps-settings-group.js.map +1 -0
  57. package/dist/components/layout/Layout.d.ts +6 -4
  58. package/dist/components/layout/Layout.d.ts.map +1 -1
  59. package/dist/components/layout/Layout.js +97 -80
  60. package/dist/components/layout/Layout.js.map +1 -1
  61. package/dist/components/workspace-app-card.d.ts.map +1 -1
  62. package/dist/components/workspace-app-card.js +9 -20
  63. package/dist/components/workspace-app-card.js.map +1 -1
  64. package/dist/components/workspace-app-layout-notice.d.ts +9 -0
  65. package/dist/components/workspace-app-layout-notice.d.ts.map +1 -0
  66. package/dist/components/workspace-app-layout-notice.js +27 -0
  67. package/dist/components/workspace-app-layout-notice.js.map +1 -0
  68. package/dist/components/workspace-app-search.d.ts.map +1 -1
  69. package/dist/components/workspace-app-search.js +11 -5
  70. package/dist/components/workspace-app-search.js.map +1 -1
  71. package/dist/components/workspace-template-card.d.ts.map +1 -1
  72. package/dist/components/workspace-template-card.js +7 -2
  73. package/dist/components/workspace-template-card.js.map +1 -1
  74. package/dist/db/index.d.ts.map +1 -1
  75. package/dist/db/index.js +1 -0
  76. package/dist/db/index.js.map +1 -1
  77. package/dist/lib/other-apps.d.ts +10 -1
  78. package/dist/lib/other-apps.d.ts.map +1 -1
  79. package/dist/lib/other-apps.js +7 -1
  80. package/dist/lib/other-apps.js.map +1 -1
  81. package/dist/lib/workspace-app-layout.d.ts +62 -6
  82. package/dist/lib/workspace-app-layout.d.ts.map +1 -1
  83. package/dist/lib/workspace-app-layout.js +196 -41
  84. package/dist/lib/workspace-app-layout.js.map +1 -1
  85. package/dist/lib/workspace-app-lists.d.ts +17 -0
  86. package/dist/lib/workspace-app-lists.d.ts.map +1 -0
  87. package/dist/lib/workspace-app-lists.js +21 -0
  88. package/dist/lib/workspace-app-lists.js.map +1 -0
  89. package/dist/lib/workspace-apps.d.ts +3 -1
  90. package/dist/lib/workspace-apps.d.ts.map +1 -1
  91. package/dist/lib/workspace-apps.js +5 -1
  92. package/dist/lib/workspace-apps.js.map +1 -1
  93. package/dist/routes/pages/apps.d.ts.map +1 -1
  94. package/dist/routes/pages/apps.js +23 -35
  95. package/dist/routes/pages/apps.js.map +1 -1
  96. package/dist/routes/pages/settings-page.d.ts.map +1 -1
  97. package/dist/routes/pages/settings-page.js +24 -3
  98. package/dist/routes/pages/settings-page.js.map +1 -1
  99. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  100. package/dist/server/lib/app-creation-store.js +13 -18
  101. package/dist/server/lib/app-creation-store.js.map +1 -1
  102. package/dist/server/lib/app-roles.d.ts +2 -0
  103. package/dist/server/lib/app-roles.d.ts.map +1 -1
  104. package/dist/server/lib/app-roles.js +26 -7
  105. package/dist/server/lib/app-roles.js.map +1 -1
  106. package/dist/server/lib/hosted-apps-settings-schema.d.ts +6 -0
  107. package/dist/server/lib/hosted-apps-settings-schema.d.ts.map +1 -0
  108. package/dist/server/lib/hosted-apps-settings-schema.js +6 -0
  109. package/dist/server/lib/hosted-apps-settings-schema.js.map +1 -0
  110. package/dist/server/lib/hosted-apps-settings.d.ts +27 -0
  111. package/dist/server/lib/hosted-apps-settings.d.ts.map +1 -0
  112. package/dist/server/lib/hosted-apps-settings.js +72 -0
  113. package/dist/server/lib/hosted-apps-settings.js.map +1 -0
  114. package/dist/server/plugins/agent-chat.js +6 -3
  115. package/dist/server/plugins/agent-chat.js.map +1 -1
  116. package/dist/server/plugins/integrations.js +3 -3
  117. package/dist/server/plugins/integrations.js.map +1 -1
  118. package/dist/shared/hosted-apps-settings.d.ts +3 -0
  119. package/dist/shared/hosted-apps-settings.d.ts.map +1 -0
  120. package/dist/shared/hosted-apps-settings.js +3 -0
  121. package/dist/shared/hosted-apps-settings.js.map +1 -0
  122. package/package.json +4 -4
  123. package/src/actions/get-hosted-apps-settings.ts +29 -0
  124. package/src/actions/hosted-apps-settings.spec.ts +100 -0
  125. package/src/actions/index.ts +4 -0
  126. package/src/actions/list-connected-agents.spec.ts +67 -0
  127. package/src/actions/list-connected-agents.ts +14 -15
  128. package/src/actions/list_apps.spec.ts +16 -0
  129. package/src/actions/list_apps.ts +5 -2
  130. package/src/actions/set-hosted-apps-settings.ts +42 -0
  131. package/src/components/app-card-tiers.tsx +85 -0
  132. package/src/components/app-list-row.spec.tsx +54 -0
  133. package/src/components/app-list-row.tsx +50 -8
  134. package/src/components/app-open-actions.tsx +16 -0
  135. package/src/components/app-pin-button.spec.tsx +68 -0
  136. package/src/components/app-pin-button.tsx +66 -0
  137. package/src/components/app-tier-sections.spec.tsx +155 -0
  138. package/src/components/app-tier-sections.tsx +158 -0
  139. package/src/components/chat-home-apps.spec.tsx +135 -29
  140. package/src/components/chat-home-apps.tsx +115 -111
  141. package/src/components/connected-app-card.spec.tsx +18 -2
  142. package/src/components/connected-app-card.tsx +18 -1
  143. package/src/components/create-app-popover.tsx +22 -0
  144. package/src/components/dispatch-control-plane.spec.tsx +89 -20
  145. package/src/components/dispatch-control-plane.tsx +91 -125
  146. package/src/components/hosted-apps-settings-group.spec.tsx +218 -0
  147. package/src/components/hosted-apps-settings-group.tsx +189 -0
  148. package/src/components/layout/Layout.spec.tsx +356 -49
  149. package/src/components/layout/Layout.tsx +189 -142
  150. package/src/components/workspace-app-card.spec.tsx +33 -29
  151. package/src/components/workspace-app-card.tsx +13 -31
  152. package/src/components/workspace-app-layout-notice.spec.tsx +106 -0
  153. package/src/components/workspace-app-layout-notice.tsx +65 -0
  154. package/src/components/workspace-app-search.spec.tsx +53 -0
  155. package/src/components/workspace-app-search.tsx +39 -32
  156. package/src/components/workspace-template-card.spec.tsx +10 -0
  157. package/src/components/workspace-template-card.tsx +7 -2
  158. package/src/db/index.spec.ts +9 -0
  159. package/src/db/index.ts +1 -0
  160. package/src/lib/other-apps.spec.ts +25 -0
  161. package/src/lib/other-apps.ts +9 -0
  162. package/src/lib/workspace-app-layout.spec.ts +61 -3
  163. package/src/lib/workspace-app-layout.store.spec.tsx +261 -0
  164. package/src/lib/workspace-app-layout.ts +265 -48
  165. package/src/lib/workspace-app-lists.ts +32 -0
  166. package/src/lib/workspace-apps.ts +10 -2
  167. package/src/routes/pages/apps.spec.tsx +133 -5
  168. package/src/routes/pages/apps.tsx +79 -140
  169. package/src/routes/pages/chat.spec.tsx +66 -49
  170. package/src/routes/pages/settings-page.tsx +31 -3
  171. package/src/routes/pages/settings.spec.tsx +37 -0
  172. package/src/server/lib/app-creation-store.spec.ts +14 -0
  173. package/src/server/lib/app-creation-store.ts +16 -29
  174. package/src/server/lib/app-creation-store.write-observation.spec.ts +216 -0
  175. package/src/server/lib/app-roles.spec.ts +48 -1
  176. package/src/server/lib/app-roles.ts +44 -12
  177. package/src/server/lib/hosted-apps-settings-schema.ts +6 -0
  178. package/src/server/lib/hosted-apps-settings.spec.ts +154 -0
  179. package/src/server/lib/hosted-apps-settings.ts +108 -0
  180. package/src/server/plugins/agent-chat.spec.ts +16 -0
  181. package/src/server/plugins/agent-chat.ts +7 -2
  182. package/src/server/plugins/integrations.ts +3 -3
  183. package/src/shared/hosted-apps-settings.ts +3 -0
  184. package/src/styles/dispatch.css +40 -11
  185. package/dist/components/available-apps-section.d.ts +0 -9
  186. package/dist/components/available-apps-section.d.ts.map +0 -1
  187. package/dist/components/available-apps-section.js +0 -97
  188. package/dist/components/available-apps-section.js.map +0 -1
  189. package/dist/components/other-apps-section.d.ts +0 -35
  190. package/dist/components/other-apps-section.d.ts.map +0 -1
  191. package/dist/components/other-apps-section.js +0 -79
  192. package/dist/components/other-apps-section.js.map +0 -1
  193. package/src/components/available-apps-section.tsx +0 -239
  194. package/src/components/other-apps-section.spec.ts +0 -50
  195. package/src/components/other-apps-section.tsx +0 -254
@@ -5,7 +5,6 @@ import {
5
5
  emitChatFirstSessionWatch,
6
6
  getChatFirstSurfaceTabsStore,
7
7
  navigateWithAgentChatViewTransition,
8
- readChatFirstAppLayout,
9
8
  readChatFirstMode,
10
9
  resolveChatFirstBrowserTarget,
11
10
  resolveChatFirstAppTarget,
@@ -51,6 +50,7 @@ import type { ChatFirstPrimaryTab } from "@agent-native/toolkit/app/chat/chat-fi
51
50
  import type {
52
51
  ChatFirstAgentActivity,
53
52
  ChatFirstAppItem,
53
+ ChatFirstAppRailSection,
54
54
  ChatFirstCopy,
55
55
  ChatFirstEmbedTarget,
56
56
  } from "@agent-native/toolkit/app/chat/chat-first/types";
@@ -107,7 +107,12 @@ import {
107
107
  type ConnectedAppSummary,
108
108
  } from "../../lib/other-apps";
109
109
  import { cn } from "../../lib/utils";
110
- import { normalizeWorkspaceAppLayout } from "../../lib/workspace-app-layout";
110
+ import {
111
+ tierWorkspaceApps,
112
+ useWorkspaceAppLayout,
113
+ appListsStatus,
114
+ type WorkspaceAppsStatus,
115
+ } from "../../lib/workspace-app-layout";
111
116
  import {
112
117
  isDispatchWorkspaceAppId,
113
118
  isPathMountedWorkspaceApp,
@@ -153,6 +158,10 @@ import {
153
158
  WorkspaceAppFrame,
154
159
  WorkspaceAppKeepAlive,
155
160
  } from "../workspace-app-host";
161
+ import {
162
+ AppNotice,
163
+ WorkspaceAppLayoutNotice,
164
+ } from "../workspace-app-layout-notice";
156
165
  import { Header } from "./Header";
157
166
  import {
158
167
  HeaderActionsProvider,
@@ -248,9 +257,9 @@ export interface DispatchNavContentProps extends DispatchSidebarBrandProps {
248
257
  collapsible?: boolean;
249
258
  onCollapsedChange?: (collapsed: boolean) => void;
250
259
  chatFirstAppLayout?: ChatFirstAppLayoutPreference;
251
- onChatFirstAppLayoutChange?: (layout: ChatFirstAppLayoutPreference) => void;
260
+ onChatFirstTogglePinned?: (appId: string) => void;
252
261
  chatFirstApps?: readonly ChatFirstAppItem[];
253
- chatFirstAppsLoading?: boolean;
262
+ chatFirstAppsStatus?: WorkspaceAppsStatus;
254
263
  chatFirstAppsError?: string | null;
255
264
  chatFirstActiveAppId?: string;
256
265
  chatFirstActivePrimaryTab?: ChatFirstPrimaryTab;
@@ -267,11 +276,29 @@ export interface DispatchLayoutProps extends DispatchSidebarBrandProps {
267
276
  }
268
277
 
269
278
  const CHROMELESS_PATHS = ["/approval", "/browser-chat", "/browser-connect"];
279
+ // With nothing pinned, the sidebar suggests this many apps; All apps has the rest.
280
+ const CHAT_FIRST_RAIL_SUGGESTED_APPS = 3;
281
+ const NO_CHAT_FIRST_APPS: ChatFirstAppItem[] = [];
270
282
  const SIDEBAR_COLLAPSE_KEY = "dispatch.sidebar.collapsed";
271
283
  const CHAT_HISTORY_SOURCE_KEY = "dispatch.chat-history.source";
272
284
  export const CHAT_FIRST_SURFACE_PANEL_TOGGLE_CLASS_NAME =
273
285
  "absolute right-3 top-2 z-20";
274
286
 
287
+ function appIds(apps: readonly ChatFirstAppItem[]): string[] {
288
+ return apps.map((app) => app.id);
289
+ }
290
+
291
+ function renderChatFirstRailIcon(app: ChatFirstAppItem) {
292
+ return (
293
+ <AppIcon
294
+ id={app.id}
295
+ name={app.name}
296
+ size="sm"
297
+ className="size-7 rounded-lg"
298
+ />
299
+ );
300
+ }
301
+
275
302
  interface DispatchChatFirstPane {
276
303
  appId: string;
277
304
  placement?: ChatFirstAppSurfacePlacement;
@@ -295,8 +322,11 @@ const DispatchExtensionsContext = createContext<
295
322
  DispatchExtensionConfig | undefined
296
323
  >(undefined);
297
324
  interface DispatchWorkspaceAppLauncher {
325
+ /** Empty until `workspaceAppsStatus` is "ready". */
298
326
  workspaceApps: readonly WorkspaceAppSummary[];
299
- isLoading: boolean;
327
+ // Waits for the hosted list too, which is only queried on chat routes and in
328
+ // chat-first mode; elsewhere it stays "loading".
329
+ workspaceAppsStatus: WorkspaceAppsStatus;
300
330
  error?: unknown;
301
331
  errorQueryName?: "list-workspace-apps" | "list-connected-agents";
302
332
  openApp: (app: ChatFirstAppItem) => void;
@@ -447,11 +477,8 @@ const DISPATCH_CHAT_FIRST_COPY_KEYS: Record<string, string> = {
447
477
  noWorkspaceApps: "chatFirstNoWorkspaceApps",
448
478
  createApp: "chatFirstCreateApp",
449
479
  retry: "chatFirstRetry",
450
- dismiss: "chatFirstDismiss",
451
- unpinApp: "chatFirstUnpinApp",
452
- pinApp: "chatFirstPinApp",
453
- removePinned: "chatFirstRemovePinned",
454
- pinTop: "chatFirstPinTop",
480
+ unpinApp: "unpinApp",
481
+ pinApp: "pinApp",
455
482
  openSideSurfaces: "chatFirstOpenSideSurfaces",
456
483
  closeTab: "chatFirstCloseTab",
457
484
  close: "chatFirstClose",
@@ -469,30 +496,11 @@ const DISPATCH_CHAT_FIRST_COPY_KEYS: Record<string, string> = {
469
496
  browserClose: "chatFirstBrowserClose",
470
497
  browserPage: "chatFirstBrowserPage",
471
498
  browserInvalidUrl: "chatFirstBrowserInvalidUrl",
472
- browserPreviewStarting: "chatFirstBrowserPreviewStarting",
473
- browserPreviewError: "chatFirstBrowserPreviewError",
474
499
  appUnavailable: "chatFirstAppUnavailable",
475
500
  appLoading: "chatFirstLoadingApp",
476
- agentActivityEyebrow: "chatFirstAgentActivityEyebrow",
477
- agentActivityTitle: "chatFirstAgentActivityTitle",
478
- agentActivityDescription: "chatFirstAgentActivityDescription",
479
- refreshAgentActivity: "chatFirstRefreshAgentActivity",
480
- noAgentSessions: "chatFirstNoAgentSessions",
481
- startAgentSession: "chatFirstStartAgentSession",
482
501
  watchSession: "chatFirstWatchSession",
483
502
  copySessionId: "chatFirstCopySessionId",
484
- copySessionIdFor: "chatFirstCopySessionIdFor",
485
503
  sessionIdCopied: "chatFirstSessionIdCopied",
486
- sessionIdShort: "chatFirstSessionIdShort",
487
- copied: "chatFirstCopied",
488
- agentActivityStatusQueued: "chatFirstAgentActivityStatusQueued",
489
- agentActivityStatusRunning: "chatFirstAgentActivityStatusRunning",
490
- agentActivityStatusPaused: "chatFirstAgentActivityStatusPaused",
491
- agentActivityStatusNeedsApproval: "chatFirstAgentActivityStatusNeedsApproval",
492
- agentActivityStatusCompleted: "chatFirstAgentActivityStatusCompleted",
493
- agentActivityStatusErrored: "chatFirstAgentActivityStatusErrored",
494
- agentActivityStatusRecent: "chatFirstAgentActivityStatusRecent",
495
- agentActivityStatusUnknown: "chatFirstAgentActivityStatusUnknown",
496
504
  watchingSession: "chatFirstWatchingSession",
497
505
  session: "chatFirstSession",
498
506
  stopWatchingSession: "chatFirstStopWatchingSession",
@@ -550,7 +558,12 @@ function createDispatchChatFirstCopy(
550
558
  }
551
559
  }
552
560
  const translationKey = DISPATCH_CHAT_FIRST_COPY_KEYS[key];
553
- if (!translationKey) return defaultChatFirstCopy(key, values);
561
+ if (!translationKey) {
562
+ return t(`agentChat.chatFirst.${key}`, {
563
+ ...(values ?? {}),
564
+ defaultValue: defaultChatFirstCopy(key, values),
565
+ });
566
+ }
554
567
  return t(`dispatch.pages.${translationKey}`, {
555
568
  ...(values ?? {}),
556
569
  defaultValue: defaultChatFirstCopy(key, values),
@@ -612,7 +625,7 @@ function readChatHistoryIncludesExternal(): boolean {
612
625
  function DispatchChatsSection({
613
626
  onNavigate,
614
627
  showNewChat = false,
615
- prelude,
628
+ appsRail,
616
629
  chatFirstMode = false,
617
630
  chatFirstEmbedded = false,
618
631
  collapsed = false,
@@ -620,7 +633,7 @@ function DispatchChatsSection({
620
633
  }: {
621
634
  onNavigate?: () => void;
622
635
  showNewChat?: boolean;
623
- prelude?: ReactNode;
636
+ appsRail?: ReactNode;
624
637
  chatFirstMode?: boolean;
625
638
  chatFirstEmbedded?: boolean;
626
639
  collapsed?: boolean;
@@ -774,12 +787,33 @@ function DispatchChatsSection({
774
787
  );
775
788
 
776
789
  const collapsedChatFirst = collapsed && chatFirstMode;
790
+ const showChatFirstHistory =
791
+ !collapsedChatFirst &&
792
+ chatFirstMode &&
793
+ (history.loading ||
794
+ history.sections.some(
795
+ (section) => section.items.length > 0 || section.error != null,
796
+ ));
797
+ // The source toggle filters every section, so it sits in the first header.
798
+ const chatFirstHistorySections = history.sections.map((section, index) =>
799
+ index === 0
800
+ ? {
801
+ ...section,
802
+ label: (
803
+ <div className="flex flex-1 items-center justify-between gap-2">
804
+ <span>{section.label}</span>
805
+ {sourceToggle}
806
+ </div>
807
+ ),
808
+ }
809
+ : section,
810
+ );
777
811
 
778
812
  return (
779
813
  <div
780
814
  className={cn(
781
- "min-w-0 space-y-0.5",
782
- showNewChat && "dispatch-chat-first-chats",
815
+ "min-w-0",
816
+ showNewChat ? "dispatch-chat-first-chats" : "space-y-0.5",
783
817
  )}
784
818
  >
785
819
  {showNewChat && chatFirstNavigation ? (
@@ -798,18 +832,12 @@ function DispatchChatsSection({
798
832
  />
799
833
  </nav>
800
834
  ) : null}
801
- {prelude}
802
- {!collapsedChatFirst ? (
835
+ {!collapsedChatFirst && !showChatFirstHistory ? (
803
836
  <div className="flex justify-end px-2 pt-0.5">{sourceToggle}</div>
804
837
  ) : null}
805
- {!collapsedChatFirst &&
806
- chatFirstMode &&
807
- (history.loading ||
808
- history.sections.some(
809
- (section) => section.items.length > 0 || section.error,
810
- )) ? (
838
+ {showChatFirstHistory ? (
811
839
  <ChatFirstChatHistory
812
- sections={history.sections}
840
+ sections={chatFirstHistorySections}
813
841
  {...history.listProps}
814
842
  onDelete={listProps.onDelete}
815
843
  onNewChat={() => void handleNewChat()}
@@ -925,6 +953,7 @@ function DispatchChatsSection({
925
953
  className="min-w-0 px-2"
926
954
  />
927
955
  ) : null}
956
+ {appsRail}
928
957
  </div>
929
958
  );
930
959
  }
@@ -940,9 +969,9 @@ export function NavContent({
940
969
  collapsible = false,
941
970
  onCollapsedChange,
942
971
  chatFirstAppLayout,
943
- onChatFirstAppLayoutChange,
944
- chatFirstApps = [],
945
- chatFirstAppsLoading = false,
972
+ onChatFirstTogglePinned,
973
+ chatFirstApps = NO_CHAT_FIRST_APPS,
974
+ chatFirstAppsStatus = "ready",
946
975
  chatFirstAppsError,
947
976
  chatFirstActiveAppId,
948
977
  chatFirstActivePrimaryTab,
@@ -998,51 +1027,105 @@ export function NavContent({
998
1027
  const feedbackButton = (
999
1028
  <FeedbackButton variant={collapsed ? "icon" : "sidebar"} side="right" />
1000
1029
  );
1001
- const chatFirstCreateAppTrigger = (
1002
- <CreateAppPopover
1003
- align="start"
1004
- trigger={
1005
- <button
1006
- type="button"
1007
- className="flex size-6 items-center justify-center rounded text-sidebar-foreground/55 hover:bg-sidebar-accent hover:text-sidebar-foreground"
1008
- aria-label={chatFirstCopy("createWorkspaceApp")}
1009
- title={chatFirstCopy("createWorkspaceApp")}
1010
- >
1011
- <IconPlus size={14} aria-hidden="true" />
1012
- </button>
1013
- }
1014
- />
1030
+ const chatFirstCreateAppTrigger = useMemo(
1031
+ () => (
1032
+ <CreateAppPopover
1033
+ align="start"
1034
+ trigger={
1035
+ <button
1036
+ type="button"
1037
+ className="flex size-6 items-center justify-center rounded text-sidebar-foreground/55 hover:bg-sidebar-accent hover:text-sidebar-foreground"
1038
+ aria-label={chatFirstCopy("createWorkspaceApp")}
1039
+ title={chatFirstCopy("createWorkspaceApp")}
1040
+ >
1041
+ <IconPlus size={14} aria-hidden="true" />
1042
+ </button>
1043
+ }
1044
+ />
1045
+ ),
1046
+ [chatFirstCopy],
1015
1047
  );
1016
1048
 
1049
+ const chatFirstAppTiers = useMemo(
1050
+ () =>
1051
+ tierWorkspaceApps(
1052
+ chatFirstApps,
1053
+ chatFirstAppLayout ?? { pinnedIds: [], orderedIds: [] },
1054
+ ),
1055
+ [chatFirstAppLayout, chatFirstApps],
1056
+ );
1057
+ // One section: the user's pins, or until they pin something, a few apps
1058
+ // (workspace apps first, else hosted ones). The open app always shows.
1059
+ const chatFirstAppSections = useMemo<ChatFirstAppRailSection[]>(() => {
1060
+ const { pinned, apps, hosted } = chatFirstAppTiers;
1061
+ const ids = appIds(
1062
+ pinned.length > 0
1063
+ ? pinned
1064
+ : (apps.length > 0 ? apps : hosted).slice(
1065
+ 0,
1066
+ CHAT_FIRST_RAIL_SUGGESTED_APPS,
1067
+ ),
1068
+ );
1069
+ if (
1070
+ chatFirstActiveAppId &&
1071
+ !ids.includes(chatFirstActiveAppId) &&
1072
+ chatFirstApps.some((app) => app.id === chatFirstActiveAppId)
1073
+ ) {
1074
+ ids.push(chatFirstActiveAppId);
1075
+ }
1076
+ return [
1077
+ {
1078
+ id: pinned.length > 0 ? "pinned" : "apps",
1079
+ label:
1080
+ pinned.length > 0
1081
+ ? t("dispatch.pages.pinnedApps")
1082
+ : t("dispatch.nav.apps"),
1083
+ appIds: ids,
1084
+ action: chatFirstCreateAppTrigger,
1085
+ ...(chatFirstAppsStatus === "ready" && ids.length === 0
1086
+ ? { emptyLabel: chatFirstCopy("noWorkspaceApps") }
1087
+ : {}),
1088
+ },
1089
+ ];
1090
+ }, [
1091
+ chatFirstActiveAppId,
1092
+ chatFirstAppTiers,
1093
+ chatFirstApps,
1094
+ chatFirstAppsStatus,
1095
+ chatFirstCopy,
1096
+ chatFirstCreateAppTrigger,
1097
+ t,
1098
+ ]);
1099
+
1100
+ const openChatFirstRailApp = useCallback(
1101
+ (app: ChatFirstAppItem) => {
1102
+ onChatFirstAppOpen?.(app);
1103
+ onNavigate?.();
1104
+ },
1105
+ [onChatFirstAppOpen, onNavigate],
1106
+ );
1107
+ const openChatFirstAllApps = useCallback(() => {
1108
+ void navigate(dispatchNavLinkTarget("/apps"));
1109
+ onNavigate?.();
1110
+ }, [navigate, onNavigate]);
1111
+
1017
1112
  const chatFirstAppsRail = (
1018
1113
  <ChatFirstAppsRail
1019
1114
  apps={chatFirstApps}
1115
+ sections={chatFirstAppSections}
1020
1116
  activeAppId={chatFirstActiveAppId}
1021
1117
  activeTab={chatFirstActivePrimaryTab}
1022
1118
  collapsed={collapsed}
1023
1119
  grayscaleInactiveIcons={false}
1024
- loading={chatFirstAppsLoading}
1120
+ loading={chatFirstAppsStatus === "loading"}
1025
1121
  error={chatFirstAppsError}
1026
1122
  layout={chatFirstAppLayout}
1027
- onLayoutChange={onChatFirstAppLayoutChange}
1123
+ onTogglePinned={onChatFirstTogglePinned}
1028
1124
  onRetry={onChatFirstAppsRetry}
1029
- onOpenApp={(app) => {
1030
- onChatFirstAppOpen?.(app);
1031
- onNavigate?.();
1032
- }}
1033
- onOpenAllApps={() => {
1034
- void navigate(dispatchNavLinkTarget("/apps"));
1035
- onNavigate?.();
1036
- }}
1125
+ onOpenApp={openChatFirstRailApp}
1126
+ onOpenAllApps={openChatFirstAllApps}
1037
1127
  createAppTrigger={chatFirstCreateAppTrigger}
1038
- renderIcon={(app) => (
1039
- <AppIcon
1040
- id={app.id}
1041
- name={app.name}
1042
- size="sm"
1043
- className="size-7 rounded-lg"
1044
- />
1045
- )}
1128
+ renderIcon={renderChatFirstRailIcon}
1046
1129
  copy={chatFirstCopy}
1047
1130
  />
1048
1131
  );
@@ -1185,7 +1268,7 @@ export function NavContent({
1185
1268
  onNavigate?.();
1186
1269
  },
1187
1270
  }}
1188
- prelude={chatFirstAppsRail}
1271
+ appsRail={chatFirstAppsRail}
1189
1272
  />
1190
1273
  </div>
1191
1274
  ) : null}
@@ -1357,11 +1440,10 @@ function DispatchLayout({
1357
1440
  `${url.pathname}${url.search}${url.hash}`,
1358
1441
  );
1359
1442
  }, [electronEmbedded, location.pathname, location.search]);
1360
- const [chatFirstAppLayout, setChatFirstAppLayout] =
1361
- useState<ChatFirstAppLayoutPreference>(() =>
1362
- normalizeWorkspaceAppLayout(readChatFirstAppLayout()),
1363
- );
1364
- const chatFirstAppLayoutHydratedRef = useRef(false);
1443
+ const { layout: chatFirstAppLayout, togglePinned: toggleChatFirstAppPinned } =
1444
+ useWorkspaceAppLayout({
1445
+ hydrate: chatFirstMode,
1446
+ });
1365
1447
  const chatFirstAppsQuery = useActionQuery<WorkspaceAppSummary[]>(
1366
1448
  "list-workspace-apps",
1367
1449
  { includeAgentCards: false, includeArchived: true },
@@ -1381,6 +1463,7 @@ function DispatchLayout({
1381
1463
  filterBuiltInApps(
1382
1464
  chatFirstConnectedAppsQuery.data ?? [],
1383
1465
  chatFirstAppsQuery.data ?? [],
1466
+ { includeHiddenFromLaunchers: true },
1384
1467
  ),
1385
1468
  [chatFirstAppsQuery.data, chatFirstConnectedAppsQuery.data],
1386
1469
  );
@@ -1390,8 +1473,6 @@ function DispatchLayout({
1390
1473
  );
1391
1474
  const chatFirstWorkspaceAppsLoading = chatFirstAppsQuery.isLoading;
1392
1475
  const chatFirstConnectedAppsLoading = chatFirstConnectedAppsQuery.isLoading;
1393
- const chatFirstAppsLoading =
1394
- chatFirstWorkspaceAppsLoading || chatFirstConnectedAppsLoading;
1395
1476
  const chatFirstAppsFailed = chatFirstAppsQuery.isError;
1396
1477
  const chatFirstAppsError =
1397
1478
  chatFirstAppsQuery.error ?? chatFirstConnectedAppsQuery.error;
@@ -1401,6 +1482,10 @@ function DispatchLayout({
1401
1482
  : chatFirstConnectedAppsQuery.error != null
1402
1483
  ? "list-connected-agents"
1403
1484
  : undefined;
1485
+ const chatFirstWorkspaceAppsStatus = appListsStatus(
1486
+ chatFirstAppsQuery,
1487
+ chatFirstConnectedAppsQuery,
1488
+ );
1404
1489
  const chatFirstResolvableApps = useMemo(
1405
1490
  () =>
1406
1491
  mergeChatFirstWorkspaceApps(
@@ -1412,6 +1497,7 @@ function DispatchLayout({
1412
1497
  description: app.description,
1413
1498
  url: app.homeUrl?.trim() || app.url,
1414
1499
  source: "builtin" as const,
1500
+ hiddenFromLaunchers: app.hiddenFromLaunchers,
1415
1501
  })),
1416
1502
  ).filter((app) => app.status !== "pending" && app.archived !== true),
1417
1503
  [
@@ -1422,8 +1508,12 @@ function DispatchLayout({
1422
1508
  );
1423
1509
  const chatFirstWorkspaceApps = useMemo(
1424
1510
  () =>
1425
- chatFirstResolvableApps.filter(isWorkspaceAppVisibleInDefaultLaunchers),
1426
- [chatFirstResolvableApps],
1511
+ chatFirstWorkspaceAppsStatus === "ready"
1512
+ ? chatFirstResolvableApps.filter(
1513
+ isWorkspaceAppVisibleInDefaultLaunchers,
1514
+ )
1515
+ : [],
1516
+ [chatFirstResolvableApps, chatFirstWorkspaceAppsStatus],
1427
1517
  );
1428
1518
  const chatFirstAppRegistrations = useMemo<ChatFirstAppRegistration[]>(() => {
1429
1519
  return chatFirstResolvableApps.map((app) => ({
@@ -1486,7 +1576,7 @@ function DispatchLayout({
1486
1576
  const chatHomeAppLauncher = useMemo<DispatchWorkspaceAppLauncher>(
1487
1577
  () => ({
1488
1578
  workspaceApps: chatFirstWorkspaceApps,
1489
- isLoading: chatFirstAppsLoading,
1579
+ workspaceAppsStatus: chatFirstWorkspaceAppsStatus,
1490
1580
  error: chatFirstAppsError,
1491
1581
  errorQueryName: chatFirstAppsErrorQueryName,
1492
1582
  openApp: openChatFirstApp,
@@ -1498,10 +1588,10 @@ function DispatchLayout({
1498
1588
  [
1499
1589
  chatFirstAppsError,
1500
1590
  chatFirstAppsErrorQueryName,
1501
- chatFirstAppsLoading,
1502
1591
  chatFirstAppsQuery.refetch,
1503
1592
  chatFirstConnectedAppsQuery.refetch,
1504
1593
  chatFirstWorkspaceApps,
1594
+ chatFirstWorkspaceAppsStatus,
1505
1595
  openChatFirstApp,
1506
1596
  ],
1507
1597
  );
@@ -1787,40 +1877,6 @@ function DispatchLayout({
1787
1877
  );
1788
1878
  }, []);
1789
1879
 
1790
- useEffect(() => {
1791
- if (
1792
- !chatFirstMode ||
1793
- !isChatRoute ||
1794
- chatFirstAppLayoutHydratedRef.current
1795
- ) {
1796
- return;
1797
- }
1798
- chatFirstAppLayoutHydratedRef.current = true;
1799
- void readClientAppState<unknown>("chat-first-app-layout")
1800
- .then((value) => {
1801
- if (!value || typeof value !== "object") return;
1802
- setChatFirstAppLayout(normalizeWorkspaceAppLayout(value));
1803
- })
1804
- .catch(() => {
1805
- // Device-local layout remains the fallback when workspace state is unavailable.
1806
- });
1807
- }, [chatFirstMode, isChatRoute]);
1808
-
1809
- const persistChatFirstAppLayout = useCallback(
1810
- (layout: ChatFirstAppLayoutPreference) => {
1811
- const normalizedLayout = normalizeWorkspaceAppLayout(layout);
1812
- setChatFirstAppLayout(normalizedLayout);
1813
- void writeClientAppState("chat-first-app-layout", normalizedLayout).catch(
1814
- () => {
1815
- setChatFirstNotice(
1816
- "App order changed locally, but workspace state could not be synced.",
1817
- );
1818
- },
1819
- );
1820
- },
1821
- [],
1822
- );
1823
-
1824
1880
  useEffect(() => {
1825
1881
  if (!chatFirstMode) {
1826
1882
  setChatFirstPane(null);
@@ -2313,6 +2369,7 @@ function DispatchLayout({
2313
2369
  <div className="relative flex min-w-0 flex-1 flex-col overflow-hidden">
2314
2370
  <Header showAgentToggle={false} />
2315
2371
  {notice}
2372
+ <WorkspaceAppLayoutNotice />
2316
2373
  <InvitationBanner />
2317
2374
  <main className="relative flex min-h-0 flex-1 overflow-hidden">
2318
2375
  <div className="min-w-0 flex-1 overflow-y-auto">
@@ -2394,22 +2451,12 @@ function DispatchLayout({
2394
2451
  const appContent = (
2395
2452
  <div className="relative flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
2396
2453
  {notice}
2454
+ <WorkspaceAppLayoutNotice />
2397
2455
  <InvitationBanner />
2398
2456
  {isChatRoute && chatFirstMode && chatFirstNotice ? (
2399
- <div
2400
- className="dispatch-chat-first-notice"
2401
- role="status"
2402
- aria-live="polite"
2403
- >
2404
- <span>{chatFirstNotice}</span>
2405
- <button
2406
- type="button"
2407
- onClick={() => setChatFirstNotice(null)}
2408
- aria-label="Dismiss app notice"
2409
- >
2410
- Dismiss
2411
- </button>
2412
- </div>
2457
+ <AppNotice onDismiss={() => setChatFirstNotice(null)}>
2458
+ {chatFirstNotice}
2459
+ </AppNotice>
2413
2460
  ) : null}
2414
2461
  <main
2415
2462
  className={cn(
@@ -2551,9 +2598,9 @@ function DispatchLayout({
2551
2598
  chatFirstEmbedded={chatFirstEmbedded}
2552
2599
  collapsed={sidebarCollapsed}
2553
2600
  chatFirstAppLayout={chatFirstAppLayout}
2554
- onChatFirstAppLayoutChange={persistChatFirstAppLayout}
2601
+ onChatFirstTogglePinned={toggleChatFirstAppPinned}
2555
2602
  chatFirstApps={chatFirstAppItems}
2556
- chatFirstAppsLoading={chatFirstAppsLoading}
2603
+ chatFirstAppsStatus={chatFirstWorkspaceAppsStatus}
2557
2604
  chatFirstAppsError={
2558
2605
  chatFirstAppsError ? chatFirstCopy("appsLoadError") : null
2559
2606
  }
@@ -2599,9 +2646,9 @@ function DispatchLayout({
2599
2646
  chatFirstEmbedded={chatFirstEmbedded}
2600
2647
  collapsed={false}
2601
2648
  chatFirstAppLayout={chatFirstAppLayout}
2602
- onChatFirstAppLayoutChange={persistChatFirstAppLayout}
2649
+ onChatFirstTogglePinned={toggleChatFirstAppPinned}
2603
2650
  chatFirstApps={chatFirstAppItems}
2604
- chatFirstAppsLoading={chatFirstAppsLoading}
2651
+ chatFirstAppsStatus={chatFirstWorkspaceAppsStatus}
2605
2652
  chatFirstAppsError={
2606
2653
  chatFirstAppsError ? chatFirstCopy("appsLoadError") : null
2607
2654
  }
@@ -341,29 +341,27 @@ describe("WorkspaceAppCard", () => {
341
341
  ".app-open-actions__primary",
342
342
  );
343
343
  expect(openButton).not.toBeNull();
344
- expect(openButton?.textContent).toContain("Open app");
344
+ expect(openButton?.textContent).toContain("Open");
345
345
  expect(container.querySelector('a[href="/analytics"]')).toBeNull();
346
346
 
347
- expect(
348
- container.querySelector(
349
- 'button[aria-label="Open options for Analytics"]',
350
- ),
351
- ).not.toBeNull();
352
- expect(document.body.textContent).not.toContain("Open in new tab");
347
+ expect(container.querySelector(".app-open-actions__menu")).not.toBeNull();
348
+ expect(document.body.textContent).not.toContain(
349
+ "dispatch.pages.chatFirstOpenInNewTab",
350
+ );
353
351
  expect(document.body.textContent).not.toContain("Add app");
354
352
 
355
353
  await act(async () => {
356
354
  container
357
- .querySelector<HTMLButtonElement>(
358
- 'button[aria-label="Open options for Analytics"]',
359
- )
355
+ .querySelector<HTMLButtonElement>(".app-open-actions__menu")
360
356
  ?.dispatchEvent(
361
357
  new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
362
358
  );
363
359
  });
364
360
  const newTabItem = Array.from(
365
361
  document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
366
- ).find((item) => item.textContent?.includes("Open in new tab"));
362
+ ).find((item) =>
363
+ item.textContent?.includes("dispatch.pages.chatFirstOpenInNewTab"),
364
+ );
367
365
  expect(newTabItem).not.toBeUndefined();
368
366
  expect(newTabItem?.getAttribute("href")).toBe(
369
367
  "https://analytics.agent-native.com/home",
@@ -431,7 +429,7 @@ describe("WorkspaceAppCard", () => {
431
429
  ".app-open-actions__primary",
432
430
  );
433
431
  expect(openButton).not.toBeNull();
434
- expect(openButton?.textContent).toContain("Open app");
432
+ expect(openButton?.textContent).toContain("Open");
435
433
  expect(
436
434
  container.querySelector(
437
435
  'a[href="https://agent-workspace.builder.io/feedback-leaderboard/leaderboard"]',
@@ -686,7 +684,7 @@ describe("WorkspaceAppCard", () => {
686
684
  expect(document.activeElement).toBe(settingsButton);
687
685
  });
688
686
 
689
- it("keeps pinning in the app open menu", async () => {
687
+ it("pins from a pressed button beside Open, not from the open menu", async () => {
690
688
  const onTogglePinned = vi.fn();
691
689
  await act(async () => {
692
690
  root.render(
@@ -707,24 +705,32 @@ describe("WorkspaceAppCard", () => {
707
705
  );
708
706
  });
709
707
 
710
- const optionsButton = container.querySelector<HTMLButtonElement>(
711
- 'button[aria-label="Open options for Analytics"]',
708
+ const pinButton = container.querySelector<HTMLButtonElement>(
709
+ 'button[aria-label="dispatch.pages.unpinApp"]',
712
710
  );
713
- expect(optionsButton).not.toBeNull();
711
+ expect(pinButton?.getAttribute("aria-pressed")).toBe("true");
712
+ // The button stays visible while this is set, and on hover otherwise.
713
+ expect(pinButton?.getAttribute("data-pinned")).toBe("true");
714
+ const openActions = container.querySelector(".app-open-actions");
715
+ expect(
716
+ pinButton!.compareDocumentPosition(openActions!) &
717
+ Node.DOCUMENT_POSITION_FOLLOWING,
718
+ ).toBeTruthy();
719
+ await act(async () => pinButton?.click());
720
+ expect(onTogglePinned).toHaveBeenCalledOnce();
714
721
 
715
722
  await act(async () => {
716
- optionsButton?.dispatchEvent(
717
- new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
718
- );
723
+ container
724
+ .querySelector<HTMLButtonElement>(".app-open-actions__menu")
725
+ ?.dispatchEvent(
726
+ new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
727
+ );
719
728
  });
720
-
721
- const pinItem = Array.from(
729
+ const menuItems = Array.from(
722
730
  document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
723
- ).find((item) => item.textContent?.includes("Unpin this app"));
724
- expect(pinItem).not.toBeUndefined();
725
-
726
- await act(async () => pinItem?.click());
727
- expect(onTogglePinned).toHaveBeenCalledOnce();
731
+ ).map((item) => item.textContent);
732
+ expect(menuItems.length).toBeGreaterThan(0);
733
+ expect(menuItems.join(" ")).not.toMatch(/pin/i);
728
734
  });
729
735
 
730
736
  it("labels per-app context as agent resources while preserving workspace scope", async () => {
@@ -808,9 +814,7 @@ describe("WorkspaceAppCard", () => {
808
814
  expect(container.textContent).toContain("Open in Builder");
809
815
  expect(container.textContent).not.toContain("pending-branch-id");
810
816
  expect(container.querySelector(".app-open-actions")).toBeNull();
811
- expect(
812
- container.querySelector('button[aria-label="Open options for New app"]'),
813
- ).toBeNull();
817
+ expect(container.querySelector(".app-open-actions__menu")).toBeNull();
814
818
  });
815
819
 
816
820
  it("shows ownership metadata in the settings menu", async () => {