@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,17 +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
|
-
detectOutputLanguage,
|
|
7
11
|
transcriptRoleKind,
|
|
8
|
-
type TranscriptRoleKind,
|
|
9
12
|
formatBytes,
|
|
10
13
|
formatMessageOffset,
|
|
11
14
|
formatMessageTimestamp,
|
|
12
15
|
formatMs,
|
|
13
|
-
|
|
16
|
+
formatTurnDuration,
|
|
14
17
|
requesterLabel,
|
|
18
|
+
summarizeMessages,
|
|
19
|
+
summarizeToolCalls,
|
|
20
|
+
summarizeUsage,
|
|
21
|
+
turnMessageCount,
|
|
15
22
|
stringifyPartValue,
|
|
16
23
|
unavailableTranscriptLabel,
|
|
17
24
|
visualStatusForSession,
|
|
@@ -24,6 +31,13 @@ import type {
|
|
|
24
31
|
} from "../types";
|
|
25
32
|
import { StatusBadge } from "./StatusBadge";
|
|
26
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";
|
|
27
41
|
import { TranscriptText } from "./TranscriptText";
|
|
28
42
|
import { TranscriptToolView } from "./TranscriptToolView";
|
|
29
43
|
import {
|
|
@@ -40,6 +54,15 @@ import {
|
|
|
40
54
|
} from "./transcriptStyles";
|
|
41
55
|
import { previewToolValue } from "./transcriptPreview";
|
|
42
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
|
+
|
|
43
66
|
/** Render one conversation turn as actor messages and tool events. */
|
|
44
67
|
export function TurnTranscript(props: {
|
|
45
68
|
number: number;
|
|
@@ -148,22 +171,10 @@ function TurnHeader(props: { number: number; turn: ConversationTurn }) {
|
|
|
148
171
|
<div className="break-all text-[1.05rem] font-bold leading-tight tracking-normal">
|
|
149
172
|
Turn {props.number}
|
|
150
173
|
</div>
|
|
151
|
-
<
|
|
152
|
-
{
|
|
153
|
-
{props.turn
|
|
154
|
-
|
|
155
|
-
{" · "}
|
|
156
|
-
<a
|
|
157
|
-
className="text-white no-underline hover:underline"
|
|
158
|
-
href={props.turn.sentryTraceUrl}
|
|
159
|
-
rel="noreferrer"
|
|
160
|
-
target="_blank"
|
|
161
|
-
>
|
|
162
|
-
View in Sentry
|
|
163
|
-
</a>
|
|
164
|
-
</>
|
|
165
|
-
) : null}
|
|
166
|
-
</div>
|
|
174
|
+
<MetricList
|
|
175
|
+
className={cn(mutedTranscriptMetaClass(), "mt-1")}
|
|
176
|
+
items={turnMeta(props.turn)}
|
|
177
|
+
/>
|
|
167
178
|
</div>
|
|
168
179
|
<StatusBadge status={status} />
|
|
169
180
|
</div>
|
|
@@ -175,10 +186,28 @@ function TurnEvents(props: {
|
|
|
175
186
|
view: TranscriptViewMode;
|
|
176
187
|
}) {
|
|
177
188
|
return (
|
|
178
|
-
<div className="grid gap-
|
|
189
|
+
<div className="grid gap-2 pt-3">
|
|
179
190
|
{props.turn.transcriptAvailable ? (
|
|
180
|
-
|
|
181
|
-
|
|
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) => (
|
|
182
211
|
<TranscriptToolView
|
|
183
212
|
call={entry.call}
|
|
184
213
|
key={`${props.turn.id}:${index}`}
|
|
@@ -187,15 +216,8 @@ function TurnEvents(props: {
|
|
|
187
216
|
timestamp={entry.timestamp}
|
|
188
217
|
view={props.view}
|
|
189
218
|
/>
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
key={`${props.turn.id}:${index}`}
|
|
193
|
-
message={entry.message}
|
|
194
|
-
turn={props.turn}
|
|
195
|
-
view={props.view}
|
|
196
|
-
/>
|
|
197
|
-
),
|
|
198
|
-
)
|
|
219
|
+
)}
|
|
220
|
+
/>
|
|
199
221
|
) : props.turn.transcriptRedacted &&
|
|
200
222
|
props.turn.transcriptMetadata?.length ? (
|
|
201
223
|
<RedactedTranscriptView turn={props.turn} />
|
|
@@ -208,31 +230,187 @@ function TurnEvents(props: {
|
|
|
208
230
|
);
|
|
209
231
|
}
|
|
210
232
|
|
|
211
|
-
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
|
+
|
|
212
319
|
return (
|
|
213
320
|
<>
|
|
214
|
-
{
|
|
215
|
-
(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
),
|
|
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,
|
|
231
337
|
)}
|
|
232
338
|
</>
|
|
233
339
|
);
|
|
234
340
|
}
|
|
235
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
|
+
|
|
236
414
|
function RedactedMessageView(props: {
|
|
237
415
|
message: TranscriptMessage;
|
|
238
416
|
turn: ConversationTurn;
|
|
@@ -297,6 +475,31 @@ function RedactedMarker() {
|
|
|
297
475
|
);
|
|
298
476
|
}
|
|
299
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
|
+
|
|
300
503
|
function RedactedToolView(props: {
|
|
301
504
|
call?: TranscriptPart;
|
|
302
505
|
result?: TranscriptPart;
|
|
@@ -316,7 +519,9 @@ function RedactedToolView(props: {
|
|
|
316
519
|
? formatMs(props.resultTimestamp - props.timestamp)
|
|
317
520
|
: undefined;
|
|
318
521
|
const meta = [
|
|
319
|
-
|
|
522
|
+
typeof props.timestamp === "number"
|
|
523
|
+
? formatMessageTimestamp(props.timestamp)
|
|
524
|
+
: undefined,
|
|
320
525
|
duration,
|
|
321
526
|
props.result ? undefined : "missing result",
|
|
322
527
|
].filter(isString);
|
|
@@ -327,7 +532,7 @@ function RedactedToolView(props: {
|
|
|
327
532
|
raw
|
|
328
533
|
signature={
|
|
329
534
|
<>
|
|
330
|
-
<strong className="min-w-0 break-words font-bold text-
|
|
535
|
+
<strong className="min-w-0 break-words font-bold text-[#d6d6d6]">
|
|
331
536
|
{toolName}
|
|
332
537
|
</strong>
|
|
333
538
|
{props.call?.inputKeys?.length ? (
|
|
@@ -347,16 +552,140 @@ function redactedMessageSize(part: TranscriptPart): string | undefined {
|
|
|
347
552
|
}
|
|
348
553
|
|
|
349
554
|
function turnActorLabel(turn: ConversationTurn): string {
|
|
350
|
-
return (
|
|
351
|
-
|
|
352
|
-
|
|
555
|
+
return requesterLabel(turn.requesterIdentity) ?? "User";
|
|
556
|
+
}
|
|
557
|
+
|
|
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));
|
|
353
619
|
}
|
|
354
620
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
);
|
|
360
689
|
}
|
|
361
690
|
|
|
362
691
|
function TranscriptMessageView(props: {
|
|
@@ -364,6 +693,16 @@ function TranscriptMessageView(props: {
|
|
|
364
693
|
turn: ConversationTurn;
|
|
365
694
|
view: TranscriptViewMode;
|
|
366
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
|
+
|
|
367
706
|
const offset = formatMessageOffset(props.turn, props.message.timestamp);
|
|
368
707
|
const renderedParts = groupTranscriptParts(props.message.parts);
|
|
369
708
|
const rawText = messageRawText(props.message);
|
|
@@ -452,9 +791,9 @@ function TranscriptPartView(props: {
|
|
|
452
791
|
const rendered = stringifyPartValue(value);
|
|
453
792
|
return (
|
|
454
793
|
<details className={toolFrameClass()}>
|
|
455
|
-
<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">
|
|
456
795
|
<span className="text-[#888]">{part.type}</span>
|
|
457
|
-
<strong className="min-w-0 break-words font-bold text-
|
|
796
|
+
<strong className="min-w-0 break-words font-bold text-[#d6d6d6]">
|
|
458
797
|
{part.name ?? part.id ?? "unknown"}
|
|
459
798
|
</strong>
|
|
460
799
|
<span className="min-w-0 break-words text-right max-md:text-left">
|
|
@@ -466,32 +805,49 @@ function TranscriptPartView(props: {
|
|
|
466
805
|
);
|
|
467
806
|
}
|
|
468
807
|
|
|
469
|
-
function ThinkingPartView(props: {
|
|
808
|
+
function ThinkingPartView(props: {
|
|
809
|
+
timestamp?: number;
|
|
810
|
+
turn?: ConversationTurn;
|
|
811
|
+
value: unknown;
|
|
812
|
+
}) {
|
|
470
813
|
const [open, setOpen] = useState(false);
|
|
471
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);
|
|
472
824
|
|
|
473
825
|
return (
|
|
474
826
|
<details
|
|
475
|
-
className="
|
|
827
|
+
className="py-0.5 text-[0.84rem] leading-relaxed text-[#888]"
|
|
476
828
|
onToggle={(event) => {
|
|
477
829
|
if (event.currentTarget !== event.target) return;
|
|
478
830
|
setOpen(event.currentTarget.open);
|
|
479
831
|
}}
|
|
480
832
|
open={open}
|
|
481
833
|
>
|
|
482
|
-
<summary className="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)] items-
|
|
483
|
-
<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>
|
|
484
838
|
{open ? null : (
|
|
485
|
-
<span className="min-w-0 truncate">
|
|
839
|
+
<span className="min-w-0 truncate italic">
|
|
486
840
|
{previewToolValue(props.value)}
|
|
487
841
|
</span>
|
|
488
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}
|
|
489
848
|
</summary>
|
|
490
|
-
<div className="
|
|
491
|
-
|
|
492
|
-
code={rendered || "{}"}
|
|
493
|
-
language={detectOutputLanguage(rendered)}
|
|
494
|
-
/>
|
|
849
|
+
<div className="min-w-0 whitespace-pre-wrap break-words py-1 italic text-[#9a9a9a]">
|
|
850
|
+
{rendered || "{}"}
|
|
495
851
|
</div>
|
|
496
852
|
</details>
|
|
497
853
|
);
|