stagent 0.1.10 → 0.1.12

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 (112) hide show
  1. package/README.md +58 -27
  2. package/package.json +3 -3
  3. package/src/__tests__/e2e/blueprint.test.ts +63 -0
  4. package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
  5. package/src/__tests__/e2e/helpers.ts +286 -0
  6. package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
  7. package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
  8. package/src/__tests__/e2e/setup.ts +156 -0
  9. package/src/__tests__/e2e/single-task.test.ts +170 -0
  10. package/src/app/api/command-palette/recent/route.ts +41 -18
  11. package/src/app/api/context/batch/route.ts +44 -0
  12. package/src/app/api/permissions/presets/route.ts +80 -0
  13. package/src/app/api/playbook/status/route.ts +15 -0
  14. package/src/app/api/profiles/route.ts +23 -21
  15. package/src/app/api/settings/pricing/route.ts +15 -0
  16. package/src/app/costs/page.tsx +53 -43
  17. package/src/app/globals.css +0 -5
  18. package/src/app/playbook/[slug]/page.tsx +76 -0
  19. package/src/app/playbook/page.tsx +54 -0
  20. package/src/app/profiles/page.tsx +7 -4
  21. package/src/app/settings/page.tsx +2 -2
  22. package/src/app/tasks/page.tsx +5 -0
  23. package/src/components/costs/cost-dashboard.tsx +226 -320
  24. package/src/components/dashboard/activity-feed.tsx +6 -2
  25. package/src/components/notifications/batch-proposal-review.tsx +150 -0
  26. package/src/components/notifications/notification-item.tsx +6 -3
  27. package/src/components/notifications/pending-approval-host.tsx +57 -11
  28. package/src/components/playbook/adoption-heatmap.tsx +69 -0
  29. package/src/components/playbook/journey-card.tsx +110 -0
  30. package/src/components/playbook/playbook-action-button.tsx +22 -0
  31. package/src/components/playbook/playbook-browser.tsx +143 -0
  32. package/src/components/playbook/playbook-card.tsx +102 -0
  33. package/src/components/playbook/playbook-detail-view.tsx +223 -0
  34. package/src/components/playbook/playbook-homepage.tsx +142 -0
  35. package/src/components/playbook/playbook-toc.tsx +90 -0
  36. package/src/components/playbook/playbook-updated-badge.tsx +23 -0
  37. package/src/components/playbook/related-docs.tsx +30 -0
  38. package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
  39. package/src/components/profiles/context-proposal-review.tsx +7 -3
  40. package/src/components/profiles/learned-context-panel.tsx +116 -8
  41. package/src/components/profiles/profile-detail-view.tsx +7 -19
  42. package/src/components/profiles/profile-form-view.tsx +0 -22
  43. package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
  44. package/src/components/settings/api-key-form.tsx +5 -43
  45. package/src/components/settings/auth-config-section.tsx +10 -6
  46. package/src/components/settings/auth-status-badge.tsx +8 -0
  47. package/src/components/settings/budget-guardrails-section.tsx +403 -620
  48. package/src/components/settings/connection-test-control.tsx +63 -0
  49. package/src/components/settings/permissions-section.tsx +85 -75
  50. package/src/components/settings/permissions-sections.tsx +24 -0
  51. package/src/components/settings/presets-section.tsx +159 -0
  52. package/src/components/settings/pricing-registry-panel.tsx +164 -0
  53. package/src/components/shared/app-sidebar.tsx +2 -0
  54. package/src/components/shared/command-palette.tsx +30 -0
  55. package/src/components/shared/light-markdown.tsx +134 -0
  56. package/src/components/workflows/loop-status-view.tsx +8 -4
  57. package/src/components/workflows/workflow-status-view.tsx +16 -9
  58. package/src/lib/agents/__tests__/claude-agent.test.ts +7 -2
  59. package/src/lib/agents/__tests__/learned-context.test.ts +500 -0
  60. package/src/lib/agents/__tests__/pattern-extractor.test.ts +243 -0
  61. package/src/lib/agents/__tests__/sweep.test.ts +202 -0
  62. package/src/lib/agents/claude-agent.ts +104 -78
  63. package/src/lib/agents/learned-context.ts +32 -28
  64. package/src/lib/agents/learning-session.ts +234 -0
  65. package/src/lib/agents/pattern-extractor.ts +34 -64
  66. package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
  67. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +0 -1
  68. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +0 -1
  69. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +0 -1
  70. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +0 -1
  71. package/src/lib/agents/profiles/builtins/general/profile.yaml +0 -1
  72. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +0 -1
  73. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +0 -1
  74. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +0 -1
  75. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +0 -1
  76. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +0 -1
  77. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +0 -1
  78. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +0 -1
  79. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +0 -1
  80. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +0 -1
  81. package/src/lib/agents/profiles/registry.ts +0 -1
  82. package/src/lib/agents/profiles/sort.ts +7 -0
  83. package/src/lib/agents/profiles/types.ts +0 -1
  84. package/src/lib/agents/runtime/catalog.ts +1 -1
  85. package/src/lib/agents/runtime/claude.ts +66 -0
  86. package/src/lib/constants/settings.ts +1 -0
  87. package/src/lib/constants/task-status.ts +6 -0
  88. package/src/lib/data/seed-data/profiles.ts +0 -3
  89. package/src/lib/db/schema.ts +3 -0
  90. package/src/lib/docs/adoption.ts +105 -0
  91. package/src/lib/docs/journey-tracker.ts +21 -0
  92. package/src/lib/docs/reader.ts +102 -0
  93. package/src/lib/docs/types.ts +54 -0
  94. package/src/lib/docs/usage-stage.ts +60 -0
  95. package/src/lib/notifications/actionable.ts +18 -10
  96. package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
  97. package/src/lib/settings/budget-guardrails.ts +213 -85
  98. package/src/lib/settings/permission-presets.ts +150 -0
  99. package/src/lib/settings/runtime-setup.ts +71 -0
  100. package/src/lib/usage/__tests__/ledger.test.ts +29 -5
  101. package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
  102. package/src/lib/usage/ledger.ts +4 -2
  103. package/src/lib/usage/pricing-registry.ts +570 -0
  104. package/src/lib/usage/pricing.ts +15 -41
  105. package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
  106. package/src/lib/utils/learned-context-history.ts +150 -0
  107. package/src/lib/validators/__tests__/profile.test.ts +0 -15
  108. package/src/lib/validators/__tests__/settings.test.ts +23 -16
  109. package/src/lib/validators/profile.ts +0 -1
  110. package/src/lib/validators/settings.ts +3 -9
  111. package/src/lib/workflows/__tests__/engine.test.ts +2 -0
  112. package/src/lib/workflows/engine.ts +20 -1
