@sentry/junior-dashboard 0.264.0 → 0.265.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 (191) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -5
  3. package/dist/app.js +119 -35
  4. package/dist/client.js +145 -136
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +119 -35
  7. package/dist/tailwind.css +1 -1
  8. package/package.json +5 -4
  9. package/src/api/schema.ts +0 -1
  10. package/src/app.ts +5 -20
  11. package/src/client/App.tsx +29 -31
  12. package/src/client/api.ts +7 -0
  13. package/src/client/components/BottomSheet.tsx +12 -2
  14. package/src/client/components/CopyButton.tsx +83 -0
  15. package/src/client/components/DetailsPanel.tsx +133 -0
  16. package/src/client/components/Drawer.tsx +15 -24
  17. package/src/client/components/FilterBar.tsx +16 -31
  18. package/src/client/components/Metric.tsx +33 -23
  19. package/src/client/components/PageContentSkeleton.tsx +1 -1
  20. package/src/client/components/Pagination.tsx +1 -1
  21. package/src/client/components/ParticipantAvatarStack.tsx +40 -36
  22. package/src/client/components/ProfileMenu.tsx +27 -93
  23. package/src/client/components/README.md +86 -77
  24. package/src/client/components/SegmentedNav.tsx +1 -1
  25. package/src/client/components/ShimmerText.tsx +2 -6
  26. package/src/client/components/ThemeSwitcher.tsx +19 -15
  27. package/src/client/components/charts/ActivityChart.tsx +173 -288
  28. package/src/client/components/charts/CacheWrites.tsx +63 -50
  29. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  30. package/src/client/components/charts/README.md +4 -4
  31. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  32. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  33. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  34. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  35. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  36. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  37. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  38. package/src/client/components/ui/accordion.tsx +77 -0
  39. package/src/client/components/ui/alert-dialog.tsx +89 -0
  40. package/src/client/components/ui/button.tsx +85 -0
  41. package/src/client/components/ui/chart.tsx +74 -0
  42. package/src/client/components/ui/checkbox.tsx +26 -0
  43. package/src/client/components/ui/collapsible.tsx +22 -0
  44. package/src/client/components/ui/field.tsx +49 -0
  45. package/src/client/components/ui/input.tsx +35 -0
  46. package/src/client/components/ui/native-select.tsx +57 -0
  47. package/src/client/components/ui/popover.tsx +67 -0
  48. package/src/client/components/ui/radio-group.tsx +32 -0
  49. package/src/client/components/ui/sheet.tsx +13 -3
  50. package/src/client/components/ui/skeleton.tsx +19 -0
  51. package/src/client/components/ui/table.tsx +80 -0
  52. package/src/client/components/ui/tabs.tsx +75 -0
  53. package/src/client/components/ui/textarea.tsx +17 -0
  54. package/src/client/components/ui/toast.tsx +215 -0
  55. package/src/client/components/ui/toggle-group.tsx +36 -0
  56. package/src/client/components/ui/toggle.tsx +37 -0
  57. package/src/client/components/ui/tooltip.tsx +168 -0
  58. package/src/client/components/ui/trace.tsx +96 -0
  59. package/src/client/conversations/ChatLayout.tsx +7 -3
  60. package/src/client/conversations/ConversationBrief.tsx +17 -6
  61. package/src/client/conversations/ConversationComposer.tsx +26 -12
  62. package/src/client/conversations/ConversationDetails.tsx +147 -62
  63. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  64. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  65. package/src/client/conversations/ConversationHeader.tsx +8 -3
  66. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  67. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  68. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  69. package/src/client/conversations/ConversationMeta.tsx +55 -88
  70. package/src/client/conversations/ConversationPage.tsx +220 -101
  71. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  72. package/src/client/conversations/ConversationWatches.tsx +165 -0
  73. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  74. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  75. package/src/client/conversations/DetailsList.tsx +124 -0
  76. package/src/client/conversations/EventDetails.tsx +42 -8
  77. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  78. package/src/client/conversations/ObjectCard.tsx +20 -8
  79. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  80. package/src/client/conversations/README.md +86 -13
  81. package/src/client/conversations/ShareConversationButton.tsx +25 -13
  82. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  83. package/src/client/conversations/ToolFrame.tsx +36 -52
  84. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  85. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  86. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  87. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  88. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  89. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  90. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  91. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  92. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  93. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  94. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  95. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  96. package/src/client/conversations/TranscriptView.tsx +63 -27
  97. package/src/client/conversations/eventLog.ts +57 -0
  98. package/src/client/conversations/eventTime.ts +60 -0
  99. package/src/client/conversations/queries.ts +29 -0
  100. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  101. package/src/client/format.ts +5 -0
  102. package/src/client/pages/ConnectionsPage.tsx +2 -2
  103. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  104. package/src/client/pages/SettingsPage.tsx +9 -10
  105. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  106. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  107. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  108. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  109. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  110. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  111. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  112. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  113. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  114. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  115. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  116. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  117. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  118. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  119. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  120. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  121. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  122. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  123. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  124. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  125. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  126. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  127. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  128. package/src/client/pages/automations/README.md +57 -0
  129. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  130. package/src/client/pages/automations/automationFormat.ts +37 -23
  131. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  132. package/src/client/pages/code/CodePage.tsx +52 -55
  133. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  134. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  135. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  136. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  137. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  138. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  139. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  140. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  141. package/src/client/pages/memory/memoryRecord.ts +1 -1
  142. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  143. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  144. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  145. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  146. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  147. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  148. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  149. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  150. package/src/client/pages/system/README.md +23 -0
  151. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  152. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  153. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  154. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  155. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  156. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  157. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  158. package/src/client/pages/system/sandboxSize.ts +8 -0
  159. package/src/client/pages/system/workspaceQueries.ts +53 -0
  160. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  161. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  162. package/src/client/styles.ts +4 -0
  163. package/src/client.tsx +7 -1
  164. package/src/mock-reporting/automation-collection.ts +4 -2
  165. package/src/mock-reporting/fixtures.ts +23 -7
  166. package/src/mock-reporting/routes.ts +12 -0
  167. package/src/mock-reporting/watches.ts +94 -0
  168. package/src/shell.ts +4 -9
  169. package/src/tailwind.css +0 -10
  170. package/src/client/components/Button.tsx +0 -118
  171. package/src/client/components/Field.tsx +0 -53
  172. package/src/client/components/SegmentedTabs.tsx +0 -119
  173. package/src/client/components/Select.tsx +0 -52
  174. package/src/client/components/Skeleton.tsx +0 -17
  175. package/src/client/components/TextInput.tsx +0 -58
  176. package/src/client/components/Tooltip.tsx +0 -216
  177. package/src/client/components/charts/useChartLayout.ts +0 -27
  178. package/src/client/components/ui/sheet.LICENSE +0 -21
  179. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  180. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  181. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  182. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  183. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  184. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  185. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  186. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  187. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  188. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  189. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  190. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  191. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -1,33 +1,37 @@
