@sentry/junior-dashboard 0.62.0 → 0.64.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.d.ts +2 -0
- package/dist/app.js +1285 -39
- package/dist/assets.d.ts +2 -0
- package/dist/client/api.d.ts +1 -1
- package/dist/client/components/Button.d.ts +15 -0
- package/dist/client/components/Metric.d.ts +22 -0
- package/dist/client/components/StatusBadge.d.ts +2 -1
- package/dist/client/components/TelemetryMetrics.d.ts +24 -0
- package/dist/client/components/ToolFrame.d.ts +3 -1
- package/dist/client/components/Transcript.d.ts +2 -0
- package/dist/client/components/TranscriptHeader.d.ts +2 -0
- package/dist/client/components/TranscriptHeadingRow.d.ts +16 -0
- package/dist/client/components/TranscriptThinkingView.d.ts +7 -0
- package/dist/client/components/TranscriptToolRun.d.ts +9 -0
- package/dist/client/components/transcriptRenderModel.d.ts +25 -9
- package/dist/client/format.d.ts +45 -8
- package/dist/client/markdownExport.d.ts +3 -0
- package/dist/client/toolInvocations.d.ts +7 -0
- package/dist/client/types.d.ts +17 -100
- package/dist/client.js +85 -55998
- package/dist/dashboardLoader.d.ts +1 -0
- package/dist/handler.js +1288 -40
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1807 -0
- package/dist/mock-conversations.d.ts +3 -0
- package/dist/mock-release-conversation.d.ts +3 -0
- package/dist/nitro.d.ts +7 -1
- package/dist/tailwind.css +1 -1
- package/dist/url.d.ts +13 -0
- package/package.json +9 -5
- package/src/app.ts +42 -18
- package/src/assets.ts +2 -0
- package/src/auth.ts +2 -35
- package/src/client/App.tsx +33 -74
- package/src/client/code.tsx +1 -1
- package/src/client/components/Button.tsx +75 -0
- package/src/client/components/ConversationRowStats.tsx +3 -2
- package/src/client/components/FilterTabs.tsx +10 -11
- package/src/client/components/LoadingView.tsx +7 -2
- package/src/client/components/Metric.tsx +159 -0
- package/src/client/components/StatusBadge.tsx +10 -1
- package/src/client/components/TelemetryMetrics.tsx +124 -0
- package/src/client/components/ToolFrame.tsx +57 -14
- package/src/client/components/Transcript.tsx +6 -2
- package/src/client/components/TranscriptHeader.tsx +18 -19
- package/src/client/components/TranscriptHeadingRow.tsx +64 -0
- package/src/client/components/TranscriptThinkingView.tsx +157 -0
- package/src/client/components/TranscriptToolRun.tsx +66 -0
- package/src/client/components/TranscriptToolView.tsx +16 -8
- package/src/client/components/TranscriptTurn.tsx +368 -132
- package/src/client/components/TurnDurationChart.tsx +236 -78
- package/src/client/components/transcriptRenderModel.ts +60 -20
- package/src/client/format.ts +329 -87
- package/src/client/markdownExport.ts +360 -0
- package/src/client/pages/CommandCenter.tsx +1 -1
- package/src/client/pages/ConversationPage.tsx +142 -45
- package/src/client/pages/ConversationsPage.tsx +1 -1
- package/src/client/toolInvocations.ts +16 -0
- package/src/client/types.ts +34 -90
- package/src/config.ts +4 -0
- package/src/dashboardLoader.ts +12 -0
- package/src/index.ts +78 -0
- package/src/mock-conversations.ts +726 -0
- package/src/mock-release-conversation.ts +605 -0
- package/src/nitro.ts +7 -1
- package/src/tailwind.css +11 -0
- package/src/url.ts +68 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Fragment, useMemo, useState, type ReactNode } from "react";
|
|
1
2
|
import { useQuery } from "@tanstack/react-query";
|
|
2
3
|
import { useNavigate } from "react-router";
|
|
3
4
|
import {
|
|
@@ -12,20 +13,38 @@ import {
|
|
|
12
13
|
|
|
13
14
|
import { readConversationData } from "../api";
|
|
14
15
|
import {
|
|
16
|
+
buildConversations,
|
|
17
|
+
conversationDisplayTitle,
|
|
18
|
+
conversationRequesterLabel,
|
|
15
19
|
conversationIdForSession,
|
|
16
20
|
conversationPath,
|
|
21
|
+
formatDurationTick,
|
|
17
22
|
formatMs,
|
|
18
|
-
formatTokenTotal,
|
|
19
23
|
requesterLabel,
|
|
20
24
|
slackLocationLabel,
|
|
21
|
-
|
|
25
|
+
summarizeMessages,
|
|
26
|
+
summarizeToolCalls,
|
|
27
|
+
summarizeUsage,
|
|
22
28
|
visualStatusForSession,
|
|
29
|
+
visualStatusForConversation,
|
|
23
30
|
} from "../format";
|
|
24
31
|
import { cn } from "../styles";
|
|
25
|
-
import type {
|
|
32
|
+
import type {
|
|
33
|
+
Conversation,
|
|
34
|
+
ConversationDetailFeed,
|
|
35
|
+
ConversationTurn,
|
|
36
|
+
Session,
|
|
37
|
+
VisualStatus,
|
|
38
|
+
} from "../types";
|
|
39
|
+
import { MetricValue } from "./Metric";
|
|
26
40
|
import { Section } from "./Section";
|
|
27
41
|
import { SectionHeader } from "./SectionHeader";
|
|
28
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
DurationMetric,
|
|
44
|
+
MessagesMetric,
|
|
45
|
+
TokenMetric,
|
|
46
|
+
ToolCallsMetric,
|
|
47
|
+
} from "./TelemetryMetrics";
|
|
29
48
|
import { statusBorderClass } from "./statusStyles";
|
|
30
49
|
|
|
31
50
|
/** Render recent turns by start time and duration. */
|
|
@@ -33,6 +52,7 @@ export function TurnDurationChart(props: {
|
|
|
33
52
|
sessions: Session[];
|
|
34
53
|
timeZone: string;
|
|
35
54
|
}) {
|
|
55
|
+
const [mode, setMode] = useState<DurationChartMode>("conversations");
|
|
36
56
|
const navigate = useNavigate();
|
|
37
57
|
const nowMs = Date.now();
|
|
38
58
|
const rangeStartMs = nowMs - 7 * 24 * 60 * 60 * 1000;
|
|
@@ -40,9 +60,18 @@ export function TurnDurationChart(props: {
|
|
|
40
60
|
const chartEdgePaddingMs = 6 * 60 * 60 * 1000;
|
|
41
61
|
const chartRangeStartMs = rangeStartMs - chartEdgePaddingMs;
|
|
42
62
|
const chartRangeEndMs = rangeEndMs + chartEdgePaddingMs;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
.
|
|
63
|
+
const conversations = useMemo(
|
|
64
|
+
() => (mode === "conversations" ? buildConversations(props.sessions) : []),
|
|
65
|
+
[mode, props.sessions],
|
|
66
|
+
);
|
|
67
|
+
const points = (
|
|
68
|
+
mode === "turns"
|
|
69
|
+
? props.sessions.map((session) => turnPoint(session, props.timeZone))
|
|
70
|
+
: conversations.map((conversation) =>
|
|
71
|
+
conversationPoint(conversation, props.timeZone),
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
.filter((point): point is DurationPoint => Boolean(point))
|
|
46
75
|
.filter((point) => point.x >= rangeStartMs && point.x <= rangeEndMs)
|
|
47
76
|
.sort((left, right) => left.x - right.x);
|
|
48
77
|
const totals = points.reduce(
|
|
@@ -61,26 +90,27 @@ export function TurnDurationChart(props: {
|
|
|
61
90
|
const dayTicks = Array.from({ length: 7 }, (_, index) => {
|
|
62
91
|
return rangeStartMs + index * 24 * 60 * 60 * 1000;
|
|
63
92
|
});
|
|
64
|
-
const openPoint = (point:
|
|
65
|
-
navigate(conversationPath(
|
|
93
|
+
const openPoint = (point: DurationPoint) => {
|
|
94
|
+
navigate(conversationPath(point.conversationId));
|
|
66
95
|
};
|
|
96
|
+
const modeLabel = mode === "turns" ? "turns" : "conversations";
|
|
67
97
|
|
|
68
98
|
return (
|
|
69
99
|
<Section>
|
|
70
100
|
<SectionHeader
|
|
71
101
|
actions={
|
|
72
|
-
<div className="flex flex-wrap items-center gap-3 text-[0.78rem] font-semibold uppercase leading-none text-[#888]">
|
|
102
|
+
<div className="flex flex-wrap items-center justify-end gap-3 text-[0.78rem] font-semibold uppercase leading-none text-[#888]">
|
|
73
103
|
<ChartLegendItem className="bg-[#b8b8b8]" label="Complete" />
|
|
74
104
|
<ChartLegendItem className="bg-amber-400" label="Hung" />
|
|
75
105
|
<ChartLegendItem className="bg-rose-400" label="Error" />
|
|
76
106
|
</div>
|
|
77
107
|
}
|
|
78
108
|
>
|
|
79
|
-
<
|
|
109
|
+
<ChartModeToggle mode={mode} onChange={setMode} />
|
|
80
110
|
</SectionHeader>
|
|
81
111
|
<div
|
|
82
112
|
className="min-h-48 px-3 pb-2 pt-4"
|
|
83
|
-
aria-label=
|
|
113
|
+
aria-label={`${modeLabel} by duration over the last 7 days`}
|
|
84
114
|
>
|
|
85
115
|
<ResponsiveContainer height={190} width="100%">
|
|
86
116
|
<ScatterChart margin={{ bottom: 0, left: 0, right: 4, top: 14 }}>
|
|
@@ -107,7 +137,7 @@ export function TurnDurationChart(props: {
|
|
|
107
137
|
axisLine={false}
|
|
108
138
|
dataKey="durationMs"
|
|
109
139
|
domain={[0, durationAxisMaxMs]}
|
|
110
|
-
tickFormatter={(value) =>
|
|
140
|
+
tickFormatter={(value) => formatDurationTick(Number(value))}
|
|
111
141
|
tick={{
|
|
112
142
|
fill: "#888",
|
|
113
143
|
fontFamily:
|
|
@@ -116,6 +146,7 @@ export function TurnDurationChart(props: {
|
|
|
116
146
|
}}
|
|
117
147
|
tickLine={false}
|
|
118
148
|
type="number"
|
|
149
|
+
width={48}
|
|
119
150
|
/>
|
|
120
151
|
<Tooltip
|
|
121
152
|
content={<TurnDurationTooltip />}
|
|
@@ -131,7 +162,7 @@ export function TurnDurationChart(props: {
|
|
|
131
162
|
</ResponsiveContainer>
|
|
132
163
|
</div>
|
|
133
164
|
<div className="border-t border-white/10 px-4 py-3 text-[0.84rem] leading-tight text-[#888]">
|
|
134
|
-
{totals.total}
|
|
165
|
+
{totals.total} {modeLabel} / {totals.hung} hung / {totals.failed} errors
|
|
135
166
|
</div>
|
|
136
167
|
</Section>
|
|
137
168
|
);
|
|
@@ -147,38 +178,133 @@ function ChartLegendItem(props: { className: string; label: string }) {
|
|
|
147
178
|
}
|
|
148
179
|
|
|
149
180
|
type PlottedTurnStatus = Exclude<VisualStatus, "active">;
|
|
181
|
+
type DurationChartMode = "conversations" | "turns";
|
|
150
182
|
|
|
151
|
-
type
|
|
183
|
+
type DurationPoint = {
|
|
184
|
+
conversation?: Conversation;
|
|
185
|
+
conversationId: string;
|
|
186
|
+
durationLabel: string;
|
|
152
187
|
durationMs: number;
|
|
153
|
-
|
|
154
|
-
|
|
188
|
+
endedAt: string;
|
|
189
|
+
kind: DurationChartMode;
|
|
190
|
+
session?: Session;
|
|
191
|
+
startedAt: string;
|
|
155
192
|
status: PlottedTurnStatus;
|
|
193
|
+
title: string;
|
|
194
|
+
tooltipLabel: string;
|
|
156
195
|
x: number;
|
|
157
196
|
};
|
|
158
197
|
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
163
|
-
const
|
|
198
|
+
function ChartModeToggle(props: {
|
|
199
|
+
mode: DurationChartMode;
|
|
200
|
+
onChange(mode: DurationChartMode): void;
|
|
201
|
+
}) {
|
|
202
|
+
const modes: Array<{ label: string; value: DurationChartMode }> = [
|
|
203
|
+
{ label: "Conversations", value: "conversations" },
|
|
204
|
+
{ label: "Turns", value: "turns" },
|
|
205
|
+
];
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
aria-label="Duration chart mode"
|
|
209
|
+
className="inline-flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-1"
|
|
210
|
+
role="group"
|
|
211
|
+
>
|
|
212
|
+
{modes.map((mode, index) => (
|
|
213
|
+
<Fragment key={mode.value}>
|
|
214
|
+
{index > 0 ? (
|
|
215
|
+
<span className="text-[1.05rem] font-bold leading-tight text-[#666]">
|
|
216
|
+
/
|
|
217
|
+
</span>
|
|
218
|
+
) : null}
|
|
219
|
+
<button
|
|
220
|
+
aria-pressed={props.mode === mode.value}
|
|
221
|
+
className={cn(
|
|
222
|
+
"cursor-pointer border-0 bg-transparent p-0 text-[1.05rem] font-bold leading-tight tracking-normal transition-colors",
|
|
223
|
+
props.mode === mode.value
|
|
224
|
+
? "text-white"
|
|
225
|
+
: "text-[#888] hover:text-white",
|
|
226
|
+
)}
|
|
227
|
+
onClick={() => props.onChange(mode.value)}
|
|
228
|
+
type="button"
|
|
229
|
+
>
|
|
230
|
+
{mode.label}
|
|
231
|
+
</button>
|
|
232
|
+
</Fragment>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function plottedStatus(status: VisualStatus): PlottedTurnStatus | null {
|
|
239
|
+
return status === "active" ? null : status;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function finiteDurationMs(value: number | undefined): number | undefined {
|
|
243
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
244
|
+
return Math.max(0, Math.floor(value));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function turnPoint(session: Session, timeZone: string): DurationPoint | null {
|
|
248
|
+
const startedAtMs = Date.parse(session.startedAt);
|
|
164
249
|
if (!Number.isFinite(startedAtMs)) {
|
|
165
250
|
return null;
|
|
166
251
|
}
|
|
167
|
-
const status = visualStatusForSession(session);
|
|
168
|
-
if (status
|
|
252
|
+
const status = plottedStatus(visualStatusForSession(session));
|
|
253
|
+
if (!status) {
|
|
169
254
|
return null;
|
|
170
255
|
}
|
|
171
256
|
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
? Math.max(0, lastSeenAtMs - startedAtMs)
|
|
177
|
-
: 0);
|
|
257
|
+
const durationMs = finiteDurationMs(session.cumulativeDurationMs);
|
|
258
|
+
if (durationMs === undefined) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
178
261
|
return {
|
|
262
|
+
conversationId: conversationIdForSession(session),
|
|
263
|
+
durationLabel: formatMs(durationMs),
|
|
179
264
|
durationMs,
|
|
265
|
+
endedAt: session.completedAt ?? session.lastSeenAt,
|
|
266
|
+
kind: "turns",
|
|
180
267
|
session,
|
|
268
|
+
startedAt: session.startedAt,
|
|
181
269
|
status,
|
|
270
|
+
title: turnTooltipTitle(session),
|
|
271
|
+
tooltipLabel: new Date(startedAtMs).toLocaleString(undefined, {
|
|
272
|
+
timeZone,
|
|
273
|
+
}),
|
|
274
|
+
x: startedAtMs,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function conversationPoint(
|
|
279
|
+
conversation: Conversation,
|
|
280
|
+
timeZone: string,
|
|
281
|
+
): DurationPoint | null {
|
|
282
|
+
const startedAtMs = Date.parse(conversation.startedAt);
|
|
283
|
+
if (!Number.isFinite(startedAtMs)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const status = plottedStatus(visualStatusForConversation(conversation));
|
|
287
|
+
if (!status) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const durations = conversation.turns
|
|
291
|
+
.map((turn) => finiteDurationMs(turn.cumulativeDurationMs))
|
|
292
|
+
.filter((duration): duration is number => duration !== undefined);
|
|
293
|
+
if (durations.length === 0) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const durationMs = durations.reduce((sum, duration) => sum + duration, 0);
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
conversation,
|
|
300
|
+
conversationId: conversation.id,
|
|
301
|
+
durationLabel: formatMs(durationMs),
|
|
302
|
+
durationMs,
|
|
303
|
+
endedAt: conversation.lastSeenAt,
|
|
304
|
+
kind: "conversations",
|
|
305
|
+
startedAt: conversation.startedAt,
|
|
306
|
+
status,
|
|
307
|
+
title: conversationDisplayTitle(conversation),
|
|
182
308
|
tooltipLabel: new Date(startedAtMs).toLocaleString(undefined, {
|
|
183
309
|
timeZone,
|
|
184
310
|
}),
|
|
@@ -189,13 +315,10 @@ function turnPoint(
|
|
|
189
315
|
type DurationDotProps = {
|
|
190
316
|
cx?: number;
|
|
191
317
|
cy?: number;
|
|
192
|
-
payload?:
|
|
318
|
+
payload?: DurationPoint;
|
|
193
319
|
};
|
|
194
320
|
|
|
195
|
-
function durationDot(
|
|
196
|
-
onOpen: (point: TurnDurationPoint) => void,
|
|
197
|
-
active: boolean,
|
|
198
|
-
) {
|
|
321
|
+
function durationDot(onOpen: (point: DurationPoint) => void, active: boolean) {
|
|
199
322
|
return (props: DurationDotProps) => {
|
|
200
323
|
if (props.cx == null || props.cy == null || !props.payload) {
|
|
201
324
|
return <g />;
|
|
@@ -205,7 +328,7 @@ function durationDot(
|
|
|
205
328
|
const fill = durationDotFill(point.status, active);
|
|
206
329
|
return (
|
|
207
330
|
<circle
|
|
208
|
-
aria-label={`Open ${point.
|
|
331
|
+
aria-label={`Open ${point.title}`}
|
|
209
332
|
className="cursor-pointer outline-none transition-[filter,stroke,stroke-width] hover:brightness-125 focus-visible:stroke-emerald-400 focus-visible:stroke-2"
|
|
210
333
|
cx={props.cx}
|
|
211
334
|
cy={props.cy}
|
|
@@ -239,12 +362,10 @@ function durationDotFill(status: PlottedTurnStatus, active: boolean): string {
|
|
|
239
362
|
|
|
240
363
|
function TurnDurationTooltip(props: {
|
|
241
364
|
active?: boolean;
|
|
242
|
-
payload?: Array<{ payload:
|
|
365
|
+
payload?: Array<{ payload: DurationPoint }>;
|
|
243
366
|
}) {
|
|
244
367
|
const point = props.payload?.[0]?.payload;
|
|
245
|
-
const conversationId = point
|
|
246
|
-
? conversationIdForSession(point.session)
|
|
247
|
-
: undefined;
|
|
368
|
+
const conversationId = point?.conversationId;
|
|
248
369
|
const detail = useQuery({
|
|
249
370
|
enabled: Boolean(props.active && conversationId),
|
|
250
371
|
queryKey: ["conversation", conversationId],
|
|
@@ -256,7 +377,7 @@ function TurnDurationTooltip(props: {
|
|
|
256
377
|
if (!props.active || !point) {
|
|
257
378
|
return null;
|
|
258
379
|
}
|
|
259
|
-
const rows =
|
|
380
|
+
const rows = chartTooltipRows(point, detail.data);
|
|
260
381
|
return (
|
|
261
382
|
<div
|
|
262
383
|
className={cn(
|
|
@@ -264,18 +385,16 @@ function TurnDurationTooltip(props: {
|
|
|
264
385
|
statusBorderClass(point.status),
|
|
265
386
|
)}
|
|
266
387
|
>
|
|
267
|
-
<div className="flex items-
|
|
388
|
+
<div className="flex items-center justify-between gap-3">
|
|
268
389
|
<div className="min-w-0">
|
|
269
390
|
<div className="truncate text-[0.92rem] font-bold leading-tight text-white">
|
|
270
|
-
{
|
|
391
|
+
{point.title}
|
|
271
392
|
</div>
|
|
272
393
|
<div className="mt-0.5 text-[0.78rem] leading-tight text-[#888]">
|
|
273
394
|
{point.tooltipLabel}
|
|
274
395
|
</div>
|
|
275
396
|
</div>
|
|
276
|
-
|
|
277
|
-
{point.status}
|
|
278
|
-
</span>
|
|
397
|
+
{chartTooltipStatus(point.status)}
|
|
279
398
|
</div>
|
|
280
399
|
<div className="mt-2 grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1">
|
|
281
400
|
{rows.map(([label, value]) => (
|
|
@@ -293,51 +412,90 @@ function TurnDurationTooltip(props: {
|
|
|
293
412
|
);
|
|
294
413
|
}
|
|
295
414
|
|
|
296
|
-
function
|
|
297
|
-
point:
|
|
415
|
+
function chartTooltipRows(
|
|
416
|
+
point: DurationPoint,
|
|
298
417
|
detail: ConversationDetailFeed | undefined,
|
|
299
|
-
): Array<[string,
|
|
300
|
-
const session = point.session;
|
|
301
|
-
const requester =
|
|
302
|
-
|
|
303
|
-
|
|
418
|
+
): Array<[string, ReactNode]> {
|
|
419
|
+
const session = point.session ?? point.conversation;
|
|
420
|
+
const requester =
|
|
421
|
+
point.kind === "conversations"
|
|
422
|
+
? conversationRequesterLabel(point.conversation)
|
|
423
|
+
: requesterLabel(session?.requesterIdentity);
|
|
424
|
+
const location = session
|
|
425
|
+
? slackLocationLabel(session, { includeId: false })
|
|
426
|
+
: undefined;
|
|
427
|
+
const turns = chartTooltipTurns(point, detail);
|
|
428
|
+
const tokenSummary = summarizeUsage(
|
|
429
|
+
point.kind === "turns"
|
|
430
|
+
? [point.session?.cumulativeUsage]
|
|
431
|
+
: (detail?.turns ?? point.conversation?.turns ?? []).map(
|
|
432
|
+
(turn) => turn.cumulativeUsage,
|
|
433
|
+
),
|
|
304
434
|
);
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
[
|
|
309
|
-
|
|
310
|
-
|
|
435
|
+
const messageSummary = detail ? summarizeMessages(turns) : undefined;
|
|
436
|
+
const toolSummary = detail ? summarizeToolCalls(turns) : undefined;
|
|
437
|
+
const rows: Array<[string, ReactNode] | null> = [
|
|
438
|
+
[
|
|
439
|
+
"duration",
|
|
440
|
+
<DurationMetric
|
|
441
|
+
align="right"
|
|
442
|
+
endedAt={point.endedAt}
|
|
443
|
+
label={point.durationLabel}
|
|
444
|
+
startedAt={point.startedAt}
|
|
445
|
+
/>,
|
|
446
|
+
],
|
|
447
|
+
tokenSummary
|
|
448
|
+
? ["tokens", <TokenMetric align="right" summary={tokenSummary} />]
|
|
449
|
+
: null,
|
|
450
|
+
[
|
|
451
|
+
"messages",
|
|
452
|
+
detail ? <MessagesMetric summary={messageSummary} /> : "loading",
|
|
453
|
+
],
|
|
454
|
+
!detail || (toolSummary && toolSummary.total > 0)
|
|
455
|
+
? [
|
|
456
|
+
"tool calls",
|
|
457
|
+
detail ? (
|
|
458
|
+
<ToolCallsMetric align="right" summary={toolSummary} />
|
|
459
|
+
) : (
|
|
460
|
+
"loading"
|
|
461
|
+
),
|
|
462
|
+
]
|
|
463
|
+
: null,
|
|
311
464
|
requester ? ["requester", requester] : null,
|
|
312
465
|
location ? ["surface", location] : null,
|
|
313
|
-
]
|
|
466
|
+
];
|
|
467
|
+
return rows.filter((row): row is [string, ReactNode] => row !== null);
|
|
314
468
|
}
|
|
315
469
|
|
|
316
|
-
function
|
|
317
|
-
point:
|
|
470
|
+
function chartTooltipTurns(
|
|
471
|
+
point: DurationPoint,
|
|
318
472
|
detail: ConversationDetailFeed | undefined,
|
|
319
|
-
):
|
|
320
|
-
if (
|
|
321
|
-
return
|
|
473
|
+
): ConversationTurn[] {
|
|
474
|
+
if (point.kind === "conversations") {
|
|
475
|
+
return detail?.turns ?? [];
|
|
322
476
|
}
|
|
323
|
-
const turn = detail
|
|
324
|
-
return
|
|
477
|
+
const turn = detail?.turns.find((item) => item.id === point.session?.id);
|
|
478
|
+
return turn ? [turn] : [];
|
|
325
479
|
}
|
|
326
480
|
|
|
327
481
|
function turnTooltipTitle(session: Session): string {
|
|
328
|
-
return
|
|
329
|
-
session.conversationTitle ??
|
|
330
|
-
session.title ??
|
|
331
|
-
conversationIdForSession(session)
|
|
332
|
-
);
|
|
482
|
+
return session.conversationTitle ?? session.title;
|
|
333
483
|
}
|
|
334
484
|
|
|
335
|
-
function
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
485
|
+
function chartTooltipStatus(status: PlottedTurnStatus): ReactNode {
|
|
486
|
+
if (status === "idle") {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
return (
|
|
490
|
+
<MetricValue
|
|
491
|
+
className={cn(
|
|
492
|
+
"shrink-0 text-[0.68rem] font-bold uppercase leading-none",
|
|
493
|
+
status === "failed" && "text-rose-300",
|
|
494
|
+
status === "hung" && "text-amber-300",
|
|
495
|
+
)}
|
|
496
|
+
>
|
|
497
|
+
{status === "failed" ? "error" : status}
|
|
498
|
+
</MetricValue>
|
|
341
499
|
);
|
|
342
500
|
}
|
|
343
501
|
|
|
@@ -1,23 +1,58 @@
|
|
|
1
1
|
import { countStructuredBlockChildren } from "../code";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
parseMarkdownBlocks,
|
|
4
|
+
stringifyPartValue,
|
|
5
|
+
transcriptRoleKind,
|
|
6
|
+
} from "../format";
|
|
7
|
+
import { sameToolInvocation } from "../toolInvocations";
|
|
3
8
|
import type { TranscriptMessage, TranscriptPart } from "../types";
|
|
4
9
|
|
|
10
|
+
type RenderedToolPart =
|
|
11
|
+
| { call: TranscriptPart; kind: "tool"; result?: TranscriptPart }
|
|
12
|
+
| { call?: undefined; kind: "tool"; result: TranscriptPart };
|
|
13
|
+
|
|
5
14
|
export type RenderedTranscriptPart =
|
|
6
15
|
| { kind: "part"; part: TranscriptPart }
|
|
7
|
-
|
|
|
16
|
+
| RenderedToolPart;
|
|
8
17
|
|
|
9
|
-
type RenderedTranscriptEntry =
|
|
18
|
+
export type RenderedTranscriptEntry =
|
|
10
19
|
| { kind: "message"; message: TranscriptMessage }
|
|
20
|
+
| RenderedThinkingEntry
|
|
11
21
|
| RenderedToolEntry;
|
|
12
22
|
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
result?: TranscriptPart;
|
|
17
|
-
resultTimestamp?: number;
|
|
23
|
+
type RenderedThinkingEntry = {
|
|
24
|
+
kind: "thinking";
|
|
25
|
+
part: TranscriptPart;
|
|
18
26
|
timestamp?: number;
|
|
19
27
|
};
|
|
20
28
|
|
|
29
|
+
export type RenderedToolEntry =
|
|
30
|
+
| {
|
|
31
|
+
call: TranscriptPart;
|
|
32
|
+
kind: "tool";
|
|
33
|
+
result?: TranscriptPart;
|
|
34
|
+
resultTimestamp?: number;
|
|
35
|
+
timestamp?: number;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
call?: undefined;
|
|
39
|
+
kind: "tool";
|
|
40
|
+
result: TranscriptPart;
|
|
41
|
+
resultTimestamp?: number;
|
|
42
|
+
timestamp?: never;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type RenderedToolCallEntry = Extract<
|
|
46
|
+
RenderedToolEntry,
|
|
47
|
+
{ call: TranscriptPart }
|
|
48
|
+
>;
|
|
49
|
+
|
|
50
|
+
function isRenderedToolCallEntry(
|
|
51
|
+
entry: RenderedTranscriptEntry,
|
|
52
|
+
): entry is RenderedToolCallEntry {
|
|
53
|
+
return entry.kind === "tool" && entry.call !== undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
21
56
|
export type TranscriptViewMode = "raw" | "rich";
|
|
22
57
|
|
|
23
58
|
function isToolCall(part: TranscriptPart): boolean {
|
|
@@ -28,17 +63,12 @@ function isToolResult(part: TranscriptPart): boolean {
|
|
|
28
63
|
return part.type === "tool_result";
|
|
29
64
|
}
|
|
30
65
|
|
|
31
|
-
function
|
|
32
|
-
return
|
|
66
|
+
function isThinking(part: TranscriptPart): boolean {
|
|
67
|
+
return part.type === "thinking";
|
|
33
68
|
}
|
|
34
69
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
result: TranscriptPart,
|
|
38
|
-
): boolean {
|
|
39
|
-
if (call.id && result.id) return call.id === result.id;
|
|
40
|
-
if (call.name && result.name) return call.name === result.name;
|
|
41
|
-
return false;
|
|
70
|
+
function isString(value: string | undefined): value is string {
|
|
71
|
+
return typeof value === "string" && value.length > 0;
|
|
42
72
|
}
|
|
43
73
|
|
|
44
74
|
/** Group inline transcript parts so matching tool calls/results render together. */
|
|
@@ -83,11 +113,11 @@ export function groupTranscriptParts(
|
|
|
83
113
|
function findToolEntry(
|
|
84
114
|
entries: RenderedTranscriptEntry[],
|
|
85
115
|
result: TranscriptPart,
|
|
86
|
-
):
|
|
116
|
+
): RenderedToolCallEntry | undefined {
|
|
87
117
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
88
118
|
const entry = entries[index]!;
|
|
89
|
-
if (entry
|
|
90
|
-
if (
|
|
119
|
+
if (!isRenderedToolCallEntry(entry) || entry.result) continue;
|
|
120
|
+
if (sameToolInvocation(entry.call, result)) {
|
|
91
121
|
return entry;
|
|
92
122
|
}
|
|
93
123
|
}
|
|
@@ -138,6 +168,16 @@ export function groupTranscriptMessages(
|
|
|
138
168
|
continue;
|
|
139
169
|
}
|
|
140
170
|
|
|
171
|
+
if (isThinking(part)) {
|
|
172
|
+
flushMessage();
|
|
173
|
+
entries.push({
|
|
174
|
+
kind: "thinking",
|
|
175
|
+
part,
|
|
176
|
+
timestamp: message.timestamp,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
141
181
|
messageParts.push(part);
|
|
142
182
|
}
|
|
143
183
|
|