@agent-native/dispatch 0.30.3 → 0.31.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/dist/actions/provider-api-register.d.ts +5 -5
- package/dist/components/automation-details-panel.d.ts +8 -0
- package/dist/components/automation-details-panel.d.ts.map +1 -0
- package/dist/components/automation-details-panel.js +80 -0
- package/dist/components/automation-details-panel.js.map +1 -0
- package/dist/lib/automations.d.ts +12 -0
- package/dist/lib/automations.d.ts.map +1 -1
- package/dist/lib/automations.js +9 -0
- package/dist/lib/automations.js.map +1 -1
- package/dist/routes/pages/admin.d.ts.map +1 -1
- package/dist/routes/pages/admin.js +6 -1
- package/dist/routes/pages/admin.js.map +1 -1
- package/dist/routes/pages/automations.d.ts.map +1 -1
- package/dist/routes/pages/automations.js +59 -21
- 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 +45 -18
- package/dist/routes/pages/chat.js.map +1 -1
- package/package.json +4 -4
- package/src/components/automation-details-panel.tsx +420 -0
- package/src/lib/automations.spec.ts +19 -1
- package/src/lib/automations.ts +23 -0
- package/src/routes/pages/admin.tsx +8 -1
- package/src/routes/pages/automations.tsx +177 -97
- package/src/routes/pages/chat.spec.tsx +49 -0
- package/src/routes/pages/chat.tsx +51 -0
- package/dist/components/automation-run-history-dialog.d.ts +0 -9
- package/dist/components/automation-run-history-dialog.d.ts.map +0 -1
- package/dist/components/automation-run-history-dialog.js +0 -63
- package/dist/components/automation-run-history-dialog.js.map +0 -1
- package/src/components/automation-run-history-dialog.tsx +0 -247
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { listDispatchAutomations } from "./automations.js";
|
|
3
|
+
import { automationRunScope, listDispatchAutomations } from "./automations.js";
|
|
4
|
+
|
|
5
|
+
describe("automationRunScope", () => {
|
|
6
|
+
it("uses the requesting user for legacy shared history", () => {
|
|
7
|
+
expect(
|
|
8
|
+
automationRunScope({ owner: "__shared__", scope: "organization" }),
|
|
9
|
+
).toBe("personal");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("uses organization history when a shared automation has an org id", () => {
|
|
13
|
+
expect(
|
|
14
|
+
automationRunScope({
|
|
15
|
+
owner: "__shared__",
|
|
16
|
+
orgId: "org-1",
|
|
17
|
+
scope: "organization",
|
|
18
|
+
}),
|
|
19
|
+
).toBe("organization");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
4
22
|
|
|
5
23
|
describe("listDispatchAutomations", () => {
|
|
6
24
|
afterEach(() => {
|
package/src/lib/automations.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface DispatchAutomationItem {
|
|
|
6
6
|
path: string;
|
|
7
7
|
owner: string;
|
|
8
8
|
appId?: string;
|
|
9
|
+
orgId?: string;
|
|
9
10
|
scope?: "personal" | "organization";
|
|
10
11
|
canUpdate?: boolean;
|
|
11
12
|
triggerType?: "schedule" | "event" | string;
|
|
@@ -23,6 +24,28 @@ export interface DispatchAutomationItem {
|
|
|
23
24
|
lastError?: string;
|
|
24
25
|
nextRun?: string;
|
|
25
26
|
createdBy?: string;
|
|
27
|
+
body?: string;
|
|
28
|
+
model?: string;
|
|
29
|
+
mcpTools?: string[];
|
|
30
|
+
runAs?: "creator" | "shared";
|
|
31
|
+
executionHostId?: string;
|
|
32
|
+
executionEngine?: string;
|
|
33
|
+
executionCwd?: string;
|
|
34
|
+
deliveryPlatform?: string;
|
|
35
|
+
deliveryDestination?: string;
|
|
36
|
+
deliveryThreadRef?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function automationRunScope(
|
|
40
|
+
automation: Pick<DispatchAutomationItem, "owner" | "orgId" | "scope">,
|
|
41
|
+
): "personal" | "organization" {
|
|
42
|
+
if (automation.owner === "__shared__" && !automation.orgId) {
|
|
43
|
+
return "personal";
|
|
44
|
+
}
|
|
45
|
+
return automation.scope === "organization" ||
|
|
46
|
+
automation.owner.startsWith("__organization__:")
|
|
47
|
+
? "organization"
|
|
48
|
+
: "personal";
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
export interface SetDispatchAutomationEnabledInput {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Outlet } from "react-router";
|
|
1
|
+
import { Outlet, useLocation } from "react-router";
|
|
2
2
|
|
|
3
3
|
import { AdminShell } from "../../components/admin-navigation";
|
|
4
4
|
|
|
@@ -7,6 +7,13 @@ export function meta() {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export default function AdminRoute() {
|
|
10
|
+
const location = useLocation();
|
|
11
|
+
const isAutomationsRoute =
|
|
12
|
+
location.pathname === "/admin/automations" ||
|
|
13
|
+
location.pathname.startsWith("/admin/automations/");
|
|
14
|
+
|
|
15
|
+
if (isAutomationsRoute) return <Outlet />;
|
|
16
|
+
|
|
10
17
|
return (
|
|
11
18
|
<AdminShell>
|
|
12
19
|
<Outlet />
|
|
@@ -5,17 +5,19 @@ import {
|
|
|
5
5
|
IconFileSearch,
|
|
6
6
|
IconListDetails,
|
|
7
7
|
IconPlus,
|
|
8
|
+
IconSearch,
|
|
8
9
|
IconSettingsAutomation,
|
|
9
10
|
} from "@tabler/icons-react";
|
|
10
11
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
11
|
-
import { useMemo, useState } from "react";
|
|
12
|
+
import { useEffect, useMemo, useState } from "react";
|
|
12
13
|
import { Link } from "react-router";
|
|
13
14
|
import { toast } from "sonner";
|
|
14
15
|
|
|
15
|
-
import {
|
|
16
|
+
import { AutomationDetailsPanel } from "../../components/automation-details-panel";
|
|
16
17
|
import { DispatchShell } from "../../components/dispatch-shell";
|
|
17
18
|
import { Badge } from "../../components/ui/badge";
|
|
18
19
|
import { Button } from "../../components/ui/button";
|
|
20
|
+
import { Input } from "../../components/ui/input";
|
|
19
21
|
import {
|
|
20
22
|
Popover,
|
|
21
23
|
PopoverContent,
|
|
@@ -188,6 +190,7 @@ function CreateAutomationButton() {
|
|
|
188
190
|
|
|
189
191
|
export default function AutomationsRoute() {
|
|
190
192
|
const [view, setView] = useState<"dispatch" | "all">("dispatch");
|
|
193
|
+
const [query, setQuery] = useState("");
|
|
191
194
|
const [detailsTarget, setDetailsTarget] =
|
|
192
195
|
useState<DispatchAutomationItem | null>(null);
|
|
193
196
|
const automationsQuery = useAutomations();
|
|
@@ -204,6 +207,25 @@ export default function AutomationsRoute() {
|
|
|
204
207
|
() => sortAutomations(visibleAutomations),
|
|
205
208
|
[visibleAutomations],
|
|
206
209
|
);
|
|
210
|
+
const filtered = useMemo(() => {
|
|
211
|
+
const normalized = query.trim().toLowerCase();
|
|
212
|
+
if (!normalized) return ordered;
|
|
213
|
+
return ordered.filter((item) =>
|
|
214
|
+
[
|
|
215
|
+
item.name,
|
|
216
|
+
item.event,
|
|
217
|
+
item.schedule,
|
|
218
|
+
item.scheduleDescription,
|
|
219
|
+
item.body,
|
|
220
|
+
item.model,
|
|
221
|
+
item.domain,
|
|
222
|
+
]
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.join(" ")
|
|
225
|
+
.toLowerCase()
|
|
226
|
+
.includes(normalized),
|
|
227
|
+
);
|
|
228
|
+
}, [ordered, query]);
|
|
207
229
|
const enabledCount = visibleAutomations.filter((item) => item.enabled).length;
|
|
208
230
|
const errorCount = visibleAutomations.filter(
|
|
209
231
|
(item) =>
|
|
@@ -216,6 +238,18 @@ export default function AutomationsRoute() {
|
|
|
216
238
|
: null
|
|
217
239
|
: null;
|
|
218
240
|
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
if (!detailsTarget) return;
|
|
243
|
+
const current = filtered.find(
|
|
244
|
+
(item) => automationIdentity(item) === automationIdentity(detailsTarget),
|
|
245
|
+
);
|
|
246
|
+
if (current) {
|
|
247
|
+
if (current !== detailsTarget) setDetailsTarget(current);
|
|
248
|
+
} else {
|
|
249
|
+
setDetailsTarget(null);
|
|
250
|
+
}
|
|
251
|
+
}, [detailsTarget, filtered]);
|
|
252
|
+
|
|
219
253
|
return (
|
|
220
254
|
<DispatchShell
|
|
221
255
|
title="Automations"
|
|
@@ -223,12 +257,28 @@ export default function AutomationsRoute() {
|
|
|
223
257
|
>
|
|
224
258
|
<section className="flex flex-col gap-4">
|
|
225
259
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
226
|
-
<div className="flex min-w-0
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
260
|
+
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-3">
|
|
261
|
+
<div className="relative w-full max-w-sm">
|
|
262
|
+
<IconSearch
|
|
263
|
+
size={15}
|
|
264
|
+
aria-hidden="true"
|
|
265
|
+
className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground"
|
|
266
|
+
/>
|
|
267
|
+
<Input
|
|
268
|
+
value={query}
|
|
269
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
270
|
+
placeholder="Search automations"
|
|
271
|
+
aria-label="Search automations"
|
|
272
|
+
className="h-8 pl-8 text-xs"
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
276
|
+
<IconSettingsAutomation size={16} className="shrink-0" />
|
|
277
|
+
<span>
|
|
278
|
+
{enabledCount} enabled
|
|
279
|
+
{errorCount > 0 ? ` · ${errorCount} errors` : ""}
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
232
282
|
</div>
|
|
233
283
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
234
284
|
<Select
|
|
@@ -252,61 +302,75 @@ export default function AutomationsRoute() {
|
|
|
252
302
|
</div>
|
|
253
303
|
</div>
|
|
254
304
|
|
|
255
|
-
<div className="
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
<
|
|
305
|
+
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(24rem,32rem)]">
|
|
306
|
+
<div className="min-w-0 divide-y rounded-lg bg-card">
|
|
307
|
+
{automationsQuery.isLoading && ordered.length === 0 ? (
|
|
308
|
+
Array.from({ length: 4 }).map((_, index) => (
|
|
309
|
+
<div key={index} className="px-4 py-3">
|
|
310
|
+
<Skeleton className="h-4 w-40" />
|
|
311
|
+
<Skeleton className="mt-2 h-3 w-28" />
|
|
312
|
+
</div>
|
|
313
|
+
))
|
|
314
|
+
) : filtered.length > 0 ? (
|
|
315
|
+
filtered.map((item) => {
|
|
316
|
+
const status = automationStatus(item);
|
|
317
|
+
const canUpdate = item.canUpdate !== false;
|
|
318
|
+
const isToggling =
|
|
319
|
+
pendingToggleIdentity === automationIdentity(item);
|
|
320
|
+
const isSelected =
|
|
321
|
+
detailsTarget !== null &&
|
|
322
|
+
automationIdentity(detailsTarget) ===
|
|
323
|
+
automationIdentity(item);
|
|
324
|
+
return (
|
|
325
|
+
<div
|
|
326
|
+
key={item.id}
|
|
327
|
+
className={cn(
|
|
328
|
+
"grid grid-cols-[minmax(0,1fr)_auto] gap-3 px-4 py-3 transition-colors",
|
|
329
|
+
isSelected && "bg-accent/40",
|
|
330
|
+
)}
|
|
331
|
+
>
|
|
332
|
+
<div className="min-w-0">
|
|
333
|
+
<button
|
|
334
|
+
type="button"
|
|
335
|
+
className="w-full min-w-0 cursor-pointer text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card"
|
|
336
|
+
aria-pressed={isSelected}
|
|
337
|
+
onClick={() => setDetailsTarget(item)}
|
|
338
|
+
>
|
|
339
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
340
|
+
<StatusDot tone={status.tone} />
|
|
341
|
+
<span className="truncate text-sm font-medium text-foreground">
|
|
342
|
+
{item.name}
|
|
343
|
+
</span>
|
|
344
|
+
</div>
|
|
345
|
+
<div className="mt-1 truncate text-xs text-muted-foreground">
|
|
346
|
+
{automationTarget(item)}
|
|
347
|
+
</div>
|
|
348
|
+
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
|
|
349
|
+
<span>Last {automationLastRun(item)}</span>
|
|
350
|
+
<span>Next {automationNextRun(item)}</span>
|
|
351
|
+
<span>{automationScopeLabel(item)}</span>
|
|
352
|
+
{item.lastCheck ? (
|
|
353
|
+
<span>Checked {automationLastCheck(item)}</span>
|
|
354
|
+
) : null}
|
|
355
|
+
</div>
|
|
356
|
+
</button>
|
|
357
|
+
{item.lastError ? (
|
|
358
|
+
<div className="mt-1 flex flex-wrap items-baseline gap-x-2 gap-y-1 text-xs text-destructive">
|
|
359
|
+
<span className="min-w-0 break-words">
|
|
360
|
+
{item.lastError}
|
|
361
|
+
</span>
|
|
362
|
+
<Link
|
|
363
|
+
to={automationTroubleshootPath(item)}
|
|
364
|
+
className="inline-flex shrink-0 items-center gap-1 font-medium text-foreground underline-offset-4 hover:underline"
|
|
365
|
+
aria-label={`Troubleshoot ${item.name} in Thread Debug`}
|
|
366
|
+
>
|
|
367
|
+
<IconFileSearch size={13} aria-hidden="true" />
|
|
368
|
+
Troubleshoot in Thread Debug
|
|
369
|
+
</Link>
|
|
370
|
+
</div>
|
|
290
371
|
) : null}
|
|
291
372
|
</div>
|
|
292
|
-
|
|
293
|
-
<div className="mt-1 flex flex-wrap items-baseline gap-x-2 gap-y-1 text-xs text-destructive">
|
|
294
|
-
<span className="min-w-0 break-words">
|
|
295
|
-
{item.lastError}
|
|
296
|
-
</span>
|
|
297
|
-
<Link
|
|
298
|
-
to={automationTroubleshootPath(item)}
|
|
299
|
-
className="inline-flex shrink-0 items-center gap-1 font-medium text-foreground underline-offset-4 hover:underline"
|
|
300
|
-
aria-label={`Troubleshoot ${item.name} in Thread Debug`}
|
|
301
|
-
>
|
|
302
|
-
<IconFileSearch size={13} aria-hidden="true" />
|
|
303
|
-
Troubleshoot in Thread Debug
|
|
304
|
-
</Link>
|
|
305
|
-
</div>
|
|
306
|
-
) : null}
|
|
307
|
-
</div>
|
|
308
|
-
<div className="flex flex-col items-end gap-2">
|
|
309
|
-
<div className="flex items-center gap-2">
|
|
373
|
+
<div className="flex flex-col items-end gap-2">
|
|
310
374
|
<Badge
|
|
311
375
|
variant={
|
|
312
376
|
status.tone === "danger" ? "destructive" : "outline"
|
|
@@ -315,49 +379,65 @@ export default function AutomationsRoute() {
|
|
|
315
379
|
>
|
|
316
380
|
{status.label}
|
|
317
381
|
</Badge>
|
|
318
|
-
<
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
382
|
+
<Switch
|
|
383
|
+
checked={!!item.enabled}
|
|
384
|
+
disabled={!canUpdate || isToggling}
|
|
385
|
+
aria-label={`${item.enabled ? "Disable" : "Enable"} automation ${item.name}`}
|
|
386
|
+
onCheckedChange={(checked) =>
|
|
387
|
+
toggleAutomation.mutate({
|
|
388
|
+
owner: item.owner,
|
|
389
|
+
path: item.path,
|
|
390
|
+
enabled: checked,
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
/>
|
|
328
394
|
</div>
|
|
329
|
-
<Switch
|
|
330
|
-
checked={!!item.enabled}
|
|
331
|
-
disabled={!canUpdate || isToggling}
|
|
332
|
-
aria-label={`${item.enabled ? "Disable" : "Enable"} automation ${item.name}`}
|
|
333
|
-
onCheckedChange={(checked) =>
|
|
334
|
-
toggleAutomation.mutate({
|
|
335
|
-
owner: item.owner,
|
|
336
|
-
path: item.path,
|
|
337
|
-
enabled: checked,
|
|
338
|
-
})
|
|
339
|
-
}
|
|
340
|
-
/>
|
|
341
395
|
</div>
|
|
342
|
-
|
|
343
|
-
)
|
|
344
|
-
|
|
396
|
+
);
|
|
397
|
+
})
|
|
398
|
+
) : (
|
|
399
|
+
<div className="px-4 py-10 text-center text-sm text-muted-foreground">
|
|
400
|
+
{query
|
|
401
|
+
? "No matching automations."
|
|
402
|
+
: view === "all"
|
|
403
|
+
? "No automations yet. Create one here, or ask Dispatch to set up a scheduled or event-triggered job."
|
|
404
|
+
: "No Dispatch automations yet. Switch to All apps to inspect workspace automations."}
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
{detailsTarget ? (
|
|
410
|
+
<AutomationDetailsPanel
|
|
411
|
+
automation={detailsTarget}
|
|
412
|
+
isToggling={
|
|
413
|
+
pendingToggleIdentity === automationIdentity(detailsTarget)
|
|
414
|
+
}
|
|
415
|
+
onToggle={() =>
|
|
416
|
+
toggleAutomation.mutate({
|
|
417
|
+
owner: detailsTarget.owner,
|
|
418
|
+
path: detailsTarget.path,
|
|
419
|
+
enabled: !detailsTarget.enabled,
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
/>
|
|
345
423
|
) : (
|
|
346
|
-
<div className="
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
424
|
+
<div className="flex min-h-[34rem] flex-col items-center justify-center rounded-lg border border-dashed bg-card p-6 text-center">
|
|
425
|
+
<IconListDetails
|
|
426
|
+
size={24}
|
|
427
|
+
strokeWidth={1.6}
|
|
428
|
+
className="text-muted-foreground"
|
|
429
|
+
/>
|
|
430
|
+
<p className="mt-3 text-sm font-medium text-foreground">
|
|
431
|
+
Select an automation
|
|
432
|
+
</p>
|
|
433
|
+
<p className="mt-1 max-w-xs text-sm text-muted-foreground">
|
|
434
|
+
Inspect its prompt, configuration, capabilities, and run
|
|
435
|
+
history.
|
|
436
|
+
</p>
|
|
350
437
|
</div>
|
|
351
438
|
)}
|
|
352
439
|
</div>
|
|
353
440
|
</section>
|
|
354
|
-
<AutomationRunHistoryDialog
|
|
355
|
-
automation={detailsTarget}
|
|
356
|
-
open={detailsTarget !== null}
|
|
357
|
-
onOpenChange={(open) => {
|
|
358
|
-
if (!open) setDetailsTarget(null);
|
|
359
|
-
}}
|
|
360
|
-
/>
|
|
361
441
|
</DispatchShell>
|
|
362
442
|
);
|
|
363
443
|
}
|
|
@@ -8,6 +8,8 @@ import ChatRoute from "./chat";
|
|
|
8
8
|
|
|
9
9
|
const clientState = vi.hoisted(() => ({
|
|
10
10
|
surfaceProps: null as Record<string, unknown> | null,
|
|
11
|
+
activeRunId: null as string | null,
|
|
12
|
+
writeClipboardText: vi.fn(),
|
|
11
13
|
agents: [] as Array<{
|
|
12
14
|
id: string;
|
|
13
15
|
name: string;
|
|
@@ -27,6 +29,7 @@ vi.mock("@agent-native/core/client/agent-chat", () => ({
|
|
|
27
29
|
insertAgentComposerReference: vi.fn(),
|
|
28
30
|
markAgentChatHomeHandoff: vi.fn(),
|
|
29
31
|
readChatFirstMode: () => true,
|
|
32
|
+
useActiveAgentChatRunId: () => clientState.activeRunId,
|
|
30
33
|
navigateWithAgentChatViewTransition: (
|
|
31
34
|
navigate: (path: string) => void,
|
|
32
35
|
path: string,
|
|
@@ -34,6 +37,10 @@ vi.mock("@agent-native/core/client/agent-chat", () => ({
|
|
|
34
37
|
sendToAgentChat: vi.fn(),
|
|
35
38
|
}));
|
|
36
39
|
|
|
40
|
+
vi.mock("@agent-native/core/client/clipboard", () => ({
|
|
41
|
+
writeClipboardText: clientState.writeClipboardText,
|
|
42
|
+
}));
|
|
43
|
+
|
|
37
44
|
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
38
45
|
useActionQuery: () => ({
|
|
39
46
|
data: clientState.agents,
|
|
@@ -72,6 +79,7 @@ describe("Dispatch ChatRoute", () => {
|
|
|
72
79
|
vi.useFakeTimers();
|
|
73
80
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
74
81
|
clientState.surfaceProps = null;
|
|
82
|
+
clientState.activeRunId = null;
|
|
75
83
|
clientState.agents = [];
|
|
76
84
|
container = document.createElement("div");
|
|
77
85
|
document.body.appendChild(container);
|
|
@@ -180,4 +188,45 @@ describe("Dispatch ChatRoute", () => {
|
|
|
180
188
|
).getPath("thread-1"),
|
|
181
189
|
).toBe("/chat/thread-1?agent=agents%2Fresearch-partner.md");
|
|
182
190
|
});
|
|
191
|
+
|
|
192
|
+
it("exposes a copyable request ID affordance on threaded chats", async () => {
|
|
193
|
+
clientState.activeRunId = "run-456";
|
|
194
|
+
clientState.writeClipboardText.mockResolvedValue(true);
|
|
195
|
+
|
|
196
|
+
await act(async () => {
|
|
197
|
+
root.render(
|
|
198
|
+
<MemoryRouter initialEntries={["/chat/chat-123"]}>
|
|
199
|
+
<ChatRoute />
|
|
200
|
+
</MemoryRouter>,
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const button = Array.from(container.querySelectorAll("button")).find((el) =>
|
|
205
|
+
el.textContent?.includes("Copy request ID"),
|
|
206
|
+
);
|
|
207
|
+
expect(button).toBeTruthy();
|
|
208
|
+
|
|
209
|
+
await act(async () => {
|
|
210
|
+
button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
211
|
+
await Promise.resolve();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(clientState.writeClipboardText).toHaveBeenCalledWith("run-456");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("keeps the request ID affordance unavailable before a run starts", async () => {
|
|
218
|
+
await act(async () => {
|
|
219
|
+
root.render(
|
|
220
|
+
<MemoryRouter initialEntries={["/chat/chat-123"]}>
|
|
221
|
+
<ChatRoute />
|
|
222
|
+
</MemoryRouter>,
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const button = Array.from(container.querySelectorAll("button")).find((el) =>
|
|
227
|
+
el.textContent?.includes("Request ID unavailable"),
|
|
228
|
+
);
|
|
229
|
+
expect(button).toBeTruthy();
|
|
230
|
+
expect(button).toHaveProperty("disabled", true);
|
|
231
|
+
});
|
|
183
232
|
});
|
|
@@ -3,21 +3,26 @@ import {
|
|
|
3
3
|
insertAgentComposerReference,
|
|
4
4
|
markAgentChatHomeHandoff,
|
|
5
5
|
readChatFirstMode,
|
|
6
|
+
useActiveAgentChatRunId,
|
|
6
7
|
} from "@agent-native/core/client/agent-chat";
|
|
7
8
|
import { appBasePath, appPath } from "@agent-native/core/client/api-path";
|
|
9
|
+
import { writeClipboardText } from "@agent-native/core/client/clipboard";
|
|
8
10
|
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
9
11
|
import { useT } from "@agent-native/core/client/i18n";
|
|
12
|
+
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
|
10
13
|
import {
|
|
11
14
|
useCallback,
|
|
12
15
|
useEffect,
|
|
13
16
|
useMemo,
|
|
14
17
|
useRef,
|
|
18
|
+
useState,
|
|
15
19
|
type ComponentProps,
|
|
16
20
|
} from "react";
|
|
17
21
|
import { useLocation, useNavigate } from "react-router";
|
|
18
22
|
|
|
19
23
|
import { ActionQueryError } from "../../components/action-query-error";
|
|
20
24
|
import { useDispatchExtensions } from "../../components/layout/Layout";
|
|
25
|
+
import { Button } from "../../components/ui/button";
|
|
21
26
|
import { Skeleton } from "../../components/ui/skeleton";
|
|
22
27
|
import { submitOverviewPrompt } from "../../lib/overview-chat";
|
|
23
28
|
|
|
@@ -86,6 +91,46 @@ function DispatchAgentChatSurface(props: DispatchAgentChatSurfaceProps) {
|
|
|
86
91
|
return <AgentChatSurface {...props} />;
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
function DispatchRequestIdButton({ requestId }: { requestId: string | null }) {
|
|
95
|
+
const t = useT();
|
|
96
|
+
const [copied, setCopied] = useState(false);
|
|
97
|
+
|
|
98
|
+
const handleCopy = useCallback(() => {
|
|
99
|
+
if (!requestId) return;
|
|
100
|
+
void writeClipboardText(requestId).then((ok) => {
|
|
101
|
+
if (!ok) return;
|
|
102
|
+
setCopied(true);
|
|
103
|
+
window.setTimeout(() => setCopied(false), 1000);
|
|
104
|
+
});
|
|
105
|
+
}, [requestId]);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Button
|
|
109
|
+
type="button"
|
|
110
|
+
variant="outline"
|
|
111
|
+
size="sm"
|
|
112
|
+
disabled={!requestId}
|
|
113
|
+
onClick={handleCopy}
|
|
114
|
+
aria-label={t("dispatch.pages.copyRequestId", {
|
|
115
|
+
defaultValue: requestId ? "Copy request ID" : "Request ID unavailable",
|
|
116
|
+
})}
|
|
117
|
+
>
|
|
118
|
+
{copied ? (
|
|
119
|
+
<IconCheck aria-hidden="true" />
|
|
120
|
+
) : (
|
|
121
|
+
<IconCopy aria-hidden="true" />
|
|
122
|
+
)}
|
|
123
|
+
{copied
|
|
124
|
+
? t("dispatch.pages.copied", { defaultValue: "Copied" })
|
|
125
|
+
: t("dispatch.pages.copyRequestId", {
|
|
126
|
+
defaultValue: requestId
|
|
127
|
+
? "Copy request ID"
|
|
128
|
+
: "Request ID unavailable",
|
|
129
|
+
})}
|
|
130
|
+
</Button>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
89
134
|
interface DispatchChatLocationState {
|
|
90
135
|
dispatchPrompt?: {
|
|
91
136
|
id?: string | number;
|
|
@@ -109,6 +154,7 @@ export default function ChatRoute() {
|
|
|
109
154
|
const location = useLocation();
|
|
110
155
|
const navigate = useNavigate();
|
|
111
156
|
const routeThreadId = threadIdFromPath(location.pathname);
|
|
157
|
+
const activeRunId = useActiveAgentChatRunId(routeThreadId);
|
|
112
158
|
const agentPath = new URLSearchParams(location.search).get("agent");
|
|
113
159
|
const agentsQuery = useActionQuery<WorkspaceAgentResource[]>(
|
|
114
160
|
"list-workspace-resources",
|
|
@@ -275,6 +321,11 @@ export default function ChatRoute() {
|
|
|
275
321
|
|
|
276
322
|
return (
|
|
277
323
|
<div className="flex h-full min-h-0 flex-col bg-background">
|
|
324
|
+
{threadUrlSync.routeThreadId ? (
|
|
325
|
+
<div className="flex justify-end px-4 pt-4 sm:px-6">
|
|
326
|
+
<DispatchRequestIdButton requestId={activeRunId} />
|
|
327
|
+
</div>
|
|
328
|
+
) : null}
|
|
278
329
|
<DispatchAgentChatSurface
|
|
279
330
|
key={agent ? `agent-${agent.id}` : "dispatch"}
|
|
280
331
|
mode="page"
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { DispatchAutomationItem } from "../lib/automations.js";
|
|
2
|
-
interface AutomationRunHistoryDialogProps {
|
|
3
|
-
automation: DispatchAutomationItem | null;
|
|
4
|
-
open: boolean;
|
|
5
|
-
onOpenChange: (open: boolean) => void;
|
|
6
|
-
}
|
|
7
|
-
export declare function AutomationRunHistoryDialog({ automation, open, onOpenChange, }: AutomationRunHistoryDialogProps): import("node_modules/@types/react").JSX.Element;
|
|
8
|
-
export {};
|
|
9
|
-
//# sourceMappingURL=automation-run-history-dialog.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"automation-run-history-dialog.d.ts","sourceRoot":"","sources":["../../src/components/automation-run-history-dialog.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAsBjE,UAAU,+BAA+B;IACvC,UAAU,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC1C,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AAgCD,wBAAgB,0BAA0B,CAAC,EACzC,UAAU,EACV,IAAI,EACJ,YAAY,GACb,EAAE,+BAA+B,mDA4KjC"}
|