1
- import { ChevronRight } from "lucide-react";
2
- import { memo, useEffect, useId, useMemo, useRef, useState } from "react";
1
+ import { memo, useEffect, useMemo, useRef, useState } from "react";
3
2
  import { createPortal } from "react-dom";
4
3
 
5
- import { Drawer } from "../components/Drawer";
4
+ import { Skeleton } from "../components/ui/skeleton";
5
+ import { Toggle } from "../components/ui/toggle";
6
+ import { useEventTime } from "./eventTime";
7
+ import { SearchInput } from "../components/SearchInput";
6
8
  import { cn } from "../styles";
7
9
  import type { ConversationTranscript } from "../types";
8
10
  import {
9
- eventLogModel,
10
- eventLogSummary,
11
+ adjacentEventLogSeqs,
12
+ eventLogRows,
11
13
  eventLogTone,
12
- eventLogUsage,
14
+ filterEventLogRows,
13
15
  } from "./eventLog";
14
- import { EventDetails } from "./EventDetails";
15
- import { ForkConversationButton } from "./ForkConversationButton";
16
+ import { ConversationEventDetails } from "./ConversationEventDetails";
16
17
  import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
17
18
 
18
19
  const rowClass =
19
- "grid grid-cols-[12ch_minmax(0,1fr)_1rem] items-center gap-x-3 px-3 @min-[48rem]:grid-cols-[12ch_19ch_minmax(0,1fr)_1rem]";
20
+ "grid grid-cols-[12ch_minmax(0,1fr)] items-baseline gap-x-3 px-4 @min-[32rem]:grid-cols-[12ch_17ch_minmax(0,1fr)]";
20
21
 
