@agent-native/dispatch 0.27.19 → 0.28.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.
- package/dist/actions/update-workspace-app-metadata.js +1 -1
- package/dist/actions/update-workspace-app-metadata.js.map +1 -1
- package/dist/actions/view-screen.js +50 -1
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +3 -8
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-host.d.ts +17 -0
- package/dist/components/workspace-app-host.d.ts.map +1 -1
- package/dist/components/workspace-app-host.js +90 -8
- package/dist/components/workspace-app-host.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +2 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +18 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +0 -1
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/workspace-app-chat-proxy.d.ts +52 -0
- package/dist/server/lib/workspace-app-chat-proxy.d.ts.map +1 -0
- package/dist/server/lib/workspace-app-chat-proxy.js +306 -0
- package/dist/server/lib/workspace-app-chat-proxy.js.map +1 -0
- package/dist/server/plugins/core-routes.d.ts +3 -2
- package/dist/server/plugins/core-routes.d.ts.map +1 -1
- package/dist/server/plugins/core-routes.js +12 -4
- package/dist/server/plugins/core-routes.js.map +1 -1
- package/dist/shared/chat-first-pane.d.ts +8 -0
- package/dist/shared/chat-first-pane.d.ts.map +1 -0
- package/dist/shared/chat-first-pane.js +8 -0
- package/dist/shared/chat-first-pane.js.map +1 -0
- package/dist/shared/workspace-app-chat.d.ts +23 -0
- package/dist/shared/workspace-app-chat.d.ts.map +1 -0
- package/dist/shared/workspace-app-chat.js +45 -0
- package/dist/shared/workspace-app-chat.js.map +1 -0
- package/package.json +2 -2
- package/src/actions/update-workspace-app-metadata.ts +1 -1
- package/src/actions/view-screen.spec.ts +93 -1
- package/src/actions/view-screen.ts +74 -1
- package/src/components/layout/Layout.app-chat.spec.ts +22 -13
- package/src/components/layout/Layout.tsx +6 -19
- package/src/components/workspace-app-chat-rail.spec.tsx +182 -0
- package/src/components/workspace-app-host.tsx +172 -21
- package/src/hooks/use-navigation-state.spec.ts +33 -0
- package/src/hooks/use-navigation-state.ts +28 -1
- package/src/server/lib/app-creation-store.spec.ts +32 -20
- package/src/server/lib/app-creation-store.ts +0 -1
- package/src/server/lib/workspace-app-chat-proxy.spec.ts +322 -0
- package/src/server/lib/workspace-app-chat-proxy.ts +389 -0
- package/src/server/plugins/core-routes.ts +21 -4
- package/src/shared/chat-first-pane.ts +7 -0
- package/src/shared/workspace-app-chat.ts +45 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AgentSidebar } from "@agent-native/core/client/agent-chat";
|
|
2
|
+
import { agentNativePath } from "@agent-native/core/client/api-path";
|
|
2
3
|
import {
|
|
3
4
|
ChatFirstAppPane,
|
|
4
5
|
defaultChatFirstCopy,
|
|
@@ -11,9 +12,20 @@ import {
|
|
|
11
12
|
} from "@agent-native/core/client/hooks";
|
|
12
13
|
import { useT } from "@agent-native/core/client/i18n";
|
|
13
14
|
import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
IconAlertTriangle,
|
|
17
|
+
IconArrowLeft,
|
|
18
|
+
IconClockHour4,
|
|
19
|
+
} from "@tabler/icons-react";
|
|
15
20
|
import { useTheme } from "next-themes";
|
|
16
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
useCallback,
|
|
23
|
+
useEffect,
|
|
24
|
+
useMemo,
|
|
25
|
+
useRef,
|
|
26
|
+
useState,
|
|
27
|
+
type ReactNode,
|
|
28
|
+
} from "react";
|
|
17
29
|
import { Link } from "react-router";
|
|
18
30
|
|
|
19
31
|
import { isEmbedSessionExpiredMessage } from "../lib/embed-session-recovery";
|
|
@@ -25,7 +37,9 @@ import {
|
|
|
25
37
|
type WorkspaceAppSummary,
|
|
26
38
|
} from "../lib/workspace-apps";
|
|
27
39
|
import { DISPATCH_WORKSPACE_SSO_FLAG } from "../shared/feature-flags";
|
|
40
|
+
import { workspaceAppChatProxyPath } from "../shared/workspace-app-chat";
|
|
28
41
|
import { ActionQueryError } from "./action-query-error";
|
|
42
|
+
import { Alert, AlertDescription } from "./ui/alert";
|
|
29
43
|
import { Badge } from "./ui/badge";
|
|
30
44
|
import { Button } from "./ui/button";
|
|
31
45
|
import { Skeleton } from "./ui/skeleton";
|
|
@@ -67,6 +81,160 @@ export function buildChatFirstEmbedSessionInput(
|
|
|
67
81
|
return { app: appId, path, chrome: "minimal" };
|
|
68
82
|
}
|
|
69
83
|
|
|
84
|
+
async function readWorkspaceAppChatProxyError(
|
|
85
|
+
response: Response,
|
|
86
|
+
): Promise<string> {
|
|
87
|
+
let body: string;
|
|
88
|
+
try {
|
|
89
|
+
body = await response.text();
|
|
90
|
+
} catch {
|
|
91
|
+
// coercion-ok: an unreadable body is reported as such, not as an empty error.
|
|
92
|
+
return `Agent chat proxy returned ${response.status} with an unreadable body.`;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(body) as { error?: unknown };
|
|
96
|
+
if (typeof parsed.error === "string" && parsed.error) return parsed.error;
|
|
97
|
+
} catch {
|
|
98
|
+
// coercion-ok: a non-JSON body is still reportable as the status line.
|
|
99
|
+
}
|
|
100
|
+
return body.trim() || `Agent chat proxy returned ${response.status}.`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Point the app pane's chat rail at the app's OWN agent through the Dispatch
|
|
105
|
+
* proxy, and prove the proxy answers before claiming it works. A rail that
|
|
106
|
+
* quietly fell back to Dispatch's agent would look identical while running the
|
|
107
|
+
* wrong tools, instructions, and app resources, so a failed probe is a visible
|
|
108
|
+
* error state instead.
|
|
109
|
+
*/
|
|
110
|
+
function useWorkspaceAppChatApi(appId: string) {
|
|
111
|
+
const apiUrl = useMemo(
|
|
112
|
+
() => agentNativePath(workspaceAppChatProxyPath(appId)),
|
|
113
|
+
[appId],
|
|
114
|
+
);
|
|
115
|
+
const [attempt, setAttempt] = useState(0);
|
|
116
|
+
const [unavailable, setUnavailable] = useState(false);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
let cancelled = false;
|
|
120
|
+
setUnavailable(false);
|
|
121
|
+
// `/mode` is the app's own dev-mode surface: reaching it proves the proxy
|
|
122
|
+
// minted an app session and the app's agent-chat routes answer.
|
|
123
|
+
void fetch(`${apiUrl}/mode`, { credentials: "include" })
|
|
124
|
+
.then(async (response) => {
|
|
125
|
+
if (response.ok) return;
|
|
126
|
+
throw new Error(await readWorkspaceAppChatProxyError(response));
|
|
127
|
+
})
|
|
128
|
+
.catch((cause: unknown) => {
|
|
129
|
+
if (cancelled) return;
|
|
130
|
+
console.warn(
|
|
131
|
+
`[dispatch] app chat proxy unavailable for ${appId}`,
|
|
132
|
+
cause,
|
|
133
|
+
);
|
|
134
|
+
setUnavailable(true);
|
|
135
|
+
});
|
|
136
|
+
return () => {
|
|
137
|
+
cancelled = true;
|
|
138
|
+
};
|
|
139
|
+
}, [apiUrl, appId, attempt]);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
apiUrl,
|
|
143
|
+
unavailable,
|
|
144
|
+
retry: useCallback(() => setAttempt((value) => value + 1), []),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface WorkspaceAppChatRailProps {
|
|
149
|
+
appId: string;
|
|
150
|
+
appName: string;
|
|
151
|
+
children: ReactNode;
|
|
152
|
+
copy?: ChatFirstCopy;
|
|
153
|
+
agentPageHref?: string;
|
|
154
|
+
onFullscreenRequest?: () => void;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The chat beside an open workspace app. Every surface that hosts an app pane
|
|
159
|
+
* must go through here so the rail is always the app's own agent — same tools,
|
|
160
|
+
* AGENTS.md, skills, app-scoped resources, and dev-mode surface as the app's
|
|
161
|
+
* native chat — and so an unreachable app is one visible error state rather
|
|
162
|
+
* than a per-surface silent handoff back to Dispatch's agent.
|
|
163
|
+
*/
|
|
164
|
+
export function WorkspaceAppChatRail({
|
|
165
|
+
appId,
|
|
166
|
+
appName,
|
|
167
|
+
children,
|
|
168
|
+
copy = defaultChatFirstCopy,
|
|
169
|
+
agentPageHref,
|
|
170
|
+
onFullscreenRequest,
|
|
171
|
+
}: WorkspaceAppChatRailProps) {
|
|
172
|
+
const t = useT();
|
|
173
|
+
const appChat = useWorkspaceAppChatApi(appId);
|
|
174
|
+
|
|
175
|
+
if (appChat.unavailable) {
|
|
176
|
+
return (
|
|
177
|
+
<div className="flex h-full min-h-0">
|
|
178
|
+
<div
|
|
179
|
+
data-dispatch-app-chat-unavailable
|
|
180
|
+
className="w-88 shrink-0 overflow-auto border-r p-4"
|
|
181
|
+
>
|
|
182
|
+
<Alert variant="destructive">
|
|
183
|
+
<IconAlertTriangle className="size-4" />
|
|
184
|
+
<AlertDescription className="flex flex-wrap items-center justify-between gap-3">
|
|
185
|
+
<span>
|
|
186
|
+
{t("dispatch.pages.appChatUnavailable", {
|
|
187
|
+
defaultValue:
|
|
188
|
+
"Dispatch could not connect to {{name}}'s agent, so its chat is unavailable here.",
|
|
189
|
+
name: appName,
|
|
190
|
+
})}
|
|
191
|
+
</span>
|
|
192
|
+
<Button
|
|
193
|
+
type="button"
|
|
194
|
+
variant="outline"
|
|
195
|
+
size="sm"
|
|
196
|
+
onClick={appChat.retry}
|
|
197
|
+
>
|
|
198
|
+
{copy("retry")}
|
|
199
|
+
</Button>
|
|
200
|
+
</AlertDescription>
|
|
201
|
+
</Alert>
|
|
202
|
+
</div>
|
|
203
|
+
<div className="min-w-0 flex-1">{children}</div>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<AgentSidebar
|
|
210
|
+
position="left"
|
|
211
|
+
defaultOpen
|
|
212
|
+
openStorageKey="dispatch-app-chat"
|
|
213
|
+
storageKey={`dispatch-app-chat:${appId}`}
|
|
214
|
+
scope={{
|
|
215
|
+
type: "workspace-app",
|
|
216
|
+
id: appId,
|
|
217
|
+
label: appName,
|
|
218
|
+
contextKey: `workspace-app:${appId}`,
|
|
219
|
+
}}
|
|
220
|
+
// The app's own server answers this chat, so its tools, AGENTS.md,
|
|
221
|
+
// skills, app-scoped resources, and dev-mode surface are the real ones
|
|
222
|
+
// rather than a copy maintained inside Dispatch.
|
|
223
|
+
apiUrl={appChat.apiUrl}
|
|
224
|
+
agentChatSurface="app"
|
|
225
|
+
showTabBar
|
|
226
|
+
suppressInlineOpenApp
|
|
227
|
+
dynamicSuggestions={false}
|
|
228
|
+
suggestions={[]}
|
|
229
|
+
emptyStateText={`Ask about ${appName}`}
|
|
230
|
+
{...(agentPageHref ? { agentPageHref } : {})}
|
|
231
|
+
{...(onFullscreenRequest ? { onFullscreenRequest } : {})}
|
|
232
|
+
>
|
|
233
|
+
{children}
|
|
234
|
+
</AgentSidebar>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
70
238
|
export interface WorkspaceAppFrameApp {
|
|
71
239
|
id: string;
|
|
72
240
|
name: string;
|
|
@@ -250,26 +418,9 @@ export function WorkspaceAppFrame({
|
|
|
250
418
|
if (!chatSidebar) return appPane;
|
|
251
419
|
|
|
252
420
|
return (
|
|
253
|
-
<
|
|
254
|
-
position="left"
|
|
255
|
-
defaultOpen
|
|
256
|
-
openStorageKey="dispatch-app-chat"
|
|
257
|
-
storageKey={`dispatch-app-chat:${app.id}`}
|
|
258
|
-
scope={{
|
|
259
|
-
type: "workspace-app",
|
|
260
|
-
id: app.id,
|
|
261
|
-
label: app.name,
|
|
262
|
-
contextKey: `workspace-app:${app.id}`,
|
|
263
|
-
}}
|
|
264
|
-
agentChatSurface="app"
|
|
265
|
-
showTabBar
|
|
266
|
-
suppressInlineOpenApp
|
|
267
|
-
dynamicSuggestions={false}
|
|
268
|
-
suggestions={[]}
|
|
269
|
-
emptyStateText={`Ask about ${app.name}`}
|
|
270
|
-
>
|
|
421
|
+
<WorkspaceAppChatRail appId={app.id} appName={app.name} copy={copy}>
|
|
271
422
|
{appPane}
|
|
272
|
-
</
|
|
423
|
+
</WorkspaceAppChatRail>
|
|
273
424
|
);
|
|
274
425
|
}
|
|
275
426
|
|
|
@@ -42,6 +42,39 @@ describe("buildDispatchNavigationState", () => {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
it("keeps the embedded app identity on workspace app routes", () => {
|
|
46
|
+
expect(buildDispatchNavigationState("/apps/mail")).toEqual({
|
|
47
|
+
view: "workspace-app",
|
|
48
|
+
path: "/apps/mail",
|
|
49
|
+
workspaceAppId: "mail",
|
|
50
|
+
workspaceAppPath: "/",
|
|
51
|
+
});
|
|
52
|
+
expect(buildDispatchNavigationState("/apps/mail/inbox")).toEqual({
|
|
53
|
+
view: "workspace-app",
|
|
54
|
+
path: "/apps/mail/inbox",
|
|
55
|
+
workspaceAppId: "mail",
|
|
56
|
+
workspaceAppPath: "/inbox",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("still treats the apps index as the list", () => {
|
|
61
|
+
expect(buildDispatchNavigationState("/apps")).toEqual({
|
|
62
|
+
view: "apps",
|
|
63
|
+
path: "/apps",
|
|
64
|
+
});
|
|
65
|
+
expect(buildDispatchNavigationState("/apps/")).toEqual({
|
|
66
|
+
view: "apps",
|
|
67
|
+
path: "/apps/",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("reports an undecodable app route as a workspace app with no id", () => {
|
|
72
|
+
expect(buildDispatchNavigationState("/apps/%E0%A4%A")).toEqual({
|
|
73
|
+
view: "workspace-app",
|
|
74
|
+
path: "/apps/%E0%A4%A",
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
45
78
|
it("preserves dreams query context", () => {
|
|
46
79
|
expect(
|
|
47
80
|
buildDispatchNavigationState(
|
|
@@ -16,10 +16,16 @@ import type {
|
|
|
16
16
|
DispatchExtensionConfig,
|
|
17
17
|
DispatchNavItem,
|
|
18
18
|
} from "../components/index.js";
|
|
19
|
+
import {
|
|
20
|
+
workspaceAppIdFromRoute,
|
|
21
|
+
workspaceAppRoute,
|
|
22
|
+
} from "../lib/workspace-apps.js";
|
|
19
23
|
|
|
20
24
|
export interface NavigationState {
|
|
21
25
|
view: string;
|
|
22
26
|
path?: string;
|
|
27
|
+
workspaceAppId?: string;
|
|
28
|
+
workspaceAppPath?: string;
|
|
23
29
|
extensionId?: string;
|
|
24
30
|
extensionSlug?: string;
|
|
25
31
|
dreamId?: string;
|
|
@@ -153,6 +159,14 @@ export function buildDispatchNavigationState(
|
|
|
153
159
|
return state;
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
if (state.view === "workspace-app") {
|
|
163
|
+
const workspaceAppId = workspaceAppIdFromRoute(pathname);
|
|
164
|
+
if (workspaceAppId) {
|
|
165
|
+
state.workspaceAppId = workspaceAppId;
|
|
166
|
+
state.workspaceAppPath = pathname.replace(/^\/apps\/[^/]+/, "") || "/";
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
156
170
|
if (state.view === "dreams") {
|
|
157
171
|
const params = new URLSearchParams(search);
|
|
158
172
|
const dreamId = params.get("dreamId");
|
|
@@ -265,6 +279,12 @@ function resolveView(
|
|
|
265
279
|
}
|
|
266
280
|
if (pathname.startsWith("/browser-chat")) return "browser-chat";
|
|
267
281
|
if (pathname.startsWith("/chat")) return "chat";
|
|
282
|
+
// A route below /apps/ always embeds one app, so it must not collapse to the
|
|
283
|
+
// apps list. An id that fails to decode still resolves here without a
|
|
284
|
+
// `workspaceAppId`, which view-screen reports as an unknown app.
|
|
285
|
+
if (pathname.startsWith("/apps/") && pathname !== "/apps/") {
|
|
286
|
+
return "workspace-app";
|
|
287
|
+
}
|
|
268
288
|
if (pathname.startsWith("/apps")) return "apps";
|
|
269
289
|
if (pathname.startsWith("/operations")) return "operations";
|
|
270
290
|
if (pathname.startsWith("/metrics")) return "metrics";
|
|
@@ -291,7 +311,10 @@ function resolveView(
|
|
|
291
311
|
function resolvePath(
|
|
292
312
|
view?: string,
|
|
293
313
|
extensions?: DispatchExtensionConfig,
|
|
294
|
-
command?: Pick<
|
|
314
|
+
command?: Pick<
|
|
315
|
+
NavigationState,
|
|
316
|
+
"extensionId" | "threadId" | "workspaceAppId"
|
|
317
|
+
>,
|
|
295
318
|
): string | undefined {
|
|
296
319
|
switch (view) {
|
|
297
320
|
case "admin":
|
|
@@ -305,6 +328,10 @@ function resolvePath(
|
|
|
305
328
|
return "/overview";
|
|
306
329
|
case "apps":
|
|
307
330
|
return "/apps";
|
|
331
|
+
case "workspace-app":
|
|
332
|
+
return command?.workspaceAppId
|
|
333
|
+
? workspaceAppRoute(command.workspaceAppId)
|
|
334
|
+
: "/apps";
|
|
308
335
|
case "operations":
|
|
309
336
|
case "monitoring":
|
|
310
337
|
case "observability":
|
|
@@ -504,31 +504,43 @@ describe("listWorkspaceApps", () => {
|
|
|
504
504
|
}),
|
|
505
505
|
},
|
|
506
506
|
});
|
|
507
|
-
expect(mocks.getDbExec).toHaveBeenCalled();
|
|
508
507
|
});
|
|
509
508
|
|
|
510
|
-
it("
|
|
509
|
+
it("lets workspace members update app display metadata", async () => {
|
|
511
510
|
mocks.state.orgRole = "member";
|
|
512
511
|
stubNoPendingContext();
|
|
513
|
-
stubManifest([
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
512
|
+
stubManifest([
|
|
513
|
+
{
|
|
514
|
+
id: "todo",
|
|
515
|
+
name: "Todo",
|
|
516
|
+
description: "Original description",
|
|
517
|
+
path: "/todo",
|
|
518
|
+
},
|
|
519
|
+
]);
|
|
520
|
+
|
|
521
|
+
const updated = await runWithRequestContext(
|
|
522
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
523
|
+
() =>
|
|
524
|
+
updateWorkspaceAppMetadata({
|
|
525
|
+
appId: "todo",
|
|
526
|
+
name: "Todo Board",
|
|
527
|
+
description: "Tracks team work.",
|
|
528
|
+
}),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(updated).toMatchObject({
|
|
532
|
+
name: "Todo Board",
|
|
533
|
+
description: "Tracks team work.",
|
|
534
|
+
});
|
|
535
|
+
expect(mocks.settings.get("workspace-app-metadata:org:org-123")).toEqual({
|
|
536
|
+
apps: {
|
|
537
|
+
todo: expect.objectContaining({
|
|
538
|
+
name: "Todo Board",
|
|
539
|
+
description: "Tracks team work.",
|
|
540
|
+
updatedBy: "dev@example.test",
|
|
541
|
+
}),
|
|
542
|
+
},
|
|
528
543
|
});
|
|
529
|
-
expect(
|
|
530
|
-
mocks.settings.get("workspace-app-metadata:org:org-123"),
|
|
531
|
-
).toBeUndefined();
|
|
532
544
|
});
|
|
533
545
|
|
|
534
546
|
it("generates a concise seed description from an app prompt", () => {
|
|
@@ -1355,7 +1355,6 @@ export async function updateWorkspaceAppMetadata(input: {
|
|
|
1355
1355
|
name?: string | null;
|
|
1356
1356
|
description?: string | null;
|
|
1357
1357
|
}): Promise<WorkspaceAppSummary> {
|
|
1358
|
-
await assertCanManageAppCreationSettings();
|
|
1359
1358
|
const appId = input.appId.trim();
|
|
1360
1359
|
assertValidWorkspaceAppId(appId);
|
|
1361
1360
|
|