@agent-native/dispatch 0.20.4 → 0.22.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 (87) hide show
  1. package/dist/actions/get-agent-thread-debug.d.ts +2 -0
  2. package/dist/actions/provider-api-register.d.ts +7 -7
  3. package/dist/actions/search-agent-threads.js +2 -2
  4. package/dist/actions/search-agent-threads.js.map +1 -1
  5. package/dist/actions/start-workspace-app-creation.js +1 -1
  6. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  7. package/dist/components/create-app-popover.d.ts.map +1 -1
  8. package/dist/components/create-app-popover.js +5 -71
  9. package/dist/components/create-app-popover.js.map +1 -1
  10. package/dist/components/layout/HeaderActions.d.ts +1 -17
  11. package/dist/components/layout/HeaderActions.d.ts.map +1 -1
  12. package/dist/components/layout/HeaderActions.js +1 -51
  13. package/dist/components/layout/HeaderActions.js.map +1 -1
  14. package/dist/components/layout/Layout.d.ts +23 -1
  15. package/dist/components/layout/Layout.d.ts.map +1 -1
  16. package/dist/components/layout/Layout.js +707 -18
  17. package/dist/components/layout/Layout.js.map +1 -1
  18. package/dist/components/ui/dialog.d.ts +1 -19
  19. package/dist/components/ui/dialog.d.ts.map +1 -1
  20. package/dist/components/ui/dialog.js +1 -22
  21. package/dist/components/ui/dialog.js.map +1 -1
  22. package/dist/components/ui/dropdown-menu.d.ts +1 -27
  23. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  24. package/dist/components/ui/dropdown-menu.js +1 -35
  25. package/dist/components/ui/dropdown-menu.js.map +1 -1
  26. package/dist/components/ui/popover.d.ts +2 -1
  27. package/dist/components/ui/popover.d.ts.map +1 -1
  28. package/dist/components/ui/popover.js +2 -1
  29. package/dist/components/ui/popover.js.map +1 -1
  30. package/dist/components/ui/select.d.ts +1 -13
  31. package/dist/components/ui/select.d.ts.map +1 -1
  32. package/dist/components/ui/select.js +1 -26
  33. package/dist/components/ui/select.js.map +1 -1
  34. package/dist/hooks/use-mobile.d.ts +1 -1
  35. package/dist/hooks/use-mobile.d.ts.map +1 -1
  36. package/dist/hooks/use-mobile.js +1 -15
  37. package/dist/hooks/use-mobile.js.map +1 -1
  38. package/dist/lib/automation-display.js +1 -1
  39. package/dist/lib/automation-display.js.map +1 -1
  40. package/dist/lib/workspace-apps.d.ts +6 -0
  41. package/dist/lib/workspace-apps.d.ts.map +1 -1
  42. package/dist/lib/workspace-apps.js +21 -0
  43. package/dist/lib/workspace-apps.js.map +1 -1
  44. package/dist/routes/pages/settings.d.ts.map +1 -1
  45. package/dist/routes/pages/settings.js +21 -1
  46. package/dist/routes/pages/settings.js.map +1 -1
  47. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  48. package/dist/routes/pages/thread-debug.js +288 -118
  49. package/dist/routes/pages/thread-debug.js.map +1 -1
  50. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  51. package/dist/server/lib/app-creation-store.js +13 -0
  52. package/dist/server/lib/app-creation-store.js.map +1 -1
  53. package/dist/server/lib/thread-debug-store.d.ts +2 -0
  54. package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
  55. package/dist/server/lib/thread-debug-store.js +6 -2
  56. package/dist/server/lib/thread-debug-store.js.map +1 -1
  57. package/dist/server/plugins/agent-chat.js +1 -0
  58. package/dist/server/plugins/agent-chat.js.map +1 -1
  59. package/dist/server/plugins/integrations.js +1 -1
  60. package/dist/server/plugins/integrations.js.map +1 -1
  61. package/package.json +3 -3
  62. package/src/actions/search-agent-threads.ts +2 -2
  63. package/src/actions/start-workspace-app-creation.ts +1 -1
  64. package/src/components/create-app-popover.spec.tsx +22 -0
  65. package/src/components/create-app-popover.tsx +30 -94
  66. package/src/components/layout/HeaderActions.tsx +1 -84
  67. package/src/components/layout/Layout.spec.tsx +35 -10
  68. package/src/components/layout/Layout.tsx +1394 -74
  69. package/src/components/ui/dialog.tsx +1 -120
  70. package/src/components/ui/dropdown-menu.tsx +1 -198
  71. package/src/components/ui/popover.tsx +3 -1
  72. package/src/components/ui/select.tsx +1 -158
  73. package/src/hooks/use-mobile.tsx +1 -21
  74. package/src/lib/automation-display.spec.ts +1 -1
  75. package/src/lib/automation-display.ts +1 -1
  76. package/src/lib/workspace-apps.ts +24 -0
  77. package/src/routes/pages/settings.tsx +72 -0
  78. package/src/routes/pages/thread-debug.spec.tsx +103 -5
  79. package/src/routes/pages/thread-debug.tsx +1051 -451
  80. package/src/server/lib/app-creation-store.spec.ts +13 -0
  81. package/src/server/lib/app-creation-store.ts +13 -0
  82. package/src/server/lib/mcp-gateway.spec.ts +18 -0
  83. package/src/server/lib/thread-debug-store.spec.ts +17 -0
  84. package/src/server/lib/thread-debug-store.ts +9 -2
  85. package/src/server/plugins/agent-chat.ts +1 -0
  86. package/src/server/plugins/integrations.ts +1 -1
  87. package/src/styles/dispatch.css +29 -0
