@sentry/junior-dashboard 0.265.0 → 0.267.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 (104) hide show
  1. package/dist/app.js +49 -12
  2. package/dist/client.js +94 -63
  3. package/dist/index.js +49 -12
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +11 -5
  6. package/src/app.ts +0 -2
  7. package/src/auth.ts +4 -7
  8. package/src/client/App.tsx +35 -53
  9. package/src/client/components/ActorAvatar.tsx +10 -22
  10. package/src/client/components/ConnectionBanner.tsx +4 -3
  11. package/src/client/components/CopyButton.tsx +3 -4
  12. package/src/client/components/DetailsPanel.tsx +12 -13
  13. package/src/client/components/Drawer.tsx +46 -23
  14. package/src/client/components/EmptyTelemetry.tsx +5 -3
  15. package/src/client/components/ImageAttachment.tsx +49 -63
  16. package/src/client/components/InlineError.tsx +8 -4
  17. package/src/client/components/Notice.tsx +19 -26
  18. package/src/client/components/Pagination.tsx +26 -26
  19. package/src/client/components/ParticipantAvatarStack.tsx +13 -10
  20. package/src/client/components/README.md +28 -3
  21. package/src/client/components/SearchInput.tsx +51 -41
  22. package/src/client/components/VersionDriftBanner.tsx +9 -7
  23. package/src/client/components/charts/CacheWrites.tsx +52 -43
  24. package/src/client/components/ui/alert.tsx +64 -0
  25. package/src/client/components/ui/avatar.tsx +53 -0
  26. package/src/client/components/ui/button-group.tsx +59 -0
  27. package/src/client/components/ui/button.tsx +2 -1
  28. package/src/client/components/ui/dialog.tsx +60 -0
  29. package/src/client/components/ui/drawer.tsx +140 -0
  30. package/src/client/components/ui/empty.tsx +61 -0
  31. package/src/client/components/ui/input-group.tsx +96 -0
  32. package/src/client/components/ui/pagination.tsx +60 -0
  33. package/src/client/components/ui/separator.tsx +22 -0
  34. package/src/client/components/ui/spinner.tsx +21 -0
  35. package/src/client/components/ui/table.tsx +22 -1
  36. package/src/client/components/ui/tooltip.tsx +1 -1
  37. package/src/client/components/ui/useSheet.ts +5 -1
  38. package/src/client/conversations/ComposerImages.tsx +66 -14
  39. package/src/client/conversations/ConversationChildAgents.tsx +5 -5
  40. package/src/client/conversations/ConversationComposer.tsx +84 -12
  41. package/src/client/conversations/ConversationDetails.tsx +3 -27
  42. package/src/client/conversations/ConversationEventDetails.tsx +26 -10
  43. package/src/client/conversations/ConversationHeader.tsx +5 -4
  44. package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
  45. package/src/client/conversations/ConversationPage.tsx +156 -2
  46. package/src/client/conversations/ConversationSidebar.tsx +5 -4
  47. package/src/client/conversations/ConversationTranscript.tsx +48 -16
  48. package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
  49. package/src/client/conversations/ConversationWatches.tsx +67 -59
  50. package/src/client/conversations/DetailsList.tsx +77 -29
  51. package/src/client/conversations/EventDetails.tsx +46 -19
  52. package/src/client/conversations/EventUsage.tsx +99 -74
  53. package/src/client/conversations/ForkConversationButton.tsx +79 -73
  54. package/src/client/conversations/PendingMailboxStack.tsx +312 -119
  55. package/src/client/conversations/README.md +30 -10
  56. package/src/client/conversations/ShareConversationButton.tsx +3 -4
  57. package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
  58. package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
  59. package/src/client/conversations/TranscriptAttachment.tsx +39 -4
  60. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
  61. package/src/client/conversations/TranscriptEventView.tsx +35 -16
  62. package/src/client/conversations/TranscriptFailureView.tsx +10 -13
  63. package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
  64. package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
  65. package/src/client/conversations/TranscriptMessageView.tsx +7 -1
  66. package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
  67. package/src/client/conversations/TranscriptSummary.tsx +13 -7
  68. package/src/client/conversations/TranscriptTurnContextView.tsx +78 -56
  69. package/src/client/conversations/TranscriptView.tsx +7 -3
  70. package/src/client/conversations/UserInputRequestCard.tsx +244 -0
  71. package/src/client/conversations/conversationOutbox.ts +46 -0
  72. package/src/client/conversations/eventLog.ts +1 -0
  73. package/src/client/conversations/eventTime.ts +13 -1
  74. package/src/client/conversations/eventTranscript.ts +12 -5
  75. package/src/client/conversations/queries.ts +127 -26
  76. package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
  77. package/src/client/conversations/transcriptMentions.tsx +18 -5
  78. package/src/client/conversations/userInputRequest.ts +56 -0
  79. package/src/client/format.ts +10 -3
  80. package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
  81. package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
  82. package/src/client/pages/automations/AutomationEditor.tsx +182 -156
  83. package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
  84. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
  85. package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
  86. package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
  87. package/src/client/pages/automations/AutomationOverview.tsx +238 -0
  88. package/src/client/pages/automations/AutomationPage.tsx +343 -0
  89. package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
  90. package/src/client/pages/automations/AutomationRow.tsx +11 -13
  91. package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
  92. package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
  93. package/src/client/pages/automations/AutomationStatus.tsx +164 -108
  94. package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
  95. package/src/client/pages/automations/AutomationsPage.tsx +10 -98
  96. package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
  97. package/src/client/pages/automations/README.md +56 -46
  98. package/src/client/types.ts +3 -0
  99. package/src/mock-reporting/fixtures.ts +44 -2
  100. package/src/mock-reporting/watches.ts +1 -1
  101. package/src/client/conversations/transcriptStyles.ts +0 -10
  102. package/src/client/pages/automations/AutomationDetails.tsx +0 -193
  103. package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
  104. package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
