@agent-native/dispatch 0.21.0 → 0.22.1

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.
Files changed (56) hide show
  1. package/dist/actions/get-agent-thread-debug.d.ts +2 -0
  2. package/dist/actions/provider-api-register.d.ts +8 -8
  3. package/dist/actions/search-agent-threads.js +2 -2
  4. package/dist/actions/search-agent-threads.js.map +1 -1
  5. package/dist/components/layout/HeaderActions.d.ts +1 -17
  6. package/dist/components/layout/HeaderActions.d.ts.map +1 -1
  7. package/dist/components/layout/HeaderActions.js +1 -51
  8. package/dist/components/layout/HeaderActions.js.map +1 -1
  9. package/dist/components/layout/Layout.d.ts.map +1 -1
  10. package/dist/components/layout/Layout.js +22 -14
  11. package/dist/components/layout/Layout.js.map +1 -1
  12. package/dist/components/ui/dialog.d.ts +1 -19
  13. package/dist/components/ui/dialog.d.ts.map +1 -1
  14. package/dist/components/ui/dialog.js +1 -22
  15. package/dist/components/ui/dialog.js.map +1 -1
  16. package/dist/components/ui/dropdown-menu.d.ts +1 -27
  17. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  18. package/dist/components/ui/dropdown-menu.js +1 -35
  19. package/dist/components/ui/dropdown-menu.js.map +1 -1
  20. package/dist/components/ui/popover.d.ts +2 -1
  21. package/dist/components/ui/popover.d.ts.map +1 -1
  22. package/dist/components/ui/popover.js +2 -1
  23. package/dist/components/ui/popover.js.map +1 -1
  24. package/dist/components/ui/select.d.ts +1 -13
  25. package/dist/components/ui/select.d.ts.map +1 -1
  26. package/dist/components/ui/select.js +1 -26
  27. package/dist/components/ui/select.js.map +1 -1
  28. package/dist/hooks/use-mobile.d.ts +1 -1
  29. package/dist/hooks/use-mobile.d.ts.map +1 -1
  30. package/dist/hooks/use-mobile.js +1 -15
  31. package/dist/hooks/use-mobile.js.map +1 -1
  32. package/dist/lib/automation-display.js +1 -1
  33. package/dist/lib/automation-display.js.map +1 -1
  34. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  35. package/dist/routes/pages/thread-debug.js +288 -118
  36. package/dist/routes/pages/thread-debug.js.map +1 -1
  37. package/dist/server/lib/thread-debug-store.d.ts +2 -0
  38. package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
  39. package/dist/server/lib/thread-debug-store.js +6 -2
  40. package/dist/server/lib/thread-debug-store.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/actions/search-agent-threads.ts +2 -2
  43. package/src/components/layout/HeaderActions.tsx +1 -84
  44. package/src/components/layout/Layout.spec.tsx +94 -10
  45. package/src/components/layout/Layout.tsx +107 -101
  46. package/src/components/ui/dialog.tsx +1 -120
  47. package/src/components/ui/dropdown-menu.tsx +1 -198
  48. package/src/components/ui/popover.tsx +3 -1
  49. package/src/components/ui/select.tsx +1 -158
  50. package/src/hooks/use-mobile.tsx +1 -21
  51. package/src/lib/automation-display.spec.ts +1 -1
  52. package/src/lib/automation-display.ts +1 -1
  53. package/src/routes/pages/thread-debug.spec.tsx +103 -5
  54. package/src/routes/pages/thread-debug.tsx +1051 -451
  55. package/src/server/lib/thread-debug-store.spec.ts +17 -0
  56. package/src/server/lib/thread-debug-store.ts +9 -2
