@agent-native/dispatch 0.14.3 → 0.14.5
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/README.md +114 -128
- package/dist/actions/list_apps.d.ts +1 -0
- package/dist/actions/list_apps.js +16 -8
- package/dist/actions/list_apps.js.map +1 -1
- package/dist/actions/set-mcp-app-access.js +1 -4
- package/dist/actions/set-mcp-app-access.js.map +1 -1
- package/dist/components/action-query-error.d.ts +8 -0
- package/dist/components/action-query-error.d.ts.map +1 -0
- package/dist/components/action-query-error.js +10 -0
- package/dist/components/action-query-error.js.map +1 -0
- package/dist/components/app-keys-popover.d.ts +4 -0
- package/dist/components/app-keys-popover.d.ts.map +1 -1
- package/dist/components/app-keys-popover.js +26 -14
- package/dist/components/app-keys-popover.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +31 -27
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +1 -0
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +19 -21
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/task-queue-health.d.ts.map +1 -1
- package/dist/components/task-queue-health.js +9 -1
- package/dist/components/task-queue-health.js.map +1 -1
- package/dist/components/ui/sheet.js +1 -1
- package/dist/components/ui/sheet.js.map +1 -1
- package/dist/components/ui/toast.js +1 -1
- package/dist/components/ui/toast.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +8 -5
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
- package/dist/components/workspace-resource-effective-stack.js +6 -1
- package/dist/components/workspace-resource-effective-stack.js.map +1 -1
- package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -1
- package/dist/components/workspace-resource-impact-preview.js +6 -1
- package/dist/components/workspace-resource-impact-preview.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +6 -1
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/agents.d.ts.map +1 -1
- package/dist/routes/pages/agents.js +5 -8
- package/dist/routes/pages/agents.js.map +1 -1
- package/dist/routes/pages/approval.d.ts.map +1 -1
- package/dist/routes/pages/approval.js +6 -1
- package/dist/routes/pages/approval.js.map +1 -1
- package/dist/routes/pages/approvals.d.ts.map +1 -1
- package/dist/routes/pages/approvals.js +8 -5
- package/dist/routes/pages/approvals.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +4 -2
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +10 -42
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/audit.d.ts.map +1 -1
- package/dist/routes/pages/audit.js +4 -2
- package/dist/routes/pages/audit.js.map +1 -1
- package/dist/routes/pages/automations.d.ts.map +1 -1
- package/dist/routes/pages/automations.js +0 -1
- package/dist/routes/pages/automations.js.map +1 -1
- package/dist/routes/pages/chat.d.ts.map +1 -1
- package/dist/routes/pages/chat.js +16 -2
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/routes/pages/destinations.d.ts.map +1 -1
- package/dist/routes/pages/destinations.js +5 -3
- package/dist/routes/pages/destinations.js.map +1 -1
- package/dist/routes/pages/identities.d.ts.map +1 -1
- package/dist/routes/pages/identities.js +5 -3
- package/dist/routes/pages/identities.js.map +1 -1
- package/dist/routes/pages/integrations.d.ts.map +1 -1
- package/dist/routes/pages/integrations.js +9 -3
- package/dist/routes/pages/integrations.js.map +1 -1
- package/dist/routes/pages/metrics.d.ts.map +1 -1
- package/dist/routes/pages/metrics.js +1 -1
- package/dist/routes/pages/metrics.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts.map +1 -1
- package/dist/routes/pages/thread-debug.js +7 -6
- package/dist/routes/pages/thread-debug.js.map +1 -1
- package/dist/routes/pages/vault.d.ts.map +1 -1
- package/dist/routes/pages/vault.js +23 -7
- package/dist/routes/pages/vault.js.map +1 -1
- package/dist/routes/pages/workspace.d.ts.map +1 -1
- package/dist/routes/pages/workspace.js +9 -3
- package/dist/routes/pages/workspace.js.map +1 -1
- package/dist/server/lib/dispatch-store.d.ts.map +1 -1
- package/dist/server/lib/dispatch-store.js +2 -2
- package/dist/server/lib/dispatch-store.js.map +1 -1
- package/dist/server/lib/mcp-access-store.d.ts.map +1 -1
- package/dist/server/lib/mcp-access-store.js +30 -2
- package/dist/server/lib/mcp-access-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.d.ts +1 -1
- package/package.json +2 -2
- package/src/actions/list_apps.spec.ts +52 -0
- package/src/actions/list_apps.ts +16 -8
- package/src/actions/set-mcp-app-access.spec.ts +73 -0
- package/src/actions/set-mcp-app-access.ts +1 -5
- package/src/components/action-query-error.spec.tsx +56 -0
- package/src/components/action-query-error.tsx +32 -0
- package/src/components/app-keys-popover.spec.tsx +85 -0
- package/src/components/app-keys-popover.tsx +76 -50
- package/src/components/dispatch-control-plane.spec.tsx +106 -0
- package/src/components/dispatch-control-plane.tsx +71 -115
- package/src/components/index.ts +1 -0
- package/src/components/layout/Layout.spec.tsx +42 -2
- package/src/components/layout/Layout.tsx +22 -27
- package/src/components/task-queue-health.tsx +9 -1
- package/src/components/ui/sheet.tsx +1 -1
- package/src/components/ui/toast.tsx +1 -1
- package/src/components/workspace-app-card.spec.tsx +80 -0
- package/src/components/workspace-app-card.tsx +36 -25
- package/src/components/workspace-resource-effective-stack.tsx +13 -1
- package/src/components/workspace-resource-impact-preview.tsx +12 -1
- package/src/routes/pages/$appId.tsx +17 -1
- package/src/routes/pages/agents.tsx +24 -11
- package/src/routes/pages/approval.tsx +16 -4
- package/src/routes/pages/approvals.tsx +108 -89
- package/src/routes/pages/apps.$appId.tsx +9 -2
- package/src/routes/pages/apps.tsx +13 -177
- package/src/routes/pages/audit.tsx +29 -20
- package/src/routes/pages/automations.tsx +0 -1
- package/src/routes/pages/chat.spec.tsx +94 -0
- package/src/routes/pages/chat.tsx +30 -10
- package/src/routes/pages/destinations.tsx +75 -62
- package/src/routes/pages/identities.tsx +74 -63
- package/src/routes/pages/integrations.tsx +17 -10
- package/src/routes/pages/metrics.tsx +0 -1
- package/src/routes/pages/thread-debug.tsx +18 -12
- package/src/routes/pages/vault.tsx +59 -26
- package/src/routes/pages/workspace.tsx +69 -55
- package/src/server/lib/approval-fencing.spec.ts +113 -2
- package/src/server/lib/dispatch-store.ts +2 -0
- package/src/server/lib/mcp-access-store.spec.ts +116 -4
- package/src/server/lib/mcp-access-store.ts +37 -2
- package/src/server/lib/mcp-gateway.spec.ts +79 -3
- package/src/styles/dispatch.css +5 -5
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { ActionQueryError } from "./action-query-error";
|
|
7
|
+
|
|
8
|
+
vi.mock("@agent-native/core/client", () => ({
|
|
9
|
+
useT: () => (key: string) =>
|
|
10
|
+
({
|
|
11
|
+
"dispatch.pages.dataLoadFailed": "Couldn't load data",
|
|
12
|
+
"dispatch.pages.dataLoadFailedDescription":
|
|
13
|
+
"Dispatch couldn't load this data.",
|
|
14
|
+
"dispatch.pages.tryAgain": "Try again",
|
|
15
|
+
})[key] ?? key,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe("ActionQueryError", () => {
|
|
19
|
+
let container: HTMLDivElement;
|
|
20
|
+
let root: Root;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
24
|
+
container = document.createElement("div");
|
|
25
|
+
document.body.appendChild(container);
|
|
26
|
+
root = createRoot(container);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
act(() => root.unmount());
|
|
31
|
+
container.remove();
|
|
32
|
+
vi.unstubAllGlobals();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("shows a safe query error and retries on request", () => {
|
|
36
|
+
const onRetry = vi.fn();
|
|
37
|
+
act(() => {
|
|
38
|
+
root.render(
|
|
39
|
+
<ActionQueryError
|
|
40
|
+
error={new Error("Database unavailable")}
|
|
41
|
+
onRetry={onRetry}
|
|
42
|
+
/>,
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(container.textContent).toContain("Couldn't load data");
|
|
47
|
+
expect(container.textContent).toContain(
|
|
48
|
+
"Dispatch couldn't load this data.",
|
|
49
|
+
);
|
|
50
|
+
expect(container.textContent).not.toContain("Database unavailable");
|
|
51
|
+
act(() => {
|
|
52
|
+
container.querySelector("button")?.click();
|
|
53
|
+
});
|
|
54
|
+
expect(onRetry).toHaveBeenCalledOnce();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client";
|
|
2
|
+
import { IconAlertTriangle, IconRefresh } from "@tabler/icons-react";
|
|
3
|
+
|
|
4
|
+
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
|
5
|
+
import { Button } from "./ui/button";
|
|
6
|
+
|
|
7
|
+
interface ActionQueryErrorProps {
|
|
8
|
+
error?: unknown;
|
|
9
|
+
onRetry: () => unknown;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ActionQueryError({
|
|
14
|
+
onRetry,
|
|
15
|
+
className,
|
|
16
|
+
}: ActionQueryErrorProps) {
|
|
17
|
+
const t = useT();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Alert variant="destructive" className={className}>
|
|
21
|
+
<IconAlertTriangle className="size-4" />
|
|
22
|
+
<AlertTitle>{t("dispatch.pages.dataLoadFailed")}</AlertTitle>
|
|
23
|
+
<AlertDescription className="flex flex-wrap items-center justify-between gap-3">
|
|
24
|
+
<span>{t("dispatch.pages.dataLoadFailedDescription")}</span>
|
|
25
|
+
<Button type="button" variant="outline" size="sm" onClick={onRetry}>
|
|
26
|
+
<IconRefresh className="size-4" />
|
|
27
|
+
{t("dispatch.pages.tryAgain")}
|
|
28
|
+
</Button>
|
|
29
|
+
</AlertDescription>
|
|
30
|
+
</Alert>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { AppKeysPanel } from "./app-keys-popover";
|
|
7
|
+
|
|
8
|
+
const queryState = vi.hoisted(() => ({
|
|
9
|
+
refetchSecrets: vi.fn(),
|
|
10
|
+
refetchGrants: vi.fn(),
|
|
11
|
+
refetchAccess: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@agent-native/core/client", () => ({
|
|
15
|
+
useT: () => (key: string) =>
|
|
16
|
+
({
|
|
17
|
+
"dispatch.pages.dataLoadFailed": "Couldn't load data",
|
|
18
|
+
"dispatch.pages.dataLoadFailedDescription":
|
|
19
|
+
"Dispatch couldn't load this data.",
|
|
20
|
+
"dispatch.pages.tryAgain": "Try again",
|
|
21
|
+
})[key] ?? key,
|
|
22
|
+
useActionQuery: (name: string) => {
|
|
23
|
+
const queries = {
|
|
24
|
+
"list-vault-secret-options": {
|
|
25
|
+
data: undefined,
|
|
26
|
+
isLoading: false,
|
|
27
|
+
error: new Error("Vault unavailable"),
|
|
28
|
+
refetch: queryState.refetchSecrets,
|
|
29
|
+
},
|
|
30
|
+
"list-vault-grants": {
|
|
31
|
+
data: [],
|
|
32
|
+
isLoading: false,
|
|
33
|
+
error: null,
|
|
34
|
+
refetch: queryState.refetchGrants,
|
|
35
|
+
},
|
|
36
|
+
"get-vault-access-settings": {
|
|
37
|
+
data: undefined,
|
|
38
|
+
isLoading: false,
|
|
39
|
+
error: null,
|
|
40
|
+
refetch: queryState.refetchAccess,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return queries[name as keyof typeof queries];
|
|
44
|
+
},
|
|
45
|
+
useActionMutation: () => ({ mutate: vi.fn(), isPending: false }),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
describe("AppKeysPanel", () => {
|
|
49
|
+
let container: HTMLDivElement;
|
|
50
|
+
let root: Root;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
container = document.createElement("div");
|
|
56
|
+
document.body.appendChild(container);
|
|
57
|
+
root = createRoot(container);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
act(() => root.unmount());
|
|
62
|
+
container.remove();
|
|
63
|
+
vi.unstubAllGlobals();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("shows a retryable error instead of an empty key list", () => {
|
|
67
|
+
act(() => {
|
|
68
|
+
root.render(<AppKeysPanel appId="mail" appName="Mail" />);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(container.textContent).toContain("Couldn't load data");
|
|
72
|
+
expect(container.textContent).toContain(
|
|
73
|
+
"Dispatch couldn't load this data.",
|
|
74
|
+
);
|
|
75
|
+
expect(container.textContent).not.toContain("Vault unavailable");
|
|
76
|
+
expect(container.textContent).not.toContain("No vault keys yet");
|
|
77
|
+
|
|
78
|
+
act(() => {
|
|
79
|
+
container.querySelector("button")?.click();
|
|
80
|
+
});
|
|
81
|
+
expect(queryState.refetchSecrets).toHaveBeenCalledOnce();
|
|
82
|
+
expect(queryState.refetchGrants).toHaveBeenCalledOnce();
|
|
83
|
+
expect(queryState.refetchAccess).toHaveBeenCalledOnce();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
import { useMemo, useState, type ReactNode } from "react";
|
|
9
9
|
import { toast } from "sonner";
|
|
10
10
|
|
|
11
|
+
import { ActionQueryError } from "./action-query-error";
|
|
11
12
|
import { Button } from "./ui/button";
|
|
12
13
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
|
13
14
|
import { Skeleton } from "./ui/skeleton";
|
|
15
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
|
14
16
|
|
|
15
17
|
interface VaultSecret {
|
|
16
18
|
id: string;
|
|
@@ -46,23 +48,30 @@ export function AppKeysPopover({
|
|
|
46
48
|
|
|
47
49
|
return (
|
|
48
50
|
<Popover open={open} onOpenChange={setOpen}>
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
51
|
+
<Tooltip>
|
|
52
|
+
<TooltipTrigger asChild>
|
|
53
|
+
<PopoverTrigger asChild>
|
|
54
|
+
{trigger ?? (
|
|
55
|
+
<Button
|
|
56
|
+
type="button"
|
|
57
|
+
variant="ghost"
|
|
58
|
+
size="sm"
|
|
59
|
+
aria-label={`Manage keys for ${appName}`}
|
|
60
|
+
onClick={(event) => {
|
|
61
|
+
// Keep parent card click handlers from also firing. Do not
|
|
62
|
+
// preventDefault here: Radix uses the same click to open the
|
|
63
|
+
// popover trigger.
|
|
64
|
+
event.stopPropagation();
|
|
65
|
+
}}
|
|
66
|
+
className="size-7 rounded-md p-0 text-muted-foreground transition-[background-color,color] hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:bg-accent data-[state=open]:text-foreground"
|
|
67
|
+
>
|
|
68
|
+
<IconSettings size={14} />
|
|
69
|
+
</Button>
|
|
70
|
+
)}
|
|
71
|
+
</PopoverTrigger>
|
|
72
|
+
</TooltipTrigger>
|
|
73
|
+
<TooltipContent>Manage keys</TooltipContent>
|
|
74
|
+
</Tooltip>
|
|
66
75
|
<PopoverContent
|
|
67
76
|
align={align}
|
|
68
77
|
side={side}
|
|
@@ -76,20 +85,23 @@ export function AppKeysPopover({
|
|
|
76
85
|
);
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
function AppKeysPanel({
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
export function AppKeysPanel({
|
|
89
|
+
appId,
|
|
90
|
+
appName,
|
|
91
|
+
}: {
|
|
92
|
+
appId: string;
|
|
93
|
+
appName: string;
|
|
94
|
+
}) {
|
|
95
|
+
const secretsQuery = useActionQuery("list-vault-secret-options", {});
|
|
96
|
+
const grantsQuery = useActionQuery("list-vault-grants", { appId });
|
|
97
|
+
const accessQuery = useActionQuery("get-vault-access-settings", {});
|
|
98
|
+
const { data: secrets = [], isLoading: secretsLoading } = secretsQuery;
|
|
84
99
|
const {
|
|
85
100
|
data: grants = [],
|
|
86
101
|
isLoading: grantsLoading,
|
|
87
102
|
refetch: refetchGrants,
|
|
88
|
-
} =
|
|
89
|
-
const { data: accessSettings, isLoading: accessLoading } =
|
|
90
|
-
"get-vault-access-settings",
|
|
91
|
-
{},
|
|
92
|
-
);
|
|
103
|
+
} = grantsQuery;
|
|
104
|
+
const { data: accessSettings, isLoading: accessLoading } = accessQuery;
|
|
93
105
|
const accessMode =
|
|
94
106
|
(accessSettings as any)?.mode === "manual" ? "manual" : "all-apps";
|
|
95
107
|
|
|
@@ -139,6 +151,7 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
|
|
|
139
151
|
});
|
|
140
152
|
|
|
141
153
|
const isLoading = secretsLoading || grantsLoading || accessLoading;
|
|
154
|
+
const error = secretsQuery.error ?? grantsQuery.error ?? accessQuery.error;
|
|
142
155
|
const grantedCount = grantBySecretId.size;
|
|
143
156
|
const typedSecrets = secrets as VaultSecret[];
|
|
144
157
|
const allApps = accessMode !== "manual";
|
|
@@ -164,34 +177,47 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
|
|
|
164
177
|
Keys for {appName}
|
|
165
178
|
</p>
|
|
166
179
|
<p className="text-[11px] text-muted-foreground">
|
|
167
|
-
{
|
|
168
|
-
?
|
|
169
|
-
:
|
|
180
|
+
{error
|
|
181
|
+
? null
|
|
182
|
+
: allApps
|
|
183
|
+
? `${typedSecrets.length} available`
|
|
184
|
+
: `${grantedCount} of ${typedSecrets.length} granted`}
|
|
170
185
|
</p>
|
|
171
186
|
</div>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
{!error ? (
|
|
188
|
+
<Button
|
|
189
|
+
type="button"
|
|
190
|
+
variant="outline"
|
|
191
|
+
size="sm"
|
|
192
|
+
disabled={
|
|
193
|
+
syncMutation.isPending ||
|
|
194
|
+
typedSecrets.length === 0 ||
|
|
195
|
+
(!allApps && grantedCount === 0)
|
|
196
|
+
}
|
|
197
|
+
onClick={() => syncMutation.mutate({ appId })}
|
|
198
|
+
className="h-7 px-2"
|
|
199
|
+
>
|
|
200
|
+
{syncMutation.isPending ? (
|
|
201
|
+
<IconLoader2 className="h-3 w-3 animate-spin" />
|
|
202
|
+
) : (
|
|
203
|
+
<IconRefresh className="h-3 w-3" />
|
|
204
|
+
)}
|
|
205
|
+
<span className="ml-1 text-xs">Sync</span>
|
|
206
|
+
</Button>
|
|
207
|
+
) : null}
|
|
191
208
|
</div>
|
|
192
209
|
|
|
193
210
|
<div className="max-h-[320px] space-y-1.5 overflow-y-auto rounded-md border border-border bg-card p-1.5">
|
|
194
|
-
{
|
|
211
|
+
{error ? (
|
|
212
|
+
<ActionQueryError
|
|
213
|
+
error={error}
|
|
214
|
+
onRetry={() => {
|
|
215
|
+
void secretsQuery.refetch();
|
|
216
|
+
void grantsQuery.refetch();
|
|
217
|
+
void accessQuery.refetch();
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
) : isLoading ? (
|
|
195
221
|
<div className="space-y-1.5 p-1.5">
|
|
196
222
|
{Array.from({ length: 3 }).map((_, index) => (
|
|
197
223
|
<div
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { MemoryRouter } from "react-router";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { DispatchControlPlane } from "./dispatch-control-plane";
|
|
8
|
+
import { TooltipProvider } from "./ui/tooltip";
|
|
9
|
+
|
|
10
|
+
const clientState = vi.hoisted(() => ({
|
|
11
|
+
navigateWithTransition: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@agent-native/core/client", () => ({
|
|
15
|
+
PromptComposer: ({
|
|
16
|
+
onSubmit,
|
|
17
|
+
placeholder,
|
|
18
|
+
}: {
|
|
19
|
+
onSubmit: (value: string) => void;
|
|
20
|
+
placeholder: string;
|
|
21
|
+
}) => (
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
data-placeholder={placeholder}
|
|
25
|
+
onClick={() => onSubmit("Route onboarding work")}
|
|
26
|
+
>
|
|
27
|
+
Composer
|
|
28
|
+
</button>
|
|
29
|
+
),
|
|
30
|
+
isInBuilderFrame: () => false,
|
|
31
|
+
navigateWithAgentChatViewTransition: (
|
|
32
|
+
navigate: unknown,
|
|
33
|
+
path: string,
|
|
34
|
+
options?: unknown,
|
|
35
|
+
) => clientState.navigateWithTransition(navigate, path, options),
|
|
36
|
+
useActionQuery: () => ({
|
|
37
|
+
data: [],
|
|
38
|
+
isLoading: false,
|
|
39
|
+
isError: false,
|
|
40
|
+
refetch: vi.fn(),
|
|
41
|
+
}),
|
|
42
|
+
useChatModels: () => ({ selectedModel: "auto" }),
|
|
43
|
+
useT: () => (key: string, values?: { defaultValue?: string }) =>
|
|
44
|
+
values?.defaultValue ?? key,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock("./create-app-popover", () => ({
|
|
48
|
+
CreateAppPopover: () => <div>Create app</div>,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
describe("DispatchControlPlane", () => {
|
|
52
|
+
let container: HTMLDivElement;
|
|
53
|
+
let root: Root;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
57
|
+
clientState.navigateWithTransition.mockReset();
|
|
58
|
+
container = document.createElement("div");
|
|
59
|
+
document.body.appendChild(container);
|
|
60
|
+
root = createRoot(container);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
act(() => root.unmount());
|
|
65
|
+
container.remove();
|
|
66
|
+
vi.unstubAllGlobals();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders a minimal Ask surface and transitions submitted prompts into Chat", async () => {
|
|
70
|
+
await act(async () => {
|
|
71
|
+
root.render(
|
|
72
|
+
<MemoryRouter initialEntries={["/overview"]}>
|
|
73
|
+
<TooltipProvider>
|
|
74
|
+
<DispatchControlPlane />
|
|
75
|
+
</TooltipProvider>
|
|
76
|
+
</MemoryRouter>,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(container.textContent).toContain("Chat across your apps");
|
|
81
|
+
expect(container.textContent).not.toContain("Open chat");
|
|
82
|
+
expect(container.textContent).not.toContain("Also");
|
|
83
|
+
expect(container.textContent).not.toContain("active");
|
|
84
|
+
expect(container.querySelector("nav")).toBeNull();
|
|
85
|
+
expect(
|
|
86
|
+
container.querySelector('[data-placeholder="Ask Dispatch anything..."]'),
|
|
87
|
+
).not.toBeNull();
|
|
88
|
+
|
|
89
|
+
await act(async () => {
|
|
90
|
+
container.querySelector<HTMLButtonElement>("[data-placeholder]")?.click();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(clientState.navigateWithTransition).toHaveBeenCalledWith(
|
|
94
|
+
expect.any(Function),
|
|
95
|
+
"/chat",
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
state: {
|
|
98
|
+
dispatchPrompt: expect.objectContaining({
|
|
99
|
+
message: "Route onboarding work",
|
|
100
|
+
selectedModel: "auto",
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|