@sentry/junior-dashboard 0.264.0 → 0.266.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 (213) 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/ActorAvatar.tsx +10 -22
  14. package/src/client/components/BottomSheet.tsx +12 -2
  15. package/src/client/components/ConnectionBanner.tsx +4 -3
  16. package/src/client/components/CopyButton.tsx +82 -0
  17. package/src/client/components/DetailsPanel.tsx +133 -0
  18. package/src/client/components/Drawer.tsx +15 -24
  19. package/src/client/components/EmptyTelemetry.tsx +5 -3
  20. package/src/client/components/FilterBar.tsx +16 -31
  21. package/src/client/components/ImageAttachment.tsx +49 -63
  22. package/src/client/components/InlineError.tsx +8 -4
  23. package/src/client/components/Metric.tsx +33 -23
  24. package/src/client/components/Notice.tsx +19 -26
  25. package/src/client/components/PageContentSkeleton.tsx +1 -1
  26. package/src/client/components/Pagination.tsx +27 -27
  27. package/src/client/components/ParticipantAvatarStack.tsx +43 -39
  28. package/src/client/components/ProfileMenu.tsx +27 -93
  29. package/src/client/components/README.md +103 -78
  30. package/src/client/components/SearchInput.tsx +51 -41
  31. package/src/client/components/SegmentedNav.tsx +1 -1
  32. package/src/client/components/ShimmerText.tsx +2 -6
  33. package/src/client/components/ThemeSwitcher.tsx +19 -15
  34. package/src/client/components/VersionDriftBanner.tsx +9 -7
  35. package/src/client/components/charts/ActivityChart.tsx +173 -288
  36. package/src/client/components/charts/CacheWrites.tsx +59 -37
  37. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  38. package/src/client/components/charts/README.md +4 -4
  39. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  40. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  41. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  42. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  43. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  44. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  45. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  46. package/src/client/components/ui/accordion.tsx +77 -0
  47. package/src/client/components/ui/alert-dialog.tsx +89 -0
  48. package/src/client/components/ui/alert.tsx +64 -0
  49. package/src/client/components/ui/avatar.tsx +53 -0
  50. package/src/client/components/ui/button.tsx +86 -0
  51. package/src/client/components/ui/chart.tsx +74 -0
  52. package/src/client/components/ui/checkbox.tsx +26 -0
  53. package/src/client/components/ui/collapsible.tsx +22 -0
  54. package/src/client/components/ui/dialog.tsx +66 -0
  55. package/src/client/components/ui/empty.tsx +32 -0
  56. package/src/client/components/ui/field.tsx +49 -0
  57. package/src/client/components/ui/input-group.tsx +96 -0
  58. package/src/client/components/ui/input.tsx +35 -0
  59. package/src/client/components/ui/native-select.tsx +57 -0
  60. package/src/client/components/ui/pagination.tsx +60 -0
  61. package/src/client/components/ui/popover.tsx +67 -0
  62. package/src/client/components/ui/radio-group.tsx +32 -0
  63. package/src/client/components/ui/sheet.tsx +13 -3
  64. package/src/client/components/ui/skeleton.tsx +19 -0
  65. package/src/client/components/ui/spinner.tsx +21 -0
  66. package/src/client/components/ui/table.tsx +101 -0
  67. package/src/client/components/ui/tabs.tsx +75 -0
  68. package/src/client/components/ui/textarea.tsx +17 -0
  69. package/src/client/components/ui/toast.tsx +215 -0
  70. package/src/client/components/ui/toggle-group.tsx +36 -0
  71. package/src/client/components/ui/toggle.tsx +37 -0
  72. package/src/client/components/ui/tooltip.tsx +168 -0
  73. package/src/client/components/ui/trace.tsx +96 -0
  74. package/src/client/conversations/ChatLayout.tsx +7 -3
  75. package/src/client/conversations/ConversationBrief.tsx +17 -6
  76. package/src/client/conversations/ConversationChildAgents.tsx +5 -5
  77. package/src/client/conversations/ConversationComposer.tsx +26 -12
  78. package/src/client/conversations/ConversationDetails.tsx +147 -62
  79. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  80. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  81. package/src/client/conversations/ConversationHeader.tsx +13 -7
  82. package/src/client/conversations/ConversationHeaderActions.tsx +97 -93
  83. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  84. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  85. package/src/client/conversations/ConversationMeta.tsx +55 -88
  86. package/src/client/conversations/ConversationPage.tsx +220 -101
  87. package/src/client/conversations/ConversationSidebar.tsx +49 -29
  88. package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
  89. package/src/client/conversations/ConversationWatches.tsx +173 -0
  90. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  91. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  92. package/src/client/conversations/DetailsList.tsx +139 -0
  93. package/src/client/conversations/EventDetails.tsx +42 -8
  94. package/src/client/conversations/EventUsage.tsx +99 -74
  95. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  96. package/src/client/conversations/ObjectCard.tsx +20 -8
  97. package/src/client/conversations/PendingMailboxStack.tsx +52 -38
  98. package/src/client/conversations/README.md +89 -13
  99. package/src/client/conversations/ShareConversationButton.tsx +28 -17
  100. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  101. package/src/client/conversations/ToolFrame.tsx +36 -52
  102. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  103. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  104. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  105. package/src/client/conversations/TranscriptEventView.tsx +35 -16
  106. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  107. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  108. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  109. package/src/client/conversations/TranscriptMessageContextView.tsx +35 -18
  110. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  111. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  112. package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
  113. package/src/client/conversations/TranscriptSummary.tsx +34 -2
  114. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  115. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  116. package/src/client/conversations/TranscriptTurnContextView.tsx +144 -65
  117. package/src/client/conversations/TranscriptView.tsx +63 -27
  118. package/src/client/conversations/eventLog.ts +58 -0
  119. package/src/client/conversations/eventTime.ts +60 -0
  120. package/src/client/conversations/queries.ts +29 -0
  121. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  122. package/src/client/conversations/transcriptMentions.tsx +18 -5
  123. package/src/client/format.ts +5 -0
  124. package/src/client/pages/ConnectionsPage.tsx +2 -2
  125. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  126. package/src/client/pages/SettingsPage.tsx +9 -10
  127. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  128. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  129. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  130. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  131. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  132. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  133. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  134. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  135. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  136. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  137. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  138. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  139. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  140. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  141. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  142. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  143. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  144. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  145. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  146. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  147. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  148. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  149. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  150. package/src/client/pages/automations/README.md +57 -0
  151. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  152. package/src/client/pages/automations/automationFormat.ts +37 -23
  153. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  154. package/src/client/pages/code/CodePage.tsx +52 -55
  155. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  156. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  157. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  158. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  159. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  160. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  161. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  162. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  163. package/src/client/pages/memory/memoryRecord.ts +1 -1
  164. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  165. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  166. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  167. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  168. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  169. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  170. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  171. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  172. package/src/client/pages/system/README.md +23 -0
  173. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  174. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  175. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  176. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  177. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  178. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  179. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  180. package/src/client/pages/system/sandboxSize.ts +8 -0
  181. package/src/client/pages/system/workspaceQueries.ts +53 -0
  182. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  183. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  184. package/src/client/styles.ts +4 -0
  185. package/src/client.tsx +7 -1
  186. package/src/mock-reporting/automation-collection.ts +4 -2
  187. package/src/mock-reporting/fixtures.ts +23 -7
  188. package/src/mock-reporting/routes.ts +12 -0
  189. package/src/mock-reporting/watches.ts +94 -0
  190. package/src/shell.ts +4 -9
  191. package/src/tailwind.css +0 -10
  192. package/src/client/components/Button.tsx +0 -118
  193. package/src/client/components/Field.tsx +0 -53
  194. package/src/client/components/SegmentedTabs.tsx +0 -119
  195. package/src/client/components/Select.tsx +0 -52
  196. package/src/client/components/Skeleton.tsx +0 -17
  197. package/src/client/components/TextInput.tsx +0 -58
  198. package/src/client/components/Tooltip.tsx +0 -216
  199. package/src/client/components/charts/useChartLayout.ts +0 -27
  200. package/src/client/components/ui/sheet.LICENSE +0 -21
  201. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  202. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  203. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  204. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  205. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  206. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  207. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  208. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  209. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  210. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  211. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  212. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  213. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -1,66 +1,108 @@
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";
12
+ import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
5
13
  import { Drawer } from "../components/Drawer";
