@sentry/junior-dashboard 0.264.0 → 0.265.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -5
  3. package/dist/app.js +119 -35
  4. package/dist/client.js +145 -136
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +119 -35
  7. package/dist/tailwind.css +1 -1
  8. package/package.json +5 -4
  9. package/src/api/schema.ts +0 -1
  10. package/src/app.ts +5 -20
  11. package/src/client/App.tsx +29 -31
  12. package/src/client/api.ts +7 -0
  13. package/src/client/components/BottomSheet.tsx +12 -2
  14. package/src/client/components/CopyButton.tsx +83 -0
  15. package/src/client/components/DetailsPanel.tsx +133 -0
  16. package/src/client/components/Drawer.tsx +15 -24
  17. package/src/client/components/FilterBar.tsx +16 -31
  18. package/src/client/components/Metric.tsx +33 -23
  19. package/src/client/components/PageContentSkeleton.tsx +1 -1
  20. package/src/client/components/Pagination.tsx +1 -1
  21. package/src/client/components/ParticipantAvatarStack.tsx +40 -36
  22. package/src/client/components/ProfileMenu.tsx +27 -93
  23. package/src/client/components/README.md +86 -77
  24. package/src/client/components/SegmentedNav.tsx +1 -1
  25. package/src/client/components/ShimmerText.tsx +2 -6
  26. package/src/client/components/ThemeSwitcher.tsx +19 -15
  27. package/src/client/components/charts/ActivityChart.tsx +173 -288
  28. package/src/client/components/charts/CacheWrites.tsx +63 -50
  29. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  30. package/src/client/components/charts/README.md +4 -4
  31. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  32. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  33. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  34. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  35. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  36. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  37. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  38. package/src/client/components/ui/accordion.tsx +77 -0
  39. package/src/client/components/ui/alert-dialog.tsx +89 -0
  40. package/src/client/components/ui/button.tsx +85 -0
  41. package/src/client/components/ui/chart.tsx +74 -0
  42. package/src/client/components/ui/checkbox.tsx +26 -0
  43. package/src/client/components/ui/collapsible.tsx +22 -0
  44. package/src/client/components/ui/field.tsx +49 -0
  45. package/src/client/components/ui/input.tsx +35 -0
  46. package/src/client/components/ui/native-select.tsx +57 -0
  47. package/src/client/components/ui/popover.tsx +67 -0
  48. package/src/client/components/ui/radio-group.tsx +32 -0
  49. package/src/client/components/ui/sheet.tsx +13 -3
  50. package/src/client/components/ui/skeleton.tsx +19 -0
  51. package/src/client/components/ui/table.tsx +80 -0
  52. package/src/client/components/ui/tabs.tsx +75 -0
  53. package/src/client/components/ui/textarea.tsx +17 -0
  54. package/src/client/components/ui/toast.tsx +215 -0
  55. package/src/client/components/ui/toggle-group.tsx +36 -0
  56. package/src/client/components/ui/toggle.tsx +37 -0
  57. package/src/client/components/ui/tooltip.tsx +168 -0
  58. package/src/client/components/ui/trace.tsx +96 -0
  59. package/src/client/conversations/ChatLayout.tsx +7 -3
  60. package/src/client/conversations/ConversationBrief.tsx +17 -6
  61. package/src/client/conversations/ConversationComposer.tsx +26 -12
  62. package/src/client/conversations/ConversationDetails.tsx +147 -62
  63. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  64. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  65. package/src/client/conversations/ConversationHeader.tsx +8 -3
  66. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  67. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  68. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  69. package/src/client/conversations/ConversationMeta.tsx +55 -88
  70. package/src/client/conversations/ConversationPage.tsx +220 -101
  71. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  72. package/src/client/conversations/ConversationWatches.tsx +165 -0
  73. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  74. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  75. package/src/client/conversations/DetailsList.tsx +124 -0
  76. package/src/client/conversations/EventDetails.tsx +42 -8
  77. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  78. package/src/client/conversations/ObjectCard.tsx +20 -8
  79. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  80. package/src/client/conversations/README.md +86 -13
  81. package/src/client/conversations/ShareConversationButton.tsx +25 -13
  82. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  83. package/src/client/conversations/ToolFrame.tsx +36 -52
  84. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  85. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  86. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  87. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  88. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  89. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  90. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  91. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  92. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  93. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  94. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  95. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  96. package/src/client/conversations/TranscriptView.tsx +63 -27
  97. package/src/client/conversations/eventLog.ts +57 -0
  98. package/src/client/conversations/eventTime.ts +60 -0
  99. package/src/client/conversations/queries.ts +29 -0
  100. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  101. package/src/client/format.ts +5 -0
  102. package/src/client/pages/ConnectionsPage.tsx +2 -2
  103. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  104. package/src/client/pages/SettingsPage.tsx +9 -10
  105. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  106. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  107. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  108. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  109. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  110. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  111. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  112. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  113. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  114. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  115. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  116. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  117. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  118. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  119. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  120. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  121. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  122. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  123. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  124. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  125. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  126. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  127. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  128. package/src/client/pages/automations/README.md +57 -0
  129. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  130. package/src/client/pages/automations/automationFormat.ts +37 -23
  131. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  132. package/src/client/pages/code/CodePage.tsx +52 -55
  133. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  134. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  135. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  136. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  137. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  138. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  139. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  140. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  141. package/src/client/pages/memory/memoryRecord.ts +1 -1
  142. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  143. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  144. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  145. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  146. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  147. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  148. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  149. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  150. package/src/client/pages/system/README.md +23 -0
  151. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  152. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  153. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  154. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  155. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  156. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  157. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  158. package/src/client/pages/system/sandboxSize.ts +8 -0
  159. package/src/client/pages/system/workspaceQueries.ts +53 -0
  160. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  161. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  162. package/src/client/styles.ts +4 -0
  163. package/src/client.tsx +7 -1
  164. package/src/mock-reporting/automation-collection.ts +4 -2
  165. package/src/mock-reporting/fixtures.ts +23 -7
  166. package/src/mock-reporting/routes.ts +12 -0
  167. package/src/mock-reporting/watches.ts +94 -0
  168. package/src/shell.ts +4 -9
  169. package/src/tailwind.css +0 -10
  170. package/src/client/components/Button.tsx +0 -118
  171. package/src/client/components/Field.tsx +0 -53
  172. package/src/client/components/SegmentedTabs.tsx +0 -119
  173. package/src/client/components/Select.tsx +0 -52
  174. package/src/client/components/Skeleton.tsx +0 -17
  175. package/src/client/components/TextInput.tsx +0 -58
  176. package/src/client/components/Tooltip.tsx +0 -216
  177. package/src/client/components/charts/useChartLayout.ts +0 -27
  178. package/src/client/components/ui/sheet.LICENSE +0 -21
  179. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  180. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  181. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  182. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  183. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  184. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  185. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  186. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  187. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  188. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  189. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  190. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  191. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -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
- }
@@ -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
+ }
@@ -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
+ }