@sentry/junior-dashboard 0.261.0 → 0.262.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 (29) hide show
  1. package/dist/app.js +2 -2
  2. package/dist/client.js +96 -74
  3. package/dist/index.js +2 -2
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +4 -3
  6. package/src/client/components/BottomSheet.tsx +32 -0
  7. package/src/client/components/Button.tsx +2 -1
  8. package/src/client/components/Drawer.tsx +23 -109
  9. package/src/client/components/README.md +12 -3
  10. package/src/client/components/SegmentedTabs.tsx +23 -4
  11. package/src/client/components/Sheet.LICENSE +21 -0
  12. package/src/client/components/Sheet.tsx +141 -0
  13. package/src/client/conversations/{ConversationDetailsDrawer.tsx → ConversationDetails.tsx} +19 -38
  14. package/src/client/conversations/ConversationDetailsFrame.tsx +91 -0
  15. package/src/client/conversations/ConversationEventLog.tsx +0 -1
  16. package/src/client/conversations/ConversationHeader.tsx +20 -28
  17. package/src/client/conversations/ConversationHeaderActions.tsx +5 -1
  18. package/src/client/conversations/ConversationPage.tsx +49 -36
  19. package/src/client/conversations/ForkConversationButton.tsx +0 -1
  20. package/src/client/conversations/README.md +18 -2
  21. package/src/client/conversations/SubagentTranscriptDrawer.tsx +0 -1
  22. package/src/client/conversations/TelemetryMetrics.tsx +2 -2
  23. package/src/client/conversations/TranscriptSubagentView.tsx +1 -1
  24. package/src/client/conversations/TranscriptTurnContextView.tsx +0 -1
  25. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -1
  26. package/src/client/pages/dev/ComponentsPage.tsx +16 -2
  27. package/src/client/pages/dev/SegmentedFixtures.tsx +8 -7
  28. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -1
  29. package/LICENSE +0 -201
@@ -22,37 +22,33 @@ import {
22
22
  TranscriptViewToggle,
23
23
  type ConversationArchiveAction,
24
24
  } from "./ConversationHeaderActions";
25
- import { ConversationDetailsDrawer } from "./ConversationDetailsDrawer";
26
25
  import type { TranscriptViewMode } from "./transcriptRenderModel";
27
- import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
28
26
 
29
27
  /** Show the conversation title, search, and details. */
