@agent-native/dispatch 0.24.0 → 0.24.2

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 (161) hide show
  1. package/dist/actions/provider-api-register.d.ts +12 -12
  2. package/dist/components/admin-navigation.d.ts.map +1 -1
  3. package/dist/components/admin-navigation.js +21 -5
  4. package/dist/components/admin-navigation.js.map +1 -1
  5. package/dist/components/app-list-row.d.ts +1 -1
  6. package/dist/components/app-list-row.d.ts.map +1 -1
  7. package/dist/components/app-list-row.js +1 -1
  8. package/dist/components/app-list-row.js.map +1 -1
  9. package/dist/components/app-open-actions.d.ts +1 -17
  10. package/dist/components/app-open-actions.d.ts.map +1 -1
  11. package/dist/components/app-open-actions.js +1 -20
  12. package/dist/components/app-open-actions.js.map +1 -1
  13. package/dist/components/app-row-settings.d.ts +6 -0
  14. package/dist/components/app-row-settings.d.ts.map +1 -0
  15. package/dist/components/app-row-settings.js +11 -0
  16. package/dist/components/app-row-settings.js.map +1 -0
  17. package/dist/components/automation-run-history-dialog.d.ts.map +1 -1
  18. package/dist/components/automation-run-history-dialog.js +3 -2
  19. package/dist/components/automation-run-history-dialog.js.map +1 -1
  20. package/dist/components/connected-app-card.d.ts.map +1 -1
  21. package/dist/components/connected-app-card.js +1 -1
  22. package/dist/components/connected-app-card.js.map +1 -1
  23. package/dist/components/create-app-popover.js +2 -2
  24. package/dist/components/create-app-popover.js.map +1 -1
  25. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  26. package/dist/components/dispatch-control-plane.js +23 -7
  27. package/dist/components/dispatch-control-plane.js.map +1 -1
  28. package/dist/components/dispatch-shell.d.ts.map +1 -1
  29. package/dist/components/dispatch-shell.js +5 -1
  30. package/dist/components/dispatch-shell.js.map +1 -1
  31. package/dist/components/docs-link.d.ts +16 -0
  32. package/dist/components/docs-link.d.ts.map +1 -0
  33. package/dist/components/docs-link.js +34 -0
  34. package/dist/components/docs-link.js.map +1 -0
  35. package/dist/components/layout/Layout.d.ts +1 -0
  36. package/dist/components/layout/Layout.d.ts.map +1 -1
  37. package/dist/components/layout/Layout.js +37 -28
  38. package/dist/components/layout/Layout.js.map +1 -1
  39. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  40. package/dist/components/messaging-setup-panel.js +3 -2
  41. package/dist/components/messaging-setup-panel.js.map +1 -1
  42. package/dist/components/usage-alerts-panel.d.ts +11 -0
  43. package/dist/components/usage-alerts-panel.d.ts.map +1 -0
  44. package/dist/components/usage-alerts-panel.js +125 -0
  45. package/dist/components/usage-alerts-panel.js.map +1 -0
  46. package/dist/components/workspace-app-card.d.ts +3 -1
  47. package/dist/components/workspace-app-card.d.ts.map +1 -1
  48. package/dist/components/workspace-app-card.js +70 -29
  49. package/dist/components/workspace-app-card.js.map +1 -1
  50. package/dist/components/workspace-app-search.d.ts +10 -0
  51. package/dist/components/workspace-app-search.d.ts.map +1 -0
  52. package/dist/components/workspace-app-search.js +27 -0
  53. package/dist/components/workspace-app-search.js.map +1 -0
  54. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
  55. package/dist/components/workspace-resource-effective-stack.js +3 -2
  56. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  57. package/dist/components/workspace-template-card.d.ts.map +1 -1
  58. package/dist/components/workspace-template-card.js +2 -1
  59. package/dist/components/workspace-template-card.js.map +1 -1
  60. package/dist/lib/workspace-app-layout.d.ts +19 -0
  61. package/dist/lib/workspace-app-layout.d.ts.map +1 -0
  62. package/dist/lib/workspace-app-layout.js +75 -0
  63. package/dist/lib/workspace-app-layout.js.map +1 -0
  64. package/dist/lib/workspace-apps.d.ts +10 -0
  65. package/dist/lib/workspace-apps.d.ts.map +1 -1
  66. package/dist/lib/workspace-apps.js +59 -0
  67. package/dist/lib/workspace-apps.js.map +1 -1
  68. package/dist/lib/workspace-apps.test.js +18 -1
  69. package/dist/lib/workspace-apps.test.js.map +1 -1
  70. package/dist/routes/pages/$appId.d.ts.map +1 -1
  71. package/dist/routes/pages/$appId.js +4 -2
  72. package/dist/routes/pages/$appId.js.map +1 -1
  73. package/dist/routes/pages/agents.d.ts.map +1 -1
  74. package/dist/routes/pages/agents.js +2 -3
  75. package/dist/routes/pages/agents.js.map +1 -1
  76. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  77. package/dist/routes/pages/apps.$appId.js +8 -3
  78. package/dist/routes/pages/apps.$appId.js.map +1 -1
  79. package/dist/routes/pages/apps.d.ts.map +1 -1
  80. package/dist/routes/pages/apps.js +31 -16
  81. package/dist/routes/pages/apps.js.map +1 -1
  82. package/dist/routes/pages/audit.d.ts.map +1 -1
  83. package/dist/routes/pages/audit.js +2 -1
  84. package/dist/routes/pages/audit.js.map +1 -1
  85. package/dist/routes/pages/dreams.d.ts.map +1 -1
  86. package/dist/routes/pages/dreams.js +14 -18
  87. package/dist/routes/pages/dreams.js.map +1 -1
  88. package/dist/routes/pages/integrations.d.ts.map +1 -1
  89. package/dist/routes/pages/integrations.js +51 -14
  90. package/dist/routes/pages/integrations.js.map +1 -1
  91. package/dist/routes/pages/metrics.d.ts.map +1 -1
  92. package/dist/routes/pages/metrics.js +4 -1
  93. package/dist/routes/pages/metrics.js.map +1 -1
  94. package/dist/routes/pages/operations.js +1 -1
  95. package/dist/routes/pages/operations.js.map +1 -1
  96. package/dist/routes/pages/settings.d.ts.map +1 -1
  97. package/dist/routes/pages/settings.js +7 -2
  98. package/dist/routes/pages/settings.js.map +1 -1
  99. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  100. package/dist/routes/pages/thread-debug.js +5 -8
  101. package/dist/routes/pages/thread-debug.js.map +1 -1
  102. package/dist/routes/pages/vault.d.ts.map +1 -1
  103. package/dist/routes/pages/vault.js +1 -1
  104. package/dist/routes/pages/vault.js.map +1 -1
  105. package/dist/routes/pages/workspace.d.ts.map +1 -1
  106. package/dist/routes/pages/workspace.js +13 -10
  107. package/dist/routes/pages/workspace.js.map +1 -1
  108. package/dist/server/index.d.ts.map +1 -1
  109. package/dist/server/index.js +3 -0
  110. package/dist/server/index.js.map +1 -1
  111. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  112. package/dist/server/lib/app-creation-store.js +12 -52
  113. package/dist/server/lib/app-creation-store.js.map +1 -1
  114. package/dist/shared/app-roles.d.ts +9 -0
  115. package/dist/shared/app-roles.d.ts.map +1 -0
  116. package/dist/shared/app-roles.js +7 -0
  117. package/dist/shared/app-roles.js.map +1 -0
  118. package/package.json +4 -3
  119. package/src/components/admin-navigation.tsx +57 -11
  120. package/src/components/app-list-row.tsx +1 -2
  121. package/src/components/app-open-actions.tsx +5 -114
  122. package/src/components/app-row-settings.tsx +54 -0
  123. package/src/components/automation-run-history-dialog.tsx +8 -4
  124. package/src/components/connected-app-card.tsx +0 -1
  125. package/src/components/create-app-popover.spec.tsx +1 -1
  126. package/src/components/create-app-popover.tsx +2 -2
  127. package/src/components/dispatch-control-plane.spec.tsx +36 -2
  128. package/src/components/dispatch-control-plane.tsx +83 -27
  129. package/src/components/dispatch-shell.tsx +7 -1
  130. package/src/components/docs-link.tsx +52 -0
  131. package/src/components/layout/Layout.spec.tsx +10 -0
  132. package/src/components/layout/Layout.tsx +50 -23
  133. package/src/components/messaging-setup-panel.tsx +10 -3
  134. package/src/components/usage-alerts-panel.tsx +445 -0
  135. package/src/components/workspace-app-card.spec.tsx +130 -9
  136. package/src/components/workspace-app-card.tsx +128 -71
  137. package/src/components/workspace-app-search.tsx +96 -0
  138. package/src/components/workspace-resource-effective-stack.tsx +10 -5
  139. package/src/components/workspace-template-card.spec.tsx +52 -23
  140. package/src/components/workspace-template-card.tsx +7 -10
  141. package/src/lib/workspace-app-layout.spec.ts +47 -0
  142. package/src/lib/workspace-app-layout.ts +121 -0
  143. package/src/lib/workspace-apps.test.ts +45 -1
  144. package/src/lib/workspace-apps.ts +72 -0
  145. package/src/routes/pages/$appId.tsx +3 -6
  146. package/src/routes/pages/agents.tsx +6 -3
  147. package/src/routes/pages/apps.$appId.tsx +6 -6
  148. package/src/routes/pages/apps.tsx +116 -62
  149. package/src/routes/pages/audit.tsx +28 -8
  150. package/src/routes/pages/dreams.tsx +92 -142
  151. package/src/routes/pages/integrations.tsx +147 -68
  152. package/src/routes/pages/metrics.tsx +8 -0
  153. package/src/routes/pages/operations.tsx +2 -2
  154. package/src/routes/pages/settings.tsx +14 -9
  155. package/src/routes/pages/thread-debug.tsx +18 -36
  156. package/src/routes/pages/vault.tsx +7 -2
  157. package/src/routes/pages/workspace.tsx +92 -56
  158. package/src/server/index.ts +4 -0
  159. package/src/server/lib/app-creation-store.spec.ts +90 -43
  160. package/src/server/lib/app-creation-store.ts +15 -61
  161. package/src/shared/app-roles.ts +8 -0
