@sentry/junior-dashboard 0.58.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.
Files changed (89) hide show
  1. package/LICENSE +201 -0
  2. package/dist/app.d.ts +22 -0
  3. package/dist/app.js +492 -0
  4. package/dist/auth.d.ts +30 -0
  5. package/dist/client/App.d.ts +2 -0
  6. package/dist/client/api.d.ts +10 -0
  7. package/dist/client/code.d.ts +15 -0
  8. package/dist/client/components/CommandRail.d.ts +6 -0
  9. package/dist/client/components/ConversationList.d.ts +7 -0
  10. package/dist/client/components/ConversationRowStats.d.ts +6 -0
  11. package/dist/client/components/ConversationStack.d.ts +5 -0
  12. package/dist/client/components/ConversationSummary.d.ts +5 -0
  13. package/dist/client/components/EmptyTelemetry.d.ts +5 -0
  14. package/dist/client/components/FilterTabs.d.ts +6 -0
  15. package/dist/client/components/JuniorLogo.d.ts +2 -0
  16. package/dist/client/components/LoadingView.d.ts +4 -0
  17. package/dist/client/components/Section.d.ts +6 -0
  18. package/dist/client/components/SectionHeader.d.ts +6 -0
  19. package/dist/client/components/SectionTitle.d.ts +5 -0
  20. package/dist/client/components/StatusBadge.d.ts +6 -0
  21. package/dist/client/components/ToolFrame.d.ts +10 -0
  22. package/dist/client/components/Transcript.d.ts +5 -0
  23. package/dist/client/components/TranscriptHeader.d.ts +7 -0
  24. package/dist/client/components/TranscriptLoading.d.ts +2 -0
  25. package/dist/client/components/TranscriptText.d.ts +6 -0
  26. package/dist/client/components/TranscriptToolView.d.ts +9 -0
  27. package/dist/client/components/TranscriptTurn.d.ts +8 -0
  28. package/dist/client/components/TurnDurationChart.d.ts +6 -0
  29. package/dist/client/components/statusStyles.d.ts +3 -0
  30. package/dist/client/components/transcriptPreview.d.ts +4 -0
  31. package/dist/client/components/transcriptRenderModel.d.ts +30 -0
  32. package/dist/client/components/transcriptStyles.d.ts +4 -0
  33. package/dist/client/format.d.ts +64 -0
  34. package/dist/client/pages/CommandCenter.d.ts +6 -0
  35. package/dist/client/pages/ConversationPage.d.ts +5 -0
  36. package/dist/client/pages/ConversationsPage.d.ts +5 -0
  37. package/dist/client/styles.d.ts +2 -0
  38. package/dist/client/types.d.ts +153 -0
  39. package/dist/client.d.ts +7 -0
  40. package/dist/client.js +56012 -0
  41. package/dist/config.d.ts +4 -0
  42. package/dist/handler.d.ts +2 -0
  43. package/dist/handler.js +563 -0
  44. package/dist/nitro.d.ts +16 -0
  45. package/dist/nitro.js +80 -0
  46. package/dist/tailwind.css +2 -0
  47. package/package.json +60 -0
  48. package/src/app.ts +421 -0
  49. package/src/auth.ts +202 -0
  50. package/src/client/App.tsx +175 -0
  51. package/src/client/api.ts +99 -0
  52. package/src/client/code.tsx +139 -0
  53. package/src/client/components/CommandRail.tsx +74 -0
  54. package/src/client/components/ConversationList.tsx +94 -0
  55. package/src/client/components/ConversationRowStats.tsx +21 -0
  56. package/src/client/components/ConversationStack.tsx +67 -0
  57. package/src/client/components/ConversationSummary.tsx +40 -0
  58. package/src/client/components/EmptyTelemetry.tsx +11 -0
  59. package/src/client/components/FilterTabs.tsx +35 -0
  60. package/src/client/components/JuniorLogo.tsx +8 -0
  61. package/src/client/components/LoadingView.tsx +16 -0
  62. package/src/client/components/Section.tsx +17 -0
  63. package/src/client/components/SectionHeader.tsx +16 -0
  64. package/src/client/components/SectionTitle.tsx +10 -0
  65. package/src/client/components/StatusBadge.tsx +40 -0
  66. package/src/client/components/ToolFrame.tsx +52 -0
  67. package/src/client/components/Transcript.tsx +39 -0
  68. package/src/client/components/TranscriptHeader.tsx +52 -0
  69. package/src/client/components/TranscriptLoading.tsx +10 -0
  70. package/src/client/components/TranscriptText.tsx +45 -0
  71. package/src/client/components/TranscriptToolView.tsx +207 -0
  72. package/src/client/components/TranscriptTurn.tsx +487 -0
  73. package/src/client/components/TurnDurationChart.tsx +349 -0
  74. package/src/client/components/statusStyles.ts +9 -0
  75. package/src/client/components/transcriptPreview.ts +20 -0
  76. package/src/client/components/transcriptRenderModel.ts +193 -0
  77. package/src/client/components/transcriptStyles.ts +11 -0
  78. package/src/client/format.ts +628 -0
  79. package/src/client/pages/CommandCenter.tsx +37 -0
  80. package/src/client/pages/ConversationPage.tsx +145 -0
  81. package/src/client/pages/ConversationsPage.tsx +59 -0
  82. package/src/client/styles.ts +6 -0
  83. package/src/client/types.ts +153 -0
  84. package/src/client.tsx +79 -0
  85. package/src/config.ts +72 -0
  86. package/src/handler.ts +26 -0
  87. package/src/nitro.ts +110 -0
  88. package/src/tailwind.css +13 -0
  89. package/src/virtual-modules.d.ts +5 -0
