stagent 0.1.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 (333) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +399 -0
  3. package/components.json +21 -0
  4. package/dist/cli.js +171 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.mjs +15 -0
  7. package/package.json +114 -0
  8. package/postcss.config.mjs +8 -0
  9. package/public/icon-512.png +0 -0
  10. package/public/icon.svg +13 -0
  11. package/public/readme/home-workspace.png +0 -0
  12. package/public/readme/inbox-approvals.png +0 -0
  13. package/public/readme/workflow-blueprints.png +0 -0
  14. package/public/stagent-s-128.png +0 -0
  15. package/public/stagent-s-64.png +0 -0
  16. package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
  17. package/src/app/api/blueprints/[id]/route.ts +39 -0
  18. package/src/app/api/blueprints/import/route.ts +68 -0
  19. package/src/app/api/blueprints/route.ts +29 -0
  20. package/src/app/api/command-palette/recent/route.ts +31 -0
  21. package/src/app/api/data/clear/route.ts +22 -0
  22. package/src/app/api/data/seed/route.ts +22 -0
  23. package/src/app/api/documents/[id]/file/route.ts +44 -0
  24. package/src/app/api/documents/[id]/route.ts +123 -0
  25. package/src/app/api/documents/route.ts +59 -0
  26. package/src/app/api/logs/stream/route.ts +101 -0
  27. package/src/app/api/notifications/[id]/route.ts +36 -0
  28. package/src/app/api/notifications/mark-all-read/route.ts +13 -0
  29. package/src/app/api/notifications/pending-approvals/route.ts +10 -0
  30. package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
  31. package/src/app/api/notifications/route.ts +34 -0
  32. package/src/app/api/permissions/route.ts +46 -0
  33. package/src/app/api/profiles/[id]/route.ts +79 -0
  34. package/src/app/api/profiles/[id]/test/route.ts +42 -0
  35. package/src/app/api/profiles/import/route.ts +108 -0
  36. package/src/app/api/profiles/route.ts +50 -0
  37. package/src/app/api/projects/[id]/route.ts +72 -0
  38. package/src/app/api/projects/route.ts +53 -0
  39. package/src/app/api/schedules/[id]/route.ts +185 -0
  40. package/src/app/api/schedules/route.ts +117 -0
  41. package/src/app/api/settings/budgets/route.ts +24 -0
  42. package/src/app/api/settings/openai/route.ts +24 -0
  43. package/src/app/api/settings/route.ts +21 -0
  44. package/src/app/api/settings/test/route.ts +26 -0
  45. package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +90 -0
  47. package/src/app/api/tasks/[id]/logs/route.ts +95 -0
  48. package/src/app/api/tasks/[id]/output/route.ts +47 -0
  49. package/src/app/api/tasks/[id]/respond/route.ts +64 -0
  50. package/src/app/api/tasks/[id]/resume/route.ts +76 -0
  51. package/src/app/api/tasks/[id]/route.ts +77 -0
  52. package/src/app/api/tasks/assist/route.ts +35 -0
  53. package/src/app/api/tasks/route.ts +82 -0
  54. package/src/app/api/uploads/[id]/route.ts +81 -0
  55. package/src/app/api/uploads/cleanup/route.ts +7 -0
  56. package/src/app/api/uploads/route.ts +66 -0
  57. package/src/app/api/workflows/[id]/execute/route.ts +82 -0
  58. package/src/app/api/workflows/[id]/route.ts +133 -0
  59. package/src/app/api/workflows/[id]/status/route.ts +54 -0
  60. package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
  61. package/src/app/api/workflows/route.ts +61 -0
  62. package/src/app/apple-icon.tsx +31 -0
  63. package/src/app/costs/page.tsx +256 -0
  64. package/src/app/dashboard/page.tsx +44 -0
  65. package/src/app/documents/[id]/page.tsx +46 -0
  66. package/src/app/documents/page.tsx +45 -0
  67. package/src/app/error.tsx +26 -0
  68. package/src/app/global-error.tsx +23 -0
  69. package/src/app/globals.css +733 -0
  70. package/src/app/icon.tsx +30 -0
  71. package/src/app/inbox/loading.tsx +15 -0
  72. package/src/app/inbox/page.tsx +35 -0
  73. package/src/app/layout.tsx +78 -0
  74. package/src/app/manifest.ts +32 -0
  75. package/src/app/monitor/page.tsx +37 -0
  76. package/src/app/page.tsx +162 -0
  77. package/src/app/profiles/[id]/edit/page.tsx +39 -0
  78. package/src/app/profiles/[id]/page.tsx +33 -0
  79. package/src/app/profiles/new/page.tsx +22 -0
  80. package/src/app/profiles/page.tsx +19 -0
  81. package/src/app/projects/[id]/page.tsx +134 -0
  82. package/src/app/projects/loading.tsx +17 -0
  83. package/src/app/projects/page.tsx +32 -0
  84. package/src/app/schedules/[id]/page.tsx +47 -0
  85. package/src/app/schedules/page.tsx +18 -0
  86. package/src/app/settings/loading.tsx +24 -0
  87. package/src/app/settings/page.tsx +27 -0
  88. package/src/app/tasks/[id]/page.tsx +45 -0
  89. package/src/app/tasks/new/page.tsx +27 -0
  90. package/src/app/workflows/[id]/edit/page.tsx +66 -0
  91. package/src/app/workflows/[id]/page.tsx +37 -0
  92. package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
  93. package/src/app/workflows/blueprints/new/page.tsx +20 -0
  94. package/src/app/workflows/blueprints/page.tsx +11 -0
  95. package/src/app/workflows/new/page.tsx +36 -0
  96. package/src/app/workflows/page.tsx +18 -0
  97. package/src/components/charts/donut-ring.tsx +64 -0
  98. package/src/components/charts/mini-bar.tsx +75 -0
  99. package/src/components/charts/sparkline.tsx +107 -0
  100. package/src/components/costs/cost-dashboard.tsx +877 -0
  101. package/src/components/costs/cost-filters.tsx +179 -0
  102. package/src/components/dashboard/activity-feed.tsx +95 -0
  103. package/src/components/dashboard/greeting.tsx +30 -0
  104. package/src/components/dashboard/priority-queue.tsx +79 -0
  105. package/src/components/dashboard/quick-actions.tsx +62 -0
  106. package/src/components/dashboard/recent-projects.tsx +79 -0
  107. package/src/components/dashboard/stats-cards.tsx +114 -0
  108. package/src/components/documents/document-browser.tsx +235 -0
  109. package/src/components/documents/document-detail-view.tsx +367 -0
  110. package/src/components/documents/document-grid.tsx +78 -0
  111. package/src/components/documents/document-preview.tsx +68 -0
  112. package/src/components/documents/document-table.tsx +119 -0
  113. package/src/components/documents/document-upload-dialog.tsx +153 -0
  114. package/src/components/documents/types.ts +6 -0
  115. package/src/components/documents/utils.ts +57 -0
  116. package/src/components/monitoring/connection-indicator.tsx +14 -0
  117. package/src/components/monitoring/log-entry.tsx +79 -0
  118. package/src/components/monitoring/log-filters.tsx +57 -0
  119. package/src/components/monitoring/log-stream.tsx +144 -0
  120. package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
  121. package/src/components/monitoring/monitor-overview.tsx +119 -0
  122. package/src/components/notifications/failure-action.tsx +38 -0
  123. package/src/components/notifications/inbox-list.tsx +165 -0
  124. package/src/components/notifications/message-response.tsx +196 -0
  125. package/src/components/notifications/notification-item.tsx +250 -0
  126. package/src/components/notifications/pending-approval-host.tsx +478 -0
  127. package/src/components/notifications/permission-action.tsx +37 -0
  128. package/src/components/notifications/permission-response-actions.tsx +126 -0
  129. package/src/components/notifications/unread-badge.tsx +35 -0
  130. package/src/components/profiles/profile-browser.tsx +117 -0
  131. package/src/components/profiles/profile-card.tsx +78 -0
  132. package/src/components/profiles/profile-detail-view.tsx +564 -0
  133. package/src/components/profiles/profile-form-view.tsx +480 -0
  134. package/src/components/profiles/profile-import-dialog.tsx +113 -0
  135. package/src/components/projects/project-card.tsx +58 -0
  136. package/src/components/projects/project-create-dialog.tsx +140 -0
  137. package/src/components/projects/project-detail.tsx +68 -0
  138. package/src/components/projects/project-edit-dialog.tsx +219 -0
  139. package/src/components/projects/project-list.tsx +108 -0
  140. package/src/components/schedules/schedule-create-dialog.tsx +403 -0
  141. package/src/components/schedules/schedule-detail-view.tsx +274 -0
  142. package/src/components/schedules/schedule-list.tsx +242 -0
  143. package/src/components/schedules/schedule-status-badge.tsx +16 -0
  144. package/src/components/settings/api-key-form.tsx +141 -0
  145. package/src/components/settings/auth-config-section.tsx +141 -0
  146. package/src/components/settings/auth-method-selector.tsx +67 -0
  147. package/src/components/settings/auth-status-badge.tsx +40 -0
  148. package/src/components/settings/auth-status-dot.tsx +59 -0
  149. package/src/components/settings/budget-guardrails-section.tsx +842 -0
  150. package/src/components/settings/data-management-section.tsx +141 -0
  151. package/src/components/settings/openai-runtime-section.tsx +104 -0
  152. package/src/components/settings/permissions-section.tsx +91 -0
  153. package/src/components/shared/app-sidebar.tsx +123 -0
  154. package/src/components/shared/card-skeleton.tsx +42 -0
  155. package/src/components/shared/command-palette.tsx +250 -0
  156. package/src/components/shared/confirm-dialog.tsx +52 -0
  157. package/src/components/shared/empty-state.tsx +24 -0
  158. package/src/components/shared/error-state.tsx +32 -0
  159. package/src/components/shared/form-section-card.tsx +33 -0
  160. package/src/components/shared/section-heading.tsx +14 -0
  161. package/src/components/shared/stagent-logo.tsx +21 -0
  162. package/src/components/shared/theme-toggle.tsx +46 -0
  163. package/src/components/tasks/ai-assist-panel.tsx +210 -0
  164. package/src/components/tasks/content-preview.tsx +89 -0
  165. package/src/components/tasks/empty-board.tsx +12 -0
  166. package/src/components/tasks/file-upload.tsx +120 -0
  167. package/src/components/tasks/kanban-board.tsx +275 -0
  168. package/src/components/tasks/kanban-column.tsx +75 -0
  169. package/src/components/tasks/skeleton-board.tsx +21 -0
  170. package/src/components/tasks/task-attachments.tsx +114 -0
  171. package/src/components/tasks/task-card.tsx +101 -0
  172. package/src/components/tasks/task-create-panel.tsx +360 -0
  173. package/src/components/tasks/task-detail-view.tsx +356 -0
  174. package/src/components/ui/alert-dialog.tsx +196 -0
  175. package/src/components/ui/badge.tsx +50 -0
  176. package/src/components/ui/button.tsx +71 -0
  177. package/src/components/ui/card.tsx +92 -0
  178. package/src/components/ui/checkbox.tsx +32 -0
  179. package/src/components/ui/command.tsx +184 -0
  180. package/src/components/ui/dialog.tsx +158 -0
  181. package/src/components/ui/dropdown-menu.tsx +257 -0
  182. package/src/components/ui/form.tsx +167 -0
  183. package/src/components/ui/input.tsx +21 -0
  184. package/src/components/ui/label.tsx +24 -0
  185. package/src/components/ui/popover.tsx +89 -0
  186. package/src/components/ui/progress.tsx +31 -0
  187. package/src/components/ui/radio-group.tsx +45 -0
  188. package/src/components/ui/scroll-area.tsx +58 -0
  189. package/src/components/ui/select.tsx +190 -0
  190. package/src/components/ui/separator.tsx +28 -0
  191. package/src/components/ui/sheet.tsx +143 -0
  192. package/src/components/ui/sidebar.tsx +726 -0
  193. package/src/components/ui/skeleton.tsx +13 -0
  194. package/src/components/ui/slider.tsx +63 -0
  195. package/src/components/ui/sonner.tsx +36 -0
  196. package/src/components/ui/switch.tsx +35 -0
  197. package/src/components/ui/table.tsx +116 -0
  198. package/src/components/ui/tabs.tsx +91 -0
  199. package/src/components/ui/textarea.tsx +18 -0
  200. package/src/components/ui/tooltip.tsx +57 -0
  201. package/src/components/workflows/blueprint-editor.tsx +109 -0
  202. package/src/components/workflows/blueprint-gallery.tsx +155 -0
  203. package/src/components/workflows/blueprint-preview.tsx +240 -0
  204. package/src/components/workflows/loop-status-view.tsx +272 -0
  205. package/src/components/workflows/swarm-dashboard.tsx +185 -0
  206. package/src/components/workflows/workflow-form-view.tsx +1376 -0
  207. package/src/components/workflows/workflow-list.tsx +230 -0
  208. package/src/components/workflows/workflow-status-view.tsx +477 -0
  209. package/src/hooks/use-mobile.ts +19 -0
  210. package/src/instrumentation.ts +7 -0
  211. package/src/lib/agents/claude-agent.ts +737 -0
  212. package/src/lib/agents/execution-manager.ts +27 -0
  213. package/src/lib/agents/profiles/assignment-validation.ts +75 -0
  214. package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
  215. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
  216. package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
  217. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
  218. package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
  219. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
  220. package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
  221. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
  222. package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
  223. package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
  224. package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
  225. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
  226. package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
  227. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
  228. package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
  229. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
  230. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
  231. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
  232. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
  233. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
  234. package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
  235. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
  236. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
  237. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
  238. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
  239. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
  240. package/src/lib/agents/profiles/compatibility.ts +109 -0
  241. package/src/lib/agents/profiles/registry.ts +293 -0
  242. package/src/lib/agents/profiles/test-runner.ts +18 -0
  243. package/src/lib/agents/profiles/test-types.ts +20 -0
  244. package/src/lib/agents/profiles/types.ts +43 -0
  245. package/src/lib/agents/router.ts +56 -0
  246. package/src/lib/agents/runtime/catalog.ts +85 -0
  247. package/src/lib/agents/runtime/claude-sdk.ts +12 -0
  248. package/src/lib/agents/runtime/claude.ts +370 -0
  249. package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
  250. package/src/lib/agents/runtime/index.ts +167 -0
  251. package/src/lib/agents/runtime/openai-codex.ts +1089 -0
  252. package/src/lib/agents/runtime/task-assist-types.ts +8 -0
  253. package/src/lib/agents/runtime/types.ts +30 -0
  254. package/src/lib/constants/settings.ts +13 -0
  255. package/src/lib/constants/status-colors.ts +44 -0
  256. package/src/lib/constants/task-status.ts +49 -0
  257. package/src/lib/data/clear.ts +63 -0
  258. package/src/lib/data/seed-data/documents.ts +715 -0
  259. package/src/lib/data/seed-data/logs.ts +195 -0
  260. package/src/lib/data/seed-data/notifications.ts +141 -0
  261. package/src/lib/data/seed-data/profiles.ts +175 -0
  262. package/src/lib/data/seed-data/projects.ts +61 -0
  263. package/src/lib/data/seed-data/schedules.ts +108 -0
  264. package/src/lib/data/seed-data/tasks.ts +341 -0
  265. package/src/lib/data/seed-data/usage-ledger.ts +130 -0
  266. package/src/lib/data/seed-data/workflows.ts +213 -0
  267. package/src/lib/data/seed.ts +129 -0
  268. package/src/lib/db/index.ts +221 -0
  269. package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
  270. package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
  271. package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
  272. package/src/lib/db/migrations/0003_add_settings.sql +5 -0
  273. package/src/lib/db/migrations/0004_add_documents.sql +20 -0
  274. package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
  275. package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
  276. package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
  277. package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
  278. package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
  279. package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
  280. package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
  281. package/src/lib/db/migrations/meta/_journal.json +27 -0
  282. package/src/lib/db/schema.ts +227 -0
  283. package/src/lib/documents/cleanup.ts +50 -0
  284. package/src/lib/documents/context-builder.ts +75 -0
  285. package/src/lib/documents/output-scanner.ts +166 -0
  286. package/src/lib/documents/processor.ts +120 -0
  287. package/src/lib/documents/processors/image.ts +21 -0
  288. package/src/lib/documents/processors/office.ts +36 -0
  289. package/src/lib/documents/processors/pdf.ts +12 -0
  290. package/src/lib/documents/processors/spreadsheet.ts +18 -0
  291. package/src/lib/documents/processors/text.ts +8 -0
  292. package/src/lib/documents/registry.ts +25 -0
  293. package/src/lib/notifications/actionable.ts +108 -0
  294. package/src/lib/notifications/permissions.ts +169 -0
  295. package/src/lib/queries/chart-data.ts +184 -0
  296. package/src/lib/schedules/interval-parser.ts +110 -0
  297. package/src/lib/schedules/scheduler.ts +220 -0
  298. package/src/lib/settings/auth.ts +98 -0
  299. package/src/lib/settings/budget-guardrails.ts +590 -0
  300. package/src/lib/settings/helpers.ts +23 -0
  301. package/src/lib/settings/openai-auth.ts +80 -0
  302. package/src/lib/settings/permissions.ts +102 -0
  303. package/src/lib/usage/ledger.ts +489 -0
  304. package/src/lib/usage/pricing.ts +68 -0
  305. package/src/lib/utils/crypto.ts +90 -0
  306. package/src/lib/utils/format-timestamp.ts +46 -0
  307. package/src/lib/utils/session-cleanup.ts +26 -0
  308. package/src/lib/utils/stagent-paths.ts +18 -0
  309. package/src/lib/utils.ts +6 -0
  310. package/src/lib/validators/blueprint.ts +43 -0
  311. package/src/lib/validators/profile.ts +64 -0
  312. package/src/lib/validators/project.ts +17 -0
  313. package/src/lib/validators/settings.ts +57 -0
  314. package/src/lib/validators/task.ts +30 -0
  315. package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
  316. package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
  317. package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
  318. package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
  319. package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
  320. package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
  321. package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
  322. package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
  323. package/src/lib/workflows/blueprints/instantiator.ts +131 -0
  324. package/src/lib/workflows/blueprints/registry.ts +128 -0
  325. package/src/lib/workflows/blueprints/template.ts +58 -0
  326. package/src/lib/workflows/blueprints/types.ts +38 -0
  327. package/src/lib/workflows/definition-validation.ts +121 -0
  328. package/src/lib/workflows/engine.ts +1113 -0
  329. package/src/lib/workflows/loop-executor.ts +270 -0
  330. package/src/lib/workflows/parallel.ts +55 -0
  331. package/src/lib/workflows/swarm.ts +97 -0
  332. package/src/lib/workflows/types.ts +112 -0
  333. package/tsconfig.json +41 -0
