@agent-native/dispatch 0.30.5 → 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.
@@ -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 { AutomationRunHistoryDialog } from "../../components/automation-run-history-dialog";
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 items-center gap-2 text-sm text-muted-foreground">
227
- <IconSettingsAutomation size={16} className="shrink-0" />
228
- <span>
229
- {enabledCount} enabled
230
- {errorCount > 0 ? ` · ${errorCount} errors` : ""}
231
- </span>
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="divide-y rounded-lg bg-card">
256
- {automationsQuery.isLoading && ordered.length === 0 ? (
257
- Array.from({ length: 4 }).map((_, index) => (
258
- <div key={index} className="px-4 py-3">
259
- <Skeleton className="h-4 w-40" />
260
- <Skeleton className="mt-2 h-3 w-28" />
261
- </div>
262
- ))
263
- ) : ordered.length > 0 ? (
264
- ordered.map((item) => {
265
- const status = automationStatus(item);
266
- const canUpdate = item.canUpdate !== false;
267
- const isToggling =
268
- pendingToggleIdentity === automationIdentity(item);
269
- return (
270
- <div
271
- key={item.id}
272
- className="grid grid-cols-[minmax(0,1fr)_auto] gap-3 px-4 py-3"
273
- >
274
- <div className="min-w-0">
275
- <div className="flex min-w-0 items-center gap-2">
276
- <StatusDot tone={status.tone} />
277
- <span className="truncate text-sm font-medium text-foreground">
278
- {item.name}
279
- </span>
280
- </div>
281
- <div className="mt-1 truncate text-xs text-muted-foreground">
282
- {automationTarget(item)}
283
- </div>
284
- <div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
285
- <span>Last {automationLastRun(item)}</span>
286
- <span>Next {automationNextRun(item)}</span>
287
- <span>{automationScopeLabel(item)}</span>
288
- {item.lastCheck ? (
289
- <span>Checked {automationLastCheck(item)}</span>
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
- {item.lastError ? (
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
- <Button
319
- type="button"
320
- variant="ghost"
321
- size="sm"
322
- className="h-7 px-2 text-xs"
323
- onClick={() => setDetailsTarget(item)}
324
- >
325
- <IconListDetails size={14} />
326
- Details
327
- </Button>
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
- </div>
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="px-4 py-10 text-center text-sm text-muted-foreground">
347
- {view === "all"
348
- ? "No automations yet. Create one here, or ask Dispatch to set up a scheduled or event-triggered job."
349
- : "No Dispatch automations yet. Switch to All apps to inspect workspace automations."}
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
  }
@@ -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"}
@@ -1,63 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useActionQuery } from "@agent-native/core/client/hooks";
3
- import { IconAlertTriangle, IconClock, IconExternalLink, } from "@tabler/icons-react";
4
- import { Link } from "react-router";
5
- import { automationScopeLabel, automationTarget, } from "../lib/automation-display.js";
6
- import { ActionQueryError } from "./action-query-error.js";
7
- import { Badge } from "./ui/badge.js";
8
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "./ui/dialog.js";
9
- import { Skeleton } from "./ui/skeleton.js";
10
- function runStatusVariant(status) {
11
- if (status === "error")
12
- return "destructive";
13
- if (status === "success")
14
- return "default";
15
- return "outline";
16
- }
17
- function formatTimestamp(value) {
18
- const date = new Date(value);
19
- return Number.isNaN(date.getTime()) ? "Unknown time" : date.toLocaleString();
20
- }
21
- function formatDuration(run) {
22
- if (!run.finishedAt)
23
- return null;
24
- const seconds = Math.max(0, Math.round((run.finishedAt - run.startedAt) / 1000));
25
- return `${seconds}s`;
26
- }
27
- function runDebugPath(run) {
28
- const params = new URLSearchParams();
29
- if (run.runId)
30
- params.set("runId", run.runId);
31
- else if (run.threadId)
32
- params.set("threadId", run.threadId);
33
- else
34
- return null;
35
- return `/admin/thread-debug?${params.toString()}`;
36
- }
37
- export function AutomationRunHistoryDialog({ automation, open, onOpenChange, }) {
38
- const scope = automation?.scope === "organization" ||
39
- automation?.owner.startsWith("__organization__:") ||
40
- automation?.owner === "__shared__"
41
- ? "organization"
42
- : "personal";
43
- const runsQuery = useActionQuery("list-automation-runs", {
44
- name: automation?.name ?? "",
45
- scope,
46
- appId: automation?.appId ?? "dispatch",
47
- }, {
48
- enabled: open && Boolean(automation),
49
- staleTime: 5_000,
50
- });
51
- const runs = runsQuery.data ?? [];
52
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-h-[85vh] max-w-2xl overflow-y-auto", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: automation?.name ?? "Automation details" }), _jsx(DialogDescription, { children: automation
53
- ? `${automationTarget(automation)} · ${automationScopeLabel(automation)}`
54
- : "Execution history" })] }), automation ? (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-x-4 gap-y-3 rounded-lg border bg-muted/20 p-3 text-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: "Status" }), _jsx("div", { className: "mt-1 font-medium", children: automation.lastStatus ??
55
- (automation.enabled ? "ready" : "paused") })] }), _jsxs("div", { children: [_jsx("div", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: "Last check" }), _jsx("div", { className: "mt-1 font-medium", children: automation.lastCheck
56
- ? formatTimestamp(new Date(automation.lastCheck).getTime())
57
- : "Not recorded" })] }), _jsxs("div", { children: [_jsx("div", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: "Created by" }), _jsx("div", { className: "mt-1 break-words font-medium", children: automation.createdBy ?? "Unknown" })] }), _jsxs("div", { children: [_jsx("div", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: "Timezone" }), _jsx("div", { className: "mt-1 font-medium", children: automation.timezone ?? "Account default" })] })] }), automation.lastError ? (_jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3", children: [_jsx(IconAlertTriangle, { className: "mt-0.5 size-4 shrink-0 text-destructive" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium text-destructive", children: "Latest scheduler result" }), _jsx("p", { className: "mt-1 break-words text-sm text-muted-foreground", children: automation.lastError })] })] })) : null, _jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Past runs" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Only executions that actually started appear here. Blocked or skipped scheduler checks are shown above, not as fake runs." })] }), runs.length > 0 ? (_jsxs(Badge, { variant: "outline", children: [runs.length, " shown"] })) : null] }), _jsx("div", { className: "mt-3", children: runsQuery.isLoading ? (_jsx("div", { className: "space-y-3 rounded-lg border p-4", children: Array.from({ length: 3 }).map((_, index) => (_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-2/3" }), _jsx(Skeleton, { className: "h-3 w-1/2" })] }, index))) })) : runsQuery.isError ? (_jsx(ActionQueryError, { error: runsQuery.error, onRetry: () => void runsQuery.refetch() })) : runs.length === 0 ? (_jsx("div", { className: "rounded-lg border border-dashed p-4 text-sm text-muted-foreground", children: "This automation has not recorded an execution yet. If it is enabled but shows no runs, inspect the latest scheduler result and last check above." })) : (_jsx("ul", { className: "divide-y rounded-lg border", children: runs.map((run) => {
58
- const debugPath = runDebugPath(run);
59
- const duration = formatDuration(run);
60
- return (_jsxs("li", { className: "space-y-2 p-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [_jsx(Badge, { variant: runStatusVariant(run.status), children: run.status }), _jsx("span", { className: "text-muted-foreground", children: formatTimestamp(run.startedAt) }), duration ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(IconClock, { className: "size-3.5" }), duration] })) : null, debugPath ? (_jsxs(Link, { to: debugPath, className: "ml-auto inline-flex items-center gap-1 text-xs font-medium text-foreground underline-offset-4 hover:underline", children: [_jsx(IconExternalLink, { className: "size-3.5" }), "Open debug"] })) : null] }), run.error ? (_jsx("p", { className: "break-words text-sm text-destructive", children: run.error })) : null] }, run.id));
61
- }) })) })] })] })) : null] }) }));
62
- }
63
- //# sourceMappingURL=automation-run-history-dialog.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"automation-run-history-dialog.js","sourceRoot":"","sources":["../../src/components/automation-run-history-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useActionQuery } from \"@agent-native/core/client/hooks\";\nimport {\n IconAlertTriangle,\n IconClock,\n IconExternalLink,\n} from \"@tabler/icons-react\";\nimport { Link } from \"react-router\";\n\nimport {\n automationScopeLabel,\n automationTarget,\n} from \"../lib/automation-display\";\nimport type { DispatchAutomationItem } from \"../lib/automations\";\nimport { ActionQueryError } from \"./action-query-error\";\nimport { Badge } from \"./ui/badge\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"./ui/dialog\";\nimport { Skeleton } from \"./ui/skeleton\";\n\ninterface AutomationRun {\n id: string;\n runId: string | null;\n threadId: string | null;\n status: \"running\" | \"success\" | \"error\" | \"interrupted\";\n startedAt: number;\n finishedAt: number | null;\n error: string | null;\n}\n\ninterface AutomationRunHistoryDialogProps {\n automation: DispatchAutomationItem | null;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}\n\nfunction runStatusVariant(\n status: AutomationRun[\"status\"],\n): \"default\" | \"destructive\" | \"outline\" {\n if (status === \"error\") return \"destructive\";\n if (status === \"success\") return \"default\";\n return \"outline\";\n}\n\nfunction formatTimestamp(value: number): string {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? \"Unknown time\" : date.toLocaleString();\n}\n\nfunction formatDuration(run: AutomationRun): string | null {\n if (!run.finishedAt) return null;\n const seconds = Math.max(\n 0,\n Math.round((run.finishedAt - run.startedAt) / 1000),\n );\n return `${seconds}s`;\n}\n\nfunction runDebugPath(run: AutomationRun): string | null {\n const params = new URLSearchParams();\n if (run.runId) params.set(\"runId\", run.runId);\n else if (run.threadId) params.set(\"threadId\", run.threadId);\n else return null;\n return `/admin/thread-debug?${params.toString()}`;\n}\n\nexport function AutomationRunHistoryDialog({\n automation,\n open,\n onOpenChange,\n}: AutomationRunHistoryDialogProps) {\n const scope =\n automation?.scope === \"organization\" ||\n automation?.owner.startsWith(\"__organization__:\") ||\n automation?.owner === \"__shared__\"\n ? \"organization\"\n : \"personal\";\n const runsQuery = useActionQuery<AutomationRun[]>(\n \"list-automation-runs\",\n {\n name: automation?.name ?? \"\",\n scope,\n appId: automation?.appId ?? \"dispatch\",\n },\n {\n enabled: open && Boolean(automation),\n staleTime: 5_000,\n },\n );\n const runs = runsQuery.data ?? [];\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-h-[85vh] max-w-2xl overflow-y-auto\">\n <DialogHeader>\n <DialogTitle>{automation?.name ?? \"Automation details\"}</DialogTitle>\n <DialogDescription>\n {automation\n ? `${automationTarget(automation)} · ${automationScopeLabel(automation)}`\n : \"Execution history\"}\n </DialogDescription>\n </DialogHeader>\n\n {automation ? (\n <div className=\"space-y-4\">\n <div className=\"grid grid-cols-2 gap-x-4 gap-y-3 rounded-lg border bg-muted/20 p-3 text-sm\">\n <div>\n <div className=\"text-[11px] uppercase tracking-wide text-muted-foreground\">\n Status\n </div>\n <div className=\"mt-1 font-medium\">\n {automation.lastStatus ??\n (automation.enabled ? \"ready\" : \"paused\")}\n </div>\n </div>\n <div>\n <div className=\"text-[11px] uppercase tracking-wide text-muted-foreground\">\n Last check\n </div>\n <div className=\"mt-1 font-medium\">\n {automation.lastCheck\n ? formatTimestamp(new Date(automation.lastCheck).getTime())\n : \"Not recorded\"}\n </div>\n </div>\n <div>\n <div className=\"text-[11px] uppercase tracking-wide text-muted-foreground\">\n Created by\n </div>\n <div className=\"mt-1 break-words font-medium\">\n {automation.createdBy ?? \"Unknown\"}\n </div>\n </div>\n <div>\n <div className=\"text-[11px] uppercase tracking-wide text-muted-foreground\">\n Timezone\n </div>\n <div className=\"mt-1 font-medium\">\n {automation.timezone ?? \"Account default\"}\n </div>\n </div>\n </div>\n\n {automation.lastError ? (\n <div className=\"flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3\">\n <IconAlertTriangle className=\"mt-0.5 size-4 shrink-0 text-destructive\" />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-destructive\">\n Latest scheduler result\n </p>\n <p className=\"mt-1 break-words text-sm text-muted-foreground\">\n {automation.lastError}\n </p>\n </div>\n </div>\n ) : null}\n\n <div>\n <div className=\"flex items-center justify-between gap-3\">\n <div>\n <h3 className=\"text-sm font-semibold text-foreground\">\n Past runs\n </h3>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n Only executions that actually started appear here. Blocked\n or skipped scheduler checks are shown above, not as fake\n runs.\n </p>\n </div>\n {runs.length > 0 ? (\n <Badge variant=\"outline\">{runs.length} shown</Badge>\n ) : null}\n </div>\n\n <div className=\"mt-3\">\n {runsQuery.isLoading ? (\n <div className=\"space-y-3 rounded-lg border p-4\">\n {Array.from({ length: 3 }).map((_, index) => (\n <div key={index} className=\"space-y-2\">\n <Skeleton className=\"h-4 w-2/3\" />\n <Skeleton className=\"h-3 w-1/2\" />\n </div>\n ))}\n </div>\n ) : runsQuery.isError ? (\n <ActionQueryError\n error={runsQuery.error}\n onRetry={() => void runsQuery.refetch()}\n />\n ) : runs.length === 0 ? (\n <div className=\"rounded-lg border border-dashed p-4 text-sm text-muted-foreground\">\n This automation has not recorded an execution yet. If it is\n enabled but shows no runs, inspect the latest scheduler\n result and last check above.\n </div>\n ) : (\n <ul className=\"divide-y rounded-lg border\">\n {runs.map((run) => {\n const debugPath = runDebugPath(run);\n const duration = formatDuration(run);\n return (\n <li key={run.id} className=\"space-y-2 p-3\">\n <div className=\"flex flex-wrap items-center gap-2 text-sm\">\n <Badge variant={runStatusVariant(run.status)}>\n {run.status}\n </Badge>\n <span className=\"text-muted-foreground\">\n {formatTimestamp(run.startedAt)}\n </span>\n {duration ? (\n <span className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\">\n <IconClock className=\"size-3.5\" />\n {duration}\n </span>\n ) : null}\n {debugPath ? (\n <Link\n to={debugPath}\n className=\"ml-auto inline-flex items-center gap-1 text-xs font-medium text-foreground underline-offset-4 hover:underline\"\n >\n <IconExternalLink className=\"size-3.5\" />\n Open debug\n </Link>\n ) : null}\n </div>\n {run.error ? (\n <p className=\"break-words text-sm text-destructive\">\n {run.error}\n </p>\n ) : null}\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n </div>\n ) : null}\n </DialogContent>\n </Dialog>\n );\n}\n"]}