@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
|
@@ -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 () => {
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
useAgentChatHomeHandoff,
|
|
10
10
|
useAgentChatHomeHandoffLinks,
|
|
11
11
|
useChatThreads,
|
|
12
|
-
useFormatters,
|
|
13
12
|
useT,
|
|
14
13
|
type ChatThreadSummary,
|
|
15
14
|
} from "@agent-native/core/client";
|
|
@@ -94,13 +93,6 @@ export interface DispatchExtensionConfig {
|
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
const PRIMARY_NAV_ITEMS = [
|
|
97
|
-
{
|
|
98
|
-
id: "chat",
|
|
99
|
-
to: "/chat",
|
|
100
|
-
label: "Chat",
|
|
101
|
-
icon: IconMessageQuestion,
|
|
102
|
-
section: "primary",
|
|
103
|
-
},
|
|
104
96
|
{
|
|
105
97
|
id: "overview",
|
|
106
98
|
to: "/overview",
|
|
@@ -108,6 +100,13 @@ const PRIMARY_NAV_ITEMS = [
|
|
|
108
100
|
icon: IconBroadcast,
|
|
109
101
|
section: "primary",
|
|
110
102
|
},
|
|
103
|
+
{
|
|
104
|
+
id: "chat",
|
|
105
|
+
to: "/chat",
|
|
106
|
+
label: "Chat",
|
|
107
|
+
icon: IconMessageQuestion,
|
|
108
|
+
section: "primary",
|
|
109
|
+
},
|
|
111
110
|
{
|
|
112
111
|
id: "apps",
|
|
113
112
|
to: "/apps",
|
|
@@ -312,22 +311,17 @@ function threadIdFromPath(pathname: string): string | null {
|
|
|
312
311
|
}
|
|
313
312
|
}
|
|
314
313
|
|
|
315
|
-
function formatThreadAge(
|
|
316
|
-
|
|
317
|
-
formatDate: ReturnType<typeof useFormatters>["formatDate"],
|
|
318
|
-
) {
|
|
319
|
-
const diffMs = Math.max(0, Date.now() - updatedAt);
|
|
314
|
+
export function formatThreadAge(updatedAt: number, now = Date.now()) {
|
|
315
|
+
const diffMs = Math.max(0, now - updatedAt);
|
|
320
316
|
const minutes = Math.floor(diffMs / 60_000);
|
|
321
317
|
if (minutes < 1) return "now";
|
|
322
318
|
if (minutes < 60) return `${minutes}m`;
|
|
323
319
|
const hours = Math.floor(minutes / 60);
|
|
324
320
|
if (hours < 24) return `${hours}h`;
|
|
325
321
|
const days = Math.floor(hours / 24);
|
|
326
|
-
if (days <
|
|
327
|
-
return
|
|
328
|
-
|
|
329
|
-
day: "numeric",
|
|
330
|
-
});
|
|
322
|
+
if (days < 14) return `${days}d`;
|
|
323
|
+
if (days < 365) return `${Math.floor(days / 7)}w`;
|
|
324
|
+
return `${Math.floor(days / 365)}y`;
|
|
331
325
|
}
|
|
332
326
|
|
|
333
327
|
function threadTitle(thread: ChatThreadSummary, fallback: string) {
|
|
@@ -344,7 +338,6 @@ function threadUpdatedAt(thread: ChatThreadSummary) {
|
|
|
344
338
|
|
|
345
339
|
function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
|
|
346
340
|
const t = useT();
|
|
347
|
-
const { formatDate } = useFormatters();
|
|
348
341
|
const navigate = useNavigate();
|
|
349
342
|
const location = useLocation();
|
|
350
343
|
const {
|
|
@@ -510,19 +503,21 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
|
|
|
510
503
|
<button
|
|
511
504
|
type="button"
|
|
512
505
|
onClick={() => openThread(thread.id)}
|
|
513
|
-
className="min-w-0 flex-1 cursor-pointer px-2 py-1.5 pe-
|
|
506
|
+
className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 px-2 py-1.5 pe-1 text-start text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
514
507
|
>
|
|
515
|
-
<span className="block min-w-0 truncate">
|
|
508
|
+
<span className="block min-w-0 flex-1 truncate">
|
|
509
|
+
{title}
|
|
510
|
+
</span>
|
|
511
|
+
<time className="w-8 shrink-0 whitespace-nowrap text-end text-[11px] tabular-nums text-muted-foreground/60 transition-opacity group-hover/item:opacity-0 group-focus-within/item:opacity-0">
|
|
512
|
+
{isActive
|
|
513
|
+
? ""
|
|
514
|
+
: formatThreadAge(threadUpdatedAt(thread))}
|
|
515
|
+
</time>
|
|
516
516
|
</button>
|
|
517
517
|
</TooltipTrigger>
|
|
518
518
|
<TooltipContent side="right">{title}</TooltipContent>
|
|
519
519
|
</Tooltip>
|
|
520
520
|
<div className="pointer-events-none absolute end-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5">
|
|
521
|
-
<span className="pointer-events-none pe-1 text-[11px] text-muted-foreground/60 transition-opacity group-hover/item:opacity-0 group-focus-within/item:opacity-0">
|
|
522
|
-
{isActive
|
|
523
|
-
? ""
|
|
524
|
-
: formatThreadAge(threadUpdatedAt(thread), formatDate)}
|
|
525
|
-
</span>
|
|
526
521
|
<DropdownMenu>
|
|
527
522
|
<Tooltip>
|
|
528
523
|
<TooltipTrigger asChild>
|
|
@@ -532,7 +527,7 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
|
|
|
532
527
|
aria-label={t("dispatch.sidebar.chatOptions", {
|
|
533
528
|
title,
|
|
534
529
|
})}
|
|
535
|
-
className="pointer-events-auto rounded p-0.5 text-muted-foreground/50 opacity-0 transition-
|
|
530
|
+
className="pointer-events-auto rounded p-0.5 text-muted-foreground/50 opacity-0 transition-[color,opacity] hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/item:opacity-100 group-focus-within/item:opacity-100 data-[state=open]:opacity-100 data-[state=open]:text-foreground"
|
|
536
531
|
>
|
|
537
532
|
<IconDots className="size-3" />
|
|
538
533
|
</button>
|
|
@@ -25,7 +25,15 @@ function useTaskQueueStats() {
|
|
|
25
25
|
queryKey: [...TASK_QUEUE_QUERY_KEY, version],
|
|
26
26
|
queryFn: getDispatchTaskQueueStats,
|
|
27
27
|
placeholderData: (prev) => prev,
|
|
28
|
-
|
|
28
|
+
// Queue work can finish outside the action transport, so retain a bounded
|
|
29
|
+
// fallback only here. Idle workspaces back off aggressively; active queues
|
|
30
|
+
// keep the existing monitoring cadence.
|
|
31
|
+
refetchInterval: (query) => {
|
|
32
|
+
const stats = query.state.data;
|
|
33
|
+
return stats && (stats.pending > 0 || stats.processing > 0)
|
|
34
|
+
? 15_000
|
|
35
|
+
: 300_000;
|
|
36
|
+
},
|
|
29
37
|
staleTime: 5_000,
|
|
30
38
|
});
|
|
31
39
|
}
|
|
@@ -29,7 +29,7 @@ const SheetOverlay = React.forwardRef<
|
|
|
29
29
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
|
30
30
|
|
|
31
31
|
const sheetVariants = cva(
|
|
32
|
-
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-
|
|
32
|
+
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-150 data-[state=open]:duration-200",
|
|
33
33
|
{
|
|
34
34
|
variants: {
|
|
35
35
|
side: {
|
|
@@ -23,7 +23,7 @@ const ToastViewport = React.forwardRef<
|
|
|
23
23
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
|
24
24
|
|
|
25
25
|
const toastVariants = cva(
|
|
26
|
-
"group pointer-events-auto relative flex w-full items-center justify-between gap-3 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-
|
|
26
|
+
"group pointer-events-auto relative flex w-full items-center justify-between gap-3 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-transform motion-reduce:animate-none motion-reduce:transition-none data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
|
27
27
|
{
|
|
28
28
|
variants: {
|
|
29
29
|
variant: {
|
|
@@ -0,0 +1,80 @@
|
|
|
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 { TooltipProvider } from "./ui/tooltip";
|
|
7
|
+
import { WorkspaceAppCard } from "./workspace-app-card";
|
|
8
|
+
|
|
9
|
+
vi.mock("@agent-native/core/client", () => ({
|
|
10
|
+
useActionMutation: () => ({ mutate: vi.fn(), isPending: false }),
|
|
11
|
+
useActionQuery: () => ({
|
|
12
|
+
data: { resources: [], counts: {} },
|
|
13
|
+
isLoading: false,
|
|
14
|
+
isError: false,
|
|
15
|
+
error: null,
|
|
16
|
+
refetch: vi.fn(),
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe("WorkspaceAppCard", () => {
|
|
21
|
+
let container: HTMLDivElement;
|
|
22
|
+
let root: Root;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
26
|
+
container = document.createElement("div");
|
|
27
|
+
document.body.appendChild(container);
|
|
28
|
+
root = createRoot(container);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
act(() => root.unmount());
|
|
33
|
+
container.remove();
|
|
34
|
+
vi.unstubAllGlobals();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("uses one visible, consistent action treatment for context, keys, and more", async () => {
|
|
38
|
+
await act(async () => {
|
|
39
|
+
root.render(
|
|
40
|
+
<TooltipProvider>
|
|
41
|
+
<WorkspaceAppCard
|
|
42
|
+
app={{
|
|
43
|
+
id: "analytics",
|
|
44
|
+
name: "Analytics",
|
|
45
|
+
path: "/analytics",
|
|
46
|
+
description: "Explore product and growth performance.",
|
|
47
|
+
status: "ready",
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
</TooltipProvider>,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(
|
|
55
|
+
container.querySelector('a[aria-label="Open Analytics"]')?.className,
|
|
56
|
+
).toContain("focus-visible:ring-2");
|
|
57
|
+
|
|
58
|
+
const actions = [
|
|
59
|
+
container.querySelector<HTMLButtonElement>(
|
|
60
|
+
'button[aria-label="View context resources for Analytics"]',
|
|
61
|
+
),
|
|
62
|
+
container.querySelector<HTMLButtonElement>(
|
|
63
|
+
'button[aria-label="Manage keys for Analytics"]',
|
|
64
|
+
),
|
|
65
|
+
container.querySelector<HTMLButtonElement>(
|
|
66
|
+
'button[aria-label="More actions for Analytics"]',
|
|
67
|
+
),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
for (const action of actions) {
|
|
71
|
+
expect(action).not.toBeNull();
|
|
72
|
+
expect(action?.className).toContain("size-7");
|
|
73
|
+
expect(action?.className).toContain("text-muted-foreground");
|
|
74
|
+
expect(action?.className).toContain(
|
|
75
|
+
"transition-[background-color,color]",
|
|
76
|
+
);
|
|
77
|
+
expect(action?.className).not.toContain("opacity-0");
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|