@@ -1,158 +1 @@
1
- import * as SelectPrimitive from "@radix-ui/react-select";
2
- import { IconCheck, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
3
- import * as React from "react";
4
-
5
- import { cn } from "../../lib/utils";
6
-
7
- const Select = SelectPrimitive.Root;
8
-
9
- const SelectGroup = SelectPrimitive.Group;
10
-
11
- const SelectValue = SelectPrimitive.Value;
12
-
13
- const SelectTrigger = React.forwardRef<
14
- React.ElementRef<typeof SelectPrimitive.Trigger>,
15
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
16
- >(({ className, children, ...props }, ref) => (
17
- <SelectPrimitive.Trigger
18
- ref={ref}
19
- className={cn(
20
- "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
21
- className,
22
- )}
23
- {...props}
24
- >
25
- {children}
26
- <SelectPrimitive.Icon asChild>
27
- <IconChevronDown className="h-4 w-4 opacity-50" />
28
- </SelectPrimitive.Icon>
29
- </SelectPrimitive.Trigger>
30
- ));
31
- SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
32
-
33
- const SelectScrollUpButton = React.forwardRef<
34
- React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
35
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
36
- >(({ className, ...props }, ref) => (
37
- <SelectPrimitive.ScrollUpButton
38
- ref={ref}
39
- className={cn(
40
- "flex cursor-default items-center justify-center py-1",
41
- className,
42
- )}
43
- {...props}
44
- >
45
- <IconChevronUp className="h-4 w-4" />
46
- </SelectPrimitive.ScrollUpButton>
47
- ));
48
- SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
49
-
50
- const SelectScrollDownButton = React.forwardRef<
51
- React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
52
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
53
- >(({ className, ...props }, ref) => (
54
- <SelectPrimitive.ScrollDownButton
55
- ref={ref}
56
- className={cn(
57
- "flex cursor-default items-center justify-center py-1",
58
- className,
59
- )}
60
- {...props}
61
- >
62
- <IconChevronDown className="h-4 w-4" />
63
- </SelectPrimitive.ScrollDownButton>
64
- ));
65
- SelectScrollDownButton.displayName =
66
- SelectPrimitive.ScrollDownButton.displayName;
67
-
68
- const SelectContent = React.forwardRef<
69
- React.ElementRef<typeof SelectPrimitive.Content>,
70
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
71
- >(({ className, children, position = "popper", ...props }, ref) => (
72
- <SelectPrimitive.Portal>
73
- <SelectPrimitive.Content
74
- ref={ref}
75
- className={cn(
76
- "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
77
- position === "popper" &&
78
- "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
79
- className,
80
- )}
81
- position={position}
82
- {...props}
83
- >
84
- <SelectScrollUpButton />
85
- <SelectPrimitive.Viewport
86
- className={cn(
87
- "p-1",
88
- position === "popper" &&
89
- "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
90
- )}
91
- >
92
- {children}
93
- </SelectPrimitive.Viewport>
94
- <SelectScrollDownButton />
95
- </SelectPrimitive.Content>
96
- </SelectPrimitive.Portal>
97
- ));
98
- SelectContent.displayName = SelectPrimitive.Content.displayName;
99
-
100
- const SelectLabel = React.forwardRef<
101
- React.ElementRef<typeof SelectPrimitive.Label>,
102
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
103
- >(({ className, ...props }, ref) => (
104
- <SelectPrimitive.Label
105
- ref={ref}
106
- className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
107
- {...props}
108
- />
109
- ));
110
- SelectLabel.displayName = SelectPrimitive.Label.displayName;
111
-
112
- const SelectItem = React.forwardRef<
113
- React.ElementRef<typeof SelectPrimitive.Item>,
114
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
115
- >(({ className, children, ...props }, ref) => (
116
- <SelectPrimitive.Item
117
- ref={ref}
118
- className={cn(
119
- "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
120
- className,
121
- )}
122
- {...props}
123
- >
124
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
125
- <SelectPrimitive.ItemIndicator>
126
- <IconCheck className="h-4 w-4" />
127
- </SelectPrimitive.ItemIndicator>
128
- </span>
129
-
130
- <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
131
- </SelectPrimitive.Item>
132
- ));
133
- SelectItem.displayName = SelectPrimitive.Item.displayName;
134
-
135
- const SelectSeparator = React.forwardRef<
136
- React.ElementRef<typeof SelectPrimitive.Separator>,
137
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
138
- >(({ className, ...props }, ref) => (
139
- <SelectPrimitive.Separator
140
- ref={ref}
141
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
142
- {...props}
143
- />
144
- ));
145
- SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
146
-
147
- export {
148
- Select,
149
- SelectGroup,
150
- SelectValue,
151
- SelectTrigger,
152
- SelectContent,
153
- SelectLabel,
154
- SelectItem,
155
- SelectSeparator,
156
- SelectScrollUpButton,
157
- SelectScrollDownButton,
158
- };
1
+ export * from "@agent-native/toolkit/ui/select";
@@ -1,21 +1 @@
1
- import * as React from "react";
2
-
3
- const MOBILE_BREAKPOINT = 768;
4
-
5
- export function useIsMobile() {
6
- const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
7
- undefined,
8
- );
9
-
10
- React.useEffect(() => {
11
- const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12
- const onChange = () => {
13
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
14
- };
15
- mql.addEventListener("change", onChange);
16
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
17
- return () => mql.removeEventListener("change", onChange);
18
- }, []);
19
-
20
- return !!isMobile;
21
- }
1
+ export * from "@agent-native/toolkit/hooks";
@@ -32,7 +32,7 @@ describe("automation-display", () => {
32
32
  it("prepares a thread-debug search for an automation", () => {
33
33
  expect(
34
34
  automationTroubleshootPath({ name: "coach onboarding reminder" }),
35
- ).toBe("/admin/thread-debug?query=coach+onboarding+reminder");
35
+ ).toBe("/admin/thread-debug?mode=threads&query=coach+onboarding+reminder");
36
36
  });
37
37
 
38
38
  it("keeps Dispatch-owned and legacy automations in the default view", () => {
@@ -27,7 +27,7 @@ export function belongsToDispatch(
27
27
  export function automationTroubleshootPath(
28
28
  item: Pick<DispatchAutomationItem, "name">,
29
29
  ): string {
30
- const params = new URLSearchParams({ query: item.name });
30
+ const params = new URLSearchParams({ mode: "threads", query: item.name });
31
31
  return `/admin/thread-debug?${params.toString()}`;
32
32
  }
33
33
 
@@ -27,13 +27,17 @@ const failedRun = {
27
27
  threadTitle: "Mail agent failed",
28
28
  threadPreview: "Provider timed out",
29
29
  status: "errored",
30
- errorCode: "provider_timeout",
31
- errorDetail: "The provider did not respond.",
32
- terminalReason: "provider_timeout",
30
+ errorCode: "stale_run",
31
+ errorDetail:
32
+ "The run heartbeat stopped while the run was still marked running.",
33
+ terminalReason: "stale_run",
33
34
  abortReason: null,
34
35
  dispatchMode: "background-processing",
35
36
  diagStage: null,
36
37
  workerStage: "model",
38
+ heartbeatAt: 1_722_400_029_000,
39
+ lastProgressAt: 1_722_400_028_000,
40
+ regime: "scheduled",
37
41
  startedAt: 1_722_400_000_000,
38
42
  completedAt: 1_722_400_030_000,
39
43
  durationMs: 30_000,
@@ -67,7 +71,25 @@ const detail = {
67
71
  queuedMessages: [],
68
72
  threadData: {},
69
73
  rawThreadData: "{}",
70
- runs: [],
74
+ runs: [
75
+ {
76
+ ...failedRun,
77
+ events: [
78
+ {
79
+ seq: 1,
80
+ event: { type: "tool_start", tool: "search-mail" },
81
+ rawEventData: '{"type":"tool_start","tool":"search-mail"}',
82
+ },
83
+ {
84
+ seq: 2,
85
+ event: { type: "error", errorCode: "stale_run" },
86
+ rawEventData: '{"type":"error","errorCode":"stale_run"}',
87
+ },
88
+ ],
89
+ completedAt: null,
90
+ durationMs: null,
91
+ },
92
+ ],
71
93
  traces: { summaries: [], spans: [] },
72
94
  feedback: [],
73
95
  satisfaction: [],
@@ -75,6 +97,17 @@ const detail = {
75
97
  checkpoints: [],
76
98
  };
77
99
 
100
+ const threadResult = {
101
+ id: "thread-chat-1",
102
+ ownerEmail: "ops@example.com",
103
+ title: "Job: builder-agent-refresh — 8/9/2026",
104
+ preview: "thread-chat-1",
105
+ messageCount: 3,
106
+ createdAt: 1_722_400_000_000,
107
+ updatedAt: 1_722_400_030_000,
108
+ snippet: "{}",
109
+ };
110
+
78
111
  vi.mock("@agent-native/core/client/hooks", () => ({
79
112
  useActionQuery: (
80
113
  name: string,
@@ -158,6 +191,25 @@ vi.mock("@agent-native/core/client/hooks", () => ({
158
191
  if (name === "get-agent-thread-debug") {
159
192
  return { ...base, data: enabled ? detail : undefined };
160
193
  }
194
+ if (name === "search-agent-threads") {
195
+ return {
196
+ ...base,
197
+ data: enabled
198
+ ? {
199
+ count: 1,
200
+ threads: [threadResult],
201
+ access: {
202
+ scope: "current organization",
203
+ canInspectAll: true,
204
+ },
205
+ source: {
206
+ id: "current",
207
+ label: "Current Dispatch DB",
208
+ },
209
+ }
210
+ : undefined,
211
+ };
212
+ }
161
213
  return { ...base, data: undefined };
162
214
  },
163
215
  }));
@@ -219,10 +271,13 @@ describe("ThreadDebugRoute", () => {
219
271
  ).toMatchObject({
220
272
  sourceId: "all",
221
273
  status: "all",
274
+ regime: "all",
222
275
  lookbackHours: 24,
223
276
  });
224
277
  expect(container.textContent).toContain("Partial results");
225
278
  expect(container.textContent).toContain("Clips (unavailable)");
279
+ expect(container.textContent).not.toContain(failedRun.id);
280
+ expect(container.textContent).not.toContain(failedRun.threadId);
226
281
 
227
282
  const failureButton = [...container.querySelectorAll("button")].find(
228
283
  (button) => button.textContent?.includes("Inspect failed run"),
@@ -243,6 +298,8 @@ describe("ThreadDebugRoute", () => {
243
298
  sourceId: "mail",
244
299
  runId: failedRun.id,
245
300
  });
301
+ expect(container.textContent).toContain("Worker stopped reporting");
302
+ expect(container.textContent).toContain("retained 2 execution events");
246
303
  });
247
304
 
248
305
  it("passes valid URL-backed failure filters to the action", async () => {
@@ -250,7 +307,7 @@ describe("ThreadDebugRoute", () => {
250
307
  root.render(
251
308
  <MemoryRouter
252
309
  initialEntries={[
253
- "/thread-debug?source=mail&owner=ops%40example.com&status=errored&range=7d",
310
+ "/thread-debug?source=mail&owner=ops%40example.com&status=errored&regime=scheduled&range=7d",
254
311
  ]}
255
312
  >
256
313
  <ThreadDebugRoute />
@@ -266,7 +323,48 @@ describe("ThreadDebugRoute", () => {
266
323
  sourceId: "mail",
267
324
  ownerEmail: "ops@example.com",
268
325
  status: "errored",
326
+ regime: "scheduled",
269
327
  lookbackHours: 168,
270
328
  });
271
329
  });
330
+
331
+ it("keeps thread list rows human-readable without technical identifiers", async () => {
332
+ await act(async () => {
333
+ root.render(
334
+ <MemoryRouter initialEntries={["/thread-debug?mode=threads"]}>
335
+ <ThreadDebugRoute />
336
+ </MemoryRouter>,
337
+ );
338
+ });
339
+
340
+ expect(container.textContent).toContain("Builder Agent Refresh");
341
+ expect(container.textContent).toContain("3 messages");
342
+ expect(container.textContent).toContain("ops@example.com");
343
+ expect(container.textContent).toContain("Advanced");
344
+ expect(container.textContent).not.toContain("Owner email");
345
+ expect(container.textContent).not.toContain("Find a thread");
346
+ expect(container.textContent).not.toContain("Search by title");
347
+ expect(container.textContent).not.toContain(threadResult.id);
348
+ expect(container.textContent).not.toContain("{}");
349
+ expect(container.querySelector("summary svg")).toBeTruthy();
350
+ const searchCall = queryState.calls.find(
351
+ (call) => call.name === "search-agent-threads" && call.enabled,
352
+ );
353
+ expect(searchCall?.params).toMatchObject({
354
+ sourceId: "current",
355
+ limit: 25,
356
+ });
357
+ expect(searchCall?.params).not.toHaveProperty("ownerEmail");
358
+
359
+ const searchInput = container.querySelector(
360
+ 'input[aria-label="Search threads or email"]',
361
+ );
362
+ expect(searchInput).toBeTruthy();
363
+ await act(async () => {
364
+ (searchInput as HTMLInputElement).focus();
365
+ });
366
+ expect(document.body.querySelector('[role="option"]')?.textContent).toBe(
367
+ "ops@example.com",
368
+ );
369
+ });
272
370
  });