@agent-native/dispatch 0.23.5 → 0.24.2

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 (224) hide show
  1. package/dist/actions/list-connected-agents.d.ts +1 -0
  2. package/dist/actions/list-connected-agents.js +5 -1
  3. package/dist/actions/list-connected-agents.js.map +1 -1
  4. package/dist/actions/list-dispatch-usage-metrics.d.ts +2 -0
  5. package/dist/actions/list-dispatch-usage-metrics.js +11 -1
  6. package/dist/actions/list-dispatch-usage-metrics.js.map +1 -1
  7. package/dist/actions/provider-api-register.d.ts +12 -12
  8. package/dist/actions/remix-workspace-template.js +4 -3
  9. package/dist/actions/remix-workspace-template.js.map +1 -1
  10. package/dist/actions/scaffold-workspace-app.js +3 -2
  11. package/dist/actions/scaffold-workspace-app.js.map +1 -1
  12. package/dist/actions/start-workspace-app-creation.js +3 -2
  13. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  14. package/dist/actions/view-screen.js +11 -1
  15. package/dist/actions/view-screen.js.map +1 -1
  16. package/dist/components/admin-navigation.d.ts.map +1 -1
  17. package/dist/components/admin-navigation.js +21 -5
  18. package/dist/components/admin-navigation.js.map +1 -1
  19. package/dist/components/app-icon.js +1 -1
  20. package/dist/components/app-icon.js.map +1 -1
  21. package/dist/components/app-list-row.d.ts +1 -1
  22. package/dist/components/app-list-row.d.ts.map +1 -1
  23. package/dist/components/app-list-row.js +1 -1
  24. package/dist/components/app-list-row.js.map +1 -1
  25. package/dist/components/app-open-actions.d.ts +1 -17
  26. package/dist/components/app-open-actions.d.ts.map +1 -1
  27. package/dist/components/app-open-actions.js +1 -20
  28. package/dist/components/app-open-actions.js.map +1 -1
  29. package/dist/components/app-row-settings.d.ts +6 -0
  30. package/dist/components/app-row-settings.d.ts.map +1 -0
  31. package/dist/components/app-row-settings.js +11 -0
  32. package/dist/components/app-row-settings.js.map +1 -0
  33. package/dist/components/automation-run-history-dialog.d.ts.map +1 -1
  34. package/dist/components/automation-run-history-dialog.js +3 -2
  35. package/dist/components/automation-run-history-dialog.js.map +1 -1
  36. package/dist/components/connected-app-card.d.ts.map +1 -1
  37. package/dist/components/connected-app-card.js +3 -2
  38. package/dist/components/connected-app-card.js.map +1 -1
  39. package/dist/components/create-app-popover.js +2 -2
  40. package/dist/components/create-app-popover.js.map +1 -1
  41. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  42. package/dist/components/dispatch-control-plane.js +23 -7
  43. package/dist/components/dispatch-control-plane.js.map +1 -1
  44. package/dist/components/dispatch-shell.d.ts.map +1 -1
  45. package/dist/components/dispatch-shell.js +5 -1
  46. package/dist/components/dispatch-shell.js.map +1 -1
  47. package/dist/components/docs-link.d.ts +16 -0
  48. package/dist/components/docs-link.d.ts.map +1 -0
  49. package/dist/components/docs-link.js +34 -0
  50. package/dist/components/docs-link.js.map +1 -0
  51. package/dist/components/layout/Layout.d.ts +1 -0
  52. package/dist/components/layout/Layout.d.ts.map +1 -1
  53. package/dist/components/layout/Layout.js +43 -35
  54. package/dist/components/layout/Layout.js.map +1 -1
  55. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  56. package/dist/components/messaging-setup-panel.js +3 -2
  57. package/dist/components/messaging-setup-panel.js.map +1 -1
  58. package/dist/components/usage-alerts-panel.d.ts +11 -0
  59. package/dist/components/usage-alerts-panel.d.ts.map +1 -0
  60. package/dist/components/usage-alerts-panel.js +125 -0
  61. package/dist/components/usage-alerts-panel.js.map +1 -0
  62. package/dist/components/workspace-app-card.d.ts +3 -1
  63. package/dist/components/workspace-app-card.d.ts.map +1 -1
  64. package/dist/components/workspace-app-card.js +70 -29
  65. package/dist/components/workspace-app-card.js.map +1 -1
  66. package/dist/components/workspace-app-search.d.ts +10 -0
  67. package/dist/components/workspace-app-search.d.ts.map +1 -0
  68. package/dist/components/workspace-app-search.js +27 -0
  69. package/dist/components/workspace-app-search.js.map +1 -0
  70. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
  71. package/dist/components/workspace-resource-effective-stack.js +3 -2
  72. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  73. package/dist/components/workspace-template-card.d.ts.map +1 -1
  74. package/dist/components/workspace-template-card.js +10 -15
  75. package/dist/components/workspace-template-card.js.map +1 -1
  76. package/dist/hooks/use-navigation-state.d.ts +2 -0
  77. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  78. package/dist/hooks/use-navigation-state.js +10 -0
  79. package/dist/hooks/use-navigation-state.js.map +1 -1
  80. package/dist/lib/embed-session-recovery.d.ts +3 -0
  81. package/dist/lib/embed-session-recovery.d.ts.map +1 -0
  82. package/dist/lib/embed-session-recovery.js +18 -0
  83. package/dist/lib/embed-session-recovery.js.map +1 -0
  84. package/dist/lib/other-apps.d.ts +2 -0
  85. package/dist/lib/other-apps.d.ts.map +1 -1
  86. package/dist/lib/other-apps.js.map +1 -1
  87. package/dist/lib/workspace-app-layout.d.ts +19 -0
  88. package/dist/lib/workspace-app-layout.d.ts.map +1 -0
  89. package/dist/lib/workspace-app-layout.js +75 -0
  90. package/dist/lib/workspace-app-layout.js.map +1 -0
  91. package/dist/lib/workspace-apps.d.ts +14 -0
  92. package/dist/lib/workspace-apps.d.ts.map +1 -1
  93. package/dist/lib/workspace-apps.js +66 -0
  94. package/dist/lib/workspace-apps.js.map +1 -1
  95. package/dist/lib/workspace-apps.test.d.ts +2 -0
  96. package/dist/lib/workspace-apps.test.d.ts.map +1 -0
  97. package/dist/lib/workspace-apps.test.js +33 -0
  98. package/dist/lib/workspace-apps.test.js.map +1 -0
  99. package/dist/routes/pages/$appId.d.ts.map +1 -1
  100. package/dist/routes/pages/$appId.js +4 -2
  101. package/dist/routes/pages/$appId.js.map +1 -1
  102. package/dist/routes/pages/agents.d.ts.map +1 -1
  103. package/dist/routes/pages/agents.js +2 -3
  104. package/dist/routes/pages/agents.js.map +1 -1
  105. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  106. package/dist/routes/pages/apps.$appId.js +17 -13
  107. package/dist/routes/pages/apps.$appId.js.map +1 -1
  108. package/dist/routes/pages/apps.d.ts.map +1 -1
  109. package/dist/routes/pages/apps.js +31 -16
  110. package/dist/routes/pages/apps.js.map +1 -1
  111. package/dist/routes/pages/audit.d.ts.map +1 -1
  112. package/dist/routes/pages/audit.js +2 -1
  113. package/dist/routes/pages/audit.js.map +1 -1
  114. package/dist/routes/pages/chat.d.ts.map +1 -1
  115. package/dist/routes/pages/chat.js +7 -2
  116. package/dist/routes/pages/chat.js.map +1 -1
  117. package/dist/routes/pages/dreams.d.ts.map +1 -1
  118. package/dist/routes/pages/dreams.js +14 -18
  119. package/dist/routes/pages/dreams.js.map +1 -1
  120. package/dist/routes/pages/integrations.d.ts.map +1 -1
  121. package/dist/routes/pages/integrations.js +51 -14
  122. package/dist/routes/pages/integrations.js.map +1 -1
  123. package/dist/routes/pages/metrics.d.ts.map +1 -1
  124. package/dist/routes/pages/metrics.js +146 -17
  125. package/dist/routes/pages/metrics.js.map +1 -1
  126. package/dist/routes/pages/operations.js +1 -1
  127. package/dist/routes/pages/operations.js.map +1 -1
  128. package/dist/routes/pages/settings.d.ts.map +1 -1
  129. package/dist/routes/pages/settings.js +19 -2
  130. package/dist/routes/pages/settings.js.map +1 -1
  131. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  132. package/dist/routes/pages/thread-debug.js +5 -8
  133. package/dist/routes/pages/thread-debug.js.map +1 -1
  134. package/dist/routes/pages/vault.d.ts.map +1 -1
  135. package/dist/routes/pages/vault.js +1 -1
  136. package/dist/routes/pages/vault.js.map +1 -1
  137. package/dist/routes/pages/workspace.d.ts.map +1 -1
  138. package/dist/routes/pages/workspace.js +13 -10
  139. package/dist/routes/pages/workspace.js.map +1 -1
  140. package/dist/server/index.d.ts.map +1 -1
  141. package/dist/server/index.js +3 -0
  142. package/dist/server/index.js.map +1 -1
  143. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  144. package/dist/server/lib/app-creation-store.js +12 -52
  145. package/dist/server/lib/app-creation-store.js.map +1 -1
  146. package/dist/server/lib/usage-metrics-store.d.ts +17 -0
  147. package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
  148. package/dist/server/lib/usage-metrics-store.js +187 -30
  149. package/dist/server/lib/usage-metrics-store.js.map +1 -1
  150. package/dist/server/plugins/agent-chat.js +2 -0
  151. package/dist/server/plugins/agent-chat.js.map +1 -1
  152. package/dist/server/plugins/integrations.d.ts.map +1 -1
  153. package/dist/server/plugins/integrations.js +1 -0
  154. package/dist/server/plugins/integrations.js.map +1 -1
  155. package/dist/shared/app-roles.d.ts +9 -0
  156. package/dist/shared/app-roles.d.ts.map +1 -0
  157. package/dist/shared/app-roles.js +7 -0
  158. package/dist/shared/app-roles.js.map +1 -0
  159. package/package.json +4 -3
  160. package/src/actions/list-connected-agents.spec.ts +73 -0
  161. package/src/actions/list-connected-agents.ts +6 -2
  162. package/src/actions/list-dispatch-usage-metrics.ts +11 -1
  163. package/src/actions/remix-workspace-template.spec.ts +29 -0
  164. package/src/actions/remix-workspace-template.ts +13 -6
  165. package/src/actions/scaffold-workspace-app.ts +12 -5
  166. package/src/actions/start-workspace-app-creation.ts +8 -2
  167. package/src/actions/view-screen.ts +13 -1
  168. package/src/components/admin-navigation.tsx +57 -11
  169. package/src/components/app-icon.spec.tsx +34 -0
  170. package/src/components/app-icon.tsx +1 -1
  171. package/src/components/app-list-row.tsx +1 -2
  172. package/src/components/app-open-actions.tsx +5 -114
  173. package/src/components/app-row-settings.tsx +54 -0
  174. package/src/components/automation-run-history-dialog.tsx +8 -4
  175. package/src/components/connected-app-card.tsx +3 -2
  176. package/src/components/create-app-popover.spec.tsx +1 -1
  177. package/src/components/create-app-popover.tsx +2 -2
  178. package/src/components/dispatch-control-plane.spec.tsx +44 -3
  179. package/src/components/dispatch-control-plane.tsx +83 -27
  180. package/src/components/dispatch-shell.tsx +7 -1
  181. package/src/components/docs-link.tsx +52 -0
  182. package/src/components/layout/Layout.spec.tsx +13 -0
  183. package/src/components/layout/Layout.tsx +63 -32
  184. package/src/components/messaging-setup-panel.tsx +10 -3
  185. package/src/components/usage-alerts-panel.tsx +445 -0
  186. package/src/components/workspace-app-card.spec.tsx +130 -9
  187. package/src/components/workspace-app-card.tsx +128 -71
  188. package/src/components/workspace-app-search.tsx +96 -0
  189. package/src/components/workspace-resource-effective-stack.tsx +10 -5
  190. package/src/components/workspace-template-card.spec.tsx +53 -24
  191. package/src/components/workspace-template-card.tsx +16 -26
  192. package/src/hooks/use-navigation-state.ts +12 -0
  193. package/src/lib/embed-session-recovery.spec.ts +63 -0
  194. package/src/lib/embed-session-recovery.ts +26 -0
  195. package/src/lib/other-apps.ts +2 -0
  196. package/src/lib/workspace-app-layout.spec.ts +47 -0
  197. package/src/lib/workspace-app-layout.ts +121 -0
  198. package/src/lib/workspace-apps.test.ts +64 -0
  199. package/src/lib/workspace-apps.ts +82 -0
  200. package/src/routes/pages/$appId.tsx +3 -6
  201. package/src/routes/pages/agents.tsx +6 -3
  202. package/src/routes/pages/apps.$appId.tsx +16 -17
  203. package/src/routes/pages/apps.tsx +116 -62
  204. package/src/routes/pages/audit.tsx +28 -8
  205. package/src/routes/pages/chat.spec.tsx +10 -0
  206. package/src/routes/pages/chat.tsx +8 -0
  207. package/src/routes/pages/dreams.tsx +92 -142
  208. package/src/routes/pages/integrations.tsx +147 -68
  209. package/src/routes/pages/metrics.tsx +460 -102
  210. package/src/routes/pages/operations.tsx +2 -2
  211. package/src/routes/pages/settings.tsx +27 -10
  212. package/src/routes/pages/thread-debug.tsx +18 -36
  213. package/src/routes/pages/vault.tsx +7 -2
  214. package/src/routes/pages/workspace.tsx +92 -56
  215. package/src/server/index.ts +4 -0
  216. package/src/server/lib/app-creation-store.spec.ts +90 -43
  217. package/src/server/lib/app-creation-store.ts +15 -61
  218. package/src/server/lib/usage-metrics-store.spec.ts +117 -0
  219. package/src/server/lib/usage-metrics-store.ts +249 -30
  220. package/src/server/plugins/agent-chat.spec.ts +4 -0
  221. package/src/server/plugins/agent-chat.ts +2 -0
  222. package/src/server/plugins/integrations.ts +1 -0
  223. package/src/shared/app-roles.ts +8 -0
  224. package/src/styles/dispatch.css +9 -0