@@ -2,17 +2,17 @@ import Link from "next/link";
2
2
  import {
3
3
  AlertTriangle,
4
4
  ArrowRight,
5
+ CalendarClock,
5
6
  CalendarRange,
6
7
  Coins,
7
8
  ShieldAlert,
8
9
  ShieldCheck,
9
10
  Wallet,
10
11
  } from "lucide-react";
11
- import { listRuntimeCatalog } from "@/lib/agents/runtime/catalog";
12
- import type {
13
- ProviderModelBreakdownEntry,
14
- UsageAuditEntry,
15
- } from "@/lib/usage/ledger";
12
+ import type { UsageAuditEntry, ProviderModelBreakdownEntry } from "@/lib/usage/ledger";
13
+ import type { BudgetWindowStatus } from "@/lib/settings/budget-guardrails";
14
+ import type { RuntimeSetupState } from "@/lib/settings/runtime-setup";
15
+ import type { PricingRegistrySnapshot } from "@/lib/usage/pricing-registry";
16
16
  import { Badge } from "@/components/ui/badge";
17
17
  import { Button } from "@/components/ui/button";
18
18
  import { DonutRing } from "@/components/charts/donut-ring";
@@ -29,29 +29,12 @@ import {
29
29
  } from "@/components/ui/table";
30
30
  import { EmptyState } from "@/components/shared/empty-state";
31
31
  import { CostFilters } from "@/components/costs/cost-filters";
32
-
33
- type BudgetHealth = "unlimited" | "ok" | "warning" | "blocked";
34
- type BudgetMetric = "spend" | "tokens";
35
- type BudgetWindow = "daily" | "monthly";
36
-
37
- interface BudgetStatus {
38
- id: string;
39
- scopeId: string;
40
- scopeLabel: string;
41
- runtimeId: string | null;
42
- metric: BudgetMetric;
43
- window: BudgetWindow;
44
- currentValue: number;
45
- limitValue: number | null;
46
- ratio: number | null;
47
- health: BudgetHealth;
48
- resetAtIso: string;
49
- }
32
+ import { PricingRegistryPanel } from "@/components/settings/pricing-registry-panel";
50
33
 
51
34
  interface CostSummary {
52
- todaySpendMicros: number;
53
35
  monthSpendMicros: number;
54
- todayTokens: number;
36
+ derivedDailyBudgetMicros: number;
37
+ remainingMonthlyHeadroomMicros: number;
55
38
  monthTokens: number;
56
39
  }
57
40
 
@@ -66,12 +49,6 @@ interface RuntimeBreakdownRow {
66
49
  unknownPricingRuns: number;
67
50
  }
68
51
 
