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