@agent-native/dispatch 0.24.6 → 0.25.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 (151) hide show
  1. package/README.md +12 -1
  2. package/dist/actions/connect-external-agent.d.ts +18 -0
  3. package/dist/actions/connect-external-agent.d.ts.map +1 -0
  4. package/dist/actions/connect-external-agent.js +68 -0
  5. package/dist/actions/connect-external-agent.js.map +1 -0
  6. package/dist/actions/create-workspace-resource.d.ts +14 -14
  7. package/dist/actions/create-workspace-resource.js +9 -2
  8. package/dist/actions/create-workspace-resource.js.map +1 -1
  9. package/dist/actions/create_workspace_app_embed_session.d.ts +13 -0
  10. package/dist/actions/create_workspace_app_embed_session.d.ts.map +1 -0
  11. package/dist/actions/create_workspace_app_embed_session.js +30 -0
  12. package/dist/actions/create_workspace_app_embed_session.js.map +1 -0
  13. package/dist/actions/import-agent-pack.d.ts +96 -0
  14. package/dist/actions/import-agent-pack.d.ts.map +1 -0
  15. package/dist/actions/import-agent-pack.js +116 -0
  16. package/dist/actions/import-agent-pack.js.map +1 -0
  17. package/dist/actions/import-agent.d.ts +60 -0
  18. package/dist/actions/import-agent.d.ts.map +1 -0
  19. package/dist/actions/import-agent.js +82 -0
  20. package/dist/actions/import-agent.js.map +1 -0
  21. package/dist/actions/index.d.ts.map +1 -1
  22. package/dist/actions/index.js +10 -0
  23. package/dist/actions/index.js.map +1 -1
  24. package/dist/actions/list-agent-pack.d.ts +36 -0
  25. package/dist/actions/list-agent-pack.d.ts.map +1 -0
  26. package/dist/actions/list-agent-pack.js +32 -0
  27. package/dist/actions/list-agent-pack.js.map +1 -0
  28. package/dist/actions/list-workspace-resource-options.d.ts +1 -1
  29. package/dist/actions/list-workspace-resource-options.js +8 -1
  30. package/dist/actions/list-workspace-resource-options.js.map +1 -1
  31. package/dist/actions/list-workspace-resources.d.ts +1 -1
  32. package/dist/actions/list-workspace-resources.js +8 -1
  33. package/dist/actions/list-workspace-resources.js.map +1 -1
  34. package/dist/actions/navigate.js +1 -1
  35. package/dist/actions/navigate.js.map +1 -1
  36. package/dist/actions/provider-api-register.d.ts +10 -10
  37. package/dist/actions/update-workspace-resource.d.ts +13 -13
  38. package/dist/actions/view-screen.js +18 -1
  39. package/dist/actions/view-screen.js.map +1 -1
  40. package/dist/components/index.d.ts +1 -0
  41. package/dist/components/index.d.ts.map +1 -1
  42. package/dist/components/index.js +1 -0
  43. package/dist/components/index.js.map +1 -1
  44. package/dist/components/layout/Layout.d.ts +1 -8
  45. package/dist/components/layout/Layout.d.ts.map +1 -1
  46. package/dist/components/layout/Layout.js +33 -106
  47. package/dist/components/layout/Layout.js.map +1 -1
  48. package/dist/components/simple-agents-panel.d.ts +9 -0
  49. package/dist/components/simple-agents-panel.d.ts.map +1 -0
  50. package/dist/components/simple-agents-panel.js +405 -0
  51. package/dist/components/simple-agents-panel.js.map +1 -0
  52. package/dist/components/workspace-app-host.d.ts +29 -0
  53. package/dist/components/workspace-app-host.d.ts.map +1 -0
  54. package/dist/components/workspace-app-host.js +145 -0
  55. package/dist/components/workspace-app-host.js.map +1 -0
  56. package/dist/db/migrations.d.ts +2 -4
  57. package/dist/db/migrations.d.ts.map +1 -1
  58. package/dist/db/migrations.js +25 -0
  59. package/dist/db/migrations.js.map +1 -1
  60. package/dist/db/schema.js +1 -1
  61. package/dist/db/schema.js.map +1 -1
  62. package/dist/hooks/use-navigation-state.d.ts +1 -0
  63. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  64. package/dist/hooks/use-navigation-state.js +10 -0
  65. package/dist/hooks/use-navigation-state.js.map +1 -1
  66. package/dist/lib/agent-pack.d.ts +31 -0
  67. package/dist/lib/agent-pack.d.ts.map +1 -0
  68. package/dist/lib/agent-pack.js +149 -0
  69. package/dist/lib/agent-pack.js.map +1 -0
  70. package/dist/lib/simple-agent-profile.d.ts +36 -0
  71. package/dist/lib/simple-agent-profile.d.ts.map +1 -0
  72. package/dist/lib/simple-agent-profile.js +183 -0
  73. package/dist/lib/simple-agent-profile.js.map +1 -0
  74. package/dist/routes/pages/agents.d.ts.map +1 -1
  75. package/dist/routes/pages/agents.js +11 -1
  76. package/dist/routes/pages/agents.js.map +1 -1
  77. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  78. package/dist/routes/pages/apps.$appId.js +4 -95
  79. package/dist/routes/pages/apps.$appId.js.map +1 -1
  80. package/dist/routes/pages/chat.d.ts.map +1 -1
  81. package/dist/routes/pages/chat.js +62 -10
  82. package/dist/routes/pages/chat.js.map +1 -1
  83. package/dist/server/lib/agent-pack-store.d.ts +24 -0
  84. package/dist/server/lib/agent-pack-store.d.ts.map +1 -0
  85. package/dist/server/lib/agent-pack-store.js +22 -0
  86. package/dist/server/lib/agent-pack-store.js.map +1 -0
  87. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  88. package/dist/server/lib/dispatch-store.js +4 -0
  89. package/dist/server/lib/dispatch-store.js.map +1 -1
  90. package/dist/server/lib/mcp-gateway.d.ts +11 -0
  91. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  92. package/dist/server/lib/mcp-gateway.js +155 -9
  93. package/dist/server/lib/mcp-gateway.js.map +1 -1
  94. package/dist/server/lib/workspace-resources-store.d.ts +15 -1
  95. package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
  96. package/dist/server/lib/workspace-resources-store.js +11 -2
  97. package/dist/server/lib/workspace-resources-store.js.map +1 -1
  98. package/dist/server/plugins/agent-chat.js +13 -0
  99. package/dist/server/plugins/agent-chat.js.map +1 -1
  100. package/dist/shared/feature-flags.d.ts +7 -0
  101. package/dist/shared/feature-flags.d.ts.map +1 -0
  102. package/dist/shared/feature-flags.js +12 -0
  103. package/dist/shared/feature-flags.js.map +1 -0
  104. package/dist/shared/workspace-sso.d.ts +36 -0
  105. package/dist/shared/workspace-sso.d.ts.map +1 -0
  106. package/dist/shared/workspace-sso.js +99 -0
  107. package/dist/shared/workspace-sso.js.map +1 -0
  108. package/package.json +5 -3
  109. package/src/actions/connect-external-agent.ts +92 -0
  110. package/src/actions/create-workspace-resource.ts +9 -2
  111. package/src/actions/create_workspace_app_embed_session.ts +32 -0
  112. package/src/actions/import-agent-pack.ts +146 -0
  113. package/src/actions/import-agent.ts +102 -0
  114. package/src/actions/index.spec.ts +13 -0
  115. package/src/actions/index.ts +10 -0
  116. package/src/actions/list-agent-pack.ts +43 -0
  117. package/src/actions/list-workspace-resource-options.ts +8 -1
  118. package/src/actions/list-workspace-resources.ts +8 -1
  119. package/src/actions/navigate.ts +1 -1
  120. package/src/actions/view-screen.spec.ts +29 -1
  121. package/src/actions/view-screen.ts +19 -1
  122. package/src/components/app-icon.spec.tsx +10 -0
  123. package/src/components/index.ts +1 -0
  124. package/src/components/layout/Layout.spec.tsx +2 -0
  125. package/src/components/layout/Layout.tsx +59 -171
  126. package/src/components/simple-agents-panel.spec.tsx +73 -0
  127. package/src/components/simple-agents-panel.tsx +1226 -0
  128. package/src/components/workspace-app-host.spec.tsx +204 -0
  129. package/src/components/workspace-app-host.tsx +359 -0
  130. package/src/db/migrations.spec.ts +5 -1
  131. package/src/db/migrations.ts +28 -1
  132. package/src/db/schema.ts +1 -1
  133. package/src/hooks/use-navigation-state.spec.ts +25 -0
  134. package/src/hooks/use-navigation-state.ts +11 -0
  135. package/src/lib/agent-pack.spec.ts +47 -0
  136. package/src/lib/agent-pack.ts +226 -0
  137. package/src/lib/simple-agent-profile.spec.ts +77 -0
  138. package/src/lib/simple-agent-profile.ts +247 -0
  139. package/src/routes/index.spec.ts +1 -0
  140. package/src/routes/pages/agents.tsx +21 -1
  141. package/src/routes/pages/apps.$appId.tsx +3 -240
  142. package/src/routes/pages/chat.spec.tsx +62 -0
  143. package/src/routes/pages/chat.tsx +117 -9
  144. package/src/server/lib/agent-pack-store.ts +35 -0
  145. package/src/server/lib/dispatch-store.ts +8 -0
  146. package/src/server/lib/mcp-gateway.spec.ts +141 -0
  147. package/src/server/lib/mcp-gateway.ts +227 -15
  148. package/src/server/lib/workspace-resources-store.ts +9 -2
  149. package/src/server/plugins/agent-chat.ts +13 -0
  150. package/src/shared/feature-flags.ts +13 -0
  151. package/src/shared/workspace-sso.ts +118 -0
