@agent-native/dispatch 0.27.21 → 0.28.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.
- package/dist/actions/update-workspace-app-metadata.js +1 -1
- package/dist/actions/update-workspace-app-metadata.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +2 -7
- 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/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/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/components/layout/Layout.app-chat.spec.ts +22 -13
- package/src/components/layout/Layout.tsx +5 -18
- package/src/components/workspace-app-chat-rail.spec.tsx +182 -0
- package/src/components/workspace-app-host.tsx +172 -21
- 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/workspace-app-chat.ts +45 -0
|
@@ -125,6 +125,7 @@ import { Sheet, SheetContent, SheetDescription, SheetTitle } from "../ui/sheet";
|
|
|
125
125
|
import { Skeleton } from "../ui/skeleton";
|
|
126
126
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
|
127
127
|
import {
|
|
128
|
+
WorkspaceAppChatRail,
|
|
128
129
|
WorkspaceAppFrame,
|
|
129
130
|
WorkspaceAppKeepAlive,
|
|
130
131
|
} from "../workspace-app-host";
|
|
@@ -2349,28 +2350,14 @@ export function Layout({
|
|
|
2349
2350
|
"Workspace app";
|
|
2350
2351
|
const workspaceAppContent =
|
|
2351
2352
|
workspaceAppRouteActive && workspaceAppId ? (
|
|
2352
|
-
<
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
openStorageKey="dispatch-app-chat"
|
|
2356
|
-
storageKey={`dispatch-app-chat:${workspaceAppId}`}
|
|
2357
|
-
scope={{
|
|
2358
|
-
type: "workspace-app",
|
|
2359
|
-
id: workspaceAppId,
|
|
2360
|
-
label: workspaceAppChatName,
|
|
2361
|
-
contextKey: `workspace-app:${workspaceAppId}`,
|
|
2362
|
-
}}
|
|
2363
|
-
agentChatSurface="app"
|
|
2364
|
-
showTabBar
|
|
2365
|
-
suppressInlineOpenApp
|
|
2366
|
-
dynamicSuggestions={false}
|
|
2367
|
-
suggestions={[]}
|
|
2368
|
-
emptyStateText={`Ask about ${workspaceAppChatName}`}
|
|
2353
|
+
<WorkspaceAppChatRail
|
|
2354
|
+
appId={workspaceAppId}
|
|
2355
|
+
appName={workspaceAppChatName}
|
|
2369
2356
|
agentPageHref={agentPageHref}
|
|
2370
2357
|
onFullscreenRequest={openAskAgentFullscreen}
|
|
2371
2358
|
>
|
|
2372
2359
|
<WorkspaceAppKeepAlive activeAppId={workspaceAppId} />
|
|
2373
|
-
</
|
|
2360
|
+
</WorkspaceAppChatRail>
|
|
2374
2361
|
) : (
|
|
2375
2362
|
<WorkspaceAppKeepAlive
|
|
2376
2363
|
activeAppId={workspaceAppRouteActive ? workspaceAppId : null}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
const railState = vi.hoisted(() => ({
|
|
8
|
+
sidebarProps: [] as Array<Record<string, unknown>>,
|
|
9
|
+
fetchImpl: vi.fn(),
|
|
10
|
+
// Stable identity: the embed effect depends on it, so a fresh mock per
|
|
11
|
+
// render would re-run the effect forever.
|
|
12
|
+
mutateAsync: vi.fn().mockResolvedValue({ startUrl: "about:blank" }),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@agent-native/core/client/agent-chat", () => ({
|
|
16
|
+
AgentSidebar: (props: Record<string, unknown>) => {
|
|
17
|
+
railState.sidebarProps.push(props);
|
|
18
|
+
return (
|
|
19
|
+
<div data-agent-sidebar data-api-url={String(props.apiUrl ?? "")}>
|
|
20
|
+
{props.children as React.ReactNode}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("@agent-native/core/client/api-path", () => ({
|
|
27
|
+
agentNativePath: (path: string) => path,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("@agent-native/core/client/chat-first", () => ({
|
|
31
|
+
ChatFirstAppPane: ({ app }: { app: { name: string } | null }) => (
|
|
32
|
+
<div data-app-pane>{app?.name}</div>
|
|
33
|
+
),
|
|
34
|
+
defaultChatFirstCopy: (key: string) => key,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock("@agent-native/core/client/feature-flags", () => ({
|
|
38
|
+
useFeatureFlag: () => false,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
42
|
+
useActionMutation: () => ({ mutateAsync: railState.mutateAsync }),
|
|
43
|
+
useActionQuery: () => ({
|
|
44
|
+
data: [],
|
|
45
|
+
isError: false,
|
|
46
|
+
isLoading: false,
|
|
47
|
+
refetch: vi.fn(),
|
|
48
|
+
}),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
52
|
+
useT: () => (key: string, options?: { defaultValue?: string }) =>
|
|
53
|
+
options?.defaultValue ?? key,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
vi.mock("next-themes", () => ({
|
|
57
|
+
useTheme: () => ({ resolvedTheme: "light" }),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
import { WorkspaceAppChatRail, WorkspaceAppFrame } from "./workspace-app-host";
|
|
61
|
+
|
|
62
|
+
function jsonResponse(status: number, body: unknown): Response {
|
|
63
|
+
return new Response(JSON.stringify(body), {
|
|
64
|
+
status,
|
|
65
|
+
headers: { "content-type": "application/json" },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("WorkspaceAppChatRail", () => {
|
|
70
|
+
let container: HTMLDivElement;
|
|
71
|
+
let root: Root;
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
75
|
+
railState.sidebarProps.length = 0;
|
|
76
|
+
railState.fetchImpl.mockReset();
|
|
77
|
+
vi.stubGlobal("fetch", railState.fetchImpl);
|
|
78
|
+
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
79
|
+
container = document.createElement("div");
|
|
80
|
+
document.body.appendChild(container);
|
|
81
|
+
root = createRoot(container);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
act(() => root.unmount());
|
|
86
|
+
container.remove();
|
|
87
|
+
vi.unstubAllGlobals();
|
|
88
|
+
vi.restoreAllMocks();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
async function render(node: React.ReactNode) {
|
|
92
|
+
await act(async () => {
|
|
93
|
+
root.render(node);
|
|
94
|
+
await Promise.resolve();
|
|
95
|
+
await Promise.resolve();
|
|
96
|
+
await Promise.resolve();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
it("points the rail at the app's own agent through the Dispatch proxy", async () => {
|
|
101
|
+
railState.fetchImpl.mockResolvedValue(
|
|
102
|
+
jsonResponse(200, { devMode: false, canToggle: false }),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await render(
|
|
106
|
+
<WorkspaceAppChatRail appId="mail" appName="Mail">
|
|
107
|
+
<div data-app-surface />
|
|
108
|
+
</WorkspaceAppChatRail>,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(railState.fetchImpl).toHaveBeenCalledWith(
|
|
112
|
+
"/_agent-native/workspace-app-chat/mail/mode",
|
|
113
|
+
{ credentials: "include" },
|
|
114
|
+
);
|
|
115
|
+
const sidebar = container.querySelector("[data-agent-sidebar]");
|
|
116
|
+
expect(sidebar?.getAttribute("data-api-url")).toBe(
|
|
117
|
+
"/_agent-native/workspace-app-chat/mail",
|
|
118
|
+
);
|
|
119
|
+
expect(container.querySelector("[data-app-surface]")).not.toBeNull();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("shows an error instead of quietly answering from Dispatch's own agent", async () => {
|
|
123
|
+
railState.fetchImpl.mockResolvedValue(
|
|
124
|
+
jsonResponse(502, { error: "mail chat is unavailable: no app session" }),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
await render(
|
|
128
|
+
<WorkspaceAppChatRail appId="mail" appName="Mail">
|
|
129
|
+
<div data-app-surface />
|
|
130
|
+
</WorkspaceAppChatRail>,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(
|
|
134
|
+
container.querySelector("[data-dispatch-app-chat-unavailable]"),
|
|
135
|
+
).not.toBeNull();
|
|
136
|
+
// The rail is absent rather than falling back to a Dispatch-scoped chat,
|
|
137
|
+
// which would run the wrong tools and instructions while looking healthy.
|
|
138
|
+
expect(container.querySelector("[data-agent-sidebar]")).toBeNull();
|
|
139
|
+
expect(container.querySelector("[data-app-surface]")).not.toBeNull();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("re-probes the proxy when the error state is retried", async () => {
|
|
143
|
+
railState.fetchImpl.mockResolvedValueOnce(
|
|
144
|
+
jsonResponse(502, { error: "temporarily unavailable" }),
|
|
145
|
+
);
|
|
146
|
+
railState.fetchImpl.mockResolvedValue(
|
|
147
|
+
jsonResponse(200, { devMode: false }),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
await render(
|
|
151
|
+
<WorkspaceAppChatRail appId="mail" appName="Mail">
|
|
152
|
+
<div data-app-surface />
|
|
153
|
+
</WorkspaceAppChatRail>,
|
|
154
|
+
);
|
|
155
|
+
const retry = container.querySelector<HTMLButtonElement>(
|
|
156
|
+
"[data-dispatch-app-chat-unavailable] button",
|
|
157
|
+
);
|
|
158
|
+
expect(retry).not.toBeNull();
|
|
159
|
+
|
|
160
|
+
await act(async () => {
|
|
161
|
+
retry?.click();
|
|
162
|
+
await Promise.resolve();
|
|
163
|
+
await Promise.resolve();
|
|
164
|
+
await Promise.resolve();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(railState.fetchImpl).toHaveBeenCalledTimes(2);
|
|
168
|
+
expect(container.querySelector("[data-agent-sidebar]")).not.toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("leaves surfaces with no open app alone — no app rail and no proxy probe", async () => {
|
|
172
|
+
await render(
|
|
173
|
+
<WorkspaceAppFrame app={{ id: "mail", name: "Mail", path: "/mail" }} />,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(container.querySelector("[data-agent-sidebar]")).toBeNull();
|
|
177
|
+
expect(railState.fetchImpl).not.toHaveBeenCalledWith(
|
|
178
|
+
expect.stringContaining("workspace-app-chat"),
|
|
179
|
+
expect.anything(),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -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
|
|
|
@@ -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
|
|