@stigmer/react 0.0.55 → 0.0.57

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 (81) hide show
  1. package/execution/ApprovalCard.d.ts.map +1 -1
  2. package/execution/ApprovalCard.js +1 -1
  3. package/execution/ApprovalCard.js.map +1 -1
  4. package/execution/ArtifactsWidget.d.ts +1 -1
  5. package/execution/ArtifactsWidget.js +1 -1
  6. package/execution/ExecutionProgress.d.ts.map +1 -1
  7. package/execution/ExecutionProgress.js +2 -59
  8. package/execution/ExecutionProgress.js.map +1 -1
  9. package/execution/MessageThread.d.ts.map +1 -1
  10. package/execution/MessageThread.js +31 -6
  11. package/execution/MessageThread.js.map +1 -1
  12. package/execution/SubAgentSection.d.ts +25 -4
  13. package/execution/SubAgentSection.d.ts.map +1 -1
  14. package/execution/SubAgentSection.js +70 -11
  15. package/execution/SubAgentSection.js.map +1 -1
  16. package/execution/TodoList.d.ts +42 -0
  17. package/execution/TodoList.d.ts.map +1 -0
  18. package/execution/TodoList.js +108 -0
  19. package/execution/TodoList.js.map +1 -0
  20. package/execution/ToolCallItem.js +1 -1
  21. package/execution/ToolCallItem.js.map +1 -1
  22. package/execution/UsageWidget.d.ts +57 -0
  23. package/execution/UsageWidget.d.ts.map +1 -0
  24. package/execution/UsageWidget.js +72 -0
  25. package/execution/UsageWidget.js.map +1 -0
  26. package/execution/index.d.ts +4 -4
  27. package/execution/index.d.ts.map +1 -1
  28. package/execution/index.js +2 -2
  29. package/execution/index.js.map +1 -1
  30. package/execution/useExecutionArtifacts.d.ts +1 -1
  31. package/execution/useExecutionArtifacts.js +1 -1
  32. package/index.d.ts +4 -4
  33. package/index.d.ts.map +1 -1
  34. package/index.js +2 -2
  35. package/index.js.map +1 -1
  36. package/package.json +4 -4
  37. package/session/index.d.ts +2 -0
  38. package/session/index.d.ts.map +1 -1
  39. package/session/index.js +1 -0
  40. package/session/index.js.map +1 -1
  41. package/session/useSessionConversation.d.ts.map +1 -1
  42. package/session/useSessionConversation.js +42 -5
  43. package/session/useSessionConversation.js.map +1 -1
  44. package/session/useSessionUsage.d.ts +65 -0
  45. package/session/useSessionUsage.d.ts.map +1 -0
  46. package/session/useSessionUsage.js +107 -0
  47. package/session/useSessionUsage.js.map +1 -0
  48. package/src/execution/ApprovalCard.tsx +7 -13
  49. package/src/execution/ArtifactsWidget.tsx +1 -1
  50. package/src/execution/ExecutionProgress.tsx +2 -134
  51. package/src/execution/MessageThread.tsx +39 -6
  52. package/src/execution/SubAgentSection.tsx +323 -16
  53. package/src/execution/TodoList.tsx +202 -0
  54. package/src/execution/ToolCallItem.tsx +1 -1
  55. package/src/execution/{ExecutionCostSummary.tsx → UsageWidget.tsx} +43 -50
  56. package/src/execution/index.ts +10 -4
  57. package/src/execution/useExecutionArtifacts.ts +1 -1
  58. package/src/index.ts +12 -5
  59. package/src/session/index.ts +6 -0
  60. package/src/session/useSessionConversation.ts +56 -7
  61. package/src/session/useSessionUsage.ts +159 -0
  62. package/styles.css +1 -1
  63. package/execution/ExecutionCostSummary.d.ts +0 -47
  64. package/execution/ExecutionCostSummary.d.ts.map +0 -1
  65. package/execution/ExecutionCostSummary.js +0 -77
  66. package/execution/ExecutionCostSummary.js.map +0 -1
  67. package/execution/__tests__/ExecutionCostSummary.test.d.ts +0 -2
  68. package/execution/__tests__/ExecutionCostSummary.test.d.ts.map +0 -1
  69. package/execution/__tests__/ExecutionCostSummary.test.js +0 -255
  70. package/execution/__tests__/ExecutionCostSummary.test.js.map +0 -1
  71. package/execution/__tests__/useExecutionUsage.test.d.ts +0 -2
  72. package/execution/__tests__/useExecutionUsage.test.d.ts.map +0 -1
  73. package/execution/__tests__/useExecutionUsage.test.js +0 -303
  74. package/execution/__tests__/useExecutionUsage.test.js.map +0 -1
  75. package/execution/useExecutionUsage.d.ts +0 -45
  76. package/execution/useExecutionUsage.d.ts.map +0 -1
  77. package/execution/useExecutionUsage.js +0 -157
  78. package/execution/useExecutionUsage.js.map +0 -1
  79. package/src/execution/__tests__/ExecutionCostSummary.test.tsx +0 -416
  80. package/src/execution/__tests__/useExecutionUsage.test.tsx +0 -408
  81. package/src/execution/useExecutionUsage.ts +0 -213
