@agent-native/dispatch 0.41.1-nightly-20261104110058 → 0.42.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/CHANGELOG.md +26 -55
- package/dist/actions/get-hosted-apps-settings.d.ts +5 -0
- package/dist/actions/get-hosted-apps-settings.d.ts.map +1 -0
- package/dist/actions/get-hosted-apps-settings.js +25 -0
- package/dist/actions/get-hosted-apps-settings.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +4 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-connected-agents.d.ts +1 -0
- package/dist/actions/list-connected-agents.js +11 -10
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list_apps.js +3 -2
- package/dist/actions/list_apps.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +10 -10
- package/dist/actions/set-hosted-apps-settings.d.ts +7 -0
- package/dist/actions/set-hosted-apps-settings.d.ts.map +1 -0
- package/dist/actions/set-hosted-apps-settings.js +35 -0
- package/dist/actions/set-hosted-apps-settings.js.map +1 -0
- package/dist/components/app-card-tiers.d.ts +13 -0
- package/dist/components/app-card-tiers.d.ts.map +1 -0
- package/dist/components/app-card-tiers.js +22 -0
- package/dist/components/app-card-tiers.js.map +1 -0
- package/dist/components/app-list-row.d.ts +12 -4
- package/dist/components/app-list-row.d.ts.map +1 -1
- package/dist/components/app-list-row.js +19 -4
- package/dist/components/app-list-row.js.map +1 -1
- package/dist/components/app-open-actions.d.ts +4 -0
- package/dist/components/app-open-actions.d.ts.map +1 -1
- package/dist/components/app-open-actions.js +9 -0
- package/dist/components/app-open-actions.js.map +1 -1
- package/dist/components/app-pin-button.d.ts +9 -0
- package/dist/components/app-pin-button.d.ts.map +1 -0
- package/dist/components/app-pin-button.js +29 -0
- package/dist/components/app-pin-button.js.map +1 -0
- package/dist/components/app-tier-sections.d.ts +27 -0
- package/dist/components/app-tier-sections.d.ts.map +1 -0
- package/dist/components/app-tier-sections.js +44 -0
- package/dist/components/app-tier-sections.js.map +1 -0
- package/dist/components/chat-home-apps.d.ts.map +1 -1
- package/dist/components/chat-home-apps.js +19 -26
- package/dist/components/chat-home-apps.js.map +1 -1
- package/dist/components/connected-app-card.d.ts +3 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +6 -3
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/create-app-popover.d.ts +4 -0
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +6 -0
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +25 -35
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/hosted-apps-settings-group.d.ts +6 -0
- package/dist/components/hosted-apps-settings-group.d.ts.map +1 -0
- package/dist/components/hosted-apps-settings-group.js +76 -0
- package/dist/components/hosted-apps-settings-group.js.map +1 -0
- package/dist/components/layout/Layout.d.ts +6 -4
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +97 -80
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +9 -20
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-app-layout-notice.d.ts +9 -0
- package/dist/components/workspace-app-layout-notice.d.ts.map +1 -0
- package/dist/components/workspace-app-layout-notice.js +27 -0
- package/dist/components/workspace-app-layout-notice.js.map +1 -0
- package/dist/components/workspace-app-search.d.ts.map +1 -1
- package/dist/components/workspace-app-search.js +11 -5
- package/dist/components/workspace-app-search.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +7 -2
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/lib/other-apps.d.ts +10 -1
- package/dist/lib/other-apps.d.ts.map +1 -1
- package/dist/lib/other-apps.js +7 -1
- package/dist/lib/other-apps.js.map +1 -1
- package/dist/lib/workspace-app-layout.d.ts +62 -6
- package/dist/lib/workspace-app-layout.d.ts.map +1 -1
- package/dist/lib/workspace-app-layout.js +196 -41
- package/dist/lib/workspace-app-layout.js.map +1 -1
- package/dist/lib/workspace-app-lists.d.ts +17 -0
- package/dist/lib/workspace-app-lists.d.ts.map +1 -0
- package/dist/lib/workspace-app-lists.js +21 -0
- package/dist/lib/workspace-app-lists.js.map +1 -0
- package/dist/lib/workspace-apps.d.ts +3 -1
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +5 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +23 -35
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/settings-page.d.ts.map +1 -1
- package/dist/routes/pages/settings-page.js +24 -3
- package/dist/routes/pages/settings-page.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +13 -18
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/app-roles.d.ts +2 -0
- package/dist/server/lib/app-roles.d.ts.map +1 -1
- package/dist/server/lib/app-roles.js +26 -7
- package/dist/server/lib/app-roles.js.map +1 -1
- package/dist/server/lib/hosted-apps-settings-schema.d.ts +6 -0
- package/dist/server/lib/hosted-apps-settings-schema.d.ts.map +1 -0
- package/dist/server/lib/hosted-apps-settings-schema.js +6 -0
- package/dist/server/lib/hosted-apps-settings-schema.js.map +1 -0
- package/dist/server/lib/hosted-apps-settings.d.ts +27 -0
- package/dist/server/lib/hosted-apps-settings.d.ts.map +1 -0
- package/dist/server/lib/hosted-apps-settings.js +72 -0
- package/dist/server/lib/hosted-apps-settings.js.map +1 -0
- package/dist/server/plugins/agent-chat.js +6 -3
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/integrations.js +3 -3
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/hosted-apps-settings.d.ts +3 -0
- package/dist/shared/hosted-apps-settings.d.ts.map +1 -0
- package/dist/shared/hosted-apps-settings.js +3 -0
- package/dist/shared/hosted-apps-settings.js.map +1 -0
- package/package.json +4 -4
- package/src/actions/get-hosted-apps-settings.ts +29 -0
- package/src/actions/hosted-apps-settings.spec.ts +100 -0
- package/src/actions/index.ts +4 -0
- package/src/actions/list-connected-agents.spec.ts +67 -0
- package/src/actions/list-connected-agents.ts +14 -15
- package/src/actions/list_apps.spec.ts +16 -0
- package/src/actions/list_apps.ts +5 -2
- package/src/actions/set-hosted-apps-settings.ts +42 -0
- package/src/components/app-card-tiers.tsx +85 -0
- package/src/components/app-list-row.spec.tsx +54 -0
- package/src/components/app-list-row.tsx +50 -8
- package/src/components/app-open-actions.tsx +16 -0
- package/src/components/app-pin-button.spec.tsx +68 -0
- package/src/components/app-pin-button.tsx +66 -0
- package/src/components/app-tier-sections.spec.tsx +155 -0
- package/src/components/app-tier-sections.tsx +158 -0
- package/src/components/chat-home-apps.spec.tsx +135 -29
- package/src/components/chat-home-apps.tsx +115 -111
- package/src/components/connected-app-card.spec.tsx +18 -2
- package/src/components/connected-app-card.tsx +18 -1
- package/src/components/create-app-popover.tsx +22 -0
- package/src/components/dispatch-control-plane.spec.tsx +89 -20
- package/src/components/dispatch-control-plane.tsx +91 -125
- package/src/components/hosted-apps-settings-group.spec.tsx +218 -0
- package/src/components/hosted-apps-settings-group.tsx +189 -0
- package/src/components/layout/Layout.spec.tsx +356 -49
- package/src/components/layout/Layout.tsx +189 -142
- package/src/components/workspace-app-card.spec.tsx +33 -29
- package/src/components/workspace-app-card.tsx +13 -31
- package/src/components/workspace-app-layout-notice.spec.tsx +106 -0
- package/src/components/workspace-app-layout-notice.tsx +65 -0
- package/src/components/workspace-app-search.spec.tsx +53 -0
- package/src/components/workspace-app-search.tsx +39 -32
- package/src/components/workspace-template-card.spec.tsx +10 -0
- package/src/components/workspace-template-card.tsx +7 -2
- package/src/db/index.spec.ts +9 -0
- package/src/db/index.ts +1 -0
- package/src/lib/other-apps.spec.ts +25 -0
- package/src/lib/other-apps.ts +9 -0
- package/src/lib/workspace-app-layout.spec.ts +61 -3
- package/src/lib/workspace-app-layout.store.spec.tsx +261 -0
- package/src/lib/workspace-app-layout.ts +265 -48
- package/src/lib/workspace-app-lists.ts +32 -0
- package/src/lib/workspace-apps.ts +10 -2
- package/src/routes/pages/apps.spec.tsx +133 -5
- package/src/routes/pages/apps.tsx +79 -140
- package/src/routes/pages/chat.spec.tsx +66 -49
- package/src/routes/pages/settings-page.tsx +31 -3
- package/src/routes/pages/settings.spec.tsx +37 -0
- package/src/server/lib/app-creation-store.spec.ts +14 -0
- package/src/server/lib/app-creation-store.ts +16 -29
- package/src/server/lib/app-creation-store.write-observation.spec.ts +216 -0
- package/src/server/lib/app-roles.spec.ts +48 -1
- package/src/server/lib/app-roles.ts +44 -12
- package/src/server/lib/hosted-apps-settings-schema.ts +6 -0
- package/src/server/lib/hosted-apps-settings.spec.ts +154 -0
- package/src/server/lib/hosted-apps-settings.ts +108 -0
- package/src/server/plugins/agent-chat.spec.ts +16 -0
- package/src/server/plugins/agent-chat.ts +7 -2
- package/src/server/plugins/integrations.ts +3 -3
- package/src/shared/hosted-apps-settings.ts +3 -0
- package/src/styles/dispatch.css +40 -11
- package/dist/components/available-apps-section.d.ts +0 -9
- package/dist/components/available-apps-section.d.ts.map +0 -1
- package/dist/components/available-apps-section.js +0 -97
- package/dist/components/available-apps-section.js.map +0 -1
- package/dist/components/other-apps-section.d.ts +0 -35
- package/dist/components/other-apps-section.d.ts.map +0 -1
- package/dist/components/other-apps-section.js +0 -79
- package/dist/components/other-apps-section.js.map +0 -1
- package/src/components/available-apps-section.tsx +0 -239
- package/src/components/other-apps-section.spec.ts +0 -50
- package/src/components/other-apps-section.tsx +0 -254
|
@@ -4,6 +4,7 @@ import { createRoot, type Root } from "react-dom/client";
|
|
|
4
4
|
import { MemoryRouter } from "react-router";
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
+
import { TooltipProvider } from "../../components/ui/tooltip";
|
|
7
8
|
import ChatRoute from "./chat";
|
|
8
9
|
|
|
9
10
|
const clientState = vi.hoisted(() => ({
|
|
@@ -57,6 +58,7 @@ vi.mock("@agent-native/toolkit/app/chat/AgentChatHome", () => ({
|
|
|
57
58
|
|
|
58
59
|
vi.mock("../../components/create-app-popover", () => ({
|
|
59
60
|
CreateAppPopover: () => null,
|
|
61
|
+
NewAppButton: () => null,
|
|
60
62
|
}));
|
|
61
63
|
|
|
62
64
|
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
@@ -75,14 +77,15 @@ vi.mock("../../components/layout/Layout", () => ({
|
|
|
75
77
|
useDispatchWorkspaceAppLauncher: () => ({
|
|
76
78
|
apps: clientState.workspaceApps,
|
|
77
79
|
workspaceApps: clientState.workspaceApps,
|
|
78
|
-
|
|
80
|
+
workspaceAppsStatus: "ready",
|
|
79
81
|
error: clientState.workspaceAppsError,
|
|
80
82
|
openApp: clientState.openWorkspaceApp,
|
|
81
83
|
retry: clientState.retryWorkspaceApps,
|
|
82
84
|
}),
|
|
83
85
|
}));
|
|
84
86
|
|
|
85
|
-
vi.mock("../../lib/workspace-app-layout", () => ({
|
|
87
|
+
vi.mock("../../lib/workspace-app-layout", async (importOriginal) => ({
|
|
88
|
+
...(await importOriginal<typeof import("../../lib/workspace-app-layout")>()),
|
|
86
89
|
workspaceAppMatchesQuery: (
|
|
87
90
|
app: { name: string; description?: string },
|
|
88
91
|
query: string,
|
|
@@ -150,9 +153,11 @@ describe("Dispatch ChatRoute", () => {
|
|
|
150
153
|
it("keeps the centered hero layout for a direct new Chat", async () => {
|
|
151
154
|
await act(async () => {
|
|
152
155
|
root.render(
|
|
153
|
-
<
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
+
<TooltipProvider>
|
|
157
|
+
<MemoryRouter initialEntries={["/chat"]}>
|
|
158
|
+
<ChatRoute />
|
|
159
|
+
</MemoryRouter>
|
|
160
|
+
</TooltipProvider>,
|
|
156
161
|
);
|
|
157
162
|
});
|
|
158
163
|
|
|
@@ -176,24 +181,26 @@ describe("Dispatch ChatRoute", () => {
|
|
|
176
181
|
it("opens a Dispatch run with its Agent Teams parent context", async () => {
|
|
177
182
|
await act(async () => {
|
|
178
183
|
root.render(
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
{
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
<TooltipProvider>
|
|
185
|
+
<MemoryRouter
|
|
186
|
+
initialEntries={[
|
|
187
|
+
{
|
|
188
|
+
pathname: "/chat",
|
|
189
|
+
state: {
|
|
190
|
+
dispatchThread: {
|
|
191
|
+
id: "run-open-1",
|
|
192
|
+
threadId: "child-thread",
|
|
193
|
+
parentThreadId: "parent-thread",
|
|
194
|
+
description: "Review the Slides task",
|
|
195
|
+
name: "Slides review",
|
|
196
|
+
},
|
|
190
197
|
},
|
|
191
198
|
},
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
</
|
|
199
|
+
]}
|
|
200
|
+
>
|
|
201
|
+
<ChatRoute />
|
|
202
|
+
</MemoryRouter>
|
|
203
|
+
</TooltipProvider>,
|
|
197
204
|
);
|
|
198
205
|
});
|
|
199
206
|
|
|
@@ -213,9 +220,11 @@ describe("Dispatch ChatRoute", () => {
|
|
|
213
220
|
it("renders the colored app directory below the empty chat composer", async () => {
|
|
214
221
|
await act(async () => {
|
|
215
222
|
root.render(
|
|
216
|
-
<
|
|
217
|
-
<
|
|
218
|
-
|
|
223
|
+
<TooltipProvider>
|
|
224
|
+
<MemoryRouter initialEntries={["/chat"]}>
|
|
225
|
+
<ChatRoute />
|
|
226
|
+
</MemoryRouter>
|
|
227
|
+
</TooltipProvider>,
|
|
219
228
|
);
|
|
220
229
|
});
|
|
221
230
|
|
|
@@ -240,9 +249,11 @@ describe("Dispatch ChatRoute", () => {
|
|
|
240
249
|
|
|
241
250
|
await act(async () => {
|
|
242
251
|
root.render(
|
|
243
|
-
<
|
|
244
|
-
<
|
|
245
|
-
|
|
252
|
+
<TooltipProvider>
|
|
253
|
+
<MemoryRouter initialEntries={["/chat"]}>
|
|
254
|
+
<ChatRoute />
|
|
255
|
+
</MemoryRouter>
|
|
256
|
+
</TooltipProvider>,
|
|
246
257
|
);
|
|
247
258
|
});
|
|
248
259
|
|
|
@@ -263,22 +274,24 @@ describe("Dispatch ChatRoute", () => {
|
|
|
263
274
|
it("starts bottom-pinned when an Overview prompt is transitioning in", async () => {
|
|
264
275
|
await act(async () => {
|
|
265
276
|
root.render(
|
|
266
|
-
<
|
|
267
|
-
|
|
268
|
-
{
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
277
|
+
<TooltipProvider>
|
|
278
|
+
<MemoryRouter
|
|
279
|
+
initialEntries={[
|
|
280
|
+
{
|
|
281
|
+
pathname: "/chat",
|
|
282
|
+
state: {
|
|
283
|
+
dispatchPrompt: {
|
|
284
|
+
id: "overview-prompt",
|
|
285
|
+
message: "Route this across my apps",
|
|
286
|
+
selectedModel: "auto",
|
|
287
|
+
},
|
|
275
288
|
},
|
|
276
289
|
},
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
</
|
|
290
|
+
]}
|
|
291
|
+
>
|
|
292
|
+
<ChatRoute />
|
|
293
|
+
</MemoryRouter>
|
|
294
|
+
</TooltipProvider>,
|
|
282
295
|
);
|
|
283
296
|
});
|
|
284
297
|
|
|
@@ -320,11 +333,13 @@ describe("Dispatch ChatRoute", () => {
|
|
|
320
333
|
|
|
321
334
|
await act(async () => {
|
|
322
335
|
root.render(
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
336
|
+
<TooltipProvider>
|
|
337
|
+
<MemoryRouter
|
|
338
|
+
initialEntries={["/chat?agent=agents/research-partner.md"]}
|
|
339
|
+
>
|
|
340
|
+
<ChatRoute />
|
|
341
|
+
</MemoryRouter>
|
|
342
|
+
</TooltipProvider>,
|
|
328
343
|
);
|
|
329
344
|
});
|
|
330
345
|
|
|
@@ -349,9 +364,11 @@ describe("Dispatch ChatRoute", () => {
|
|
|
349
364
|
it("keeps threaded chats free of request ID chrome", async () => {
|
|
350
365
|
await act(async () => {
|
|
351
366
|
root.render(
|
|
352
|
-
<
|
|
353
|
-
<
|
|
354
|
-
|
|
367
|
+
<TooltipProvider>
|
|
368
|
+
<MemoryRouter initialEntries={["/chat/chat-123"]}>
|
|
369
|
+
<ChatRoute />
|
|
370
|
+
</MemoryRouter>
|
|
371
|
+
</TooltipProvider>,
|
|
355
372
|
);
|
|
356
373
|
});
|
|
357
374
|
|
|
@@ -10,7 +10,11 @@ import {
|
|
|
10
10
|
import { useT } from "@agent-native/core/client/i18n";
|
|
11
11
|
import { SETTINGS_REDESIGN_FLAG } from "@agent-native/core/feature-flags/registry";
|
|
12
12
|
import { ChangelogSettingsCard } from "@agent-native/toolkit/app/changelog";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
OrgAppsPage,
|
|
15
|
+
OrgMembersPage,
|
|
16
|
+
TeamPage,
|
|
17
|
+
} from "@agent-native/toolkit/app/org";
|
|
14
18
|
import {
|
|
15
19
|
AccountSettingsCard,
|
|
16
20
|
CORE_SETTINGS_PAGES,
|
|
@@ -26,6 +30,7 @@ import { IconShield } from "@tabler/icons-react";
|
|
|
26
30
|
import { useMemo, useState } from "react";
|
|
27
31
|
import { Link } from "react-router";
|
|
28
32
|
|
|
33
|
+
import { HostedAppsSettingsGroup } from "../../components/hosted-apps-settings-group";
|
|
29
34
|
import { Button } from "../../components/ui/button";
|
|
30
35
|
import { Switch } from "../../components/ui/switch";
|
|
31
36
|
import { dispatchAccessDescriptor } from "../../shared/app-roles.js";
|
|
@@ -38,14 +43,37 @@ if (!coreMembersPage) {
|
|
|
38
43
|
throw new Error("Core Settings has no members page for Dispatch app roles");
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
const coreAppsPage = CORE_SETTINGS_PAGES.find((page) => page.id === "apps");
|
|
47
|
+
if (!coreAppsPage) {
|
|
48
|
+
throw new Error("Core Settings has no apps page for Dispatch hosted apps");
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
function DispatchMembersSettingsPage() {
|
|
42
52
|
return <OrgMembersPage appRoles={dispatchAccessDescriptor} />;
|
|
43
53
|
}
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
function DispatchAppsSettingsPage() {
|
|
56
|
+
return <OrgAppsPage extraGroups={<HostedAppsSettingsGroup />} />;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The redesigned Settings shows Members and Apps from core; Dispatch's
|
|
60
|
+
// replace them so the app-role column and the hosted apps group stay.
|
|
47
61
|
registerSettingsPages([
|
|
48
62
|
{ ...coreMembersPage, component: DispatchMembersSettingsPage },
|
|
63
|
+
{
|
|
64
|
+
...coreAppsPage,
|
|
65
|
+
component: DispatchAppsSettingsPage,
|
|
66
|
+
searchEntries: [
|
|
67
|
+
...(coreAppsPage.searchEntries ?? []),
|
|
68
|
+
{
|
|
69
|
+
id: "hosted-apps",
|
|
70
|
+
labelKey: "dispatch.pages.showHostedApps",
|
|
71
|
+
keywords:
|
|
72
|
+
"agent-native.com hosted apps built-in agent-native mail calendar hide",
|
|
73
|
+
anchor: "show-hosted-apps",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
49
77
|
]);
|
|
50
78
|
|
|
51
79
|
export function meta() {
|
|
@@ -15,6 +15,7 @@ const state = vi.hoisted(() => ({
|
|
|
15
15
|
searchEntries?: unknown;
|
|
16
16
|
}>,
|
|
17
17
|
membersAppRoles: undefined as unknown,
|
|
18
|
+
appsExtraGroups: undefined as ReactNode,
|
|
18
19
|
pageProps: null as null | {
|
|
19
20
|
general?: ReactNode;
|
|
20
21
|
generalGroups?: ReactNode;
|
|
@@ -61,6 +62,14 @@ vi.mock("@agent-native/toolkit/app/org", () => ({
|
|
|
61
62
|
state.membersAppRoles = appRoles;
|
|
62
63
|
return <div data-members-page />;
|
|
63
64
|
},
|
|
65
|
+
OrgAppsPage: ({ extraGroups }: { extraGroups?: ReactNode }) => {
|
|
66
|
+
state.appsExtraGroups = extraGroups;
|
|
67
|
+
return <div data-apps-page />;
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
vi.mock("../../components/hosted-apps-settings-group", () => ({
|
|
72
|
+
HostedAppsSettingsGroup: () => <section data-hosted-apps-group />,
|
|
64
73
|
}));
|
|
65
74
|
|
|
66
75
|
vi.mock("@agent-native/toolkit/app/settings", () => ({
|
|
@@ -72,6 +81,11 @@ vi.mock("@agent-native/toolkit/app/settings", () => ({
|
|
|
72
81
|
component: () => null,
|
|
73
82
|
searchEntries: [{ id: "invite-members" }],
|
|
74
83
|
},
|
|
84
|
+
{
|
|
85
|
+
id: "apps",
|
|
86
|
+
component: () => null,
|
|
87
|
+
searchEntries: [{ id: "new-app-privacy" }],
|
|
88
|
+
},
|
|
75
89
|
],
|
|
76
90
|
registerSettingsPages: (pages: typeof state.registered) => {
|
|
77
91
|
state.registered.push(...pages);
|
|
@@ -145,6 +159,29 @@ describe("Dispatch settings route", () => {
|
|
|
145
159
|
expect(state.membersAppRoles).toBe(dispatchAccessDescriptor);
|
|
146
160
|
});
|
|
147
161
|
|
|
162
|
+
it("replaces core Apps with one that adds the hosted apps group", () => {
|
|
163
|
+
const apps = state.registered.find((page) => page.id === "apps");
|
|
164
|
+
expect(apps?.searchEntries).toEqual([
|
|
165
|
+
{ id: "new-app-privacy" },
|
|
166
|
+
expect.objectContaining({
|
|
167
|
+
id: "hosted-apps",
|
|
168
|
+
labelKey: "dispatch.pages.showHostedApps",
|
|
169
|
+
anchor: "show-hosted-apps",
|
|
170
|
+
}),
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const Apps = apps!.component;
|
|
174
|
+
act(() => {
|
|
175
|
+
root.render(<Apps />);
|
|
176
|
+
});
|
|
177
|
+
expect(container.querySelector("[data-apps-page]")).not.toBeNull();
|
|
178
|
+
|
|
179
|
+
act(() => {
|
|
180
|
+
root.render(<>{state.appsExtraGroups}</>);
|
|
181
|
+
});
|
|
182
|
+
expect(container.querySelector("[data-hosted-apps-group]")).not.toBeNull();
|
|
183
|
+
});
|
|
184
|
+
|
|
148
185
|
it("keeps the language row and Admin link on today's General tab", () => {
|
|
149
186
|
render();
|
|
150
187
|
|
|
@@ -209,6 +209,20 @@ vi.mock("@agent-native/core/sharing", async (importOriginal) => {
|
|
|
209
209
|
return {
|
|
210
210
|
...actual,
|
|
211
211
|
resolveAccess: (...args: any[]) => mocks.resolveAccess(...args),
|
|
212
|
+
// core's own tests prove resolveAccessMany agrees with resolveAccess, so
|
|
213
|
+
// this double answers through the same per-app mock the cases configure.
|
|
214
|
+
resolveAccessMany: async (
|
|
215
|
+
resourceType: string,
|
|
216
|
+
resourceIds: string[],
|
|
217
|
+
...rest: any[]
|
|
218
|
+
) => {
|
|
219
|
+
const access = new Map<string, unknown>();
|
|
220
|
+
for (const id of resourceIds) {
|
|
221
|
+
const resolved = await mocks.resolveAccess(resourceType, id, ...rest);
|
|
222
|
+
if (resolved) access.set(id, resolved);
|
|
223
|
+
}
|
|
224
|
+
return access;
|
|
225
|
+
},
|
|
212
226
|
};
|
|
213
227
|
});
|
|
214
228
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
ActionContractError,
|
|
13
13
|
isActionContractError,
|
|
14
|
+
noteExternalWrite,
|
|
14
15
|
} from "@agent-native/core/action";
|
|
15
16
|
import { describeErrorWithCauses } from "@agent-native/core/agent/engine/error-detail";
|
|
16
17
|
import { getAppConfig } from "@agent-native/core/app-config";
|
|
@@ -55,7 +56,7 @@ import {
|
|
|
55
56
|
parseAgentProfileMetadata,
|
|
56
57
|
type ConnectRequiredCard,
|
|
57
58
|
} from "@agent-native/core/shared";
|
|
58
|
-
import {
|
|
59
|
+
import { resolveAccessMany } from "@agent-native/core/sharing";
|
|
59
60
|
|
|
60
61
|
import { getDb } from "../../db/index.js";
|
|
61
62
|
import { workspaceAppRoute } from "../../shared/workspace-app-route.js";
|
|
@@ -95,7 +96,6 @@ const WORKSPACE_APPS_ENV_KEY = "AGENT_NATIVE_WORKSPACE_APPS_JSON";
|
|
|
95
96
|
const WORKSPACE_APPS_MANIFEST_FILE = "workspace-apps.json";
|
|
96
97
|
const WORKSPACE_APPS_GATEWAY_PATH = "/_workspace/apps";
|
|
97
98
|
const WORKSPACE_APPS_GATEWAY_TIMEOUT_MS = 2_500;
|
|
98
|
-
const WORKSPACE_APP_ACCESS_CONCURRENCY = 8;
|
|
99
99
|
const MAX_PENDING_APPS = 50;
|
|
100
100
|
const PENDING_WORKSPACE_APP_TTL_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
101
101
|
const AGENT_CARD_PATH = "/.well-known/agent-card.json";
|
|
@@ -1889,33 +1889,17 @@ async function filterWorkspaceAppsByAccess(
|
|
|
1889
1889
|
candidates.push(app);
|
|
1890
1890
|
}
|
|
1891
1891
|
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
try {
|
|
1904
|
-
const access = await resolveAccess(
|
|
1905
|
-
"workspace-app",
|
|
1906
|
-
app.id,
|
|
1907
|
-
{ userEmail, orgId },
|
|
1908
|
-
{ skipResourceBody: true },
|
|
1909
|
-
);
|
|
1910
|
-
return { app, allowed: Boolean(access) };
|
|
1911
|
-
} catch (error) {
|
|
1912
|
-
console.warn("[dispatch] workspace app access lookup failed", error);
|
|
1913
|
-
return { app, allowed: false };
|
|
1914
|
-
}
|
|
1915
|
-
}),
|
|
1916
|
-
);
|
|
1917
|
-
for (const decision of decisions) {
|
|
1918
|
-
if (decision.allowed) visibleIds.add(decision.app.id);
|
|
1892
|
+
if (candidates.length > 0) {
|
|
1893
|
+
try {
|
|
1894
|
+
const access = await resolveAccessMany(
|
|
1895
|
+
"workspace-app",
|
|
1896
|
+
candidates.map((app) => app.id),
|
|
1897
|
+
{ userEmail, ...(orgId ? { orgId } : {}) },
|
|
1898
|
+
{ skipResourceBody: true },
|
|
1899
|
+
);
|
|
1900
|
+
for (const id of access.keys()) visibleIds.add(id);
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
console.warn("[dispatch] workspace app access lookup failed", error);
|
|
1919
1903
|
}
|
|
1920
1904
|
}
|
|
1921
1905
|
|
|
@@ -2829,6 +2813,7 @@ export async function scaffoldWorkspaceAppFromTemplate(input: {
|
|
|
2829
2813
|
});
|
|
2830
2814
|
} catch (error) {
|
|
2831
2815
|
if (fs.existsSync(appDir)) {
|
|
2816
|
+
noteExternalWrite("local-disk");
|
|
2832
2817
|
fs.rmSync(appDir, { recursive: true, force: true });
|
|
2833
2818
|
}
|
|
2834
2819
|
throw error;
|
|
@@ -2845,6 +2830,7 @@ export async function scaffoldWorkspaceAppFromTemplate(input: {
|
|
|
2845
2830
|
});
|
|
2846
2831
|
} catch (error) {
|
|
2847
2832
|
if (fs.existsSync(appDir)) {
|
|
2833
|
+
noteExternalWrite("local-disk");
|
|
2848
2834
|
fs.rmSync(appDir, { recursive: true, force: true });
|
|
2849
2835
|
}
|
|
2850
2836
|
throw error;
|
|
@@ -2885,6 +2871,7 @@ function runScaffoldCli(input: {
|
|
|
2885
2871
|
args: string[];
|
|
2886
2872
|
}): Promise<string> {
|
|
2887
2873
|
return new Promise((resolve, reject) => {
|
|
2874
|
+
noteExternalWrite("child-process");
|
|
2888
2875
|
// pnpm is a pnpm.cmd shim on Windows, which Node starts only through a
|
|
2889
2876
|
// shell. The app id is validated and the template comes from an allowlist,
|
|
2890
2877
|
// so one joined command line is safe there.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { runWithRequestContext } from "@agent-native/core/server";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
|
|
9
|
+
import { scaffoldWorkspaceAppFromTemplate } from "./app-creation-store.js";
|
|
10
|
+
|
|
11
|
+
// Scaffolding runs the `agent-native add-app` CLI as a child process, and when
|
|
12
|
+
// it fails the app directory it left behind is removed. Each of those writes
|
|
13
|
+
// reports itself before it runs, so the framework can tell the call wrote.
|
|
14
|
+
const mocks = vi.hoisted(() => ({
|
|
15
|
+
noteExternalWrite: vi.fn(),
|
|
16
|
+
spawn: vi.fn(),
|
|
17
|
+
getSetting: vi.fn(async (_key: string): Promise<unknown> => null),
|
|
18
|
+
putSetting: vi.fn(async () => {}),
|
|
19
|
+
mutateSetting: vi.fn(async () => null),
|
|
20
|
+
getOrgSetting: vi.fn(
|
|
21
|
+
async (_orgId: string, _key: string): Promise<unknown> => null,
|
|
22
|
+
),
|
|
23
|
+
execute: vi.fn(async () => ({ rows: [] as unknown[], rowsAffected: 0 })),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("@agent-native/core/action", async (importOriginal) => ({
|
|
27
|
+
...(await importOriginal<typeof import("@agent-native/core/action")>()),
|
|
28
|
+
noteExternalWrite: mocks.noteExternalWrite,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("node:child_process", async (importOriginal) => ({
|
|
32
|
+
...(await importOriginal<typeof import("node:child_process")>()),
|
|
33
|
+
spawn: (...args: unknown[]) => mocks.spawn(...args),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("@agent-native/core/db", async (importOriginal) => ({
|
|
37
|
+
...(await importOriginal<typeof import("@agent-native/core/db")>()),
|
|
38
|
+
getDbExec: () => ({ execute: mocks.execute }),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
vi.mock("@agent-native/core/settings", () => ({
|
|
42
|
+
getSetting: mocks.getSetting,
|
|
43
|
+
putSetting: mocks.putSetting,
|
|
44
|
+
mutateSetting: mocks.mutateSetting,
|
|
45
|
+
getOrgSetting: mocks.getOrgSetting,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
const ORG_ID = "org-123";
|
|
49
|
+
const METADATA_KEY = "workspace-app-metadata:org:org-123";
|
|
50
|
+
|
|
51
|
+
const roots: string[] = [];
|
|
52
|
+
const spies: { mockRestore: () => void }[] = [];
|
|
53
|
+
|
|
54
|
+
class FakeCli extends EventEmitter {
|
|
55
|
+
readonly stdout = new EventEmitter();
|
|
56
|
+
readonly stderr = new EventEmitter();
|
|
57
|
+
readonly kill = vi.fn();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The position, within the whole run, of a spy's index-th call. */
|
|
61
|
+
function callOrder(
|
|
62
|
+
spy: { mock: { invocationCallOrder: number[] } },
|
|
63
|
+
index = 0,
|
|
64
|
+
): number {
|
|
65
|
+
const order = spy.mock.invocationCallOrder[index];
|
|
66
|
+
if (order === undefined) {
|
|
67
|
+
throw new Error(`Expected call ${index} to be recorded.`);
|
|
68
|
+
}
|
|
69
|
+
return order;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A workspace root that `findWorkspaceRoot` finds from the process cwd. */
|
|
73
|
+
function workspace(): string {
|
|
74
|
+
const root = fs.mkdtempSync(
|
|
75
|
+
path.join(os.tmpdir(), "dispatch-scaffold-observed-"),
|
|
76
|
+
);
|
|
77
|
+
roots.push(root);
|
|
78
|
+
fs.writeFileSync(
|
|
79
|
+
path.join(root, "package.json"),
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
name: "workspace",
|
|
82
|
+
"agent-native": { workspaceCore: "@agent-native/core" },
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
spies.push(vi.spyOn(process, "cwd").mockReturnValue(root));
|
|
86
|
+
return root;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function observeRemovals() {
|
|
90
|
+
const spy = vi.spyOn(fs, "rmSync");
|
|
91
|
+
spies.push(spy);
|
|
92
|
+
return spy;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Stands in for `pnpm exec agent-native`: runs `beforeExit`, then exits. */
|
|
96
|
+
function fakeCli(input: {
|
|
97
|
+
code: number;
|
|
98
|
+
stderr?: string;
|
|
99
|
+
beforeExit?: () => void;
|
|
100
|
+
}): void {
|
|
101
|
+
mocks.spawn.mockImplementation(() => {
|
|
102
|
+
const child = new FakeCli();
|
|
103
|
+
setImmediate(() => {
|
|
104
|
+
input.beforeExit?.();
|
|
105
|
+
if (input.stderr) child.stderr.emit("data", input.stderr);
|
|
106
|
+
child.emit("exit", input.code);
|
|
107
|
+
});
|
|
108
|
+
return child;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function scaffold() {
|
|
113
|
+
return runWithRequestContext(
|
|
114
|
+
{ userEmail: "dev@example.test", orgId: ORG_ID },
|
|
115
|
+
() => scaffoldWorkspaceAppFromTemplate({ template: "mail" }),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
mocks.spawn.mockReset();
|
|
121
|
+
mocks.spawn.mockImplementation(() => {
|
|
122
|
+
throw new Error("Unexpected CLI spawn in the scaffold write spec.");
|
|
123
|
+
});
|
|
124
|
+
mocks.getSetting.mockReset();
|
|
125
|
+
mocks.getSetting.mockResolvedValue(null);
|
|
126
|
+
mocks.getOrgSetting.mockReset();
|
|
127
|
+
mocks.getOrgSetting.mockResolvedValue(null);
|
|
128
|
+
mocks.execute.mockReset();
|
|
129
|
+
mocks.execute.mockResolvedValue({ rows: [], rowsAffected: 0 });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterEach(() => {
|
|
133
|
+
for (const spy of spies.splice(0)) spy.mockRestore();
|
|
134
|
+
for (const root of roots.splice(0)) {
|
|
135
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
vi.clearAllMocks();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("scaffoldWorkspaceAppFromTemplate write observation", () => {
|
|
141
|
+
it("notes the child-process write before the scaffold CLI spawns", async () => {
|
|
142
|
+
const root = workspace();
|
|
143
|
+
fakeCli({ code: 1, stderr: "boom" });
|
|
144
|
+
const rmSync = observeRemovals();
|
|
145
|
+
|
|
146
|
+
await expect(scaffold()).rejects.toThrow("Scaffold failed: boom");
|
|
147
|
+
|
|
148
|
+
expect(mocks.spawn).toHaveBeenCalledWith(
|
|
149
|
+
"pnpm",
|
|
150
|
+
["exec", "agent-native", "add-app", "mail", "--template", "mail"],
|
|
151
|
+
expect.objectContaining({ cwd: root }),
|
|
152
|
+
);
|
|
153
|
+
expect(mocks.noteExternalWrite).toHaveBeenCalledTimes(1);
|
|
154
|
+
expect(mocks.noteExternalWrite).toHaveBeenCalledWith("child-process");
|
|
155
|
+
expect(callOrder(mocks.noteExternalWrite)).toBeLessThan(
|
|
156
|
+
callOrder(mocks.spawn),
|
|
157
|
+
);
|
|
158
|
+
expect(rmSync).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("notes the local-disk write before removing a half-made app when the CLI fails", async () => {
|
|
162
|
+
const root = workspace();
|
|
163
|
+
const appDir = path.join(root, "apps", "mail");
|
|
164
|
+
fakeCli({
|
|
165
|
+
code: 1,
|
|
166
|
+
stderr: "boom",
|
|
167
|
+
beforeExit: () => {
|
|
168
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
169
|
+
fs.writeFileSync(path.join(appDir, "package.json"), "{}");
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const rmSync = observeRemovals();
|
|
173
|
+
|
|
174
|
+
await expect(scaffold()).rejects.toThrow("Scaffold failed: boom");
|
|
175
|
+
|
|
176
|
+
expect(mocks.noteExternalWrite).toHaveBeenNthCalledWith(2, "local-disk");
|
|
177
|
+
expect(rmSync).toHaveBeenCalledWith(appDir, {
|
|
178
|
+
recursive: true,
|
|
179
|
+
force: true,
|
|
180
|
+
});
|
|
181
|
+
expect(callOrder(mocks.noteExternalWrite, 1)).toBeLessThan(
|
|
182
|
+
callOrder(rmSync),
|
|
183
|
+
);
|
|
184
|
+
expect(fs.existsSync(appDir)).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("notes the local-disk write before removing the app when its metadata write fails", async () => {
|
|
188
|
+
const root = workspace();
|
|
189
|
+
const appDir = path.join(root, "apps", "mail");
|
|
190
|
+
fakeCli({
|
|
191
|
+
code: 0,
|
|
192
|
+
beforeExit: () => {
|
|
193
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
mocks.getSetting.mockImplementation(async (key: string) => {
|
|
197
|
+
if (key === METADATA_KEY) {
|
|
198
|
+
throw new Error("metadata store unavailable");
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
});
|
|
202
|
+
const rmSync = observeRemovals();
|
|
203
|
+
|
|
204
|
+
await expect(scaffold()).rejects.toThrow("metadata store unavailable");
|
|
205
|
+
|
|
206
|
+
expect(mocks.noteExternalWrite).toHaveBeenNthCalledWith(2, "local-disk");
|
|
207
|
+
expect(rmSync).toHaveBeenCalledWith(appDir, {
|
|
208
|
+
recursive: true,
|
|
209
|
+
force: true,
|
|
210
|
+
});
|
|
211
|
+
expect(callOrder(mocks.noteExternalWrite, 1)).toBeLessThan(
|
|
212
|
+
callOrder(rmSync),
|
|
213
|
+
);
|
|
214
|
+
expect(fs.existsSync(appDir)).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
});
|