@@ -1,22 +1,39 @@
1
+ import { sendToAgentChat } from "@agent-native/core/client/agent-chat";
1
2
  import { useActionQuery } from "@agent-native/core/client/hooks";
2
3
  import { useT } from "@agent-native/core/client/i18n";
3
4
  import {
4
5
  IconActivity,
5
6
  IconAlertTriangle,
7
+ IconArrowUpRight,
6
8
  IconApps,
7
9
  IconChartBar,
8
- IconClockHour4,
9
10
  IconCoin,
10
11
  IconMessages,
11
12
  IconUsersGroup,
12
13
  } from "@tabler/icons-react";
13
14
  import { useMemo, useState, type ReactNode } from "react";
15
+ import { Link, useSearchParams } from "react-router";
16
+ import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
14
17
 
15
18
  import { DispatchShell } from "../../components/dispatch-shell";
16
19
  import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert";
17
20
  import { Badge } from "../../components/ui/badge";
18
21
  import { Button } from "../../components/ui/button";
22
+ import {
23
+ ChartContainer,
24
+ ChartTooltip,
25
+ ChartTooltipContent,
26
+ } from "../../components/ui/chart";
27
+ import {
28
+ Select,
29
+ SelectContent,
30
+ SelectGroup,
31
+ SelectItem,
32
+ SelectTrigger,
33
+ SelectValue,
34
+ } from "../../components/ui/select";
19
35
  import { Skeleton } from "../../components/ui/skeleton";
