@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
@@ -50,7 +50,10 @@ export function TranscriptMessageContextView(props: {
50
50
  </div>
51
51
  {!props.redacted && props.message.contexts?.length ? (
52
52
  <div className="mt-2">
53
- <TranscriptTurnContextView contexts={props.message.contexts} />
53
+ <TranscriptTurnContextView
54
+ contexts={props.message.contexts}
55
+ messageId={props.message.messageId}
56
+ />
54
57
  </div>
55
58
  ) : null}
56
59
  </details>
@@ -78,7 +81,10 @@ export function TranscriptMessageContextView(props: {
78
81
  </div>
79
82
  {!props.redacted && props.message.contexts?.length ? (
80
83
  <div className="mt-2">
81
- <TranscriptTurnContextView contexts={props.message.contexts} />
84
+ <TranscriptTurnContextView
85
+ contexts={props.message.contexts}
86
+ messageId={props.message.messageId}
87
+ />
82
88
  </div>
83
89
  ) : null}
84
90
  </article>
@@ -122,7 +122,12 @@ function messageAction(
122
122
  conversation: ConversationTranscript,
123
123
  ): ReactNode {
124
124
  if (message.role === "user" && message.contexts?.length) {
125
- return <TranscriptTurnContextView contexts={message.contexts} />;
125
+ return (
126
+ <TranscriptTurnContextView
127
+ contexts={message.contexts}
128
+ messageId={message.messageId}
129
+ />
130
+ );
126
131
  }
127
132
  if (
128
133
  conversation.canFork &&
@@ -1,15 +1,18 @@
1
+ import { useState } from "react";
1
2
  import { Lightbulb } from "lucide-react";
2
3
 
4
+ import {
5
+ TraceCollapsible,
6
+ TraceCollapsibleContent,
7
+ TraceCollapsibleTrigger,
8
+ } from "../components/ui/trace";
3
9
  import { formatMessageTimestamp } from "../format";
4
10
  import type { TranscriptViewReasoningPart } from "../types";
5
- import { cn } from "../styles";
6
11
  import {
7
12
  TranscriptHeadingMeta,
8
13
  TranscriptHeadingRow,
9
14
  } from "./TranscriptHeadingRow";
10
15
  import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
11
- import { TranscriptSummary } from "./TranscriptSummary";
12
- import { transcriptActivityBodyClass } from "./ToolFrame";
13
16
 
14
17
  /** Render reasoning collapsed with a short preview until expanded or searched. */
15
18
  export function TranscriptReasoningView(props: {
@@ -17,74 +20,57 @@ export function TranscriptReasoningView(props: {
17
20
  timestamp?: number;
18
21
  }) {
19
22
  const { active: searchActive } = useTranscriptSearch();
23
+ const [open, setOpen] = useState(false);
24
+ const expanded = searchActive || open;
20
25
  const rendered = props.part.redacted ? "Reasoning redacted" : props.part.text;
21
26
  const timestamp = formatMessageTimestamp(props.timestamp);
22
- const summary = (
23
- <>
24
- <span
25
- aria-label="Reasoning"
26
- className="inline-flex size-4 shrink-0 items-center justify-center text-violet-200/80"
27
- title="Reasoning"
28
- >
29
- <Lightbulb aria-hidden="true" size={14} strokeWidth={2} />
30
- </span>
31
- <TranscriptHeadingRow
32
- left={
33
- searchActive ? (
34
- <span className="block min-w-0 text-dashboard-text-muted">
35
- Reasoning
36
- </span>
37
- ) : (
38
- <>
39
- <span className="block min-w-0 truncate italic text-dashboard-text-muted group-open/reasoning:hidden">
40
- {rendered}
41
- </span>
42
- <span className="hidden min-w-0 text-dashboard-text-muted group-open/reasoning:inline">
43
- Reasoning
44
- </span>
45
- </>
46
- )
47
- }
48
- right={
49
- timestamp ? (
50
- <TranscriptHeadingMeta className="min-w-0 break-words text-xs tabular-nums text-dashboard-text-muted max-md:hidden">
51
- {timestamp}
52
- </TranscriptHeadingMeta>
53
- ) : undefined
54
- }
55
- rightClassName="min-w-0 max-md:hidden"
56
- />
57
- </>
58
- );
59
- const content = (
60
- <div className={cn(transcriptActivityBodyClass, "px-3 py-2.5")}>
61
- <div className="min-w-0 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text-subtle">
62
- <HighlightText text={rendered} />
63
- </div>
64
- </div>
65
- );
66
-
67
- if (searchActive) {
68
- return (
69
- <div className={reasoningFrameClass()}>
70
- <div className="grid list-none grid-cols-[1rem_minmax(0,1fr)] items-center gap-2 py-1.5">
71
- {summary}
72
- </div>
73
- {content}
74
- </div>
75
- );
76
- }
77
27
 
78
28
  return (
79
- <details className={cn("group/reasoning", reasoningFrameClass())}>
80
- <TranscriptSummary className="-mx-2 grid grid-cols-[1rem_minmax(0,1fr)] items-center gap-2">
81
- {summary}
82
- </TranscriptSummary>
83
- {content}
84
- </details>
29
+ <TraceCollapsible
30
+ className="text-xs leading-tight"
31
+ // Search holds rows open, so the row button is off until search ends.
32
+ disabled={searchActive}
33
+ open={expanded}
34
+ onOpenChange={setOpen}
35
+ >
36
+ <TraceCollapsibleTrigger className="-mx-2 w-auto">
37
+ <span className="grid min-w-0 flex-1 grid-cols-[1rem_minmax(0,1fr)] items-center gap-2">
38
+ <span
39
+ aria-label="Reasoning"
40
+ className="inline-flex size-4 shrink-0 items-center justify-center text-violet-200/80"
41
+ title="Reasoning"
42
+ >
43
+ <Lightbulb aria-hidden="true" size={14} strokeWidth={2} />
44
+ </span>
45
+ <TranscriptHeadingRow
46
+ left={
47
+ expanded ? (
48
+ <span className="block min-w-0 text-dashboard-text-muted">
49
+ Reasoning
50
+ </span>
51
+ ) : (
52
+ <span className="block min-w-0 truncate italic text-dashboard-text-muted">
53
+ {rendered}
54
+ </span>
55
+ )
56
+ }
57
+ right={
58
+ timestamp ? (
59
+ <TranscriptHeadingMeta className="min-w-0 break-words text-xs tabular-nums text-dashboard-text-muted max-md:hidden">
60
+ {timestamp}
61
+ </TranscriptHeadingMeta>
62
+ ) : undefined
63
+ }
64
+ rightClassName="min-w-0 max-md:hidden"
65
+ />
66
+ </span>
67
+ </TraceCollapsibleTrigger>
68
+ {/* Keep closed reasoning in the page, so browser find can reach it. */}
69
+ <TraceCollapsibleContent className="px-3 py-2.5" hiddenUntilFound>
70
+ <div className="min-w-0 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text-subtle">
71
+ <HighlightText text={rendered} />
72
+ </div>
73
+ </TraceCollapsibleContent>
74
+ </TraceCollapsible>
85
75
  );
86
76
  }
87
-
88
- function reasoningFrameClass(): string {
89
- return "min-w-0 text-xs leading-tight";
90
- }
@@ -8,9 +8,9 @@ import { subagentStatusTone } from "./ConversationChildAgents";
8
8
  import { HighlightText } from "./transcriptSearch";
9
9
 
10
10
  /**
11
- * Render a child agent as a compact card in the transcript stream. The card
12
- * matches the child agent rows in conversation details: name, status, and run
13
- * time first, then the task title. It opens the child transcript.
11
+ * Render a child agent as a compact card in the transcript stream: name,
12
+ * status, and run time first, then the task title. It opens the child
13
+ * transcript.
14
14
  */
15
15
  export function TranscriptSubagentView(props: {
16
16
  onOpenTranscript?: (part: TranscriptViewSubagentPart) => void;
@@ -1,6 +1,10 @@
1
1
  import { createContext, useContext, useState, type ReactNode } from "react";
2
2
 
3
- import { Tooltip } from "../components/Tooltip";
3
+ import {
4
+ Tooltip,
5
+ TooltipContent,
6
+ TooltipTrigger,
7
+ } from "../components/ui/tooltip";
4
8
  import {
5
9
  formatMessageTimestamp,
6
10
  formatRelativeMessageTimestamp,
@@ -51,18 +55,16 @@ export function TranscriptTimestamp(props: { value: number | undefined }) {
51
55
 
52
56
  const details = formatTranscriptTimestampDetails(props.value!);
53
57
  return (
54
- <Tooltip
55
- content={
58
+ <Tooltip>
59
+ <TooltipTrigger render={button} />
60
+ <TooltipContent>
56
61
  <span className="grid grid-cols-[auto_auto] gap-x-3">
57
62
  <span>Local</span>
58
63
  <span>{details.local}</span>
59
64
  <span>UTC</span>
60
65
  <span>{details.utc}</span>
61
66
  </span>
62
- }
63
- placement="above"
64
- >
65
- {button}
67
+ </TooltipContent>
66
68
  </Tooltip>
67
69
  );
68
70
  }
@@ -96,7 +96,7 @@ function ToolSignature(props: {
96
96
  <HighlightText text={props.name} />
97
97
  </ShimmerText>
98
98
  {props.preview && !searchActive ? (
99
- <code className="min-w-0 truncate font-[inherit] text-dashboard-text-muted group-open:hidden">
99
+ <code className="min-w-0 truncate font-[inherit] text-dashboard-text-muted group-aria-expanded/trace-collapsible-trigger:hidden">
100
100
  (<HighlightText text={props.preview} />)
101
101
  </code>
102
102
  ) : null}
@@ -1,7 +1,14 @@
1
1
  import { Brain, Braces, ChevronRight, Layers } from "lucide-react";
2
- import { useId, useState } from "react";
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useId,
6
+ useState,
7
+ type ReactNode,
8
+ } from "react";
3
9
  import { createPortal } from "react-dom";
4
10
 
11
+ import { DetailsPanel } from "../components/DetailsPanel";
5
12
  import { Drawer } from "../components/Drawer";
6
13
  import type { TranscriptViewTurnContext } from "../types";
7
14
  import { formatMessageTimestamp, formatTime } from "../format";
@@ -10,57 +17,91 @@ import { cn } from "../styles";
10
17
  import { HighlightText } from "./transcriptSearch";
11
18
  import { TranscriptSummary } from "./TranscriptSummary";
12
19
 
13
- /** Show structured context attached to one transcript user message. */
20
+ /** The turn context that a page panel shows, keyed by its message. */
21
+ export type TurnContextSelection = {
22
+ contexts: TranscriptViewTurnContext[];
23
+ id: string;
24
+ };
25
+
26
+ /** Let the page that owns a panel show turn context there. */
27
+ export type TurnContextPanel = {
28
+ openId?: string;
29
+ onToggle(selection: TurnContextSelection): void;
30
+ };
31
+
32
+ const TurnContextPanelContext = createContext<TurnContextPanel | undefined>(
33
+ undefined,
34
+ );
35
+
36
+ /**
37
+ * Route turn context in one transcript to its owner's panel. Without a
38
+ * `panel`, turn context opens in a modal drawer. Each transcript sets this, so
39
+ * a nested transcript does not use the panel of the page behind it.
40
+ */
41
+ export function TurnContextPanelProvider(props: {
42
+ children: ReactNode;
43
+ panel?: TurnContextPanel;
44
+ }) {
45
+ return (
46
+ <TurnContextPanelContext value={props.panel}>
47
+ {props.children}
48
+ </TurnContextPanelContext>
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Show structured context attached to one transcript user message. The panel
54
+ * keys context by `messageId`, so every control for one message, including a
55
+ * remounted one, shows the same open state.
56
+ */
14
57
  export function TranscriptTurnContextView(props: {
15
58
  contexts: TranscriptViewTurnContext[];
59
+ messageId?: string;
16
60
  }) {
61
+ const panel = useContext(TurnContextPanelContext);
17
62
  const [open, setOpen] = useState(false);
18
- const titleId = useId();
63
+ const instanceId = useId();
64
+ const id = props.messageId ?? instanceId;
65
+ const expanded = panel ? panel.openId === id : open;
19
66
 
20
67
  return (
21
68
  <>
22
69
  <button
23
- aria-haspopup="dialog"
24
- aria-expanded={open}
70
+ aria-haspopup={panel ? undefined : "dialog"}
71
+ aria-expanded={expanded}
25
72
  aria-label="View turn context"
26
73
  className={cn(
27
74
  "inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus",
28
- open && "bg-dashboard-fill-hover text-dashboard-text",
75
+ expanded && "bg-dashboard-fill-hover text-dashboard-text",
29
76
  )}
30
- onClick={() => setOpen(true)}
77
+ data-turn-context-id={id}
78
+ onClick={() => {
79
+ if (!panel) return setOpen(true);
80
+ panel.onToggle({ contexts: props.contexts, id });
81
+ }}
31
82
  title="View context"
32
83
  type="button"
33
84
  >
34
85
  <Layers aria-hidden="true" size={14} strokeWidth={1.8} />
35
86
  </button>
36
87
 
37
- {open
88
+ {!panel && open
38
89
  ? createPortal(
39
90
  <Drawer
40
91
  closeLabel="Close turn context"
41
92
  header={
42
- <>
43
- <h2
44
- className="m-0 text-sm font-semibold text-dashboard-text"
45
- id={titleId}
46
- >
47
- Turn context
48
- </h2>
49
- <p className="m-0 mt-1 text-xs text-dashboard-text-muted">
50
- Context supplied with this message
51
- </p>
52
- </>
93
+ <h2
94
+ className="m-0 text-sm font-semibold text-dashboard-text"
95
+ id={instanceId}
96
+ >
97
+ Turn context
98
+ </h2>
53
99
  }
54
100
  onClose={() => setOpen(false)}
55
- openKey={titleId}
56
- titleId={titleId}
101
+ openKey={instanceId}
102
+ titleId={instanceId}
57
103
  >
58
- {props.contexts.map((context, index) => (
59
- <TurnContext
60
- context={context}
61
- key={`${context.pluginName}:${context.kind}:${context.version}:${index}`}
62
- />
63
- ))}
104
+ <TurnContextList contexts={props.contexts} />
64
105
  </Drawer>,
65
106
  document.body,
66
107
  )
@@ -69,6 +110,37 @@ export function TranscriptTurnContextView(props: {
69
110
  );
70
111
  }
71
112
 
113
+ /** Show turn context in the page panel: an aside, or a sheet on small screens. */
114
+ export function TurnContextDetails(props: {
115
+ contexts: TranscriptViewTurnContext[];
116
+ onClose(): void;
117
+ }) {
118
+ return (
119
+ <DetailsPanel
120
+ label="Turn context"
121
+ onClose={props.onClose}
122
+ title={
123
+ <h2 className="m-0 text-base font-semibold text-dashboard-text">
124
+ Turn context
125
+ </h2>
126
+ }
127
+ >
128
+ <div className="px-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
129
+ <TurnContextList contexts={props.contexts} />
130
+ </div>
131
+ </DetailsPanel>
132
+ );
133
+ }
134
+
135
+ function TurnContextList(props: { contexts: TranscriptViewTurnContext[] }) {
136
+ return props.contexts.map((context, index) => (
137
+ <TurnContext
138
+ context={context}
139
+ key={`${context.pluginName}:${context.kind}:${context.version}:${index}`}
140
+ />
141
+ ));
142
+ }
143
+
72
144
  function TurnContext(props: { context: TranscriptViewTurnContext }) {
73
145
  const memory = memoryRecallContent(props.context);
74
146
 
@@ -1,20 +1,31 @@
1
+ import { useState } from "react";
1
2
  import { ArrowDownToLine } from "lucide-react";
2
3
 
3
4
  import type {
4
5
  ConversationTranscript,
5
6
  TranscriptViewSubagentPart,
6
7
  } from "../types";
7
- import { Button } from "../components/Button";
8
+ import { Button } from "../components/ui/button";
9
+ import {
10
+ Tooltip,
11
+ TooltipContent,
12
+ TooltipTrigger,
13
+ } from "../components/ui/tooltip";
8
14
  import { ConversationTranscriptView } from "./ConversationTranscript";
9
15
  import { ConversationEventLog } from "./ConversationEventLog";
10
16
  import {
11
17
  transcriptBottomVersion,
12
18
  transcriptJuniorMessageVersion,
19
+ useAutoLoadPreviousPage,
13
20
  usePinnedTranscriptBottom,
14
21
  } from "./transcriptBottomPinning";
15
22
  import type { TranscriptViewMode } from "./transcriptRenderModel";
16
23
  import { transcriptEmptyClass } from "./transcriptStyles";
17
24
  import { TranscriptSearchProvider } from "./transcriptSearch";
25
+ import {
26
+ TurnContextPanelProvider,
27
+ type TurnContextPanel,
28
+ } from "./TranscriptTurnContextView";
18
29
 
19
30
  /** Render one conversation transcript as ordered message and tool events. */
20
31
  export function Transcript(props: {
@@ -28,16 +39,20 @@ export function Transcript(props: {
28
39
  onSelectedEventSeqChange?(seq: number | undefined): void;
29
40
  responding?: boolean;
30
41
  search?: string;
42
+ onSearchChange?(value: string): void;
31
43
  selectedEventSeq?: number;
32
44
  onOpenSubagentTranscript?: (args: {
33
45
  part: TranscriptViewSubagentPart;
34
46
  conversation: ConversationTranscript;
35
47
  }) => void;
36
48
  transcript?: ConversationTranscript;
49
+ /** Show turn context in the owner's panel. Without it, a drawer opens. */
50
+ turnContextPanel?: TurnContextPanel;
37
51
  view?: TranscriptViewMode;
38
52
  }) {
39
53
  const view = props.view ?? "rich";
40
- const search = props.search ?? "";
54
+ const [localSearch, setLocalSearch] = useState("");
55
+ const search = props.search ?? localSearch;
41
56
  const historyLabel = view === "raw" ? "events" : "messages";
42
57
  const redacted = props.transcript?.eventHistory.status === "redacted";
43
58
  const bottomPinning = usePinnedTranscriptBottom({
@@ -54,6 +69,21 @@ export function Transcript(props: {
54
69
  },
55
70
  });
56
71
 
72
+ const loadPreviousPage = () => {
73
+ bottomPinning.preserveViewportForPrepend();
74
+ props.onLoadPreviousPage?.();
75
+ };
76
+ const previousPageRef = useAutoLoadPreviousPage({
77
+ enabled: Boolean(
78
+ props.onLoadPreviousPage &&
79
+ props.hasPreviousPage &&
80
+ !props.loadingPreviousPage &&
81
+ !props.historyError,
82
+ ),
83
+ historyVersion: props.historyVersion ?? "empty",
84
+ onLoad: loadPreviousPage,
85
+ });
86
+
57
87
  if (!props.transcript) {
58
88
  return (
59
89
  <div className={transcriptEmptyClass()}>
@@ -71,16 +101,13 @@ export function Transcript(props: {
71
101
  </div>
72
102
  ) : null}
73
103
  {props.hasPreviousPage || props.loadingPreviousPage ? (
74
- <div className="mb-3 flex items-center gap-3">
104
+ <div className="mb-3 flex items-center gap-3" ref={previousPageRef}>
75
105
  <span className="h-px min-w-4 flex-1 bg-dashboard-fill-strong" />
76
106
  <button
77
107
  aria-label="Load earlier events"
78
108
  className="shrink-0 cursor-pointer border-0 bg-transparent px-1 py-0.5 font-sans text-xs font-medium text-dashboard-text-muted transition-colors hover:text-dashboard-text focus-visible:outline focus-visible:outline-1 focus-visible:outline-dashboard-focus/55 disabled:cursor-not-allowed disabled:opacity-50"
79
109
  disabled={props.loadingPreviousPage}
80
- onClick={() => {
81
- bottomPinning.preserveViewportForPrepend();
82
- props.onLoadPreviousPage?.();
83
- }}
110
+ onClick={loadPreviousPage}
84
111
  title="Load earlier events"
85
112
  type="button"
86
113
  >
@@ -103,15 +130,18 @@ export function Transcript(props: {
103
130
  <ConversationEventLog
104
131
  key={props.transcript.conversationId}
105
132
  conversation={props.transcript}
133
+ onSearchChange={props.onSearchChange ?? setLocalSearch}
106
134
  onSelectedSeqChange={props.onSelectedEventSeqChange}
107
135
  selectedSeq={props.selectedEventSeq}
108
136
  />
109
137
  ) : (
110
- <ConversationTranscriptView
111
- onOpenSubagentTranscript={props.onOpenSubagentTranscript}
112
- conversation={props.transcript}
113
- responding={props.responding ?? props.live ?? false}
114
- />
138
+ <TurnContextPanelProvider panel={props.turnContextPanel}>
139
+ <ConversationTranscriptView
140
+ onOpenSubagentTranscript={props.onOpenSubagentTranscript}
141
+ conversation={props.transcript}
142
+ responding={props.responding ?? props.live ?? false}
143
+ />
144
+ </TurnContextPanelProvider>
115
145
  )}
116
146
  <div
117
147
  aria-hidden="true"
@@ -141,21 +171,27 @@ function JumpToLatestButton(props: {
141
171
 
142
172
  return (
143
173
  <div className="pointer-events-none sticky bottom-3 z-20 flex h-0 justify-center px-3 md:fixed md:inset-x-auto md:bottom-6 md:right-8 md:h-auto md:justify-end md:px-0">
144
- <Button
145
- aria-label={label}
146
- className="pointer-events-auto relative -translate-y-full rounded-lg !border !border-dashboard-border-emphasis !bg-dashboard-surface-active text-dashboard-text shadow-[0_6px_24px_rgba(0,0,0,0.36)] hover:!border-dashboard-border-interactive hover:!bg-dashboard-surface-hover hover:!text-dashboard-text md:translate-y-0"
147
- onClick={props.onClick}
148
- size="icon"
149
- title={label}
150
- >
151
- <ArrowDownToLine aria-hidden="true" size={16} strokeWidth={2} />
152
- {props.hasPendingUpdate ? (
153
- <span
154
- aria-hidden="true"
155
- className="absolute right-1.5 top-1.5 size-2 bg-emerald-300"
156
- />
157
- ) : null}
158
- </Button>
174
+ <Tooltip>
175
+ <TooltipTrigger
176
+ render={
177
+ <Button
178
+ aria-label={label}
179
+ className="pointer-events-auto relative -translate-y-full rounded-lg !border !border-dashboard-border-emphasis !bg-dashboard-surface-active text-dashboard-text shadow-[0_6px_24px_rgba(0,0,0,0.36)] hover:!border-dashboard-border-interactive hover:!bg-dashboard-surface-hover hover:!text-dashboard-text md:translate-y-0"
180
+ onClick={props.onClick}
181
+ size="icon"
182
+ >
183
+ <ArrowDownToLine aria-hidden="true" size={16} strokeWidth={2} />
184
+ {props.hasPendingUpdate ? (
185
+ <span
186
+ aria-hidden="true"
187
+ className="absolute right-1.5 top-1.5 size-2 bg-emerald-300"
188
+ />
189
+ ) : null}
190
+ </Button>
191
+ }
192
+ />
193
+ <TooltipContent size="compact">{label}</TooltipContent>
194
+ </Tooltip>
159
195
  </div>
160
196
  );
161
197
  }
@@ -118,3 +118,60 @@ export function eventLogTone(data: ConversationReportEventData): string {
118
118
  }
119
119
  return "text-dashboard-text-subtle";
120
120
  }
121
+
122
+ /** One event log entry with its display text. */
123
+ export type EventLogRow = {
124
+ event: ConversationReportEvent;
125
+ summary: string;
126
+ metadata: string;
127
+ };
128
+
129
+ /** Build the display text for each event log entry. */
130
+ export function eventLogRows(
131
+ events: readonly ConversationReportEvent[],
132
+ ): EventLogRow[] {
133
+ return events.map((event) => {
134
+ const model = eventLogModel(event);
135
+ return {
136
+ event,
137
+ summary: eventLogSummary(event.data).replace(/\s+/g, " ").slice(0, 300),
138
+ metadata: [
139
+ model?.modelId,
140
+ model?.modelProfile,
141
+ model?.reasoningLevel && `${model.reasoningLevel} reasoning`,
142
+ eventLogUsage(event),
143
+ ]
144
+ .filter(Boolean)
145
+ .join(" · "),
146
+ };
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Keep the rows that match a trimmed, lowercase search query. The match
152
+ * includes the full event payload, so large payloads are serialized here.
153
+ */
154
+ export function filterEventLogRows(
155
+ rows: EventLogRow[],
156
+ normalizedQuery: string,
157
+ ): EventLogRow[] {
158
+ if (!normalizedQuery) return rows;
159
+ return rows.filter(({ event, summary, metadata }) =>
160
+ `${summary} ${metadata} ${JSON.stringify(event)}`
161
+ .toLowerCase()
162
+ .includes(normalizedQuery),
163
+ );
164
+ }
165
+
166
+ /** Find the visible rows before and after the selected event. */
167
+ export function adjacentEventLogSeqs(
168
+ rows: readonly EventLogRow[],
169
+ seq: number | undefined,
170
+ ): { previous?: number; next?: number } {
171
+ const index = rows.findIndex((row) => row.event.seq === seq);
172
+ if (index === -1) return {};
173
+ return {
174
+ previous: rows[index - 1]?.event.seq,
175
+ next: rows[index + 1]?.event.seq,
176
+ };
177
+ }