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
@@ -0,0 +1,170 @@
1
+ /**
2
+ * E2E: Single task execution across profiles and runtimes.
3
+ *
4
+ * Tests that individual tasks execute and produce results via both
5
+ * Claude Code and Codex runtimes with different agent profiles.
6
+ */
7
+
8
+ import {
9
+ setupE2E,
10
+ teardownE2E,
11
+ testProjectId,
12
+ claudeAvailable,
13
+ codexAvailable,
14
+ } from "./setup";
15
+ import {
16
+ createTask,
17
+ executeTask,
18
+ pollTaskUntilDone,
19
+ updateTask,
20
+ } from "./helpers";
21
+
22
+ beforeAll(async () => {
23
+ await setupE2E();
24
+ });
25
+
26
+ afterAll(async () => {
27
+ await teardownE2E();
28
+ });
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Claude Code runtime
32
+ // ---------------------------------------------------------------------------
33
+
34
+ describe("Single Task — Claude Code", () => {
35
+ beforeAll(() => {
36
+ if (!claudeAvailable) {
37
+ console.warn("Skipping Claude Code tests — runtime not available");
38
+ }
39
+ });
40
+
41
+ it.skipIf(!claudeAvailable)(
42
+ "general profile describes code",
43
+ async () => {
44
+ const { ok, data: task } = await createTask({
45
+ title: "Describe the TypeScript code in src/",
46
+ description:
47
+ "Read the TypeScript files in the project and describe what the code does.",
48
+ projectId: testProjectId,
49
+ agentProfile: "general",
50
+ });
51
+ expect(ok).toBe(true);
52
+
53
+ // Queue → execute
54
+ await updateTask(task!.id, { status: "queued" });
55
+ const exec = await executeTask(task!.id);
56
+ expect(exec.status).toBe(202);
57
+
58
+ // Poll until done
59
+ const result = await pollTaskUntilDone(task!.id);
60
+ expect(result.status).toBe("completed");
61
+ expect(result.result).toBeTruthy();
62
+ expect(result.result!.length).toBeGreaterThan(50);
63
+ }
64
+ );
65
+
66
+ it.skipIf(!claudeAvailable)(
67
+ "code-reviewer profile finds bugs",
68
+ async () => {
69
+ const { ok, data: task } = await createTask({
70
+ title: "Review code for bugs",
71
+ description:
72
+ "Review all TypeScript files in the project. Find bugs and report them with severity levels.",
73
+ projectId: testProjectId,
74
+ agentProfile: "code-reviewer",
75
+ });
76
+ expect(ok).toBe(true);
77
+
78
+ await updateTask(task!.id, { status: "queued" });
79
+ const exec = await executeTask(task!.id);
80
+ expect(exec.status).toBe(202);
81
+
82
+ const result = await pollTaskUntilDone(task!.id);
83
+ expect(result.status).toBe("completed");
84
+ expect(result.result).toBeTruthy();
85
+ // Code reviewer should find at least some issues
86
+ expect(result.result!.length).toBeGreaterThan(100);
87
+ }
88
+ );
89
+
90
+ it.skipIf(!claudeAvailable)(
91
+ "document-writer profile generates overview",
92
+ async () => {
93
+ const { ok, data: task } = await createTask({
94
+ title: "Write a technical overview document",
95
+ description:
96
+ "Generate a technical overview of this project including structure, modules, and dependencies.",
97
+ projectId: testProjectId,
98
+ agentProfile: "document-writer",
99
+ });
100
+ expect(ok).toBe(true);
101
+
102
+ await updateTask(task!.id, { status: "queued" });
103
+ const exec = await executeTask(task!.id);
104
+ expect(exec.status).toBe(202);
105
+
106
+ const result = await pollTaskUntilDone(task!.id);
107
+ expect(result.status).toBe("completed");
108
+ expect(result.result).toBeTruthy();
109
+ }
110
+ );
111
+ });
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Codex runtime
115
+ // ---------------------------------------------------------------------------
116
+
117
+ describe("Single Task — Codex", () => {
118
+ beforeAll(() => {
119
+ if (!codexAvailable) {
120
+ console.warn("Skipping Codex tests — runtime not available");
121
+ }
122
+ });
123
+
124
+ it.skipIf(!codexAvailable)(
125
+ "general profile describes code via Codex",
126
+ async () => {
127
+ const { ok, data: task } = await createTask({
128
+ title: "Describe the TypeScript code in src/",
129
+ description:
130
+ "Read the TypeScript files in the project and describe what the code does.",
131
+ projectId: testProjectId,
132
+ assignedAgent: "codex",
133
+ agentProfile: "general",
134
+ });
135
+ expect(ok).toBe(true);
136
+
137
+ await updateTask(task!.id, { status: "queued" });
138
+ const exec = await executeTask(task!.id);
139
+ expect(exec.status).toBe(202);
140
+
141
+ const result = await pollTaskUntilDone(task!.id);
142
+ expect(result.status).toBe("completed");
143
+ expect(result.result).toBeTruthy();
144
+ expect(result.result!.length).toBeGreaterThan(50);
145
+ }
146
+ );
147
+
148
+ it.skipIf(!codexAvailable)(
149
+ "code-reviewer profile finds bugs via Codex",
150
+ async () => {
151
+ const { ok, data: task } = await createTask({
152
+ title: "Review code for bugs",
153
+ description:
154
+ "Review all TypeScript files in the project. Find bugs and report them with severity levels.",
155
+ projectId: testProjectId,
156
+ assignedAgent: "codex",
157
+ agentProfile: "code-reviewer",
158
+ });
159
+ expect(ok).toBe(true);
160
+
161
+ await updateTask(task!.id, { status: "queued" });
162
+ const exec = await executeTask(task!.id);
163
+ expect(exec.status).toBe(202);
164
+
165
+ const result = await pollTaskUntilDone(task!.id);
166
+ expect(result.status).toBe("completed");
167
+ expect(result.result).toBeTruthy();
168
+ }
169
+ );
170
+ });
@@ -2,30 +2,53 @@ import { NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
3
  import { projects, tasks } from "@/lib/db/schema";
4
4
  import { desc } from "drizzle-orm";
5
+ import { getManifest } from "@/lib/docs/reader";
5
6
 
6
7
  export async function GET() {
7
- const recentProjects = await db
8
- .select({
9
- id: projects.id,
10
- name: projects.name,
11
- status: projects.status,
12
- })
13
- .from(projects)
14
- .orderBy(desc(projects.updatedAt))
15
- .limit(5);
8
+ const [recentProjects, recentTasks] = await Promise.all([
9
+ db
10
+ .select({
11
+ id: projects.id,
12
+ name: projects.name,
13
+ status: projects.status,
14
+ })
15
+ .from(projects)
16
+ .orderBy(desc(projects.updatedAt))
17
+ .limit(5),
18
+ db
19
+ .select({
20
+ id: tasks.id,
21
+ title: tasks.title,
22
+ status: tasks.status,
23
+ })
24
+ .from(tasks)
25
+ .orderBy(desc(tasks.updatedAt))
26
+ .limit(5),
27
+ ]);
16
28
 
17
- const recentTasks = await db
18
- .select({
19
- id: tasks.id,
20
- title: tasks.title,
21
- status: tasks.status,
22
- })
23
- .from(tasks)
24
- .orderBy(desc(tasks.updatedAt))
25
- .limit(5);
29
+ // Read playbook items from manifest
30
+ let playbook: { slug: string; title: string; tags: string[] }[] = [];
31
+ try {
32
+ const manifest = getManifest();
33
+ playbook = [
34
+ ...manifest.sections.map((s) => ({
35
+ slug: s.slug,
36
+ title: s.title,
37
+ tags: s.tags,
38
+ })),
39
+ ...manifest.journeys.map((j) => ({
40
+ slug: j.slug,
41
+ title: j.title,
42
+ tags: [j.persona, j.difficulty],
43
+ })),
44
+ ];
45
+ } catch {
46
+ // docs/manifest.json may not exist — graceful fallback
47
+ }
26
48
 
27
49
  return NextResponse.json({
28
50
  projects: recentProjects,
29
51
  tasks: recentTasks,
52
+ playbook,
30
53
  });
31
54
  }
@@ -0,0 +1,44 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import {
4
+ batchApproveProposals,
5
+ batchRejectProposals,
6
+ } from "@/lib/agents/learning-session";
7
+
8
+ const batchSchema = z.object({
9
+ proposalIds: z.array(z.string().min(1)).min(1),
10
+ action: z.enum(["approve", "reject"]),
11
+ });
12
+
13
+ /**
14
+ * POST /api/context/batch — batch approve or reject context proposals.
15
+ *
16
+ * Used by the batch proposal review UI after workflow completion.
17
+ * Accepts an array of learned_context row IDs and an action.
18
+ */
19
+ export async function POST(req: NextRequest) {
20
+ try {
21
+ const body = await req.json();
22
+ const parsed = batchSchema.safeParse(body);
23
+
24
+ if (!parsed.success) {
25
+ return NextResponse.json(
26
+ { error: "proposalIds (string[]) and action ('approve'|'reject') are required" },
27
+ { status: 400 }
28
+ );
29
+ }
30
+
31
+ const { proposalIds, action } = parsed.data;
32
+
33
+ const count =
34
+ action === "approve"
35
+ ? await batchApproveProposals(proposalIds)
36
+ : await batchRejectProposals(proposalIds);
37
+
38
+ return NextResponse.json({ success: true, action, count });
39
+ } catch (err: unknown) {
40
+ const message =
41
+ err instanceof Error ? err.message : "Batch operation failed";
42
+ return NextResponse.json({ error: message }, { status: 500 });
43
+ }
44
+ }
@@ -0,0 +1,80 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import {
4
+ getPreset,
5
+ getActivePresets,
6
+ applyPreset,
7
+ removePreset,
8
+ PRESETS,
9
+ } from "@/lib/settings/permission-presets";
10
+
11
+ const presetSchema = z.object({
12
+ presetId: z.string().min(1),
13
+ });
14
+
15
+ /**
16
+ * GET /api/permissions/presets — list all presets with their active status.
17
+ */
18
+ export async function GET() {
19
+ const activeIds = await getActivePresets();
20
+ const activeSet = new Set(activeIds);
21
+
22
+ const presets = PRESETS.map((p) => ({
23
+ ...p,
24
+ active: activeSet.has(p.id),
25
+ }));
26
+
27
+ return NextResponse.json({ presets });
28
+ }
29
+
30
+ /**
31
+ * POST /api/permissions/presets — enable a preset (adds all its patterns).
32
+ */
33
+ export async function POST(req: NextRequest) {
34
+ const body = await req.json();
35
+ const parsed = presetSchema.safeParse(body);
36
+
37
+ if (!parsed.success) {
38
+ return NextResponse.json(
39
+ { error: "presetId (string) is required" },
40
+ { status: 400 }
41
+ );
42
+ }
43
+
44
+ const preset = getPreset(parsed.data.presetId);
45
+ if (!preset) {
46
+ return NextResponse.json(
47
+ { error: `Unknown preset: ${parsed.data.presetId}` },
48
+ { status: 404 }
49
+ );
50
+ }
51
+
52
+ await applyPreset(parsed.data.presetId);
53
+ return NextResponse.json({ success: true });
54
+ }
55
+
56
+ /**
57
+ * DELETE /api/permissions/presets — disable a preset (removes unique patterns).
58
+ */
59
+ export async function DELETE(req: NextRequest) {
60
+ const body = await req.json();
61
+ const parsed = presetSchema.safeParse(body);
62
+
63
+ if (!parsed.success) {
64
+ return NextResponse.json(
65
+ { error: "presetId (string) is required" },
66
+ { status: 400 }
67
+ );
68
+ }
69
+
70
+ const preset = getPreset(parsed.data.presetId);
71
+ if (!preset) {
72
+ return NextResponse.json(
73
+ { error: `Unknown preset: ${parsed.data.presetId}` },
74
+ { status: 404 }
75
+ );
76
+ }
77
+
78
+ await removePreset(parsed.data.presetId);
79
+ return NextResponse.json({ success: true });
80
+ }
@@ -0,0 +1,15 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getDocsLastGenerated } from "@/lib/docs/reader";
3
+ import { getSetting } from "@/lib/settings/helpers";
4
+
5
+ export async function GET() {
6
+ const lastGenerated = getDocsLastGenerated();
7
+ const lastVisit = await getSetting("lastPlaybookVisit");
8
+
9
+ const hasUpdates =
10
+ lastGenerated != null &&
11
+ lastVisit != null &&
12
+ new Date(lastGenerated) > new Date(lastVisit);
13
+
14
+ return NextResponse.json({ hasUpdates });
15
+ }
@@ -1,29 +1,31 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { listProfiles, createProfile, isBuiltin } from "@/lib/agents/profiles/registry";
3
+ import { sortProfilesByName } from "@/lib/agents/profiles/sort";
3
4
  import { ProfileConfigSchema } from "@/lib/validators/profile";