@@ -1,249 +1,12 @@
1
- import {
2
- useActionMutation,
3
- useActionQuery,
4
- } from "@agent-native/core/client/hooks";
5
- import { useT } from "@agent-native/core/client/i18n";
6
- import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
7
- import { IconArrowLeft, IconClockHour4 } from "@tabler/icons-react";
8
- import { useEffect, useMemo, useRef, useState } from "react";
9
- import { Link, useParams } from "react-router";
1
+ import { useParams } from "react-router";
10
2
 
11
- import { ActionQueryError } from "../../components/action-query-error";
12
- import { Badge } from "../../components/ui/badge";
13
- import { Button } from "../../components/ui/button";
14
- import { Skeleton } from "../../components/ui/skeleton";
15
- import { Spinner } from "../../components/ui/spinner";
16
- import { isEmbedSessionExpiredMessage } from "../../lib/embed-session-recovery";
17
- import {
18
- workspaceAppDirectHref,
19
- workspaceAppEmbedTarget,
20
- workspaceAppHref,
21
- type WorkspaceAppSummary,
22
- } from "../../lib/workspace-apps";
23
-
24
- interface EmbedSessionResult {
25
- startUrl: string;
26
- targetPath?: string;
27
- expiresAt?: number;
28
- app: string;
29
- }
30
-
31
- interface EmbedSessionInput {
32
- app: string;
33
- path?: string;
34
- url?: string;
35
- chrome: "minimal";
36
- }
3
+ import { WorkspaceAppHost } from "../../components/workspace-app-host";
37
4
 
