@sentry/junior-dashboard 0.61.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/TranscriptText.d.ts +1 -0
- package/dist/client/components/transcriptRenderModel.d.ts +24 -8
- package/dist/client/format.d.ts +65 -13
- package/dist/client/toolInvocations.d.ts +7 -0
- package/dist/client/types.d.ts +17 -100
- package/dist/client.js +59 -55992
- 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/TranscriptText.tsx +5 -2
- package/src/client/components/TranscriptToolView.tsx +9 -7
- package/src/client/components/TranscriptTurn.tsx +435 -84
- package/src/client/components/TurnDurationChart.tsx +229 -78
- package/src/client/components/transcriptRenderModel.ts +66 -22
- package/src/client/format.ts +369 -103
- 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,15 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Fragment,
|
|
3
|
+
useState,
|
|
4
|
+
type ClipboardEventHandler,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
} from "react";
|
|
2
7
|
|
|
3
8
|
import { HighlightedCode } from "../code";
|
|
4
9
|
import {
|
|
5
10
|
detectLanguage,
|
|
6
|
-
|
|
11
|
+
transcriptRoleKind,
|
|
7
12
|
formatBytes,
|
|
8
13
|
formatMessageOffset,
|
|
9
14
|
formatMessageTimestamp,
|
|
10
15
|
formatMs,
|
|
11
|
-
|
|
16
|
+
formatTurnDuration,
|
|
12
17
|
requesterLabel,
|
|
18
|
+
summarizeMessages,
|
|
19
|
+
summarizeToolCalls,
|
|
20
|
+
summarizeUsage,
|
|
21
|
+
turnMessageCount,
|
|
13
22
|
stringifyPartValue,
|
|
14
23
|
unavailableTranscriptLabel,
|
|
15
24
|
visualStatusForSession,
|
|
@@ -22,6 +31,13 @@ import type {
|
|
|
22
31
|
} from "../types";
|
|
23
32
|
import { StatusBadge } from "./StatusBadge";
|
|
24
33
|
import { ToolFrame, toolFrameClass } from "./ToolFrame";
|
|
34
|
+
import { MetricList, type MetricListItem } from "./Metric";
|
|
35
|
+
import {
|
|
36
|
+
DurationMetric,
|
|
37
|
+
MessagesMetric,
|
|
38
|
+
TokenMetric,
|
|
39
|
+
ToolCallsMetric,
|
|
40
|
+
} from "./TelemetryMetrics";
|
|
25
41
|
import { TranscriptText } from "./TranscriptText";
|
|
26
42
|
import { TranscriptToolView } from "./TranscriptToolView";
|
|
27
43
|
import {
|
|
@@ -38,6 +54,15 @@ import {
|
|
|
38
54
|
} from "./transcriptStyles";
|
|
39
55
|
import { previewToolValue } from "./transcriptPreview";
|
|
40
56
|
|
|
57
|
+
type TranscriptEntry = ReturnType<typeof groupTranscriptMessages>[number];
|
|
58
|
+
type TranscriptMessageEntry = Extract<TranscriptEntry, { kind: "message" }>;
|
|
59
|
+
type TranscriptThinkingEntry = Extract<TranscriptEntry, { kind: "thinking" }>;
|
|
60
|
+
type TranscriptToolEntry = Extract<TranscriptEntry, { kind: "tool" }>;
|
|
61
|
+
|
|
62
|
+
const TOOL_RUN_COLLAPSE_THRESHOLD = 10;
|
|
63
|
+
const TOOL_RUN_HEAD_COUNT = 4;
|
|
64
|
+
const TOOL_RUN_TAIL_COUNT = 2;
|
|
65
|
+
|
|
41
66
|
/** Render one conversation turn as actor messages and tool events. */
|
|
42
67
|
export function TurnTranscript(props: {
|
|
43
68
|
number: number;
|
|
@@ -72,17 +97,6 @@ function turnMarkerClass(
|
|
|
72
97
|
);
|
|
73
98
|
}
|
|
74
99
|
|
|
75
|
-
type TranscriptRoleKind = "assistant" | "other" | "system" | "tool" | "user";
|
|
76
|
-
|
|
77
|
-
function transcriptRoleKind(role: string): TranscriptRoleKind {
|
|
78
|
-
const normalized = role.toLowerCase();
|
|
79
|
-
if (normalized === "assistant") return "assistant";
|
|
80
|
-
if (normalized === "user") return "user";
|
|
81
|
-
if (normalized === "system") return "system";
|
|
82
|
-
if (normalized.includes("tool")) return "tool";
|
|
83
|
-
return "other";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
100
|
function transcriptRoleLabel(role: string, turn: ConversationTurn): string {
|
|
87
101
|
const kind = transcriptRoleKind(role);
|
|
88
102
|
if (kind === "assistant") return "Junior";
|
|
@@ -157,22 +171,10 @@ function TurnHeader(props: { number: number; turn: ConversationTurn }) {
|
|
|
157
171
|
<div className="break-all text-[1.05rem] font-bold leading-tight tracking-normal">
|
|
158
172
|
Turn {props.number}
|
|
159
173
|
</div>
|
|
160
|
-
<
|
|
161
|
-
{
|
|
162
|
-
{props.turn
|
|
163
|
-
|
|
164
|
-
{" · "}
|
|
165
|
-
<a
|
|
166
|
-
className="text-white no-underline hover:underline"
|
|
167
|
-
href={props.turn.sentryTraceUrl}
|
|
168
|
-
rel="noreferrer"
|
|
169
|
-
target="_blank"
|
|
170
|
-
>
|
|
171
|
-
View in Sentry
|
|
172
|
-
</a>
|
|
173
|
-
</>
|
|
174
|
-
) : null}
|
|
175
|
-
</div>
|
|
174
|
+
<MetricList
|
|
175
|
+
className={cn(mutedTranscriptMetaClass(), "mt-1")}
|
|
176
|
+
items={turnMeta(props.turn)}
|
|
177
|
+
/>
|
|
176
178
|
</div>
|
|
177
179
|
<StatusBadge status={status} />
|
|
178
180
|
</div>
|
|
@@ -184,10 +186,28 @@ function TurnEvents(props: {
|
|
|
184
186
|
view: TranscriptViewMode;
|
|
185
187
|
}) {
|
|
186
188
|
return (
|
|
187
|
-
<div className="grid gap-
|
|
189
|
+
<div className="grid gap-2 pt-3">
|
|
188
190
|
{props.turn.transcriptAvailable ? (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
<TranscriptEntryList
|
|
192
|
+
entries={groupTranscriptMessages(props.turn.transcript)}
|
|
193
|
+
keyPrefix={props.turn.id}
|
|
194
|
+
renderMessage={(entry, index) => (
|
|
195
|
+
<TranscriptMessageView
|
|
196
|
+
key={`${props.turn.id}:${index}`}
|
|
197
|
+
message={entry.message}
|
|
198
|
+
turn={props.turn}
|
|
199
|
+
view={props.view}
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
renderThinking={(entry, index) => (
|
|
203
|
+
<ThinkingPartView
|
|
204
|
+
key={`${props.turn.id}:thinking:${index}`}
|
|
205
|
+
timestamp={entry.timestamp}
|
|
206
|
+
turn={props.turn}
|
|
207
|
+
value={entry.part.output}
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
renderTool={(entry, index) => (
|
|
191
211
|
<TranscriptToolView
|
|
192
212
|
call={entry.call}
|
|
193
213
|
key={`${props.turn.id}:${index}`}
|
|
@@ -196,15 +216,8 @@ function TurnEvents(props: {
|
|
|
196
216
|
timestamp={entry.timestamp}
|
|
197
217
|
view={props.view}
|
|
198
218
|
/>
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
key={`${props.turn.id}:${index}`}
|
|
202
|
-
message={entry.message}
|
|
203
|
-
turn={props.turn}
|
|
204
|
-
view={props.view}
|
|
205
|
-
/>
|
|
206
|
-
),
|
|
207
|
-
)
|
|
219
|
+
)}
|
|
220
|
+
/>
|
|
208
221
|
) : props.turn.transcriptRedacted &&
|
|
209
222
|
props.turn.transcriptMetadata?.length ? (
|
|
210
223
|
<RedactedTranscriptView turn={props.turn} />
|
|
@@ -217,31 +230,187 @@ function TurnEvents(props: {
|
|
|
217
230
|
);
|
|
218
231
|
}
|
|
219
232
|
|
|
220
|
-
function
|
|
233
|
+
function TranscriptEntryList(props: {
|
|
234
|
+
entries: TranscriptEntry[];
|
|
235
|
+
keyPrefix: string;
|
|
236
|
+
renderMessage: (entry: TranscriptMessageEntry, index: number) => ReactNode;
|
|
237
|
+
renderThinking: (entry: TranscriptThinkingEntry, index: number) => ReactNode;
|
|
238
|
+
renderTool: (entry: TranscriptToolEntry, index: number) => ReactNode;
|
|
239
|
+
}) {
|
|
240
|
+
const rows: ReactNode[] = [];
|
|
241
|
+
|
|
242
|
+
for (let index = 0; index < props.entries.length; ) {
|
|
243
|
+
const entry = props.entries[index]!;
|
|
244
|
+
|
|
245
|
+
if (entry.kind === "tool") {
|
|
246
|
+
const startIndex = index;
|
|
247
|
+
const tools: TranscriptToolEntry[] = [];
|
|
248
|
+
while (props.entries[index]?.kind === "tool") {
|
|
249
|
+
tools.push(props.entries[index] as TranscriptToolEntry);
|
|
250
|
+
index += 1;
|
|
251
|
+
}
|
|
252
|
+
rows.push(
|
|
253
|
+
<ToolRunView
|
|
254
|
+
entries={tools}
|
|
255
|
+
key={`${props.keyPrefix}:tool-run:${startIndex}`}
|
|
256
|
+
keyPrefix={props.keyPrefix}
|
|
257
|
+
renderTool={props.renderTool}
|
|
258
|
+
startIndex={startIndex}
|
|
259
|
+
/>,
|
|
260
|
+
);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
rows.push(
|
|
265
|
+
<Fragment key={`${props.keyPrefix}:${entry.kind}:${index}`}>
|
|
266
|
+
{entry.kind === "thinking"
|
|
267
|
+
? props.renderThinking(entry, index)
|
|
268
|
+
: props.renderMessage(entry, index)}
|
|
269
|
+
</Fragment>,
|
|
270
|
+
);
|
|
271
|
+
index += 1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return <>{rows}</>;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function ToolRunView(props: {
|
|
278
|
+
entries: TranscriptToolEntry[];
|
|
279
|
+
keyPrefix: string;
|
|
280
|
+
renderTool: (entry: TranscriptToolEntry, index: number) => ReactNode;
|
|
281
|
+
startIndex: number;
|
|
282
|
+
}) {
|
|
283
|
+
const [expanded, setExpanded] = useState(false);
|
|
284
|
+
|
|
285
|
+
if (props.entries.length < TOOL_RUN_COLLAPSE_THRESHOLD) {
|
|
286
|
+
return (
|
|
287
|
+
<>
|
|
288
|
+
{renderToolEntries(
|
|
289
|
+
props.entries,
|
|
290
|
+
props.startIndex,
|
|
291
|
+
props.keyPrefix,
|
|
292
|
+
props.renderTool,
|
|
293
|
+
)}
|
|
294
|
+
</>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (expanded) {
|
|
299
|
+
return (
|
|
300
|
+
<>
|
|
301
|
+
{renderToolEntries(
|
|
302
|
+
props.entries,
|
|
303
|
+
props.startIndex,
|
|
304
|
+
props.keyPrefix,
|
|
305
|
+
props.renderTool,
|
|
306
|
+
)}
|
|
307
|
+
<ToolRunToggle
|
|
308
|
+
expanded
|
|
309
|
+
onClick={() => setExpanded(false)}
|
|
310
|
+
totalCount={props.entries.length}
|
|
311
|
+
/>
|
|
312
|
+
</>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const hiddenCount =
|
|
317
|
+
props.entries.length - TOOL_RUN_HEAD_COUNT - TOOL_RUN_TAIL_COUNT;
|
|
318
|
+
|
|
221
319
|
return (
|
|
222
320
|
<>
|
|
223
|
-
{
|
|
224
|
-
(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
),
|
|
321
|
+
{renderToolEntries(
|
|
322
|
+
props.entries.slice(0, TOOL_RUN_HEAD_COUNT),
|
|
323
|
+
props.startIndex,
|
|
324
|
+
props.keyPrefix,
|
|
325
|
+
props.renderTool,
|
|
326
|
+
)}
|
|
327
|
+
<ToolRunToggle
|
|
328
|
+
hiddenCount={hiddenCount}
|
|
329
|
+
onClick={() => setExpanded(true)}
|
|
330
|
+
totalCount={props.entries.length}
|
|
331
|
+
/>
|
|
332
|
+
{renderToolEntries(
|
|
333
|
+
props.entries.slice(-TOOL_RUN_TAIL_COUNT),
|
|
334
|
+
props.startIndex + props.entries.length - TOOL_RUN_TAIL_COUNT,
|
|
335
|
+
props.keyPrefix,
|
|
336
|
+
props.renderTool,
|
|
240
337
|
)}
|
|
241
338
|
</>
|
|
242
339
|
);
|
|
243
340
|
}
|
|
244
341
|
|
|
342
|
+
function renderToolEntries(
|
|
343
|
+
entries: TranscriptToolEntry[],
|
|
344
|
+
startIndex: number,
|
|
345
|
+
keyPrefix: string,
|
|
346
|
+
renderTool: (entry: TranscriptToolEntry, index: number) => ReactNode,
|
|
347
|
+
): ReactNode[] {
|
|
348
|
+
return entries.map((entry, offset) => {
|
|
349
|
+
const index = startIndex + offset;
|
|
350
|
+
return (
|
|
351
|
+
<Fragment key={`${keyPrefix}:tool:${index}`}>
|
|
352
|
+
{renderTool(entry, index)}
|
|
353
|
+
</Fragment>
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function ToolRunToggle(props: {
|
|
359
|
+
expanded?: boolean;
|
|
360
|
+
hiddenCount?: number;
|
|
361
|
+
onClick: () => void;
|
|
362
|
+
totalCount: number;
|
|
363
|
+
}) {
|
|
364
|
+
const label = props.expanded
|
|
365
|
+
? `collapse ${props.totalCount} tool calls`
|
|
366
|
+
: `show ${props.hiddenCount ?? 0} more tool calls`;
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<button
|
|
370
|
+
aria-expanded={props.expanded ?? false}
|
|
371
|
+
className="group flex w-full items-center gap-2 py-1 pl-3 text-left font-mono text-[0.78rem] leading-tight text-[#888] transition-colors hover:text-[#d6d6d6] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#beaaff]/55"
|
|
372
|
+
onClick={props.onClick}
|
|
373
|
+
type="button"
|
|
374
|
+
>
|
|
375
|
+
<span className="h-px min-w-4 flex-1 bg-white/10 transition-colors group-hover:bg-white/20" />
|
|
376
|
+
<span className="shrink-0">{label}</span>
|
|
377
|
+
<span className="h-px min-w-4 flex-1 bg-white/10 transition-colors group-hover:bg-white/20" />
|
|
378
|
+
</button>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function RedactedTranscriptView(props: { turn: ConversationTurn }) {
|
|
383
|
+
return (
|
|
384
|
+
<TranscriptEntryList
|
|
385
|
+
entries={groupTranscriptMessages(props.turn.transcriptMetadata ?? [])}
|
|
386
|
+
keyPrefix={`${props.turn.id}:redacted`}
|
|
387
|
+
renderMessage={(entry, index) => (
|
|
388
|
+
<RedactedMessageView
|
|
389
|
+
key={`${props.turn.id}:redacted:${index}`}
|
|
390
|
+
message={entry.message}
|
|
391
|
+
turn={props.turn}
|
|
392
|
+
/>
|
|
393
|
+
)}
|
|
394
|
+
renderThinking={(entry, index) => (
|
|
395
|
+
<RedactedThinkingView
|
|
396
|
+
key={`${props.turn.id}:redacted:thinking:${index}`}
|
|
397
|
+
timestamp={entry.timestamp}
|
|
398
|
+
turn={props.turn}
|
|
399
|
+
/>
|
|
400
|
+
)}
|
|
401
|
+
renderTool={(entry, index) => (
|
|
402
|
+
<RedactedToolView
|
|
403
|
+
call={entry.call}
|
|
404
|
+
key={`${props.turn.id}:redacted:${index}`}
|
|
405
|
+
result={entry.result}
|
|
406
|
+
resultTimestamp={entry.resultTimestamp}
|
|
407
|
+
timestamp={entry.timestamp}
|
|
408
|
+
/>
|
|
409
|
+
)}
|
|
410
|
+
/>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
245
414
|
function RedactedMessageView(props: {
|
|
246
415
|
message: TranscriptMessage;
|
|
247
416
|
turn: ConversationTurn;
|
|
@@ -306,6 +475,31 @@ function RedactedMarker() {
|
|
|
306
475
|
);
|
|
307
476
|
}
|
|
308
477
|
|
|
478
|
+
function RedactedThinkingView(props: {
|
|
479
|
+
timestamp?: number;
|
|
480
|
+
turn: ConversationTurn;
|
|
481
|
+
}) {
|
|
482
|
+
const offset = formatMessageOffset(props.turn, props.timestamp);
|
|
483
|
+
const meta = [
|
|
484
|
+
typeof props.timestamp === "number"
|
|
485
|
+
? formatMessageTimestamp(props.timestamp)
|
|
486
|
+
: undefined,
|
|
487
|
+
offset,
|
|
488
|
+
].filter(isString);
|
|
489
|
+
|
|
490
|
+
return (
|
|
491
|
+
<div className="py-0.5 text-[0.84rem] leading-relaxed text-[#888]">
|
|
492
|
+
<div className="grid min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-baseline gap-3 max-md:grid-cols-1 max-md:gap-1">
|
|
493
|
+
<span className="font-mono text-[0.78rem] text-[#777]">thought</span>
|
|
494
|
+
<RedactedMarker />
|
|
495
|
+
<span className="min-w-0 break-words text-right font-mono text-[0.78rem] text-[#777] max-md:text-left">
|
|
496
|
+
{meta.join(" · ")}
|
|
497
|
+
</span>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
309
503
|
function RedactedToolView(props: {
|
|
310
504
|
call?: TranscriptPart;
|
|
311
505
|
result?: TranscriptPart;
|
|
@@ -325,7 +519,9 @@ function RedactedToolView(props: {
|
|
|
325
519
|
? formatMs(props.resultTimestamp - props.timestamp)
|
|
326
520
|
: undefined;
|
|
327
521
|
const meta = [
|
|
328
|
-
|
|
522
|
+
typeof props.timestamp === "number"
|
|
523
|
+
? formatMessageTimestamp(props.timestamp)
|
|
524
|
+
: undefined,
|
|
329
525
|
duration,
|
|
330
526
|
props.result ? undefined : "missing result",
|
|
331
527
|
].filter(isString);
|
|
@@ -336,7 +532,7 @@ function RedactedToolView(props: {
|
|
|
336
532
|
raw
|
|
337
533
|
signature={
|
|
338
534
|
<>
|
|
339
|
-
<strong className="min-w-0 break-words font-bold text-
|
|
535
|
+
<strong className="min-w-0 break-words font-bold text-[#d6d6d6]">
|
|
340
536
|
{toolName}
|
|
341
537
|
</strong>
|
|
342
538
|
{props.call?.inputKeys?.length ? (
|
|
@@ -356,16 +552,140 @@ function redactedMessageSize(part: TranscriptPart): string | undefined {
|
|
|
356
552
|
}
|
|
357
553
|
|
|
358
554
|
function turnActorLabel(turn: ConversationTurn): string {
|
|
359
|
-
return (
|
|
360
|
-
requesterLabel(turn.requesterIdentity, turn.requester) ?? "unknown actor"
|
|
361
|
-
);
|
|
555
|
+
return requesterLabel(turn.requesterIdentity) ?? "User";
|
|
362
556
|
}
|
|
363
557
|
|
|
364
|
-
function
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
558
|
+
function turnMessageSummary(turn: ConversationTurn) {
|
|
559
|
+
const summary = summarizeMessages([turn]);
|
|
560
|
+
if (summary.total > 0) return summary;
|
|
561
|
+
const total = turnMessageCount(turn);
|
|
562
|
+
return total > 0 ? { items: [], total } : undefined;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function turnMeta(turn: ConversationTurn): MetricListItem[] {
|
|
566
|
+
const duration = formatTurnDuration(turn);
|
|
567
|
+
const tokenSummary = summarizeUsage([turn.cumulativeUsage]);
|
|
568
|
+
const toolSummary = summarizeToolCalls([turn]);
|
|
569
|
+
const messageSummary = turnMessageSummary(turn);
|
|
570
|
+
const items: Array<MetricListItem | undefined> = [
|
|
571
|
+
duration !== "none"
|
|
572
|
+
? {
|
|
573
|
+
content: (
|
|
574
|
+
<DurationMetric
|
|
575
|
+
endedAt={turn.completedAt ?? turn.lastSeenAt}
|
|
576
|
+
label={duration}
|
|
577
|
+
startedAt={turn.startedAt}
|
|
578
|
+
/>
|
|
579
|
+
),
|
|
580
|
+
key: "duration",
|
|
581
|
+
}
|
|
582
|
+
: undefined,
|
|
583
|
+
tokenSummary
|
|
584
|
+
? {
|
|
585
|
+
content: <TokenMetric summary={tokenSummary} />,
|
|
586
|
+
key: "tokens",
|
|
587
|
+
}
|
|
588
|
+
: undefined,
|
|
589
|
+
messageSummary
|
|
590
|
+
? {
|
|
591
|
+
content: <MessagesMetric summary={messageSummary} />,
|
|
592
|
+
key: "messages",
|
|
593
|
+
}
|
|
594
|
+
: undefined,
|
|
595
|
+
toolSummary.total > 0
|
|
596
|
+
? {
|
|
597
|
+
content: <ToolCallsMetric summary={toolSummary} />,
|
|
598
|
+
key: "tools",
|
|
599
|
+
}
|
|
600
|
+
: undefined,
|
|
601
|
+
turn.sentryTraceUrl
|
|
602
|
+
? {
|
|
603
|
+
content: (
|
|
604
|
+
<a
|
|
605
|
+
className="text-white no-underline hover:underline"
|
|
606
|
+
href={turn.sentryTraceUrl}
|
|
607
|
+
rel="noreferrer"
|
|
608
|
+
target="_blank"
|
|
609
|
+
>
|
|
610
|
+
View in Sentry
|
|
611
|
+
</a>
|
|
612
|
+
),
|
|
613
|
+
key: "sentry",
|
|
614
|
+
}
|
|
615
|
+
: undefined,
|
|
616
|
+
];
|
|
617
|
+
|
|
618
|
+
return items.filter((item): item is MetricListItem => Boolean(item));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Render the system prompt as a collapsed disclosure. Uses the same
|
|
623
|
+
* groupTranscriptParts → TranscriptPartView → TranscriptText pipeline as every
|
|
624
|
+
* other message so XML tag collapsing, syntax highlighting, and copy behaviour
|
|
625
|
+
* stay consistent. detectLanguage returns "xml" for the system prompt once the
|
|
626
|
+
* block-level XML heuristic in format.ts fires.
|
|
627
|
+
*/
|
|
628
|
+
function SystemMessageView(props: {
|
|
629
|
+
message: TranscriptMessage;
|
|
630
|
+
turn: ConversationTurn;
|
|
631
|
+
view: TranscriptViewMode;
|
|
632
|
+
}) {
|
|
633
|
+
const [open, setOpen] = useState(false);
|
|
634
|
+
const rawText = messageRawText(props.message);
|
|
635
|
+
const role = props.message.role;
|
|
636
|
+
const byteCount = new TextEncoder().encode(rawText).byteLength;
|
|
637
|
+
const renderedParts = groupTranscriptParts(props.message.parts);
|
|
638
|
+
const totalRenderedChildren = renderedParts.reduce(
|
|
639
|
+
(count, part) => count + countRenderedTranscriptChildren(part, role),
|
|
640
|
+
0,
|
|
641
|
+
);
|
|
642
|
+
let seenRenderedChildren = 0;
|
|
643
|
+
|
|
644
|
+
return (
|
|
645
|
+
<details
|
|
646
|
+
className={cn(transcriptMessageClass(role), !open && "gap-0")}
|
|
647
|
+
onToggle={(event) => {
|
|
648
|
+
if (event.currentTarget !== event.target) return;
|
|
649
|
+
setOpen(event.currentTarget.open);
|
|
650
|
+
}}
|
|
651
|
+
open={open}
|
|
652
|
+
>
|
|
653
|
+
<summary className="flex min-h-6 cursor-pointer list-none items-center [&::-webkit-details-marker]:hidden">
|
|
654
|
+
<div className={cn(transcriptRoleClass(role), "items-center")}>
|
|
655
|
+
<span className={transcriptRoleLabelClass(role)}>
|
|
656
|
+
{transcriptRoleLabel(role, props.turn)}
|
|
657
|
+
</span>
|
|
658
|
+
{open ? null : (
|
|
659
|
+
<span className="font-mono text-[0.78rem] text-[#888]">
|
|
660
|
+
{formatBytes(byteCount)}
|
|
661
|
+
</span>
|
|
662
|
+
)}
|
|
663
|
+
</div>
|
|
664
|
+
</summary>
|
|
665
|
+
{props.view === "raw" ? (
|
|
666
|
+
<HighlightedCode
|
|
667
|
+
code={rawText || "{}"}
|
|
668
|
+
language={detectLanguage(rawText)}
|
|
669
|
+
/>
|
|
670
|
+
) : (
|
|
671
|
+
<div className="grid min-w-0 gap-2">
|
|
672
|
+
{renderedParts.map((part, index) => {
|
|
673
|
+
const firstChildIndex = seenRenderedChildren;
|
|
674
|
+
seenRenderedChildren += countRenderedTranscriptChildren(part, role);
|
|
675
|
+
return (
|
|
676
|
+
<TranscriptPartView
|
|
677
|
+
firstChildIndex={firstChildIndex}
|
|
678
|
+
key={index}
|
|
679
|
+
lastChildIndex={totalRenderedChildren - 1}
|
|
680
|
+
part={part}
|
|
681
|
+
role={role}
|
|
682
|
+
/>
|
|
683
|
+
);
|
|
684
|
+
})}
|
|
685
|
+
</div>
|
|
686
|
+
)}
|
|
687
|
+
</details>
|
|
688
|
+
);
|
|
369
689
|
}
|
|
370
690
|
|
|
371
691
|
function TranscriptMessageView(props: {
|
|
@@ -373,11 +693,22 @@ function TranscriptMessageView(props: {
|
|
|
373
693
|
turn: ConversationTurn;
|
|
374
694
|
view: TranscriptViewMode;
|
|
375
695
|
}) {
|
|
696
|
+
if (transcriptRoleKind(props.message.role) === "system") {
|
|
697
|
+
return (
|
|
698
|
+
<SystemMessageView
|
|
699
|
+
message={props.message}
|
|
700
|
+
turn={props.turn}
|
|
701
|
+
view={props.view}
|
|
702
|
+
/>
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
376
706
|
const offset = formatMessageOffset(props.turn, props.message.timestamp);
|
|
377
707
|
const renderedParts = groupTranscriptParts(props.message.parts);
|
|
378
708
|
const rawText = messageRawText(props.message);
|
|
709
|
+
const role = props.message.role;
|
|
379
710
|
const totalRenderedChildren = renderedParts.reduce(
|
|
380
|
-
(count, part) => count + countRenderedTranscriptChildren(part),
|
|
711
|
+
(count, part) => count + countRenderedTranscriptChildren(part, role),
|
|
381
712
|
0,
|
|
382
713
|
);
|
|
383
714
|
let seenRenderedChildren = 0;
|
|
@@ -411,13 +742,14 @@ function TranscriptMessageView(props: {
|
|
|
411
742
|
<div className="grid min-w-0 gap-2">
|
|
412
743
|
{renderedParts.map((part, index) => {
|
|
413
744
|
const firstChildIndex = seenRenderedChildren;
|
|
414
|
-
seenRenderedChildren += countRenderedTranscriptChildren(part);
|
|
745
|
+
seenRenderedChildren += countRenderedTranscriptChildren(part, role);
|
|
415
746
|
return (
|
|
416
747
|
<TranscriptPartView
|
|
417
748
|
firstChildIndex={firstChildIndex}
|
|
418
749
|
key={index}
|
|
419
750
|
lastChildIndex={totalRenderedChildren - 1}
|
|
420
751
|
part={part}
|
|
752
|
+
role={role}
|
|
421
753
|
/>
|
|
422
754
|
);
|
|
423
755
|
})}
|
|
@@ -431,6 +763,7 @@ function TranscriptPartView(props: {
|
|
|
431
763
|
firstChildIndex: number;
|
|
432
764
|
lastChildIndex: number;
|
|
433
765
|
part: RenderedTranscriptPart;
|
|
766
|
+
role?: string;
|
|
434
767
|
}) {
|
|
435
768
|
if (props.part.kind === "tool") {
|
|
436
769
|
return (
|
|
@@ -444,6 +777,7 @@ function TranscriptPartView(props: {
|
|
|
444
777
|
<TranscriptText
|
|
445
778
|
firstChildIndex={props.firstChildIndex}
|
|
446
779
|
lastChildIndex={props.lastChildIndex}
|
|
780
|
+
role={props.role}
|
|
447
781
|
text={part.text ?? ""}
|
|
448
782
|
/>
|
|
449
783
|
);
|
|
@@ -457,9 +791,9 @@ function TranscriptPartView(props: {
|
|
|
457
791
|
const rendered = stringifyPartValue(value);
|
|
458
792
|
return (
|
|
459
793
|
<details className={toolFrameClass()}>
|
|
460
|
-
<summary className="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3
|
|
794
|
+
<summary className="grid cursor-pointer list-none grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 py-1.5 font-mono text-[0.82rem] leading-tight text-[#b8b8b8] transition-colors hover:text-[#d6d6d6] max-md:grid-cols-1 max-md:gap-1 [&::-webkit-details-marker]:hidden">
|
|
461
795
|
<span className="text-[#888]">{part.type}</span>
|
|
462
|
-
<strong className="min-w-0 break-words font-bold text-
|
|
796
|
+
<strong className="min-w-0 break-words font-bold text-[#d6d6d6]">
|
|
463
797
|
{part.name ?? part.id ?? "unknown"}
|
|
464
798
|
</strong>
|
|
465
799
|
<span className="min-w-0 break-words text-right max-md:text-left">
|
|
@@ -471,32 +805,49 @@ function TranscriptPartView(props: {
|
|
|
471
805
|
);
|
|
472
806
|
}
|
|
473
807
|
|
|
474
|
-
function ThinkingPartView(props: {
|
|
808
|
+
function ThinkingPartView(props: {
|
|
809
|
+
timestamp?: number;
|
|
810
|
+
turn?: ConversationTurn;
|
|
811
|
+
value: unknown;
|
|
812
|
+
}) {
|
|
475
813
|
const [open, setOpen] = useState(false);
|
|
476
814
|
const rendered = stringifyPartValue(props.value);
|
|
815
|
+
const offset = props.turn
|
|
816
|
+
? formatMessageOffset(props.turn, props.timestamp)
|
|
817
|
+
: undefined;
|
|
818
|
+
const meta = [
|
|
819
|
+
typeof props.timestamp === "number"
|
|
820
|
+
? formatMessageTimestamp(props.timestamp)
|
|
821
|
+
: undefined,
|
|
822
|
+
offset,
|
|
823
|
+
].filter(isString);
|
|
477
824
|
|
|
478
825
|
return (
|
|
479
826
|
<details
|
|
480
|
-
className="
|
|
827
|
+
className="py-0.5 text-[0.84rem] leading-relaxed text-[#888]"
|
|
481
828
|
onToggle={(event) => {
|
|
482
829
|
if (event.currentTarget !== event.target) return;
|
|
483
830
|
setOpen(event.currentTarget.open);
|
|
484
831
|
}}
|
|
485
832
|
open={open}
|
|
486
833
|
>
|
|
487
|
-
<summary className="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)] items-
|
|
488
|
-
<span className="
|
|
834
|
+
<summary className="grid cursor-pointer list-none grid-cols-[auto_minmax(0,1fr)_auto] items-baseline gap-3 transition-colors hover:text-[#b8b8b8] max-md:grid-cols-1 max-md:gap-1 [&::-webkit-details-marker]:hidden">
|
|
835
|
+
<span className="font-mono text-[0.78rem] not-italic text-[#777]">
|
|
836
|
+
thought
|
|
837
|
+
</span>
|
|
489
838
|
{open ? null : (
|
|
490
|
-
<span className="min-w-0 truncate">
|
|
839
|
+
<span className="min-w-0 truncate italic">
|
|
491
840
|
{previewToolValue(props.value)}
|
|
492
841
|
</span>
|
|
493
842
|
)}
|
|
843
|
+
{meta.length ? (
|
|
844
|
+
<span className="min-w-0 break-words text-right font-mono text-[0.78rem] not-italic text-[#777] max-md:text-left">
|
|
845
|
+
{meta.join(" · ")}
|
|
846
|
+
</span>
|
|
847
|
+
) : null}
|
|
494
848
|
</summary>
|
|
495
|
-
<div className="
|
|
496
|
-
|
|
497
|
-
code={rendered || "{}"}
|
|
498
|
-
language={detectOutputLanguage(rendered)}
|
|
499
|
-
/>
|
|
849
|
+
<div className="min-w-0 whitespace-pre-wrap break-words py-1 italic text-[#9a9a9a]">
|
|
850
|
+
{rendered || "{}"}
|
|
500
851
|
</div>
|
|
501
852
|
</details>
|
|
502
853
|
);
|