@agent-native/dispatch 0.22.0 → 0.23.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 (51) hide show
  1. package/dist/components/app-icon.d.ts +2 -1
  2. package/dist/components/app-icon.d.ts.map +1 -1
  3. package/dist/components/app-icon.js +4 -2
  4. package/dist/components/app-icon.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 +2 -1
  10. package/dist/components/app-open-actions.d.ts.map +1 -1
  11. package/dist/components/app-open-actions.js +2 -2
  12. package/dist/components/app-open-actions.js.map +1 -1
  13. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  14. package/dist/components/dispatch-control-plane.js +16 -17
  15. package/dist/components/dispatch-control-plane.js.map +1 -1
  16. package/dist/components/layout/Layout.d.ts.map +1 -1
  17. package/dist/components/layout/Layout.js +91 -57
  18. package/dist/components/layout/Layout.js.map +1 -1
  19. package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
  20. package/dist/components/layout/workspace-apps-rail.js +1 -1
  21. package/dist/components/layout/workspace-apps-rail.js.map +1 -1
  22. package/dist/components/other-apps-section.d.ts +3 -1
  23. package/dist/components/other-apps-section.d.ts.map +1 -1
  24. package/dist/components/other-apps-section.js +9 -6
  25. package/dist/components/other-apps-section.js.map +1 -1
  26. package/dist/components/workspace-app-card.d.ts.map +1 -1
  27. package/dist/components/workspace-app-card.js +7 -2
  28. package/dist/components/workspace-app-card.js.map +1 -1
  29. package/dist/components/workspace-template-card.d.ts.map +1 -1
  30. package/dist/components/workspace-template-card.js +2 -5
  31. package/dist/components/workspace-template-card.js.map +1 -1
  32. package/dist/lib/overview-chat.d.ts.map +1 -1
  33. package/dist/lib/overview-chat.js +0 -8
  34. package/dist/lib/overview-chat.js.map +1 -1
  35. package/package.json +3 -3
  36. package/src/components/app-icon.tsx +6 -2
  37. package/src/components/app-list-row.tsx +1 -1
  38. package/src/components/app-open-actions.tsx +4 -2
  39. package/src/components/dispatch-control-plane.spec.tsx +43 -7
  40. package/src/components/dispatch-control-plane.tsx +56 -75
  41. package/src/components/layout/Layout.spec.tsx +121 -1
  42. package/src/components/layout/Layout.tsx +187 -114
  43. package/src/components/layout/workspace-apps-rail.tsx +1 -0
  44. package/src/components/other-apps-section.tsx +61 -40
  45. package/src/components/workspace-app-card.spec.tsx +63 -66
  46. package/src/components/workspace-app-card.tsx +10 -3
  47. package/src/components/workspace-template-card.spec.tsx +7 -8
  48. package/src/components/workspace-template-card.tsx +21 -11
  49. package/src/lib/overview-chat.spec.ts +0 -19
  50. package/src/lib/overview-chat.ts +0 -9
  51. package/src/styles/dispatch.css +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"overview-chat.js","sourceRoot":"","sources":["../../src/lib/overview-chat.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,GAEhB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAQlE,MAAM,UAAU,oBAAoB,CAClC,OAAe,EACf,aAA6B,EAC7B,OAA6B;IAE7B,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAC/B,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,IAAI,gBAAgB,EAAE,EAAE,CAAC;QACvB,OAAO,eAAe,CAAC;YACrB,OAAO,EAAE,OAAO;YAChB,MAAM,EAAE,IAAI;YACZ,IAAI,EAAE,MAAM;SACb,CAAC,CAAC;IACL,CAAC;IAED,OAAO,eAAe,CAAC;QACrB,OAAO,EAAE,OAAO;QAChB,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE,aAAa,IAAI,SAAS;QACjC,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClE,CAAC,CAAC;AACL,CAAC","sourcesContent":["import {\n sendToAgentChat,\n type AgentChatMessage,\n} from \"@agent-native/core/client/agent-chat\";\nimport { isInBuilderFrame } from \"@agent-native/core/client/host\";\n\ninterface OverviewChatOptions {\n openSidebar?: boolean;\n selectedEngine?: string | null;\n selectedEffort?: AgentChatMessage[\"effort\"];\n}\n\nexport function submitOverviewPrompt(\n message: string,\n selectedModel?: string | null,\n options?: OverviewChatOptions,\n): string | null {\n const trimmed = message.trim();\n if (!trimmed) return null;\n\n if (isInBuilderFrame()) {\n return sendToAgentChat({\n message: trimmed,\n submit: true,\n type: \"code\",\n });\n }\n\n return sendToAgentChat({\n message: trimmed,\n submit: true,\n newTab: true,\n model: selectedModel || undefined,\n ...(options?.selectedEngine ? { engine: options.selectedEngine } : {}),\n ...(options?.selectedEffort ? { effort: options.selectedEffort } : {}),\n ...(options?.openSidebar === false ? { openSidebar: false } : {}),\n });\n}\n"]}