38
5
  export function meta() {
39
6
  return [{ title: "Workspace app - Dispatch" }];
40
7
  }
41
8
 
42
9
  export default function WorkspaceAppRoute() {
43
- const t = useT();
44
10
  const { appId } = useParams();
45
- const appsQuery = useActionQuery("list-workspace-apps", {
46
- includeAgentCards: false,
47
- });
48
- const { data: apps = [], isLoading } = appsQuery;
49
- const app = useMemo(
50
- () =>
51
- (apps as WorkspaceAppSummary[]).find((item) => item.id === appId) ?? null,
52
- [appId, apps],
53
- );
54
- const href = app ? workspaceAppHref(app) : null;
55
- const [embedUrl, setEmbedUrl] = useState<string | null>(null);
56
- const [embedError, setEmbedError] = useState<Error | null>(null);
57
- const [embedAttempt, setEmbedAttempt] = useState(0);
58
- const embedFrameRef = useRef<HTMLIFrameElement>(null);
59
- const createEmbedSession = useActionMutation<
60
- EmbedSessionResult,
61
- EmbedSessionInput
62
- >("create_embed_session", {
63
- skipActionQueryInvalidation: true,
64
- });
65
- const embedInput = useMemo<EmbedSessionInput | null>(() => {
66
- if (!app || !href) return null;
67
- return {
68
- app: app.id,
69
- ...workspaceAppEmbedTarget(app),
70
- chrome: "minimal",
71
- };
72
- }, [app?.id, app?.path, app?.url, href]);
73
-
74
- useEffect(() => {
75
- if (!app || app.status === "pending" || !embedInput) return;
76
- let cancelled = false;
77
- setEmbedUrl(null);
78
- setEmbedError(null);
79
- void createEmbedSession
80
- .mutateAsync(embedInput)
81
- .then((result) => {
82
- if (!cancelled) setEmbedUrl(result.startUrl);
83
- })
84
- .catch((cause: unknown) => {
85
- if (!cancelled) {
86
- const directAppUrl = workspaceAppDirectHref(app, "/");
87
- if (directAppUrl) setEmbedUrl(directAppUrl);
88
- setEmbedError(
89
- cause instanceof Error ? cause : new Error(String(cause)),
90
- );
91
- }
92
- });
93
- return () => {
94
- cancelled = true;
95
- };
96
- }, [
97
- app?.id,
98
- app?.path,
99
- app?.status,
100
- createEmbedSession.mutateAsync,
101
- embedAttempt,
102
- embedInput,
103
- ]);
104
-
105
- useEffect(() => {
106
- const handleEmbedSessionExpired = (event: MessageEvent) => {
107
- if (!isEmbedSessionExpiredMessage(event, embedFrameRef.current, null))
108
- return;
109
- setEmbedAttempt((attempt) => attempt + 1);
110
- };
111
-
112
- window.addEventListener("message", handleEmbedSessionExpired);
113
- return () =>
114
- window.removeEventListener("message", handleEmbedSessionExpired);
115
- }, []);
116
-
117
- if (appsQuery.isError) {
118
- return (
119
- <div className="flex h-full min-h-0 items-center justify-center p-6">
120
- <div className="w-full max-w-2xl">
121
- <ActionQueryError
122
- error={appsQuery.error}
123
- onRetry={() => void appsQuery.refetch()}
124
- />
125
- </div>
126
- </div>
127
- );
128
- }
129
-
130
- if (isLoading && !app) {
131
- return (
132
- <div className="flex h-full min-h-0 items-center justify-center p-6">
133
- <div className="w-full max-w-2xl space-y-3 rounded-xl border bg-card p-6">
134
- <Skeleton className="h-5 w-48" />
135
- <Skeleton className="h-4 w-full" />
136
- <Skeleton className="h-4 w-2/3" />
137
- </div>
138
- </div>
139
- );
140
- }
141
-
142
- if (!app) {
143
- return (
144
- <div className="flex h-full min-h-0 items-center justify-center p-6">
145
- <div className="w-full max-w-2xl rounded-xl border bg-card p-6">
146
- <Button asChild size="sm" variant="ghost" className="-ml-2 mb-4">
147
- <Link to="/apps">
148
- <IconArrowLeft size={15} className="mr-1.5" />
149
- {t("dispatch.nav.apps")}
150
- </Link>
151
- </Button>
152
- <div className="space-y-3">
153
- <h2 className="text-base font-semibold text-foreground">
154
- {t("dispatch.pages.appNotFound")}
155
- </h2>
156
- <p className="text-sm text-muted-foreground">
157
- {t("dispatch.pages.pageNotFoundDescription")}
158
- </p>
159
- </div>
160
- </div>
161
- </div>
162
- );
163
- }
164
-
165
- if (app.status === "pending") {
166
- return (
167
- <div className="flex h-full min-h-0 items-center justify-center p-6">
168
- <div className="w-full max-w-2xl rounded-xl border bg-card p-6">
169
- <Button asChild size="sm" variant="ghost" className="-ml-2 mb-4">
170
- <Link to="/apps">
171
- <IconArrowLeft size={15} className="mr-1.5" />
172
- {t("dispatch.nav.apps")}
173
- </Link>
174
- </Button>
175
- <div className="space-y-4">
176
- <div className="flex flex-wrap items-center gap-2">
177
- <h2 className="text-base font-semibold text-foreground">
178
- {app.name}
179
- </h2>
180
- <Badge
181
- variant="outline"
182
- className="gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
183
- >
184
- <IconClockHour4 size={12} />
185
- {t("dispatch.pages.building")}
186
- </Badge>
187
- </div>
188
- <p className="text-sm text-muted-foreground">
189
- {t("dispatch.pages.appBuildingPrefix")}{" "}
190
- <span className="font-mono text-foreground">{app.path}</span>{" "}
191
- {t("dispatch.pages.appBuildingSuffix")}
192
- </p>
193
- {app.builderUrl ? (
194
- <Button asChild>
195
- <a
196
- href={withBuilderUtmTrackingParams(app.builderUrl, {
197
- campaign: "product",
198
- content: "dispatch_branch",
199
- })}
200
- target="_blank"
201
- rel="noreferrer"
202
- >
203
- {t("dispatch.pages.openBuilderBranch", {
204
- defaultValue: "Open in Builder",
205
- })}
206
- </a>
207
- </Button>
208
- ) : null}
209
- </div>
210
- </div>
211
- </div>
212
- );
213
- }
214
-
215
- return (
216
- <div
217
- data-dispatch-workspace-app-host
218
- className="flex h-full min-h-0 flex-col bg-background"
219
- >
220
- <div className="min-h-0 flex-1 bg-muted/20">
221
- {embedUrl ? (
222
- <iframe
223
- key={`${embedUrl}:${embedAttempt}`}
224
- data-dispatch-workspace-app-frame
225
- src={embedUrl}
226
- title={app.name}
227
- ref={embedFrameRef}
228
- referrerPolicy="no-referrer"
229
- className="h-full w-full border-0 bg-background"
230
- />
231
- ) : embedError ? (
232
- <div className="flex h-full items-center justify-center p-6">
233
- <div className="w-full max-w-2xl">
234
- <ActionQueryError
235
- error={embedError}
236
- onRetry={() => setEmbedAttempt((attempt) => attempt + 1)}
237
- />
238
- </div>
239
- </div>
240
- ) : (
241
- <div className="flex h-full items-center justify-center">
242
- <Spinner className="size-5 text-muted-foreground" />
243
- <span className="sr-only">{t("dispatch.pages.loading")}</span>
244
- </div>
245
- )}
246
- </div>
247
- </div>
248
- );
11
+ return <WorkspaceAppHost appId={appId} />;
249
12
  }
