@sentry/junior-dashboard 0.247.0 → 0.248.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.
- package/dist/app.js +69 -15
- package/dist/client.js +50 -50
- package/dist/index.js +69 -15
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/client/components/Metric.tsx +8 -13
- package/src/client/conversations/ConversationChildAgents.tsx +6 -0
- package/src/client/conversations/ConversationMeta.tsx +20 -1
- package/src/client/conversations/ConversationPage.tsx +11 -4
- package/src/client/conversations/ConversationUsageBreakdown.tsx +278 -0
- package/src/client/conversations/TelemetryMetrics.tsx +327 -180
- package/src/client/conversations/UsageCard.tsx +172 -0
- package/src/client/format.ts +1 -0
- package/src/client/types.ts +1 -0
- package/src/mock-reporting/fixtures.ts +73 -14
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ConversationAuxiliaryCosts,
|
|
3
|
+
ConversationChildAgentTotals,
|
|
3
4
|
ConversationModelUsage,
|
|
4
5
|
} from "@sentry/junior/api/schema";
|
|
5
6
|
import {
|
|
6
|
-
formatCostBreakdown,
|
|
7
7
|
formatCostSummary,
|
|
8
8
|
formatCompactNumber,
|
|
9
|
+
formatRuntime,
|
|
9
10
|
formatTime,
|
|
10
11
|
formatTokenSummary,
|
|
11
12
|
summarizeCost,
|
|
@@ -19,6 +20,13 @@ import {
|
|
|
19
20
|
} from "../format";
|
|
20
21
|
import { MetricValue, type MetricTooltipLine } from "../components/Metric";
|
|
21
22
|
import { ShimmerText } from "../components/ShimmerText";
|
|
23
|
+
import {
|
|
24
|
+
UsageCard,
|
|
25
|
+
type UsageCardProps,
|
|
26
|
+
type UsageCardRow,
|
|
27
|
+
type UsageCardSection,
|
|
28
|
+
type UsageCardSplit,
|
|
29
|
+
} from "./UsageCard";
|
|
22
30
|
|
|
23
31
|
function plural(label: string, count: number): string {
|
|
24
32
|
return `${formatCompactNumber(count)} ${label}${count === 1 ? "" : "s"}`;
|
|
@@ -30,44 +38,66 @@ function isMetricTooltipLine(
|
|
|
30
38
|
return Boolean(line);
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
/** List the recorded token types, such as input and output. */
|
|
42
|
+
export function tokenTypeRows(summary: TokenUsageSummary): UsageCardRow[] {
|
|
43
|
+
return (
|
|
44
|
+
[
|
|
45
|
+
["input", summary.inputTokens],
|
|
46
|
+
["output", summary.outputTokens],
|
|
47
|
+
["cached", summary.cachedInputTokens],
|
|
48
|
+
["cache write", summary.cacheCreationTokens],
|
|
49
|
+
["reasoning", summary.reasoningTokens],
|
|
50
|
+
] as const
|
|
51
|
+
).flatMap(([label, value]) =>
|
|
52
|
+
value ? [{ label, value: formatCompactNumber(value) }] : [],
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Add up the tokens and cost of all subagents, including auxiliary cost. */
|
|
57
|
+
export function subagentUsage(
|
|
58
|
+
childAgentTotals: ConversationChildAgentTotals | undefined,
|
|
59
|
+
): { cost: number; tokens: number } {
|
|
60
|
+
return {
|
|
61
|
+
cost:
|
|
62
|
+
(summarizeCost(childAgentTotals?.cumulativeUsage)?.total ?? 0) +
|
|
63
|
+
(childAgentTotals?.auxiliaryCostUsd ?? 0),
|
|
64
|
+
tokens: summarizeUsage(childAgentTotals?.cumulativeUsage)?.totalTokens ?? 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Cap the subagent part of a total to the total. Saved subagent usage can
|
|
70
|
+
* run ahead of a total built from model calls, such as when a child's
|
|
71
|
+
* transcript is gone.
|
|
72
|
+
*/
|
|
73
|
+
export function subagentShare(part: number, total: number): number {
|
|
74
|
+
return Math.min(Math.max(0, part), Math.max(0, total));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Split a total that includes child agents into the part of this agent and the
|
|
79
|
+
* part of its subagents. Return no lines when there are no subagents.
|
|
80
|
+
*/
|
|
81
|
+
function agentSplitLines(
|
|
82
|
+
childAgentTotals: ConversationChildAgentTotals | undefined,
|
|
83
|
+
total: number,
|
|
84
|
+
subagentPart: number,
|
|
85
|
+
format: (value: number) => string,
|
|
86
|
+
): MetricTooltipLine[] {
|
|
87
|
+
if (!childAgentTotals) return [];
|
|
88
|
+
const subagents = subagentShare(subagentPart, total);
|
|
36
89
|
return [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
summary.cachedInputTokens !== undefined
|
|
44
|
-
? {
|
|
45
|
-
label: "cached",
|
|
46
|
-
value: formatCompactNumber(summary.cachedInputTokens),
|
|
47
|
-
}
|
|
48
|
-
: undefined,
|
|
49
|
-
summary.cacheCreationTokens !== undefined
|
|
50
|
-
? {
|
|
51
|
-
label: "cache write",
|
|
52
|
-
value: formatCompactNumber(summary.cacheCreationTokens),
|
|
53
|
-
}
|
|
54
|
-
: undefined,
|
|
55
|
-
summary.reasoningTokens !== undefined
|
|
56
|
-
? {
|
|
57
|
-
label: "reasoning",
|
|
58
|
-
value: formatCompactNumber(summary.reasoningTokens),
|
|
59
|
-
}
|
|
60
|
-
: undefined,
|
|
61
|
-
summary.providerTotalTokens !== undefined
|
|
62
|
-
? {
|
|
63
|
-
label: "provider",
|
|
64
|
-
value: formatCompactNumber(summary.providerTotalTokens),
|
|
65
|
-
}
|
|
66
|
-
: undefined,
|
|
90
|
+
{ value: "By agent", valueStyle: "heading" },
|
|
91
|
+
{ label: "this agent", value: format(total - subagents) },
|
|
92
|
+
{
|
|
93
|
+
label: plural("subagent", childAgentTotals.count),
|
|
94
|
+
value: format(subagents),
|
|
95
|
+
},
|
|
67
96
|
];
|
|
68
97
|
}
|
|
69
98
|
|
|
70
|
-
|
|
99
|
+
/** Drop the provider prefix from a model id, such as `openai/gpt-5`. */
|
|
100
|
+
export function modelLabel(modelId: string): string {
|
|
71
101
|
return modelId.split("/").at(-1) ?? modelId;
|
|
72
102
|
}
|
|
73
103
|
|
|
@@ -82,158 +112,227 @@ function modelHeading(
|
|
|
82
112
|
};
|
|
83
113
|
}
|
|
84
114
|
|
|
85
|
-
|
|
115
|
+
/** Rows shown before the rest of a list rolls into one "N more" line. */
|
|
116
|
+
const MODEL_LIMIT = 4;
|
|
117
|
+
const AUXILIARY_LIMIT = 3;
|
|
118
|
+
const DETAILS_FOOTER = "Full breakdown in Details";
|
|
119
|
+
|
|
120
|
+
type RankedRow = UsageCardRow & { amount: number };
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Keep the largest rows and roll the rest into one "N more" line. A single
|
|
124
|
+
* extra row shows as itself, since "1 more" saves no space.
|
|
125
|
+
*/
|
|
126
|
+
function capRows(
|
|
127
|
+
rows: RankedRow[],
|
|
128
|
+
limit: number,
|
|
129
|
+
format: (value: number) => string,
|
|
130
|
+
): Pick<UsageCardSection, "more" | "rows"> {
|
|
131
|
+
if (rows.length <= limit + 1) return { rows };
|
|
132
|
+
const rest = rows.slice(limit);
|
|
133
|
+
return {
|
|
134
|
+
more: {
|
|
135
|
+
count: rest.length,
|
|
136
|
+
value: format(rest.reduce((sum, row) => sum + row.amount, 0)),
|
|
137
|
+
},
|
|
138
|
+
rows: rows.slice(0, limit),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* One row per model, largest first. The live model leads the list before it
|
|
144
|
+
* reports usage.
|
|
145
|
+
*/
|
|
146
|
+
function modelRows<Summary>(
|
|
147
|
+
modelUsage: ConversationModelUsage[] | undefined,
|
|
148
|
+
liveModelId: string | undefined,
|
|
149
|
+
summarize: (usage: ConversationModelUsage["usage"]) => Summary | undefined,
|
|
150
|
+
size: (summary: Summary) => number,
|
|
151
|
+
format: (value: number) => string,
|
|
152
|
+
): RankedRow[] {
|
|
153
|
+
const rows = (modelUsage ?? [])
|
|
154
|
+
.flatMap((item) => {
|
|
155
|
+
const summary = summarize(item.usage);
|
|
156
|
+
return summary ? [{ amount: size(summary), modelId: item.modelId }] : [];
|
|
157
|
+
})
|
|
158
|
+
.sort((a, b) => b.amount - a.amount)
|
|
159
|
+
.map((item) => ({
|
|
160
|
+
amount: item.amount,
|
|
161
|
+
code: true,
|
|
162
|
+
label: modelLabel(item.modelId),
|
|
163
|
+
live: item.modelId === liveModelId,
|
|
164
|
+
value: format(item.amount),
|
|
165
|
+
}));
|
|
166
|
+
if (liveModelId && !rows.some((row) => row.live)) {
|
|
167
|
+
rows.unshift({
|
|
168
|
+
amount: 0,
|
|
169
|
+
code: true,
|
|
170
|
+
label: modelLabel(liveModelId),
|
|
171
|
+
live: true,
|
|
172
|
+
value: "…",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return rows;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Split a total that includes subagents into this agent's part and theirs. */
|
|
179
|
+
function agentSplit(
|
|
180
|
+
childAgentTotals: ConversationChildAgentTotals | undefined,
|
|
181
|
+
total: number,
|
|
182
|
+
subagentPart: number,
|
|
183
|
+
format: (value: number) => string,
|
|
184
|
+
): UsageCardSplit | undefined {
|
|
185
|
+
if (!childAgentTotals) return undefined;
|
|
186
|
+
const subagents = subagentShare(subagentPart, total);
|
|
187
|
+
return {
|
|
188
|
+
count: childAgentTotals.count,
|
|
189
|
+
self: format(total - subagents),
|
|
190
|
+
selfShare: total > 0 ? (total - subagents) / total : 1,
|
|
191
|
+
subagents: format(subagents),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function usdFormat(digits: number): Intl.NumberFormat {
|
|
196
|
+
return new Intl.NumberFormat("en-US", {
|
|
197
|
+
currency: "USD",
|
|
198
|
+
maximumFractionDigits: digits,
|
|
199
|
+
minimumFractionDigits: digits,
|
|
200
|
+
style: "currency",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Format every amount on one cost card with the same precision, so the
|
|
206
|
+
* decimal points line up down the column. An amount too small for that
|
|
207
|
+
* precision keeps 4 digits instead of showing as zero.
|
|
208
|
+
*/
|
|
209
|
+
export function costFormatter(total: number): (value: number) => string {
|
|
210
|
+
const digits = total >= 10 ? 2 : total >= 1 ? 3 : 4;
|
|
211
|
+
const format = usdFormat(digits);
|
|
212
|
+
const smallFormat = usdFormat(4);
|
|
213
|
+
return (value) =>
|
|
214
|
+
value > 0 && value < 0.5 * 10 ** -digits
|
|
215
|
+
? smallFormat.format(value)
|
|
216
|
+
: format.format(value);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function tokenCard(
|
|
86
220
|
summary: TokenUsageSummary,
|
|
87
221
|
modelUsage: ConversationModelUsage[] | undefined,
|
|
88
222
|
compactionCount: number | undefined,
|
|
89
223
|
liveModelId: string | undefined,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
224
|
+
childAgentTotals: ConversationChildAgentTotals | undefined,
|
|
225
|
+
detailsHint: boolean,
|
|
226
|
+
): UsageCardProps {
|
|
227
|
+
const models = modelRows(
|
|
228
|
+
modelUsage,
|
|
229
|
+
liveModelId,
|
|
230
|
+
summarizeUsage,
|
|
231
|
+
(model) => model.totalTokens,
|
|
232
|
+
formatCompactNumber,
|
|
233
|
+
);
|
|
234
|
+
return {
|
|
235
|
+
footer: detailsHint && models.length ? DETAILS_FOOTER : undefined,
|
|
236
|
+
note: compactionCount ? plural("compaction", compactionCount) : undefined,
|
|
237
|
+
sections: models.length
|
|
238
|
+
? [
|
|
239
|
+
{
|
|
240
|
+
title: "By model",
|
|
241
|
+
...capRows(models, MODEL_LIMIT, formatCompactNumber),
|
|
242
|
+
},
|
|
243
|
+
]
|
|
244
|
+
: [{ rows: tokenTypeRows(summary), title: "By type" }].filter(
|
|
245
|
+
(section) => section.rows.length,
|
|
246
|
+
),
|
|
247
|
+
split: agentSplit(
|
|
248
|
+
childAgentTotals,
|
|
249
|
+
summary.totalTokens,
|
|
250
|
+
subagentUsage(childAgentTotals).tokens,
|
|
251
|
+
formatCompactNumber,
|
|
252
|
+
),
|
|
253
|
+
title: "Tokens",
|
|
254
|
+
total: formatCompactNumber(summary.totalTokens),
|
|
255
|
+
};
|
|
115
256
|
}
|
|
116
257
|
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
: undefined,
|
|
132
|
-
summary.cacheRead !== undefined
|
|
133
|
-
? {
|
|
134
|
-
label: "cache read",
|
|
135
|
-
value: formatCostBreakdown({ total: summary.cacheRead }),
|
|
136
|
-
}
|
|
137
|
-
: undefined,
|
|
138
|
-
summary.cacheWrite !== undefined
|
|
139
|
-
? {
|
|
140
|
-
label: "cache write",
|
|
141
|
-
value: formatCostBreakdown({ total: summary.cacheWrite }),
|
|
142
|
-
}
|
|
143
|
-
: undefined,
|
|
144
|
-
];
|
|
145
|
-
return lines.filter(isMetricTooltipLine);
|
|
258
|
+
function costTypeRows(
|
|
259
|
+
summary: CostUsageSummary,
|
|
260
|
+
format: (value: number) => string,
|
|
261
|
+
): UsageCardRow[] {
|
|
262
|
+
return (
|
|
263
|
+
[
|
|
264
|
+
["input", summary.input],
|
|
265
|
+
["output", summary.output],
|
|
266
|
+
["cache read", summary.cacheRead],
|
|
267
|
+
["cache write", summary.cacheWrite],
|
|
268
|
+
] as const
|
|
269
|
+
).flatMap(([label, value]) =>
|
|
270
|
+
value === undefined ? [] : [{ label, value: format(value) }],
|
|
271
|
+
);
|
|
146
272
|
}
|
|
147
273
|
|
|
148
|
-
|
|
274
|
+
/** Summarize cost. Return nothing until there is a cost to show. */
|
|
275
|
+
function costCard(
|
|
149
276
|
summary: CostUsageSummary | undefined,
|
|
150
277
|
modelUsage: ConversationModelUsage[] | undefined,
|
|
151
278
|
auxiliaryCosts: ConversationAuxiliaryCosts | undefined,
|
|
152
279
|
liveModelId: string | undefined,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
} {
|
|
280
|
+
childAgentTotals: ConversationChildAgentTotals | undefined,
|
|
281
|
+
detailsHint: boolean,
|
|
282
|
+
): UsageCardProps | undefined {
|
|
157
283
|
const total = totalConversationCost(summary, auxiliaryCosts);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
tooltip: [
|
|
180
|
-
...modelSummaries.flatMap((item) => [
|
|
181
|
-
modelHeading(item.modelId, liveModelId),
|
|
182
|
-
...costTooltipLines(item.summary).map((line) => ({
|
|
183
|
-
...line,
|
|
184
|
-
label: `• ${line.label}`,
|
|
185
|
-
})),
|
|
186
|
-
]),
|
|
187
|
-
...pendingLines,
|
|
188
|
-
],
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const conversationLines: MetricTooltipLine[] = [
|
|
193
|
-
{ value: "Conversation", valueStyle: "heading" },
|
|
194
|
-
{ label: "total", value: formatCostBreakdown(total) },
|
|
195
|
-
];
|
|
196
|
-
if (summary) {
|
|
197
|
-
conversationLines.push({
|
|
198
|
-
label: "agent",
|
|
199
|
-
value: formatCostBreakdown(summary),
|
|
284
|
+
if (!total) return undefined;
|
|
285
|
+
const format = costFormatter(total.total);
|
|
286
|
+
const models = modelRows(
|
|
287
|
+
modelUsage,
|
|
288
|
+
liveModelId,
|
|
289
|
+
summarizeCost,
|
|
290
|
+
(model) => model.total,
|
|
291
|
+
format,
|
|
292
|
+
);
|
|
293
|
+
const operations: RankedRow[] = [...(auxiliaryCosts?.operations ?? [])]
|
|
294
|
+
.sort((a, b) => b.costUsd - a.costUsd)
|
|
295
|
+
.map((operation) => ({
|
|
296
|
+
amount: operation.costUsd,
|
|
297
|
+
label: auxiliaryOperationLabel(operation),
|
|
298
|
+
value: format(operation.costUsd),
|
|
299
|
+
}));
|
|
300
|
+
const sections: UsageCardSection[] = [];
|
|
301
|
+
if (models.length) {
|
|
302
|
+
sections.push({
|
|
303
|
+
title: "By model",
|
|
304
|
+
...capRows(models, MODEL_LIMIT, format),
|
|
200
305
|
});
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.filter((line) => line.label !== "total")
|
|
205
|
-
.map((line) => ({ ...line, label: `• ${line.label}` })),
|
|
206
|
-
);
|
|
207
|
-
} else {
|
|
208
|
-
for (const item of modelSummaries) {
|
|
209
|
-
conversationLines.push(
|
|
210
|
-
modelHeading(item.modelId, liveModelId),
|
|
211
|
-
...costTooltipLines(item.summary).map((line) => ({
|
|
212
|
-
...line,
|
|
213
|
-
label: `• ${line.label}`,
|
|
214
|
-
})),
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
306
|
+
} else if (summary) {
|
|
307
|
+
const rows = costTypeRows(summary, format);
|
|
308
|
+
if (rows.length) sections.push({ rows, title: "By type" });
|
|
218
309
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
value: formatCostBreakdown({ total: auxiliaryCosts.costUsd }),
|
|
225
|
-
},
|
|
226
|
-
];
|
|
227
|
-
for (const operation of auxiliaryCosts.operations) {
|
|
228
|
-
auxiliaryLines.push({
|
|
229
|
-
label: `${auxiliaryOperationLabel(operation)} (${formatCompactNumber(operation.events)})`,
|
|
230
|
-
value: formatCostBreakdown({ total: operation.costUsd }),
|
|
310
|
+
if (auxiliaryCosts && operations.length) {
|
|
311
|
+
sections.push({
|
|
312
|
+
title: "Auxiliary",
|
|
313
|
+
total: format(auxiliaryCosts.costUsd),
|
|
314
|
+
...capRows(operations, AUXILIARY_LIMIT, format),
|
|
231
315
|
});
|
|
232
316
|
}
|
|
233
|
-
return {
|
|
317
|
+
return {
|
|
318
|
+
footer:
|
|
319
|
+
detailsHint && (models.length || operations.length)
|
|
320
|
+
? DETAILS_FOOTER
|
|
321
|
+
: undefined,
|
|
322
|
+
sections,
|
|
323
|
+
split: agentSplit(
|
|
324
|
+
childAgentTotals,
|
|
325
|
+
total.total,
|
|
326
|
+
subagentUsage(childAgentTotals).cost,
|
|
327
|
+
format,
|
|
328
|
+
),
|
|
329
|
+
title: "Estimated cost",
|
|
330
|
+
total: format(total.total),
|
|
331
|
+
};
|
|
234
332
|
}
|
|
235
333
|
|
|
236
|
-
|
|
334
|
+
/** Name an auxiliary operation, such as memory recall or turn routing. */
|
|
335
|
+
export function auxiliaryOperationLabel(
|
|
237
336
|
operation: ConversationAuxiliaryCosts["operations"][number],
|
|
238
337
|
): string {
|
|
239
338
|
const knownLabels: Readonly<Record<string, string>> = {
|
|
@@ -254,6 +353,9 @@ function auxiliaryOperationLabel(
|
|
|
254
353
|
export function CostMetric(props: {
|
|
255
354
|
align?: "left" | "right";
|
|
256
355
|
auxiliaryCosts?: ConversationAuxiliaryCosts;
|
|
356
|
+
childAgentTotals?: ConversationChildAgentTotals;
|
|
357
|
+
/** Point to the Details drawer, which has the full breakdown. */
|
|
358
|
+
detailsHint?: boolean;
|
|
257
359
|
live?: boolean;
|
|
258
360
|
liveModelId?: string;
|
|
259
361
|
modelUsage?: ConversationModelUsage[];
|
|
@@ -267,16 +369,24 @@ export function CostMetric(props: {
|
|
|
267
369
|
const label = total
|
|
268
370
|
? `${formatCostSummary(total)}${pending ? "+" : ""}`
|
|
269
371
|
: "$…";
|
|
372
|
+
const card = costCard(
|
|
373
|
+
summary,
|
|
374
|
+
props.modelUsage,
|
|
375
|
+
props.auxiliaryCosts,
|
|
376
|
+
props.liveModelId,
|
|
377
|
+
props.childAgentTotals,
|
|
378
|
+
Boolean(props.detailsHint),
|
|
379
|
+
);
|
|
270
380
|
return (
|
|
271
381
|
<MetricValue
|
|
272
382
|
align={props.align}
|
|
273
383
|
tooltipPlacement="above"
|
|
274
|
-
{
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
384
|
+
tooltip={
|
|
385
|
+
!card && props.liveModelId
|
|
386
|
+
? [modelHeading(props.liveModelId, props.liveModelId)]
|
|
387
|
+
: undefined
|
|
388
|
+
}
|
|
389
|
+
tooltipCard={card ? <UsageCard {...card} /> : undefined}
|
|
280
390
|
>
|
|
281
391
|
<ShimmerText active={pending}>{label}</ShimmerText>
|
|
282
392
|
</MetricValue>
|
|
@@ -286,7 +396,10 @@ export function CostMetric(props: {
|
|
|
286
396
|
/** Render total token usage with a hoverable breakdown. */
|
|
287
397
|
export function TokenMetric(props: {
|
|
288
398
|
align?: "left" | "right";
|
|
399
|
+
childAgentTotals?: ConversationChildAgentTotals;
|
|
289
400
|
compactionCount?: number;
|
|
401
|
+
/** Point to the Details drawer, which has the full breakdown. */
|
|
402
|
+
detailsHint?: boolean;
|
|
290
403
|
live?: boolean;
|
|
291
404
|
liveModelId?: string;
|
|
292
405
|
modelUsage?: ConversationModelUsage[];
|
|
@@ -296,12 +409,18 @@ export function TokenMetric(props: {
|
|
|
296
409
|
return (
|
|
297
410
|
<MetricValue
|
|
298
411
|
align={props.align}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
412
|
+
tooltipCard={
|
|
413
|
+
<UsageCard
|
|
414
|
+
{...tokenCard(
|
|
415
|
+
props.summary,
|
|
416
|
+
props.modelUsage,
|
|
417
|
+
props.compactionCount,
|
|
418
|
+
props.liveModelId,
|
|
419
|
+
props.childAgentTotals,
|
|
420
|
+
Boolean(props.detailsHint),
|
|
421
|
+
)}
|
|
422
|
+
/>
|
|
423
|
+
}
|
|
305
424
|
>
|
|
306
425
|
<ShimmerText active={props.live}>
|
|
307
426
|
{formatTokenSummary(props.summary)}
|
|
@@ -313,6 +432,8 @@ export function TokenMetric(props: {
|
|
|
313
432
|
/** Render a duration value with start/end timestamps in the tooltip. */
|
|
314
433
|
export function DurationMetric(props: {
|
|
315
434
|
align?: "left" | "right";
|
|
435
|
+
childAgentTotals?: ConversationChildAgentTotals;
|
|
436
|
+
durationMs?: number;
|
|
316
437
|
endedAt?: string;
|
|
317
438
|
label: string;
|
|
318
439
|
startedAt?: string;
|
|
@@ -325,6 +446,12 @@ export function DurationMetric(props: {
|
|
|
325
446
|
props.endedAt
|
|
326
447
|
? { label: "ended", value: formatTime(props.endedAt) }
|
|
327
448
|
: undefined,
|
|
449
|
+
...agentSplitLines(
|
|
450
|
+
props.childAgentTotals,
|
|
451
|
+
props.durationMs ?? 0,
|
|
452
|
+
props.childAgentTotals?.cumulativeDurationMs ?? 0,
|
|
453
|
+
(value) => formatRuntime(value) || "0s",
|
|
454
|
+
),
|
|
328
455
|
];
|
|
329
456
|
const tooltip = lines.filter(isMetricTooltipLine);
|
|
330
457
|
return (
|
|
@@ -334,6 +461,26 @@ export function DurationMetric(props: {
|
|
|
334
461
|
);
|
|
335
462
|
}
|
|
336
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Say how many subagents the tokens, cost, and runtime of a Conversation
|
|
466
|
+
* include. Render nothing for a Conversation without subagents.
|
|
467
|
+
*/
|
|
468
|
+
export function SubagentsMetric(props: {
|
|
469
|
+
childAgentTotals: ConversationChildAgentTotals | undefined;
|
|
470
|
+
}) {
|
|
471
|
+
if (!props.childAgentTotals) return null;
|
|
472
|
+
const label = plural("subagent", props.childAgentTotals.count);
|
|
473
|
+
return (
|
|
474
|
+
<MetricValue
|
|
475
|
+
tooltip={[
|
|
476
|
+
{ value: `Tokens, cost, and runtime include ${label} at any depth.` },
|
|
477
|
+
]}
|
|
478
|
+
>
|
|
479
|
+
{label}
|
|
480
|
+
</MetricValue>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
337
484
|
/** Render a tool-call count with top tool names and counts. */
|
|
338
485
|
export function ToolCallsMetric(props: {
|
|
339
486
|
align?: "left" | "right";
|