@sentry/junior-dashboard 0.263.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 (197) 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 +148 -135
  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 +9 -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 +26 -18
  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 +34 -33
  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 +101 -64
  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 +151 -0
  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/components/ui/useSheet.ts +74 -0
  60. package/src/client/conversations/ChatLayout.tsx +7 -3
  61. package/src/client/conversations/ConversationBrief.tsx +17 -6
  62. package/src/client/conversations/ConversationChildAgents.tsx +87 -58
  63. package/src/client/conversations/ConversationComposer.tsx +26 -12
  64. package/src/client/conversations/ConversationDetails.tsx +198 -95
  65. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  66. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  67. package/src/client/conversations/ConversationHeader.tsx +8 -3
  68. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  69. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  70. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  71. package/src/client/conversations/ConversationMeta.tsx +111 -92
  72. package/src/client/conversations/ConversationPage.tsx +241 -110
  73. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  74. package/src/client/conversations/ConversationUsageBreakdown.tsx +16 -3
  75. package/src/client/conversations/ConversationWatches.tsx +165 -0
  76. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  77. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  78. package/src/client/conversations/DetailsList.tsx +124 -0
  79. package/src/client/conversations/EventDetails.tsx +42 -8
  80. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  81. package/src/client/conversations/ObjectCard.tsx +20 -8
  82. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  83. package/src/client/conversations/README.md +93 -19
  84. package/src/client/conversations/ShareConversationButton.tsx +28 -3
  85. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  86. package/src/client/conversations/ToolFrame.tsx +36 -52
  87. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  88. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  89. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  90. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  91. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  92. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  93. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  94. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  95. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  96. package/src/client/conversations/TranscriptSubagentView.tsx +3 -3
  97. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  98. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  99. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  100. package/src/client/conversations/TranscriptView.tsx +63 -27
  101. package/src/client/conversations/eventLog.ts +57 -0
  102. package/src/client/conversations/eventTime.ts +60 -0
  103. package/src/client/conversations/queries.ts +29 -0
  104. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  105. package/src/client/format.ts +5 -0
  106. package/src/client/lib/utils.ts +1 -0
  107. package/src/client/pages/ConnectionsPage.tsx +2 -2
  108. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  109. package/src/client/pages/SettingsPage.tsx +9 -10
  110. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  111. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  112. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  113. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  114. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  115. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  116. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  117. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  118. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  119. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  120. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  121. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  122. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  123. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  124. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  125. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  126. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  127. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  128. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  129. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  130. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  131. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  132. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  133. package/src/client/pages/automations/README.md +57 -0
  134. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  135. package/src/client/pages/automations/automationFormat.ts +37 -23
  136. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  137. package/src/client/pages/code/CodePage.tsx +52 -55
  138. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  139. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  140. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  141. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  142. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  143. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  144. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  145. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  146. package/src/client/pages/memory/memoryRecord.ts +1 -1
  147. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  148. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  149. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  150. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  151. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  152. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  153. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  154. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  155. package/src/client/pages/system/README.md +23 -0
  156. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  157. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  158. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  159. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  160. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  161. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  162. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  163. package/src/client/pages/system/sandboxSize.ts +8 -0
  164. package/src/client/pages/system/workspaceQueries.ts +53 -0
  165. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  166. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  167. package/src/client/styles.ts +4 -0
  168. package/src/client.tsx +7 -1
  169. package/src/mock-reporting/automation-collection.ts +4 -2
  170. package/src/mock-reporting/fixtures.ts +23 -7
  171. package/src/mock-reporting/routes.ts +12 -0
  172. package/src/mock-reporting/watches.ts +94 -0
  173. package/src/shell.ts +4 -9
  174. package/src/tailwind.css +77 -10
  175. package/src/client/components/Button.tsx +0 -118
  176. package/src/client/components/Field.tsx +0 -53
  177. package/src/client/components/SegmentedTabs.tsx +0 -119
  178. package/src/client/components/Select.tsx +0 -52
  179. package/src/client/components/Sheet.LICENSE +0 -21
  180. package/src/client/components/Sheet.tsx +0 -141
  181. package/src/client/components/Skeleton.tsx +0 -17
  182. package/src/client/components/TextInput.tsx +0 -58
  183. package/src/client/components/Tooltip.tsx +0 -216
  184. package/src/client/components/charts/useChartLayout.ts +0 -27
  185. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -91
  186. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  187. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  188. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  189. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  190. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  191. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  192. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  193. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  194. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  195. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  196. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  197. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -1,4 +1,12 @@
