@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.
@@ -1,17 +1,24 @@
1
- import { useState, type ClipboardEventHandler, type ReactNode } from "react";
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
- formatUsage,
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
- <div className={cn(mutedTranscriptMetaClass(), "mt-1")}>
152
- {turnMeta(props.turn).join(" · ")}
153
- {props.turn.sentryTraceUrl ? (
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-3 pt-3">
189
+ <div className="grid gap-2 pt-3">
179
190
  {props.turn.transcriptAvailable ? (
180
- groupTranscriptMessages(props.turn.transcript).map((entry, index) =>
181
- entry.kind === "tool" ? (
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
- <TranscriptMessageView
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 RedactedTranscriptView(props: { turn: ConversationTurn }) {
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
- {groupTranscriptMessages(props.turn.transcriptMetadata ?? []).map(
215
- (entry, index) =>
216
- entry.kind === "tool" ? (
217
- <RedactedToolView
218
- call={entry.call}
219
- key={`${props.turn.id}:redacted:${index}`}
220
- result={entry.result}
221
- resultTimestamp={entry.resultTimestamp}
222
- timestamp={entry.timestamp}
223
- />
224
- ) : (
225
- <RedactedMessageView
226
- key={`${props.turn.id}:redacted:${index}`}
227
- message={entry.message}
228
- turn={props.turn}
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
- props.timestamp ? formatMessageTimestamp(props.timestamp) : undefined,
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-white">
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
- requesterLabel(turn.requesterIdentity, turn.requester) ?? "unknown actor"
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
- function turnMeta(turn: ConversationTurn): string[] {
356
- return [
357
- formatMs(turn.cumulativeDurationMs),
358
- formatUsage(turn.cumulativeUsage),
359
- ].filter((value) => value && value !== "none");
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 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">
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-white">
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: { value: unknown }) {
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="border border-[#beaaff]/20 bg-white/[0.03] transition-colors hover:border-[#beaaff]/45 hover:bg-[rgba(190,170,255,0.06)]"
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-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>
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="border-t border-[#beaaff]/15 px-3 py-3">
491
- <HighlightedCode
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
  );