69
- interface ModelVisualMeta {
70
- share: number;
71
- valueLabel: string;
72
- basisLabel: string;
73
- }
74
-
75
52
  interface TrendSeries {
76
53
  spend7: number[];
77
54
  spend30: number[];
@@ -90,17 +67,14 @@ interface CostDashboardProps {
90
67
  filters: FilterState;
91
68
  summary: CostSummary;
92
69
  trendSeries: TrendSeries;
93
- budgetStatuses: BudgetStatus[];
70
+ budgetStatuses: Array<BudgetWindowStatus & { resetAtIso: string }>;
71
+ runtimeStates: Record<string, RuntimeSetupState>;
72
+ pricing: PricingRegistrySnapshot;
94
73
  runtimeBreakdown: RuntimeBreakdownRow[];
95
74
  modelBreakdown: ProviderModelBreakdownEntry[];
96
75
  auditEntries: UsageAuditEntry[];
97
76
  }
98
77
 
99
- const runtimeCatalog = listRuntimeCatalog();
100
- const runtimeLabelMap = new Map<string, string>(
101
- runtimeCatalog.map((runtime) => [runtime.id, runtime.label])
102
- );
103
-
104
78
  function formatCurrencyMicros(value: number | null | undefined) {
105
79
  const amount = value ?? 0;
106
80
  return new Intl.NumberFormat("en-US", {
@@ -126,10 +100,6 @@ function formatPercent(value: number) {
126
100
  return `${Math.round(value)}%`;
127
101
  }
128
102
 
129
- function clampPercent(value: number) {
130
- return Math.max(0, Math.min(100, value));
131
- }
132
-
133
103
  function formatDateTime(value: string) {
134
104
  return new Date(value).toLocaleString(undefined, {
135
105
  dateStyle: "medium",
@@ -164,17 +134,15 @@ function formatActivityLabel(value: UsageAuditEntry["activityType"]) {
164
134
  return "Task assist";
165
135
  case "profile_test":
166
136
  return "Profile test";
137
+ case "pattern_extraction":
138
+ return "Pattern extraction";
139
+ case "context_summarization":
140
+ return "Context summarization";
167
141
  default:
168
- return value;
169
- }
170
- }
171
-
172
- function formatLedgerStatusLabel(value: UsageAuditEntry["status"]) {
173
- switch (value) {
174
- case "unknown_pricing":
175
- return "Unknown pricing";
176
- default:
177
- return value.charAt(0).toUpperCase() + value.slice(1);
142
+ return value
143
+ .split("_")
144
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
145
+ .join(" ");
178
146
  }
179
147
  }
180
148
 
@@ -194,44 +162,14 @@ function statusBadge(status: UsageAuditEntry["status"]) {
194
162
  </Badge>
195
163
  );
196
164
  case "unknown_pricing":
197
- return (
198
- <Badge variant="outline" className="border-border/70 text-muted-foreground">
199
- Unknown pricing
200
- </Badge>
201
- );
165
+ return <Badge variant="secondary">Pricing unavailable</Badge>;
202
166
  case "cancelled":
203
167
  return <Badge variant="secondary">Cancelled</Badge>;
204
168
  default:
205
- return <Badge variant="secondary">{formatLedgerStatusLabel(status)}</Badge>;
169
+ return <Badge variant="secondary">{status}</Badge>;
206
170
  }
207
171
  }
208
172
 
209
- function budgetBadge(status: BudgetStatus) {
210
- if (status.health === "blocked") {
211
- return <Badge variant="destructive">Blocked</Badge>;
212
- }
213
- if (status.health === "warning") {
214
- return (
215
- <Badge
216
- variant="outline"
217
- className="border-status-warning/30 bg-status-warning/10 text-status-warning"
218
- >
219
- Warning
220
- </Badge>
221
- );
222
- }
223
- if (status.health === "ok") {
224
- return <Badge variant="success">Tracked</Badge>;
225
- }
226
- return <Badge variant="secondary">Unlimited</Badge>;
227
- }
228
-
229
- function formatBudgetValue(status: BudgetStatus, value: number) {
230
- return status.metric === "spend"
231
- ? formatCurrencyMicros(value)
232
- : formatTokenCount(value);
233
- }
234
-
235
173
  function renderEntityLink(entry: UsageAuditEntry) {
236
174
  if (entry.taskId && entry.taskTitle) {
237
175
  return (
@@ -264,36 +202,6 @@ function renderEntityLink(entry: UsageAuditEntry) {
264
202
  return <span className="font-medium">{formatActivityLabel(entry.activityType)}</span>;
265
203
  }
266
204
 
267
- function resolveModelVisualMeta(
268
- row: ProviderModelBreakdownEntry,
269
- totals: { costMicros: number; totalTokens: number }
270
- ): ModelVisualMeta {
271
- if (totals.costMicros > 0 && row.costMicros > 0) {
272
- const share = clampPercent((row.costMicros / totals.costMicros) * 100);
273
- return {
274
- share,
275
- valueLabel: formatCurrencyMicros(row.costMicros),
276
- basisLabel: `${formatPercent(share)} of filtered spend`,
277
- };
278
- }
279
-
280
- if (totals.totalTokens > 0 && row.totalTokens > 0) {
281
- const share = clampPercent((row.totalTokens / totals.totalTokens) * 100);
282
- return {
283
- share,
284
- valueLabel: `${formatCompactCount(row.totalTokens)} tokens`,
285
- basisLabel: `${formatPercent(share)} of filtered tokens`,
286
- };
287
- }
288
-
289
- return {
290
- share: 0,
291
- valueLabel:
292
- row.unknownPricingRuns === row.runs ? "Pricing unavailable" : formatCurrencyMicros(0),
293
- basisLabel: "No measurable cost or token usage",
294
- };
295
- }
296
-
297
205
  function SummaryCard({
298
206
  eyebrow,
299
207
  title,
@@ -328,21 +236,30 @@ function SummaryCard({
328
236
  );
329
237
  }
330
238
 
239
+ function getStatus(
240
+ statuses: Array<BudgetWindowStatus & { resetAtIso: string }>,
241
+ scopeId: string,
242
+ window: "daily" | "monthly"
243
+ ) {
244
+ return statuses.find(
245
+ (status) => status.scopeId === scopeId && status.window === window
246
+ );
247
+ }
248
+
331
249
  export function CostDashboard({
332
250
  filters,
333
251
  summary,
334
252
  trendSeries,
335
253
  budgetStatuses,
254
+ runtimeStates,
255
+ pricing,
336
256
  runtimeBreakdown,
337
257
  modelBreakdown,
338
258
  auditEntries,
339
259
  }: CostDashboardProps) {
260
+ const configuredRuntimes = Object.values(runtimeStates).filter((runtime) => runtime.configured);
340
261
  const warnings = budgetStatuses.filter((status) => status.health === "warning");
341
262
  const blocked = budgetStatuses.filter((status) => status.health === "blocked");
342
- const configuredBudgets = budgetStatuses.filter((status) => status.limitValue != null);
343
- const nearestBudget = configuredBudgets
344
- .slice()
345
- .sort((left, right) => (right.ratio ?? 0) - (left.ratio ?? 0))[0];
346
263
  const hasUsage =
347
264
  summary.monthSpendMicros > 0 ||
348
265
  summary.monthTokens > 0 ||
@@ -352,13 +269,16 @@ export function CostDashboard({
352
269
  (total, row) => total + row.unknownPricingRuns,
353
270
  0
354
271
  );
355
- const modelTotals = modelBreakdown.reduce(
356
- (totals, row) => ({
357
- costMicros: totals.costMicros + row.costMicros,
358
- totalTokens: totals.totalTokens + row.totalTokens,
359
- }),
360
- { costMicros: 0, totalTokens: 0 }
361
- );
272
+ const activeMixLabel =
273
+ configuredRuntimes.length === 0
274
+ ? "No providers configured"
275
+ : configuredRuntimes.length === 1
276
+ ? configuredRuntimes[0]!.label
277
+ : configuredRuntimes.map((runtime) => runtime.label).join(" + ");
278
+ const dominantRuntime = runtimeBreakdown[0] ?? null;
279
+ const pacingTone =
280
+ blocked.length > 0 ? "blocked" : warnings.length > 0 ? "warning" : "healthy";
281
+ const overallMonthly = getStatus(budgetStatuses, "overall", "monthly");
362
282
 
363
283
  return (
364
284
  <div className="flex flex-col gap-6">
@@ -370,8 +290,8 @@ export function CostDashboard({
370
290
  <div className="space-y-2">
371
291
  <h1 className="text-2xl font-bold tracking-tight">Cost &amp; Usage</h1>
372
292
  <p className="max-w-2xl text-sm text-muted-foreground">
373
- Review spend, token usage, and the execution history behind each paid
374
- runtime action without leaving the operational shell.
293
+ Track current spend pacing, provider mix, and the execution history behind paid
294
+ runtime work without juggling a second budgeting model.
375
295
  </p>
376
296
  </div>
377
297
  </div>
@@ -381,151 +301,136 @@ export function CostDashboard({
381
301
  runtimeId={filters.runtimeId}
382
302
  status={filters.status}
383
303
  activityType={filters.activityType}
384
- runtimeOptions={runtimeCatalog.map((runtime) => ({
385
- id: runtime.id,
304
+ runtimeOptions={configuredRuntimes.map((runtime) => ({
305
+ id: runtime.runtimeId,
386
306
  label: runtime.label,
387
307
  }))}
388
308
  />
389
309
 
390
- {blocked.length > 0 ? (
391
- <div className="surface-card rounded-3xl border border-status-failed/25 bg-status-failed/8 p-5">
392
- <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
393
- <div className="space-y-2">
394
- <div className="flex items-center gap-2 text-status-failed">
395
- <ShieldAlert className="h-4 w-4" />
396
- <p className="text-sm font-semibold">Provider activity is currently blocked</p>
397
- </div>
398
- <p className="text-sm text-muted-foreground">
399
- One or more active budget windows have been exceeded. New paid work
400
- will remain blocked until the affected window resets.
401
- </p>
402
- </div>
403
- <div className="grid gap-2 lg:min-w-[320px]">
404
- {blocked.slice(0, 2).map((status) => (
405
- <div
406
- key={status.id}
407
- className="surface-card-muted flex items-start justify-between gap-3 rounded-2xl p-3"
408
- >
409
- <div>
410
- <p className="text-sm font-medium">
411
- {status.scopeLabel} {status.window} {status.metric}
412
- </p>
413
- <p className="text-xs text-muted-foreground">
414
- {formatBudgetValue(status, status.currentValue)} of{" "}
415
- {formatBudgetValue(status, status.limitValue ?? 0)} used
416
- </p>
417
- </div>
418
- <p className="text-right text-xs text-muted-foreground">
419
- Resets {formatDateTime(status.resetAtIso)}
420
- </p>
421
- </div>
422
- ))}
423
- </div>
424
- </div>
425
- </div>
426
- ) : null}
427
-
428
- {blocked.length === 0 && warnings.length > 0 ? (
429
- <div className="surface-card rounded-3xl border border-status-warning/25 bg-status-warning/8 p-5">
430
- <div className="flex items-start gap-3">
431
- <AlertTriangle className="mt-0.5 h-4 w-4 text-status-warning" />
432
- <div className="space-y-2">
433
- <p className="text-sm font-semibold">Budget usage is approaching a cap</p>
434
- <p className="text-sm text-muted-foreground">
435
- {warnings[0].scopeLabel} {warnings[0].window} {warnings[0].metric} is at{" "}
436
- {formatPercent((warnings[0].ratio ?? 0) * 100)} of its configured
437
- limit and resets {formatDateTime(warnings[0].resetAtIso)}.
438
- </p>
439
- </div>
440
- </div>
441
- </div>
442
- ) : null}
443
-
444
310
  <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
445
311
  <SummaryCard
446
- eyebrow="Today"
312
+ eyebrow="Month"
447
313
  title="Spend"
448
- value={formatCurrencyMicros(summary.todaySpendMicros)}
449
- detail="Current-day spend across governed runtimes"
314
+ value={formatCurrencyMicros(summary.monthSpendMicros)}
315
+ detail="Budget basis for the current month"
450
316
  icon={Wallet}
451
317
  />
452
318
  <SummaryCard
453
- eyebrow="Month"
454
- title="Spend"
455
- value={formatCurrencyMicros(summary.monthSpendMicros)}
456
- detail="Current-month spend used so far"
319
+ eyebrow="Derived"
320
+ title="Daily Budget"
321
+ value={formatCurrencyMicros(summary.derivedDailyBudgetMicros)}
322
+ detail="Calculated from the monthly cap"
457
323
  icon={CalendarRange}
458
324
  />
459
325
  <SummaryCard
460
- eyebrow="Today"
461
- title="Tokens"
462
- value={formatCompactCount(summary.todayTokens)}
463
- detail={`${formatTokenCount(summary.todayTokens)} total tokens today`}
464
- icon={Coins}
326
+ eyebrow="Remaining"
327
+ title="Monthly Headroom"
328
+ value={formatCurrencyMicros(summary.remainingMonthlyHeadroomMicros)}
329
+ detail="Spend left before the monthly cap"
330
+ icon={ShieldCheck}
465
331
  />
466
332
  <SummaryCard
467
- eyebrow="Month"
468
- title="Tokens"
469
- value={formatCompactCount(summary.monthTokens)}
470
- detail={`${formatTokenCount(summary.monthTokens)} total tokens this month`}
471
- icon={Coins}
333
+ eyebrow="Providers"
334
+ title="Active Mix"
335
+ value={configuredRuntimes.length === 0 ? "None" : String(configuredRuntimes.length)}
336
+ detail={activeMixLabel}
337
+ icon={ArrowRight}
472
338
  />
339
+ <SummaryCard
340
+ eyebrow="Pricing"
341
+ title="Freshness"
342
+ value={pricing.stale ? "Stale" : "Current"}
343
+ detail={
344
+ pricing.lastUpdatedIso
345
+ ? `Updated ${formatDateTime(pricing.lastUpdatedIso)}`
346
+ : "No refresh recorded yet"
347
+ }
348
+ icon={CalendarClock}
349
+ />
350
+ </div>
473
351
 
474
- <div className="surface-card rounded-3xl p-5">
475
- <div className="mb-4 flex items-start justify-between gap-3">
476
- <div className="space-y-1">
477
- <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
478
- Budgets
479
- </p>
480
- <h2 className="text-sm font-medium text-foreground">Guardrail state</h2>
481
- </div>
482
- <div className="surface-card-muted rounded-2xl p-2.5">
483
- {blocked.length > 0 ? (
484
- <ShieldAlert className="h-4 w-4 text-status-failed" />
352
+ <div
353
+ className={`surface-card rounded-3xl p-5 ${
354
+ pacingTone === "blocked"
355
+ ? "border border-status-failed/25 bg-status-failed/8"
356
+ : pacingTone === "warning"
357
+ ? "border border-status-warning/25 bg-status-warning/8"
358
+ : ""
359
+ }`}
360
+ >
361
+ <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
362
+ <div className="space-y-2">
363
+ <div
364
+ className={`flex items-center gap-2 ${
365
+ pacingTone === "blocked"
366
+ ? "text-status-failed"
367
+ : pacingTone === "warning"
368
+ ? "text-status-warning"
369
+ : "text-status-completed"
370
+ }`}
371
+ >
372
+ {pacingTone === "blocked" ? (
373
+ <ShieldAlert className="h-4 w-4" />
374
+ ) : pacingTone === "warning" ? (
375
+ <AlertTriangle className="h-4 w-4" />
485
376
  ) : (
486
- <ShieldCheck className="h-4 w-4 text-status-completed" />
377
+ <ShieldCheck className="h-4 w-4" />
487
378
  )}
379
+ <p className="text-sm font-semibold">
380
+ {pacingTone === "blocked"
381
+ ? "Budget pacing is blocked"
382
+ : pacingTone === "warning"
383
+ ? "Budget pacing is near a cap"
384
+ : "Budget pacing is on track"}
385
+ </p>
488
386
  </div>
387
+ <p className="text-sm text-muted-foreground">
388
+ {pacingTone === "blocked"
389
+ ? "One or more active spend windows have been exceeded. New paid work remains blocked until the affected window resets."
390
+ : pacingTone === "warning"
391
+ ? "A configured spend window is approaching its limit. Review the active provider mix before it becomes a hard stop."
392
+ : "Spend is within the configured pacing windows. Derived daily caps continue to roll forward from the monthly budget."}
393
+ </p>
489
394
  </div>
490
395
 
491
- {nearestBudget ? (
492
- <div className="space-y-3">
493
- <div className="flex items-center gap-2">
494
- {budgetBadge(nearestBudget)}
495
- <span className="text-xs text-muted-foreground">
496
- {nearestBudget.scopeLabel} {nearestBudget.window} {nearestBudget.metric}
497
- </span>
498
- </div>
499
- <div className="space-y-1">
500
- <p className="text-2xl font-bold tracking-tight">
501
- {formatBudgetValue(
502
- nearestBudget,
503
- Math.max((nearestBudget.limitValue ?? 0) - nearestBudget.currentValue, 0)
504
- )}
396
+ <div className="grid gap-2 lg:min-w-[340px]">
397
+ <div className="surface-card-muted rounded-2xl p-3">
398
+ <p className="text-sm font-medium">Primary spend driver</p>
399
+ <p className="text-xs text-muted-foreground">
400
+ {dominantRuntime
401
+ ? `${dominantRuntime.label} represents ${formatPercent(
402
+ dominantRuntime.share
403
+ )} of filtered spend.`
404
+ : "No provider has recorded spend in the current filtered window."}
405
+ </p>
406
+ </div>
407
+ {overallMonthly ? (
408
+ <div className="surface-card-muted rounded-2xl p-3">
409
+ <p className="text-sm font-medium">
410
+ {formatCurrencyMicros(overallMonthly.currentValue)} of{" "}
411
+ {overallMonthly.limitValue == null
412
+ ? "Unlimited"
413
+ : formatCurrencyMicros(overallMonthly.limitValue)}
505
414
  </p>
506
415
  <p className="text-xs text-muted-foreground">
507
- Remaining before the nearest configured cap. Resets{" "}
508
- {formatDateTime(nearestBudget.resetAtIso)}.
416
+ Monthly reset {formatDateTime(overallMonthly.resetAtIso)}.
509
417
  </p>
510
418
  </div>
511
- </div>
512
- ) : (
513
- <div className="space-y-2">
514
- <Badge variant="secondary">Unconfigured</Badge>
515
- <p className="text-sm text-muted-foreground">
516
- No spend or token caps are configured yet. Usage is being metered,
517
- but there is no automatic stop condition.
518
- </p>
519
- </div>
520
- )}
419
+ ) : null}
420
+ </div>
521
421
  </div>
522
422
  </div>
523
423
 
424
+ <PricingRegistryPanel
425
+ initialSnapshot={pricing}
426
+ showClaudePlans={runtimeStates["claude-code"]?.billingMode === "subscription"}
427
+ />
428
+
524
429
  {hasUsage ? (
525
430
  <>
526
431
  <div className="grid gap-6 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
527
432
  <div className="surface-card rounded-3xl p-5">
528
- <SectionHeading>Trend View</SectionHeading>
433
+ <SectionHeading>Spend Trends</SectionHeading>
529
434
  <div className="grid gap-4 lg:grid-cols-2">
530
435
  <div className="surface-card-muted rounded-2xl p-4">
531
436
  <div className="mb-4 flex items-center justify-between gap-3">
@@ -572,9 +477,9 @@ export function CostDashboard({
572
477
  <div className="surface-card-muted rounded-2xl p-4">
573
478
  <div className="mb-4 flex items-center justify-between gap-3">
574
479
  <div>
575
- <p className="text-sm font-medium">Token velocity</p>
480
+ <p className="text-sm font-medium">Activity tokens</p>
576
481
  <p className="text-xs text-muted-foreground">
577
- 7-day and 30-day token series
482
+ Secondary telemetry for the same window
578
483
  </p>
579
484
  </div>
580
485
  <Badge variant="outline">{formatCompactCount(summary.monthTokens)} tokens</Badge>
@@ -614,7 +519,9 @@ export function CostDashboard({
614
519
  </div>
615
520
 
616
521
  <div className="surface-card rounded-3xl p-5">
617
- <SectionHeading>Runtime Breakdown</SectionHeading>
522
+ <SectionHeading>
523
+ {runtimeBreakdown.length <= 1 ? "Active Provider" : "Provider Breakdown"}
524
+ </SectionHeading>
618
525
  <div className="space-y-3">
619
526
  {runtimeBreakdown.length > 0 ? (
620
527
  runtimeBreakdown.map((runtime) => (
@@ -623,22 +530,32 @@ export function CostDashboard({
623
530
  className="surface-card-muted flex items-center justify-between gap-4 rounded-2xl p-4"
624
531
  >
625
532
  <div className="flex items-center gap-4">
626
- <DonutRing
627
- value={runtime.share}
628
- size={44}
629
- strokeWidth={4}
630
- color="var(--chart-1)"
631
- trackColor="var(--muted)"
632
- label={`${runtime.label} share of spend`}
633
- />
533
+ {runtimeBreakdown.length > 1 ? (
534
+ <DonutRing
535
+ value={runtime.share}
536
+ size={44}
537
+ strokeWidth={4}
538
+ color="var(--chart-1)"
539
+ trackColor="var(--muted)"
540
+ label={`${runtime.label} share of spend`}
541
+ />
542
+ ) : (
543
+ <div className="rounded-2xl border border-border/60 bg-background/40 px-3 py-2 text-sm font-semibold">
544
+ {runtime.label}
545
+ </div>
546
+ )}
634
547
  <div className="space-y-1">
635
548
  <div className="flex items-center gap-2">
636
549
  <p className="text-sm font-medium">{runtime.label}</p>
637
550
  <Badge variant="outline">{runtime.providerId}</Badge>
551
+ {runtimeStates[runtime.runtimeId]?.billingMode === "subscription" ? (
552
+ <Badge variant="secondary">Plan priced</Badge>
553
+ ) : null}
638
554
  </div>
639
555
  <p className="text-xs text-muted-foreground">
640
- {formatPercent(runtime.share)} of filtered spend across{" "}
641
- {runtime.runs} runs
556
+ {runtimeBreakdown.length > 1
557
+ ? `${formatPercent(runtime.share)} of filtered spend across ${runtime.runs} runs`
558
+ : `${runtime.runs} filtered runs in the selected window`}
642
559
  </p>
643
560
  </div>
644
561
  </div>
@@ -662,9 +579,8 @@ export function CostDashboard({
662
579
  {runtime.unknownPricingRuns > 0 ? (
663
580
  <div className="col-span-2">
664
581
  <p className="text-xs text-muted-foreground">
665
- {runtime.unknownPricingRuns} run
666
- {runtime.unknownPricingRuns === 1 ? "" : "s"} missing
667
- pricing data
582
+ {runtime.unknownPricingRuns} row
583
+ {runtime.unknownPricingRuns === 1 ? "" : "s"} without price data
668
584
  </p>
669
585
  </div>
670
586
  ) : null}
@@ -673,7 +589,8 @@ export function CostDashboard({
673
589
  ))
674
590
  ) : (
675
591
  <div className="surface-card-muted rounded-2xl p-4 text-sm text-muted-foreground">
676
- No metered runtime activity exists for {formatDateRangeLabel(filters.dateRange).toLowerCase()}.
592
+ No metered provider activity exists for{" "}
593
+ {formatDateRangeLabel(filters.dateRange).toLowerCase()}.
677
594
  </div>
678
595
  )}
679
596
  </div>
@@ -685,12 +602,13 @@ export function CostDashboard({
685
602
  <div>
686
603
  <SectionHeading className="mb-2">Model Breakdown</SectionHeading>
687
604
  <p className="text-sm text-muted-foreground">
688
- Concentration by model for {formatDateRangeLabel(filters.dateRange).toLowerCase()}.
605
+ Spend-first concentration by model for{" "}
606
+ {formatDateRangeLabel(filters.dateRange).toLowerCase()}.
689
607
  </p>
690
608
  </div>
691
609
  {filteredUnknownPricingRuns > 0 ? (
692
- <Badge variant="outline">
693
- {filteredUnknownPricingRuns} unknown-pricing row
610
+ <Badge variant="secondary">
611
+ {filteredUnknownPricingRuns} pricing gap
694
612
  {filteredUnknownPricingRuns === 1 ? "" : "s"}
695
613
  </Badge>
696
614
  ) : null}
@@ -698,66 +616,49 @@ export function CostDashboard({
698
616
 
699
617
  {modelBreakdown.length > 0 ? (
700
618
  <div className="space-y-3">
701
- {modelBreakdown.map((row) => {
702
- const visual = resolveModelVisualMeta(row, modelTotals);
703
- return (
704
- <div
705
- key={`${row.runtimeId}-${row.modelId ?? "unknown"}`}
706
- className="surface-card-muted rounded-2xl p-4"
707
- >
708
- <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
709
- <div className="min-w-0 flex-1 space-y-3">
710
- <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
711
- <div className="min-w-0 space-y-1">
712
- <div className="flex flex-wrap items-center gap-2">
713
- <p className="text-sm font-medium">
714
- {row.modelId ?? "Unknown model"}
715
- </p>
716
- <Badge variant="outline">
717
- {runtimeLabelMap.get(row.runtimeId) ?? row.runtimeId}
718
- </Badge>
719
- {row.unknownPricingRuns > 0 ? (
720
- <Badge variant="outline">Pricing unavailable</Badge>
721
- ) : null}
722
- </div>
723
- <p className="text-xs text-muted-foreground">
724
- {row.providerId} • {row.runs} run
725
- {row.runs === 1 ? "" : "s"} •{" "}
726
- {formatCompactCount(row.totalTokens)} tokens
727
- </p>
728
- </div>
729
- <div className="text-left sm:text-right">
730
- <p className="text-sm font-medium">{visual.valueLabel}</p>
731
- <p className="text-xs text-muted-foreground">
732
- {visual.basisLabel}
733
- </p>
734
- </div>
735
- </div>
619
+ {modelBreakdown.map((row) => (
620
+ <div
621
+ key={`${row.runtimeId}-${row.modelId ?? "unknown"}`}
622
+ className="surface-card-muted rounded-2xl p-4"
623
+ >
624
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
625
+ <div className="min-w-0 space-y-1">
626
+ <div className="flex flex-wrap items-center gap-2">
627
+ <p className="text-sm font-medium">
628
+ {row.modelId ?? "Unknown model"}
629
+ </p>
630
+ <Badge variant="outline">
631
+ {runtimeStates[row.runtimeId]?.label ?? row.runtimeId}
632
+ </Badge>
633
+ {row.unknownPricingRuns > 0 ? (
634
+ <Badge variant="secondary">Pricing unavailable</Badge>
635
+ ) : null}
636
+ </div>
637
+ <p className="text-xs text-muted-foreground">
638
+ {row.providerId} {row.runs} run
639
+ {row.runs === 1 ? "" : "s"} • {formatCompactCount(row.totalTokens)} tokens
640
+ </p>
641
+ </div>
736
642
 
737
- <div className="space-y-2">
738
- <div className="h-2.5 overflow-hidden rounded-full bg-background/70">
739
- <div
740
- className="h-full rounded-full bg-[linear-gradient(90deg,var(--chart-1),var(--chart-2))]"
741
- style={{ width: `${Math.max(visual.share, visual.share > 0 ? 6 : 0)}%` }}
742
- />
743
- </div>
744
- <div className="flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
745
- <span>{formatPercent(visual.share)} of current filtered volume</span>
746
- {row.unknownPricingRuns > 0 ? (
747
- <span>
748
- {row.unknownPricingRuns} run
749
- {row.unknownPricingRuns === 1 ? "" : "s"} without price data
750
- </span>
751
- ) : (
752
- <span>Cost and token totals are both shown above</span>
753
- )}
754
- </div>
755
- </div>
643
+ <div className="grid gap-2 text-left sm:min-w-[180px] sm:text-right">
644
+ <div>
645
+ <p className="text-xs uppercase tracking-wide text-muted-foreground">
646
+ Spend
647
+ </p>
648
+ <p className="font-medium">{formatCurrencyMicros(row.costMicros)}</p>
649
+ </div>
650
+ <div>
651
+ <p className="text-xs uppercase tracking-wide text-muted-foreground">
652
+ Tokens
653
+ </p>
654
+ <p className="text-sm text-muted-foreground">
655
+ {formatCompactCount(row.totalTokens)}
656
+ </p>
756
657
  </div>
757
658
  </div>
758
659
  </div>
759
- );
760
- })}
660
+ </div>
661
+ ))}
761
662
  </div>
762
663
  ) : (
763
664
  <div className="surface-card-muted rounded-2xl p-4 text-sm text-muted-foreground">
@@ -786,8 +687,8 @@ export function CostDashboard({
786
687
  <TableHead>Activity</TableHead>
787
688
  <TableHead>Linked entity</TableHead>
788
689
  <TableHead>Runtime</TableHead>
789
- <TableHead>Tokens</TableHead>
790
690
  <TableHead>Cost</TableHead>
691
+ <TableHead>Tokens</TableHead>
791
692
  <TableHead>Status</TableHead>
792
693
  </TableRow>
793
694
  </TableHeader>
@@ -825,19 +726,24 @@ export function CostDashboard({
825
726
  <TableCell className="align-top">
826
727
  <div className="space-y-1">
827
728
  <p className="font-medium">
828
- {runtimeLabelMap.get(entry.runtimeId) ?? entry.runtimeId}
729
+ {runtimeStates[entry.runtimeId]?.label ?? entry.runtimeId}
730
+ </p>
731
+ <p className="text-xs text-muted-foreground">
732
+ {entry.providerId}
733
+ {runtimeStates[entry.runtimeId]?.billingMode === "subscription"
734
+ ? " • plan priced"
735
+ : ""}
829
736
  </p>
830
- <p className="text-xs text-muted-foreground">{entry.providerId}</p>
831
737
  </div>
832
738
  </TableCell>
833
- <TableCell className="align-top text-right">
834
- {formatCompactCount(entry.totalTokens ?? 0)}
835
- </TableCell>
836
739
  <TableCell className="align-top text-right">
837
740
  {entry.status === "unknown_pricing"
838
741
  ? "Unavailable"
839
742
  : formatCurrencyMicros(entry.costMicros)}
840
743
  </TableCell>
744
+ <TableCell className="align-top text-right text-muted-foreground">
745
+ {formatCompactCount(entry.totalTokens ?? 0)}
746
+ </TableCell>
841
747
  <TableCell className="align-top">{statusBadge(entry.status)}</TableCell>
842
748
  </TableRow>
843
749
  ))}