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