@@ -0,0 +1,877 @@
1
+ import Link from "next/link";
2
+ import {
3
+ AlertTriangle,
4
+ ArrowRight,
5
+ CalendarRange,
6
+ Coins,
7
+ ShieldAlert,
8
+ ShieldCheck,
9
+ Wallet,
10
+ } from "lucide-react";
11
+ import { listRuntimeCatalog } from "@/lib/agents/runtime/catalog";
12
+ import type {
13
+ ProviderModelBreakdownEntry,
14
+ UsageAuditEntry,
15
+ } from "@/lib/usage/ledger";
16
+ import { Badge } from "@/components/ui/badge";
17
+ import { Button } from "@/components/ui/button";
18
+ import { DonutRing } from "@/components/charts/donut-ring";
19
+ import { MiniBar } from "@/components/charts/mini-bar";
20
+ import { Sparkline } from "@/components/charts/sparkline";
21
+ import { SectionHeading } from "@/components/shared/section-heading";
22
+ import {
23
+ Table,
24
+ TableBody,
25
+ TableCell,
26
+ TableHead,
27
+ TableHeader,
28
+ TableRow,
29
+ } from "@/components/ui/table";
30
+ import { EmptyState } from "@/components/shared/empty-state";
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
+ }
50
+
51
+ interface CostSummary {
52
+ todaySpendMicros: number;
53
+ monthSpendMicros: number;
54
+ todayTokens: number;
55
+ monthTokens: number;
56
+ }
57
+
58
+ interface RuntimeBreakdownRow {
59
+ runtimeId: string;
60
+ label: string;
61
+ providerId: string;
62
+ costMicros: number;
63
+ totalTokens: number;
64
+ runs: number;
65
+ share: number;
66
+ unknownPricingRuns: number;
67
+ }
68
+
69
+ interface ModelVisualMeta {
70
+ share: number;
71
+ valueLabel: string;
72
+ basisLabel: string;
73
+ }
74
+
75
+ interface TrendSeries {
76
+ spend7: number[];
77
+ spend30: number[];
78
+ tokens7: number[];
79
+ tokens30: number[];
80
+ }
81
+
82
+ interface FilterState {
83
+ dateRange: string;
84
+ runtimeId: string;
85
+ status: string;
86
+ activityType: string;
87
+ }
88
+
89
+ interface CostDashboardProps {
90
+ filters: FilterState;
91
+ summary: CostSummary;
92
+ trendSeries: TrendSeries;
93
+ budgetStatuses: BudgetStatus[];
94
+ runtimeBreakdown: RuntimeBreakdownRow[];
95
+ modelBreakdown: ProviderModelBreakdownEntry[];
96
+ auditEntries: UsageAuditEntry[];
97
+ }
98
+
99
+ const runtimeCatalog = listRuntimeCatalog();
100
+ const runtimeLabelMap = new Map<string, string>(
101
+ runtimeCatalog.map((runtime) => [runtime.id, runtime.label])
102
+ );
103
+
104
+ function formatCurrencyMicros(value: number | null | undefined) {
105
+ const amount = value ?? 0;
106
+ return new Intl.NumberFormat("en-US", {
107
+ style: "currency",
108
+ currency: "USD",
109
+ minimumFractionDigits: 2,
110
+ maximumFractionDigits: amount >= 1_000_000 ? 2 : 4,
111
+ }).format(amount / 1_000_000);
112
+ }
113
+
114
+ function formatTokenCount(value: number | null | undefined) {
115
+ return new Intl.NumberFormat("en-US").format(value ?? 0);
116
+ }
117
+
118
+ function formatCompactCount(value: number | null | undefined) {
119
+ return new Intl.NumberFormat("en-US", {
120
+ notation: "compact",
121
+ maximumFractionDigits: 1,
122
+ }).format(value ?? 0);
123
+ }
124
+
125
+ function formatPercent(value: number) {
126
+ return `${Math.round(value)}%`;
127
+ }
128
+
129
+ function clampPercent(value: number) {
130
+ return Math.max(0, Math.min(100, value));
131
+ }
132
+
133
+ function formatDateTime(value: string) {
134
+ return new Date(value).toLocaleString(undefined, {
135
+ dateStyle: "medium",
136
+ timeStyle: "short",
137
+ });
138
+ }
139
+
140
+ function formatDateRangeLabel(range: string) {
141
+ switch (range) {
142
+ case "7d":
143
+ return "Last 7 days";
144
+ case "90d":
145
+ return "Last 90 days";
146
+ case "all":
147
+ return "All time";
148
+ default:
149
+ return "Last 30 days";
150
+ }
151
+ }
152
+
153
+ function formatActivityLabel(value: UsageAuditEntry["activityType"]) {
154
+ switch (value) {
155
+ case "task_run":
156
+ return "Task run";
157
+ case "task_resume":
158
+ return "Task resume";
159
+ case "workflow_step":
160
+ return "Workflow step";
161
+ case "scheduled_firing":
162
+ return "Scheduled firing";
163
+ case "task_assist":
164
+ return "Task assist";
165
+ case "profile_test":
166
+ return "Profile test";
167
+ 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);
178
+ }
179
+ }
180
+
181
+ function statusBadge(status: UsageAuditEntry["status"]) {
182
+ switch (status) {
183
+ case "completed":
184
+ return <Badge variant="success">Completed</Badge>;
185
+ case "failed":
186
+ return <Badge variant="destructive">Failed</Badge>;
187
+ case "blocked":
188
+ return (
189
+ <Badge
190
+ variant="outline"
191
+ className="border-status-warning/30 bg-status-warning/10 text-status-warning"
192
+ >
193
+ Blocked
194
+ </Badge>
195
+ );
196
+ case "unknown_pricing":
197
+ return (
198
+ <Badge variant="outline" className="border-border/70 text-muted-foreground">
199
+ Unknown pricing
200
+ </Badge>
201
+ );
202
+ case "cancelled":
203
+ return <Badge variant="secondary">Cancelled</Badge>;
204
+ default:
205
+ return <Badge variant="secondary">{formatLedgerStatusLabel(status)}</Badge>;
206
+ }
207
+ }
208
+
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
+ function renderEntityLink(entry: UsageAuditEntry) {
236
+ if (entry.taskId && entry.taskTitle) {
237
+ return (
238
+ <Link href={`/tasks/${entry.taskId}`} className="font-medium hover:underline">
239
+ {entry.taskTitle}
240
+ </Link>
241
+ );
242
+ }
243
+ if (entry.workflowId && entry.workflowName) {
244
+ return (
245
+ <Link
246
+ href={`/workflows/${entry.workflowId}`}
247
+ className="font-medium hover:underline"
248
+ >
249
+ {entry.workflowName}
250
+ </Link>
251
+ );
252
+ }
253
+ if (entry.scheduleId && entry.scheduleName) {
254
+ return (
255
+ <Link
256
+ href={`/schedules/${entry.scheduleId}`}
257
+ className="font-medium hover:underline"
258
+ >
259
+ {entry.scheduleName}
260
+ </Link>
261
+ );
262
+ }
263
+
264
+ return <span className="font-medium">{formatActivityLabel(entry.activityType)}</span>;
265
+ }
266
+
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
+ function SummaryCard({
298
+ eyebrow,
299
+ title,
300
+ value,
301
+ detail,
302
+ icon: Icon,
303
+ }: {
304
+ eyebrow: string;
305
+ title: string;
306
+ value: string;
307
+ detail: string;
308
+ icon: typeof Wallet;
309
+ }) {
310
+ return (
311
+ <div className="surface-card rounded-3xl p-5">
312
+ <div className="mb-4 flex items-start justify-between gap-3">
313
+ <div className="space-y-1">
314
+ <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
315
+ {eyebrow}
316
+ </p>
317
+ <h2 className="text-sm font-medium text-foreground">{title}</h2>
318
+ </div>
319
+ <div className="surface-card-muted rounded-2xl p-2.5">
320
+ <Icon className="h-4 w-4 text-muted-foreground" />
321
+ </div>
322
+ </div>
323
+ <div className="space-y-1">
324
+ <p className="text-2xl font-bold tracking-tight">{value}</p>
325
+ <p className="text-xs text-muted-foreground">{detail}</p>
326
+ </div>
327
+ </div>
328
+ );
329
+ }
330
+
331
+ export function CostDashboard({
332
+ filters,
333
+ summary,
334
+ trendSeries,
335
+ budgetStatuses,
336
+ runtimeBreakdown,
337
+ modelBreakdown,
338
+ auditEntries,
339
+ }: CostDashboardProps) {
340
+ const warnings = budgetStatuses.filter((status) => status.health === "warning");
341
+ 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
+ const hasUsage =
347
+ summary.monthSpendMicros > 0 ||
348
+ summary.monthTokens > 0 ||
349
+ modelBreakdown.length > 0 ||
350
+ auditEntries.length > 0;
351
+ const filteredUnknownPricingRuns = modelBreakdown.reduce(
352
+ (total, row) => total + row.unknownPricingRuns,
353
+ 0
354
+ );
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
+ );
362
+
363
+ return (
364
+ <div className="mx-auto flex max-w-7xl flex-col gap-6">
365
+ <div className="max-w-3xl space-y-3">
366
+ <div className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-background/55 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
367
+ <Wallet className="h-3.5 w-3.5" />
368
+ Governance &amp; Analytics
369
+ </div>
370
+ <div className="space-y-2">
371
+ <h1 className="text-2xl font-bold tracking-tight">Cost &amp; Usage</h1>
372
+ <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.
375
+ </p>
376
+ </div>
377
+ </div>
378
+
379
+ <CostFilters
380
+ dateRange={filters.dateRange}
381
+ runtimeId={filters.runtimeId}
382
+ status={filters.status}
383
+ activityType={filters.activityType}
384
+ runtimeOptions={runtimeCatalog.map((runtime) => ({
385
+ id: runtime.id,
386
+ label: runtime.label,
387
+ }))}
388
+ />
389
+
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
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
445
+ <SummaryCard
446
+ eyebrow="Today"
447
+ title="Spend"
448
+ value={formatCurrencyMicros(summary.todaySpendMicros)}
449
+ detail="Current-day spend across governed runtimes"
450
+ icon={Wallet}
451
+ />
452
+ <SummaryCard
453
+ eyebrow="Month"
454
+ title="Spend"
455
+ value={formatCurrencyMicros(summary.monthSpendMicros)}
456
+ detail="Current-month spend used so far"
457
+ icon={CalendarRange}
458
+ />
459
+ <SummaryCard
460
+ eyebrow="Today"
461
+ title="Tokens"
462
+ value={formatCompactCount(summary.todayTokens)}
463
+ detail={`${formatTokenCount(summary.todayTokens)} total tokens today`}
464
+ icon={Coins}
465
+ />
466
+ <SummaryCard
467
+ eyebrow="Month"
468
+ title="Tokens"
469
+ value={formatCompactCount(summary.monthTokens)}
470
+ detail={`${formatTokenCount(summary.monthTokens)} total tokens this month`}
471
+ icon={Coins}
472
+ />
473
+
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" />
485
+ ) : (
486
+ <ShieldCheck className="h-4 w-4 text-status-completed" />
487
+ )}
488
+ </div>
489
+ </div>
490
+
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
+ )}
505
+ </p>
506
+ <p className="text-xs text-muted-foreground">
507
+ Remaining before the nearest configured cap. Resets{" "}
508
+ {formatDateTime(nearestBudget.resetAtIso)}.
509
+ </p>
510
+ </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
+ )}
521
+ </div>
522
+ </div>
523
+
524
+ {hasUsage ? (
525
+ <>
526
+ <div className="grid gap-6 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
527
+ <div className="surface-card rounded-3xl p-5">
528
+ <SectionHeading>Trend View</SectionHeading>
529
+ <div className="grid gap-4 lg:grid-cols-2">
530
+ <div className="surface-card-muted rounded-2xl p-4">
531
+ <div className="mb-4 flex items-center justify-between gap-3">
532
+ <div>
533
+ <p className="text-sm font-medium">Spend velocity</p>
534
+ <p className="text-xs text-muted-foreground">
535
+ 7-day and 30-day spend series
536
+ </p>
537
+ </div>
538
+ <Badge variant="outline">{formatCurrencyMicros(summary.monthSpendMicros)}</Badge>
539
+ </div>
540
+ <div className="grid gap-3 sm:grid-cols-2">
541
+ <div className="rounded-2xl border border-border/50 bg-background/40 p-3">
542
+ <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
543
+ 7-day
544
+ </p>
545
+ <Sparkline
546
+ data={trendSeries.spend7}
547
+ width={160}
548
+ height={48}
549
+ color="var(--chart-1)"
550
+ label="7 day spend trend"
551
+ className="w-full"
552
+ />
553
+ </div>
554
+ <div className="rounded-2xl border border-border/50 bg-background/40 p-3">
555
+ <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
556
+ 30-day
557
+ </p>
558
+ <MiniBar
559
+ data={trendSeries.spend30.map((value) => ({
560
+ value,
561
+ color: "var(--chart-1)",
562
+ }))}
563
+ width={220}
564
+ height={48}
565
+ label="30 day spend trend"
566
+ className="w-full"
567
+ />
568
+ </div>
569
+ </div>
570
+ </div>
571
+
572
+ <div className="surface-card-muted rounded-2xl p-4">
573
+ <div className="mb-4 flex items-center justify-between gap-3">
574
+ <div>
575
+ <p className="text-sm font-medium">Token velocity</p>
576
+ <p className="text-xs text-muted-foreground">
577
+ 7-day and 30-day token series
578
+ </p>
579
+ </div>
580
+ <Badge variant="outline">{formatCompactCount(summary.monthTokens)} tokens</Badge>
581
+ </div>
582
+ <div className="grid gap-3 sm:grid-cols-2">
583
+ <div className="rounded-2xl border border-border/50 bg-background/40 p-3">
584
+ <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
585
+ 7-day
586
+ </p>
587
+ <Sparkline
588
+ data={trendSeries.tokens7}
589
+ width={160}
590
+ height={48}
591
+ color="var(--chart-2)"
592
+ label="7 day token trend"
593
+ className="w-full"
594
+ />
595
+ </div>
596
+ <div className="rounded-2xl border border-border/50 bg-background/40 p-3">
597
+ <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
598
+ 30-day
599
+ </p>
600
+ <MiniBar
601
+ data={trendSeries.tokens30.map((value) => ({
602
+ value,
603
+ color: "var(--chart-2)",
604
+ }))}
605
+ width={220}
606
+ height={48}
607
+ label="30 day token trend"
608
+ className="w-full"
609
+ />
610
+ </div>
611
+ </div>
612
+ </div>
613
+ </div>
614
+ </div>
615
+
616
+ <div className="surface-card rounded-3xl p-5">
617
+ <SectionHeading>Runtime Breakdown</SectionHeading>
618
+ <div className="space-y-3">
619
+ {runtimeBreakdown.length > 0 ? (
620
+ runtimeBreakdown.map((runtime) => (
621
+ <div
622
+ key={runtime.runtimeId}
623
+ className="surface-card-muted flex items-center justify-between gap-4 rounded-2xl p-4"
624
+ >
625
+ <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
+ />
634
+ <div className="space-y-1">
635
+ <div className="flex items-center gap-2">
636
+ <p className="text-sm font-medium">{runtime.label}</p>
637
+ <Badge variant="outline">{runtime.providerId}</Badge>
638
+ </div>
639
+ <p className="text-xs text-muted-foreground">
640
+ {formatPercent(runtime.share)} of filtered spend across{" "}
641
+ {runtime.runs} runs
642
+ </p>
643
+ </div>
644
+ </div>
645
+ <div className="grid grid-cols-2 gap-3 text-right text-sm">
646
+ <div>
647
+ <p className="text-xs uppercase tracking-wide text-muted-foreground">
648
+ Spend
649
+ </p>
650
+ <p className="font-medium">
651
+ {formatCurrencyMicros(runtime.costMicros)}
652
+ </p>
653
+ </div>
654
+ <div>
655
+ <p className="text-xs uppercase tracking-wide text-muted-foreground">
656
+ Tokens
657
+ </p>
658
+ <p className="font-medium">
659
+ {formatCompactCount(runtime.totalTokens)}
660
+ </p>
661
+ </div>
662
+ {runtime.unknownPricingRuns > 0 ? (
663
+ <div className="col-span-2">
664
+ <p className="text-xs text-muted-foreground">
665
+ {runtime.unknownPricingRuns} run
666
+ {runtime.unknownPricingRuns === 1 ? "" : "s"} missing
667
+ pricing data
668
+ </p>
669
+ </div>
670
+ ) : null}
671
+ </div>
672
+ </div>
673
+ ))
674
+ ) : (
675
+ <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()}.
677
+ </div>
678
+ )}
679
+ </div>
680
+ </div>
681
+ </div>
682
+
683
+ <div className="surface-card rounded-3xl p-5">
684
+ <div className="mb-4 flex items-start justify-between gap-3">
685
+ <div>
686
+ <SectionHeading className="mb-2">Model Breakdown</SectionHeading>
687
+ <p className="text-sm text-muted-foreground">
688
+ Concentration by model for {formatDateRangeLabel(filters.dateRange).toLowerCase()}.
689
+ </p>
690
+ </div>
691
+ {filteredUnknownPricingRuns > 0 ? (
692
+ <Badge variant="outline">
693
+ {filteredUnknownPricingRuns} unknown-pricing row
694
+ {filteredUnknownPricingRuns === 1 ? "" : "s"}
695
+ </Badge>
696
+ ) : null}
697
+ </div>
698
+
699
+ {modelBreakdown.length > 0 ? (
700
+ <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>
736
+
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>
756
+ </div>
757
+ </div>
758
+ </div>
759
+ );
760
+ })}
761
+ </div>
762
+ ) : (
763
+ <div className="surface-card-muted rounded-2xl p-4 text-sm text-muted-foreground">
764
+ No model breakdown is available for the selected window yet.
765
+ </div>
766
+ )}
767
+ </div>
768
+
769
+ <div className="surface-card rounded-3xl p-5">
770
+ <div className="mb-4 flex items-start justify-between gap-3">
771
+ <div>
772
+ <SectionHeading className="mb-2">Audit Log</SectionHeading>
773
+ <p className="text-sm text-muted-foreground">
774
+ Filtered execution history for {formatDateRangeLabel(filters.dateRange).toLowerCase()}.
775
+ </p>
776
+ </div>
777
+ <Badge variant="outline">{auditEntries.length} rows</Badge>
778
+ </div>
779
+
780
+ {auditEntries.length > 0 ? (
781
+ <div className="surface-scroll rounded-2xl">
782
+ <Table>
783
+ <TableHeader>
784
+ <TableRow>
785
+ <TableHead>Timestamp</TableHead>
786
+ <TableHead>Activity</TableHead>
787
+ <TableHead>Linked entity</TableHead>
788
+ <TableHead>Runtime</TableHead>
789
+ <TableHead>Tokens</TableHead>
790
+ <TableHead>Cost</TableHead>
791
+ <TableHead>Status</TableHead>
792
+ </TableRow>
793
+ </TableHeader>
794
+ <TableBody>
795
+ {auditEntries.map((entry) => (
796
+ <TableRow key={entry.id}>
797
+ <TableCell className="align-top text-xs text-muted-foreground">
798
+ {formatDateTime(entry.finishedAt.toISOString())}
799
+ </TableCell>
800
+ <TableCell className="align-top">
801
+ <div className="space-y-1">
802
+ <p className="font-medium">
803
+ {formatActivityLabel(entry.activityType)}
804
+ </p>
805
+ <p className="text-xs text-muted-foreground">
806
+ {entry.modelId ?? "Unknown model"}
807
+ </p>
808
+ </div>
809
+ </TableCell>
810
+ <TableCell className="align-top">
811
+ <div className="space-y-1">
812
+ {renderEntityLink(entry)}
813
+ {entry.projectId && entry.projectName ? (
814
+ <p className="text-xs text-muted-foreground">
815
+ <Link
816
+ href={`/projects/${entry.projectId}`}
817
+ className="hover:underline"
818
+ >
819
+ {entry.projectName}
820
+ </Link>
821
+ </p>
822
+ ) : null}
823
+ </div>
824
+ </TableCell>
825
+ <TableCell className="align-top">
826
+ <div className="space-y-1">
827
+ <p className="font-medium">
828
+ {runtimeLabelMap.get(entry.runtimeId) ?? entry.runtimeId}
829
+ </p>
830
+ <p className="text-xs text-muted-foreground">{entry.providerId}</p>
831
+ </div>
832
+ </TableCell>
833
+ <TableCell className="align-top text-right">
834
+ {formatCompactCount(entry.totalTokens ?? 0)}
835
+ </TableCell>
836
+ <TableCell className="align-top text-right">
837
+ {entry.status === "unknown_pricing"
838
+ ? "Unavailable"
839
+ : formatCurrencyMicros(entry.costMicros)}
840
+ </TableCell>
841
+ <TableCell className="align-top">{statusBadge(entry.status)}</TableCell>
842
+ </TableRow>
843
+ ))}
844
+ </TableBody>
845
+ </Table>
846
+ </div>
847
+ ) : (
848
+ <div className="surface-card-muted rounded-2xl p-4 text-sm text-muted-foreground">
849
+ No audit rows match the current filters. Adjust the runtime, status,
850
+ activity, or date range to widen the view.
851
+ </div>
852
+ )}
853
+ </div>
854
+ </>
855
+ ) : (
856
+ <EmptyState
857
+ icon={Wallet}
858
+ heading="No usage recorded yet"
859
+ description="Metering is wired, but there are no paid runtime rows to visualize yet. Run a task, schedule, or workflow to populate the dashboard."
860
+ action={
861
+ <div className="flex flex-wrap items-center justify-center gap-2">
862
+ <Button asChild size="sm">
863
+ <Link href="/dashboard?create=task">
864
+ Create Task
865
+ <ArrowRight className="h-3.5 w-3.5" />
866
+ </Link>
867
+ </Button>
868
+ <Button asChild variant="outline" size="sm">
869
+ <Link href="/settings">Review Budgets</Link>
870
+ </Button>
871
+ </div>
872
+ }
873
+ />
874
+ )}
875
+ </div>
876
+ );
877
+ }