@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,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,7 +31,21 @@ import type {
|
|
|
24
31
|
} from "../types";
|
|
25
32
|
import { StatusBadge } from "./StatusBadge";
|
|
26
33
|
import { ToolFrame, toolFrameClass } from "./ToolFrame";
|
|
34
|
+
import {
|
|
35
|
+
TranscriptHeadingMeta,
|
|
36
|
+
TranscriptHeadingRow,
|
|
37
|
+
TranscriptThoughtLabel,
|
|
38
|
+
} from "./TranscriptHeadingRow";
|
|
39
|
+
import { MetricList, type MetricListItem } from "./Metric";
|
|
40
|
+
import {
|
|
41
|
+
DurationMetric,
|
|
42
|
+
MessagesMetric,
|
|
43
|
+
TokenMetric,
|
|
44
|
+
ToolCallsMetric,
|
|
45
|
+
} from "./TelemetryMetrics";
|
|
27
46
|
import { TranscriptText } from "./TranscriptText";
|
|
47
|
+
import { TranscriptThinkingView } from "./TranscriptThinkingView";
|
|
48
|
+
import { TranscriptToolRun } from "./TranscriptToolRun";
|
|
28
49
|
import { TranscriptToolView } from "./TranscriptToolView";
|
|
29
50
|
import {
|
|
30
51
|
countRenderedTranscriptChildren,
|
|
@@ -40,6 +61,11 @@ import {
|
|
|
40
61
|
} from "./transcriptStyles";
|
|
41
62
|
import { previewToolValue } from "./transcriptPreview";
|
|
42
63
|
|
|
64
|
+
type TranscriptEntry = ReturnType<typeof groupTranscriptMessages>[number];
|
|
65
|
+
type TranscriptMessageEntry = Extract<TranscriptEntry, { kind: "message" }>;
|
|
66
|
+
type TranscriptThinkingEntry = Extract<TranscriptEntry, { kind: "thinking" }>;
|
|
67
|
+
type TranscriptToolEntry = Extract<TranscriptEntry, { kind: "tool" }>;
|
|
68
|
+
|
|
43
69
|
/** Render one conversation turn as actor messages and tool events. */
|
|
44
70
|
export function TurnTranscript(props: {
|
|
45
71
|
number: number;
|
|
@@ -87,10 +113,10 @@ function transcriptMessageClass(role: string): string {
|
|
|
87
113
|
const kind = transcriptRoleKind(role);
|
|
88
114
|
|
|
89
115
|
return cn(
|
|
90
|
-
"grid min-w-0 gap-2 border-l-4 py-2 pl-3",
|
|
116
|
+
"grid min-w-0 grid-cols-[minmax(0,1fr)] gap-2 border-l-4 py-2 pl-3",
|
|
91
117
|
kind === "assistant" &&
|
|
92
|
-
"border-l-violet-300 bg-[rgba(190,170,255,0.
|
|
93
|
-
kind === "user" && "border-l-white/70 bg-white/[0.
|
|
118
|
+
"border-l-violet-300 bg-[rgba(190,170,255,0.14)] pr-3 text-white",
|
|
119
|
+
kind === "user" && "border-l-white/70 bg-white/[0.08] pr-3 text-[#f4f4f4]",
|
|
94
120
|
kind === "system" &&
|
|
95
121
|
"border-l-amber-300 bg-amber-300/[0.06] pr-3 text-[#f4f4f4]",
|
|
96
122
|
kind === "tool" && "border-l-[#888] text-[#b8b8b8]",
|
|
@@ -102,7 +128,7 @@ function transcriptRoleClass(role: string): string {
|
|
|
102
128
|
const kind = transcriptRoleKind(role);
|
|
103
129
|
|
|
104
130
|
return cn(
|
|
105
|
-
"
|
|
131
|
+
"text-[0.88rem] leading-snug",
|
|
106
132
|
kind === "assistant" && "text-[#d8ccff]",
|
|
107
133
|
kind === "user" && "text-white",
|
|
108
134
|
kind === "system" && "text-amber-200",
|
|
@@ -139,6 +165,32 @@ function TranscriptMessageShell(props: {
|
|
|
139
165
|
);
|
|
140
166
|
}
|
|
141
167
|
|
|
168
|
+
function TranscriptMessageHeader(props: {
|
|
169
|
+
meta?: Array<string | undefined>;
|
|
170
|
+
role: string;
|
|
171
|
+
turn: ConversationTurn;
|
|
172
|
+
}) {
|
|
173
|
+
const metaText = props.meta?.filter(isString).join(" · ");
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<TranscriptHeadingRow
|
|
177
|
+
left={
|
|
178
|
+
<span className={transcriptRoleLabelClass(props.role)}>
|
|
179
|
+
{transcriptRoleLabel(props.role, props.turn)}
|
|
180
|
+
</span>
|
|
181
|
+
}
|
|
182
|
+
leftClassName={transcriptRoleClass(props.role)}
|
|
183
|
+
right={
|
|
184
|
+
metaText ? (
|
|
185
|
+
<TranscriptHeadingMeta className="text-[0.78rem] text-[#888]">
|
|
186
|
+
{metaText}
|
|
187
|
+
</TranscriptHeadingMeta>
|
|
188
|
+
) : undefined
|
|
189
|
+
}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
142
194
|
function TurnHeader(props: { number: number; turn: ConversationTurn }) {
|
|
143
195
|
const status = visualStatusForSession(props.turn);
|
|
144
196
|
|
|
@@ -148,22 +200,10 @@ function TurnHeader(props: { number: number; turn: ConversationTurn }) {
|
|
|
148
200
|
<div className="break-all text-[1.05rem] font-bold leading-tight tracking-normal">
|
|
149
201
|
Turn {props.number}
|
|
150
202
|
</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>
|
|
203
|
+
<MetricList
|
|
204
|
+
className={cn(mutedTranscriptMetaClass(), "mt-1")}
|
|
205
|
+
items={turnMeta(props.turn)}
|
|
206
|
+
/>
|
|
167
207
|
</div>
|
|
168
208
|
<StatusBadge status={status} />
|
|
169
209
|
</div>
|
|
@@ -175,10 +215,28 @@ function TurnEvents(props: {
|
|
|
175
215
|
view: TranscriptViewMode;
|
|
176
216
|
}) {
|
|
177
217
|
return (
|
|
178
|
-
<div className="grid gap-
|
|
218
|
+
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-2 pt-3">
|
|
179
219
|
{props.turn.transcriptAvailable ? (
|
|
180
|
-
|
|
181
|
-
|
|
220
|
+
<TranscriptEntryList
|
|
221
|
+
entries={groupTranscriptMessages(props.turn.transcript)}
|
|
222
|
+
keyPrefix={props.turn.id}
|
|
223
|
+
renderMessage={(entry, index) => (
|
|
224
|
+
<TranscriptMessageView
|
|
225
|
+
key={`${props.turn.id}:${index}`}
|
|
226
|
+
message={entry.message}
|
|
227
|
+
turn={props.turn}
|
|
228
|
+
view={props.view}
|
|
229
|
+
/>
|
|
230
|
+
)}
|
|
231
|
+
renderThinking={(entry, index) => (
|
|
232
|
+
<TranscriptThinkingView
|
|
233
|
+
key={`${props.turn.id}:thinking:${index}`}
|
|
234
|
+
timestamp={entry.timestamp}
|
|
235
|
+
turn={props.turn}
|
|
236
|
+
value={entry.part.output}
|
|
237
|
+
/>
|
|
238
|
+
)}
|
|
239
|
+
renderTool={(entry, index) => (
|
|
182
240
|
<TranscriptToolView
|
|
183
241
|
call={entry.call}
|
|
184
242
|
key={`${props.turn.id}:${index}`}
|
|
@@ -187,15 +245,8 @@ function TurnEvents(props: {
|
|
|
187
245
|
timestamp={entry.timestamp}
|
|
188
246
|
view={props.view}
|
|
189
247
|
/>
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
key={`${props.turn.id}:${index}`}
|
|
193
|
-
message={entry.message}
|
|
194
|
-
turn={props.turn}
|
|
195
|
-
view={props.view}
|
|
196
|
-
/>
|
|
197
|
-
),
|
|
198
|
-
)
|
|
248
|
+
)}
|
|
249
|
+
/>
|
|
199
250
|
) : props.turn.transcriptRedacted &&
|
|
200
251
|
props.turn.transcriptMetadata?.length ? (
|
|
201
252
|
<RedactedTranscriptView turn={props.turn} />
|
|
@@ -208,28 +259,79 @@ function TurnEvents(props: {
|
|
|
208
259
|
);
|
|
209
260
|
}
|
|
210
261
|
|
|
262
|
+
function TranscriptEntryList(props: {
|
|
263
|
+
entries: TranscriptEntry[];
|
|
264
|
+
keyPrefix: string;
|
|
265
|
+
renderMessage: (entry: TranscriptMessageEntry, index: number) => ReactNode;
|
|
266
|
+
renderThinking: (entry: TranscriptThinkingEntry, index: number) => ReactNode;
|
|
267
|
+
renderTool: (entry: TranscriptToolEntry, index: number) => ReactNode;
|
|
268
|
+
}) {
|
|
269
|
+
const rows: ReactNode[] = [];
|
|
270
|
+
|
|
271
|
+
for (let index = 0; index < props.entries.length; ) {
|
|
272
|
+
const entry = props.entries[index]!;
|
|
273
|
+
|
|
274
|
+
if (entry.kind === "tool") {
|
|
275
|
+
const startIndex = index;
|
|
276
|
+
const tools: TranscriptToolEntry[] = [];
|
|
277
|
+
while (props.entries[index]?.kind === "tool") {
|
|
278
|
+
tools.push(props.entries[index] as TranscriptToolEntry);
|
|
279
|
+
index += 1;
|
|
280
|
+
}
|
|
281
|
+
rows.push(
|
|
282
|
+
<TranscriptToolRun
|
|
283
|
+
entries={tools}
|
|
284
|
+
key={`${props.keyPrefix}:tool-run:${startIndex}`}
|
|
285
|
+
keyPrefix={props.keyPrefix}
|
|
286
|
+
renderTool={props.renderTool}
|
|
287
|
+
startIndex={startIndex}
|
|
288
|
+
/>,
|
|
289
|
+
);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
rows.push(
|
|
294
|
+
<Fragment key={`${props.keyPrefix}:${entry.kind}:${index}`}>
|
|
295
|
+
{entry.kind === "thinking"
|
|
296
|
+
? props.renderThinking(entry, index)
|
|
297
|
+
: props.renderMessage(entry, index)}
|
|
298
|
+
</Fragment>,
|
|
299
|
+
);
|
|
300
|
+
index += 1;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return <>{rows}</>;
|
|
304
|
+
}
|
|
305
|
+
|
|
211
306
|
function RedactedTranscriptView(props: { turn: ConversationTurn }) {
|
|
212
307
|
return (
|
|
213
|
-
|
|
214
|
-
{groupTranscriptMessages(props.turn.transcriptMetadata ?? [])
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
308
|
+
<TranscriptEntryList
|
|
309
|
+
entries={groupTranscriptMessages(props.turn.transcriptMetadata ?? [])}
|
|
310
|
+
keyPrefix={`${props.turn.id}:redacted`}
|
|
311
|
+
renderMessage={(entry, index) => (
|
|
312
|
+
<RedactedMessageView
|
|
313
|
+
key={`${props.turn.id}:redacted:${index}`}
|
|
314
|
+
message={entry.message}
|
|
315
|
+
turn={props.turn}
|
|
316
|
+
/>
|
|
317
|
+
)}
|
|
318
|
+
renderThinking={(entry, index) => (
|
|
319
|
+
<RedactedThinkingView
|
|
320
|
+
key={`${props.turn.id}:redacted:thinking:${index}`}
|
|
321
|
+
timestamp={entry.timestamp}
|
|
322
|
+
turn={props.turn}
|
|
323
|
+
/>
|
|
324
|
+
)}
|
|
325
|
+
renderTool={(entry, index) => (
|
|
326
|
+
<RedactedToolView
|
|
327
|
+
call={entry.call}
|
|
328
|
+
key={`${props.turn.id}:redacted:${index}`}
|
|
329
|
+
result={entry.result}
|
|
330
|
+
resultTimestamp={entry.resultTimestamp}
|
|
331
|
+
timestamp={entry.timestamp}
|
|
332
|
+
/>
|
|
231
333
|
)}
|
|
232
|
-
|
|
334
|
+
/>
|
|
233
335
|
);
|
|
234
336
|
}
|
|
235
337
|
|
|
@@ -244,20 +346,12 @@ function RedactedMessageView(props: {
|
|
|
244
346
|
|
|
245
347
|
return (
|
|
246
348
|
<TranscriptMessageShell role={props.message.role}>
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
className="font-mono text-[0.78rem] text-[#888]"
|
|
254
|
-
key={`${index}-${value}`}
|
|
255
|
-
>
|
|
256
|
-
{value}
|
|
257
|
-
</span>
|
|
258
|
-
))}
|
|
259
|
-
</div>
|
|
260
|
-
<div className="grid min-w-0 gap-1 font-mono text-[0.9rem] leading-snug text-[#b8b8b8]">
|
|
349
|
+
<TranscriptMessageHeader
|
|
350
|
+
meta={meta}
|
|
351
|
+
role={props.message.role}
|
|
352
|
+
turn={props.turn}
|
|
353
|
+
/>
|
|
354
|
+
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-1 font-mono text-[0.9rem] leading-snug text-[#b8b8b8]">
|
|
261
355
|
{props.message.parts.map((part, index) => (
|
|
262
356
|
<RedactedPartLine key={index} part={part} />
|
|
263
357
|
))}
|
|
@@ -297,6 +391,41 @@ function RedactedMarker() {
|
|
|
297
391
|
);
|
|
298
392
|
}
|
|
299
393
|
|
|
394
|
+
function RedactedThinkingView(props: {
|
|
395
|
+
timestamp?: number;
|
|
396
|
+
turn: ConversationTurn;
|
|
397
|
+
}) {
|
|
398
|
+
const offset = formatMessageOffset(props.turn, props.timestamp);
|
|
399
|
+
const meta = [
|
|
400
|
+
typeof props.timestamp === "number"
|
|
401
|
+
? formatMessageTimestamp(props.timestamp)
|
|
402
|
+
: undefined,
|
|
403
|
+
offset,
|
|
404
|
+
].filter(isString);
|
|
405
|
+
const metaText = meta.join(" · ");
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<div className="py-1.5 text-[0.84rem] leading-relaxed text-[#888]">
|
|
409
|
+
<TranscriptHeadingRow
|
|
410
|
+
left={
|
|
411
|
+
<>
|
|
412
|
+
<TranscriptThoughtLabel />
|
|
413
|
+
<RedactedMarker />
|
|
414
|
+
</>
|
|
415
|
+
}
|
|
416
|
+
leftClassName="gap-3"
|
|
417
|
+
right={
|
|
418
|
+
metaText ? (
|
|
419
|
+
<TranscriptHeadingMeta className="text-[0.78rem] text-[#777]">
|
|
420
|
+
{metaText}
|
|
421
|
+
</TranscriptHeadingMeta>
|
|
422
|
+
) : undefined
|
|
423
|
+
}
|
|
424
|
+
/>
|
|
425
|
+
</div>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
300
429
|
function RedactedToolView(props: {
|
|
301
430
|
call?: TranscriptPart;
|
|
302
431
|
result?: TranscriptPart;
|
|
@@ -316,22 +445,27 @@ function RedactedToolView(props: {
|
|
|
316
445
|
? formatMs(props.resultTimestamp - props.timestamp)
|
|
317
446
|
: undefined;
|
|
318
447
|
const meta = [
|
|
319
|
-
|
|
448
|
+
typeof props.timestamp === "number"
|
|
449
|
+
? formatMessageTimestamp(props.timestamp)
|
|
450
|
+
: undefined,
|
|
320
451
|
duration,
|
|
321
452
|
props.result ? undefined : "missing result",
|
|
322
453
|
].filter(isString);
|
|
454
|
+
const mobileSummaryMeta =
|
|
455
|
+
duration ?? (props.call && !props.result ? "missing result" : undefined);
|
|
323
456
|
|
|
324
457
|
return (
|
|
325
458
|
<ToolFrame
|
|
326
459
|
meta={meta}
|
|
460
|
+
mobileSummaryMeta={mobileSummaryMeta}
|
|
327
461
|
raw
|
|
328
462
|
signature={
|
|
329
463
|
<>
|
|
330
|
-
<strong className="min-w-0 break-words font-bold text-
|
|
464
|
+
<strong className="min-w-0 break-words font-bold text-[#d6d6d6]">
|
|
331
465
|
{toolName}
|
|
332
466
|
</strong>
|
|
333
467
|
{props.call?.inputKeys?.length ? (
|
|
334
|
-
<code className="min-w-0 break-words font-[inherit] text-[#b8b8b8]">
|
|
468
|
+
<code className="min-w-0 break-words font-[inherit] text-[#b8b8b8] max-md:hidden">
|
|
335
469
|
({props.call.inputKeys.join(", ")})
|
|
336
470
|
</code>
|
|
337
471
|
) : null}
|
|
@@ -347,16 +481,135 @@ function redactedMessageSize(part: TranscriptPart): string | undefined {
|
|
|
347
481
|
}
|
|
348
482
|
|
|
349
483
|
function turnActorLabel(turn: ConversationTurn): string {
|
|
350
|
-
return (
|
|
351
|
-
|
|
352
|
-
|
|
484
|
+
return requesterLabel(turn.requesterIdentity) ?? "User";
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function turnMessageSummary(turn: ConversationTurn) {
|
|
488
|
+
const summary = summarizeMessages([turn]);
|
|
489
|
+
if (summary.total > 0) return summary;
|
|
490
|
+
const total = turnMessageCount(turn);
|
|
491
|
+
return total > 0 ? { items: [], total } : undefined;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function turnMeta(turn: ConversationTurn): MetricListItem[] {
|
|
495
|
+
const duration = formatTurnDuration(turn);
|
|
496
|
+
const tokenSummary = summarizeUsage([turn.cumulativeUsage]);
|
|
497
|
+
const toolSummary = summarizeToolCalls([turn]);
|
|
498
|
+
const messageSummary = turnMessageSummary(turn);
|
|
499
|
+
const items: Array<MetricListItem | undefined> = [
|
|
500
|
+
duration !== "none"
|
|
501
|
+
? {
|
|
502
|
+
content: (
|
|
503
|
+
<DurationMetric
|
|
504
|
+
endedAt={turn.completedAt ?? turn.lastSeenAt}
|
|
505
|
+
label={duration}
|
|
506
|
+
startedAt={turn.startedAt}
|
|
507
|
+
/>
|
|
508
|
+
),
|
|
509
|
+
key: "duration",
|
|
510
|
+
}
|
|
511
|
+
: undefined,
|
|
512
|
+
tokenSummary
|
|
513
|
+
? {
|
|
514
|
+
content: <TokenMetric summary={tokenSummary} />,
|
|
515
|
+
key: "tokens",
|
|
516
|
+
}
|
|
517
|
+
: undefined,
|
|
518
|
+
messageSummary
|
|
519
|
+
? {
|
|
520
|
+
content: <MessagesMetric summary={messageSummary} />,
|
|
521
|
+
key: "messages",
|
|
522
|
+
}
|
|
523
|
+
: undefined,
|
|
524
|
+
toolSummary.total > 0
|
|
525
|
+
? {
|
|
526
|
+
content: <ToolCallsMetric summary={toolSummary} />,
|
|
527
|
+
key: "tools",
|
|
528
|
+
}
|
|
529
|
+
: undefined,
|
|
530
|
+
turn.sentryTraceUrl
|
|
531
|
+
? {
|
|
532
|
+
content: (
|
|
533
|
+
<a
|
|
534
|
+
className="text-white no-underline hover:underline"
|
|
535
|
+
href={turn.sentryTraceUrl}
|
|
536
|
+
rel="noreferrer"
|
|
537
|
+
target="_blank"
|
|
538
|
+
>
|
|
539
|
+
View in Sentry
|
|
540
|
+
</a>
|
|
541
|
+
),
|
|
542
|
+
key: "sentry",
|
|
543
|
+
}
|
|
544
|
+
: undefined,
|
|
545
|
+
];
|
|
546
|
+
|
|
547
|
+
return items.filter((item): item is MetricListItem => Boolean(item));
|
|
353
548
|
}
|
|
354
549
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
550
|
+
/**
|
|
551
|
+
* Render the system prompt as a collapsed disclosure. Uses the same
|
|
552
|
+
* groupTranscriptParts → TranscriptPartView → TranscriptText pipeline as every
|
|
553
|
+
* other message so XML tag collapsing, syntax highlighting, and copy behaviour
|
|
554
|
+
* stay consistent. detectLanguage returns "xml" for the system prompt once the
|
|
555
|
+
* block-level XML heuristic in format.ts fires.
|
|
556
|
+
*/
|
|
557
|
+
function SystemMessageView(props: {
|
|
558
|
+
message: TranscriptMessage;
|
|
559
|
+
turn: ConversationTurn;
|
|
560
|
+
view: TranscriptViewMode;
|
|
561
|
+
}) {
|
|
562
|
+
const [open, setOpen] = useState(false);
|
|
563
|
+
const rawText = messageRawText(props.message);
|
|
564
|
+
const role = props.message.role;
|
|
565
|
+
const byteCount = new TextEncoder().encode(rawText).byteLength;
|
|
566
|
+
const renderedParts = groupTranscriptParts(props.message.parts);
|
|
567
|
+
const totalRenderedChildren = renderedParts.reduce(
|
|
568
|
+
(count, part) => count + countRenderedTranscriptChildren(part, role),
|
|
569
|
+
0,
|
|
570
|
+
);
|
|
571
|
+
let seenRenderedChildren = 0;
|
|
572
|
+
|
|
573
|
+
return (
|
|
574
|
+
<details
|
|
575
|
+
className={cn(transcriptMessageClass(role), !open && "gap-y-0")}
|
|
576
|
+
onToggle={(event) => {
|
|
577
|
+
if (event.currentTarget !== event.target) return;
|
|
578
|
+
setOpen(event.currentTarget.open);
|
|
579
|
+
}}
|
|
580
|
+
open={open}
|
|
581
|
+
>
|
|
582
|
+
<summary className="block min-h-6 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
|
|
583
|
+
<TranscriptMessageHeader
|
|
584
|
+
meta={[formatBytes(byteCount)]}
|
|
585
|
+
role={role}
|
|
586
|
+
turn={props.turn}
|
|
587
|
+
/>
|
|
588
|
+
</summary>
|
|
589
|
+
{props.view === "raw" ? (
|
|
590
|
+
<HighlightedCode
|
|
591
|
+
code={rawText || "{}"}
|
|
592
|
+
language={detectLanguage(rawText)}
|
|
593
|
+
/>
|
|
594
|
+
) : (
|
|
595
|
+
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-2">
|
|
596
|
+
{renderedParts.map((part, index) => {
|
|
597
|
+
const firstChildIndex = seenRenderedChildren;
|
|
598
|
+
seenRenderedChildren += countRenderedTranscriptChildren(part, role);
|
|
599
|
+
return (
|
|
600
|
+
<TranscriptPartView
|
|
601
|
+
firstChildIndex={firstChildIndex}
|
|
602
|
+
key={index}
|
|
603
|
+
lastChildIndex={totalRenderedChildren - 1}
|
|
604
|
+
part={part}
|
|
605
|
+
role={role}
|
|
606
|
+
/>
|
|
607
|
+
);
|
|
608
|
+
})}
|
|
609
|
+
</div>
|
|
610
|
+
)}
|
|
611
|
+
</details>
|
|
612
|
+
);
|
|
360
613
|
}
|
|
361
614
|
|
|
362
615
|
function TranscriptMessageView(props: {
|
|
@@ -364,6 +617,16 @@ function TranscriptMessageView(props: {
|
|
|
364
617
|
turn: ConversationTurn;
|
|
365
618
|
view: TranscriptViewMode;
|
|
366
619
|
}) {
|
|
620
|
+
if (transcriptRoleKind(props.message.role) === "system") {
|
|
621
|
+
return (
|
|
622
|
+
<SystemMessageView
|
|
623
|
+
message={props.message}
|
|
624
|
+
turn={props.turn}
|
|
625
|
+
view={props.view}
|
|
626
|
+
/>
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
367
630
|
const offset = formatMessageOffset(props.turn, props.message.timestamp);
|
|
368
631
|
const renderedParts = groupTranscriptParts(props.message.parts);
|
|
369
632
|
const rawText = messageRawText(props.message);
|
|
@@ -383,24 +646,18 @@ function TranscriptMessageView(props: {
|
|
|
383
646
|
event.preventDefault();
|
|
384
647
|
}}
|
|
385
648
|
>
|
|
386
|
-
<
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
{formatMessageTimestamp(props.message.timestamp)}
|
|
392
|
-
</span>
|
|
393
|
-
{offset ? (
|
|
394
|
-
<span className="font-mono text-[0.78rem] text-[#888]">{offset}</span>
|
|
395
|
-
) : null}
|
|
396
|
-
</div>
|
|
649
|
+
<TranscriptMessageHeader
|
|
650
|
+
meta={[formatMessageTimestamp(props.message.timestamp), offset]}
|
|
651
|
+
role={props.message.role}
|
|
652
|
+
turn={props.turn}
|
|
653
|
+
/>
|
|
397
654
|
{props.view === "raw" ? (
|
|
398
655
|
<HighlightedCode
|
|
399
656
|
code={rawText || "{}"}
|
|
400
657
|
language={detectLanguage(rawText)}
|
|
401
658
|
/>
|
|
402
659
|
) : (
|
|
403
|
-
<div className="grid min-w-0 gap-2">
|
|
660
|
+
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-2">
|
|
404
661
|
{renderedParts.map((part, index) => {
|
|
405
662
|
const firstChildIndex = seenRenderedChildren;
|
|
406
663
|
seenRenderedChildren += countRenderedTranscriptChildren(part, role);
|
|
@@ -446,57 +703,36 @@ function TranscriptPartView(props: {
|
|
|
446
703
|
|
|
447
704
|
const value = part.output;
|
|
448
705
|
if (part.type === "thinking") {
|
|
449
|
-
return <
|
|
706
|
+
return <TranscriptThinkingView value={value} />;
|
|
450
707
|
}
|
|
451
708
|
|
|
452
709
|
const rendered = stringifyPartValue(value);
|
|
453
710
|
return (
|
|
454
711
|
<details className={toolFrameClass()}>
|
|
455
|
-
<summary className="
|
|
456
|
-
<
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
712
|
+
<summary className="block cursor-pointer list-none py-1.5 font-mono text-[0.82rem] leading-tight text-[#b8b8b8] transition-colors hover:text-[#d6d6d6] [&::-webkit-details-marker]:hidden">
|
|
713
|
+
<TranscriptHeadingRow
|
|
714
|
+
left={
|
|
715
|
+
<>
|
|
716
|
+
<span className="text-[#888] max-md:hidden">{part.type}</span>
|
|
717
|
+
<strong className="min-w-0 break-words font-bold text-[#d6d6d6]">
|
|
718
|
+
{part.name ?? part.id ?? "unknown"}
|
|
719
|
+
</strong>
|
|
720
|
+
</>
|
|
721
|
+
}
|
|
722
|
+
leftClassName="gap-3"
|
|
723
|
+
right={
|
|
724
|
+
<span className="min-w-0 break-words text-right max-md:hidden">
|
|
725
|
+
{previewToolValue(value)}
|
|
726
|
+
</span>
|
|
727
|
+
}
|
|
728
|
+
rightClassName="min-w-0 max-md:hidden"
|
|
729
|
+
/>
|
|
463
730
|
</summary>
|
|
464
731
|
<HighlightedCode code={rendered || "{}"} language="json" />
|
|
465
732
|
</details>
|
|
466
733
|
);
|
|
467
734
|
}
|
|
468
735
|
|
|
469
|
-
function ThinkingPartView(props: { value: unknown }) {
|
|
470
|
-
const [open, setOpen] = useState(false);
|
|
471
|
-
const rendered = stringifyPartValue(props.value);
|
|
472
|
-
|
|
473
|
-
return (
|
|
474
|
-
<details
|
|
475
|
-
className="border border-[#beaaff]/20 bg-white/[0.03] transition-colors hover:border-[#beaaff]/45 hover:bg-[rgba(190,170,255,0.06)]"
|
|
476
|
-
onToggle={(event) => {
|
|
477
|
-
if (event.currentTarget !== event.target) return;
|
|
478
|
-
setOpen(event.currentTarget.open);
|
|
479
|
-
}}
|
|
480
|
-
open={open}
|
|
481
|
-
>
|
|
482
|
-
<summary className="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)] items-center gap-3 px-3 py-2 font-mono text-[0.8rem] leading-tight text-[#888] hover:bg-[rgba(190,170,255,0.07)] max-md:grid-cols-1 max-md:gap-1">
|
|
483
|
-
<span className="uppercase text-[#b8b8b8]">thinking</span>
|
|
484
|
-
{open ? null : (
|
|
485
|
-
<span className="min-w-0 truncate">
|
|
486
|
-
{previewToolValue(props.value)}
|
|
487
|
-
</span>
|
|
488
|
-
)}
|
|
489
|
-
</summary>
|
|
490
|
-
<div className="border-t border-[#beaaff]/15 px-3 py-3">
|
|
491
|
-
<HighlightedCode
|
|
492
|
-
code={rendered || "{}"}
|
|
493
|
-
language={detectOutputLanguage(rendered)}
|
|
494
|
-
/>
|
|
495
|
-
</div>
|
|
496
|
-
</details>
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
736
|
function isString(value: string | undefined): value is string {
|
|
501
737
|
return typeof value === "string" && value.length > 0;
|
|
502
738
|
}
|