@@ -1,6 +1,7 @@
1
1
  import { useQuery } from "@tanstack/react-query";
2
2
  import {
3
3
  Bot,
4
+ ChevronRight,
4
5
  CircleDot,
5
6
  Eye,
6
7
  FolderGit2,
@@ -14,7 +15,7 @@ import {
14
15
  Timer,
15
16
  Users,
16
17
  } from "lucide-react";
17
- import type { ReactNode } from "react";
18
+ import { useState, type ReactNode } from "react";
18
19
  import {
19
20
  conversationWatchesReportSchema,
20
21
  type ConversationWatch,
@@ -22,7 +23,8 @@ import {
22
23
 
23
24
  import { formatTime } from "../format";
24
25
  import { fetchDashboardJson } from "../http";
25
- import { DetailsList, DetailsRow, DetailsRowLine } from "./DetailsList";
26
+ import { dashboardDisclosureChevronClass } from "../styles";
27
+ import { DetailsList, DetailsRow } from "./DetailsList";
26
28
 
27
29
  /** Load the active Watches that one Conversation owns. */
28
30
  export function useConversationWatches(conversationId: string) {
@@ -39,8 +41,8 @@ export function useConversationWatches(conversationId: string) {
39
41
  }
40
42
 
41
43
  /**
42
- * Show what can wake this Conversation: each active Watch with the resource,
43
- * the reason, and the event types that deliver to it.
44
+ * Show what can wake this Conversation: one line per active Watch. A line
45
+ * expands to show the reason and the event types that deliver to it.
44
46
  */
45
47
  export function ConversationWatches(props: { watches: ConversationWatch[] }) {
46
48
  return (
@@ -66,65 +68,81 @@ const resourceIcons: Partial<Record<string, LucideIcon>> = {
66
68
  };
67
69
 
68
70
  /**
69
- * Show one Watch in up to three short lines: the resource and time left, the
70
- * reason it exists, then the event types that wake the Conversation.
71
+ * Show one Watch as a single line with its resource. Clicking the line shows
72
+ * why the Watch exists and which events wake the Conversation. The expiry is
73
+ * an internal limit, so the row does not show it.
71
74
  */
72
75
  function WatchRow(props: { watch: ConversationWatch }) {
73
76
  const { watch } = props;
77
+ const [expanded, setExpanded] = useState(false);
74
78
  const Icon = watch.redacted
75
79
  ? LockKeyhole
76
80
  : (resourceIcons[watch.resourceType] ?? Eye);
77
- const isTimer = watch.firesAt !== undefined;
78
- const until = watch.firesAt ?? watch.expiresAt;
79
81
  const resource = `${watch.namespace} ${watch.resourceType.replaceAll("_", " ")}`;
80
- // A timer has one event, and the time already says when it fires.
81
- const events = isTimer ? [] : watch.events;
82
+ // A timer has one event, and its fire time already says when it wakes.
83
+ const events = watch.firesAt ? [] : watch.events;
82
84
  const filters = Object.entries(watch.match ?? {});
83
-
84
- return (
85
+ const row = (
85
86
  <DetailsRow
87
+ expanded={expanded}
86
88
  icon={<Icon aria-hidden="true" className="size-3.5" />}
87
89
  meta={
88
- <time dateTime={until}>
89
- {isTimer
90
- ? `in ${formatRemaining(until)}`
91
- : `${formatRemaining(until)} left`}
92
- </time>
90
+ <span className="grid size-5 place-items-center">
91
+ <ChevronRight
92
+ aria-hidden="true"
93
+ className={dashboardDisclosureChevronClass}
94
+ />
95
+ </span>
93
96
  }
97
+ onToggle={() => setExpanded((value) => !value)}
94
98
  title={watch.label ?? `Private ${resource}`}
95
- tooltip={`${isTimer ? "Fires" : "Expires"} ${formatTime(until, {
96
- dateStyle: "medium",
97
- timeStyle: "short",
98
- })}`}
99
- >
100
- {watch.intent ? (
101
- <DetailsRowLine className="line-clamp-2 break-words">
102
- {watch.intent}
103
- </DetailsRowLine>
104
- ) : null}
105
- {events.length > 0 || filters.length > 0 || watch.childAgent ? (
106
- <DetailsRowLine className="flex flex-wrap items-center gap-1 pt-0.5">
107
- {events.map((event) => (
108
- <WatchChip key={event}>
109
- {watchEventLabel(event, watch.resourceType)}
110
- </WatchChip>
111
- ))}
112
- {filters.map(([key, value]) => (
113
- <WatchChip filter key={`match:${key}`}>
114
- {key.replaceAll("_", " ")}: {[value].flat().join(", ")}
115
- </WatchChip>
116
- ))}
117
- {watch.childAgent ? (
118
- <span className="inline-flex min-w-0 items-center gap-1 px-0.5">
119
- <Bot aria-hidden="true" className="size-3 shrink-0" />
120
- <span className="truncate">
121
- via {watch.childAgent.name ?? "subagent"}
122
- </span>
99
+ />
100
+ );
101
+ if (!expanded) return row;
102
+
103
+ return (
104
+ <>
105
+ {row}
106
+ {/* Indent under the title: icon column, column gap, and row padding. */}
107
+ <div className="grid min-w-0 gap-1.5 pb-2 pl-9 pr-1.5 pt-0.5 text-xs leading-4 text-dashboard-text-muted">
108
+ {watch.intent ? (
109
+ <p className="m-0 break-words">{watch.intent}</p>
110
+ ) : null}
111
+ {watch.firesAt ? (
112
+ <p className="m-0">
113
+ Fires{" "}
114
+ <time dateTime={watch.firesAt}>
115
+ {formatTime(watch.firesAt, {
116
+ dateStyle: "medium",
117
+ timeStyle: "short",
118
+ })}
119
+ </time>
120
+ </p>
121
+ ) : null}
122
+ {events.length > 0 || filters.length > 0 ? (
123
+ <div className="flex flex-wrap items-center gap-1">
124
+ {events.map((event) => (
125
+ <WatchChip key={event}>
126
+ {watchEventLabel(event, watch.resourceType)}
127
+ </WatchChip>
128
+ ))}
129
+ {filters.map(([key, value]) => (
130
+ <WatchChip filter key={`match:${key}`}>
131
+ {key.replaceAll("_", " ")}: {[value].flat().join(", ")}
132
+ </WatchChip>
133
+ ))}
134
+ </div>
135
+ ) : null}
136
+ {watch.childAgent ? (
137
+ <p className="m-0 flex min-w-0 items-center gap-1">
138
+ <Bot aria-hidden="true" className="size-3 shrink-0" />
139
+ <span className="truncate">
140
+ via {watch.childAgent.name ?? "subagent"}
123
141
  </span>
124
- ) : null}
125
- </DetailsRowLine>
126
- ) : null}
127
- </DetailsRow>
142
+ </p>
143
+ ) : null}
144
+ </div>
145
+ </>
128
146
  );
129
147
  }
130
148
 
@@ -153,13 +171,3 @@ function watchEventLabel(event: string, resourceType: string): string {
153
171
  parts[0] === resourceType && parts.length > 1 ? parts.slice(1) : parts;
154
172
  return rest.join(" ").replaceAll("_", " ");
155
173
  }
156
-
157
- /** Format the time until a Watch fires or expires as one compact unit. */
158
- function formatRemaining(value: string): string {
159
- const ms = Math.max(0, Date.parse(value) - Date.now());
160
- const minutes = Math.round(ms / 60_000);
161
- if (minutes < 60) return `${Math.max(1, minutes)}m`;
162
- const hours = Math.round(minutes / 60);
163
- if (hours < 48) return `${hours}h`;
164
- return `${Math.round(hours / 24)}d`;
165
- }
@@ -3,12 +3,45 @@ import { useState, type ReactNode } from "react";
3
3
 
4
4
  import { cn, dashboardDisclosureChevronClass } from "../styles";
5
5
 
6
+ /**
7
+ * Group one kind of detail in a compact card. Conversation details and event
8
+ * details share it, so both panels read the same way. A `label` names the
9
+ * card as a region.
10
+ */
11
+ export function DetailsCard(props: {
12
+ children: ReactNode;
13
+ count?: number;
14
+ label?: string;
15
+ title?: string;
16
+ }) {
17
+ return (
18
+ <section
19
+ aria-label={props.label}
20
+ className="grid min-w-0 gap-2.5 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel p-3"
21
+ >
22
+ {props.title ? (
23
+ <h3 className="m-0 flex min-w-0 items-center gap-1.5 break-words font-sans text-xs font-medium text-dashboard-text-muted">
24
+ {props.title}
25
+ {props.count ? (
26
+ <span className="tabular-nums text-dashboard-text-faint">
27
+ {props.count.toLocaleString("en-US")}
28
+ </span>
29
+ ) : null}
30
+ </h3>
31
+ ) : null}
32
+ <div className="min-w-0 break-words font-sans text-xs leading-relaxed text-dashboard-text-muted">
33
+ {props.children}
34
+ </div>
35
+ </section>
36
+ );
37
+ }
38
+
6
39
  /** Rows a details list shows before the reader asks for the rest. */
7
40
  const COLLAPSED_ROW_COUNT = 3;
8
41
 
9
42
  /**
10
- * Show rows in a details card. Rows are split by thin rules. A list with more
11
- * than three rows shows the first three and a row that shows the rest.
43
+ * Show rows in a details card. A list with more than three rows shows the
44
+ * first three and a row that shows the rest.
12
45
  */
13
46
  export function DetailsList<T>(props: {
14
47
  items: readonly T[];
@@ -23,26 +56,21 @@ export function DetailsList<T>(props: {
23
56
  : props.items.slice(0, COLLAPSED_ROW_COUNT);
24
57
 
25
58
  return (
26
- // Rows own their padding, so hover fills reach the rules. The negative
27
- // margin keeps the first and last text on the card padding.
28
- <ul className="-my-2.5 grid list-none p-0">
59
+ // Rows own their padding, so a hover fill sits inside the card with an
60
+ // even inset. The negative margins keep row text on the card padding.
61
+ <ul className="-mx-1.5 -mb-1.5 -mt-1 grid list-none gap-0.5 p-0">
29
62
  {visible.map((item) => (
30
- <li
31
- className="border-t border-dashboard-border-subtle first:border-t-0"
32
- key={props.itemKey(item)}
33
- >
34
- {props.renderItem(item)}
35
- </li>
63
+ <li key={props.itemKey(item)}>{props.renderItem(item)}</li>
36
64
  ))}
37
65
  {hidden > 0 ? (
38
- <li className="border-t border-dashboard-border-subtle">
66
+ <li>
39
67
  <button
40
68
  aria-expanded={expanded}
41
- className="group -mx-2 flex min-h-8 items-center gap-2.5 rounded-md px-2 py-2 text-left text-xs font-medium leading-4 text-dashboard-text-muted transition-colors hover:bg-dashboard-surface-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
69
+ className="group flex min-h-8 w-full items-center gap-2.5 rounded-md px-1.5 py-1.5 text-left text-xs font-medium leading-4 text-dashboard-text-muted transition-colors hover:bg-dashboard-surface-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
42
70
  onClick={() => setExpanded((value) => !value)}
43
71
  type="button"
44
72
  >
45
- <span className="grid size-4 shrink-0 place-items-center">
73
+ <span className="grid size-5 shrink-0 place-items-center">
46
74
  <ChevronRight
47
75
  aria-hidden="true"
48
76
  className={dashboardDisclosureChevronClass}
@@ -59,20 +87,24 @@ export function DetailsList<T>(props: {
59
87
  /**
60
88
  * Lay out one details list row: an icon, a title, and short trailing text on
61
89
  * the first line, then optional lines under the title. A row with `href`
62
- * opens that link in a new tab.
90
+ * opens that link in a new tab. A row with `onToggle` is a disclosure button
91
+ * that shows or hides content the caller renders under it.
63
92
  */
64
93
  export function DetailsRow(props: {
65
94
  children?: ReactNode;
95
+ expanded?: boolean;
66
96
  href?: string;
67
97
  icon: ReactNode;
68
98
  meta?: ReactNode;
99
+ onToggle?(): void;
69
100
  title: ReactNode;
70
101
  /** Native hover text with detail that the row does not show. */
71
102
  tooltip?: string;
72
103
  }) {
104
+ const interactive = props.href !== undefined || props.onToggle !== undefined;
73
105
  const className = cn(
74
- "-mx-2 grid min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-x-2.5 gap-y-1 rounded-md px-2 py-2.5 text-inherit no-underline",
75
- props.href &&
106
+ "grid w-full min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-x-2.5 gap-y-1 rounded-md px-1.5 py-1.5 text-left text-inherit no-underline",
107
+ interactive &&
76
108
  "group transition-colors hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus",
77
109
  );
78
110
  const content = (
@@ -80,7 +112,7 @@ export function DetailsRow(props: {
80
112
  <span className="grid size-5 shrink-0 place-items-center text-dashboard-text-muted">
81
113
  {props.icon}
82
114
  </span>
83
- <span className="min-w-0 truncate text-sm font-medium leading-5 text-dashboard-text group-hover:underline">
115
+ <span className="min-w-0 truncate text-sm font-medium leading-5 text-dashboard-text">
84
116
  {props.title}
85
117
  </span>
86
118
  <span className="shrink-0 text-xs leading-5 tabular-nums text-dashboard-text-muted">
@@ -89,17 +121,33 @@ export function DetailsRow(props: {
89
121
  {props.children}
90
122
  </>
91
123
  );
92
- return props.href ? (
93
- <a
94
- className={className}
95
- href={props.href}
96
- rel="noreferrer"
97
- target="_blank"
98
- title={props.tooltip}
99
- >
100
- {content}
101
- </a>
102
- ) : (
124
+ if (props.href) {
125
+ return (
126
+ <a
127
+ className={className}
128
+ href={props.href}
129
+ rel="noreferrer"
130
+ target="_blank"
131
+ title={props.tooltip}
132
+ >
133
+ {content}
134
+ </a>
135
+ );
136
+ }
137
+ if (props.onToggle) {
138
+ return (
139
+ <button
140
+ aria-expanded={props.expanded ?? false}
141
+ className={className}
142
+ onClick={props.onToggle}
143
+ title={props.tooltip}
144
+ type="button"
145
+ >
146
+ {content}
147
+ </button>
148
+ );
149
+ }
150
+ return (
103
151
  <div className={className} title={props.tooltip}>
104
152
  {content}
105
153
  </div>
@@ -12,6 +12,7 @@ import {
12
12
  import { dashboardDisclosureChevronClass } from "../styles";
13
13
 
14
14
  import { RedactedMarker } from "./TranscriptRedacted";
15
+ import { DetailsCard } from "./DetailsList";
15
16
  import { TranscriptText } from "./TranscriptText";
16
17
  import { eventLogModel } from "./eventLog";
17
18
  import { formatCostBreakdown } from "../format";
@@ -61,7 +62,9 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
61
62
  content = (
62
63
  <>
63
64
  {Object.keys(metadata).length > 0 ? (
64
- <DetailValue value={metadata} />
65
+ <DetailSection>
66
+ <DetailValue value={metadata} />
67
+ </DetailSection>
65
68
  ) : null}
66
69
  {summary ? (
67
70
  <DetailSection title="Continuation summary">
@@ -76,7 +79,9 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
76
79
  const { type: _, text, redacted, ...metadata } = data;
77
80
  content = (
78
81
  <>
79
- <DetailValue value={metadata} />
82
+ <DetailSection>
83
+ <DetailValue value={metadata} />
84
+ </DetailSection>
80
85
  <DetailSection title="Message">
81
86
  {redacted ? (
82
87
  <RedactedMarker />
@@ -97,14 +102,14 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
97
102
  <DetailSection key={call.toolCallId} title={name}>
98
103
  <DetailValue value={metadata} />
99
104
  {input !== undefined ? (
100
- <DetailSection title="Input">
105
+ <DetailSubsection title="Input">
101
106
  <DetailValue value={input} />
102
- </DetailSection>
107
+ </DetailSubsection>
103
108
  ) : null}
104
109
  {output !== undefined ? (
105
- <DetailSection title="Output">
110
+ <DetailSubsection title="Output">
106
111
  <DetailValue value={output} />
107
- </DetailSection>
112
+ </DetailSubsection>
108
113
  ) : null}
109
114
  </DetailSection>
110
115
  );
@@ -132,13 +137,15 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
132
137
  const { type: _, presentation, ...metadata } = data;
133
138
  content = (
134
139
  <>
135
- <DetailValue value={metadata} />
140
+ <DetailSection>
141
+ <DetailValue value={metadata} />
142
+ </DetailSection>
136
143
  <DetailSection title={presentation.title}>
137
144
  {presentation.preview ? (
138
145
  <TranscriptText mentions={mentions} text={presentation.preview} />
139
146
  ) : null}
140
147
  {presentation.details?.map((detail, index) => (
141
- <DetailSection key={index} title={detail.title}>
148
+ <DetailSubsection key={index} title={detail.title}>
142
149
  {detail.description ? (
143
150
  <TranscriptText
144
151
  mentions={mentions}
@@ -149,7 +156,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
149
156
  {detail.metadata ? (
150
157
  <DetailValue value={detail.metadata} />
151
158
  ) : null}
152
- </DetailSection>
159
+ </DetailSubsection>
153
160
  ))}
154
161
  </DetailSection>
155
162
  </>
@@ -158,12 +165,16 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
158
165
  }
159
166
  default: {
160
167
  const { type: _, ...fields } = data;
161
- content = <DetailValue value={fields} />;
168
+ content = (
169
+ <DetailSection>
170
+ <DetailValue value={fields} />
171
+ </DetailSection>
172
+ );
162
173
  }
163
174
  }
164
175
 
165
176
  return (
166
- <div className="@container grid min-w-0 gap-6">
177
+ <div className="@container grid min-w-0 gap-3">
167
178
  <DetailSection
168
179
  title={
169
180
  data.type === "handoff" ? "Target configuration" : "Configuration"
@@ -197,7 +208,7 @@ function RawEventJson({ event }: { event: ConversationReportEvent }) {
197
208
  const [open, setOpen] = useState(false);
198
209
  const json = open ? JSON.stringify(event, null, 2) : "";
199
210
  return (
200
- <div className="relative min-w-0 border-t border-dashboard-border pt-4">
211
+ <div className="relative min-w-0">
201
212
  <Collapsible onOpenChange={setOpen} open={open}>
202
213
  <CollapsibleTrigger className="group flex min-h-9 cursor-pointer items-center gap-1 pr-12 text-xs text-dashboard-text-muted hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-dashboard-focus">
203
214
  <ChevronRight
@@ -207,7 +218,7 @@ function RawEventJson({ event }: { event: ConversationReportEvent }) {
207
218
  Raw JSON
208
219
  </CollapsibleTrigger>
209
220
  <CollapsibleContent>
210
- <div className="absolute right-0 top-4">
221
+ <div className="absolute right-0 top-0">
211
222
  <CopyButton getText={() => json} label="Copy JSON" />
212
223
  </div>
213
224
  <div
@@ -224,12 +235,28 @@ function RawEventJson({ event }: { event: ConversationReportEvent }) {
224
235
  );
225
236
  }
226
237
 
227
- function DetailSection(props: { title: string; children: ReactNode }) {
238
+ // Top-level groups use the conversation details card. A titled card is a
239
+ // region, so readers can jump to a section such as "Message".
240
+ function DetailSection(props: { title?: string; children: ReactNode }) {
241
+ return (
242
+ <DetailsCard label={props.title} title={props.title}>
243
+ <div className="grid min-w-0 gap-3 text-sm text-dashboard-text">
244
+ {props.children}
245
+ </div>
246
+ </DetailsCard>
247
+ );
248
+ }
249
+
250
+ // A group inside a card, such as a tool input. It does not nest another card.
251
+ function DetailSubsection(props: { title: string; children: ReactNode }) {
228
252
  return (
229
- <section aria-label={props.title} className="grid min-w-0 gap-2">
230
- <h3 className="m-0 border-b border-dashboard-border pb-2 text-sm font-semibold text-dashboard-text">
253
+ <section
254
+ aria-label={props.title}
255
+ className="grid min-w-0 gap-1.5 border-t border-dashboard-border-subtle pt-3"
256
+ >
257
+ <h4 className="m-0 font-sans text-xs font-medium text-dashboard-text-muted">
231
258
  {props.title}
232
- </h3>
259
+ </h4>
233
260
  {props.children}
234
261
  </section>
235
262
  );
@@ -275,8 +302,8 @@ function DetailValue({ value }: { value: unknown }) {
275
302
  key={key}
276
303
  className={
277
304
  nested
278
- ? "grid min-w-0 gap-2 py-2"
279
- : "grid min-w-0 grid-cols-[minmax(0,7rem)_minmax(0,1fr)] items-baseline gap-x-4 py-2 @min-[30rem]:grid-cols-[minmax(0,10rem)_minmax(0,1fr)]"
305
+ ? "grid min-w-0 gap-2 py-2 first:pt-0 last:pb-0"
306
+ : "grid min-w-0 grid-cols-[minmax(0,7rem)_minmax(0,1fr)] items-baseline gap-x-4 py-2 first:pt-0 last:pb-0 @min-[30rem]:grid-cols-[minmax(0,10rem)_minmax(0,1fr)]"
280
307
  }
281
308
  >
282
309
  <dt className="break-words text-xs leading-relaxed text-dashboard-text-muted">