@@ -0,0 +1,207 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { HighlightedCode } from "../code";
4
+ import {
5
+ formatBytes,
6
+ formatMessageTimestamp,
7
+ formatMs,
8
+ stringifyPartValue,
9
+ } from "../format";
10
+ import { cn } from "../styles";
11
+ import type { TranscriptPart } from "../types";
12
+ import { ToolFrame } from "./ToolFrame";
13
+ import { isPreviewableValue } from "./transcriptPreview";
14
+
15
+ /** Render a tool call/result pair in rich or raw transcript mode. */
16
+ export function TranscriptToolView(props: {
17
+ call?: TranscriptPart;
18
+ result?: TranscriptPart;
19
+ resultTimestamp?: number;
20
+ timestamp?: number;
21
+ view?: "raw" | "rich";
22
+ }) {
23
+ const toolName =
24
+ props.call?.name ??
25
+ props.result?.name ??
26
+ props.call?.id ??
27
+ props.result?.id ??
28
+ "unknown";
29
+ const input = props.call?.input;
30
+ const output = props.result?.output;
31
+ const outputBytes = props.result
32
+ ? new TextEncoder().encode(stringifyPartValue(output)).length
33
+ : undefined;
34
+ const duration =
35
+ typeof props.timestamp === "number" &&
36
+ typeof props.resultTimestamp === "number" &&
37
+ props.resultTimestamp >= props.timestamp
38
+ ? formatMs(props.resultTimestamp - props.timestamp)
39
+ : undefined;
40
+ const meta = [
41
+ props.timestamp ? formatMessageTimestamp(props.timestamp) : undefined,
42
+ duration,
43
+ props.result ? formatBytes(outputBytes) : undefined,
44
+ props.result ? undefined : "missing result",
45
+ ].filter(isString);
46
+ const args = <ToolArgumentsPreview input={input} />;
47
+
48
+ if (props.view === "raw") {
49
+ return (
50
+ <ToolFrame
51
+ meta={meta}
52
+ raw
53
+ signature={
54
+ <strong className="min-w-0 break-words font-bold text-white">
55
+ {toolName}
56
+ </strong>
57
+ }
58
+ >
59
+ <ToolBodySection>
60
+ <HighlightedCode
61
+ code={stringifyPartValue({
62
+ call: props.call,
63
+ result: props.result,
64
+ })}
65
+ language="json"
66
+ />
67
+ </ToolBodySection>
68
+ </ToolFrame>
69
+ );
70
+ }
71
+
72
+ return (
73
+ <ToolFrame
74
+ meta={meta}
75
+ signature={
76
+ <>
77
+ <strong className="min-w-0 break-words font-bold text-white">
78
+ {toolName}
79
+ </strong>
80
+ {isPreviewableValue(input) ? (
81
+ <code className="min-w-0 break-words font-[inherit] text-[#b8b8b8]">
82
+ ({args})
83
+ </code>
84
+ ) : null}
85
+ </>
86
+ }
87
+ >
88
+ {props.call ? (
89
+ <ToolBodySection label="arguments">
90
+ <HighlightedCode
91
+ code={stringifyPartValue(input) || "{}"}
92
+ language="json"
93
+ />
94
+ </ToolBodySection>
95
+ ) : null}
96
+ {props.result ? (
97
+ <ToolBodySection label="result">
98
+ <HighlightedCode
99
+ code={stringifyPartValue(output) || "{}"}
100
+ language="json"
101
+ />
102
+ </ToolBodySection>
103
+ ) : null}
104
+ </ToolFrame>
105
+ );
106
+ }
107
+
108
+ function ToolBodySection(props: {
109
+ children: ReactNode;
110
+ label?: string;
111
+ padded?: boolean;
112
+ }) {
113
+ return (
114
+ <div
115
+ className={cn(
116
+ "border-t border-white/10 px-3",
117
+ props.padded === false ? "" : "py-3",
118
+ )}
119
+ >
120
+ {props.label ? (
121
+ <div className="pb-2 font-mono text-[0.78rem] uppercase leading-none text-[#888]">
122
+ {props.label}
123
+ </div>
124
+ ) : null}
125
+ {props.children}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ function ToolArgumentsPreview(props: { input: unknown }) {
131
+ const input = props.input;
132
+ if (input == null || input === "") return null;
133
+
134
+ if (typeof input === "string") {
135
+ const formatted = stringifyPartValue(input).replace(/\s+/g, " ").trim();
136
+ return <ToolArgValue value={truncateText(formatted, 96)} />;
137
+ }
138
+
139
+ if (Array.isArray(input)) {
140
+ return (
141
+ <ToolArgValue
142
+ value={truncateText(
143
+ stringifyPartValue(input).replace(/\s+/g, " ").trim(),
144
+ 96,
145
+ )}
146
+ />
147
+ );
148
+ }
149
+
150
+ if (typeof input === "object") {
151
+ const entries = Object.entries(input as Record<string, unknown>).slice(
152
+ 0,
153
+ 4,
154
+ );
155
+ return (
156
+ <>
157
+ {entries.map(([key, value], index) => (
158
+ <ToolArgEntry
159
+ index={index}
160
+ key={key}
161
+ name={key}
162
+ value={previewArgumentValue(value)}
163
+ />
164
+ ))}
165
+ </>
166
+ );
167
+ }
168
+
169
+ return <ToolArgValue value={truncateText(String(input), 96)} />;
170
+ }
171
+
172
+ function ToolArgEntry(props: { index: number; name: string; value: string }) {
173
+ return (
174
+ <span>
175
+ {props.index > 0 ? <span className="text-[#888]">, </span> : null}
176
+ <span className="text-white">{props.name}</span>
177
+ <span className="text-[#888]">: </span>
178
+ <ToolArgValue value={props.value} />
179
+ </span>
180
+ );
181
+ }
182
+
183
+ function ToolArgValue(props: { value: string }) {
184
+ return <span className="text-[#b8b8b8]">{props.value}</span>;
185
+ }
186
+
187
+ function previewArgumentValue(value: unknown): string {
188
+ if (value == null) return "null";
189
+ if (typeof value === "string") return JSON.stringify(truncateText(value, 48));
190
+ if (typeof value === "number" || typeof value === "boolean") {
191
+ return String(value);
192
+ }
193
+ return truncateText(
194
+ stringifyPartValue(value).replace(/\s+/g, " ").trim(),
195
+ 48,
196
+ );
197
+ }
198
+
199
+ function truncateText(value: string, maxLength: number): string {
200
+ return value.length > maxLength
201
+ ? `${value.slice(0, Math.max(0, maxLength - 3))}...`
202
+ : value;
203
+ }
204
+
205
+ function isString(value: string | undefined): value is string {
206
+ return typeof value === "string" && value.length > 0;
207
+ }
@@ -0,0 +1,487 @@
1
+ import type { ClipboardEventHandler, ReactNode } from "react";
2
+
3
+ import { HighlightedCode } from "../code";
4
+ import {
5
+ detectLanguage,
6
+ formatBytes,
7
+ formatMessageOffset,
8
+ formatMessageTimestamp,
9
+ formatMs,
10
+ formatUsage,
11
+ requesterLabel,
12
+ stringifyPartValue,
13
+ unavailableTranscriptLabel,
14
+ visualStatusForSession,
15
+ } from "../format";
16
+ import { cn } from "../styles";
17
+ import type {
18
+ ConversationTurn,
19
+ TranscriptMessage,
20
+ TranscriptPart,
21
+ } from "../types";
22
+ import { StatusBadge } from "./StatusBadge";
23
+ import { ToolFrame, toolFrameClass } from "./ToolFrame";
24
+ import { TranscriptText } from "./TranscriptText";
25
+ import { TranscriptToolView } from "./TranscriptToolView";
26
+ import {
27
+ countRenderedTranscriptChildren,
28
+ groupTranscriptMessages,
29
+ groupTranscriptParts,
30
+ messageRawText,
31
+ type RenderedTranscriptPart,
32
+ type TranscriptViewMode,
33
+ } from "./transcriptRenderModel";
34
+ import {
35
+ transcriptEmptyClass,
36
+ mutedTranscriptMetaClass,
37
+ } from "./transcriptStyles";
38
+ import { previewToolValue } from "./transcriptPreview";
39
+
40
+ /** Render one conversation turn as actor messages and tool events. */
41
+ export function TurnTranscript(props: {
42
+ number: number;
43
+ turn: ConversationTurn;
44
+ view: TranscriptViewMode;
45
+ }) {
46
+ const status = visualStatusForSession(props.turn);
47
+
48
+ return (
49
+ <section className="grid min-w-0 grid-cols-[0.875rem_minmax(0,1fr)] gap-3 border-t border-white/10 py-4 first:border-t-0">
50
+ <div className="flex flex-col items-center pt-2" aria-hidden="true">
51
+ <span className={turnMarkerClass(status)} />
52
+ <span className="mt-2 w-px flex-1 bg-[#beaaff]/20" />
53
+ </div>
54
+ <div className="min-w-0">
55
+ <TurnHeader number={props.number} turn={props.turn} />
56
+ <TurnEvents turn={props.turn} view={props.view} />
57
+ </div>
58
+ </section>
59
+ );
60
+ }
61
+
62
+ function turnMarkerClass(
63
+ status: ReturnType<typeof visualStatusForSession>,
64
+ ): string {
65
+ return cn(
66
+ "size-2.5 shrink-0 border",
67
+ status === "active" && "border-emerald-300 bg-emerald-300",
68
+ status === "hung" && "border-amber-300 bg-amber-300",
69
+ status === "failed" && "border-rose-300 bg-rose-300",
70
+ status === "idle" && "border-[#beaaff]/70 bg-[#beaaff]/50",
71
+ );
72
+ }
73
+
74
+ type TranscriptRoleKind = "assistant" | "other" | "system" | "tool" | "user";
75
+
76
+ function transcriptRoleKind(role: string): TranscriptRoleKind {
77
+ const normalized = role.toLowerCase();
78
+ if (normalized === "assistant") return "assistant";
79
+ if (normalized === "user") return "user";
80
+ if (normalized === "system") return "system";
81
+ if (normalized.includes("tool")) return "tool";
82
+ return "other";
83
+ }
84
+
85
+ function transcriptRoleLabel(role: string, turn: ConversationTurn): string {
86
+ const kind = transcriptRoleKind(role);
87
+ if (kind === "assistant") return "Junior";
88
+ if (kind === "user") return turnActorLabel(turn);
89
+ if (kind === "system") return "System";
90
+ if (kind === "tool") return "Tool";
91
+ return role;
92
+ }
93
+
94
+ function transcriptMessageClass(role: string): string {
95
+ const kind = transcriptRoleKind(role);
96
+
97
+ return cn(
98
+ "grid min-w-0 gap-2 border-l-4 py-2 pl-3",
99
+ kind === "assistant" &&
100
+ "border-l-violet-300 bg-[rgba(190,170,255,0.08)] pr-3 text-white",
101
+ kind === "user" && "border-l-white/70 bg-white/[0.04] pr-3 text-[#f4f4f4]",
102
+ kind === "system" &&
103
+ "border-l-amber-300 bg-amber-300/[0.06] pr-3 text-[#f4f4f4]",
104
+ kind === "tool" && "border-l-[#888] text-[#b8b8b8]",
105
+ kind === "other" && "border-l-white/35 text-[#f4f4f4]",
106
+ );
107
+ }
108
+
109
+ function transcriptRoleClass(role: string): string {
110
+ const kind = transcriptRoleKind(role);
111
+
112
+ return cn(
113
+ "flex flex-wrap items-baseline gap-2 text-[0.88rem] leading-snug",
114
+ kind === "assistant" && "text-[#d8ccff]",
115
+ kind === "user" && "text-white",
116
+ kind === "system" && "text-amber-200",
117
+ kind === "tool" && "text-[#b8b8b8]",
118
+ kind === "other" && "text-[#f4f4f4]",
119
+ );
120
+ }
121
+
122
+ function transcriptRoleLabelClass(role: string): string {
123
+ const kind = transcriptRoleKind(role);
124
+
125
+ return cn(
126
+ "inline-block max-w-full break-all text-[0.98rem] font-extrabold leading-tight",
127
+ kind === "assistant" && "text-violet-200",
128
+ kind === "user" && "text-white",
129
+ kind === "system" && "text-amber-200",
130
+ kind === "tool" && "text-[#b8b8b8]",
131
+ kind === "other" && "text-white",
132
+ );
133
+ }
134
+
135
+ function TranscriptMessageShell(props: {
136
+ children: ReactNode;
137
+ onCopy?: ClipboardEventHandler<HTMLElement>;
138
+ role: string;
139
+ }) {
140
+ return (
141
+ <article
142
+ className={transcriptMessageClass(props.role)}
143
+ onCopy={props.onCopy}
144
+ >
145
+ {props.children}
146
+ </article>
147
+ );
148
+ }
149
+
150
+ function TurnHeader(props: { number: number; turn: ConversationTurn }) {
151
+ const status = visualStatusForSession(props.turn);
152
+
153
+ return (
154
+ <div className="flex items-start justify-between gap-3 max-md:flex-col">
155
+ <div className="min-w-0">
156
+ <div className="break-all text-[1.05rem] font-bold leading-tight tracking-normal">
157
+ Turn {props.number}
158
+ </div>
159
+ <div className={cn(mutedTranscriptMetaClass(), "mt-1")}>
160
+ {turnMeta(props.turn).join(" · ")}
161
+ {props.turn.sentryTraceUrl ? (
162
+ <>
163
+ {" · "}
164
+ <a
165
+ className="text-white no-underline hover:underline"
166
+ href={props.turn.sentryTraceUrl}
167
+ rel="noreferrer"
168
+ target="_blank"
169
+ >
170
+ View in Sentry
171
+ </a>
172
+ </>
173
+ ) : null}
174
+ </div>
175
+ </div>
176
+ <StatusBadge status={status} />
177
+ </div>
178
+ );
179
+ }
180
+
181
+ function TurnEvents(props: {
182
+ turn: ConversationTurn;
183
+ view: TranscriptViewMode;
184
+ }) {
185
+ return (
186
+ <div className="grid gap-3 pt-3">
187
+ {props.turn.transcriptAvailable ? (
188
+ groupTranscriptMessages(props.turn.transcript).map((entry, index) =>
189
+ entry.kind === "tool" ? (
190
+ <TranscriptToolView
191
+ call={entry.call}
192
+ key={`${props.turn.id}:${index}`}
193
+ result={entry.result}
194
+ resultTimestamp={entry.resultTimestamp}
195
+ timestamp={entry.timestamp}
196
+ view={props.view}
197
+ />
198
+ ) : (
199
+ <TranscriptMessageView
200
+ key={`${props.turn.id}:${index}`}
201
+ message={entry.message}
202
+ turn={props.turn}
203
+ view={props.view}
204
+ />
205
+ ),
206
+ )
207
+ ) : props.turn.transcriptRedacted &&
208
+ props.turn.transcriptMetadata?.length ? (
209
+ <RedactedTranscriptView turn={props.turn} />
210
+ ) : (
211
+ <div className={transcriptEmptyClass()}>
212
+ {unavailableTranscriptLabel(props.turn)}
213
+ </div>
214
+ )}
215
+ </div>
216
+ );
217
+ }
218
+
219
+ function RedactedTranscriptView(props: { turn: ConversationTurn }) {
220
+ return (
221
+ <>
222
+ {groupTranscriptMessages(props.turn.transcriptMetadata ?? []).map(
223
+ (entry, index) =>
224
+ entry.kind === "tool" ? (
225
+ <RedactedToolView
226
+ call={entry.call}
227
+ key={`${props.turn.id}:redacted:${index}`}
228
+ result={entry.result}
229
+ resultTimestamp={entry.resultTimestamp}
230
+ timestamp={entry.timestamp}
231
+ />
232
+ ) : (
233
+ <RedactedMessageView
234
+ key={`${props.turn.id}:redacted:${index}`}
235
+ message={entry.message}
236
+ turn={props.turn}
237
+ />
238
+ ),
239
+ )}
240
+ </>
241
+ );
242
+ }
243
+
244
+ function RedactedMessageView(props: {
245
+ message: TranscriptMessage;
246
+ turn: ConversationTurn;
247
+ }) {
248
+ const offset = formatMessageOffset(props.turn, props.message.timestamp);
249
+ const meta = [formatMessageTimestamp(props.message.timestamp), offset].filter(
250
+ isString,
251
+ );
252
+
253
+ return (
254
+ <TranscriptMessageShell role={props.message.role}>
255
+ <div className={transcriptRoleClass(props.message.role)}>
256
+ <span className={transcriptRoleLabelClass(props.message.role)}>
257
+ {transcriptRoleLabel(props.message.role, props.turn)}
258
+ </span>
259
+ {meta.map((value, index) => (
260
+ <span
261
+ className="font-mono text-[0.78rem] text-[#888]"
262
+ key={`${index}-${value}`}
263
+ >
264
+ {value}
265
+ </span>
266
+ ))}
267
+ </div>
268
+ <div className="grid min-w-0 gap-1 font-mono text-[0.9rem] leading-snug text-[#b8b8b8]">
269
+ {props.message.parts.map((part, index) => (
270
+ <RedactedPartLine key={index} part={part} />
271
+ ))}
272
+ </div>
273
+ </TranscriptMessageShell>
274
+ );
275
+ }
276
+
277
+ function RedactedPartLine(props: { part: TranscriptPart }) {
278
+ if (props.part.type === "text") {
279
+ return <RedactedMetadataRow meta={redactedMessageSize(props.part)} />;
280
+ }
281
+ if (props.part.type === "thinking") {
282
+ return <RedactedMetadataRow />;
283
+ }
284
+ return <RedactedMetadataRow />;
285
+ }
286
+
287
+ function RedactedMetadataRow(props: { meta?: string }) {
288
+ return (
289
+ <div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 py-1 max-md:grid-cols-1">
290
+ <RedactedMarker />
291
+ {props.meta ? (
292
+ <span className="min-w-0 break-words text-right text-[#888] max-md:text-left">
293
+ {props.meta}
294
+ </span>
295
+ ) : null}
296
+ </div>
297
+ );
298
+ }
299
+
300
+ function RedactedMarker() {
301
+ return (
302
+ <code className="inline-flex w-fit font-mono text-[0.82rem] leading-tight text-[#b8b8b8]">
303
+ {"<redacted>"}
304
+ </code>
305
+ );
306
+ }
307
+
308
+ function RedactedToolView(props: {
309
+ call?: TranscriptPart;
310
+ result?: TranscriptPart;
311
+ resultTimestamp?: number;
312
+ timestamp?: number;
313
+ }) {
314
+ const toolName =
315
+ props.call?.name ??
316
+ props.result?.name ??
317
+ props.call?.id ??
318
+ props.result?.id ??
319
+ "unknown";
320
+ const duration =
321
+ typeof props.timestamp === "number" &&
322
+ typeof props.resultTimestamp === "number" &&
323
+ props.resultTimestamp >= props.timestamp
324
+ ? formatMs(props.resultTimestamp - props.timestamp)
325
+ : undefined;
326
+ const meta = [
327
+ props.timestamp ? formatMessageTimestamp(props.timestamp) : undefined,
328
+ duration,
329
+ props.result ? undefined : "missing result",
330
+ ].filter(isString);
331
+
332
+ return (
333
+ <ToolFrame
334
+ meta={meta}
335
+ raw
336
+ signature={
337
+ <>
338
+ <strong className="min-w-0 break-words font-bold text-white">
339
+ {toolName}
340
+ </strong>
341
+ {props.call?.inputKeys?.length ? (
342
+ <code className="min-w-0 break-words font-[inherit] text-[#b8b8b8]">
343
+ ({props.call.inputKeys.join(", ")})
344
+ </code>
345
+ ) : null}
346
+ </>
347
+ }
348
+ />
349
+ );
350
+ }
351
+
352
+ function redactedMessageSize(part: TranscriptPart): string | undefined {
353
+ if (typeof part.bytes === "number") return formatBytes(part.bytes);
354
+ return typeof part.chars === "number" ? `${part.chars} chars` : undefined;
355
+ }
356
+
357
+ function turnActorLabel(turn: ConversationTurn): string {
358
+ return (
359
+ requesterLabel(turn.requesterIdentity, turn.requester) ?? "unknown actor"
360
+ );
361
+ }
362
+
363
+ function turnMeta(turn: ConversationTurn): string[] {
364
+ return [
365
+ formatMs(turn.cumulativeDurationMs),
366
+ formatUsage(turn.cumulativeUsage),
367
+ ].filter((value) => value && value !== "none");
368
+ }
369
+
370
+ function TranscriptMessageView(props: {
371
+ message: TranscriptMessage;
372
+ turn: ConversationTurn;
373
+ view: TranscriptViewMode;
374
+ }) {
375
+ const offset = formatMessageOffset(props.turn, props.message.timestamp);
376
+ const renderedParts = groupTranscriptParts(props.message.parts);
377
+ const rawText = messageRawText(props.message);
378
+ const totalRenderedChildren = renderedParts.reduce(
379
+ (count, part) => count + countRenderedTranscriptChildren(part),
380
+ 0,
381
+ );
382
+ let seenRenderedChildren = 0;
383
+
384
+ return (
385
+ <TranscriptMessageShell
386
+ role={props.message.role}
387
+ onCopy={(event) => {
388
+ if (props.view !== "rich" || !rawText) return;
389
+ event.clipboardData.setData("text/plain", rawText);
390
+ event.preventDefault();
391
+ }}
392
+ >
393
+ <div className={transcriptRoleClass(props.message.role)}>
394
+ <span className={transcriptRoleLabelClass(props.message.role)}>
395
+ {transcriptRoleLabel(props.message.role, props.turn)}
396
+ </span>
397
+ <span className="font-mono text-[0.78rem] text-[#888]">
398
+ {formatMessageTimestamp(props.message.timestamp)}
399
+ </span>
400
+ {offset ? (
401
+ <span className="font-mono text-[0.78rem] text-[#888]">{offset}</span>
402
+ ) : null}
403
+ </div>
404
+ {props.view === "raw" ? (
405
+ <HighlightedCode
406
+ code={rawText || "{}"}
407
+ language={detectLanguage(rawText)}
408
+ />
409
+ ) : (
410
+ <div className="grid min-w-0 gap-2">
411
+ {renderedParts.map((part, index) => {
412
+ const firstChildIndex = seenRenderedChildren;
413
+ seenRenderedChildren += countRenderedTranscriptChildren(part);
414
+ return (
415
+ <TranscriptPartView
416
+ firstChildIndex={firstChildIndex}
417
+ key={index}
418
+ lastChildIndex={totalRenderedChildren - 1}
419
+ part={part}
420
+ />
421
+ );
422
+ })}
423
+ </div>
424
+ )}
425
+ </TranscriptMessageShell>
426
+ );
427
+ }
428
+
429
+ function TranscriptPartView(props: {
430
+ firstChildIndex: number;
431
+ lastChildIndex: number;
432
+ part: RenderedTranscriptPart;
433
+ }) {
434
+ if (props.part.kind === "tool") {
435
+ return (
436
+ <TranscriptToolView call={props.part.call} result={props.part.result} />
437
+ );
438
+ }
439
+
440
+ const part = props.part.part;
441
+ if (part.type === "text") {
442
+ return (
443
+ <TranscriptText
444
+ firstChildIndex={props.firstChildIndex}
445
+ lastChildIndex={props.lastChildIndex}
446
+ text={part.text ?? ""}
447
+ />
448
+ );
449
+ }
450
+
451
+ const value = part.output;
452
+ if (part.type === "thinking") {
453
+ const rendered = stringifyPartValue(value);
454
+ return (
455
+ <details className="border border-[#beaaff]/20 bg-white/[0.03] transition-colors hover:border-[#beaaff]/45 hover:bg-[rgba(190,170,255,0.06)]">
456
+ <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">
457
+ <span className="uppercase text-[#b8b8b8]">thinking</span>
458
+ <span className="min-w-0 truncate">{previewToolValue(value)}</span>
459
+ </summary>
460
+ <HighlightedCode
461
+ code={rendered || "{}"}
462
+ language={detectLanguage(rendered)}
463
+ />
464
+ </details>
465
+ );
466
+ }
467
+
468
+ const rendered = stringifyPartValue(value);
469
+ return (
470
+ <details className={toolFrameClass()}>
471
+ <summary className="grid cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 px-3 py-2 font-mono text-[0.86rem] leading-tight text-[#b8b8b8] hover:bg-white/[0.04] max-md:grid-cols-1 max-md:gap-1">
472
+ <span className="text-[#888]">{part.type}</span>
473
+ <strong className="min-w-0 break-words font-bold text-white">
474
+ {part.name ?? part.id ?? "unknown"}
475
+ </strong>
476
+ <span className="min-w-0 break-words text-right max-md:text-left">
477
+ {previewToolValue(value)}
478
+ </span>
479
+ </summary>
480
+ <HighlightedCode code={rendered || "{}"} language="json" />
481
+ </details>
482
+ );
483
+ }
484
+
485
+ function isString(value: string | undefined): value is string {
486
+ return typeof value === "string" && value.length > 0;
487
+ }