@agent-native/dispatch 0.22.1 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/actions/provider-api-register.d.ts +1 -1
  2. package/dist/components/app-icon.d.ts +2 -1
  3. package/dist/components/app-icon.d.ts.map +1 -1
  4. package/dist/components/app-icon.js +51 -29
  5. package/dist/components/app-icon.js.map +1 -1
  6. package/dist/components/app-list-row.d.ts +1 -1
  7. package/dist/components/app-list-row.d.ts.map +1 -1
  8. package/dist/components/app-list-row.js +1 -1
  9. package/dist/components/app-list-row.js.map +1 -1
  10. package/dist/components/app-open-actions.d.ts +2 -1
  11. package/dist/components/app-open-actions.d.ts.map +1 -1
  12. package/dist/components/app-open-actions.js +2 -2
  13. package/dist/components/app-open-actions.js.map +1 -1
  14. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  15. package/dist/components/dispatch-control-plane.js +16 -17
  16. package/dist/components/dispatch-control-plane.js.map +1 -1
  17. package/dist/components/layout/Layout.d.ts.map +1 -1
  18. package/dist/components/layout/Layout.js +137 -66
  19. package/dist/components/layout/Layout.js.map +1 -1
  20. package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
  21. package/dist/components/layout/workspace-apps-rail.js +1 -1
  22. package/dist/components/layout/workspace-apps-rail.js.map +1 -1
  23. package/dist/components/other-apps-section.d.ts +3 -1
  24. package/dist/components/other-apps-section.d.ts.map +1 -1
  25. package/dist/components/other-apps-section.js +9 -6
  26. package/dist/components/other-apps-section.js.map +1 -1
  27. package/dist/components/workspace-app-card.d.ts.map +1 -1
  28. package/dist/components/workspace-app-card.js +7 -2
  29. package/dist/components/workspace-app-card.js.map +1 -1
  30. package/dist/components/workspace-template-card.d.ts.map +1 -1
  31. package/dist/components/workspace-template-card.js +2 -5
  32. package/dist/components/workspace-template-card.js.map +1 -1
  33. package/dist/lib/overview-chat.d.ts.map +1 -1
  34. package/dist/lib/overview-chat.js +0 -8
  35. package/dist/lib/overview-chat.js.map +1 -1
  36. package/dist/routes/pages/chat.js +1 -1
  37. package/dist/routes/pages/chat.js.map +1 -1
  38. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  39. package/dist/server/lib/mcp-gateway.js +14 -1
  40. package/dist/server/lib/mcp-gateway.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/components/app-icon.tsx +71 -43
  43. package/src/components/app-list-row.tsx +1 -1
  44. package/src/components/app-open-actions.tsx +4 -2
  45. package/src/components/dispatch-control-plane.spec.tsx +45 -7
  46. package/src/components/dispatch-control-plane.tsx +56 -75
  47. package/src/components/layout/Layout.spec.tsx +53 -0
  48. package/src/components/layout/Layout.tsx +227 -99
  49. package/src/components/layout/workspace-apps-rail.tsx +1 -0
  50. package/src/components/other-apps-section.tsx +61 -40
  51. package/src/components/workspace-app-card.spec.tsx +63 -66
  52. package/src/components/workspace-app-card.tsx +10 -3
  53. package/src/components/workspace-template-card.spec.tsx +7 -8
  54. package/src/components/workspace-template-card.tsx +21 -11
  55. package/src/lib/overview-chat.spec.ts +0 -19
  56. package/src/lib/overview-chat.ts +0 -9
  57. package/src/routes/pages/chat.tsx +1 -1
  58. package/src/server/lib/mcp-gateway.spec.ts +64 -2
  59. package/src/server/lib/mcp-gateway.ts +19 -1
  60. package/src/styles/dispatch-css.spec.ts +11 -0
  61. package/src/styles/dispatch.css +2 -2
@@ -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 = [];
@@ -117,6 +124,8 @@ describe("DispatchControlPlane", () => {
117
124
  });
118
125
 
119
126
  expect(container.textContent).toContain("Chat across your apps");