@@ -1,157 +0,0 @@
1
- "use client";
2
- import { useMemo } from "react";
3
- import { create } from "@bufbuild/protobuf";
4
- import { UsageMetricsSchema, ModelUsageSchema, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
5
- /**
6
- * Pure derivation hook that aggregates {@link UsageMetrics} across the
7
- * main agent and all sub-agents into a single {@link UsageMetrics} total.
8
- *
9
- * The proto scoping rule is:
10
- * > `status.usage` = main agent's direct LLM usage (excludes sub-agents)
11
- * > `subAgentExecutions[].usage` = each sub-agent's LLM usage
12
- * > Total cost = `status.usage` + sum(`subAgentExecutions[].usage`)
13
- *
14
- * This hook performs that summation, merges `modelBreakdown` entries by
15
- * `model+provider` key, and concatenates `llmCalls` sorted by timestamp.
16
- *
17
- * Returns `null` when the execution is absent or usage data has not yet
18
- * arrived from the agent runner.
19
- *
20
- * @example
21
- * ```tsx
22
- * const { execution } = useExecutionStream(executionId);
23
- * const { usage, hasSubAgentUsage } = useExecutionUsage(execution);
24
- *
25
- * if (usage) {
26
- * console.log(`Cost: $${usage.estimatedCostUsd}`);
27
- * console.log(`Tokens: ${usage.totalTokens}`);
28
- * }
29
- * ```
30
- */
31
- export function useExecutionUsage(execution) {
32
- return useMemo(() => {
33
- const usage = aggregateUsage(execution);
34
- if (!usage) {
35
- return { usage: null, hasSubAgentUsage: false, subAgentUsageCount: 0 };
36
- }
37
- const subAgents = execution?.status?.subAgentExecutions ?? [];
38
- let subAgentUsageCount = 0;
39
- for (const sub of subAgents) {
40
- if (sub.usage)
41
- subAgentUsageCount++;
42
- }
43
- return {
44
- usage,
45
- hasSubAgentUsage: subAgentUsageCount > 0,
46
- subAgentUsageCount,
47
- };
48
- }, [execution]);
49
- }
50
- // ---------------------------------------------------------------------------
51
- // Pure aggregation function — testable without React
52
- // ---------------------------------------------------------------------------
53
- /**
54
- * Aggregates usage metrics from the main agent and all sub-agents into
55
- * a single {@link UsageMetrics} proto object.
56
- *
57
- * Returns `null` when the execution or its usage data is not yet available.
58
- */
59
- export function aggregateUsage(execution) {
60
- const mainUsage = execution?.status?.usage;
61
- if (!mainUsage)
62
- return null;
63
- const subAgents = execution?.status?.subAgentExecutions ?? [];
64
- const subUsages = [];
65
- for (const sub of subAgents) {
66
- if (sub.usage)
67
- subUsages.push(sub.usage);
68
- }
69
- if (subUsages.length === 0)
70
- return mainUsage;
71
- const allUsages = [mainUsage, ...subUsages];
72
- return create(UsageMetricsSchema, {
73
- promptTokens: sumField(allUsages, "promptTokens"),
74
- completionTokens: sumField(allUsages, "completionTokens"),
75
- totalTokens: sumField(allUsages, "totalTokens"),
76
- llmCallCount: sumField(allUsages, "llmCallCount"),
77
- cacheCreationTokens: sumField(allUsages, "cacheCreationTokens"),
78
- cacheReadTokens: sumField(allUsages, "cacheReadTokens"),
79
- estimatedCostUsd: sumField(allUsages, "estimatedCostUsd"),
80
- totalDurationMs: sumField(allUsages, "totalDurationMs"),
81
- llmDurationMs: sumField(allUsages, "llmDurationMs"),
82
- toolDurationMs: sumField(allUsages, "toolDurationMs"),
83
- approvalWaitDurationMs: sumField(allUsages, "approvalWaitDurationMs"),
84
- toolResultCharsTruncated: sumBigIntField(allUsages),
85
- primaryModel: mainUsage.primaryModel,
86
- primaryProvider: mainUsage.primaryProvider,
87
- modelBreakdown: mergeModelBreakdowns(allUsages),
88
- llmCalls: mergeLlmCalls(allUsages),
89
- });
90
- }
91
- function sumField(usages, field) {
92
- let total = 0;
93
- for (const u of usages) {
94
- total += u[field];
95
- }
96
- return total;
97
- }
98
- function sumBigIntField(usages) {
99
- let total = BigInt(0);
100
- for (const u of usages) {
101
- total += u.toolResultCharsTruncated;
102
- }
103
- return total;
104
- }
105
- /**
106
- * Merges model breakdown entries across all usages by `model+provider` key.
107
- * Entries for the same model and provider are combined into a single
108
- * {@link ModelUsage} with summed numeric fields. Pricing rates are taken
109
- * from the first entry encountered for each key (rates are stamped at
110
- * execution time and are identical for the same model).
111
- */
112
- function mergeModelBreakdowns(usages) {
113
- const merged = new Map();
114
- for (const usage of usages) {
115
- for (const entry of usage.modelBreakdown) {
116
- const key = `${entry.model}\0${entry.provider}`;
117
- const existing = merged.get(key);
118
- if (existing) {
119
- existing.inputTokens += entry.inputTokens;
120
- existing.outputTokens += entry.outputTokens;
121
- existing.cacheCreationTokens += entry.cacheCreationTokens;
122
- existing.cacheReadTokens += entry.cacheReadTokens;
123
- existing.callCount += entry.callCount;
124
- existing.estimatedCostUsd += entry.estimatedCostUsd;
125
- }
126
- else {
127
- merged.set(key, {
128
- model: entry.model,
129
- provider: entry.provider,
130
- inputTokens: entry.inputTokens,
131
- outputTokens: entry.outputTokens,
132
- cacheCreationTokens: entry.cacheCreationTokens,
133
- cacheReadTokens: entry.cacheReadTokens,
134
- callCount: entry.callCount,
135
- estimatedCostUsd: entry.estimatedCostUsd,
136
- inputPricePerMillion: entry.inputPricePerMillion,
137
- outputPricePerMillion: entry.outputPricePerMillion,
138
- cacheCreationPricePerMillion: entry.cacheCreationPricePerMillion,
139
- cacheReadPricePerMillion: entry.cacheReadPricePerMillion,
140
- });
141
- }
142
- }
143
- }
144
- return Array.from(merged.values()).map((m) => create(ModelUsageSchema, m));
145
- }
146
- /**
147
- * Concatenates `llmCalls` from all usages, sorted by ISO 8601 timestamp.
148
- * This gives a globally chronological view across main agent and sub-agents,
149
- * since per-agent `sequence` numbers overlap.
150
- */
151
- function mergeLlmCalls(usages) {
152
- const all = usages.flatMap((u) => u.llmCalls);
153
- if (all.length <= 1)
154
- return all;
155
- return all.slice().sort((a, b) => a.timestamp.localeCompare(b.timestamp));
156
- }
157
- //# sourceMappingURL=useExecutionUsage.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useExecutionUsage.js","sourceRoot":"","sources":["../../src/execution/useExecutionUsage.ts"],"names":[],"mappings":"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"}
@@ -1,416 +0,0 @@
1
- import { describe, it, expect, afterEach } from "vitest";
2
- import { render, screen, cleanup } from "@testing-library/react";
3
- import { create } from "@bufbuild/protobuf";
4
-
5
- afterEach(cleanup);
6
- import {
7
- AgentExecutionSchema,
8
- AgentExecutionStatusSchema,
9
- type AgentExecution,
10
- } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
11
- import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
12
- import {
13
- UsageMetricsSchema,
14
- ModelUsageSchema,
15
- type ModelUsage,
16
- } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
17
- import {
18
- ExecutionCostSummary,
19
- formatCost,
20
- formatTokenCount,
21
- } from "../ExecutionCostSummary";
22
-
23
- // ---------------------------------------------------------------------------
24
- // Test helpers
25
- // ---------------------------------------------------------------------------
26
-
27
- function makeUsage(
28
- overrides: Partial<{
29
- promptTokens: number;
30
- completionTokens: number;
31
- totalTokens: number;
32
- llmCallCount: number;
33
- estimatedCostUsd: number;
34
- primaryModel: string;
35
- primaryProvider: string;
36
- cacheCreationTokens: number;
37
- cacheReadTokens: number;
38
- }> = {},
39
- ) {
40
- return create(UsageMetricsSchema, overrides);
41
- }
42
-
43
- function makeUsageWithModels(
44
- overrides: {
45
- promptTokens?: number;
46
- completionTokens?: number;
47
- totalTokens?: number;
48
- llmCallCount?: number;
49
- estimatedCostUsd?: number;
50
- primaryModel?: string;
51
- primaryProvider?: string;
52
- cacheCreationTokens?: number;
53
- cacheReadTokens?: number;
54
- modelBreakdown?: ModelUsage[];
55
- } = {},
56
- ) {
57
- return create(UsageMetricsSchema, overrides);
58
- }
59
-
60
- function makeModelUsage(
61
- model: string,
62
- provider: string,
63
- overrides: Partial<{
64
- inputTokens: number;
65
- outputTokens: number;
66
- callCount: number;
67
- estimatedCostUsd: number;
68
- }> = {},
69
- ) {
70
- return create(ModelUsageSchema, { model, provider, ...overrides });
71
- }
72
-
73
- function makeExecution(
74
- mainUsage?: ReturnType<typeof makeUsage>,
75
- subAgents: Array<{
76
- name: string;
77
- usage?: ReturnType<typeof makeUsage>;
78
- }> = [],
79
- ): AgentExecution {
80
- const status = create(AgentExecutionStatusSchema, {
81
- usage: mainUsage,
82
- subAgentExecutions: subAgents.map((s) =>
83
- create(SubAgentExecutionSchema, { name: s.name, usage: s.usage }),
84
- ),
85
- });
86
- return create(AgentExecutionSchema, { status });
87
- }
88
-
89
- // ---------------------------------------------------------------------------
90
- // formatCost
91
- // ---------------------------------------------------------------------------
92
-
93
- describe("formatCost", () => {
94
- it("formats zero as $0.00", () => {
95
- expect(formatCost(0)).toBe("$0.00");
96
- });
97
-
98
- it("formats sub-dollar with 4 decimal places", () => {
99
- expect(formatCost(0.0042)).toBe("$0.0042");
100
- });
101
-
102
- it("formats sub-dollar with trailing zeros", () => {
103
- expect(formatCost(0.15)).toBe("$0.1500");
104
- });
105
-
106
- it("formats $1+ with 2 decimal places", () => {
107
- expect(formatCost(1.5)).toBe("$1.50");
108
- });
109
-
110
- it("rounds $1+ to 2 decimal places", () => {
111
- expect(formatCost(123.456)).toBe("$123.46");
112
- });
113
-
114
- it("formats exactly $1 with 2 decimal places", () => {
115
- expect(formatCost(1)).toBe("$1.00");
116
- });
117
- });
118
-
119
- // ---------------------------------------------------------------------------
120
- // formatTokenCount
121
- // ---------------------------------------------------------------------------
122
-
123
- describe("formatTokenCount", () => {
124
- it("formats zero", () => {
125
- expect(formatTokenCount(0)).toBe("0");
126
- });
127
-
128
- it("formats small numbers without commas", () => {
129
- expect(formatTokenCount(999)).toBe("999");
130
- });
131
-
132
- it("formats thousands with commas", () => {
133
- expect(formatTokenCount(1234)).toBe("1,234");
134
- });
135
-
136
- it("formats millions with commas", () => {
137
- expect(formatTokenCount(1234567)).toBe("1,234,567");
138
- });
139
- });
140
-
141
- // ---------------------------------------------------------------------------
142
- // ExecutionCostSummary — component rendering
143
- // ---------------------------------------------------------------------------
144
-
145
- describe("ExecutionCostSummary", () => {
146
- it("returns null when execution is null", () => {
147
- const { container } = render(
148
- <ExecutionCostSummary execution={null} />,
149
- );
150
- expect(container.innerHTML).toBe("");
151
- });
152
-
153
- it("returns null when usage is not yet available", () => {
154
- const execution = create(AgentExecutionSchema, {
155
- status: create(AgentExecutionStatusSchema),
156
- });
157
- const { container } = render(
158
- <ExecutionCostSummary execution={execution} />,
159
- );
160
- expect(container.innerHTML).toBe("");
161
- });
162
-
163
- it("renders cost, tokens, calls, and model for a basic execution", () => {
164
- const execution = makeExecution(
165
- makeUsage({
166
- estimatedCostUsd: 0.0042,
167
- totalTokens: 1234,
168
- promptTokens: 1000,
169
- completionTokens: 234,
170
- llmCallCount: 3,
171
- primaryModel: "claude-sonnet-4",
172
- primaryProvider: "anthropic",
173
- }),
174
- );
175
-
176
- render(<ExecutionCostSummary execution={execution} />);
177
-
178
- expect(screen.getByText("$0.0042")).toBeTruthy();
179
- expect(screen.getByText("claude-sonnet-4 · anthropic")).toBeTruthy();
180
- expect(screen.getByText(/1,234 tokens/)).toBeTruthy();
181
- expect(screen.getByText(/3 calls/)).toBeTruthy();
182
- });
183
-
184
- it("renders token breakdown with prompt and completion", () => {
185
- const execution = makeExecution(
186
- makeUsage({
187
- totalTokens: 1234,
188
- promptTokens: 1000,
189
- completionTokens: 234,
190
- llmCallCount: 1,
191
- }),
192
- );
193
-
194
- render(<ExecutionCostSummary execution={execution} />);
195
-
196
- expect(screen.getByText(/prompt 1,000/)).toBeTruthy();
197
- expect(screen.getByText(/completion 234/)).toBeTruthy();
198
- });
199
-
200
- it("uses singular 'call' for a single LLM call", () => {
201
- const execution = makeExecution(
202
- makeUsage({
203
- llmCallCount: 1,
204
- totalTokens: 100,
205
- promptTokens: 80,
206
- completionTokens: 20,
207
- }),
208
- );
209
-
210
- render(<ExecutionCostSummary execution={execution} />);
211
-
212
- const metricsLine = screen.getByText(/100 tokens/);
213
- expect(metricsLine.textContent).toContain("1 call");
214
- expect(metricsLine.textContent).not.toContain("1 calls");
215
- });
216
-
217
- it("does not show cache line when cache tokens are zero", () => {
218
- const execution = makeExecution(
219
- makeUsage({
220
- totalTokens: 100,
221
- promptTokens: 80,
222
- completionTokens: 20,
223
- }),
224
- );
225
-
226
- const { container } = render(
227
- <ExecutionCostSummary execution={execution} />,
228
- );
229
- expect(container.textContent).not.toContain("cache");
230
- });
231
-
232
- it("shows cache read tokens when present", () => {
233
- const execution = makeExecution(
234
- makeUsage({
235
- totalTokens: 100,
236
- promptTokens: 80,
237
- completionTokens: 20,
238
- cacheReadTokens: 500,
239
- }),
240
- );
241
-
242
- const { container } = render(
243
- <ExecutionCostSummary execution={execution} />,
244
- );
245
- expect(container.textContent).toContain("cache");
246
- expect(container.textContent).toContain("500 read");
247
- expect(container.textContent).not.toContain("write");
248
- });
249
-
250
- it("shows cache write tokens when present", () => {
251
- const execution = makeExecution(
252
- makeUsage({
253
- totalTokens: 100,
254
- promptTokens: 80,
255
- completionTokens: 20,
256
- cacheCreationTokens: 200,
257
- }),
258
- );
259
-
260
- const { container } = render(
261
- <ExecutionCostSummary execution={execution} />,
262
- );
263
- expect(container.textContent).toContain("cache");
264
- expect(container.textContent).toContain("200 write");
265
- expect(container.textContent).not.toContain("read");
266
- });
267
-
268
- it("shows both cache read and write when both present", () => {
269
- const execution = makeExecution(
270
- makeUsage({
271
- totalTokens: 100,
272
- promptTokens: 80,
273
- completionTokens: 20,
274
- cacheReadTokens: 500,
275
- cacheCreationTokens: 100,
276
- }),
277
- );
278
-
279
- const { container } = render(
280
- <ExecutionCostSummary execution={execution} />,
281
- );
282
- expect(container.textContent).toContain("500 read");
283
- expect(container.textContent).toContain("100 write");
284
- });
285
-
286
- it("shows sub-agent annotation when sub-agents have usage", () => {
287
- const execution = makeExecution(
288
- makeUsage({
289
- totalTokens: 1000,
290
- promptTokens: 800,
291
- completionTokens: 200,
292
- }),
293
- [
294
- { name: "researcher", usage: makeUsage({ totalTokens: 500 }) },
295
- { name: "writer", usage: makeUsage({ totalTokens: 300 }) },
296
- ],
297
- );
298
-
299
- render(<ExecutionCostSummary execution={execution} />);
300
- expect(screen.getByText("Includes 2 sub-agents")).toBeTruthy();
301
- });
302
-
303
- it("uses singular 'sub-agent' for one sub-agent", () => {
304
- const execution = makeExecution(
305
- makeUsage({
306
- totalTokens: 1000,
307
- promptTokens: 800,
308
- completionTokens: 200,
309
- }),
310
- [{ name: "researcher", usage: makeUsage({ totalTokens: 500 }) }],
311
- );
312
-
313
- render(<ExecutionCostSummary execution={execution} />);
314
- expect(screen.getByText("Includes 1 sub-agent")).toBeTruthy();
315
- });
316
-
317
- it("does not show sub-agent annotation when no sub-agents have usage", () => {
318
- const execution = makeExecution(
319
- makeUsage({
320
- totalTokens: 1000,
321
- promptTokens: 800,
322
- completionTokens: 200,
323
- }),
324
- [{ name: "idle-agent" }],
325
- );
326
-
327
- const { container } = render(
328
- <ExecutionCostSummary execution={execution} />,
329
- );
330
- expect(container.textContent).not.toContain("sub-agent");
331
- });
332
-
333
- it("shows per-model breakdown when multiple models exist", () => {
334
- const execution = makeExecution(
335
- makeUsageWithModels({
336
- totalTokens: 2000,
337
- promptTokens: 1500,
338
- completionTokens: 500,
339
- estimatedCostUsd: 0.0142,
340
- primaryModel: "claude-sonnet-4",
341
- primaryProvider: "anthropic",
342
- modelBreakdown: [
343
- makeModelUsage("claude-sonnet-4", "anthropic", {
344
- estimatedCostUsd: 0.012,
345
- }),
346
- makeModelUsage("gpt-4o", "openai", {
347
- estimatedCostUsd: 0.0022,
348
- }),
349
- ],
350
- }),
351
- );
352
-
353
- render(<ExecutionCostSummary execution={execution} />);
354
-
355
- expect(screen.getByText("claude-sonnet-4")).toBeTruthy();
356
- expect(screen.getByText("$0.0120")).toBeTruthy();
357
- expect(screen.getByText("gpt-4o")).toBeTruthy();
358
- expect(screen.getByText("$0.0022")).toBeTruthy();
359
- expect(
360
- screen.queryByText("claude-sonnet-4 · anthropic"),
361
- ).toBeNull();
362
- });
363
-
364
- it("shows model breakdown list with proper accessibility", () => {
365
- const execution = makeExecution(
366
- makeUsageWithModels({
367
- totalTokens: 2000,
368
- promptTokens: 1500,
369
- completionTokens: 500,
370
- modelBreakdown: [
371
- makeModelUsage("model-a", "provider-a"),
372
- makeModelUsage("model-b", "provider-b"),
373
- ],
374
- }),
375
- );
376
-
377
- render(<ExecutionCostSummary execution={execution} />);
378
-
379
- expect(
380
- screen.getByRole("list", { name: "Model cost breakdown" }),
381
- ).toBeTruthy();
382
- });
383
-
384
- it("has proper region role and aria-label", () => {
385
- const execution = makeExecution(
386
- makeUsage({
387
- totalTokens: 100,
388
- promptTokens: 80,
389
- completionTokens: 20,
390
- }),
391
- );
392
-
393
- render(<ExecutionCostSummary execution={execution} />);
394
-
395
- expect(
396
- screen.getByRole("region", { name: "Execution cost summary" }),
397
- ).toBeTruthy();
398
- });
399
-
400
- it("accepts and applies className", () => {
401
- const execution = makeExecution(
402
- makeUsage({
403
- totalTokens: 100,
404
- promptTokens: 80,
405
- completionTokens: 20,
406
- }),
407
- );
408
-
409
- render(
410
- <ExecutionCostSummary execution={execution} className="my-custom" />,
411
- );
412
-
413
- const region = screen.getByRole("region");
414
- expect(region.className).toContain("my-custom");
415
- });
416
- });