1
+ {"version":3,"file":"overview-chat.js","sourceRoot":"","sources":["../../src/lib/overview-chat.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,GAEhB,MAAM,sCAAsC,CAAC;AAQ9C,MAAM,UAAU,oBAAoB,CAClC,OAAe,EACf,aAA6B,EAC7B,OAA6B;IAE7B,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAC/B,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,OAAO,eAAe,CAAC;QACrB,OAAO,EAAE,OAAO;QAChB,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE,aAAa,IAAI,SAAS;QACjC,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClE,CAAC,CAAC;AACL,CAAC","sourcesContent":["import {\n sendToAgentChat,\n type AgentChatMessage,\n} from \"@agent-native/core/client/agent-chat\";\n\ninterface OverviewChatOptions {\n openSidebar?: boolean;\n selectedEngine?: string | null;\n selectedEffort?: AgentChatMessage[\"effort\"];\n}\n\nexport function submitOverviewPrompt(\n message: string,\n selectedModel?: string | null,\n options?: OverviewChatOptions,\n): string | null {\n const trimmed = message.trim();\n if (!trimmed) return null;\n\n return sendToAgentChat({\n message: trimmed,\n submit: true,\n newTab: true,\n model: selectedModel || undefined,\n ...(options?.selectedEngine ? { engine: options.selectedEngine } : {}),\n ...(options?.selectedEffort ? { effort: options.selectedEffort } : {}),\n ...(options?.openSidebar === false ? { openSidebar: false } : {}),\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -84,7 +84,7 @@
84
84
  "tailwind-merge": "^3.5.0",
85
85
  "vaul": "^1.1.2",
86
86
  "zod": "^4.3.6",
87
- "@agent-native/toolkit": "0.13.7"
87
+ "@agent-native/toolkit": "0.13.8"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@react-router/dev": "^8.1.0",
@@ -98,7 +98,7 @@
98
98
  "typescript-7": "npm:typescript@^7.0.2",
99
99
  "vite": "8.1.0",
100
100
  "vitest": "^4.1.5",
101
- "@agent-native/core": "0.148.0"
101
+ "@agent-native/core": "0.149.0"
102
102
  },
103
103
  "peerDependencies": {
104
104
  "@agent-native/core": ">=0.8.0",
@@ -95,6 +95,7 @@ export function AppIcon({
95
95
  color,
96
96
  className,
97
97
  size = "md",
98
+ monochrome = false,
98
99
  }: {
99
100
  id: string;
100
101
  name: string;
@@ -102,6 +103,7 @@ export function AppIcon({
102
103
  color?: string;
103
104
  className?: string;
104
105
  size?: "sm" | "md";
106
+ monochrome?: boolean;
105
107
  }) {
106
108
  const Icon = appIconComponent(id, name, icon);
107
109
  const customColor = safeHexColor(color);
@@ -126,10 +128,12 @@ export function AppIcon({
126
128
  className={cn(
127
129
  "flex shrink-0 items-center justify-center rounded-xl",
128
130
  size === "sm" ? "size-8" : "size-10",
129
- !customColor && tone,
131
+ monochrome
132
+ ? "bg-transparent text-sidebar-foreground/70"
133
+ : !customColor && tone,
130
134
  className,
131
135
  )}
132
- style={style}
136
+ style={monochrome ? undefined : style}
133
137
  >
134
138
  <Icon size={size === "sm" ? 16 : 19} stroke={1.8} />
135
139
  </span>
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
3
3
  import { cn } from "../lib/utils";
4
4
 
5
5
  export const APP_LIST_GRID_CLASS =
6
- "xl:grid xl:grid-cols-2 xl:gap-3 xl:overflow-visible xl:rounded-none xl:bg-transparent";
6
+ "mx-auto w-full max-w-[1000px] xl:grid xl:grid-cols-2 xl:gap-3 xl:overflow-visible xl:rounded-none xl:bg-transparent";
7
7
  export const APP_LIST_GRID_ROW_CLASS = "xl:rounded-2xl xl:border-0 xl:bg-card";
8
8
 
9
9
  export function AppList({
@@ -29,6 +29,7 @@ export function AppOpenActions({
29
29
  rel,
30
30
  labels: labelOverrides,
31
31
  onAddApp,
32
+ onOpenInline,
32
33
  showInlineOption = false,
33
34
  showNewTabOption = false,
34
35
  }: {
@@ -38,6 +39,7 @@ export function AppOpenActions({
38
39
  rel?: string;
39
40
  labels?: Partial<AppOpenActionLabels>;
40
41
  onAddApp?: () => void;
42
+ onOpenInline?: () => void;
41
43
  showInlineOption?: boolean;
42
44
  showNewTabOption?: boolean;
43
45
  }) {
@@ -92,8 +94,8 @@ export function AppOpenActions({
92
94
  </DropdownMenuItem>
93
95
  ) : null}
94
96
  {showInlineOption && href ? (
95
- <DropdownMenuItem asChild>
96
- <a href={href}>{labels.openInline}</a>
97
+ <DropdownMenuItem onSelect={onOpenInline}>
98
+ {labels.openInline}
97
99
  </DropdownMenuItem>
98
100
  ) : null}
99
101
  {showNewTabOption && href ? (
@@ -8,6 +8,7 @@ import { DispatchControlPlane } from "./dispatch-control-plane";
8
8
  import { TooltipProvider } from "./ui/tooltip";
9
9
 
10
10
  const clientState = vi.hoisted(() => ({
11
+ inBuilderFrame: false,
11
12
  navigateWithTransition: vi.fn(),
12
13
  promptComposerProps: null as Record<string, unknown> | null,
13
14
  workspaceApps: [] as Array<Record<string, unknown>>,
@@ -69,7 +70,7 @@ vi.mock("@agent-native/core/client/hooks", () => ({
69
70
  }));
70
71
 
71
72
  vi.mock("@agent-native/core/client/host", () => ({
72
- isInBuilderFrame: () => false,
73
+ isInBuilderFrame: () => clientState.inBuilderFrame,
73
74
  }));
74
75
 
75
76
  vi.mock("@agent-native/core/client/i18n", () => ({
@@ -79,7 +80,12 @@ vi.mock("@agent-native/core/client/i18n", () => ({
79
80
  }));
80
81
 
81
82
  vi.mock("./create-app-popover", () => ({
82
- CreateAppPopover: () => <div>Create app</div>,
83
+ CreateAppPopover: ({ trigger }: { trigger?: React.ReactNode }) => (
84
+ <div>
85
+ {trigger}
86
+ <span>Create app</span>
87
+ </div>
88
+ ),
83
89
  }));
84
90
 
85
91
  describe("DispatchControlPlane", () => {
@@ -88,6 +94,7 @@ describe("DispatchControlPlane", () => {
88
94
 
89
95
  beforeEach(() => {
90
96
  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
97
+ clientState.inBuilderFrame = false;
91
98
  clientState.navigateWithTransition.mockReset();
92
99
  clientState.promptComposerProps = null;
93
100
  clientState.workspaceApps = [];
@@ -155,6 +162,36 @@ describe("DispatchControlPlane", () => {
155
162
  );
156
163
  });
157
164
 
165
+ it("keeps overview submissions on Dispatch Chat inside Builder frames", async () => {
166
+ clientState.inBuilderFrame = true;
167
+
168
+ await act(async () => {
169
+ root.render(
170
+ <MemoryRouter initialEntries={["/overview"]}>
171
+ <TooltipProvider>
172
+ <DispatchControlPlane />
173
+ </TooltipProvider>
174
+ </MemoryRouter>,
175
+ );
176
+ });
177
+
178
+ await act(async () => {
179
+ container.querySelector<HTMLButtonElement>("[data-placeholder]")?.click();
180
+ });
181
+
182
+ expect(clientState.navigateWithTransition).toHaveBeenCalledWith(
183
+ expect.any(Function),
184
+ "/chat",
185
+ expect.objectContaining({
186
+ state: {
187
+ dispatchPrompt: expect.objectContaining({
188
+ message: "Route onboarding work",
189
+ }),
190
+ },
191
+ }),
192
+ );
193
+ });
194
+
158
195
  it("shows mounted and connected apps together without duplicates", async () => {
159
196
  clientState.workspaceApps = [
160
197
  {
@@ -230,11 +267,10 @@ describe("DispatchControlPlane", () => {
230
267
  expect(container.textContent).toContain("Mail");
231
268
  expect(container.textContent).toContain("Clips");
232
269
  expect(container.textContent).toContain("Analytics");
233
- expect(container.textContent).toContain("Your apps");
234
- expect(container.textContent).toContain("Other apps");
235
- expect(container.textContent?.indexOf("Your apps")).toBeLessThan(
236
- container.textContent?.indexOf("Other apps") ?? -1,
237
- );
270
+ expect(container.textContent).toContain("Apps");
271
+ expect(container.textContent).toContain("New");
272
+ expect(container.textContent).not.toContain("Other apps");
273
+ expect(container.textContent).not.toContain("available");
238
274
  expect(container.textContent).not.toContain("Archived app");
239
275
  expect(container.textContent).not.toContain("Duplicate onboarding");
240
276
  expect(container.textContent).not.toContain("CRM");
@@ -4,20 +4,13 @@ import {
4
4
  } from "@agent-native/core/client/agent-chat";
5
5
  import { PromptComposer } from "@agent-native/core/client/composer";
6
6
  import { useActionQuery } from "@agent-native/core/client/hooks";
7
- import { isInBuilderFrame } from "@agent-native/core/client/host";
8
7
  import { useT } from "@agent-native/core/client/i18n";
9
- import {
10
- IconArrowUpRight,
11
- IconChevronDown,
12
- IconClockHour4,
13
- IconApps,
14
- } from "@tabler/icons-react";
8
+ import { IconChevronDown, IconClockHour4, IconPlus } from "@tabler/icons-react";
15
9
  import type { ReactNode } from "react";
16
10
  import { useState } from "react";
17
11
  import { Link, useNavigate } from "react-router";
18
12
 
19
13
  import type { ConnectedAppSummary } from "../lib/other-apps";
20
- import { submitOverviewPrompt } from "../lib/overview-chat";
21
14
  import { cn } from "../lib/utils";
22
15
  import type { WorkspaceAppSummary } from "../lib/workspace-apps";
23
16
  import { ActionQueryError } from "./action-query-error";
@@ -28,7 +21,7 @@ import {
28
21
  } from "./app-list-row";
29
22
  import { CreateAppPopover } from "./create-app-popover";
30
23
  import { useSetPageTitle } from "./layout/HeaderActions";
31
- import { OtherAppsSection } from "./other-apps-section";
24
+ import { mergeOtherAppEntries, OtherAppsSection } from "./other-apps-section";
32
25
  import { Button } from "./ui/button";
33
26
  import {
34
27
  Collapsible,
@@ -56,26 +49,6 @@ function SectionHeader({
56
49
  );
57
50
  }
58
51
 
59
- function AppGroupHeader({
60
- title,
61
- description,
62
- }: {
63
- title: string;
64
- description: string;
65
- }) {
66
- return (
67
- <div className="flex min-w-0 items-start gap-2">
68
- <IconApps size={16} className="mt-0.5 shrink-0 text-muted-foreground" />
69
- <div className="min-w-0">
70
- <h3 className="truncate text-sm font-semibold text-foreground">
71
- {title}
72
- </h3>
73
- <p className="mt-0.5 text-xs text-muted-foreground">{description}</p>
74
- </div>
75
- </div>
76
- );
77
- }
78
-
79
52
  function CommandPanel() {
80
53
  const t = useT();
81
54
  const {
@@ -104,11 +77,6 @@ function CommandPanel() {
104
77
  const trimmed = message.trim();
105
78
  if (!trimmed) return;
106
79
 
107
- if (isInBuilderFrame()) {
108
- submitOverviewPrompt(trimmed, selectedModel);
109
- return;
110
- }
111
-
112
80
  navigateWithAgentChatViewTransition(navigate, "/chat", {
113
81
  state: {
114
82
  dispatchPrompt: {
@@ -124,7 +92,7 @@ function CommandPanel() {
124
92
 
125
93
  return (
126
94
  <section className="flex flex-col">
127
- <div className="mx-auto flex w-full max-w-3xl flex-col pt-4 sm:pt-6">
95
+ <div className="mx-auto flex w-full max-w-[750px] flex-col pt-4 sm:pt-6">
128
96
  <div className="mb-5 text-center">
129
97
  <h2 className="text-2xl font-semibold tracking-tight text-foreground sm:text-3xl">
130
98
  {t("dispatch.pages.chatAcrossApps", {
@@ -196,60 +164,73 @@ function AppsPanel({
196
164
  const visibleApps = apps.filter((app) => !app.isDispatch && !app.archived);
197
165
  const activeApps = visibleApps.filter((app) => app.status !== "pending");
198
166
  const pendingApps = visibleApps.filter((app) => app.status === "pending");
167
+ const otherAppEntries = mergeOtherAppEntries({
168
+ templates: curatedTemplates,
169
+ connectedApps,
170
+ workspaceApps: apps,
171
+ });
199
172
  const showSkeletons =
200
173
  isLoading && activeApps.length === 0 && pendingApps.length === 0;
201
174
 
202
175
  return (
203
- <section className="flex flex-col gap-3">
176
+ <section className="mx-auto flex w-full max-w-[1000px] flex-col gap-3">
204
177
  <SectionHeader
205
178
  title="Apps"
206
179
  action={
207
- <Button variant="outline" size="sm" asChild>
208
- <Link to="/apps">
209
- View all
210
- <IconArrowUpRight size={14} />
211
- </Link>
212
- </Button>
180
+ <div className="flex items-center gap-1.5">
181
+ <Button variant="outline" size="sm" asChild>
182
+ <Link to="/apps">View all</Link>
183
+ </Button>
184
+ <CreateAppPopover
185
+ align="end"
186
+ trigger={
187
+ <Button variant="ghost" size="sm" className="gap-1.5">
188
+ <IconPlus size={14} />
189
+ {t("dispatch.pages.newApp", { defaultValue: "New" })}
190
+ </Button>
191
+ }
192
+ />
193
+ </div>
213
194
  }
214
195
  />
215
196
  {showSkeletons ? (
216
197
  <OverviewAppsSkeleton />
217
198
  ) : (
218
199
  <>
219
- {activeApps.length > 0 ? (
220
- <div className="space-y-3">
221
- <AppGroupHeader
222
- title={t("dispatch.pages.yourApps", {
223
- defaultValue: "Your apps",
224
- })}
225
- description={t("dispatch.pages.activeCount", {
226
- count: activeApps.length,
227
- })}
200
+ <AppList className={APP_LIST_GRID_CLASS}>
201
+ {activeApps.map((app) => (
202
+ <WorkspaceAppCard
203
+ key={app.id}
204
+ app={app}
205
+ className={APP_LIST_GRID_ROW_CLASS}
228
206
  />
229
- <AppList className={APP_LIST_GRID_CLASS}>
230
- {activeApps.map((app) => (
231
- <WorkspaceAppCard
232
- key={app.id}
233
- app={app}
234
- className={APP_LIST_GRID_ROW_CLASS}
235
- />
236
- ))}
237
- </AppList>
238
- </div>
239
- ) : (
240
- <CreateAppPopover />
241
- )}
242
- <OtherAppsSection
243
- templates={curatedTemplates}
244
- connectedApps={connectedApps}
245
- workspaceApps={apps}
246
- templatesLoading={curatedTemplatesLoading}
247
- connectedAppsLoading={connectedAppsLoading}
248
- templatesError={curatedTemplatesError}
249
- connectedAppsError={connectedAppsError}
250
- onRetryTemplates={onRetryCuratedTemplates}
251
- onRetryConnectedApps={onRetryConnectedApps}
252
- />
207
+ ))}
208
+ {activeApps.length === 0 &&
209
+ otherAppEntries.length === 0 &&
210
+ !curatedTemplatesLoading &&
211
+ !connectedAppsLoading &&
212
+ !curatedTemplatesError &&
213
+ !connectedAppsError ? (
214
+ <p className="px-4 py-3 text-sm text-muted-foreground">
215
+ {t("dispatch.pages.noApps", {
216
+ defaultValue: "No apps yet.",
217
+ })}
218
+ </p>
219
+ ) : null}
220
+ <OtherAppsSection
221
+ templates={curatedTemplates}
222
+ connectedApps={connectedApps}
223
+ workspaceApps={apps}
224
+ templatesLoading={curatedTemplatesLoading}
225
+ connectedAppsLoading={connectedAppsLoading}
226
+ templatesError={curatedTemplatesError}
227
+ connectedAppsError={connectedAppsError}
228
+ onRetryTemplates={onRetryCuratedTemplates}
229
+ onRetryConnectedApps={onRetryConnectedApps}
230
+ heading={null}
231
+ embeddedInList
232
+ />
233
+ </AppList>
253
234
  </>
254
235
  )}
255
236
  {pendingApps.length > 0 ? (
@@ -1,7 +1,7 @@
1
1
  // @vitest-environment happy-dom
2
2
  import React, { act } from "react";
3
3
  import { createRoot, type Root } from "react-dom/client";
4
- import { MemoryRouter } from "react-router";
4
+ import { MemoryRouter, useLocation } from "react-router";
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
 
7
7
  import { AdminShell } from "../admin-navigation";
@@ -87,6 +87,14 @@ vi.mock("@agent-native/core/client/org", () => ({
87
87
  OrgSwitcher: () => <div>Organization</div>,
88
88
  }));
89
89
 
90
+ function LocationProbe({ onChange }: { onChange: (path: string) => void }) {
91
+ const location = useLocation();
92
+ React.useEffect(() => {
93
+ onChange(location.pathname);
94
+ }, [location.pathname, onChange]);
95
+ return null;
96
+ }
97
+
90
98
  describe("formatThreadAge", () => {
91
99
  const now = 2_000_000_000_000;
92
100
 
@@ -310,6 +318,118 @@ describe("Dispatch NavContent", () => {
310
318
  );
311
319
  });
312
320
 
321
+ it("keeps Admin above Settings in the chat-first left sidebar", async () => {
322
+ await act(async () => {
323
+ root.render(
324
+ <MemoryRouter initialEntries={["/chat"]}>
325
+ <TooltipProvider>
326
+ <NavContent chatFirstMode />
327
+ </TooltipProvider>
328
+ </MemoryRouter>,
329
+ );
330
+ });
331
+
332
+ const adminLink = container.querySelector('a[href="/admin"]');
333
+ const settingsLink = container.querySelector('a[href="/settings"]');
334
+ const organization = [...container.querySelectorAll("div")].find(
335
+ (element) => element.textContent?.trim() === "Organization",
336
+ );
337
+
338
+ expect(adminLink).not.toBeNull();
339
+ expect(settingsLink).not.toBeNull();
340
+ expect(organization).toBeDefined();
341
+ expect(adminLink!.compareDocumentPosition(settingsLink!)).toBe(
342
+ Node.DOCUMENT_POSITION_FOLLOWING,
343
+ );
344
+ expect(settingsLink!.compareDocumentPosition(organization!)).toBe(
345
+ Node.DOCUMENT_POSITION_FOLLOWING,
346
+ );
347
+ });
348
+
349
+ it("keeps an All apps destination below the chat-first app list", async () => {
350
+ const paths: string[] = [];
351
+ const apps = Array.from({ length: 6 }, (_, index) => ({
352
+ id: `app-${index}`,
353
+ name: `App ${index}`,
354
+ }));
355
+
356
+ await act(async () => {
357
+ root.render(
358
+ <MemoryRouter initialEntries={["/chat"]}>
359
+ <TooltipProvider>
360
+ <NavContent chatFirstMode chatFirstApps={apps} />
361
+ <LocationProbe onChange={(path) => paths.push(path)} />
362
+ </TooltipProvider>
363
+ </MemoryRouter>,
364
+ );
365
+ });
366
+
367
+ expect(container.querySelectorAll("[data-chat-first-app]")).toHaveLength(5);
368
+ const showMore = [...container.querySelectorAll("button")].find(
369
+ (button) => button.textContent?.trim() === "Show more",
370
+ );
371
+ expect(showMore).toBeDefined();
372
+ await act(async () => {
373
+ showMore?.click();
374
+ });
375
+ expect(container.querySelectorAll("[data-chat-first-app]")).toHaveLength(6);
376
+
377
+ const allApps = container.querySelector<HTMLButtonElement>(
378
+ "[data-chat-first-all-apps]",
379
+ );
380
+ expect(allApps?.textContent?.trim()).toBe("All apps");
381
+ await act(async () => {
382
+ allApps?.click();
383
+ });
384
+ expect(paths.at(-1)).toBe("/apps");
385
+ });
386
+
387
+ it("omits the Chats section when chat-first has no chats", async () => {
388
+ clientState.threads = [];
389
+ await act(async () => {
390
+ root.render(
391
+ <MemoryRouter initialEntries={["/chat"]}>
392
+ <TooltipProvider>
393
+ <NavContent
394
+ chatFirstMode
395
+ chatFirstApps={[{ id: "mail", name: "Mail" }]}
396
+ />
397
+ </TooltipProvider>
398
+ </MemoryRouter>,
399
+ );
400
+ });
401
+
402
+ expect(container.textContent).not.toContain("Chats");
403
+ expect(container.querySelector("[data-chat-first-app]")).not.toBeNull();
404
+ expect(
405
+ container.querySelector("[data-chat-first-app] span[style]"),
406
+ ).toBeNull();
407
+ });
408
+
409
+ it("opens a workspace app in the main app route from the left rail", async () => {
410
+ const paths: string[] = [];
411
+ await act(async () => {
412
+ root.render(
413
+ <MemoryRouter initialEntries={["/chat"]}>
414
+ <TooltipProvider>
415
+ <NavContent
416
+ chatFirstMode
417
+ chatFirstApps={[{ id: "mail", name: "Mail" }]}
418
+ onChatFirstAppOpen={(app) => paths.push(`/apps/${app.id}`)}
419
+ />
420
+ </TooltipProvider>
421
+ </MemoryRouter>,
422
+ );
423
+ });
424
+
425
+ await act(async () => {
426
+ container
427
+ .querySelector<HTMLButtonElement>("[data-chat-first-app] button")
428
+ ?.click();
429
+ });
430
+ expect(paths).toEqual(["/apps/mail"]);
431
+ });
432
+
313
433
  it("uses the shared chat history rail and retains thread actions", async () => {
314
434
  await act(async () => {
315
435
  root.render(