@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,6 +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
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
6
 
6
7
  import { TooltipProvider } from "./ui/tooltip";
@@ -44,22 +45,24 @@ describe("WorkspaceAppCard", () => {
44
45
  it("uses one explicit open action and one settings control", async () => {
45
46
  await act(async () => {
46
47
  root.render(
47
- <TooltipProvider>
48
- <WorkspaceAppCard
49
- app={{
50
- id: "analytics",
51
- name: "Analytics",
52
- path: "/analytics",
53
- description: "Explore product and growth performance.",
54
- status: "ready",
55
- }}
56
- />
57
- </TooltipProvider>,
48
+ <MemoryRouter>
49
+ <TooltipProvider>
50
+ <WorkspaceAppCard
51
+ app={{
52
+ id: "analytics",
53
+ name: "Analytics",
54
+ path: "/analytics",
55
+ description: "Explore product and growth performance.",
56
+ status: "ready",
57
+ }}
58
+ />
59
+ </TooltipProvider>
60
+ </MemoryRouter>,
58
61
  );
59
62
  });
60
63
 
61
64
  const appLink = container.querySelector<HTMLAnchorElement>(
62
- 'a[href="/analytics"]',
65
+ 'a[href="/apps/analytics"]',
63
66
  );
64
67
  expect(appLink).not.toBeNull();
65
68
  expect(appLink?.textContent).toContain("Open app");
@@ -67,25 +70,13 @@ describe("WorkspaceAppCard", () => {
67
70
  expect(appLink?.getAttribute("target")).toBeNull();
68
71
  expect(appLink?.getAttribute("rel")).toBeNull();
69
72
  expect(appLink?.querySelector("svg")).toBeNull();
70
- expect(container.textContent).not.toContain("/analytics");
73
+ expect(container.textContent).not.toContain("/apps/analytics");
71
74
 
72
- const openOptionsButton = container.querySelector<HTMLButtonElement>(
73
- 'button[aria-label="Open options for Analytics"]',
74
- );
75
- expect(openOptionsButton).not.toBeNull();
76
- await act(async () => {
77
- openOptionsButton?.dispatchEvent(
78
- new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
79
- );
80
- });
81
- const openInlineLink = Array.from(
82
- document.querySelectorAll<HTMLAnchorElement>('[role="menu"] a'),
83
- ).find((anchor) => anchor.textContent?.includes("Open inline"));
84
- const openNewTabLink = Array.from(
85
- document.querySelectorAll<HTMLAnchorElement>('[role="menu"] a'),
86
- ).find((anchor) => anchor.textContent?.includes("Open in new tab"));
87
- expect(openInlineLink?.getAttribute("target")).toBeNull();
88
- expect(openNewTabLink?.getAttribute("target")).toBe("_blank");
75
+ expect(
76
+ container.querySelector(
77
+ 'button[aria-label="Open options for Analytics"]',
78
+ ),
79
+ ).toBeNull();
89
80
 
90
81
  const settingsButton = container.querySelector<HTMLButtonElement>(
91
82
  'button[aria-label="Settings for Analytics"]',
@@ -107,17 +98,19 @@ describe("WorkspaceAppCard", () => {
107
98
  it("labels per-app context as agent resources while preserving workspace scope", async () => {
108
99
  await act(async () => {
109
100
  root.render(
110
- <TooltipProvider>
111
- <WorkspaceAppCard
112
- app={{
113
- id: "analytics",
114
- name: "Analytics",
115
- path: "/analytics",
116
- description: "Explore product and growth performance.",
117
- status: "ready",
118
- }}
119
- />
120
- </TooltipProvider>,
101
+ <MemoryRouter>
102
+ <TooltipProvider>
103
+ <WorkspaceAppCard
104
+ app={{
105
+ id: "analytics",
106
+ name: "Analytics",
107
+ path: "/analytics",
108
+ description: "Explore product and growth performance.",
109
+ status: "ready",
110
+ }}
111
+ />
112
+ </TooltipProvider>
113
+ </MemoryRouter>,
121
114
  );
122
115
  });
123
116
 
@@ -149,17 +142,19 @@ describe("WorkspaceAppCard", () => {
149
142
  it("opens pending Builder apps in a new tab", async () => {
150
143
  await act(async () => {
151
144
  root.render(
152
- <TooltipProvider>
153
- <WorkspaceAppCard
154
- app={{
155
- id: "new-app",
156
- name: "New app",
157
- path: "/new-app",
158
- builderUrl: "https://builder.example.com/projects/new-app",
159
- status: "pending",
160
- }}
161
- />
162
- </TooltipProvider>,
145
+ <MemoryRouter>
146
+ <TooltipProvider>
147
+ <WorkspaceAppCard
148
+ app={{
149
+ id: "new-app",
150
+ name: "New app",
151
+ path: "/new-app",
152
+ builderUrl: "https://builder.example.com/projects/new-app",
153
+ status: "pending",
154
+ }}
155
+ />
156
+ </TooltipProvider>
157
+ </MemoryRouter>,
163
158
  );
164
159
  });
165
160
 
@@ -177,20 +172,22 @@ describe("WorkspaceAppCard", () => {
177
172
  it("shows ownership metadata in the settings menu", async () => {
178
173
  await act(async () => {
179
174
  root.render(
180
- <TooltipProvider>
181
- <WorkspaceAppCard
182
- app={{
183
- id: "analytics",
184
- name: "Analytics",
185
- path: "/analytics",
186
- createdAt: "2026-07-28T12:00:00.000Z",
187
- createdBy: "creator@example.com",
188
- owner: "owner@example.com",
189
- teams: ["Growth", "Operations"],
190
- status: "ready",
191
- }}
192
- />
193
- </TooltipProvider>,
175
+ <MemoryRouter>
176
+ <TooltipProvider>
177
+ <WorkspaceAppCard
178
+ app={{
179
+ id: "analytics",
180
+ name: "Analytics",
181
+ path: "/analytics",
182
+ createdAt: "2026-07-28T12:00:00.000Z",
183
+ createdBy: "creator@example.com",
184
+ owner: "owner@example.com",
185
+ teams: ["Growth", "Operations"],
186
+ status: "ready",
187
+ }}
188
+ />
189
+ </TooltipProvider>
190
+ </MemoryRouter>,
194
191
  );
195
192
  });
196
193
 
@@ -20,12 +20,14 @@ import {
20
20
  IconWorld,
21
21
  } from "@tabler/icons-react";
22
22
  import { useEffect, useState, type FormEvent } from "react";
23
+ import { useNavigate } from "react-router";
23
24
  import { toast } from "sonner";
24
25
 
25
26
  import { cn } from "../lib/utils";
26
27
  import {
27
28
  isPendingBuilderHref,
28
29
  workspaceAppHref,
30
+ workspaceAppRoute,
29
31
  type WorkspaceAppSummary,
30
32
  } from "../lib/workspace-apps";
31
33
  import { ActionQueryError } from "./action-query-error";
@@ -273,7 +275,9 @@ function WorkspaceAppOpenActions({
273
275
  app: WorkspaceAppSummary;
274
276
  href: string | null;
275
277
  }) {
278
+ const navigate = useNavigate();
276
279
  const openInNewTab = isPendingBuilderHref(app);
280
+ const appRoute = workspaceAppRoute(app.id);
277
281
 
278
282
  if (!href) {
279
283
  return (
@@ -286,11 +290,14 @@ function WorkspaceAppOpenActions({
286
290
  return (
287
291
  <AppOpenActions
288
292
  name={app.name}
289
- href={href}
293
+ href={openInNewTab ? href : appRoute}
290
294
  target={openInNewTab ? "_blank" : undefined}
291
295
  rel={openInNewTab ? "noreferrer" : undefined}
292
- showInlineOption
293
- showNewTabOption
296
+ showInlineOption={openInNewTab}
297
+ onOpenInline={() => {
298
+ navigate(appRoute);
299
+ }}
300
+ showNewTabOption={openInNewTab}
294
301
  />
295
302
  );
296
303
  }
@@ -153,16 +153,15 @@ describe("WorkspaceTemplateCard", () => {
153
153
  );
154
154
  });
155
155
 
156
- it("keeps add app inside the catalog split-button menu", async () => {
156
+ it("keeps add app out of the catalog split-button menu", async () => {
157
157
  await act(async () => {
158
158
  root.render(<WorkspaceTemplateCard template={template} catalog />);
159
159
  });
160
160
 
161
- expect(
162
- Array.from(container.querySelectorAll("button")).some((button) =>
163
- button.textContent?.includes("Add app"),
164
- ),
165
- ).toBe(false);
161
+ const addButton = Array.from(container.querySelectorAll("button")).find(
162
+ (button) => button.textContent?.includes("Add app"),
163
+ );
164
+ expect(addButton).toBeDefined();
166
165
 
167
166
  const optionsButton = container.querySelector<HTMLButtonElement>(
168
167
  'button[aria-label="Open options for Weekly report"]',
@@ -177,8 +176,8 @@ describe("WorkspaceTemplateCard", () => {
177
176
  const addItem = Array.from(
178
177
  document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
179
178
  ).find((item) => item.textContent?.includes("Add app"));
180
- expect(addItem).not.toBeUndefined();
181
- await act(async () => addItem?.click());
179
+ expect(addItem).toBeUndefined();
180
+ await act(async () => addButton?.click());
182
181
  expect(document.body.textContent).toContain("Choose the URL-safe id");
183
182
  });
184
183
 
@@ -214,17 +214,27 @@ export function WorkspaceTemplateCard({
214
214
  </div>
215
215
  <div className="flex shrink-0 items-center gap-2">
216
216
  {catalog ? (
217
- <AppOpenActions
218
- name={template.name}
219
- href={liveUrl ?? null}
220
- target="_blank"
221
- rel="noreferrer"
222
- labels={{
223
- addApp: labels.remix,
224
- openApp: labels.openApp,
225
- }}
226
- onAddApp={() => setOpen(true)}
227
- />
217
+ <>
218
+ {liveUrl ? (
219
+ <AppOpenActions
220
+ name={template.name}
221
+ href={liveUrl}
222
+ target="_blank"
223
+ rel="noreferrer"
224
+ labels={{ openApp: labels.openApp }}
225
+ showNewTabOption
226
+ />
227
+ ) : null}
228
+ <Button
229
+ type="button"
230
+ variant="outline"
231
+ size="sm"
232
+ onClick={() => setOpen(true)}
233
+ >
234
+ <IconPlus />
235
+ {labels.remix}
236
+ </Button>
237
+ </>
228
238
  ) : liveUrl ? (
229
239
  <Button variant="ghost" size="sm" className="shrink-0" asChild>
230
240
  <a href={liveUrl} target="_blank" rel="noreferrer">
@@ -1,21 +1,15 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
- const frameState = vi.hoisted(() => ({ inBuilderFrame: false }));
4
3
  const sendToAgentChatMock = vi.hoisted(() => vi.fn(() => "chat-tab"));
5
4
 
6
5
  vi.mock("@agent-native/core/client/agent-chat", () => ({
7
6
  sendToAgentChat: sendToAgentChatMock,
8
7
  }));
9
8
 
10
- vi.mock("@agent-native/core/client/host", () => ({
11
- isInBuilderFrame: () => frameState.inBuilderFrame,
12
- }));
13
-
14
9
  const { submitOverviewPrompt } = await import("./overview-chat.js");
15
10
 
16
11
  describe("submitOverviewPrompt", () => {
17
12
  beforeEach(() => {
18
- frameState.inBuilderFrame = false;
19
13
  sendToAgentChatMock.mockClear();
20
14
  });
21
15
 
@@ -50,19 +44,6 @@ describe("submitOverviewPrompt", () => {
50
44
  });
51
45
  });
52
46
 
53
- it("routes overview prompts to Builder chat inside Builder", () => {
54
- frameState.inBuilderFrame = true;
55
-
56
- const tabId = submitOverviewPrompt("ship the onboarding flow", "auto");
57
-
58
- expect(tabId).toBe("chat-tab");
59
- expect(sendToAgentChatMock).toHaveBeenCalledWith({
60
- message: "ship the onboarding flow",
61
- submit: true,
62
- type: "code",
63
- });
64
- });
65
-
66
47
  it("ignores empty prompts", () => {
67
48
  expect(submitOverviewPrompt(" ", "auto")).toBeNull();
68
49
  expect(sendToAgentChatMock).not.toHaveBeenCalled();
@@ -2,7 +2,6 @@ import {
2
2
  sendToAgentChat,
3
3
  type AgentChatMessage,
4
4
  } from "@agent-native/core/client/agent-chat";
5
- import { isInBuilderFrame } from "@agent-native/core/client/host";
6
5
 
7
6
  interface OverviewChatOptions {
8
7
  openSidebar?: boolean;
@@ -18,14 +17,6 @@ export function submitOverviewPrompt(
18
17
  const trimmed = message.trim();
19
18
  if (!trimmed) return null;
20
19
 
21
- if (isInBuilderFrame()) {
22
- return sendToAgentChat({
23
- message: trimmed,
24
- submit: true,
25
- type: "code",
26
- });
27
- }
28
-
29
20
  return sendToAgentChat({
30
21
  message: trimmed,
31
22
  submit: true,
@@ -49,7 +49,7 @@
49
49
  .dispatch-chat-panel [data-agent-empty-state="centered"] .dispatch-chat-intro {
50
50
  display: grid;
51
51
  width: 100%;
52
- max-width: min(684px, 90%);
52
+ max-width: min(750px, 90%);
53
53
  gap: 0.5rem;
54
54
  margin: 0 auto 1rem;
55
55
  text-align: center;
@@ -79,7 +79,7 @@
79
79
 
80
80
  .dispatch-chat-panel [data-agent-empty-state="centered"] .agent-composer-area {
81
81
  width: 100%;
82
- max-width: min(684px, 90%);
82
+ max-width: min(750px, 90%);
83
83
  padding: 0;
84
84
  }
85
85