4
5
 
5
6
  export async function GET() {
6
- const profiles = listProfiles().map((p) => ({
7
- id: p.id,
8
- name: p.name,
9
- description: p.description,
10
- domain: p.domain,
11
- tags: p.tags,
12
- skillMd: p.skillMd,
13
- allowedTools: p.allowedTools,
14
- mcpServers: p.mcpServers,
15
- canUseToolPolicy: p.canUseToolPolicy,
16
- temperature: p.temperature,
17
- maxTurns: p.maxTurns,
18
- outputFormat: p.outputFormat,
19
- version: p.version,
20
- author: p.author,
21
- source: p.source,
22
- tests: p.tests,
23
- supportedRuntimes: p.supportedRuntimes,
24
- runtimeOverrides: p.runtimeOverrides,
25
- isBuiltin: isBuiltin(p.id),
26
- }));
7
+ const profiles = sortProfilesByName(
8
+ listProfiles().map((p) => ({
9
+ id: p.id,
10
+ name: p.name,
11
+ description: p.description,
12
+ domain: p.domain,
13
+ tags: p.tags,
14
+ skillMd: p.skillMd,
15
+ allowedTools: p.allowedTools,
16
+ mcpServers: p.mcpServers,
17
+ canUseToolPolicy: p.canUseToolPolicy,
18
+ maxTurns: p.maxTurns,
19
+ outputFormat: p.outputFormat,
20
+ version: p.version,
21
+ author: p.author,
22
+ source: p.source,
23
+ tests: p.tests,
24
+ supportedRuntimes: p.supportedRuntimes,
25
+ runtimeOverrides: p.runtimeOverrides,
26
+ isBuiltin: isBuiltin(p.id),
27
+ }))
28
+ );
27
29
 
28
30
  return NextResponse.json(profiles);
29
31
  }
