@agent-native/dispatch 0.14.2 → 0.14.4
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 -122
- package/dist/actions/approve-dispatch-change.d.ts +10 -10
- 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/provider-api-register.js +64 -2
- package/dist/actions/provider-api-register.js.map +1 -1
- package/dist/actions/reject-dispatch-change.d.ts +10 -10
- 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/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 +9 -3
- 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/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/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 +24 -21
- package/dist/server/lib/dispatch-store.d.ts.map +1 -1
- package/dist/server/lib/dispatch-store.js +59 -23
- 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/lib/vault-store.d.ts.map +1 -1
- package/dist/server/lib/vault-store.js +102 -37
- package/dist/server/lib/vault-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.d.ts +1 -1
- package/package.json +2 -3
- package/src/actions/list_apps.spec.ts +52 -0
- package/src/actions/list_apps.ts +16 -8
- package/src/actions/provider-api-register.spec.ts +193 -0
- package/src/actions/provider-api-register.ts +66 -1
- 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/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 +20 -7
- package/src/routes/pages/audit.tsx +29 -20
- 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/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 +420 -0
- package/src/server/lib/dispatch-store.ts +94 -21
- 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/server/lib/vault-store.ts +151 -45
|
@@ -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
|
+
});
|
|
@@ -1,76 +1,56 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PromptComposer,
|
|
3
3
|
isInBuilderFrame,
|
|
4
|
+
navigateWithAgentChatViewTransition,
|
|
4
5
|
useActionQuery,
|
|
5
6
|
useChatModels,
|
|
6
7
|
useT,
|
|
7
8
|
} from "@agent-native/core/client";
|
|
8
|
-
import {
|
|
9
|
-
IconArrowUpRight,
|
|
10
|
-
IconBroadcast,
|
|
11
|
-
IconStack3,
|
|
12
|
-
type IconProps,
|
|
13
|
-
} from "@tabler/icons-react";
|
|
9
|
+
import { IconArrowUpRight } from "@tabler/icons-react";
|
|
14
10
|
import type { ReactNode } from "react";
|
|
15
11
|
import { Link, useNavigate } from "react-router";
|
|
16
12
|
|
|
17
13
|
import { submitOverviewPrompt } from "../lib/overview-chat";
|
|
18
14
|
import type { WorkspaceAppSummary } from "../lib/workspace-apps";
|
|
15
|
+
import { ActionQueryError } from "./action-query-error";
|
|
19
16
|
import { CreateAppPopover } from "./create-app-popover";
|
|
20
|
-
import {
|
|
17
|
+
import { useSetPageTitle } from "./layout/HeaderActions";
|
|
21
18
|
import { Button } from "./ui/button";
|
|
22
19
|
import { Skeleton } from "./ui/skeleton";
|
|
23
20
|
import { WorkspaceAppCard } from "./workspace-app-card";
|
|
24
21
|
|
|
25
|
-
const PROMPT_SUGGESTIONS = [
|
|
26
|
-
"Summarize the current workspace health",
|
|
27
|
-
"Create an app for onboarding requests",
|
|
28
|
-
"Check which agents can help with analytics",
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
const WORKSPACE_LINKS = [
|
|
32
|
-
{ to: "/automations", labelKey: "dispatch.nav.automations" },
|
|
33
|
-
{ to: "/approvals", labelKey: "dispatch.nav.approvals" },
|
|
34
|
-
{ to: "/destinations", labelKey: "dispatch.nav.delivery" },
|
|
35
|
-
{ to: "/agents", labelKey: "dispatch.nav.agents" },
|
|
36
|
-
{ to: "/vault", labelKey: "dispatch.nav.vault" },
|
|
37
|
-
{ to: "/audit", labelKey: "dispatch.nav.audit" },
|
|
38
|
-
] as const;
|
|
39
|
-
|
|
40
22
|
function SectionHeader({
|
|
41
|
-
icon: Icon,
|
|
42
23
|
title,
|
|
43
|
-
detail,
|
|
44
24
|
action,
|
|
45
25
|
}: {
|
|
46
|
-
icon: React.ComponentType<IconProps>;
|
|
47
26
|
title: string;
|
|
48
|
-
detail?: string;
|
|
49
27
|
action?: ReactNode;
|
|
50
28
|
}) {
|
|
51
29
|
return (
|
|
52
30
|
<div className="flex min-w-0 items-center justify-between gap-3">
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<h2 className="truncate text-sm font-semibold text-foreground">
|
|
57
|
-
{title}
|
|
58
|
-
</h2>
|
|
59
|
-
{detail ? (
|
|
60
|
-
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
61
|
-
{detail}
|
|
62
|
-
</p>
|
|
63
|
-
) : null}
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
31
|
+
<h2 className="truncate text-sm font-semibold text-foreground">
|
|
32
|
+
{title}
|
|
33
|
+
</h2>
|
|
66
34
|
{action ? <div className="shrink-0">{action}</div> : null}
|
|
67
35
|
</div>
|
|
68
36
|
);
|
|
69
37
|
}
|
|
70
38
|
|
|
71
39
|
function CommandPanel() {
|
|
40
|
+
const t = useT();
|
|
72
41
|
const { selectedModel } = useChatModels();
|
|
73
42
|
const navigate = useNavigate();
|
|
43
|
+
const promptSuggestions = [
|
|
44
|
+
t("dispatch.pages.suggestionWorkspaceHealth", {
|
|
45
|
+
defaultValue: "Summarize the current workspace health",
|
|
46
|
+
}),
|
|
47
|
+
t("dispatch.pages.suggestionOnboardingApp", {
|
|
48
|
+
defaultValue: "Create an app for onboarding requests",
|
|
49
|
+
}),
|
|
50
|
+
t("dispatch.pages.suggestionAnalyticsAgents", {
|
|
51
|
+
defaultValue: "Check which agents can help with analytics",
|
|
52
|
+
}),
|
|
53
|
+
];
|
|
74
54
|
|
|
75
55
|
function send(message: string) {
|
|
76
56
|
const trimmed = message.trim();
|
|
@@ -81,7 +61,7 @@ function CommandPanel() {
|
|
|
81
61
|
return;
|
|
82
62
|
}
|
|
83
63
|
|
|
84
|
-
navigate
|
|
64
|
+
navigateWithAgentChatViewTransition(navigate, "/chat", {
|
|
85
65
|
state: {
|
|
86
66
|
dispatchPrompt: {
|
|
87
67
|
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -93,71 +73,44 @@ function CommandPanel() {
|
|
|
93
73
|
}
|
|
94
74
|
|
|
95
75
|
return (
|
|
96
|
-
<section className="
|
|
97
|
-
<div className="
|
|
98
|
-
<div className="
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
76
|
+
<section className="flex flex-col">
|
|
77
|
+
<div className="mx-auto flex w-full max-w-3xl flex-col pt-4 sm:pt-6">
|
|
78
|
+
<div className="mb-5 text-center">
|
|
79
|
+
<h2 className="text-2xl font-semibold tracking-tight text-foreground sm:text-3xl">
|
|
80
|
+
{t("dispatch.pages.chatAcrossApps", {
|
|
81
|
+
defaultValue: "Chat across your apps",
|
|
82
|
+
})}
|
|
102
83
|
</h2>
|
|
84
|
+
<p className="mx-auto mt-2 max-w-xl text-sm leading-6 text-muted-foreground">
|
|
85
|
+
{t("dispatch.pages.chatAcrossAppsDescription", {
|
|
86
|
+
defaultValue:
|
|
87
|
+
"Route work, inspect status, or create something new from one place.",
|
|
88
|
+
})}
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
<PromptComposer
|
|
92
|
+
placeholder={t("dispatch.pages.overviewPromptPlaceholder", {
|
|
93
|
+
defaultValue: "Ask Dispatch anything...",
|
|
94
|
+
})}
|
|
95
|
+
onSubmit={(text) => send(text)}
|
|
96
|
+
/>
|
|
97
|
+
<div className="mt-3 flex flex-wrap justify-center gap-2">
|
|
98
|
+
{promptSuggestions.map((suggestion) => (
|
|
99
|
+
<button
|
|
100
|
+
key={suggestion}
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={() => send(suggestion)}
|
|
103
|
+
className="cursor-pointer rounded-md border border-transparent bg-muted/60 px-2.5 py-1.5 text-xs text-muted-foreground transition-[background-color,color] hover:bg-muted hover:text-foreground"
|
|
104
|
+
>
|
|
105
|
+
{suggestion}
|
|
106
|
+
</button>
|
|
107
|
+
))}
|
|
103
108
|
</div>
|
|
104
|
-
<Button variant="outline" size="sm" asChild>
|
|
105
|
-
<Link to="/chat">
|
|
106
|
-
Open chat
|
|
107
|
-
<IconArrowUpRight size={14} />
|
|
108
|
-
</Link>
|
|
109
|
-
</Button>
|
|
110
|
-
</div>
|
|
111
|
-
<PromptComposer
|
|
112
|
-
placeholder="Route work, inspect status, or create an app..."
|
|
113
|
-
onSubmit={(text) => send(text)}
|
|
114
|
-
/>
|
|
115
|
-
<div className="mt-3 flex flex-wrap gap-2">
|
|
116
|
-
{PROMPT_SUGGESTIONS.map((suggestion) => (
|
|
117
|
-
<button
|
|
118
|
-
key={suggestion}
|
|
119
|
-
type="button"
|
|
120
|
-
onClick={() => send(suggestion)}
|
|
121
|
-
className="cursor-pointer rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-muted-foreground transition hover:border-foreground/30 hover:text-foreground"
|
|
122
|
-
>
|
|
123
|
-
{suggestion}
|
|
124
|
-
</button>
|
|
125
|
-
))}
|
|
126
109
|
</div>
|
|
127
110
|
</section>
|
|
128
111
|
);
|
|
129
112
|
}
|
|
130
113
|
|
|
131
|
-
function WorkspaceLinks() {
|
|
132
|
-
const t = useT();
|
|
133
|
-
|
|
134
|
-
return (
|
|
135
|
-
<nav
|
|
136
|
-
aria-label={t("dispatch.pages.workspaceShortcutsAria")}
|
|
137
|
-
className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground"
|
|
138
|
-
>
|
|
139
|
-
<span className="font-medium text-foreground/70">
|
|
140
|
-
{t("dispatch.pages.also")}
|
|
141
|
-
</span>
|
|
142
|
-
{WORKSPACE_LINKS.map((item, index) => (
|
|
143
|
-
<span key={item.to} className="inline-flex items-center gap-x-3">
|
|
144
|
-
{index > 0 ? (
|
|
145
|
-
<span aria-hidden className="text-border">
|
|
146
|
-
·
|
|
147
|
-
</span>
|
|
148
|
-
) : null}
|
|
149
|
-
<Link
|
|
150
|
-
to={item.to}
|
|
151
|
-
className="transition hover:text-foreground hover:underline underline-offset-4"
|
|
152
|
-
>
|
|
153
|
-
{t(item.labelKey)}
|
|
154
|
-
</Link>
|
|
155
|
-
</span>
|
|
156
|
-
))}
|
|
157
|
-
</nav>
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
114
|
function AppsPanel({
|
|
162
115
|
apps,
|
|
163
116
|
isLoading,
|
|
@@ -171,11 +124,7 @@ function AppsPanel({
|
|
|
171
124
|
return (
|
|
172
125
|
<section className="flex flex-col gap-3">
|
|
173
126
|
<SectionHeader
|
|
174
|
-
icon={IconStack3}
|
|
175
127
|
title="Apps"
|
|
176
|
-
detail={
|
|
177
|
-
visibleApps.length === 1 ? "1 active" : `${visibleApps.length} active`
|
|
178
|
-
}
|
|
179
128
|
action={
|
|
180
129
|
<Button variant="outline" size="sm" asChild>
|
|
181
130
|
<Link to="/apps">
|
|
@@ -188,7 +137,10 @@ function AppsPanel({
|
|
|
188
137
|
{showSkeletons ? (
|
|
189
138
|
<div className="grid gap-3 md:grid-cols-2">
|
|
190
139
|
{Array.from({ length: 4 }).map((_, index) => (
|
|
191
|
-
<div
|
|
140
|
+
<div
|
|
141
|
+
key={index}
|
|
142
|
+
className="rounded-xl border border-border/60 bg-card/40 p-4"
|
|
143
|
+
>
|
|
192
144
|
<Skeleton className="h-4 w-32" />
|
|
193
145
|
<Skeleton className="mt-3 h-3 w-24" />
|
|
194
146
|
<Skeleton className="mt-3 h-3 w-full" />
|
|
@@ -209,20 +161,24 @@ function AppsPanel({
|
|
|
209
161
|
}
|
|
210
162
|
|
|
211
163
|
export function DispatchControlPlane() {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
164
|
+
useSetPageTitle(<span aria-hidden />);
|
|
165
|
+
const appsQuery = useActionQuery<WorkspaceAppSummary[]>(
|
|
166
|
+
"list-workspace-apps",
|
|
167
|
+
{ includeAgentCards: false, includeArchived: true },
|
|
168
|
+
);
|
|
169
|
+
const { data: workspaceApps = [], isLoading: appsLoading } = appsQuery;
|
|
215
170
|
|
|
216
171
|
return (
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
172
|
+
<div className="flex flex-col gap-8">
|
|
173
|
+
<CommandPanel />
|
|
174
|
+
{appsQuery.isError ? (
|
|
175
|
+
<ActionQueryError
|
|
176
|
+
error={appsQuery.error}
|
|
177
|
+
onRetry={() => void appsQuery.refetch()}
|
|
178
|
+
/>
|
|
179
|
+
) : (
|
|
224
180
|
<AppsPanel apps={workspaceApps ?? []} isLoading={appsLoading} />
|
|
225
|
-
|
|
226
|
-
</
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
227
183
|
);
|
|
228
184
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { MemoryRouter } from "react-router";
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
7
|
import { TooltipProvider } from "../ui/tooltip";
|
|
8
|
-
import { NavContent } from "./Layout";
|
|
8
|
+
import { formatThreadAge, NavContent } from "./Layout";
|
|
9
9
|
|
|
10
10
|
const clientState = vi.hoisted(() => ({
|
|
11
11
|
createThread: vi.fn<() => Promise<string | null>>(),
|
|
@@ -66,6 +66,20 @@ vi.mock("@agent-native/core/client/org", () => ({
|
|
|
66
66
|
OrgSwitcher: () => <div>Organization</div>,
|
|
67
67
|
}));
|
|
68
68
|
|
|
69
|
+
describe("formatThreadAge", () => {
|
|
70
|
+
const now = 2_000_000_000_000;
|
|
71
|
+
|
|
72
|
+
it.each([
|
|
73
|
+
[0, "now"],
|
|
74
|
+
[2 * 60 * 60_000, "2h"],
|
|
75
|
+
[7 * 24 * 60 * 60_000, "7d"],
|
|
76
|
+
[21 * 24 * 60 * 60_000, "3w"],
|
|
77
|
+
[365 * 24 * 60 * 60_000, "1y"],
|
|
78
|
+
])("formats %i milliseconds as %s", (elapsed, expected) => {
|
|
79
|
+
expect(formatThreadAge(now - elapsed, now)).toBe(expected);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
69
83
|
describe("Dispatch NavContent", () => {
|
|
70
84
|
let container: HTMLDivElement;
|
|
71
85
|
let root: Root;
|
|
@@ -105,6 +119,25 @@ describe("Dispatch NavContent", () => {
|
|
|
105
119
|
vi.unstubAllGlobals();
|
|
106
120
|
});
|
|
107
121
|
|
|
122
|
+
it("puts Overview before Chat in the primary navigation", async () => {
|
|
123
|
+
await act(async () => {
|
|
124
|
+
root.render(
|
|
125
|
+
<MemoryRouter initialEntries={["/overview"]}>
|
|
126
|
+
<TooltipProvider>
|
|
127
|
+
<NavContent />
|
|
128
|
+
</TooltipProvider>
|
|
129
|
+
</MemoryRouter>,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const primaryLabels = [...container.querySelectorAll("nav a")].map((link) =>
|
|
134
|
+
link.textContent?.trim(),
|
|
135
|
+
);
|
|
136
|
+
expect(primaryLabels.indexOf("Overview")).toBeLessThan(
|
|
137
|
+
primaryLabels.indexOf("Chat"),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
108
141
|
it("uses the quieter Analytics-style chat history and retains thread actions", async () => {
|
|
109
142
|
await act(async () => {
|
|
110
143
|
root.render(
|
|
@@ -121,6 +154,13 @@ describe("Dispatch NavContent", () => {
|
|
|
121
154
|
expect(container.textContent).toContain("Earlier Dispatch work");
|
|
122
155
|
expect(container.textContent).toContain("New chat");
|
|
123
156
|
expect(container.textContent).toContain("5m");
|
|
157
|
+
const age = [...container.querySelectorAll("time")].find(
|
|
158
|
+
(element) => element.textContent === "5m",
|
|
159
|
+
);
|
|
160
|
+
expect(age?.className).toContain("w-8");
|
|
161
|
+
expect(age?.className).toContain("shrink-0");
|
|
162
|
+
expect(age?.className).toContain("whitespace-nowrap");
|
|
163
|
+
expect(age?.className).toContain("tabular-nums");
|
|
124
164
|
expect(
|
|
125
165
|
[...container.querySelectorAll("div")].some((element) =>
|
|
126
166
|
element.className.includes("group/item"),
|
|
@@ -133,7 +173,7 @@ describe("Dispatch NavContent", () => {
|
|
|
133
173
|
expect(container.textContent).not.toContain("Workspace control plane");
|
|
134
174
|
|
|
135
175
|
const threadButton = [...container.querySelectorAll("button")].find(
|
|
136
|
-
(button) => button.textContent
|
|
176
|
+
(button) => button.textContent?.includes("Earlier Dispatch work"),
|
|
137
177
|
);
|
|
138
178
|
expect(threadButton).toBeDefined();
|
|
139
179
|
await act(async () => {
|