@sentry/junior-dashboard 0.277.0 → 0.278.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 (45) hide show
  1. package/dist/app.js +30 -41
  2. package/dist/client.js +90 -90
  3. package/dist/index.js +30 -41
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +5 -4
  6. package/src/app.ts +1 -1
  7. package/src/client/App.tsx +9 -4
  8. package/src/client/components/DetailsPanel.tsx +1 -1
  9. package/src/client/components/ui/resizable.tsx +50 -0
  10. package/src/client/conversations/AutoArchiveToggle.tsx +50 -14
  11. package/src/client/conversations/ConversationHeader.tsx +6 -2
  12. package/src/client/conversations/ConversationHeaderActions.tsx +4 -0
  13. package/src/client/conversations/ConversationMeta.tsx +2 -7
  14. package/src/client/conversations/ConversationPage.tsx +155 -72
  15. package/src/client/conversations/ConversationSidebar.tsx +2 -1
  16. package/src/client/conversations/ConversationTranscript.tsx +11 -8
  17. package/src/client/conversations/ConversationWorkspace.tsx +129 -41
  18. package/src/client/conversations/ConversationWorkspaceLoading.tsx +17 -3
  19. package/src/client/conversations/EventDetails.tsx +2 -62
  20. package/src/client/conversations/TranscriptActivityGroup.tsx +36 -14
  21. package/src/client/conversations/TranscriptContextEventView.tsx +7 -18
  22. package/src/client/conversations/TranscriptRailEvent.tsx +0 -8
  23. package/src/client/conversations/TranscriptView.tsx +89 -20
  24. package/src/client/conversations/conversationSidebarLayout.ts +61 -0
  25. package/src/client/conversations/eventLog.ts +2 -31
  26. package/src/client/conversations/eventTranscript.ts +9 -42
  27. package/src/client/conversations/queries.ts +76 -15
  28. package/src/client/conversations/transcript.ts +46 -16
  29. package/src/client/conversations/transcriptBottomPinning.ts +40 -24
  30. package/src/client/conversations/transcriptReadPosition.ts +31 -7
  31. package/src/client/conversations/transcriptRenderModel.ts +1 -14
  32. package/src/client/conversations/transcriptSearch.tsx +3 -11
  33. package/src/client/conversations/transcriptUnreadHistory.ts +248 -0
  34. package/src/client/http.ts +5 -5
  35. package/src/client/markdownExport.ts +32 -42
  36. package/src/client/pages/memory/MemoryDetails.tsx +6 -33
  37. package/src/client/pages/memory/MemoryPage.tsx +7 -92
  38. package/src/client/pages/memory/MemoryTimeline.tsx +16 -32
  39. package/src/client/pages/memory/memoryDashboard.ts +2 -38
  40. package/src/client/pages/memory/memoryRecord.ts +1 -6
  41. package/src/client/types.ts +18 -33
  42. package/src/conversation-cache.ts +6 -8
  43. package/src/mock-reporting/fixtures.ts +23 -33
  44. package/src/mock-reporting/routes.ts +3 -1
  45. package/src/tailwind.css +30 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-dashboard",
3
- "version": "0.277.0",
3
+ "version": "0.278.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -34,16 +34,17 @@
34
34
  "react-dom": "^19.2.6",
35
35
  "react-is": "^19.2.6",
36
36
  "react-markdown": "^10.1.0",
37
+ "react-resizable-panels": "^4.17.0",
37
38
  "react-router": "^7.18.2",
38
39
  "recharts": "3.8.0",
39
40
  "remark-breaks": "^4.0.0",
40
41
  "remark-gfm": "^4.0.1",
41
42
  "shiki": "4.1.0",
42
43
  "zod": "^4.5.4",
43
- "@sentry/junior-plugin-api": "0.277.0"
44
+ "@sentry/junior-plugin-api": "0.278.0"
44
45
  },