@@ -8,6 +8,15 @@ import ChatRoute from "./chat";
8
8
 
9
9
  const clientState = vi.hoisted(() => ({
10
10
  surfaceProps: null as Record<string, unknown> | null,
11
+ agents: [] as Array<{
12
+ id: string;
13
+ name: string;
14
+ description: string | null;
15
+ path: string;
16
+ content: string;
17
+ scope: "all" | "selected";
18
+ updatedAt: number;
19
+ }>,
11
20
  }));
12
21
 
13
22
  vi.mock("@agent-native/core/client/agent-chat", () => ({
@@ -15,6 +24,7 @@ vi.mock("@agent-native/core/client/agent-chat", () => ({
15
24
  clientState.surfaceProps = props;
16
25
  return <>{props.composerSlot as ReactNode}</>;
17
26
  },
27
+ insertAgentComposerReference: vi.fn(),
18
28
  markAgentChatHomeHandoff: vi.fn(),
19
29
  readChatFirstMode: () => true,
20
30
  navigateWithAgentChatViewTransition: (
@@ -24,6 +34,16 @@ vi.mock("@agent-native/core/client/agent-chat", () => ({
24
34
  sendToAgentChat: vi.fn(),
25
35
  }));
26
36
 
37
+ vi.mock("@agent-native/core/client/hooks", () => ({
38
+ useActionQuery: () => ({
39
+ data: clientState.agents,
40
+ error: null,
41
+ isError: false,
42
+ isLoading: false,
43
+ refetch: vi.fn(),
44
+ }),
45
+ }));
46
+
27
47
  vi.mock("../../components/layout/Layout", () => ({
28
48
  useDispatchExtensions: () => undefined,
29
49
  }));
@@ -52,6 +72,7 @@ describe("Dispatch ChatRoute", () => {
52
72
  vi.useFakeTimers();
53
73
  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
54
74
  clientState.surfaceProps = null;
75
+ clientState.agents = [];
55
76
  container = document.createElement("div");
56
77
  document.body.appendChild(container);
57
78
  root = createRoot(container);
@@ -118,4 +139,45 @@ describe("Dispatch ChatRoute", () => {
118
139
  );
119
140
  expect(container.textContent).not.toContain("Chat across your apps");
120
141
  });
142
+
143
+ it("keeps an agent chat scoped and preserves the scope in thread URLs", async () => {
144
+ clientState.agents = [
145
+ {
146
+ id: "agent-1",
147
+ name: "Research Partner",
148
+ description: "Synthesizes research",
149
+ path: "agents/research-partner.md",
150
+ content: "instructions",
151
+ scope: "all",
152
+ updatedAt: 1,
153
+ },
154
+ ];
155
+
156
+ await act(async () => {
157
+ root.render(
158
+ <MemoryRouter
159
+ initialEntries={["/chat?agent=agents/research-partner.md"]}
160
+ >
161
+ <ChatRoute />
162
+ </MemoryRouter>,
163
+ );
164
+ });
165
+
166
+ expect(clientState.surfaceProps).toMatchObject({
167
+ scope: {
168
+ type: "agent",
169
+ id: "agent-1",
170
+ label: "Research Partner",
171
+ },
172
+ storageKey: "dispatch-agent-agent-1",
173
+ composerPlaceholder: "Ask Research Partner...",
174
+ });
175
+ expect(
176
+ (
177
+ clientState.surfaceProps?.threadUrlSync as {
178
+ getPath: (id: string) => string;
179
+ }
180
+ ).getPath("thread-1"),
181
+ ).toBe("/chat/thread-1?agent=agents%2Fresearch-partner.md");
182
+ });
121
183
  });
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  AgentChatSurface,
3
+ insertAgentComposerReference,
3
4
  markAgentChatHomeHandoff,
4
5
  readChatFirstMode,
5
6
  } from "@agent-native/core/client/agent-chat";
6
7
  import { appBasePath, appPath } from "@agent-native/core/client/api-path";
8
+ import { useActionQuery } from "@agent-native/core/client/hooks";
7
9
  import { useT } from "@agent-native/core/client/i18n";
8
10
  import {
9
11
  useCallback,
@@ -14,11 +16,24 @@ import {
14
16
  } from "react";
15
17
  import { useLocation, useNavigate } from "react-router";
16
18
 
19
+ import { ActionQueryError } from "../../components/action-query-error";
17
20
  import { useDispatchExtensions } from "../../components/layout/Layout";
21
+ import { Skeleton } from "../../components/ui/skeleton";
18
22
  import { submitOverviewPrompt } from "../../lib/overview-chat";
19
23
 
20
- function chatThreadPath(threadId: string | null): string {
21
- return threadId ? `/chat/${encodeURIComponent(threadId)}` : "/chat";
24
+ interface WorkspaceAgentResource {
25
+ id: string;
26
+ name: string;
27
+ description: string | null;
28
+ path: string;
29
+ content: string;
30
+ scope: "all" | "selected";
31
+ updatedAt: number;
32
+ }
33
+
34
+ function chatThreadPath(threadId: string | null, agentPath?: string): string {
35
+ const path = threadId ? `/chat/${encodeURIComponent(threadId)}` : "/chat";
36
+ return agentPath ? `${path}?agent=${encodeURIComponent(agentPath)}` : path;
22
37
  }
23
38
 
24
39
  function stripBasePath(pathname: string): string {
@@ -94,6 +109,40 @@ export default function ChatRoute() {
94
109
  const location = useLocation();
95
110
  const navigate = useNavigate();
96
111
  const routeThreadId = threadIdFromPath(location.pathname);
112
+ const agentPath = new URLSearchParams(location.search).get("agent");
113
+ const agentsQuery = useActionQuery<WorkspaceAgentResource[]>(
114
+ "list-workspace-resources",
115
+ { kind: "agent" },
116
+ { enabled: Boolean(agentPath), staleTime: 30_000 },
117
+ );
118
+ const agent = useMemo(
119
+ () =>
120
+ agentPath
121
+ ? (agentsQuery.data ?? []).find((resource) => {
122
+ const profileId = resource.path
123
+ .replace(/^agents\//, "")
124
+ .replace(/\.md$/, "");
125
+ return (
126
+ resource.path === agentPath ||
127
+ resource.id === agentPath ||
128
+ profileId === agentPath
129
+ );
130
+ })
131
+ : undefined,
132
+ [agentPath, agentsQuery.data],
133
+ );
134
+ const agentScope = useMemo(
135
+ () =>
136
+ agent
137
+ ? {
138
+ type: "agent",
139
+ id: agent.id,
140
+ label: agent.name,
141
+ contextKey: `dispatch-agent:${agent.id}`,
142
+ }
143
+ : null,
144
+ [agent],
145
+ );
97
146
  const handledStateIds = useRef(new Set<string>());
98
147
  const extensions = useDispatchExtensions();
99
148
  const suppressInlineOpenApp =
@@ -109,10 +158,10 @@ export default function ChatRoute() {
109
158
  const threadUrlSync = useMemo<DispatchThreadUrlSync>(
110
159
  () => ({
111
160
  routeThreadId: routeThreadId ?? null,
112
- getPath: chatThreadPath,
161
+ getPath: (threadId) => chatThreadPath(threadId, agentPath ?? undefined),
113
162
  navigate: navigateThreadUrl,
114
163
  }),
115
- [routeThreadId, navigateThreadUrl],
164
+ [agentPath, routeThreadId, navigateThreadUrl],
116
165
  );
117
166
  const state = location.state as DispatchChatLocationState | null;
118
167
  const prompt = state?.dispatchPrompt;
@@ -176,14 +225,64 @@ export default function ChatRoute() {
176
225
  window.removeEventListener("agentNative.chatRunning", handleChatRunning);
177
226
  }, []);
178
227
 
228
+ useEffect(() => {
229
+ if (!agent) return;
230
+ const timer = window.setTimeout(() => {
231
+ insertAgentComposerReference(
232
+ {
233
+ label: agent.name,
234
+ icon: "agent",
235
+ source: "agent:custom",
236
+ refType: "custom-agent",
237
+ refId: agent.id,
238
+ refPath: agent.path,
239
+ slotKey: "dispatch-agent",
240
+ slotLabel: "Agent",
241
+ },
242
+ { openSidebar: false },
243
+ );
244
+ }, 0);
245
+ return () => window.clearTimeout(timer);
246
+ }, [agent?.id, agent?.name, agent?.path]);
247
+
248
+ if (agentPath && agentsQuery.isLoading) {
249
+ return (
250
+ <div className="flex h-full min-h-0 flex-col gap-4 bg-background px-4 py-6 sm:px-6">
251
+ <Skeleton className="mx-auto h-6 w-48" />
252
+ <Skeleton className="mx-auto h-12 w-full max-w-2xl" />
253
+ </div>
254
+ );
255
+ }
256
+
257
+ if (agentPath && agentsQuery.isError) {
258
+ return (
259
+ <div className="flex h-full min-h-0 items-center justify-center bg-background px-4">
260
+ <ActionQueryError
261
+ error={agentsQuery.error}
262
+ onRetry={() => void agentsQuery.refetch()}
263
+ />
264
+ </div>
265
+ );
266
+ }
267
+
268
+ if (agentPath && !agent) {
269
+ return (
270
+ <div className="flex h-full min-h-0 items-center justify-center bg-background px-4 text-sm text-muted-foreground">
271
+ This agent is no longer available.
272
+ </div>
273
+ );
274
+ }
275
+
179
276
  return (
180
277
  <div className="flex h-full min-h-0 flex-col bg-background">
181
278
  <DispatchAgentChatSurface
279
+ key={agent ? `agent-${agent.id}` : "dispatch"}
182
280
  mode="page"
183
281
  chatViewTransition
184
282
  className="dispatch-chat-panel px-4 sm:px-6"
185
283
  defaultMode="chat"
186
- storageKey="dispatch"
284
+ storageKey={agent ? `dispatch-agent-${agent.id}` : "dispatch"}
285
+ scope={agentScope}
187
286
  threadUrlSync={threadUrlSync}
188
287
  showHeader={false}
189
288
  showTabBar={false}
@@ -201,11 +300,20 @@ export default function ChatRoute() {
201
300
  composerLayoutVariant: "hero" as const,
202
301
  }
203
302
  : {})}
204
- composerPlaceholder={t("dispatch.pages.chatPromptPlaceholder", {
205
- defaultValue: "Ask Dispatch...",
206
- })}
303
+ composerPlaceholder={
304
+ agent
305
+ ? `Ask ${agent.name}...`
306
+ : t("dispatch.pages.chatPromptPlaceholder", {
307
+ defaultValue: "Ask Dispatch...",
308
+ })
309
+ }
207
310
  composerSlot={
208
- !prompt?.message ? (
311
+ agent ? (
312
+ <div className="dispatch-chat-intro">
313
+ <h1>{agent.name}</h1>
314
+ {agent.description ? <p>{agent.description}</p> : null}
315
+ </div>
316
+ ) : !prompt?.message ? (
209
317
  <div className="dispatch-chat-intro">
210
318
  <h1>
211
319
  {t("dispatch.pages.chatAcrossApps", {
@@ -0,0 +1,35 @@
1
+ import {
2
+ applyWorkspaceResourceCreate,
3
+ getWorkspaceResourceByPath,
4
+ requireWorkspaceResourceCtx,
5
+ type WorkspaceResourceInput,
6
+ } from "./workspace-resources-store.js";
7
+
8
+ /**
9
+ * Apply a pack as one logical operation. Approval is handled by the action
10
+ * before this helper is called, so this function must never call the
11
+ * approval-aware create wrapper for individual files.
12
+ */
13
+ export async function applyAgentPackCreate(
14
+ inputs: WorkspaceResourceInput[],
15
+ actor?: string,
16
+ ctx?: { ownerEmail: string; orgId: string | null },
17
+ ) {
18
+ const resourceCtx = ctx ?? requireWorkspaceResourceCtx();
19
+ const existing = await Promise.all(
20
+ inputs.map((input) => getWorkspaceResourceByPath(input.path, resourceCtx)),
21
+ );
22
+ const duplicate = existing.find(Boolean);
23
+ if (duplicate) {
24
+ throw new Error(
25
+ `An agent pack resource already exists at ${duplicate.path}. Rename the source before importing it.`,
26
+ );
27
+ }
28
+
29
+ const created = [];
30
+ for (const input of inputs) {
31
+ const resource = await applyWorkspaceResourceCreate(input, actor, ctx);
32
+ if (resource) created.push(resource);
33
+ }
34
+ return created;
35
+ }
@@ -588,6 +588,14 @@ async function applyApprovedRequest(request: DispatchApprovalRequest) {
588
588
  requestCtx,
589
589
  );
590
590
  }
591
+ if (request.changeType === "workspace-agent-pack.create") {
592
+ const { applyAgentPackCreate } = await import("./agent-pack-store.js");
593
+ return applyAgentPackCreate(
594
+ payload.inputs,
595
+ request.reviewedBy || currentOwnerEmail(),
596
+ requestCtx,
597
+ );
598
+ }
591
599
  if (request.changeType === "workspace-resource.update") {
592
600
  const { applyWorkspaceResourceUpdate } =
593
601
  await import("./workspace-resources-store.js");