1
- import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import {
2
+ memo,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ useSyncExternalStore,
9
+ } from "react";
2
10
  import { useSearchParams } from "react-router";
3
11
  import type {
4
12
  ConversationDetailReport,
@@ -18,7 +26,7 @@ import {
18
26
  import { conversationIsResponding } from "./transcript";
19
27
  import type { ConversationMailboxMessage } from "./conversationOutbox";
20
28
  import { buildConversationMarkdown } from "../markdownExport";
21
- import { CopyMarkdownButton } from "./CopyMarkdownButton";
29
+ import { CopyButton } from "../components/CopyButton";
22
30
  import { ConversationBrief } from "./ConversationBrief";
23
31
  import {
24
32
  CONVERSATION_EVENT_PARAM,
@@ -27,10 +35,24 @@ import {
27
35
  import { ConversationComposer } from "./ConversationComposer";
28
36
  import { ConversationHeader } from "./ConversationHeader";
29
37
  import { ConversationDetails } from "./ConversationDetails";
38
+ import { ConversationEventDetails } from "./ConversationEventDetails";
39
+ import { ConversationEventLogLoading } from "./ConversationEventLog";
40
+ import {
41
+ adjacentEventLogSeqs,
42
+ eventLogRows,
43
+ filterEventLogRows,
44
+ } from "./eventLog";
45
+ import { TranscriptSearchProvider } from "./transcriptSearch";
46
+ import {
47
+ TurnContextDetails,
48
+ type TurnContextPanel,
49
+ type TurnContextSelection,
50
+ } from "./TranscriptTurnContextView";
30
51
  import { cn } from "../styles";
31
52
  import { ConversationForkLinks } from "./ConversationForkLinks";
32
53
  import { ConversationAgentTree } from "./ConversationAgentTree";
33
54
  import { ConversationHeaderMeta } from "./ConversationHeaderMeta";
55
+ import { conversationLinks } from "./ConversationLinkedWork";
34
56
  import {
35
57
  ConversationAnnotations,
36
58
  ConversationPrivacyChip,
@@ -39,7 +61,10 @@ import {
39
61
  hasConversationStats,
40
62
  PendingAuthorization,
41
63
  } from "./ConversationMeta";
42
- import { ConversationUsageBreakdown } from "./ConversationUsageBreakdown";
64
+ import {
65
+ ConversationUsageBreakdown,
66
+ hasConversationUsageBreakdown,
67
+ } from "./ConversationUsageBreakdown";
43
68
  import { PendingMailboxStack } from "./PendingMailboxStack";
44
69
  import {
45
70
  buildConversations,
@@ -71,6 +96,14 @@ import type {
71
96
 
72
97
  export { liveModelId } from "./ConversationMeta";
73
98
 
99
+ const detailsDefaultOpenQuery = "(min-width: 1440px)";
100
+
101
+ function subscribeToDetailsLayout(onChange: () => void) {
102
+ const media = window.matchMedia(detailsDefaultOpenQuery);
103
+ media.addEventListener("change", onChange);
104
+ return () => media.removeEventListener("change", onChange);
105
+ }
106
+
74
107
  /** Render the selected conversation transcript inside the workspace. */
75
108
  export function ConversationPage(props: {
76
109
  conversationId: string;
@@ -78,7 +111,12 @@ export function ConversationPage(props: {
78
111
  onRead?(conversationId: string, lastReadAt: string): void;
79
112
  pendingArchiveUpdate?: PendingArchiveConversationUpdate;
80
113
  }) {
81
- const [detailsOpen, setDetailsOpen] = useState(false);
114
+ const detailsDefaultOpen = useSyncExternalStore(
115
+ subscribeToDetailsLayout,
116
+ () => window.matchMedia(detailsDefaultOpenQuery).matches,
117
+ () => false,
118
+ );
119
+ const pageRef = useRef<HTMLDivElement>(null);
82
120
  const [subagentTarget, setSubagentTarget] =
83
121
  useState<SubagentTranscriptTarget>();
84
122
  const [searchParams, setSearchParams] = useSearchParams();
@@ -86,6 +124,16 @@ export function ConversationPage(props: {
86
124
  const selectedEventSeq = parseConversationEventSeq(
87
125
  searchParams.get(CONVERSATION_EVENT_PARAM),
88
126
  );
127
+ // Event details and turn context show over Conversation details. Closing
128
+ // them shows details again when details are open. A linked event starts
129
+ // with details closed, so closing it does not open a panel nobody chose.
130
+ const [detailsChoice, setDetailsChoice] = useState<boolean | undefined>(() =>
131
+ selectedEventSeq === undefined ? undefined : false,
132
+ );
133
+ const detailsOpen = detailsChoice ?? detailsDefaultOpen;
134
+ const [turnContext, setTurnContext] = useState<TurnContextSelection>();
135
+ const eventOpen = selectedEventSeq !== undefined;
136
+ const showDetails = detailsOpen && !eventOpen && !turnContext;
89
137
  const [viewState, setView] = useState<TranscriptViewMode>(() =>
90
138
  selectedEventSeq === undefined ? "rich" : "raw",
91
139
  );
@@ -121,6 +169,10 @@ export function ConversationPage(props: {
121
169
  onRead?.(conversation.id, conversation.lastSeenAt);
122
170
  }, [conversation, onRead]);
123
171
  const transcript = detail.data;
172
+ const selectedEvent =
173
+ transcript?.eventHistory.status === "expired"
174
+ ? undefined
175
+ : transcript?.events.find((event) => event.seq === selectedEventSeq);
124
176
  const visualStatus = conversation
125
177
  ? visualStatusForConversation(conversation)
126
178
  : undefined;
@@ -157,6 +209,8 @@ export function ConversationPage(props: {
157
209
  if (value !== "raw" && selectedEventSeq !== undefined) {
158
210
  setSelectedEventSeq(undefined);
159
211
  }
212
+ // Turn context belongs to a message in the Conversation view.
213
+ setTurnContext(undefined);
160
214
  setView(value);
161
215
  },
162
216
  [selectedEventSeq, setSelectedEventSeq],
@@ -178,6 +232,35 @@ export function ConversationPage(props: {
178
232
  useEffect(() => {
179
233
  if (loadLinkedEvent) loadPreviousPageRef.current();
180
234
  }, [loadLinkedEvent, oldestLoadedSeq]);
235
+ // Keep the panel while the transcript or a linked event loads.
236
+ const eventLoading =
237
+ eventOpen &&
238
+ !selectedEvent &&
239
+ (detail.isPending ||
240
+ (linkedEventMissing &&
241
+ (detail.hasPreviousPage || detail.isLoadingPreviousPage) &&
242
+ !detail.historyError));
243
+ const showEvent = Boolean(selectedEvent) || eventLoading;
244
+ // Step through the same filtered rows that the event log shows.
245
+ const events = transcript?.events;
246
+ const normalizedSearch = search.trim().toLowerCase();
247
+ const eventRows = useMemo(
248
+ () =>
249
+ eventOpen && events
250
+ ? filterEventLogRows(eventLogRows(events), normalizedSearch)
251
+ : [],
252
+ [eventOpen, events, normalizedSearch],
253
+ );
254
+ const turnContextPanel = useMemo<TurnContextPanel>(
255
+ () => ({
256
+ openId: turnContext?.id,
257
+ onToggle: (selection) =>
258
+ setTurnContext((current) =>
259
+ current?.id === selection.id ? undefined : selection,
260
+ ),
261
+ }),
262
+ [turnContext?.id],
263
+ );
181
264
  const requestPin = useCallback(() => {
182
265
  setPinRequestVersion((version) => version + 1);
183
266
  }, []);
@@ -198,111 +281,139 @@ export function ConversationPage(props: {
198
281
  [],
199
282
  );
200
283
 
284
+ const closeEventDetails = () => {
285
+ const trigger = pageRef.current?.querySelector<HTMLButtonElement>(
286
+ `[data-event-seq="${selectedEventSeq}"] button`,
287
+ );
288
+ setSelectedEventSeq(undefined);
289
+ trigger?.focus({ preventScroll: true });
290
+ };
291
+ const closeTurnContext = () => {
292
+ // A message can have one context control per layout. Focus the visible one.
293
+ const trigger = [
294
+ ...(pageRef.current?.querySelectorAll<HTMLButtonElement>(
295
+ `[data-turn-context-id="${CSS.escape(turnContext?.id ?? "")}"]`,
296
+ ) ?? []),
297
+ ].find((button) => button.getClientRects().length > 0);
298
+ setTurnContext(undefined);
299
+ trigger?.focus({ preventScroll: true });
300
+ };
301
+
201
302
  return (
202
303
  <div
304
+ ref={pageRef}
203
305
  className={cn(
204
306
  "grid h-full min-h-0 min-w-0",
205
- detailsOpen && "xl:grid-cols-[minmax(0,1fr)_25rem]",
307
+ (showDetails || showEvent || turnContext) &&
308
+ "xl:grid-cols-[minmax(0,1fr)_25rem]",
206
309
  )}
207
310
  >
208
311
  <ChatLayout
312
+ header={
313
+ <ConversationHeader
314
+ agentTree={
315
+ agentTree ? <ConversationAgentTree nodes={agentTree} /> : null
316
+ }
317
+ conversationId={conversationId}
318
+ detailsOpen={showDetails}
319
+ onDetailsClick={() => {
320
+ if (eventOpen) setSelectedEventSeq(undefined);
321
+ setTurnContext(undefined);
322
+ setDetailsChoice(!showDetails);
323
+ }}
324
+ forks={
325
+ detail.data?.forkedFromConversationId ||
326
+ detail.data?.forks?.length ? (
327
+ <ConversationForkLinks
328
+ forkedFromConversationId={
329
+ detail.data.forkedFromConversationId
330
+ }
331
+ forkedFromTitle={detail.data.forkedFromTitle}
332
+ forks={detail.data.forks}
333
+ />
334
+ ) : null
335
+ }
336
+ copyAction={
337
+ <CopyButton
338
+ label="Copy as Markdown"
339
+ copyingLabel="Preparing Markdown"
340
+ key={conversationDetail?.conversationId ?? "loading"}
341
+ getText={
342
+ conversationDetail
343
+ ? async () =>
344
+ buildConversationMarkdown(
345
+ await detail.loadCompleteTranscript(),
346
+ conversation,
347
+ )
348
+ : undefined
349
+ }
350
+ />
351
+ }
352
+ linkedWork={
353
+ hasConversationAnnotations(detail.data?.annotations) ? (
354
+ <ConversationAnnotations detail={detail.data} />
355
+ ) : null
356
+ }
357
+ archive={{
358
+ archived: Boolean(conversation?.archivedAt),
359
+ disabled: !conversation || archive.isPending,
360
+ error: Boolean(archive.error),
361
+ onClick: () =>
362
+ archive.mutate({
363
+ archived: !conversation?.archivedAt,
364
+ lastSeenAt: conversation!.lastSeenAt,
365
+ }),
366
+ pending: archive.isPending,
367
+ }}
368
+ live={live}
369
+ meta={
370
+ <ConversationHeaderMeta
371
+ identity={identity}
372
+ stats={
373
+ hasConversationStats({
374
+ conversation,
375
+ detail: detail.data,
376
+ variant: "compact",
377
+ }) ? (
378
+ <ConversationStats
379
+ conversation={conversation}
380
+ detail={detail.data}
381
+ variant="compact"
382
+ />
383
+ ) : null
384
+ }
385
+ />
386
+ }
387
+ onSearchChange={setSearch}
388
+ onViewChange={onViewChange}
389
+ privacy={
390
+ <ConversationPrivacyChip visibility={conversation?.visibility} />
391
+ }
392
+ search={search}
393
+ title={title}
394
+ view={view}
395
+ />
396
+ }
209
397
  scrollMinTall
210
398
  scrollAriaLabel="Conversation transcript"
211
- scrollClassName="px-4 pb-4 md:px-7 md:pb-6"
399
+ scrollClassName={
400
+ view === "raw" ? undefined : "px-4 pb-4 md:px-7 md:pb-6"
401
+ }
212
402
  scroll={
213
403
  <section className="min-w-0">
214
- <ConversationHeader
215
- agentTree={
216
- agentTree ? <ConversationAgentTree nodes={agentTree} /> : null
217
- }
218
- conversationId={conversationId}
219
- detailsOpen={detailsOpen}
220
- onDetailsClick={() => setDetailsOpen((open) => !open)}
221
- forks={
222
- detail.data?.forkedFromConversationId ||
223
- detail.data?.forks?.length ? (
224
- <ConversationForkLinks
225
- forkedFromConversationId={
226
- detail.data.forkedFromConversationId
227
- }
228
- forkedFromTitle={detail.data.forkedFromTitle}
229
- forks={detail.data.forks}
230
- />
231
- ) : null
232
- }
233
- copyAction={
234
- <CopyMarkdownButton
235
- key={conversationDetail?.conversationId ?? "loading"}
236
- getMarkdown={
237
- conversationDetail
238
- ? async () =>
239
- buildConversationMarkdown(
240
- await detail.loadCompleteTranscript(),
241
- conversation,
242
- )
243
- : undefined
244
- }
245
- />
246
- }
247
- linkedWork={
248
- hasConversationAnnotations(detail.data?.annotations) ? (
249
- <ConversationAnnotations
250
- detail={detail.data}
251
- layout="strip"
252
- />
253
- ) : null
254
- }
255
- archive={{
256
- archived: Boolean(conversation?.archivedAt),
257
- disabled: !conversation || archive.isPending,
258
- error: Boolean(archive.error),
259
- onClick: () =>
260
- archive.mutate({
261
- archived: !conversation?.archivedAt,
262
- lastSeenAt: conversation!.lastSeenAt,
263
- }),
264
- pending: archive.isPending,
265
- }}
266
- live={live}
267
- meta={
268
- <ConversationHeaderMeta
269
- identity={identity}
270
- stats={
271
- hasConversationStats({
272
- conversation,
273
- detail: detail.data,
274
- variant: "compact",
275
- }) ? (
276
- <ConversationStats
277
- conversation={conversation}
278
- detail={detail.data}
279
- variant="compact"
280
- />
281
- ) : null
282
- }
283
- />
284
- }
285
- onSearchChange={setSearch}
286
- onViewChange={onViewChange}
287
- privacy={
288
- <ConversationPrivacyChip
289
- visibility={conversation?.visibility}
290
- />
291
- }
292
- search={search}
293
- title={title}
294
- view={view}
295
- />
296
-
297
404
  <div
298
405
  className={
299
406
  view === "raw"
300
- ? "w-full pt-5 md:pt-7"
407
+ ? "w-full"
301
408
  : "mx-auto w-full max-w-[52.5rem] pt-5 md:pt-7"
302
409
  }
303
410
  >
304
411
  {detail.isPending ? (
305
- <TranscriptLoading />
412
+ view === "raw" ? (
413
+ <ConversationEventLogLoading />
414
+ ) : (
415
+ <TranscriptLoading />
416
+ )
306
417
  ) : detail.error && !detail.data ? (
307
418
  <Card className="border-dashboard-border bg-dashboard-fill-soft p-4 font-sans text-xs leading-relaxed text-dashboard-text-muted">
308
419
  {detail.error.message}
@@ -329,8 +440,10 @@ export function ConversationPage(props: {
329
440
  }
330
441
  onOpenSubagentTranscript={onOpenSubagentTranscript}
331
442
  search={search}
443
+ onSearchChange={setSearch}
332
444
  selectedEventSeq={selectedEventSeq}
333
445
  transcript={transcript}
446
+ turnContextPanel={turnContextPanel}
334
447
  view={view}
335
448
  />
336
449
  </>
@@ -358,43 +471,61 @@ export function ConversationPage(props: {
358
471
  ) : undefined
359
472
  }
360
473
  />
361
- {detailsOpen ? (
474
+ {showDetails ? (
362
475
  <ConversationDetails
363
476
  conversationId={conversationId}
364
477
  identity={identity}
365
- lastActivityAt={conversation?.lastSeenAt}
366
478
  sentryConversationUrl={detail.data?.sentryConversationUrl}
367
- onClose={() => setDetailsOpen(false)}
479
+ onClose={() => setDetailsChoice(false)}
368
480
  onOpenSubagent={setSubagentTarget}
369
- annotations={
370
- hasConversationAnnotations(detail.data?.annotations) ? (
371
- <ConversationAnnotations detail={detail.data} />
372
- ) : null
373
- }
481
+ links={conversationLinks(detail.data)}
374
482
  brief={
375
483
  detail.data?.brief ? (
376
484
  <ConversationBrief brief={detail.data.brief} variant="summary" />
377
485
  ) : null
378
486
  }
379
- stats={
487
+ overview={
380
488
  hasConversationStats({
381
489
  conversation,
382
490
  detail: detail.data,
383
491
  }) ? (
384
- <div className="grid min-w-0 gap-5">
385
- <ConversationStats
386
- conversation={conversation}
387
- detail={detail.data}
388
- />
389
- <ConversationUsageBreakdown
390
- conversation={conversation}
391
- detail={detail.data}
392
- />
393
- </div>
492
+ <ConversationStats
493
+ conversation={conversation}
494
+ detail={detail.data}
495
+ />
496
+ ) : null
497
+ }
498
+ usage={
499
+ hasConversationUsageBreakdown({
500
+ conversation,
501
+ detail: detail.data,
502
+ }) ? (
503
+ <ConversationUsageBreakdown
504
+ conversation={conversation}
505
+ detail={detail.data}
506
+ />
394
507
  ) : null
395
508
  }
396
509
  />
397
510
  ) : null}
511
+ {showEvent ? (
512
+ <ConversationEventDetails
513
+ conversation={transcript}
514
+ event={selectedEvent}
515
+ onClose={closeEventDetails}
516
+ onSelect={setSelectedEventSeq}
517
+ {...adjacentEventLogSeqs(eventRows, selectedEventSeq)}
518
+ />
519
+ ) : null}
520
+ {turnContext ? (
521
+ <TranscriptSearchProvider query={search}>
522
+ <TurnContextDetails
523
+ contexts={turnContext.contexts}
524
+ key={turnContext.id}
525
+ onClose={closeTurnContext}
526
+ />
527
+ </TranscriptSearchProvider>
528
+ ) : null}
398
529
  <SubagentTranscriptDrawer
399
530
  onClose={() => setSubagentTarget(undefined)}
400
531
  target={subagentTarget}
@@ -19,7 +19,12 @@ import {
19
19
  } from "./conversationSections";
20
20
  import { EmptyTelemetry } from "../components/EmptyTelemetry";
21
21
  import { SearchInput } from "../components/SearchInput";
22
- import { Skeleton } from "../components/Skeleton";
22
+ import { Skeleton } from "../components/ui/skeleton";
23
+ import {
24
+ Tooltip,
25
+ TooltipContent,
26
+ TooltipTrigger,
27
+ } from "../components/ui/tooltip";
23
28
  import { ConversationSidebarAnnotations } from "./ConversationMeta";
24
29
  import { ConversationListStatusIcon } from "./ConversationListStatusIcon";
25
30
 
@@ -104,15 +109,21 @@ export function ConversationSidebar(props: {
104
109
  </h2>
105
110
  <div className="flex items-center gap-0.5">
106
111
  {isLanding ? null : (
107
- <button
108
- aria-label="New conversation"
109
- className="grid size-7 cursor-pointer place-items-center rounded-md text-dashboard-text-muted transition hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus"
110
- onClick={props.onNewConversation}
111
- title="New conversation"
112
- type="button"
113
- >
114
- <SquarePen aria-hidden="true" size={15} />
115
- </button>
112
+ <Tooltip>
113
+ <TooltipTrigger
114
+ render={
115
+ <button
116
+ aria-label="New conversation"
117
+ className="grid size-7 cursor-pointer place-items-center rounded-md text-dashboard-text-muted transition hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus"
118
+ onClick={props.onNewConversation}
119
+ type="button"
120
+ >
121
+ <SquarePen aria-hidden="true" size={15} />
122
+ </button>
123
+ }
124
+ />
125
+ <TooltipContent size="compact">New conversation</TooltipContent>
126
+ </Tooltip>
116
127
  )}
117
128
  </div>
118
129
  </div>
@@ -321,25 +332,33 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(props: {
321
332
  ) : null}
322
333
  </div>
323
334
  </Link>
324
- <button
325
- aria-label={`${props.conversation.archivedAt ? "Restore" : "Archive"} ${title}`}
326
- className="absolute right-1.5 top-1/2 z-10 grid size-7 -translate-y-1/2 cursor-pointer place-items-center rounded-md bg-dashboard-surface-active text-dashboard-text-muted transition hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus sm:pointer-events-none sm:opacity-0 sm:focus:pointer-events-auto sm:focus:opacity-100 sm:group-focus-within:pointer-events-auto sm:group-focus-within:opacity-100 sm:group-hover:pointer-events-auto sm:group-hover:opacity-100 disabled:cursor-not-allowed"
327
- disabled={archive.isPending}
328
- onClick={() =>
329
- archive.mutate({
330
- archived: !props.conversation.archivedAt,
331
- lastSeenAt: props.conversation.lastSeenAt,
332
- })
333
- }
334
- title={`${props.conversation.archivedAt ? "Restore" : "Archive"} ${title}`}
335
- type="button"
336
- >
337
- {props.conversation.archivedAt ? (
338
- <ArchiveRestore aria-hidden="true" size={14} />
339
- ) : (
340
- <Archive aria-hidden="true" size={14} />
341
- )}
342
- </button>
335
+ <Tooltip>
336
+ <TooltipTrigger
337
+ render={
338
+ <button
339
+ aria-label={`${props.conversation.archivedAt ? "Restore" : "Archive"} ${title}`}
340
+ className="absolute right-1.5 top-1/2 z-10 grid size-7 -translate-y-1/2 cursor-pointer place-items-center rounded-md bg-dashboard-surface-active text-dashboard-text-muted transition hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus sm:pointer-events-none sm:opacity-0 sm:focus:pointer-events-auto sm:focus:opacity-100 sm:group-focus-within:pointer-events-auto sm:group-focus-within:opacity-100 sm:group-hover:pointer-events-auto sm:group-hover:opacity-100 disabled:cursor-not-allowed"
341
+ disabled={archive.isPending}
342
+ onClick={() =>
343
+ archive.mutate({
344
+ archived: !props.conversation.archivedAt,
345
+ lastSeenAt: props.conversation.lastSeenAt,
346
+ })
347
+ }
348
+ type="button"
349
+ >
350
+ {props.conversation.archivedAt ? (
351
+ <ArchiveRestore aria-hidden="true" size={14} />
352
+ ) : (
353
+ <Archive aria-hidden="true" size={14} />
354
+ )}
355
+ </button>
356
+ }
357
+ />
358
+ <TooltipContent size="compact">
359
+ {props.conversation.archivedAt ? "Restore" : "Archive"}
360
+ </TooltipContent>
361
+ </Tooltip>
343
362
  </div>
344
363
  );
345
364
  });
@@ -167,6 +167,19 @@ function BreakdownTable(props: {
167
167
  );
168
168
  }
169
169
 
170
+ /** True when the Conversation has model or auxiliary usage to break down. */
171
+ export function hasConversationUsageBreakdown(props: {
172
+ conversation: Conversation | undefined;
173
+ detail?: ConversationDetailReport;
174
+ }): boolean {
175
+ if (!props.conversation) return false;
176
+ const auxiliaryCosts =
177
+ props.detail?.auxiliaryCosts ?? props.conversation.auxiliaryCosts;
178
+ return Boolean(
179
+ props.detail?.modelUsage?.length || auxiliaryCosts?.operations?.length,
180
+ );
181
+ }
182
+
170
183
  /**
171
184
  * Break a Conversation's usage down by agent, model, and auxiliary
172
185
  * operation. The header tooltips show only the largest items; this lists
@@ -177,7 +190,9 @@ export function ConversationUsageBreakdown(props: {
177
190
  conversation: Conversation | undefined;
178
191
  detail?: ConversationDetailReport;
179
192
  }) {
180
- if (!props.conversation) return null;
193
+ if (!hasConversationUsageBreakdown(props) || !props.conversation) {
194
+ return null;
195
+ }
181
196
  const modelUsage = props.detail?.modelUsage ?? [];
182
197
  const auxiliaryCosts =
183
198
  props.detail?.auxiliaryCosts ?? props.conversation.auxiliaryCosts;
@@ -193,8 +208,6 @@ export function ConversationUsageBreakdown(props: {
193
208
  const operations = [...(auxiliaryCosts?.operations ?? [])].sort(
194
209
  (a, b) => b.costUsd - a.costUsd,
195
210
  );
196
- if (!modelUsage.length && !operations.length) return null;
197
-
198
211
  const formatCost = costFormatter(cost ?? 0);
199
212
  const formatTokens = (value: number | undefined) =>
200
213
  value === undefined ? NOT_RECORDED : formatCompactNumber(value);