127
+ expect(container.querySelector('[class*="max-w-[750px]"]')).not.toBeNull();
128
+ expect(container.querySelector('[class*="max-w-[1000px]"]')).not.toBeNull();
120
129
  expect(container.textContent).not.toContain("Open chat");
121
130
  expect(container.textContent).not.toContain("Also");
122
131
  expect(container.textContent).not.toContain("active");
@@ -155,6 +164,36 @@ describe("DispatchControlPlane", () => {
155
164
  );
156
165
  });
157
166
 
167
+ it("keeps overview submissions on Dispatch Chat inside Builder frames", async () => {
168
+ clientState.inBuilderFrame = true;
169
+
170
+ await act(async () => {
171
+ root.render(
172
+ <MemoryRouter initialEntries={["/overview"]}>
173
+ <TooltipProvider>
174
+ <DispatchControlPlane />
175
+ </TooltipProvider>
176
+ </MemoryRouter>,
177
+ );
178
+ });
179
+
180
+ await act(async () => {
181
+ container.querySelector<HTMLButtonElement>("[data-placeholder]")?.click();
182
+ });
183
+
184
+ expect(clientState.navigateWithTransition).toHaveBeenCalledWith(
185
+ expect.any(Function),
186
+ "/chat",
187
+ expect.objectContaining({
188
+ state: {
189
+ dispatchPrompt: expect.objectContaining({
190
+ message: "Route onboarding work",
191
+ }),
192
+ },
193
+ }),
194
+ );
195
+ });
196
+
158
197
  it("shows mounted and connected apps together without duplicates", async () => {
159
198
  clientState.workspaceApps = [
160
199
  {
@@ -230,11 +269,10 @@ describe("DispatchControlPlane", () => {
230
269
  expect(container.textContent).toContain("Mail");
231
270
  expect(container.textContent).toContain("Clips");
232
271
  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
- );
272
+ expect(container.textContent).toContain("Apps");
273
+ expect(container.textContent).toContain("New");
274
+ expect(container.textContent).not.toContain("Other apps");
275
+ expect(container.textContent).not.toContain("available");
238
276
  expect(container.textContent).not.toContain("Archived app");
239
277
  expect(container.textContent).not.toContain("Duplicate onboarding");
240
278
  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 ? (
@@ -384,6 +384,59 @@ describe("Dispatch NavContent", () => {
384
384
  expect(paths.at(-1)).toBe("/apps");
385
385
  });
386
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
+ collapsible
396
+ chatFirstApps={[{ id: "mail", name: "Mail" }]}
397
+ />
398
+ </TooltipProvider>
399
+ </MemoryRouter>,
400
+ );
401
+ });
402
+
403
+ expect(container.textContent).not.toContain("Chats");
404
+ expect(container.querySelector("[data-chat-first-app]")).not.toBeNull();
405
+ expect(
406
+ container.querySelector("[data-chat-first-app] span[style]"),
407
+ ).not.toBeNull();
408
+ expect(
409
+ container.querySelector("[data-sidebar-footer-feedback]"),
410
+ ).not.toBeNull();
411
+ expect(
412
+ container.querySelector("[data-sidebar-footer-collapse]"),
413
+ ).not.toBeNull();
414
+ });
415
+
416
+ it("opens a workspace app in the main app route from the left rail", async () => {
417
+ const paths: string[] = [];
418
+ await act(async () => {
419
+ root.render(
420
+ <MemoryRouter initialEntries={["/chat"]}>
421
+ <TooltipProvider>
422
+ <NavContent
423
+ chatFirstMode
424
+ chatFirstApps={[{ id: "mail", name: "Mail" }]}
425
+ onChatFirstAppOpen={(app) => paths.push(`/apps/${app.id}`)}
426
+ />
427
+ </TooltipProvider>
428
+ </MemoryRouter>,
429
+ );
430
+ });
431
+
432
+ await act(async () => {
433
+ container
434
+ .querySelector<HTMLButtonElement>("[data-chat-first-app] button")
435
+ ?.click();
436
+ });
437
+ expect(paths).toEqual(["/apps/mail"]);
438
+ });
439
+
387
440
  it("uses the shared chat history rail and retains thread actions", async () => {
388
441
  await act(async () => {
389
442
  root.render(