30
28
  export function ConversationHeader(props: {
31
29
  /** Parent chain of a child Conversation. Root Conversations omit it. */
32
30
  agentTree?: ReactNode;
33
31
  copyAction?: ReactNode;
34
- annotations: ReactNode;
35
32
  archive: ConversationArchiveAction;
36
- brief?: ReactNode;
37
33
  conversationId: string;
34
+ detailsOpen: boolean;
35
+ onDetailsClick(): void;
38
36
  /** Links to the fork source and to forks of this conversation. */
39
37
  forks?: ReactNode;
40
- identity: ReactNode;
41
38
  linkedWork?: ReactNode;
42
- lastActivityAt?: string;
43
- sentryConversationUrl?: string;
44
39
  live: boolean;
45
40
  meta?: ReactNode;
46
- onOpenSubagent(target: SubagentTranscriptTarget): void;
47
41
  onSearchChange(value: string): void;
48
42
  onViewChange(value: TranscriptViewMode): void;
49
43
  privacy: ReactNode;
50
44
  search: string;
51
- stats: ReactNode;
52
45
  title: string;
53
46
  view: TranscriptViewMode;
54
47
  }) {
55
- const [detailsOpen, setDetailsOpen] = useState(false);
48
+ const detailsOpen = props.detailsOpen;
49
+ const detailsButtonRef = useRef<HTMLButtonElement>(null);
50
+ const menuButtonRef = useRef<HTMLButtonElement>(null);
51
+ const wasDetailsOpen = useRef(detailsOpen);
56
52
  const [searchOpen, setSearchOpen] = useState(false);
57
53
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
58
54
  const searchInputRef = useRef<HTMLInputElement>(null);
@@ -60,6 +56,16 @@ export function ConversationHeader(props: {
60
56
  const searchOpenVisible = searchOpen || props.search.length > 0;
61
57
  const openMobileNavigation = useOpenMobileNavigation();
62
58
 
59
+ useEffect(() => {
60
+ if (wasDetailsOpen.current && !detailsOpen) {
61
+ const trigger = window.matchMedia("(max-width: 767px)").matches
62
+ ? menuButtonRef.current
63
+ : detailsButtonRef.current;
64
+ trigger?.focus({ preventScroll: true });
65
+ }
66
+ wasDetailsOpen.current = detailsOpen;
67
+ }, [detailsOpen]);
68
+
63
69
  useEffect(() => {
64
70
  if (!searchOpenVisible) return;
65
71
  searchInputRef.current?.focus();
@@ -107,6 +113,7 @@ export function ConversationHeader(props: {
107
113
  const mobileOverflow = (
108
114
  <div className="relative" ref={mobileMenuRef}>
109
115
  <HeaderIconButton
116
+ ref={menuButtonRef}
110
117
  label={mobileMenuOpen ? "Close conversation menu" : "Conversation menu"}
111
118
  onClick={() => setMobileMenuOpen((open) => !open)}
112
119
  pressed={mobileMenuOpen}
@@ -123,7 +130,7 @@ export function ConversationHeader(props: {
123
130
  layout="menu"
124
131
  onDetailsClick={() => {
125
132
  setMobileMenuOpen(false);
126
- setDetailsOpen(true);
133
+ props.onDetailsClick();
127
134
  }}
128
135
  onSearchClick={() => {
129
136
  setMobileMenuOpen(false);
@@ -205,7 +212,8 @@ export function ConversationHeader(props: {
205
212
  conversationId={props.conversationId}
206
213
  copyAction={props.copyAction}
207
214
  detailsOpen={detailsOpen}
208
- onDetailsClick={() => setDetailsOpen(true)}
215
+ detailsButtonRef={detailsButtonRef}
216
+ onDetailsClick={props.onDetailsClick}
209
217
  onSearchClick={toggleSearch}
210
218
  onViewChange={props.onViewChange}
211
219
  searchOpen={searchOpenVisible}
@@ -252,22 +260,6 @@ export function ConversationHeader(props: {
252
260
  </div>
253
261
  ) : null}
254
262
  </header>
255
-
256
- {detailsOpen ? (
257
- <ConversationDetailsDrawer
258
- annotations={props.annotations}
259
- brief={props.brief}
260
- conversationId={props.conversationId}
261
- identity={props.identity}
262
- lastActivityAt={props.lastActivityAt}
263
- sentryConversationUrl={props.sentryConversationUrl}
264
- onClose={() => setDetailsOpen(false)}
265
- onOpenSubagent={props.onOpenSubagent}
266
- privacy={props.privacy}
267
- stats={props.stats}
268
- title={props.title}
269
- />
270
- ) : null}
271
263
  </>
272
264
  );
273
265
  }
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import type { ReactNode, Ref } from "react";
2
2
  import {
3
3
  Archive,
4
4
  ArchiveRestore,
@@ -30,6 +30,7 @@ export function ConversationHeaderActions(props: {
30
30
  conversationId: string;
31
31
  copyAction?: ReactNode;
32
32
  detailsOpen: boolean;
33
+ detailsButtonRef?: Ref<HTMLButtonElement>;
33
34
  /** `bar` is the desktop icon row. `menu` stacks actions for mobile overflow. */
34
35
  layout?: "bar" | "menu";
35
36
  onDetailsClick(): void;
@@ -94,6 +95,7 @@ export function ConversationHeaderActions(props: {
94
95
  {props.copyAction}
95
96
  <ArchiveConversationButton {...props.archive} layout="bar" />
96
97
  <Button
98
+ ref={props.detailsButtonRef}
97
99
  aria-label="Conversation details"
98
100
  aria-pressed={props.detailsOpen}
99
101
  className="ml-2"
@@ -180,6 +182,7 @@ function ViewModeButton(props: {
180
182
  /** Shared icon control used by conversation and subagent transcript headers. */
181
183
  export function HeaderIconButton(props: {
182
184
  children: ReactNode;
185
+ ref?: Ref<HTMLButtonElement>;
183
186
  label: string;
184
187
  onClick(): void;
185
188
  pressed?: boolean;
@@ -187,6 +190,7 @@ export function HeaderIconButton(props: {
187
190
  return (
188
191
  <IconButtonTooltip label={props.label}>
189
192
  <Button
193
+ ref={props.ref}
190
194
  aria-label={props.label}
191
195
  aria-pressed={props.pressed}
192
196
  className={cn(
@@ -26,6 +26,8 @@ import {
26
26
  } from "./conversationRoutes";
27
27
  import { ConversationComposer } from "./ConversationComposer";
28
28
  import { ConversationHeader } from "./ConversationHeader";
29
+ import { ConversationDetails } from "./ConversationDetails";
30
+ import { cn } from "../styles";
29
31
  import { ConversationForkLinks } from "./ConversationForkLinks";
30
32
  import { ConversationAgentTree } from "./ConversationAgentTree";
31
33
  import { ConversationHeaderMeta } from "./ConversationHeaderMeta";
@@ -76,6 +78,7 @@ export function ConversationPage(props: {
76
78
  onRead?(conversationId: string, lastReadAt: string): void;
77
79
  pendingArchiveUpdate?: PendingArchiveConversationUpdate;
78
80
  }) {
81
+ const [detailsOpen, setDetailsOpen] = useState(false);
79
82
  const [subagentTarget, setSubagentTarget] =
80
83
  useState<SubagentTranscriptTarget>();
81
84
  const [searchParams, setSearchParams] = useSearchParams();
@@ -196,7 +199,12 @@ export function ConversationPage(props: {
196
199
  );
197
200
 
198
201
  return (
199
- <>
202
+ <div
203
+ className={cn(
204
+ "grid h-full min-h-0 min-w-0",
205
+ detailsOpen && "xl:grid-cols-[minmax(0,1fr)_25rem]",
206
+ )}
207
+ >
200
208
  <ChatLayout
201
209
  scrollMinTall
202
210
  scrollAriaLabel="Conversation transcript"
@@ -208,6 +216,8 @@ export function ConversationPage(props: {
208
216
  agentTree ? <ConversationAgentTree nodes={agentTree} /> : null
209
217
  }
210
218
  conversationId={conversationId}
219
+ detailsOpen={detailsOpen}
220
+ onDetailsClick={() => setDetailsOpen((open) => !open)}
211
221
  forks={
212
222
  detail.data?.forkedFromConversationId ||
213
223
  detail.data?.forks?.length ? (
@@ -220,8 +230,6 @@ export function ConversationPage(props: {
220
230
  />
221
231
  ) : null
222
232
  }
223
- lastActivityAt={conversation?.lastSeenAt}
224
- sentryConversationUrl={detail.data?.sentryConversationUrl}
225
233
  copyAction={
226
234
  <CopyMarkdownButton
227
235
  key={conversationDetail?.conversationId ?? "loading"}
@@ -236,11 +244,6 @@ export function ConversationPage(props: {
236
244
  }
237
245
  />
238
246
  }
239
- annotations={
240
- hasConversationAnnotations(detail.data?.annotations) ? (
241
- <ConversationAnnotations detail={detail.data} />
242
- ) : null
243
- }
244
247
  linkedWork={
245
248
  hasConversationAnnotations(detail.data?.annotations) ? (
246
249
  <ConversationAnnotations
@@ -260,15 +263,6 @@ export function ConversationPage(props: {
260
263
  }),
261
264
  pending: archive.isPending,
262
265
  }}
263
- brief={
264
- detail.data?.brief ? (
265
- <ConversationBrief
266
- brief={detail.data.brief}
267
- variant="summary"
268
- />
269
- ) : null
270
- }
271
- identity={identity}
272
266
  live={live}
273
267
  meta={
274
268
  <ConversationHeaderMeta
@@ -288,7 +282,6 @@ export function ConversationPage(props: {
288
282
  }
289
283
  />
290
284
  }
291
- onOpenSubagent={setSubagentTarget}
292
285
  onSearchChange={setSearch}
293
286
  onViewChange={onViewChange}
294
287
  privacy={
@@ -297,23 +290,6 @@ export function ConversationPage(props: {
297
290
  />
298
291
  }
299
292
  search={search}
300
- stats={
301
- hasConversationStats({
302
- conversation,
303
- detail: detail.data,
304
- }) ? (
305
- <div className="grid min-w-0 gap-5">
306
- <ConversationStats
307
- conversation={conversation}
308
- detail={detail.data}
309
- />
310
- <ConversationUsageBreakdown
311
- conversation={conversation}
312
- detail={detail.data}
313
- />
314
- </div>
315
- ) : null
316
- }
317
293
  title={title}
318
294
  view={view}
319
295
  />
@@ -382,11 +358,48 @@ export function ConversationPage(props: {
382
358
  ) : undefined
383
359
  }
384
360
  />
361
+ {detailsOpen ? (
362
+ <ConversationDetails
363
+ conversationId={conversationId}
364
+ identity={identity}
365
+ lastActivityAt={conversation?.lastSeenAt}
366
+ sentryConversationUrl={detail.data?.sentryConversationUrl}
367
+ onClose={() => setDetailsOpen(false)}
368
+ onOpenSubagent={setSubagentTarget}
369
+ annotations={
370
+ hasConversationAnnotations(detail.data?.annotations) ? (
371
+ <ConversationAnnotations detail={detail.data} />
372
+ ) : null
373
+ }
374
+ brief={
375
+ detail.data?.brief ? (
376
+ <ConversationBrief brief={detail.data.brief} variant="summary" />
377
+ ) : null
378
+ }
379
+ stats={
380
+ hasConversationStats({
381
+ conversation,
382
+ detail: detail.data,
383
+ }) ? (
384
+ <div className="grid min-w-0 gap-5">
385
+ <ConversationStats
386
+ conversation={conversation}
387
+ detail={detail.data}
388
+ />
389
+ <ConversationUsageBreakdown
390
+ conversation={conversation}
391
+ detail={detail.data}
392
+ />
393
+ </div>
394
+ ) : null
395
+ }
396
+ />
397
+ ) : null}
385
398
  <SubagentTranscriptDrawer
386
399
  onClose={() => setSubagentTarget(undefined)}
387
400
  target={subagentTarget}
388
401
  />
389
- </>
402
+ </div>
390
403
  );
391
404
  }
392
405
 
@@ -60,7 +60,6 @@ function ForkConversationDialog(props: ForkTarget & { onClose(): void }) {
60
60
  return (
61
61
  <Drawer
62
62
  closeLabel="Close fork dialog"
63
- dismissLabel="Dismiss fork dialog"
64
63
  header={
65
64
  <h2 id={titleId} className="m-0 text-lg font-semibold">
66
65
  Fork Conversation
@@ -40,10 +40,26 @@ shared controls and overlays.
40
40
  Automation. Render this text with `TranscriptText` or `MentionText`. Both
41
41
  require `mentions`, so a caller cannot forget them. Do not resolve names in
42
42
  the dashboard, and do not rewrite tokens into links on the server.
43
- - `TranscriptTurnContextView` owns context content. The shared `Drawer` owns
44
- dismissal, focus trapping, focus return, scroll locking, and panel layout.
43
+ - `TranscriptTurnContextView` owns context content. The shared `Drawer` adds
44
+ its header and layout to the Base UI `Sheet` focus and dismissal boundary.
45
45
 
46
46
  Review these components together in `/dev/transcripts`. Use the conversation
47
47
  fixture for messages, events, activity, context, and typing. Use the attachment
48
48
  fixture for file wrapping. Keep visual checks out of automated assertions;
49
49
  browser tests cover opening, closing, and keyboard interaction.
50
+
51
+ ## Conversation details
52
+
53
+ `ConversationPage` owns whether details are open. The page is keyed by the
54
+ Conversation, so switching Conversations closes details. `ConversationHeader`
55
+ only toggles details and restores focus through explicit control refs after
56
+ dismissal.
57
+
58
+ `ConversationDetails` owns one set of tabs and content. `ConversationDetailsFrame`
59
+ uses a 400px aside at 1280px and above. The chat and aside scroll separately.
60
+ Smaller windows use `BottomSheet` with a plain accessible label. The sheet and
61
+ side drawers share the Base UI `Sheet` focus rules and the app's document
62
+ scroll lock. A resize changes the frame and keeps the selected tab. Tabs stay fixed above the scrolling content in both
63
+ layouts. Only the swipe handle sits above the sheet tabs. Swipe down, tap the
64
+ backdrop, or press Escape to dismiss it. Base UI owns the transition lifecycle.
65
+ A pending exit cannot close a new panel or a different Conversation.
@@ -103,7 +103,6 @@ function SubagentTranscriptDrawerContent(props: {
103
103
  </>
104
104
  }
105
105
  closeLabel="Close subagent transcript"
106
- dismissLabel="Dismiss subagent transcript"
107
106
  header={
108
107
  <>
109
108
  <div className="flex min-w-0 items-center gap-2">
@@ -354,7 +354,7 @@ export function CostMetric(props: {
354
354
  align?: "left" | "right";
355
355
  auxiliaryCosts?: ConversationAuxiliaryCosts;
356
356
  childAgentTotals?: ConversationChildAgentTotals;
357
- /** Point to the Details drawer, which has the full breakdown. */
357
+ /** Point to conversation details, which has the full breakdown. */
358
358
  detailsHint?: boolean;
359
359
  live?: boolean;
360
360
  liveModelId?: string;
@@ -398,7 +398,7 @@ export function TokenMetric(props: {
398
398
  align?: "left" | "right";
399
399
  childAgentTotals?: ConversationChildAgentTotals;
400
400
  compactionCount?: number;
401
- /** Point to the Details drawer, which has the full breakdown. */
401
+ /** Point to conversation details, which has the full breakdown. */
402
402
  detailsHint?: boolean;
403
403
  live?: boolean;
404
404
  liveModelId?: string;
@@ -9,7 +9,7 @@ import { HighlightText } from "./transcriptSearch";
9
9
 
10
10
  /**
11
11
  * Render a child agent as a compact card in the transcript stream. The card
12
- * matches the child agent rows in the details drawer: name, status, and run
12
+ * matches the child agent rows in conversation details: name, status, and run
13
13
  * time first, then the task title. It opens the child transcript.
14
14
  */
15
15
  export function TranscriptSubagentView(props: {
@@ -38,7 +38,6 @@ export function TranscriptTurnContextView(props: {
38
38
  ? createPortal(
39
39
  <Drawer
40
40
  closeLabel="Close turn context"
41
- dismissLabel="Dismiss turn context"
42
41
  header={
43
42
  <>
44
43
  <h2
@@ -65,7 +65,6 @@ export function AutomationDetailsDrawer(props: {
65
65
  return (
66
66
  <Drawer
67
67
  closeLabel="Close automation details"
68
- dismissLabel="Dismiss automation details"
69
68
  header={
70
69
  <>
71
70
  <h2
@@ -37,6 +37,7 @@ import {
37
37
  } from "../../components/controls/TimeRangeSelector";
38
38
  import { EmptyTelemetry } from "../../components/EmptyTelemetry";
39
39
  import { Drawer } from "../../components/Drawer";
40
+ import { BottomSheet } from "../../components/BottomSheet";
40
41
  import { Field } from "../../components/Field";
41
42
  import { Card } from "../../components/layout/Card";
42
43
  import { DashboardChromeProvider } from "../../components/layout/DashboardChrome";
@@ -193,6 +194,7 @@ function FoundationsGalleryPage() {
193
194
  const [pressed, setPressed] = useState(true);
194
195
  const [navigationOpen, setNavigationOpen] = useState(false);
195
196
  const [drawerOpen, setDrawerOpen] = useState(false);
197
+ const [sheetOpen, setSheetOpen] = useState(false);
196
198
  const [nestedDrawerOpen, setNestedDrawerOpen] = useState(false);
197
199
  const [searchQuery, setSearchQuery] = useState("deploy failure");
198
200
 
@@ -257,7 +259,6 @@ function FoundationsGalleryPage() {
257
259
  {drawerOpen ? (
258
260
  <Drawer
259
261
  closeLabel="Close gallery details"
260
- dismissLabel="Dismiss gallery details"
261
262
  header={
262
263
  <h2
263
264
  id="gallery-drawer-title"
@@ -281,7 +282,6 @@ function FoundationsGalleryPage() {
281
282
  ? createPortal(
282
283
  <Drawer
283
284
  closeLabel="Close nested details"
284
- dismissLabel="Dismiss nested details"
285
285
  header={
286
286
  <h2
287
287
  id="gallery-nested-title"
@@ -305,6 +305,20 @@ function FoundationsGalleryPage() {
305
305
  </Drawer>
306
306
  ) : null}
307
307
  </Fixture>
308
+ <Fixture title="Bottom sheet">
309
+ <Button onClick={() => setSheetOpen(true)}>Open bottom sheet</Button>
310
+ {sheetOpen ? (
311
+ <BottomSheet
312
+ label="Gallery details"
313
+ onClose={() => setSheetOpen(false)}
314
+ >
315
+ <p className="m-0 p-4 pb-[max(1rem,env(safe-area-inset-bottom))] text-sm text-dashboard-text-muted">
316
+ Swipe the handle down, tap outside, or press Escape to return to
317
+ the page.
318
+ </p>
319
+ </BottomSheet>
320
+ ) : null}
321
+ </Fixture>
308
322
  <Fixture title="Theme">
309
323
  <ThemeSwitcher className="max-w-64" />
310
324
  </Fixture>
@@ -5,25 +5,26 @@ import { SegmentedTabs } from "../../components/SegmentedTabs";
5
5
 
6
6
  /** Show panel tabs switching the panel below them. */
7
7
  export function SegmentedTabsFixture() {
8
- const [tab, setTab] = useState<"details" | "memories" | "usage">("details");
8
+ const [tab, setTab] = useState<"summary" | "memories" | "agents">("summary");
9
9
  return (
10
- <div className="max-w-md">
10
+ <div className="h-64 max-w-xs">
11
11
  <SegmentedTabs
12
+ scroll
12
13
  items={[
13
- { label: "Details", value: "details" },
14
+ { label: "Summary", value: "summary" },
14
15
  { count: 3, label: "Memories", value: "memories" },
15
- { label: "Usage", value: "usage" },
16
+ { count: 2, label: "Subagents", value: "agents" },
16
17
  ]}
17
18
  label="Conversation panels"
18
19
  onChange={setTab}
19
20
  value={tab}
20
21
  >
21
22
  <p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
22
- {tab === "details"
23
- ? "Conversation summary and linked work."
23
+ {tab === "summary"
24
+ ? "Checkout latency increased after the latest release. Compare traces from before and after the deployment, then inspect the slow serialization span. The public brief links the affected repository, the release, and the open code change. Keep the rollout paused until the comparison confirms the cause. The next check is to run the same request against the previous release and compare its timings. Use the linked work to record the result and any rollback decision. The participants can continue this investigation in the conversation while keeping these details open. Model usage and related child conversations remain available in their own sections."
24
25
  : tab === "memories"
25
26
  ? "What Junior learned from this conversation."
26
- : "Time, tokens, and cost for this conversation."}
27
+ : "Child conversations and their current work."}
27
28
  </p>
28
29
  </SegmentedTabs>
29
30
  </div>
@@ -67,7 +67,6 @@ export function MemoryDetailsDrawer(props: {
67
67
  return (
68
68
  <Drawer
69
69
  closeLabel="Close memory details"
70
- dismissLabel="Dismiss memory details"
71
70
  header={
72
71
  <>
73
72
  <h2