36
+ import { UsageAlertsPanel } from "../../components/usage-alerts-panel";
20
37
  import { cn } from "../../lib/utils";
21
38
 
22
39
  export function meta() {
@@ -44,6 +61,11 @@ interface UserUsageMetric extends UsageMetricBucket {
44
61
  role: string | null;
45
62
  }
46
63
 
64
+ interface UsageUserOption {
65
+ email: string;
66
+ role: string | null;
67
+ }
68
+
47
69
  interface AppAccessMetric {
48
70
  id: string;
49
71
  name: string;
@@ -78,6 +100,13 @@ interface RecentUsageMetric {
78
100
  inputTokens: number;
79
101
  outputTokens: number;
80
102
  costCents: number;
103
+ prompt: string | null;
104
+ promptSource: "thread" | "thread-preview" | "not-captured" | "unavailable";
105
+ threadId: string | null;
106
+ runId: string | null;
107
+ taskId: string | null;
108
+ sourcePlatform: string | null;
109
+ sourceId: string | null;
81
110
  }
82
111
 
83
112
  interface UsageBillingMode {
@@ -91,6 +120,9 @@ interface UsageBillingMode {
91
120
 
92
121
  interface DispatchUsageMetrics {
93
122
  billing?: UsageBillingMode;
123
+ viewScope?: "me" | "workspace";
124
+ selectedUserEmail?: string | null;
125
+ availableUsers?: UsageUserOption[];
94
126
  sinceDays: number;
95
127
  access: {
96
128
  viewerEmail: string;
@@ -187,6 +219,39 @@ function timeAgo(timestamp: number | null): string {
187
219
  return `${Math.floor(diff / 86_400_000)}d ago`;
188
220
  }
189
221
 
222
+ function formatTrendDate(value: string): string {
223
+ const date = new Date(`${value}T12:00:00`);
224
+ return date.toLocaleDateString(undefined, {
225
+ month: "short",
226
+ day: "numeric",
227
+ });
228
+ }
229
+
230
+ function completeTrendRows(rows: DailyUsageMetric[]): DailyUsageMetric[] {
231
+ if (rows.length < 2) return rows;
232
+ const byDate = new Map(rows.map((row) => [row.date, row]));
233
+ const start = new Date(`${rows[0].date}T12:00:00`);
234
+ const end = new Date(`${rows[rows.length - 1].date}T12:00:00`);
235
+ const completed: DailyUsageMetric[] = [];
236
+ for (
237
+ const cursor = start;
238
+ cursor <= end;
239
+ cursor.setDate(cursor.getDate() + 1)
240
+ ) {
241
+ const date = cursor.toISOString().slice(0, 10);
242
+ completed.push(
243
+ byDate.get(date) ?? {
244
+ date,
245
+ costCents: 0,
246
+ calls: 0,
247
+ chatCalls: 0,
248
+ activeUsers: 0,
249
+ },
250
+ );
251
+ }
252
+ return completed;
253
+ }
254
+
190
255
  function displayApp(value: string | null | undefined): string {
191
256
  const trimmed = value?.trim();
192
257
  if (!trimmed || trimmed === "unattributed") return "Unattributed";
@@ -307,6 +372,274 @@ function LoadingMetrics() {
307
372
  );
308
373
  }
309
374
 
375
+ function ScopeSelector({
376
+ value,
377
+ onChange,
378
+ }: {
379
+ value: "me" | "workspace";
380
+ onChange: (value: "me" | "workspace") => void;
381
+ }) {
382
+ return (
383
+ <div className="flex rounded-md bg-card p-0.5" aria-label="Usage scope">
384
+ {(
385
+ [
386
+ ["me", "My usage"],
387
+ ["workspace", "Workspace"],
388
+ ] as const
389
+ ).map(([scope, label]) => (
390
+ <Button
391
+ key={scope}
392
+ type="button"
393
+ variant={value === scope ? "secondary" : "ghost"}
394
+ size="sm"
395
+ className="h-7 px-3 text-xs"
396
+ aria-pressed={value === scope}
397
+ onClick={() => onChange(scope)}
398
+ >
399
+ {label}
400
+ </Button>
401
+ ))}
402
+ </div>
403
+ );
404
+ }
405
+
406
+ function UserSelector({
407
+ value,
408
+ users,
409
+ onChange,
410
+ }: {
411
+ value: string | null;
412
+ users: UsageUserOption[];
413
+ onChange: (value: string | null) => void;
414
+ }) {
415
+ return (
416
+ <Select
417
+ value={value ?? "all"}
418
+ onValueChange={(nextValue) =>
419
+ onChange(nextValue === "all" ? null : nextValue)
420
+ }
421
+ >
422
+ <SelectTrigger
423
+ className="h-8 w-[210px] text-xs"
424
+ aria-label="Filter usage by user"
425
+ >
426
+ <SelectValue placeholder="All users" />
427
+ </SelectTrigger>
428
+ <SelectContent>
429
+ <SelectGroup>
430
+ <SelectItem value="all">All users</SelectItem>
431
+ {users.map((user) => (
432
+ <SelectItem key={user.email} value={user.email}>
433
+ {user.email}
434
+ </SelectItem>
435
+ ))}
436
+ </SelectGroup>
437
+ </SelectContent>
438
+ </Select>
439
+ );
440
+ }
441
+
442
+ function UsageTrend({
443
+ rows,
444
+ billing,
445
+ }: {
446
+ rows: DailyUsageMetric[];
447
+ billing: UsageBillingMode;
448
+ }) {
449
+ const chartData = completeTrendRows(rows).map((row) => ({
450
+ ...row,
451
+ spend: displayAmountFromCostCents(row.costCents, billing),
452
+ }));
453
+
454
+ return (
455
+ <Panel
456
+ title={
457
+ billing.unit === "builder-credits"
458
+ ? "Credit usage trend"
459
+ : "Usage trend"
460
+ }
461
+ icon={<IconChartBar size={16} />}
462
+ action={
463
+ <div className="hidden items-center gap-3 text-[11px] text-muted-foreground sm:flex">
464
+ <span className="flex items-center gap-1.5">
465
+ <span className="size-2 rounded-full bg-[hsl(var(--dispatch-brand-blue))]" />
466
+ {billing.shortLabel}
467
+ </span>
468
+ <span className="flex items-center gap-1.5">
469
+ <span className="size-2 rounded-full bg-muted-foreground" />
470
+ Calls
471
+ </span>
472
+ </div>
473
+ }
474
+ >
475
+ {chartData.length === 0 ? (
476
+ <div className="flex min-h-56 items-center justify-center rounded-md border border-dashed px-4 text-sm text-muted-foreground">
477
+ No usage in this window yet.
478
+ </div>
479
+ ) : (
480
+ <ChartContainer
481
+ config={{
482
+ spend: {
483
+ label: billing.shortLabel,
484
+ color: "hsl(var(--dispatch-brand-blue))",
485
+ },
486
+ calls: {
487
+ label: "Calls",
488
+ color: "hsl(var(--muted-foreground))",
489
+ },
490
+ }}
491
+ className="h-[280px] w-full aspect-auto"
492
+ >
493
+ <AreaChart
494
+ data={chartData}
495
+ margin={{ top: 8, right: 8, left: 12, bottom: 0 }}
496
+ >
497
+ <defs>
498
+ <linearGradient id="usage-trend-fill" x1="0" y1="0" x2="0" y2="1">
499
+ <stop
500
+ offset="0%"
501
+ stopColor="var(--color-spend)"
502
+ stopOpacity={0.24}
503
+ />
504
+ <stop
505
+ offset="100%"
506
+ stopColor="var(--color-spend)"
507
+ stopOpacity={0}
508
+ />
509
+ </linearGradient>
510
+ </defs>
511
+ <CartesianGrid
512
+ vertical={false}
513
+ stroke="hsl(var(--border))"
514
+ strokeDasharray="3 3"
515
+ />
516
+ <XAxis
517
+ dataKey="date"
518
+ stroke="hsl(var(--muted-foreground))"
519
+ fontSize={11}
520
+ axisLine={false}
521
+ tickLine={false}
522
+ tickMargin={8}
523
+ minTickGap={28}
524
+ tickFormatter={formatTrendDate}
525
+ />
526
+ <YAxis
527
+ yAxisId="spend"
528
+ stroke="hsl(var(--muted-foreground))"
529
+ fontSize={11}
530
+ axisLine={false}
531
+ tickLine={false}
532
+ tickMargin={8}
533
+ width={50}
534
+ tickFormatter={(value) =>
535
+ billing.unit === "builder-credits"
536
+ ? formatCredits(Number(value))
537
+ : formatSpend(Number(value), billing)
538
+ }
539
+ />
540
+ <YAxis yAxisId="calls" orientation="right" hide />
541
+ <ChartTooltip
542
+ cursor={false}
543
+ content={
544
+ <ChartTooltipContent
545
+ labelFormatter={(value) => formatTrendDate(String(value))}
546
+ formatter={(value, name) => [
547
+ name === "spend"
548
+ ? billing.unit === "builder-credits"
549
+ ? formatCredits(Number(value))
550
+ : formatSpend(Number(value), billing)
551
+ : `${formatNumber(Number(value))} calls`,
552
+ name === "spend" ? billing.shortLabel : "Calls",
553
+ ]}
554
+ />
555
+ }
556
+ />
557
+ <Area
558
+ yAxisId="spend"
559
+ dataKey="spend"
560
+ type="monotone"
561
+ stroke="var(--color-spend)"
562
+ strokeWidth={2}
563
+ fill="url(#usage-trend-fill)"
564
+ fillOpacity={1}
565
+ dot={false}
566
+ activeDot={{ r: 4, strokeWidth: 2 }}
567
+ isAnimationActive={false}
568
+ />
569
+ <Area
570
+ yAxisId="calls"
571
+ dataKey="calls"
572
+ type="monotone"
573
+ stroke="var(--color-calls)"
574
+ strokeWidth={1.5}
575
+ fill="none"
576
+ dot={false}
577
+ activeDot={{ r: 3, strokeWidth: 2 }}
578
+ isAnimationActive={false}
579
+ />
580
+ </AreaChart>
581
+ </ChartContainer>
582
+ )}
583
+ </Panel>
584
+ );
585
+ }
586
+
587
+ function ReviewUsageButton({
588
+ metrics,
589
+ billing,
590
+ }: {
591
+ metrics: DispatchUsageMetrics;
592
+ billing: UsageBillingMode;
593
+ }) {
594
+ function reviewUsage() {
595
+ const topApps = metrics.byApp
596
+ .slice(0, 5)
597
+ .map(
598
+ (row) =>
599
+ `${displayApp(row.key)}: ${formatSpend(row.costCents, billing)} / ${row.calls} calls`,
600
+ )
601
+ .join("; ");
602
+ const topLabels = metrics.byLabel
603
+ .slice(0, 5)
604
+ .map(
605
+ (row) =>
606
+ `${row.label}: ${formatSpend(row.costCents, billing)} / ${row.calls} calls`,
607
+ )
608
+ .join("; ");
609
+ const recentPrompts = metrics.recent
610
+ .slice(0, 8)
611
+ .map((row) => {
612
+ const prompt = row.prompt
613
+ ? row.prompt.slice(0, 180)
614
+ : "prompt not captured";
615
+ return `${timeAgo(row.createdAt)} | ${displayApp(row.app)} | ${row.label} | ${prompt}`;
616
+ })
617
+ .join("\n");
618
+
619
+ sendToAgentChat({
620
+ message:
621
+ "Review this LLM usage and explain where the spend is going. Identify repeated, background, or unexpectedly expensive work, cite the strongest evidence, and suggest concrete fixes. Call out missing attribution instead of guessing.",
622
+ context: [
623
+ `Dispatch usage scope: ${metrics.viewScope === "workspace" ? "workspace" : "my account"}.`,
624
+ `Lookback: ${metrics.sinceDays} days. Viewer: ${metrics.access.viewerEmail}.`,
625
+ `Total: ${formatSpend(metrics.totals.costCents, billing)}, ${metrics.totals.calls} calls, ${metrics.totals.chatCalls} chat calls, ${formatTokens(metrics.totals.inputTokens + metrics.totals.outputTokens)} input/output tokens.`,
626
+ `Top apps: ${topApps || "none"}.`,
627
+ `Top work types: ${topLabels || "none"}.`,
628
+ `Recent prompt evidence (bounded to the latest 8 rows):\n${recentPrompts || "none"}`,
629
+ ].join("\n"),
630
+ submit: true,
631
+ openSidebar: true,
632
+ chatTarget: "local",
633
+ });
634
+ }
635
+
636
+ return (
637
+ <Button type="button" variant="outline" size="sm" onClick={reviewUsage}>
638
+ Ask agent
639
+ </Button>
640
+ );
641
+ }
642
+
310
643
  function AppSpendRows({
311
644
  rows,
312
645
  billing,
@@ -362,40 +695,6 @@ function AppSpendRows({
362
695
  );
363
696
  }
364
697
 
365
- function DailyActivity({ rows }: { rows: DailyUsageMetric[] }) {
366
- const max = Math.max(
367
- 1,
368
- rows.reduce((value, row) => Math.max(value, row.calls), 0),
369
- );
370
- if (rows.length === 0) {
371
- return (
372
- <div className="rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground">
373
- No activity in this window.
374
- </div>
375
- );
376
- }
377
- return (
378
- <div className="flex h-44 items-end gap-1">
379
- {rows.map((row) => (
380
- <div
381
- key={row.date}
382
- className="group flex min-w-0 flex-1 flex-col items-center gap-2"
383
- >
384
- <div className="relative flex h-36 w-full items-end rounded-sm bg-muted/60">
385
- <div
386
- className="w-full rounded-sm bg-foreground transition group-hover:bg-primary"
387
- style={{ height: `${Math.max(4, (row.calls / max) * 100)}%` }}
388
- />
389
- </div>
390
- <div className="hidden text-[10px] text-muted-foreground sm:block">
391
- {row.date.slice(5)}
392
- </div>
393
- </div>
394
- ))}
395
- </div>
396
- );
397
- }
398
-
399
698
  function AppAccessTable({
400
699
  rows,
401
700
  billing,
@@ -588,52 +887,49 @@ function RecentTable({
588
887
  if (rows.length === 0) {
589
888
  return (
590
889
  <div className="rounded-lg border border-dashed px-4 py-8 text-sm text-muted-foreground">
591
- No recent LLM calls.
890
+ No prompts or LLM calls in this window.
592
891
  </div>
593
892
  );
594
893
  }
595
894
  return (
596
- <div className="overflow-x-auto">
597
- <table className="w-full min-w-[760px] text-left text-xs">
598
- <thead>
599
- <tr className="border-b text-muted-foreground">
600
- <th className="px-2 py-2 font-medium">When</th>
601
- <th className="px-2 py-2 font-medium">User</th>
602
- <th className="px-2 py-2 font-medium">App</th>
603
- <th className="px-2 py-2 font-medium">Label</th>
604
- <th className="px-2 py-2 font-medium">Model</th>
605
- <th className="px-2 py-2 text-right font-medium">
606
- {billing.shortLabel}
607
- </th>
608
- </tr>
609
- </thead>
610
- <tbody>
611
- {rows.slice(0, 10).map((row) => (
612
- <tr key={row.id} className="border-b last:border-0">
613
- <td className="px-2 py-3 text-muted-foreground">
614
- {timeAgo(row.createdAt)}
615
- </td>
616
- <td className="max-w-56 px-2 py-3">
617
- <div className="truncate text-foreground">{row.ownerEmail}</div>
618
- </td>
619
- <td className="px-2 py-3 text-muted-foreground">
620
- {displayApp(row.app)}
621
- </td>
622
- <td className="px-2 py-3">
623
- <Badge variant="outline">{row.label}</Badge>
624
- </td>
625
- <td className="max-w-48 px-2 py-3">
626
- <div className="truncate text-muted-foreground">
627
- {row.model}
628
- </div>
629
- </td>
630
- <td className="px-2 py-3 text-right tabular-nums">
631
- {formatSpend(row.costCents, billing)}
632
- </td>
633
- </tr>
634
- ))}
635
- </tbody>
636
- </table>
895
+ <div className="divide-y">
896
+ {rows.slice(0, 10).map((row) => (
897
+ <article key={row.id} className="-mx-1 px-1 py-3 first:pt-0 last:pb-0">
898
+ <div className="flex items-start justify-between gap-4">
899
+ <p className="min-w-0 whitespace-pre-wrap text-sm leading-6 text-foreground">
900
+ {row.prompt ||
901
+ (row.promptSource === "unavailable"
902
+ ? "Prompt unavailable - linked thread data could not be read."
903
+ : "Prompt not captured for this call.")}
904
+ </p>
905
+ <span className="shrink-0 text-xs text-muted-foreground">
906
+ {timeAgo(row.createdAt)}
907
+ </span>
908
+ </div>
909
+ <div className="mt-2 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
910
+ <Badge variant="outline">{displayApp(row.app)}</Badge>
911
+ <Badge variant="secondary">{row.label}</Badge>
912
+ <span>{row.model}</span>
913
+ <span aria-hidden="true">·</span>
914
+ <span>{formatSpend(row.costCents, billing)}</span>
915
+ {row.ownerEmail ? (
916
+ <>
917
+ <span aria-hidden="true">·</span>
918
+ <span className="max-w-64 truncate">{row.ownerEmail}</span>
919
+ </>
920
+ ) : null}
921
+ {row.threadId ? (
922
+ <Link
923
+ to={`/admin/thread-debug?threadId=${encodeURIComponent(row.threadId)}`}
924
+ className="inline-flex items-center gap-1 rounded-md border px-2 py-0.5 font-medium text-foreground transition-colors hover:bg-muted"
925
+ >
926
+ Inspect thread
927
+ <IconArrowUpRight size={12} />
928
+ </Link>
929
+ ) : null}
930
+ </div>
931
+ </article>
932
+ ))}
637
933
  </div>
638
934
  );
639
935
  }
@@ -641,9 +937,30 @@ function RecentTable({
641
937
  export default function MetricsRoute() {
642
938
  const t = useT();
643
939
  const [sinceDays, setSinceDays] = useState(30);
940
+ const [searchParams, setSearchParams] = useSearchParams();
941
+ const scope: "me" | "workspace" =
942
+ searchParams.get("scope") === "workspace" ? "workspace" : "me";
943
+ const userEmail =
944
+ scope === "workspace" ? searchParams.get("user") || null : null;
945
+
946
+ function setScope(nextScope: "me" | "workspace") {
947
+ const next = new URLSearchParams(searchParams);
948
+ if (nextScope === "me") next.delete("scope");
949
+ else next.set("scope", nextScope);
950
+ if (nextScope === "me") next.delete("user");
951
+ setSearchParams(next, { replace: true });
952
+ }
953
+
954
+ function setUserEmail(nextUserEmail: string | null) {
955
+ const next = new URLSearchParams(searchParams);
956
+ if (nextUserEmail) next.set("user", nextUserEmail);
957
+ else next.delete("user");
958
+ setSearchParams(next, { replace: true });
959
+ }
960
+
644
961
  const { data, isLoading, error } = useActionQuery(
645
962
  "list-dispatch-usage-metrics",
646
- { sinceDays },
963
+ { sinceDays, scope, userEmail: userEmail ?? undefined },
647
964
  );
648
965
  const metrics = data as DispatchUsageMetrics | undefined;
649
966
  const billing = metrics?.billing ?? USD_BILLING;
@@ -668,12 +985,33 @@ export default function MetricsRoute() {
668
985
  >
669
986
  <div className="space-y-4">
670
987
  <div className="flex flex-wrap items-center justify-between gap-3">
671
- <div className="text-sm text-muted-foreground">
672
- {metrics?.access.scope === "organization"
673
- ? `${metrics.access.totalUsers} workspace users`
674
- : `${metrics?.access.totalUsers ?? 0} signed-in users`}
988
+ <div>
989
+ <div className="text-sm font-medium text-foreground">
990
+ {metrics?.selectedUserEmail
991
+ ? `${metrics.selectedUserEmail}'s usage`
992
+ : scope === "workspace"
993
+ ? "Workspace usage"
994
+ : "Your usage"}
995
+ </div>
996
+ <div className="mt-0.5 text-xs text-muted-foreground">
997
+ {metrics?.selectedUserEmail
998
+ ? "Filtered to this workspace member"
999
+ : scope === "workspace"
1000
+ ? `${metrics?.access.totalUsers ?? 0} users with access`
1001
+ : metrics?.access.viewerEmail || "Signed-in account"}
1002
+ </div>
1003
+ </div>
1004
+ <div className="flex flex-wrap items-center gap-2">
1005
+ <ScopeSelector value={scope} onChange={setScope} />
1006
+ {scope === "workspace" && metrics ? (
1007
+ <UserSelector
1008
+ value={metrics.selectedUserEmail ?? userEmail}
1009
+ users={metrics.availableUsers ?? []}
1010
+ onChange={setUserEmail}
1011
+ />
1012
+ ) : null}
1013
+ <RangeSelector value={sinceDays} onChange={setSinceDays} />
675
1014
  </div>
676
- <RangeSelector value={sinceDays} onChange={setSinceDays} />
677
1015
  </div>
678
1016
 
679
1017
  {error ? (
@@ -692,6 +1030,22 @@ export default function MetricsRoute() {
692
1030
 
693
1031
  {metrics ? (
694
1032
  <>
1033
+ <UsageTrend rows={metrics.daily} billing={billing} />
1034
+
1035
+ <div className="flex items-center justify-between gap-3 border-y py-3">
1036
+ <span className="text-sm text-muted-foreground">
1037
+ Review this usage with the agent
1038
+ </span>
1039
+ <ReviewUsageButton metrics={metrics} billing={billing} />
1040
+ </div>
1041
+
1042
+ <UsageAlertsPanel
1043
+ scope={scope === "workspace" ? "workspace" : "user"}
1044
+ appOptions={metrics.byApp
1045
+ .filter((row) => row.key && row.key !== "unattributed")
1046
+ .map((row) => ({ id: row.key, label: displayApp(row.key) }))}
1047
+ />
1048
+
695
1049
  <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
696
1050
  <MetricCard
697
1051
  label={billing.label}
@@ -725,21 +1079,29 @@ export default function MetricsRoute() {
725
1079
  />
726
1080
  </div>
727
1081
 
728
- <div className="grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(320px,0.65fr)]">
729
- <Panel
730
- title={
731
- billing.unit === "builder-credits"
732
- ? "Credit Spend By App"
733
- : "Spend By App"
734
- }
735
- icon={<IconChartBar size={16} />}
736
- >
737
- <AppSpendRows rows={metrics.byApp} billing={billing} />
738
- </Panel>
739
- <Panel title="Daily Activity" icon={<IconClockHour4 size={16} />}>
740
- <DailyActivity rows={metrics.daily} />
741
- </Panel>
742
- </div>
1082
+ <Panel
1083
+ title={
1084
+ billing.unit === "builder-credits"
1085
+ ? "Credit spend by app"
1086
+ : "Spend by app"
1087
+ }
1088
+ icon={<IconApps size={16} />}
1089
+ >
1090
+ <AppSpendRows rows={metrics.byApp} billing={billing} />
1091
+ </Panel>
1092
+
1093
+ <Panel
1094
+ title="Recent prompts"
1095
+ icon={<IconMessages size={16} />}
1096
+ action={
1097
+ <span className="text-xs text-muted-foreground">
1098
+ Latest {Math.min(metrics.recent.length, 10)} of{" "}
1099
+ {metrics.recent.length}
1100
+ </span>
1101
+ }
1102
+ >
1103
+ <RecentTable rows={metrics.recent} billing={billing} />
1104
+ </Panel>
743
1105
 
744
1106
  <Panel title="Access By App" icon={<IconApps size={16} />}>
745
1107
  <AppAccessTable rows={metrics.appAccess} billing={billing} />
@@ -765,10 +1127,6 @@ export default function MetricsRoute() {
765
1127
  />
766
1128
  </Panel>
767
1129
  </div>
768
-
769
- <Panel title="Recent LLM Calls" icon={<IconActivity size={16} />}>
770
- <RecentTable rows={metrics.recent} billing={billing} />
771
- </Panel>
772
1130
  </>
773
1131
  ) : null}
774
1132
  </div>
@@ -105,11 +105,11 @@ export default function OperationsRoute() {
105
105
  >
106
106
  <TabsList className="w-fit">
107
107
  <TabsTrigger value="monitoring">
108
- <IconActivity size={15} />
108
+ <IconActivity size={15} className="mr-1.5" />
109
109
  {t("dispatch.pages.monitoring")}
110
110
  </TabsTrigger>
111
111
  <TabsTrigger value="database">
112
- <IconDatabase size={15} />
112
+ <IconDatabase size={15} className="mr-1.5" />
113
113
  {t("dispatch.pages.database")}
114
114
  </TabsTrigger>
115
115
  </TabsList>