6
14
  import type { TranscriptViewTurnContext } from "../types";
7
15
  import { formatMessageTimestamp, formatTime } from "../format";
8
16
  import { memoryRecallContent, type MemoryRecallContent } from "./turnContext";
9
- import { cn } from "../styles";
17
+ import { cn, dashboardDisclosureChevronClass } from "../styles";
10
18
  import { HighlightText } from "./transcriptSearch";
11
- import { TranscriptSummary } from "./TranscriptSummary";
19
+ import { TranscriptSummaryTrigger } from "./TranscriptSummary";
12
20
 
13
- /** Show structured context attached to one transcript user message. */
21
+ /** The turn context that a page panel shows, keyed by its message. */
22
+ export type TurnContextSelection = {
23
+ contexts: TranscriptViewTurnContext[];
24
+ id: string;
25
+ };
26
+
27
+ /** Let the page that owns a panel show turn context there. */
28
+ export type TurnContextPanel = {
29
+ openId?: string;
30
+ onToggle(selection: TurnContextSelection): void;
31
+ };
32
+
33
+ const TurnContextPanelContext = createContext<TurnContextPanel | undefined>(
34
+ undefined,
35
+ );
36
+
37
+ /**
38
+ * Route turn context in one transcript to its owner's panel. Without a
39
+ * `panel`, turn context opens in a modal drawer. Each transcript sets this, so
40
+ * a nested transcript does not use the panel of the page behind it.
41
+ */
42
+ export function TurnContextPanelProvider(props: {
43
+ children: ReactNode;
44
+ panel?: TurnContextPanel;
45
+ }) {
46
+ return (
47
+ <TurnContextPanelContext value={props.panel}>
48
+ {props.children}
49
+ </TurnContextPanelContext>
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Show structured context attached to one transcript user message. The panel
55
+ * keys context by `messageId`, so every control for one message, including a
56
+ * remounted one, shows the same open state.
57
+ */
14
58
  export function TranscriptTurnContextView(props: {
15
59
  contexts: TranscriptViewTurnContext[];
60
+ messageId?: string;
16
61
  }) {
62
+ const panel = useContext(TurnContextPanelContext);
17
63
  const [open, setOpen] = useState(false);
18
- const titleId = useId();
64
+ const instanceId = useId();
65
+ const id = props.messageId ?? instanceId;
66
+ const expanded = panel ? panel.openId === id : open;
19
67
 
20
68
  return (
21
69
  <>
22
70
  <button
23
- aria-haspopup="dialog"
24
- aria-expanded={open}
71
+ aria-haspopup={panel ? undefined : "dialog"}
72
+ aria-expanded={expanded}
25
73
  aria-label="View turn context"
26
74
  className={cn(
27
75
  "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",
76
+ expanded && "bg-dashboard-fill-hover text-dashboard-text",
29
77
  )}
30
- onClick={() => setOpen(true)}
78
+ data-turn-context-id={id}
79
+ onClick={() => {
80
+ if (!panel) return setOpen(true);
81
+ panel.onToggle({ contexts: props.contexts, id });
82
+ }}
31
83
  title="View context"
32
84
  type="button"
33
85
  >
34
86
  <Layers aria-hidden="true" size={14} strokeWidth={1.8} />
35
87
  </button>
36
88
 
37
- {open
89
+ {!panel && open
38
90
  ? createPortal(
39
91
  <Drawer
40
92
  closeLabel="Close turn context"
41
93
  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
- </>
94
+ <h2
95
+ className="m-0 text-sm font-semibold text-dashboard-text"
96
+ id={instanceId}
97
+ >
98
+ Turn context
99
+ </h2>
53
100
  }
54
101
  onClose={() => setOpen(false)}
55
- openKey={titleId}
56
- titleId={titleId}
102
+ openKey={instanceId}
103
+ titleId={instanceId}
57
104
  >
58
- {props.contexts.map((context, index) => (
59
- <TurnContext
60
- context={context}
61
- key={`${context.pluginName}:${context.kind}:${context.version}:${index}`}
62
- />
63
- ))}
105
+ <TurnContextList contexts={props.contexts} />
64
106
  </Drawer>,
65
107
  document.body,
66
108
  )
@@ -69,6 +111,37 @@ export function TranscriptTurnContextView(props: {
69
111
  );
70
112
  }
71
113
 
114
+ /** Show turn context in the page panel: an aside, or a sheet on small screens. */
115
+ export function TurnContextDetails(props: {
116
+ contexts: TranscriptViewTurnContext[];
117
+ onClose(): void;
118
+ }) {
119
+ return (
120
+ <DetailsPanel
121
+ label="Turn context"
122
+ onClose={props.onClose}
123
+ title={
124
+ <h2 className="m-0 text-base font-semibold text-dashboard-text">
125
+ Turn context
126
+ </h2>
127
+ }
128
+ >
129
+ <div className="px-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
130
+ <TurnContextList contexts={props.contexts} />
131
+ </div>
132
+ </DetailsPanel>
133
+ );
134
+ }
135
+
136
+ function TurnContextList(props: { contexts: TranscriptViewTurnContext[] }) {
137
+ return props.contexts.map((context, index) => (
138
+ <TurnContext
139
+ context={context}
140
+ key={`${context.pluginName}:${context.kind}:${context.version}:${index}`}
141
+ />
142
+ ));
143
+ }
144
+
72
145
  function TurnContext(props: { context: TranscriptViewTurnContext }) {
73
146
  const memory = memoryRecallContent(props.context);
74
147
 
@@ -117,15 +190,17 @@ function MemoryRecall(props: {
117
190
  return (
118
191
  <div className="overflow-hidden rounded-lg border border-dashboard-border-strong">
119
192
  {props.memories.map((memory, index) => (
120
- <details
121
- className="group/memory border-t border-dashboard-border-strong first:border-t-0"
193
+ <Collapsible
194
+ className="border-t border-dashboard-border-strong first:border-t-0"
122
195
  key={memory.id}
123
196
  >
124
- <TranscriptSummary className="flex items-start gap-2.5">
197
+ <TranscriptSummaryTrigger className="group flex items-start gap-2.5">
125
198
  <ChevronRight
126
199
  aria-hidden="true"
127
- className="mt-0.5 shrink-0 text-dashboard-text-muted transition-transform group-open/memory:rotate-90"
128
- size={15}
200
+ className={cn(
201
+ dashboardDisclosureChevronClass,
202
+ "mt-0.5 text-dashboard-text-muted",
203
+ )}
129
204
  />
130
205
  <span className="min-w-0 flex-1">
131
206
  <span className="flex items-center justify-between gap-3">
@@ -140,39 +215,43 @@ function MemoryRecall(props: {
140
215
  <HighlightText text={memory.content} />
141
216
  </span>
142
217
  </span>
143
- </TranscriptSummary>
144
-
145
- <div className="border-t border-dashboard-border bg-dashboard-fill-faint px-4 py-4">
146
- <div className="whitespace-pre-wrap text-sm leading-6 text-dashboard-text">
147
- <HighlightText text={memory.content} />
148
- </div>
218
+ </TranscriptSummaryTrigger>
149
219
 
150
- <dl className="mt-4 grid gap-2 text-xs">
151
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
152
- <dt className="text-dashboard-text-muted">Memory ID</dt>
153
- <dd className="m-0 break-all font-mono text-dashboard-text-muted">
154
- <HighlightText text={memory.id} />
155
- </dd>
156
- </div>
157
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
158
- <dt className="text-dashboard-text-muted">Observed</dt>
159
- <dd className="m-0 text-dashboard-text-muted">
160
- {formatTime(new Date(memory.observedAtMs).toISOString())}
161
- </dd>
162
- </div>
163
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
164
- <dt className="text-dashboard-text-muted">Scope</dt>
165
- <dd className="m-0 text-dashboard-text-muted">
166
- {memory.scope}
167
- </dd>
168
- </div>
169
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
170
- <dt className="text-dashboard-text-muted">Kind</dt>
171
- <dd className="m-0 text-dashboard-text-muted">{memory.kind}</dd>
220
+ <CollapsibleContent hiddenUntilFound>
221
+ <div className="border-t border-dashboard-border bg-dashboard-fill-faint px-4 py-4">
222
+ <div className="whitespace-pre-wrap text-sm leading-6 text-dashboard-text">
223
+ <HighlightText text={memory.content} />
172
224
  </div>
173
- </dl>
174
- </div>
175
- </details>
225
+
226
+ <dl className="mt-4 grid gap-2 text-xs">
227
+ <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
228
+ <dt className="text-dashboard-text-muted">Memory ID</dt>
229
+ <dd className="m-0 break-all font-mono text-dashboard-text-muted">
230
+ <HighlightText text={memory.id} />
231
+ </dd>
232
+ </div>
233
+ <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
234
+ <dt className="text-dashboard-text-muted">Observed</dt>
235
+ <dd className="m-0 text-dashboard-text-muted">
236
+ {formatTime(new Date(memory.observedAtMs).toISOString())}
237
+ </dd>
238
+ </div>
239
+ <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
240
+ <dt className="text-dashboard-text-muted">Scope</dt>
241
+ <dd className="m-0 text-dashboard-text-muted">
242
+ {memory.scope}
243
+ </dd>
244
+ </div>
245
+ <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
246
+ <dt className="text-dashboard-text-muted">Kind</dt>
247
+ <dd className="m-0 text-dashboard-text-muted">
248
+ {memory.kind}
249
+ </dd>
250
+ </div>
251
+ </dl>
252
+ </div>
253
+ </CollapsibleContent>
254
+ </Collapsible>
176
255
  ))}
177
256
  <p className="m-0 border-t border-dashboard-border-strong px-3 py-2 text-xs text-dashboard-text-muted">
178
257
  {props.memories.length}{" "}
@@ -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
  }
@@ -111,6 +111,7 @@ export function eventLogTone(data: ConversationReportEventData): string {
111
111
  }
112
112
  if (
113
113
  (data.type === "guardian_action_reviewed" && data.decision === "ask") ||
114
+ (data.type === "turn_lifecycle" && data.state === "blocked") ||
114
115
  (data.type === "subagent" &&
115
116
  (data.status === "aborted" || data.status === "blocked"))
116
117
  ) {
@@ -118,3 +119,60 @@ export function eventLogTone(data: ConversationReportEventData): string {
118
119
  }
119
120
  return "text-dashboard-text-subtle";
120
121
  }
122
+
123
+ /** One event log entry with its display text. */
124
+ export type EventLogRow = {
125
+ event: ConversationReportEvent;
126
+ summary: string;
127
+ metadata: string;
128
+ };
129
+
130
+ /** Build the display text for each event log entry. */
131
+ export function eventLogRows(
132
+ events: readonly ConversationReportEvent[],
133
+ ): EventLogRow[] {
134
+ return events.map((event) => {
135
+ const model = eventLogModel(event);
136
+ return {
137
+ event,
138
+ summary: eventLogSummary(event.data).replace(/\s+/g, " ").slice(0, 300),
139
+ metadata: [
140
+ model?.modelId,
141
+ model?.modelProfile,
142
+ model?.reasoningLevel && `${model.reasoningLevel} reasoning`,
143
+ eventLogUsage(event),
144
+ ]
145
+ .filter(Boolean)
146
+ .join(" · "),
147
+ };
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Keep the rows that match a trimmed, lowercase search query. The match
153
+ * includes the full event payload, so large payloads are serialized here.
154
+ */
155
+ export function filterEventLogRows(
156
+ rows: EventLogRow[],
157
+ normalizedQuery: string,
158
+ ): EventLogRow[] {
159
+ if (!normalizedQuery) return rows;
160
+ return rows.filter(({ event, summary, metadata }) =>
161
+ `${summary} ${metadata} ${JSON.stringify(event)}`
162
+ .toLowerCase()
163
+ .includes(normalizedQuery),
164
+ );
165
+ }
166
+
167
+ /** Find the visible rows before and after the selected event. */
168
+ export function adjacentEventLogSeqs(
169
+ rows: readonly EventLogRow[],
170
+ seq: number | undefined,
171
+ ): { previous?: number; next?: number } {
172
+ const index = rows.findIndex((row) => row.event.seq === seq);
173
+ if (index === -1) return {};
174
+ return {
175
+ previous: rows[index - 1]?.event.seq,
176
+ next: rows[index + 1]?.event.seq,
177
+ };
178
+ }
@@ -0,0 +1,60 @@
1
+ import { useMemo } from "react";
2
+ import { useLocalStorage } from "@uidotdev/usehooks";
3
+
4
+ const storageKey = "junior:event-log:time-mode";
5
+
6
+ /** Share the browser-only UTC/local preference between event rows and details. */
7
+ export function useEventTime() {
8
+ const [mode, setMode] = useLocalStorage<"utc" | "local">(storageKey, "utc");
9
+ const local = mode === "local";
10
+ const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
11
+ const timeZone = local ? localTimeZone : "UTC";
12
+ const formatters = useMemo(() => {
13
+ const clock: Intl.DateTimeFormatOptions = {
14
+ timeZone,
15
+ hour: "2-digit",
16
+ minute: "2-digit",
17
+ second: "2-digit",
18
+ fractionalSecondDigits: 3,
19
+ hourCycle: "h23",
20
+ numberingSystem: "latn",
21
+ };
22
+ return {
23
+ clock: new Intl.DateTimeFormat("en-GB", clock),
24
+ timestamp: new Intl.DateTimeFormat(undefined, {
25
+ ...clock,
26
+ year: "numeric",
27
+ month: "short",
28
+ day: "numeric",
29
+ timeZoneName: "short",
30
+ }),
31
+ // A log can span both standard time and daylight-saving time.
32
+ zone: new Intl.DateTimeFormat(undefined, {
33
+ timeZone,
34
+ timeZoneName: "shortGeneric",
35
+ }),
36
+ };
37
+ }, [timeZone]);
38
+ const label = local
39
+ ? (formatters.zone
40
+ .formatToParts()
41
+ .find((part) => part.type === "timeZoneName")?.value ?? localTimeZone)
42
+ : "UTC";
43
+
44
+ return {
45
+ local,
46
+ label,
47
+ title: local
48
+ ? `${localTimeZone} · Switch to UTC`
49
+ : `UTC · Switch to local time (${localTimeZone})`,
50
+ toggle() {
51
+ setMode(local ? "utc" : "local");
52
+ },
53
+ formatClock(value: string) {
54
+ return formatters.clock.format(new Date(value));
55
+ },
56
+ formatTimestamp(value: string) {
57
+ return local ? formatters.timestamp.format(new Date(value)) : value;
58
+ },
59
+ };
60
+ }
@@ -663,6 +663,35 @@ export function useConversationData(conversationId: string | undefined) {
663
663
  },
664
664
  );
665
665
  }, [conversationId, detailData, mailbox, outbox.data, queryClient]);
666
+ // The title is generated after the first message, and only the open
667
+ // Conversation polls. Copy a newer title into loaded feeds, such as the
668
+ // sidebar and the home list, so they do not keep the old one.
669
+ const displayTitle = detail.data?.displayTitle;
670
+ const detailUpdatedAt = detail.dataUpdatedAt;
671
+ useEffect(() => {
672
+ if (!conversationId || !displayTitle) return;
673
+ const feeds = queryClient
674
+ .getQueryCache()
675
+ .findAll({ queryKey: ["dashboard", "conversations"] });
676
+ for (const query of feeds) {
677
+ const feed = query.state.data as ConversationFeed | undefined;
678
+ if (!feed || query.state.dataUpdatedAt > detailUpdatedAt) continue;
679
+ const stale = feed.conversations.some(
680
+ (conversation) =>
681
+ conversation.conversationId === conversationId &&
682
+ conversation.displayTitle !== displayTitle,
683
+ );
684
+ if (!stale) continue;
685
+ queryClient.setQueryData<ConversationFeed>(query.queryKey, {
686
+ ...feed,
687
+ conversations: feed.conversations.map((conversation) =>
688
+ conversation.conversationId === conversationId
689
+ ? { ...conversation, displayTitle }
690
+ : conversation,
691
+ ),
692
+ });
693
+ }
694
+ }, [conversationId, detailUpdatedAt, displayTitle, queryClient]);
666
695
  const historyStatus = detailData?.eventHistory.status;
667
696
  const historyQueryKey = useMemo(
668
697
  () => ["conversation", conversationId, "history", historyStatus] as const,
@@ -15,6 +15,7 @@ import type { ConversationTranscript } from "../types";
15
15
  import type { TranscriptViewMode } from "./transcriptRenderModel";
16
16
 
17
17
  const BOTTOM_PROXIMITY_PX = 96;
18
+ const PREVIOUS_PAGE_PROXIMITY_PX = 600;
18
19
  const USER_SCROLL_DELTA_PX = 2;
19
20
 
20
21
  type ScrollRoot = HTMLElement | Window;
@@ -650,6 +651,39 @@ export function usePinnedTranscriptBottom(input: {
650
651
  );
651
652
  }
652
653
 
654
+ /** Load the previous history page when the reader scrolls near the top. */
655
+ export function useAutoLoadPreviousPage(input: {
656
+ enabled: boolean;
657
+ historyVersion: string;
658
+ onLoad: () => void;
659
+ }): RefCallback<HTMLElement> {
660
+ const [sentinel, setSentinel] = useState<HTMLElement | null>(null);
661
+ const onLoadRef = useRef(input.onLoad);
662
+ onLoadRef.current = input.onLoad;
663
+
664
+ useEffect(() => {
665
+ if (!input.enabled || !sentinel) return;
666
+ if (typeof IntersectionObserver === "undefined") return;
667
+
668
+ const root = scrollRootFor(sentinel);
669
+ const observer = new IntersectionObserver(
670
+ (entries) => {
671
+ if (!entries.some((entry) => entry.isIntersecting)) return;
672
+ observer.disconnect();
673
+ onLoadRef.current();
674
+ },
675
+ {
676
+ root: root && !isWindowRoot(root) ? root : null,
677
+ rootMargin: `${PREVIOUS_PAGE_PROXIMITY_PX}px 0px 0px 0px`,
678
+ },
679
+ );
680
+ observer.observe(sentinel);
681
+ return () => observer.disconnect();
682
+ }, [input.enabled, input.historyVersion, sentinel]);
683
+
684
+ return setSentinel;
685
+ }
686
+
653
687
  /** Keep the previously visible content at the same viewport offset after a prepend. */
654
688
  export function scrollTopAfterPrepend(
655
689
  previous: Pick<ScrollSnapshot, "scrollHeight" | "scrollTop">,