21
22
  /**
22
23
  * Show every reporting event in sequence order, outside the transcript reducer.
23
- * Pass `onSelectedSeqChange` to own the open entry, for example in the URL.
24
+ * Pass `onSelectedSeqChange` to own selection and render details beside the log.
25
+ * Standalone transcripts keep a local selection and show an overlay.
24
26
  */
25
27
  export const ConversationEventLog = memo(function ConversationEventLog(props: {
26
28
  conversation: ConversationTranscript;
29
+ onSearchChange(value: string): void;
27
30
  onSelectedSeqChange?(seq: number | undefined): void;
28
31
  selectedSeq?: number;
29
32
  }) {
30
33
  const { conversation } = props;
34
+ const time = useEventTime();
31
35
  const search = useTranscriptSearch();
32
36
  const [localSelectedSeq, setLocalSelectedSeq] = useState<number>();
33
37
  const selectedSeq = props.onSelectedSeqChange
@@ -35,31 +39,14 @@ export const ConversationEventLog = memo(function ConversationEventLog(props: {
35
39
  : localSelectedSeq;
36
40
  const setSelectedSeq = props.onSelectedSeqChange ?? setLocalSelectedSeq;
37
41
  const listRef = useRef<HTMLOListElement>(null);
38
- // Rows opened by a click are already visible. Scroll only for a linked entry.
42
+ // Clicked rows are visible. Reveal links and keyboard selections after layout.
39
43
  const revealedSeqRef = useRef<number>(undefined);
40
- const titleId = useId();
41
44
  const events = conversation.events;
42
- const rows = useMemo(
43
- () =>
44
- events.map((event) => ({
45
- event,
46
- model: eventLogModel(event),
47
- usage: eventLogUsage(event),
48
- summary: eventLogSummary(event.data).replace(/\s+/g, " ").slice(0, 300),
49
- })),
50
- [events],
51
- );
45
+ const rows = useMemo(() => eventLogRows(events), [events]);
52
46
  // Large payloads are serialized only for an active search or the open entry.
53
47
  const visibleRows = useMemo(
54
- () =>
55
- search.active
56
- ? rows.filter(({ event, summary, usage }) =>
57
- `${summary} ${usage} ${JSON.stringify(event)}`
58
- .toLowerCase()
59
- .includes(search.normalizedQuery),
60
- )
61
- : rows,
62
- [rows, search.active, search.normalizedQuery],
48
+ () => filterEventLogRows(rows, search.normalizedQuery),
49
+ [rows, search.normalizedQuery],
63
50
  );
64
51
  // Resolve the selection from current data so polling cannot leave stale details.
65
52
  const selected = events.find((event) => event.seq === selectedSeq);
@@ -72,7 +59,7 @@ export const ConversationEventLog = memo(function ConversationEventLog(props: {
72
59
  );
73
60
  if (!row) return;
74
61
  revealedSeqRef.current = selectedSeq;
75
- row.scrollIntoView({ block: "center" });
62
+ row.scrollIntoView({ block: "nearest" });
76
63
  }, [selectedSeq, visibleRows]);
77
64
 
78
65
  if (conversation.eventHistory.status === "expired") {
@@ -85,54 +72,89 @@ export const ConversationEventLog = memo(function ConversationEventLog(props: {
85
72
 
86
73
  return (
87
74
  <section aria-label="Conversation event log" className="@container min-w-0">
88
- <div className="mb-3 flex flex-wrap items-center justify-between gap-2 text-xs text-dashboard-text-muted">
89
- <span role="status">
90
- {search.active
91
- ? `${visibleRows.length} of ${events.length} loaded events`
92
- : `${events.length} loaded events`}
93
- </span>
94
- <span>Times in UTC · Select a row for details</span>
95
- </div>
96
- <div className="overflow-hidden rounded-md border border-dashboard-border bg-dashboard-surface-panel font-mono text-xs">
97
- <div
98
- aria-hidden="true"
99
- className={cn(
100
- rowClass,
101
- "border-b border-dashboard-border bg-dashboard-surface-raised py-2 text-dashboard-text-muted",
102
- )}
103
- >
104
- <span>Time (UTC)</span>
105
- <span>Event</span>
106
- <span className="hidden @min-[48rem]:block">Summary</span>
107
- <span />
75
+ <div className="sticky top-0 z-10 bg-dashboard-bg">
76
+ <div className="flex flex-wrap items-center gap-2 border-b border-dashboard-border px-4 py-2">
77
+ <SearchInput
78
+ className="min-w-32 flex-1 @max-[28rem]:basis-full"
79
+ label="Search events"
80
+ onChange={props.onSearchChange}
81
+ placeholder="Search events…"
82
+ size="compact"
83
+ value={search.query}
84
+ />
85
+ <div className="ml-auto flex items-center gap-3 whitespace-nowrap text-xs text-dashboard-text-muted @max-[28rem]:w-full @max-[28rem]:justify-between">
86
+ <span role="status">
87
+ {search.active
88
+ ? `${visibleRows.length} of ${events.length} loaded events`
89
+ : `${events.length} loaded events`}
90
+ </span>
91
+ <Toggle
92
+ aria-label="Use local time"
93
+ onPressedChange={time.toggle}
94
+ pressed={time.local}
95
+ title={time.title}
96
+ >
97
+ <span className="max-w-32 truncate">{time.label}</span>
98
+ </Toggle>
99
+ </div>
108
100
  </div>
101
+ <EventLogColumns />
102
+ </div>
103
+ <div className="font-mono text-xs">
109
104
  <ol
110
105
  aria-label="Events"
111
106
  className="m-0 list-none divide-y divide-dashboard-border-subtle p-0"
112
107
  ref={listRef}
113
108
  >
114
- {visibleRows.map(({ event, summary, model, usage }) => (
115
- <li data-event-seq={event.seq} key={event.seq}>
109
+ {visibleRows.map(({ event, summary, metadata }, index) => (
110
+ <li
111
+ className="scroll-mt-32 scroll-mb-2"
112
+ data-event-seq={event.seq}
113
+ key={event.seq}
114
+ >
116
115
  <button
117
- aria-haspopup="dialog"
116
+ aria-expanded={selectedSeq === event.seq}
118
117
  aria-label={`Event ${event.seq}: ${event.data.type}`}
119
118
  className={cn(
120
119
  rowClass,
121
- "min-h-11 w-full cursor-pointer border-0 bg-transparent py-2 text-left leading-5 hover:bg-dashboard-fill-hover active:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus @min-[48rem]:min-h-8 @min-[48rem]:py-1",
122
- selectedSeq === event.seq && "bg-dashboard-fill-strong",
120
+ "relative min-h-11 w-full cursor-pointer border-0 bg-transparent py-2 text-left leading-5 hover:bg-dashboard-fill-hover active:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus @min-[32rem]:min-h-9 @min-[32rem]:py-1.5",
121
+ selectedSeq === event.seq &&
122
+ "bg-dashboard-fill-strong before:absolute before:inset-y-0 before:left-0 before:w-0.5 before:bg-dashboard-accent before:content-['']",
123
123
  )}
124
124
  onClick={() => {
125
125
  revealedSeqRef.current = event.seq;
126
126
  setSelectedSeq(event.seq);
127
127
  }}
128
+ onKeyDown={(keyEvent) => {
129
+ if (
130
+ keyEvent.altKey ||
131
+ keyEvent.ctrlKey ||
132
+ keyEvent.metaKey ||
133
+ keyEvent.shiftKey ||
134
+ (keyEvent.key !== "ArrowUp" && keyEvent.key !== "ArrowDown")
135
+ )
136
+ return;
137
+ keyEvent.preventDefault();
138
+ const next =
139
+ visibleRows[
140
+ index + (keyEvent.key === "ArrowDown" ? 1 : -1)
141
+ ];
142
+ if (!next) return;
143
+ setSelectedSeq(next.event.seq);
144
+ listRef.current
145
+ ?.querySelector<HTMLButtonElement>(
146
+ `[data-event-seq="${next.event.seq}"] button`,
147
+ )
148
+ ?.focus({ preventScroll: true });
149
+ }}
128
150
  type="button"
129
151
  >
130
152
  <time
131
153
  className="tabular-nums text-dashboard-text-muted"
132
154
  dateTime={event.createdAt}
133
- title={event.createdAt}
155
+ title={time.formatTimestamp(event.createdAt)}
134
156
  >
135
- {event.createdAt.slice(11, 23)}
157
+ {time.formatClock(event.createdAt)}
136
158
  </time>
137
159
  <span
138
160
  className={cn("truncate", eventLogTone(event.data))}
@@ -140,50 +162,19 @@ export const ConversationEventLog = memo(function ConversationEventLog(props: {
140
162
  >
141
163
  <HighlightText text={event.data.type} />
142
164
  </span>
143
- <span className="col-span-2 row-start-2 grid min-w-0 gap-1 text-dashboard-text @min-[48rem]:col-span-1 @min-[48rem]:row-auto">
144
- <span className="flex min-w-0 flex-wrap items-baseline gap-x-2">
145
- <span
146
- className={cn(
147
- "break-all",
148
- model
149
- ? "font-semibold text-violet-300"
150
- : "text-dashboard-text-muted",
151
- )}
152
- >
153
- <HighlightText
154
- text={model?.modelId ?? "Model not recorded"}
155
- />
156
- </span>
157
- {model?.modelProfile ? (
158
- <span className="text-dashboard-text-muted">
159
- <HighlightText text={model.modelProfile} />
160
- </span>
161
- ) : null}
162
- {model?.reasoningLevel ? (
163
- <span className="text-dashboard-text-muted">
164
- <HighlightText
165
- text={`${model.reasoningLevel} reasoning`}
166
- />
167
- </span>
168
- ) : null}
169
- </span>
165
+ <span className="col-span-2 row-start-2 grid min-w-0 gap-0.5 text-dashboard-text @min-[32rem]:col-span-1 @min-[32rem]:row-auto">
170
166
  <span className="truncate" title={summary}>
171
167
  <HighlightText text={summary} />
172
168
  </span>
173
- {usage ? (
169
+ {metadata ? (
174
170
  <span
175
- className="text-dashboard-text-muted"
176
- title="Usage for this event only; costs are estimated USD"
171
+ className="truncate text-dashboard-text-muted"
172
+ title={metadata}
177
173
  >
178
- <HighlightText text={usage} />
174
+ <HighlightText text={metadata} />
179
175
  </span>
180
176
  ) : null}
181
177
  </span>
182
- <ChevronRight
183
- aria-hidden="true"
184
- className="col-start-3 row-start-1 text-dashboard-text-muted @min-[48rem]:col-start-4"
185
- size={14}
186
- />
187
178
  </button>
188
179
  </li>
189
180
  ))}
@@ -196,45 +187,75 @@ export const ConversationEventLog = memo(function ConversationEventLog(props: {
196
187
  </p>
197
188
  ) : null}
198
189
  </div>
199
- {selected
190
+ {selected && !props.onSelectedSeqChange
200
191
  ? createPortal(
201
- <Drawer
202
- closeLabel="Close event details"
203
- header={
204
- <>
205
- <h2
206
- className="m-0 break-words font-mono text-sm font-semibold text-dashboard-text"
207
- id={titleId}
208
- >
209
- {selected.data.type}
210
- </h2>
211
- <p className="mb-0 mt-1 font-mono text-xs text-dashboard-text-muted">
212
- {selected.createdAt}
213
- </p>
214
- </>
215
- }
192
+ <ConversationEventDetails
193
+ overlay
194
+ conversation={conversation}
195
+ event={selected}
216
196
  onClose={() => setSelectedSeq(undefined)}
217
- openKey={`${conversation.conversationId}:${selected.seq}`}
218
- titleId={titleId}
219
- width="wide"
220
- >
221
- {conversation.canFork &&
222
- selected.data.type === "message" &&
223
- selected.data.role === "assistant" &&
224
- !selected.data.redacted ? (
225
- <div className="mb-3 flex items-center gap-2 text-sm">
226
- <ForkConversationButton
227
- conversationId={conversation.conversationId}
228
- messageId={selected.data.messageId}
229
- />
230
- <span>Fork after this reply</span>
231
- </div>
232
- ) : null}
233
- <EventDetails key={selected.seq} event={selected} />
234
- </Drawer>,
197
+ onSelect={setSelectedSeq}
198
+ {...adjacentEventLogSeqs(visibleRows, selected.seq)}
199
+ />,
235
200
  document.body,
236
201
  )
237
202
  : null}
238
203
  </section>
239
204
  );
240
205
  });
206
+
207
+ const loadingRowWidths = ["w-3/5", "w-2/5", "w-4/5", "w-1/2", "w-2/3", "w-1/3"];
208
+
209
+ /** Match the event log toolbar, columns, and rows while the transcript loads. */
210
+ export function ConversationEventLogLoading() {
211
+ return (
212
+ <section aria-busy="true" className="@container min-w-0">
213
+ <span className="sr-only" role="status">
214
+ Loading events
215
+ </span>
216
+ <div className="sticky top-0 z-10 bg-dashboard-bg">
217
+ <div className="flex flex-wrap items-center gap-2 border-b border-dashboard-border px-4 py-2">
218
+ <Skeleton className="h-9 min-w-32 flex-1 @max-[28rem]:basis-full" />
219
+ <Skeleton className="ml-auto h-3 w-24" />
220
+ </div>
221
+ <EventLogColumns />
222
+ </div>
223
+ <ol className="m-0 list-none divide-y divide-dashboard-border-subtle p-0">
224
+ {loadingRowWidths.map((width, index) => (
225
+ <li
226
+ className={cn(
227
+ rowClass,
228
+ "min-h-11 items-center gap-y-2 py-2 @min-[32rem]:min-h-9 @min-[32rem]:py-1.5",
229
+ )}
230
+ key={index}
231
+ >
232
+ <Skeleton className="h-3 w-[10ch]" />
233
+ <Skeleton className="h-3 w-[12ch]" />
234
+ <Skeleton
235
+ className={cn(
236
+ "col-span-2 row-start-2 h-3 @min-[32rem]:col-span-1 @min-[32rem]:row-auto",
237
+ width,
238
+ )}
239
+ />
240
+ </li>
241
+ ))}
242
+ </ol>
243
+ </section>
244
+ );
245
+ }
246
+
247
+ function EventLogColumns() {
248
+ return (
249
+ <div
250
+ aria-hidden="true"
251
+ className={cn(
252
+ rowClass,
253
+ "border-b border-dashboard-border py-2 font-mono text-xs text-dashboard-text-muted",
254
+ )}
255
+ >
256
+ <span>Time</span>
257
+ <span>Event</span>
258
+ <span className="hidden @min-[32rem]:block">Summary</span>
259
+ </div>
260
+ );
261
+ }
@@ -53,11 +53,16 @@ export function ConversationHeader(props: {
53
53
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
54
54
  const searchInputRef = useRef<HTMLInputElement>(null);
55
55
  const mobileMenuRef = useRef<HTMLDivElement>(null);
56
- const searchOpenVisible = searchOpen || props.search.length > 0;
56
+ const searchOpenVisible =
57
+ props.view === "rich" && (searchOpen || props.search.length > 0);
57
58
  const openMobileNavigation = useOpenMobileNavigation();
58
59
 
59
60
  useEffect(() => {
60
- if (wasDetailsOpen.current && !detailsOpen) {
61
+ if (
62
+ wasDetailsOpen.current &&
63
+ !detailsOpen &&
64
+ document.activeElement === document.body
65
+ ) {
61
66
  const trigger = window.matchMedia("(max-width: 767px)").matches
62
67
  ? menuButtonRef.current
63
68
  : detailsButtonRef.current;
@@ -182,7 +187,7 @@ export function ConversationHeader(props: {
182
187
  <MobileHeaderLivePortal>{liveIndicator}</MobileHeaderLivePortal>
183
188
  <header
184
189
  className={cn(
185
- "sticky top-0 z-10 -mx-4 border-b border-dashboard-border bg-dashboard-bg md:-mx-7",
190
+ "relative z-10 border-b border-dashboard-border bg-dashboard-bg",
186
191
  !showMobileHeader && "hidden md:block",
187
192
  )}
188
193
  >
@@ -10,8 +10,13 @@ import {
10
10
  X,
11
11
  } from "lucide-react";
12
12
 
13
- import { Button, ToggleButton } from "../components/Button";
14
- import { IconButtonTooltip } from "../components/Tooltip";
13
+ import { Button } from "../components/ui/button";
14
+ import {
15
+ Tooltip,
16
+ TooltipContent,
17
+ TooltipTrigger,
18
+ } from "../components/ui/tooltip";
19
+ import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group";
15
20
  import { cn } from "../styles";
16
21
  import type { TranscriptViewMode } from "./transcriptRenderModel";
17
22
  import { ShareConversationButton } from "./ShareConversationButton";
@@ -43,17 +48,19 @@ export function ConversationHeaderActions(props: {
43
48
  if (layout === "menu") {
44
49
  return (
45
50
  <div className="grid gap-0.5">
46
- <MenuActionButton
47
- label={props.searchOpen ? "Hide search" : "Search transcript"}
48
- onClick={props.onSearchClick}
49
- pressed={props.searchOpen}
50
- >
51
- {props.searchOpen ? (
52
- <X aria-hidden="true" size={16} strokeWidth={2} />
53
- ) : (
54
- <Search aria-hidden="true" size={16} strokeWidth={2} />
55
- )}
56
- </MenuActionButton>
51
+ {props.view === "rich" ? (
52
+ <MenuActionButton
53
+ label={props.searchOpen ? "Hide search" : "Search transcript"}
54
+ onClick={props.onSearchClick}
55
+ pressed={props.searchOpen}
56
+ >
57
+ {props.searchOpen ? (
58
+ <X aria-hidden="true" size={16} strokeWidth={2} />
59
+ ) : (
60
+ <Search aria-hidden="true" size={16} strokeWidth={2} />
61
+ )}
62
+ </MenuActionButton>
63
+ ) : null}
57
64
  <MenuActionButton
58
65
  label="Conversation"
59
66
  onClick={() => props.onViewChange("rich")}
@@ -88,10 +95,12 @@ export function ConversationHeaderActions(props: {
88
95
 
89
96
  return (
90
97
  <div className="flex shrink-0 items-center gap-0.5">
91
- <TranscriptSearchToggle
92
- onClick={props.onSearchClick}
93
- open={props.searchOpen}
94
- />
98
+ {props.view === "rich" ? (
99
+ <TranscriptSearchToggle
100
+ onClick={props.onSearchClick}
101
+ open={props.searchOpen}
102
+ />
103
+ ) : null}
95
104
  {props.copyAction}
96
105
  <ArchiveConversationButton {...props.archive} layout="bar" />
97
106
  <Button
@@ -115,67 +124,55 @@ export function TranscriptViewToggle(props: {
115
124
  value: TranscriptViewMode;
116
125
  }) {
117
126
  return (
118
- <div
127
+ <ToggleGroup
119
128
  aria-label="Transcript view"
120
129
  className={
121
130
  props.labelled
122
- ? "inline-flex items-center gap-6"
123
- : "inline-flex items-center gap-0.5 rounded-md border border-dashboard-border bg-dashboard-overlay-soft p-0.5"
131
+ ? "inline-flex gap-6"
132
+ : "inline-flex gap-0.5 rounded-md border border-dashboard-border bg-dashboard-overlay-soft p-0.5"
124
133
  }
125
- role="group"
134
+ onValueChange={([next]) => {
135
+ if (next) props.onChange(next);
136
+ }}
137
+ value={[props.value]}
126
138
  >
127
- <ViewModeButton
128
- active={props.value === "rich"}
129
- label="Conversation"
130
- labelled={props.labelled}
131
- onClick={() => props.onChange("rich")}
132
- >
139
+ <ViewModeItem label="Conversation" labelled={props.labelled} value="rich">
133
140
  <MessagesSquare aria-hidden="true" size={14} strokeWidth={2} />
134
- </ViewModeButton>
135
- <ViewModeButton
136
- active={props.value === "raw"}
137
- label="Event log"
138
- labelled={props.labelled}
139
- onClick={() => props.onChange("raw")}
140
- >
141
+ </ViewModeItem>
142
+ <ViewModeItem label="Event log" labelled={props.labelled} value="raw">
141
143
  <ScrollText aria-hidden="true" size={14} strokeWidth={2} />
142
- </ViewModeButton>
143
- </div>
144
+ </ViewModeItem>
145
+ </ToggleGroup>
144
146
  );
145
147
  }
146
148
 
147
- function ViewModeButton(props: {
148
- active: boolean;
149
+ function ViewModeItem(props: {
149
150
  children: ReactNode;
150
151
  label: string;
151
152
  labelled?: boolean;
152
- onClick(): void;
153
+ value: TranscriptViewMode;
153
154
  }) {
154
- const button = (
155
- <ToggleButton
155
+ const item = (
156
+ <ToggleGroupItem
156
157
  aria-label={props.label}
157
- className={cn(
158
- "!no-underline",
158
+ className={
159
159
  props.labelled
160
- ? "relative flex h-11 items-center gap-2 rounded-none px-0 py-0 text-sm after:absolute after:inset-x-0 after:bottom-0 after:h-0.5"
161
- : "grid size-7 place-items-center rounded-md px-0 py-0",
162
- props.active &&
163
- (props.labelled
164
- ? "text-dashboard-text after:bg-dashboard-text-solid"
165
- : "bg-dashboard-fill-hover text-dashboard-text"),
166
- )}
167
- onClick={props.onClick}
168
- pressed={props.active}
169
- variant="text"
160
+ ? "relative h-11 min-w-0 gap-2 rounded-none border-0 px-0 text-sm hover:bg-transparent after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 data-pressed:border-0 data-pressed:bg-transparent data-pressed:after:bg-dashboard-text-solid"
161
+ : "size-7 min-w-0 border-0 px-0 data-pressed:border-0 data-pressed:bg-dashboard-fill-hover"
162
+ }
163
+ value={props.value}
170
164
  >
171
165
  {props.children}
172
166
  {props.labelled ? props.label : null}
173
- </ToggleButton>
167
+ </ToggleGroupItem>
174
168
  );
175
169
  return props.labelled ? (
176
- button
170
+ item
177
171
  ) : (
178
- <IconButtonTooltip label={props.label}>{button}</IconButtonTooltip>
172
+ <Tooltip>
173
+ <TooltipTrigger render={item} />
174
+ <TooltipContent size="compact">{props.label}</TooltipContent>
175
+ </Tooltip>
179
176
  );
180
177
  }
181
178
 
@@ -188,21 +185,26 @@ export function HeaderIconButton(props: {
188
185
  pressed?: boolean;
189
186
  }) {
190
187
  return (
191
- <IconButtonTooltip label={props.label}>
192
- <Button
193
- ref={props.ref}
194
- aria-label={props.label}
195
- aria-pressed={props.pressed}
196
- className={cn(
197
- "text-dashboard-text-muted",
198
- props.pressed && "bg-dashboard-fill-strong text-dashboard-text",
199
- )}
200
- onClick={props.onClick}
201
- size="icon"
202
- >
203
- {props.children}
204
- </Button>
205
- </IconButtonTooltip>
188
+ <Tooltip>
189
+ <TooltipTrigger
190
+ render={
191
+ <Button
192
+ ref={props.ref}
193
+ aria-label={props.label}
194
+ aria-pressed={props.pressed}
195
+ className={cn(
196
+ "text-dashboard-text-muted",
197
+ props.pressed && "bg-dashboard-fill-strong text-dashboard-text",
198
+ )}
199
+ onClick={props.onClick}
200
+ size="icon"
201
+ >
202
+ {props.children}
203
+ </Button>
204
+ }
205
+ />
206
+ <TooltipContent size="compact">{props.label}</TooltipContent>
207
+ </Tooltip>
206
208
  );
207
209
  }
208
210
 
@@ -273,16 +275,22 @@ function ArchiveConversationButton(
273
275
  );
274
276
  }
275
277
  return (
276
- <IconButtonTooltip label={label}>
277
- <Button
278
- aria-label={label}
279
- className="hidden shrink-0 text-dashboard-text-muted md:grid"
280
- disabled={props.disabled}
281
- onClick={props.onClick}
282
- size="icon"
283
- >
284
- <Icon aria-hidden="true" size={15} strokeWidth={2} />
285
- </Button>
286
- </IconButtonTooltip>
278
+ <Tooltip>
279
+ <TooltipTrigger
280
+ render={
281
+ <Button
282
+ focusableWhenDisabled
283
+ aria-label={label}
284
+ className="hidden shrink-0 text-dashboard-text-muted md:grid"
285
+ disabled={props.disabled}
286
+ onClick={props.onClick}
287
+ size="icon"
288
+ >
289
+ <Icon aria-hidden="true" size={15} strokeWidth={2} />
290
+ </Button>
291
+ }
292
+ />
293
+ <TooltipContent size="compact">{label}</TooltipContent>
294
+ </Tooltip>
287
295
  );
288
296
  }
@@ -9,7 +9,7 @@ import {
9
9
  visualStatusForConversation,
10
10
  } from "../format";
11
11
  import { EmptyTelemetry } from "../components/EmptyTelemetry";
12
- import { Skeleton } from "../components/Skeleton";
12
+ import { Skeleton } from "../components/ui/skeleton";
13
13
  import {
14
14
  conversationParticipants,
15
15
  ParticipantAvatarStack,