@@ -7,7 +7,11 @@ import {
7
7
  import { PromptComposer } from "@agent-native/core/client/composer";
8
8
  import { isInBuilderFrame } from "@agent-native/core/client/host";
9
9
  import { useBuilderConnectFlow } from "@agent-native/core/client/settings/useBuilderStatus";
10
- import { getWorkspaceAppIdValidationError } from "@agent-native/core/shared";
10
+ import {
11
+ buildChatFirstAppCreationPrompt,
12
+ getWorkspaceAppIdValidationError,
13
+ titleFromChatFirstAppPrompt,
14
+ } from "@agent-native/core/shared";
11
15
  import {
12
16
  IconAlertTriangle,
13
17
  IconArrowLeft,
@@ -57,87 +61,6 @@ interface CreateAppPopoverProps {
57
61
  align?: "start" | "center" | "end";
58
62
  }
59
63
 
60
- function slugify(value: string): string {
61
- return value
62
- .toLowerCase()
63
- .replace(/[^a-z0-9]+/g, "-")
64
- .replace(/^-+|-+$/g, "")
65
- .replace(/^[^a-z]+/, "")
66
- .slice(0, 48);
67
- }
68
-
69
- function titleFromPrompt(prompt: string): string {
70
- const cleaned = prompt
71
- .replace(/\b(build|create|make|an?|the|app|tool|dashboard)\b/gi, " ")
72
- .replace(/\s+/g, " ")
73
- .trim();
74
- return slugify(cleaned || "new-app") || "new-app";
75
- }
76
-
77
- function buildAppCreationPrompt(input: {
78
- appId: string;
79
- prompt: string;
80
- selectedKeys: string[];
81
- selectedResources: WorkspaceResourceOption[];
82
- vaultAccessMode: VaultAccessMode;
83
- }): string {
84
- const keyList = input.selectedKeys.join(", ");
85
- const grantRequest =
86
- input.vaultAccessMode === "all-apps"
87
- ? `Dispatch vault access: all saved vault keys are available to every workspace app by default. No per-app vault grants are needed.`
88
- : keyList
89
- ? `Requested Dispatch vault key grants for this app: ${keyList}`
90
- : `Requested Dispatch vault key grants for this app: none`;
91
- const resourceList = input.selectedResources.length
92
- ? input.selectedResources
93
- .map(
94
- (resource) =>
95
- `- ${resource.name} (${resource.kind}, ${resource.path})`,
96
- )
97
- .join("\n")
98
- : "none";
99
-
100
- return [
101
- `Create a new agent-native app in this workspace.`,
102
- `This is a new workspace app request, not a feature request for the current app.`,
103
- ``,
104
- `Suggested app name: ${input.appId} (you may adjust the slug if it conflicts)`,
105
- `User prompt: ${input.prompt.trim()}`,
106
- `Generate a concise one-sentence app description from the user prompt before coding; save it in apps/${input.appId}/package.json "description" so Dispatch and A2A can describe the app.`,
107
- `If the user mentions a product or company such as Granola, Loom, Superhuman, Linear, or Notion, treat it as product inspiration unless they explicitly ask to connect to that service. Do not invent or require third-party API keys like GRANOLA_API_KEY just because a product is named.`,
108
- grantRequest,
109
- `Requested Dispatch workspace resources for this app:\n${resourceList}`,
110
- `Dispatch workspace resources with scope=all are inherited workspace context. Do not copy or sync them into the new app; every workspace app reads them at runtime and may override with app shared or personal resources.`,
111
- ``,
112
- `Pick a UI template that fits the user's prompt — analytics, assets, brain, calendar, chat, content, design, dispatch, forms, mail, slides, or clips when none of the others fit.`,
113
- `If you use the chat template, treat it as scaffolding only: the finished app must use the requested app's real name, home screen, navigation, package metadata, and manifest, and it must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind.`,
114
- `Use the workspace app layout: create it under apps/${input.appId}, mount it at /${input.appId}, keep it on the shared workspace database/hosting model, and avoid table-name collisions by namespacing any new domain tables to the app.`,
115
- `Important routing rule: from outside the app, link to /${input.appId}; inside apps/${input.appId}, React Router routes are app-local. Use <Link to="/review"> and navigate("/review"), not "/${input.appId}/review"; APP_BASE_PATH supplies the mounted prefix, and hardcoding it causes doubled URLs like /${input.appId}/${input.appId}/review.`,
116
- `Prefer useActionQuery/useActionMutation for actions. If you must raw-fetch framework endpoints, wrap them with agentNativePath("/_agent-native/actions/<name>") so mounted apps call the right URL.`,
117
- `Use relative workspace links like /${input.appId}. Do not hardcode localhost, 127.0.0.1, 8080, 8100, or any dev port; the active workspace gateway/browser origin owns the port.`,
118
- `Use the framework/template UI stack: shadcn/ui components and @tabler/icons-react. Do not add lucide-react or another icon library for standard UI.`,
119
- `Existing first-party apps are neighbors, not implementation details for this app. If the user's prompt mentions Mail, Calendar, Analytics, Brain, Assets, Dispatch, or other templates, treat them as existing hosted/connected apps that this app can link to or call through A2A/default connected agents. For example, Mail, Calendar, Analytics, Brain, and Assets already exist at https://mail.agent-native.com, https://calendar.agent-native.com, https://analytics.agent-native.com, https://brain.agent-native.com, and https://assets.agent-native.com.`,
120
- `Do not create wrapper apps or scaffold child apps/routes for Mail, Calendar, Analytics, Brain, Assets, etc. inside apps/${input.appId} just so this app can access them. If the request is a cross-app dashboard or overview, build only the new dashboard/overview app and delegate to the existing apps for domain work.`,
121
- `Only create another first-party app when the user explicitly asks for a customized app from that template; otherwise keep using the hosted/shared app so improvements to the base app keep flowing to users.`,
122
- `Do not satisfy this by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app.`,
123
- input.vaultAccessMode === "all-apps"
124
- ? `Do not create per-app Dispatch vault grants unless the workspace switches vault access to manual or the user explicitly asks for manual grants.`
125
- : keyList
126
- ? `After the app exists, grant the selected Dispatch vault keys to appId "${input.appId}" and sync them once the app server is available. Treat these as requested grants, not active grants before creation succeeds.`
127
- : `Do not grant any Dispatch vault keys unless the user asks later.`,
128
- input.selectedResources.length
129
- ? `After the app exists, grant the selected Dispatch workspace resources to appId "${input.appId}". Do not sync All-app workspace resources; they are inherited.`
130
- : `Do not grant any selected-only Dispatch workspace resources unless the user asks later.`,
131
- ``,
132
- `App readiness requirements before handing off:`,
133
- `- Ensure apps/${input.appId}/package.json exists with displayName/name and a concise description; Dispatch discovers workspace apps from apps/<app-id>/package.json, not a separate app registry.`,
134
- `- Update the app manifest/package/deploy metadata needed by the existing workspace deployment model.`,
135
- `- Ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() so /${input.appId} hydrates correctly.`,
136
- `- Verify the app's agent card/A2A metadata is ready so Dispatch can discover and delegate to the app after deployment. Every sibling workspace app is available over A2A by default through call-agent, with names and descriptions from the workspace app registry.`,
137
- `When it is ready, start or update the workspace dev server and navigate the user to the absolute path /${input.appId} on the workspace origin. Do not prefix with /dispatch/, /apps/, /workspace/, or any other Dispatch tab — the new app is mounted at the workspace root, not under Dispatch. If you have a navigate tool available, pass /${input.appId} verbatim; if you only have a window.location-style escape hatch, set it to /${input.appId}.`,
138
- ].join("\n");
139
- }
140
-
141
64
  async function fetchJson(url: string, init?: RequestInit): Promise<any> {
142
65
  const res = await fetch(url, init);
143
66
  const data = await res.json().catch(() => null);
@@ -168,6 +91,8 @@ const ERROR_FAILURE_REASONS = new Set([
168
91
  "credential-store-unavailable",
169
92
  "settings-management-required",
170
93
  ]);
94
+ const LOCAL_APP_DOCS_URL =
95
+ "https://agent-native.com/docs/multi-app-workspace#adding-a-new-app";
171
96
 
172
97
  function isErrorFailureReason(reason: string | null): boolean {
173
98
  return !!reason && ERROR_FAILURE_REASONS.has(reason);
@@ -292,14 +217,14 @@ export function CreateAppFlow({
292
217
  async function submit(rawPrompt: string) {
293
218
  const trimmed = rawPrompt.trim();
294
219
  if (!trimmed || isSubmitting) return;
295
- const appId = titleFromPrompt(trimmed);
220
+ const appId = titleFromChatFirstAppPrompt(trimmed);
296
221
  const validationError = getWorkspaceAppIdValidationError(appId);
297
222
  if (validationError) {
298
223
  setStatusMessage(validationError);
299
224
  return;
300
225
  }
301
226
 
302
- const message = buildAppCreationPrompt({
227
+ const message = buildChatFirstAppCreationPrompt({
303
228
  appId,
304
229
  prompt: trimmed,
305
230
  selectedKeys:
@@ -653,16 +578,27 @@ export function CreateAppFlow({
653
578
  ) : null}
654
579
  </div>
655
580
  {failureReason === "builder-not-connected" ? (
656
- <Button
657
- type="button"
658
- size="sm"
659
- variant="outline"
660
- onClick={() => connectFlow.start()}
661
- disabled={connectFlow.connecting}
662
- className="w-fit"
663
- >
664
- {connectFlow.connecting ? "Connecting..." : "Connect Builder"}
665
- </Button>
581
+ <div className="flex flex-wrap items-center gap-2">
582
+ <Button
583
+ type="button"
584
+ size="sm"
585
+ variant="outline"
586
+ onClick={() => connectFlow.start()}
587
+ disabled={connectFlow.connecting}
588
+ className="w-fit"
589
+ >
590
+ {connectFlow.connecting ? "Connecting..." : "Connect Builder"}
591
+ </Button>
592
+ <a
593
+ href={LOCAL_APP_DOCS_URL}
594
+ target="_blank"
595
+ rel="noreferrer"
596
+ data-create-app-local-link
597
+ className="inline-flex items-center gap-1 text-xs font-medium text-foreground underline underline-offset-2"
598
+ >
599
+ Create locally <IconArrowUpRight className="h-3 w-3" />
600
+ </a>
601
+ </div>
666
602
  ) : null}
667
603
  {failureReason === "credential-store-unavailable" ||
668
604
  failureReason === "builder-error" ? (
@@ -1,84 +1 @@
1
- import {
2
- useEffect,
3
- useSyncExternalStore,
4
- type ReactNode,
5
- type FC,
6
- } from "react";
7
-
8
- /**
9
- * External store for the page's header title + actions. We use an external
10
- * store (not React context) so that pages can inject ReactNode without
11
- * subscribing to the header state themselves — subscribing would trigger a
12
- * re-render on every update, which in turn creates new JSX, which updates
13
- * the store again, which re-renders… an infinite loop.
14
- *
15
- * Only <Header /> reads the store via useSyncExternalStore; pages only write.
16
- */
17
-
18
- type Listener = () => void;
19
-
20
- let currentTitle: ReactNode = null;
21
- let currentActions: ReactNode = null;
22
- const listeners = new Set<Listener>();
23
-
24
- function notify() {
25
- for (const l of listeners) l();
26
- }
27
-
28
- function subscribe(l: Listener): () => void {
29
- listeners.add(l);
30
- return () => {
31
- listeners.delete(l);
32
- };
33
- }
34
-
35
- /** Consumed only by <Header /> — returns the current title. */
36
- export function useHeaderTitle(): ReactNode {
37
- return useSyncExternalStore(
38
- subscribe,
39
- () => currentTitle,
40
- () => currentTitle,
41
- );
42
- }
43
-
44
- /** Consumed only by <Header /> — returns the current actions slot. */
45
- export function useHeaderActions(): ReactNode {
46
- return useSyncExternalStore(
47
- subscribe,
48
- () => currentActions,
49
- () => currentActions,
50
- );
51
- }
52
-
53
- /**
54
- * Provider is now a no-op wrapper for backwards compatibility — the state
55
- * lives in the module-level store above. Kept as a component so callers of
56
- * <HeaderActionsProvider> don't need to change.
57
- */
58
- export const HeaderActionsProvider: FC<{ children: ReactNode }> = ({
59
- children,
60
- }) => <>{children}</>;
61
-
62
- /** Mount a custom title into the app header. Cleans up on unmount. */
63
- export function useSetPageTitle(node: ReactNode) {
64
- useEffect(() => {
65
- currentTitle = node;
66
- notify();
67
- return () => {
68
- currentTitle = null;
69
- notify();
70
- };
71
- });
72
- }
73
-
74
- /** Mount ReactNode into the header's actions slot. Cleans up on unmount. */
75
- export function useSetHeaderActions(node: ReactNode) {
76
- useEffect(() => {
77
- currentActions = node;
78
- notify();
79
- return () => {
80
- currentActions = null;
81
- notify();
82
- };
83
- });
84
- }
1
+ export * from "@agent-native/toolkit/app-shell";
@@ -6,7 +6,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
 
7
7
  import { AdminShell } from "../admin-navigation";
8
8
  import { TooltipProvider } from "../ui/tooltip";
9
- import { formatThreadAge, NavContent } from "./Layout";
9
+ import {
10
+ buildChatFirstEmbedSessionInput,
11
+ formatThreadAge,
12
+ NavContent,
13
+ } from "./Layout";
10
14
 
11
15
  const clientState = vi.hoisted(() => ({
12
16
  createThread: vi.fn<() => Promise<string | null>>(),
@@ -36,6 +40,7 @@ vi.mock("@agent-native/core/client/agent-chat", () => ({
36
40
  }));
37
41
 
38
42
  vi.mock("@agent-native/core/client/api-path", () => ({
43
+ agentNativePath: (path: string) => path,
39
44
  appBasePath: () => "",
40
45
  appPath: (path: string) => path,
41
46
  }));
@@ -96,6 +101,16 @@ describe("formatThreadAge", () => {
96
101
  });
97
102
  });
98
103
 
104
+ describe("chat-first embed sessions", () => {
105
+ it("keeps the granted app id on app-relative embed requests", () => {
106
+ expect(buildChatFirstEmbedSessionInput("mail", "/mail/inbox")).toEqual({
107
+ app: "mail",
108
+ path: "/mail/inbox",
109
+ chrome: "minimal",
110
+ });
111
+ });
112
+ });
113
+
99
114
  describe("Dispatch NavContent", () => {
100
115
  let container: HTMLDivElement;
101
116
  let root: Root;
@@ -218,7 +233,7 @@ describe("Dispatch NavContent", () => {
218
233
  expect(shell?.querySelector('a[href="/admin/apps"]')).toBeNull();
219
234
  });
220
235
 
221
- it("shows ready workspace apps as host links and highlights the active app", async () => {
236
+ it("keeps workspace app discovery in the Apps destination", async () => {
222
237
  clientState.workspaceApps = [
223
238
  { id: "calendar", name: "Calendar", path: "/calendar", status: "ready" },
224
239
  { id: "pending", name: "Pending", path: "/pending", status: "pending" },
@@ -226,7 +241,7 @@ describe("Dispatch NavContent", () => {
226
241
 
227
242
  await act(async () => {
228
243
  root.render(
229
- <MemoryRouter initialEntries={["/apps/calendar"]}>
244
+ <MemoryRouter initialEntries={["/overview"]}>
230
245
  <TooltipProvider>
231
246
  <NavContent />
232
247
  </TooltipProvider>
@@ -234,14 +249,24 @@ describe("Dispatch NavContent", () => {
234
249
  );
235
250
  });
236
251
 
237
- const rail = container.querySelector("[data-dispatch-apps-rail]");
238
- expect(rail).not.toBeNull();
239
- expect(rail?.textContent).toContain("Calendar");
240
- expect(rail?.textContent).not.toContain("Pending");
252
+ expect(container.querySelector("[data-dispatch-apps-rail]")).toBeNull();
253
+ expect(container.querySelector('a[href="/apps"]')).not.toBeNull();
254
+ expect(container.textContent).not.toContain("Calendar");
255
+ expect(container.textContent).not.toContain("Pending");
241
256
 
242
- const calendarLink = rail?.querySelector('a[href="/apps/calendar"]');
243
- expect(calendarLink).not.toBeNull();
244
- expect(calendarLink?.getAttribute("aria-current")).toBe("page");
257
+ await act(async () => {
258
+ root.render(
259
+ <MemoryRouter initialEntries={["/apps/calendar"]}>
260
+ <TooltipProvider>
261
+ <NavContent />
262
+ </TooltipProvider>
263
+ </MemoryRouter>,
264
+ );
265
+ });
266
+
267
+ expect(container.querySelector('a[href="/apps"]')?.className).toContain(
268
+ "bg-sidebar-accent",
269
+ );
245
270
  });
246
271
 
247
272
  it("keeps Dispatch branding and anchors Settings above the organization picker", async () => {