@@ -12,6 +12,11 @@ import { Link, useNavigate } from "react-router";
12
12
 
13
13
  import type { ConnectedAppSummary } from "../lib/other-apps";
14
14
  import { cn } from "../lib/utils";
15
+ import {
16
+ orderWorkspaceApps,
17
+ useWorkspaceAppLayout,
18
+ workspaceAppMatchesQuery,
19
+ } from "../lib/workspace-app-layout";
15
20
  import {
16
21
  isWorkspaceAppVisibleInDefaultLaunchers,
17
22
  type WorkspaceAppSummary,
@@ -33,6 +38,10 @@ import {
33
38
  } from "./ui/collapsible";
34
39
  import { Skeleton } from "./ui/skeleton";
35
40
  import { WorkspaceAppCard } from "./workspace-app-card";
41
+ import {
42
+ WorkspaceAppSearch,
43
+ WorkspaceAppSearchEmpty,
44
+ } from "./workspace-app-search";
36
45
  import type { CuratedWorkspaceTemplatesResult } from "./workspace-template-card";
37
46
 
38
47
  function SectionHeader({
@@ -65,9 +74,6 @@ function CommandPanel() {
65
74
  } = useChatModels({ storageKey: "dispatch" });
66
75
  const navigate = useNavigate();
67
76
  const promptSuggestions = [
68
- t("dispatch.pages.suggestionWorkspaceHealth", {
69
- defaultValue: "Summarize the current workspace health",
70
- }),
71
77
  t("dispatch.pages.suggestionOnboardingApp", {
72
78
  defaultValue: "Create an app for onboarding requests",
73
79
  }),
@@ -165,11 +171,23 @@ function AppsPanel({
165
171
  }) {
166
172
  const t = useT();
167
173
  const [showPending, setShowPending] = useState(false);
174
+ const [searchQuery, setSearchQuery] = useState("");
175
+ const { layout, persistenceError, togglePinned } = useWorkspaceAppLayout();
168
176
  const visibleApps = apps.filter(
169
177
  (app) => isWorkspaceAppVisibleInDefaultLaunchers(app) && !app.archived,
170
178
  );
171
179
  const activeApps = visibleApps.filter((app) => app.status !== "pending");
172
180
  const pendingApps = visibleApps.filter((app) => app.status === "pending");
181
+ const orderedActiveApps = orderWorkspaceApps(activeApps, layout);
182
+ const orderedPendingApps = orderWorkspaceApps(pendingApps, layout);
183
+ const filteredActiveApps = orderedActiveApps.filter((app) =>
184
+ workspaceAppMatchesQuery(app, searchQuery),
185
+ );
186
+ const filteredPendingApps = orderedPendingApps.filter((app) =>
187
+ workspaceAppMatchesQuery(app, searchQuery),
188
+ );
189
+ const hasSearchResults =
190
+ filteredActiveApps.length > 0 || filteredPendingApps.length > 0;
173
191
  const otherAppEntries = mergeOtherAppEntries({
174
192
  templates: curatedTemplates,
175
193
  connectedApps,
@@ -183,14 +201,23 @@ function AppsPanel({
183
201
  <SectionHeader
184
202
  title="Apps"
185
203
  action={
186
- <div className="flex items-center gap-1.5">
187
- <Button variant="outline" size="sm" asChild>
188
- <Link to="/apps">View all</Link>
204
+ <div className="flex min-w-0 flex-wrap items-center justify-end gap-1.5">
205
+ <Button variant="ghost" size="sm" asChild>
206
+ <Link to="/apps" className="text-muted-foreground">
207
+ View all
208
+ </Link>
189
209
  </Button>
210
+ {!showSkeletons && visibleApps.length > 0 ? (
211
+ <WorkspaceAppSearch
212
+ className="w-[220px]"
213
+ query={searchQuery}
214
+ onQueryChange={setSearchQuery}
215
+ />
216
+ ) : null}
190
217
  <CreateAppPopover
191
218
  align="end"
192
219
  trigger={
193
- <Button variant="ghost" size="sm" className="gap-1.5">
220
+ <Button variant="default" size="sm" className="gap-1.5">
194
221
  <IconPlus size={14} />
195
222
  {t("dispatch.pages.newApp", { defaultValue: "New" })}
196
223
  </Button>
@@ -199,19 +226,38 @@ function AppsPanel({
199
226
  </div>
200
227
  }
201
228
  />
229
+ {!showSkeletons && visibleApps.length > 0 && persistenceError ? (
230
+ <p className="text-xs text-muted-foreground" role="status">
231
+ {persistenceError === "both"
232
+ ? t("dispatch.pages.appPinSaveFailed", {
233
+ defaultValue: "App pins could not be saved.",
234
+ })
235
+ : t("dispatch.pages.appPinSavedLocally", {
236
+ defaultValue: "App pins are saved on this device only.",
237
+ })}
238
+ </p>
239
+ ) : null}
202
240
  {showSkeletons ? (
203
241
  <OverviewAppsSkeleton />
242
+ ) : searchQuery.trim() && !hasSearchResults ? (
243
+ <WorkspaceAppSearchEmpty
244
+ query={searchQuery}
245
+ onClear={() => setSearchQuery("")}
246
+ />
204
247
  ) : (
205
248
  <>
206
249
  <AppList className={APP_LIST_GRID_CLASS}>
207
- {activeApps.map((app) => (
250
+ {filteredActiveApps.map((app) => (
208
251
  <WorkspaceAppCard
209
252
  key={app.id}
210
253
  app={app}
211
254
  className={APP_LIST_GRID_ROW_CLASS}
255
+ isPinned={layout.pinnedIds.includes(app.id)}
256
+ onTogglePinned={() => togglePinned(app.id)}
212
257
  />
213
258
  ))}
214
- {activeApps.length === 0 &&
259
+ {filteredActiveApps.length === 0 &&
260
+ !searchQuery.trim() &&
215
261
  otherAppEntries.length === 0 &&
216
262
  !curatedTemplatesLoading &&
217
263
  !connectedAppsLoading &&
@@ -223,24 +269,30 @@ function AppsPanel({
223
269
  })}
224
270
  </p>
225
271
  ) : null}
226
- <OtherAppsSection
227
- templates={curatedTemplates}
228
- connectedApps={connectedApps}
229
- workspaceApps={apps}
230
- templatesLoading={curatedTemplatesLoading}
231
- connectedAppsLoading={connectedAppsLoading}
232
- templatesError={curatedTemplatesError}
233
- connectedAppsError={connectedAppsError}
234
- onRetryTemplates={onRetryCuratedTemplates}
235
- onRetryConnectedApps={onRetryConnectedApps}
236
- heading={null}
237
- embeddedInList
238
- />
272
+ {!searchQuery.trim() ? (
273
+ <OtherAppsSection
274
+ templates={curatedTemplates}
275
+ connectedApps={connectedApps}
276
+ workspaceApps={apps}
277
+ templatesLoading={curatedTemplatesLoading}
278
+ connectedAppsLoading={connectedAppsLoading}
279
+ templatesError={curatedTemplatesError}
280
+ connectedAppsError={connectedAppsError}
281
+ onRetryTemplates={onRetryCuratedTemplates}
282
+ onRetryConnectedApps={onRetryConnectedApps}
283
+ heading={null}
284
+ embeddedInList
285
+ />
286
+ ) : null}
239
287
  </AppList>
240
288
  </>
241
289
  )}
242
- {pendingApps.length > 0 ? (
243
- <Collapsible open={showPending} onOpenChange={setShowPending}>
290
+ {pendingApps.length > 0 &&
291
+ (!searchQuery.trim() || filteredPendingApps.length > 0) ? (
292
+ <Collapsible
293
+ open={showPending || Boolean(searchQuery.trim())}
294
+ onOpenChange={setShowPending}
295
+ >
244
296
  <div className="space-y-3 border-t pt-3">
245
297
  <div className="flex flex-wrap items-center justify-between gap-3">
246
298
  <div className="flex min-w-0 items-center gap-2">
@@ -266,19 +318,23 @@ function AppsPanel({
266
318
  size="sm"
267
319
  className="h-7 gap-1.5 px-2 text-xs"
268
320
  >
269
- {showPending
321
+ {showPending || Boolean(searchQuery.trim())
270
322
  ? t("dispatch.pages.hidePendingApps")
271
323
  : t("dispatch.pages.showPendingApps")}
272
324
  <IconChevronDown
273
325
  size={13}
274
- className={showPending ? "rotate-180" : undefined}
326
+ className={
327
+ showPending || searchQuery.trim()
328
+ ? "rotate-180"
329
+ : undefined
330
+ }
275
331
  />
276
332
  </Button>
277
333
  </CollapsibleTrigger>
278
334
  </div>
279
335
  <CollapsibleContent>
280
336
  <AppList className={APP_LIST_GRID_CLASS}>
281
- {pendingApps.map((app) => (
337
+ {filteredPendingApps.map((app) => (
282
338
  <WorkspaceAppCard
283
339
  key={app.id}
284
340
  app={app}
@@ -1,7 +1,9 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
2
  import { IconInfoCircle } from "@tabler/icons-react";
3
3
  import { type ReactNode } from "react";
4
+ import { useLocation } from "react-router";
4
5
 
6
+ import { dispatchDocsHrefForPath, DocsLink } from "./docs-link";
5
7
  import { useSetPageTitle } from "./layout/HeaderActions";
6
8
  import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
7
9
 
@@ -21,12 +23,16 @@ export function DispatchShell({
21
23
  children: ReactNode;
22
24
  }) {
23
25
  const t = useT();
26
+ const location = useLocation();
27
+ const docsHref = dispatchDocsHrefForPath(location.pathname);
24
28
  useSetPageTitle(
25
29
  <div className="flex items-center gap-2 min-w-0">
26
30
  <h1 className="text-lg font-semibold tracking-tight truncate text-foreground">
27
31
  {title}
28
32
  </h1>
29
- {description ? (
33
+ {docsHref ? (
34
+ <DocsLink href={docsHref} label={`Open ${title} documentation`} />
35
+ ) : description ? (
30
36
  <Popover>
31
37
  <PopoverTrigger asChild>
32
38
  <button
@@ -0,0 +1,52 @@
1
+ import { IconHelpCircle } from "@tabler/icons-react";
2
+
3
+ import { cn } from "../lib/utils";
4
+
5
+ export const DISPATCH_DOCS = {
6
+ dispatch: "https://agent-native.com/docs/dispatch#what-it-does",
7
+ workspace: "https://agent-native.com/docs/agent-resources#global-resources",
8
+ integrations: "https://agent-native.com/docs/dispatch#secret-vault",
9
+ audit: "https://agent-native.com/docs/audit-log",
10
+ threadDebug: "https://agent-native.com/docs/dispatch#reliability",
11
+ dreams: "https://agent-native.com/docs/dispatch#dreams",
12
+ settings: "https://agent-native.com/docs/agent-surfaces",
13
+ } as const;
14
+
15
+ export function dispatchDocsHrefForPath(pathname: string): string | undefined {
16
+ if (pathname === "/admin/workspace") return DISPATCH_DOCS.workspace;
17
+ if (pathname === "/admin/integrations") return DISPATCH_DOCS.integrations;
18
+ if (pathname === "/admin/audit") return DISPATCH_DOCS.audit;
19
+ if (pathname === "/admin/thread-debug") return DISPATCH_DOCS.threadDebug;
20
+ if (pathname === "/admin/dreams") return DISPATCH_DOCS.dreams;
21
+ if (pathname.startsWith("/admin/")) return DISPATCH_DOCS.dispatch;
22
+ if (pathname === "/settings" || pathname.startsWith("/settings/")) {
23
+ return DISPATCH_DOCS.settings;
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ export function DocsLink({
29
+ href,
30
+ label,
31
+ className,
32
+ }: {
33
+ href: string;
34
+ label: string;
35
+ className?: string;
36
+ }) {
37
+ return (
38
+ <a
39
+ href={href}
40
+ target="_blank"
41
+ rel="noopener noreferrer"
42
+ aria-label={label}
43
+ title={label}
44
+ className={cn(
45
+ "inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-accent/60 hover:text-foreground",
46
+ className,
47
+ )}
48
+ >
49
+ <IconHelpCircle className="size-3" />
50
+ </a>
51
+ );
52
+ }
@@ -11,6 +11,7 @@ import {
11
11
  formatThreadAge,
12
12
  isElectronEmbeddedSearch,
13
13
  NavContent,
14
+ shouldAutoCollapseDispatchSidebar,
14
15
  } from "./Layout";
15
16
 
16
17
  const clientState = vi.hoisted(() => ({
@@ -128,6 +129,15 @@ describe("Electron control-plane mode", () => {
128
129
  });
129
130
  });
130
131
 
132
+ describe("Dispatch workspace app sidebar", () => {
133
+ it("auto-collapses for app host routes but not the app catalog", () => {
134
+ expect(shouldAutoCollapseDispatchSidebar("/apps/mail")).toBe(true);
135
+ expect(shouldAutoCollapseDispatchSidebar("/apps/mail/settings")).toBe(true);
136
+ expect(shouldAutoCollapseDispatchSidebar("/apps")).toBe(false);
137
+ expect(shouldAutoCollapseDispatchSidebar("/chat")).toBe(false);
138
+ });
139
+ });
140
+
131
141
  describe("Dispatch NavContent", () => {
132
142
  let container: HTMLDivElement;
133
143
  let root: Root;
@@ -108,6 +108,7 @@ import {
108
108
  isDispatchWorkspaceAppId,
109
109
  isWorkspaceAppVisibleInDefaultLaunchers,
110
110
  mergeChatFirstWorkspaceApps,
111
+ workspaceAppDirectHref,
111
112
  workspaceAppIdFromRoute,
112
113
  workspaceAppRoute,
113
114
  type WorkspaceAppSummary,
@@ -306,6 +307,10 @@ export function isElectronEmbeddedSearch(search: string): boolean {
306
307
  return new URLSearchParams(search).get("electron") === "1";
307
308
  }
308
309
 
310
+ export function shouldAutoCollapseDispatchSidebar(pathname: string): boolean {
311
+ return localDispatchPath(pathname).startsWith("/apps/");
312
+ }
313
+
309
314
  function chatFirstPrimaryTabForPath(
310
315
  pathname: string,
311
316
  ): ChatFirstPrimaryTab | undefined {
@@ -534,16 +539,6 @@ function threadTitle(thread: ChatThreadSummary, fallback: string) {
534
539
  return thread.title || thread.preview || fallback;
535
540
  }
536
541
 
537
- function readChatHistoryIncludesExternal(): boolean {
538
- if (typeof window === "undefined") return false;
539
- try {
540
- return localStorage.getItem(CHAT_HISTORY_SOURCE_KEY) === "all";
541
- } catch {
542
- // coercion-ok: localStorage is optional browser persistence.
543
- return false;
544
- }
545
- }
546
-
547
542
  function threadSourceIcon(platform: string | undefined): ReactNode {
548
543
  const normalized = platform?.trim().toLowerCase();
549
544
  if (!normalized) return null;
@@ -556,6 +551,16 @@ function threadSourceIcon(platform: string | undefined): ReactNode {
556
551
  return <IconWorld size={13} aria-hidden="true" />;
557
552
  }
558
553
 
554
+ function readChatHistoryIncludesExternal(): boolean {
555
+ if (typeof window === "undefined") return false;
556
+ try {
557
+ return localStorage.getItem(CHAT_HISTORY_SOURCE_KEY) === "all";
558
+ } catch {
559
+ // coercion-ok: localStorage is optional browser persistence.
560
+ return false;
561
+ }
562
+ }
563
+
559
564
  function threadUpdatedAt(thread: ChatThreadSummary) {
560
565
  return Number.isFinite(thread.updatedAt)
561
566
  ? thread.updatedAt
@@ -595,6 +600,7 @@ function DispatchChatsSection({
595
600
  threads,
596
601
  activeThreadId,
597
602
  isLoading: chatsLoading,
603
+ threadsLoadError,
598
604
  createThread,
599
605
  switchThread,
600
606
  renameThread,
@@ -611,7 +617,7 @@ function DispatchChatsSection({
611
617
  (thread) => thread.messageCount > 0 || thread.id === activeThreadId,
612
618
  )
613
619
  .sort((a, b) => threadUpdatedAt(b) - threadUpdatedAt(a))
614
- .slice(0, 15),
620
+ .slice(0, 8),
615
621
  [activeThreadId, threads],
616
622
  );
617
623
  const localPathname = localDispatchPath(location.pathname);
@@ -650,6 +656,16 @@ function DispatchChatsSection({
650
656
  : formatThreadAge(threadUpdatedAt(thread)),
651
657
  };
652
658
  });
659
+ const chatHistoryError = threadsLoadError ? (
660
+ <button
661
+ type="button"
662
+ onClick={() => void refreshThreads()}
663
+ className="flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-xs text-sidebar-foreground/65 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
664
+ >
665
+ <span>Could not load chats</span>
666
+ <span className="font-medium">Retry</span>
667
+ </button>
668
+ ) : undefined;
653
669
 
654
670
  useEffect(() => {
655
671
  setIncludeExternal(readChatHistoryIncludesExternal());
@@ -802,6 +818,7 @@ function DispatchChatsSection({
802
818
  </div>
803
819
  }
804
820
  label={t("dispatch.sidebar.chats", { defaultValue: "Chats" })}
821
+ error={chatHistoryError}
805
822
  onSelect={(threadId) => openThread(threadId)}
806
823
  renameMaxLength={160}
807
824
  onRename={(threadId, title) => void renameThread(threadId, title)}
@@ -870,6 +887,7 @@ function DispatchChatsSection({
870
887
  showMore: t("dispatch.sidebar.chats"),
871
888
  showLess: t("dispatch.sidebar.chats"),
872
889
  }}
890
+ error={chatHistoryError}
873
891
  renameMaxLength={160}
874
892
  onRename={(threadId, title) => void renameThread(threadId, title)}
875
893
  labels={{
@@ -924,7 +942,7 @@ function DispatchChatsSection({
924
942
  ) : null}
925
943
  </>
926
944
  )}
927
- className={cn("min-w-0", chatFirstMode && "px-2")}
945
+ className="min-w-0 px-2"
928
946
  />
929
947
  )}
930
948
  </div>
@@ -1348,7 +1366,9 @@ export function Layout({
1348
1366
  const isChatRoute =
1349
1367
  localPathname === "/chat" || localPathname.startsWith("/chat/");
1350
1368
  const chatFirstSurfaceScope = threadIdFromPath(localPathname) ?? "new";
1351
- const isWorkspaceAppRoute = localPathname.startsWith("/apps/");
1369
+ const isWorkspaceAppRoute = shouldAutoCollapseDispatchSidebar(
1370
+ location.pathname,
1371
+ );
1352
1372
  const isWorkspaceAppHostRoute =
1353
1373
  isWorkspaceAppRoute &&
1354
1374
  typeof window !== "undefined" &&
@@ -1373,12 +1393,6 @@ export function Layout({
1373
1393
  `${url.pathname}${url.search}${url.hash}`,
1374
1394
  );
1375
1395
  }, [electronEmbedded, location.pathname, location.search]);
1376
- useEffect(() => {
1377
- if (!chatFirstMode || chatFirstEmbedded || localPathname !== "/overview") {
1378
- return;
1379
- }
1380
- navigate(dispatchNavLinkTarget("/chat"), { replace: true });
1381
- }, [chatFirstEmbedded, chatFirstMode, localPathname, navigate]);
1382
1396
  const [chatFirstAppLayout, setChatFirstAppLayout] =
1383
1397
  useState<ChatFirstAppLayoutPreference>(() => readChatFirstAppLayout());
1384
1398
  const chatFirstAppLayoutHydratedRef = useRef(false);
@@ -1565,6 +1579,13 @@ export function Layout({
1565
1579
  })
1566
1580
  .catch((cause: unknown) => {
1567
1581
  if (!cancelled) {
1582
+ const directAppUrl = activeChatFirstAppRegistration
1583
+ ? workspaceAppDirectHref(
1584
+ activeChatFirstAppRegistration,
1585
+ chatFirstEmbedPath,
1586
+ )
1587
+ : null;
1588
+ if (directAppUrl) setChatFirstEmbedUrl(directAppUrl);
1568
1589
  setChatFirstEmbedError(
1569
1590
  cause instanceof Error
1570
1591
  ? cause.message
@@ -1629,6 +1650,7 @@ export function Layout({
1629
1650
  placement: ChatFirstAppSurfacePlacement = "side",
1630
1651
  ) => {
1631
1652
  if (placement === "side" && !chatFirstHasActiveChat) return;
1653
+ setSidebarCollapsed(true);
1632
1654
  setChatFirstNotice(null);
1633
1655
  closeChatFirstSessionWatch();
1634
1656
  if (!isChatRoute) {
@@ -2027,6 +2049,11 @@ export function Layout({
2027
2049
  isChatRoute,
2028
2050
  ]);
2029
2051
 
2052
+ useEffect(() => {
2053
+ if (!isWorkspaceAppRoute) return;
2054
+ setSidebarCollapsed(true);
2055
+ }, [isWorkspaceAppRoute, localPathname]);
2056
+
2030
2057
  useEffect(() => {
2031
2058
  if (typeof window === "undefined" || isWorkspaceAppHostRoute) return;
2032
2059
  try {
@@ -2140,10 +2167,10 @@ export function Layout({
2140
2167
  <ChatFirstAppPane
2141
2168
  app={app}
2142
2169
  status={
2143
- chatFirstEmbedError
2144
- ? "error"
2145
- : chatFirstEmbedUrl
2146
- ? "ready"
2170
+ chatFirstEmbedUrl
2171
+ ? "ready"
2172
+ : chatFirstEmbedError
2173
+ ? "error"
2147
2174
  : chatFirstAppsQuery.isLoading
2148
2175
  ? "loading"
2149
2176
  : app
@@ -57,6 +57,7 @@ import {
57
57
  CollapsibleTrigger,
58
58
  } from "./ui/collapsible";
59
59
  import { Input } from "./ui/input";
60
+ import { Skeleton } from "./ui/skeleton";
60
61
  import { Switch } from "./ui/switch";
61
62
  import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
62
63
 
@@ -553,7 +554,11 @@ export function MessagingSetupPanel() {
553
554
  : "Finish setup to connect"}
554
555
  </p>
555
556
  <p className="mt-0.5 text-xs text-muted-foreground">
556
- {credentialSummary}
557
+ {envLoading ? (
558
+ <Skeleton className="h-3 w-32" />
559
+ ) : (
560
+ credentialSummary
561
+ )}
557
562
  </p>
558
563
  </div>
559
564
  <div className="flex flex-wrap items-center gap-2">
@@ -1115,8 +1120,10 @@ export function MessagingSetupPanel() {
1115
1120
  </Accordion>
1116
1121
 
1117
1122
  {loading ? (
1118
- <div className="rounded-2xl border border-dashed px-4 py-6 text-sm text-muted-foreground">
1119
- Loading messaging status...
1123
+ <div className="space-y-3 rounded-2xl border border-dashed px-4 py-6">
1124
+ <Skeleton className="h-4 w-40" />
1125
+ <Skeleton className="h-3 w-2/3 max-w-md" />
1126
+ <Skeleton className="h-9 w-full max-w-sm" />
1120
1127
  </div>
1121
1128
  ) : null}
1122
1129
  </div>