45
46
  "peerDependencies": {
46
- "@sentry/junior": "0.277.0"
47
+ "@sentry/junior": "0.278.0"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@oxlint/plugins": "1.79.0",
@@ -64,7 +65,7 @@
64
65
  "tw-animate-css": "^1.4.0",
65
66
  "typescript": "^7.0.2",
66
67
  "vitest": "^4.1.12",
67
- "@sentry/junior": "0.277.0"
68
+ "@sentry/junior": "0.278.0"
68
69
  },
69
70
  "scripts": {
70
71
  "build": "pnpm --filter @sentry/junior-plugin-api build && pnpm --filter @sentry/junior build && pnpm run build:client && pnpm run build:css && pnpm run build:server",
package/src/app.ts CHANGED
@@ -537,7 +537,7 @@ export function createDashboardApp(
537
537
  };
538
538
 
539
539
  app.use("*", requireAuth);
540
- // Core detail reads set a version ETag before they build the report. Mock
540
+ // Core detail reads set a version token before they build the report. Mock
541
541
  // reporting has no stored version, so it revalidates on content instead.
542
542
  if (options.mockConversations) {
543
543
  app.use("/api/conversations/:conversationId", revalidateConversation);
@@ -34,6 +34,7 @@ import { useDashboardServerVersion } from "./dashboard-version";
34
34
  import { ConversationWorkspace } from "./conversations/ConversationWorkspace";
35
35
  import { ConversationWorkspaceLoading } from "./conversations/ConversationWorkspaceLoading";
36
36
  import { useConversationData } from "./conversations/queries";
37
+ import { readConversationStartPosition } from "./conversations/transcriptReadPosition";
37
38
  import { codeRepositoryTabs } from "./pages/code/codeRepositoryRoutes";
38
39
  import { RepositoryPage } from "./pages/code/RepositoryPage";
39
40
  import { CodePage } from "./pages/code/CodePage";
@@ -154,8 +155,12 @@ export function DashboardShell() {
154
155
  location.pathname.startsWith("/conversations/");
155
156
  const conversationId = conversationIdFromPath(location.pathname);
156
157
  const conversationsQuery = useConversationsData();
157
- // Detail query shares the page cache so titles outside the top-50 feed stay accurate.
158
- const conversationDetail = useConversationData(conversationId);
158
+ // The shell owns the open Conversation's data, and the page reads it. On a
159
+ // direct load, this fetch runs beside the dashboard core reads, before the
160
+ // page mounts. One fetch for each visit also brings the unread page.
161
+ const conversationDetail = useConversationData(conversationId, {
162
+ unreadPageAtOpen: readConversationStartPosition() === "unread",
163
+ });
159
164
  const mobileConversation = useMemo(() => {
160
165
  if (!conversationId) return undefined;
161
166
  return buildConversations(
@@ -439,7 +444,7 @@ export function DashboardShell() {
439
444
  loading ? (
440
445
  <ConversationWorkspaceLoading detail={false} />
441
446
  ) : data ? (
442
- <ConversationWorkspace />
447
+ <ConversationWorkspace detail={conversationDetail} />
443
448
  ) : (
444
449
  <LoadingView
445
450
  label={query.error?.message ?? "Dashboard unavailable"}
@@ -481,7 +486,7 @@ export function DashboardShell() {
481
486
  loading ? (
482
487
  <ConversationWorkspaceLoading detail />
483
488
  ) : data ? (
484
- <ConversationWorkspace />
489
+ <ConversationWorkspace detail={conversationDetail} />
485
490
  ) : (
486
491
  <LoadingView
487
492
  label={query.error?.message ?? "Dashboard unavailable"}
@@ -124,7 +124,7 @@ export function DetailsPanel(props: {
124
124
  </Button>
125
125
  </div>
126
126
  </header>
127
- <div className="min-h-0 overflow-y-auto overscroll-contain">
127
+ <div className="thin-scrollbar min-h-0 overflow-y-auto overscroll-contain">
128
128
  {props.children}
129
129
  </div>
130
130
  </aside>
@@ -0,0 +1,50 @@
1
+ import * as ResizablePrimitive from "react-resizable-panels";
2
+ import { cn } from "@dashboard/lib/utils";
3
+
4
+ /** Lay out resizable panels in one row or column. */
5
+ function ResizablePanelGroup({
6
+ className,
7
+ ...props
8
+ }: ResizablePrimitive.GroupProps) {
9
+ return (
10
+ <ResizablePrimitive.Group
11
+ data-slot="resizable-panel-group"
12
+ className={cn(
13
+ "flex h-full w-full aria-[orientation=vertical]:flex-col",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ /** Hold one resizable region inside a panel group. */
22
+ function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
23
+ return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
24
+ }
25
+
26
+ /** Drag or use arrow keys to resize the panels on each side. */
27
+ function ResizableHandle({
28
+ withHandle,
29
+ className,
30
+ ...props
31
+ }: ResizablePrimitive.SeparatorProps & {
32
+ withHandle?: boolean;
33
+ }) {
34
+ return (
35
+ <ResizablePrimitive.Separator
36
+ data-slot="resizable-handle"
37
+ className={cn(
38
+ "relative flex w-px items-center justify-center bg-border ring-offset-background after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
39
+ className,
40
+ )}
41
+ {...props}
42
+ >
43
+ {withHandle && (
44
+ <div className="z-10 flex h-6 w-1 shrink-0 rounded-lg bg-border" />
45
+ )}
46
+ </ResizablePrimitive.Separator>
47
+ );
48
+ }
49
+
50
+ export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
@@ -1,32 +1,68 @@
1
+ import { useId } from "react";
2
+ import { CircleHelp } from "lucide-react";
3
+
1
4
  import { Checkbox } from "../components/ui/checkbox";
2
5
  import { InlineError } from "../components/InlineError";
6
+ import {
7
+ Popover,
8
+ PopoverContent,
9
+ PopoverDescription,
10
+ PopoverTrigger,
11
+ } from "../components/ui/popover";
3
12
  import { useSetConversationAutoArchive } from "./autoArchiveQueries";
4
13
 
14
+ const AUTO_ARCHIVE_HELP =
15
+ "Archive this conversation for you when its linked work is merged or closed and nobody replies during your wait time.";
16
+
5
17
  /** Let a participant turn auto-archive on or off for this conversation. */
6
18
  export function AutoArchiveToggle(props: {
7
19
  autoArchive: boolean;
8
20
  conversationId: string;
9
21
  }) {
10
22
  const update = useSetConversationAutoArchive(props.conversationId);
23
+ const helpId = useId();
11
24
  return (
12
25
  <div className="grid gap-1">
13
- <label className="flex cursor-pointer items-start gap-2.5">
14
- <Checkbox
15
- checked={props.autoArchive}
16
- className="mt-0.5"
17
- disabled={update.isPending}
18
- onCheckedChange={(checked) => update.mutate(checked)}
19
- />
20
- <span className="grid gap-0.5">
26
+ <div className="flex items-center gap-1.5">
27
+ <label className="flex cursor-pointer items-center gap-2.5">
28
+ <Checkbox
29
+ aria-describedby={helpId}
30
+ checked={props.autoArchive}
31
+ disabled={update.isPending}
32
+ onCheckedChange={(checked) => update.mutate(checked)}
33
+ />
21
34
  <span className="font-semibold text-dashboard-text">
22
- Auto-archive when done
23
- </span>
24
- <span>
25
- Archive this conversation for you when its linked work is merged or
26
- closed and nobody replies during your wait time.
35
+ Auto archive
27
36
  </span>
37
+ </label>
38
+ {/* A popover, not a tooltip: touch screens open it with a tap. */}
39
+ <Popover>
40
+ <PopoverTrigger
41
+ delay={0}
42
+ openOnHover
43
+ render={
44
+ <button
45
+ aria-label="About auto archive"
46
+ className="grid place-items-center rounded-sm text-dashboard-text-muted hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus"
47
+ type="button"
48
+ >
49
+ <CircleHelp aria-hidden="true" size={14} strokeWidth={2} />
50
+ </button>
51
+ }
52
+ />
53
+ <PopoverContent
54
+ aria-label="About auto archive"
55
+ className="w-64 px-3 py-2 text-xs"
56
+ side="top"
57
+ >
58
+ <PopoverDescription>{AUTO_ARCHIVE_HELP}</PopoverDescription>
59
+ </PopoverContent>
60
+ </Popover>
61
+ {/* Keep the help text for screen readers when the popover is closed. */}
62
+ <span className="sr-only" id={helpId}>
63
+ {AUTO_ARCHIVE_HELP}
28
64
  </span>
29
- </label>
65
+ </div>
30
66
  {update.isError ? (
31
67
  <InlineError>Could not save auto-archive. Try again.</InlineError>
32
68
  ) : null}
@@ -49,6 +49,7 @@ export function ConversationHeader(props: {
49
49
  privacy: ReactNode;
50
50
  search: string;
51
51
  sentryConversationUrl?: string;
52
+ sidebarToggle?: ReactNode;
52
53
  title: string;
53
54
  view: TranscriptViewMode;
54
55
  }) {
@@ -165,8 +166,11 @@ export function ConversationHeader(props: {
165
166
  !showMobileHeader && "hidden md:block",
166
167
  )}
167
168
  >
168
- <div className="hidden h-12 min-w-0 items-center justify-between gap-4 pl-7 pr-4 md:flex">
169
+ <div className="hidden h-13 min-w-0 items-center justify-between gap-4 px-4 md:flex">
169
170
  <div className="flex min-w-0 items-center gap-2">
171
+ {props.sidebarToggle ? (
172
+ <div className="shrink-0">{props.sidebarToggle}</div>
173
+ ) : null}
170
174
  {props.agentTree ? (
171
175
  <div className="min-w-0 shrink-[2]">{props.agentTree}</div>
172
176
  ) : props.location ? (
@@ -220,9 +224,9 @@ export function ConversationHeader(props: {
220
224
  </ConversationOverflowMenu>
221
225
  <HeaderIconButton
222
226
  ref={detailsButtonRef}
227
+ expanded={detailsOpen}
223
228
  label="Conversation details"
224
229
  onClick={props.onDetailsClick}
225
- pressed={detailsOpen}
226
230
  >
227
231
  <PanelRight aria-hidden="true" size={15} strokeWidth={2} />
228
232
  </HeaderIconButton>
@@ -179,6 +179,8 @@ function ViewModeItem(props: {
179
179
  /** Shared icon control used by conversation and subagent transcript headers. */
180
180
  export function HeaderIconButton(props: {
181
181
  children: ReactNode;
182
+ controls?: string;
183
+ expanded?: boolean;
182
184
  ref?: Ref<HTMLButtonElement>;
183
185
  label: string;
184
186
  onClick(): void;
@@ -192,6 +194,8 @@ export function HeaderIconButton(props: {
192
194
  render={
193
195
  <Button
194
196
  ref={props.ref}
197
+ aria-controls={props.controls}
198
+ aria-expanded={props.expanded}
195
199
  aria-label={props.label}
196
200
  aria-pressed={props.pressed}
197
201
  className={cn(
@@ -469,7 +469,7 @@ function SourceTask(props: {
469
469
  );
470
470
  }
471
471
 
472
- /** Resolve the model still running an open turn, including mid-turn handoffs. */
472
+ /** Resolve the model that an open turn last called. */
473
473
  export function liveModelId(
474
474
  detail: ConversationDetailReport | undefined,
475
475
  ): string | undefined {
@@ -484,12 +484,7 @@ export function liveModelId(
484
484
  if (openTurns.size === 0) modelId = undefined;
485
485
  continue;
486
486
  }
487
- if (openTurns.size === 0) continue;
488
- if (data.type === "turn_routed" && openTurns.has(data.turnId)) {
489
- modelId = data.modelId;
490
- continue;
491
- }
492
- if (data.type === "handoff") modelId = data.modelId;
487
+ if (openTurns.size > 0 && event.model) modelId = event.model.modelId;
493
488
  }
494
489
  return openTurns.size > 0 ? modelId : undefined;
495
490
  }