@@ -0,0 +1,15 @@
1
+ import { NextResponse } from "next/server";
2
+ import {
3
+ getPricingRegistrySnapshot,
4
+ refreshPricingRegistry,
5
+ } from "@/lib/usage/pricing-registry";
6
+
7
+ export async function GET() {
8
+ const snapshot = await getPricingRegistrySnapshot();
9
+ return NextResponse.json(snapshot);
10
+ }
11
+
12
+ export async function POST() {
13
+ const snapshot = await refreshPricingRegistry();
14
+ return NextResponse.json(snapshot);
15
+ }
@@ -1,4 +1,3 @@
1
- import { listRuntimeCatalog } from "@/lib/agents/runtime/catalog";
2
1
  import { CostDashboard } from "@/components/costs/cost-dashboard";
3
2
  import { getBudgetGuardrailSnapshot } from "@/lib/settings/budget-guardrails";
4
3
  import {
@@ -13,8 +12,6 @@ import {
13
12
 
14
13
  export const dynamic = "force-dynamic";
15
14
 
16
- const runtimeCatalog = listRuntimeCatalog();
17
- const validRuntimeIds = new Set<string>(runtimeCatalog.map((runtime) => runtime.id));
18
15
  const validDateRanges = new Set(["7d", "30d", "90d", "all"]);
19
16
  const validStatuses = new Set<UsageLedgerStatus>([
20
17
  "completed",
@@ -40,10 +37,6 @@ function resolveDateRange(value: string | undefined) {
40
37
  return value && validDateRanges.has(value) ? value : "30d";
41
38
  }
42
39
 
43
- function resolveRuntime(value: string | undefined) {
44
- return value && validRuntimeIds.has(value) ? value : "all";
45
- }
46
-
47
40
  function resolveStatus(value: string | undefined) {
48
41
  return value && validStatuses.has(value as UsageLedgerStatus) ? value : "all";
49
42
  }
@@ -102,25 +95,6 @@ function fillSeries<T extends { day: string }>(
102
95
  return keys.map((key) => values.get(key) ?? 0);
103
96
  }
104
97
 
105
- function findOverallSpend(
106
- statuses: Array<{
107
- scopeId: string;
108
- window: string;
109
- metric: string;
110
- currentValue: number;
111
- }>,
112
- window: "daily" | "monthly"
113
- ) {
114
- return (
115
- statuses.find(
116
- (status) =>
117
- status.scopeId === "overall" &&
118
- status.window === window &&
119
- status.metric === "spend"
120
- )?.currentValue ?? 0
121
- );
122
- }
123
-
124
98
  function buildRuntimeBreakdown(
125
99
  rows: ProviderModelBreakdownEntry[]
126
100
  ): Array<{
@@ -149,9 +123,7 @@ function buildRuntimeBreakdown(
149
123
  for (const row of rows) {
150
124
  const current = totals.get(row.runtimeId) ?? {
151
125
  runtimeId: row.runtimeId,
152
- label:
153
- runtimeCatalog.find((runtime) => runtime.id === row.runtimeId)?.label ??
154
- row.runtimeId,
126
+ label: row.runtimeId,
155
127
  providerId: row.providerId,
156
128
  costMicros: 0,
157
129
  totalTokens: 0,
@@ -191,22 +163,34 @@ export default async function CostsPage({
191
163
  }) {
192
164
  const params = await searchParams;
193
165
  const dateRange = resolveDateRange(toScalar(params.range));
194
- const runtimeId = resolveRuntime(toScalar(params.runtime));
195
166
  const status = resolveStatus(toScalar(params.status));
196
167
  const activityType = resolveActivityType(toScalar(params.activity));
197
-
198
168
  const rangeStart = getRangeStart(dateRange);
199
- const [spendRows30, tokenRows30, monthBreakdown, filteredBreakdown, auditEntries, budgetSnapshot] =
169
+
170
+ const budgetSnapshot = await getBudgetGuardrailSnapshot();
171
+ const configuredRuntimeIds = Object.values(budgetSnapshot.runtimeStates)
172
+ .filter((runtime) => runtime.configured)
173
+ .map((runtime) => runtime.runtimeId);
174
+ const requestedRuntime = toScalar(params.runtime);
175
+ const runtimeId =
176
+ requestedRuntime && configuredRuntimeIds.includes(requestedRuntime as never)
177
+ ? requestedRuntime
178
+ : "all";
179
+
180
+ const [spendRows30, tokenRows30, monthBreakdown, filteredBreakdown, auditEntries] =
200
181
  await Promise.all([
201
182
  getDailySpendTotals(30),
202
183
  getDailyTokenTotals(30),
203
184
  getProviderModelBreakdown({ startedAt: startOfCurrentMonth() }),
204
- getProviderModelBreakdown(
205
- rangeStart ? { startedAt: rangeStart } : undefined
206
- ),
185
+ getProviderModelBreakdown(rangeStart ? { startedAt: rangeStart } : undefined),
207
186
  listUsageAuditEntries({
208
187
  limit: 100,
209
- runtimeIds: runtimeId === "all" ? undefined : [runtimeId],
188
+ runtimeIds:
189
+ runtimeId === "all"
190
+ ? configuredRuntimeIds.length > 0
191
+ ? configuredRuntimeIds
192
+ : undefined
193
+ : [runtimeId],
210
194
  statuses: status === "all" ? undefined : [status as UsageLedgerStatus],
211
195
  activityTypes:
212
196
  activityType === "all"
@@ -214,17 +198,38 @@ export default async function CostsPage({
214
198
  : [activityType as UsageActivityType],
215
199
  startedAt: rangeStart,
216
200
  }),
217
- getBudgetGuardrailSnapshot(),
218
201
  ]);
219
202
 
203
+ const configuredBreakdown = filteredBreakdown.filter((row) =>
204
+ configuredRuntimeIds.length > 0
205
+ ? configuredRuntimeIds.includes(row.runtimeId as never)
206
+ : true
207
+ );
208
+ const configuredMonthBreakdown = monthBreakdown.filter((row) =>
209
+ configuredRuntimeIds.length > 0
210
+ ? configuredRuntimeIds.includes(row.runtimeId as never)
211
+ : true
212
+ );
213
+
220
214
  const spendSeries30 = fillSeries(30, spendRows30, (row) => row.costMicros);
221
215
  const tokenSeries30 = fillSeries(30, tokenRows30, (row) => row.totalTokens);
222
- const runtimeBreakdown = buildRuntimeBreakdown(filteredBreakdown);
223
- const monthTokens = monthBreakdown.reduce(
216
+ const runtimeBreakdown = buildRuntimeBreakdown(configuredBreakdown).map((row) => ({
217
+ ...row,
218
+ label: budgetSnapshot.runtimeStates[row.runtimeId as keyof typeof budgetSnapshot.runtimeStates]
219
+ ?.label ?? row.runtimeId,
220
+ }));
221
+ const monthTokens = configuredMonthBreakdown.reduce(
224
222
  (sum, row) => sum + row.totalTokens,
225
223
  0
226
224
  );
227
225
 
226
+ const overallDaily = budgetSnapshot.statuses.find(
227
+ (status) => status.scopeId === "overall" && status.window === "daily"
228
+ );
229
+ const overallMonthly = budgetSnapshot.statuses.find(
230
+ (status) => status.scopeId === "overall" && status.window === "monthly"
231
+ );
232
+
228
233
  return (
229
234
  <div className="gradient-neutral min-h-screen p-6">
230
235
  <CostDashboard
@@ -235,9 +240,12 @@ export default async function CostsPage({
235
240
  activityType,
236
241
  }}
237
242
  summary={{
238
- todaySpendMicros: findOverallSpend(budgetSnapshot.statuses, "daily"),
239
- monthSpendMicros: findOverallSpend(budgetSnapshot.statuses, "monthly"),
240
- todayTokens: tokenSeries30[tokenSeries30.length - 1] ?? 0,
243
+ monthSpendMicros: overallMonthly?.currentValue ?? 0,
244
+ derivedDailyBudgetMicros: overallDaily?.limitValue ?? 0,
245
+ remainingMonthlyHeadroomMicros: Math.max(
246
+ (overallMonthly?.limitValue ?? 0) - (overallMonthly?.currentValue ?? 0),
247
+ 0
248
+ ),
241
249
  monthTokens,
242
250
  }}
243
251
  trendSeries={{
@@ -247,8 +255,10 @@ export default async function CostsPage({
247
255
  tokens30: tokenSeries30,
248
256
  }}
249
257
  budgetStatuses={budgetSnapshot.statuses}
258
+ runtimeStates={budgetSnapshot.runtimeStates}
259
+ pricing={budgetSnapshot.pricing}
250
260
  runtimeBreakdown={runtimeBreakdown}
251
- modelBreakdown={filteredBreakdown}
261
+ modelBreakdown={configuredBreakdown}
252
262
  auditEntries={auditEntries}
253
263
  />
254
264
  </div>
@@ -446,11 +446,6 @@
446
446
  box-shadow: var(--glass-shadow-sm);
447
447
  }
448
448
 
449
- /* Temperature slider gradient track */
450
- .slider-temperature [data-slot="slider-range"] {
451
- background: linear-gradient(90deg, oklch(0.6 0.18 260), oklch(0.7 0.15 55));
452
- }
453
-
454
449
  [data-slot="popover-content"],
455
450
  [data-slot="dropdown-menu-content"],
456
451
  [data-slot="select-content"] {