@sentry/junior-dashboard 0.263.0 → 0.265.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -5
  3. package/dist/app.js +119 -35
  4. package/dist/client.js +148 -135
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +119 -35
  7. package/dist/tailwind.css +1 -1
  8. package/package.json +9 -4
  9. package/src/api/schema.ts +0 -1
  10. package/src/app.ts +5 -20
  11. package/src/client/App.tsx +29 -31
  12. package/src/client/api.ts +7 -0
  13. package/src/client/components/BottomSheet.tsx +26 -18
  14. package/src/client/components/CopyButton.tsx +83 -0
  15. package/src/client/components/DetailsPanel.tsx +133 -0
  16. package/src/client/components/Drawer.tsx +34 -33
  17. package/src/client/components/FilterBar.tsx +16 -31
  18. package/src/client/components/Metric.tsx +33 -23
  19. package/src/client/components/PageContentSkeleton.tsx +1 -1
  20. package/src/client/components/Pagination.tsx +1 -1
  21. package/src/client/components/ParticipantAvatarStack.tsx +40 -36
  22. package/src/client/components/ProfileMenu.tsx +27 -93
  23. package/src/client/components/README.md +101 -64
  24. package/src/client/components/SegmentedNav.tsx +1 -1
  25. package/src/client/components/ShimmerText.tsx +2 -6
  26. package/src/client/components/ThemeSwitcher.tsx +19 -15
  27. package/src/client/components/charts/ActivityChart.tsx +173 -288
  28. package/src/client/components/charts/CacheWrites.tsx +63 -50
  29. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  30. package/src/client/components/charts/README.md +4 -4
  31. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  32. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  33. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  34. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  35. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  36. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  37. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  38. package/src/client/components/ui/accordion.tsx +77 -0
  39. package/src/client/components/ui/alert-dialog.tsx +89 -0
  40. package/src/client/components/ui/button.tsx +85 -0
  41. package/src/client/components/ui/chart.tsx +74 -0
  42. package/src/client/components/ui/checkbox.tsx +26 -0
  43. package/src/client/components/ui/collapsible.tsx +22 -0
  44. package/src/client/components/ui/field.tsx +49 -0
  45. package/src/client/components/ui/input.tsx +35 -0
  46. package/src/client/components/ui/native-select.tsx +57 -0
  47. package/src/client/components/ui/popover.tsx +67 -0
  48. package/src/client/components/ui/radio-group.tsx +32 -0
  49. package/src/client/components/ui/sheet.tsx +151 -0
  50. package/src/client/components/ui/skeleton.tsx +19 -0
  51. package/src/client/components/ui/table.tsx +80 -0
  52. package/src/client/components/ui/tabs.tsx +75 -0
  53. package/src/client/components/ui/textarea.tsx +17 -0
  54. package/src/client/components/ui/toast.tsx +215 -0
  55. package/src/client/components/ui/toggle-group.tsx +36 -0
  56. package/src/client/components/ui/toggle.tsx +37 -0
  57. package/src/client/components/ui/tooltip.tsx +168 -0
  58. package/src/client/components/ui/trace.tsx +96 -0
  59. package/src/client/components/ui/useSheet.ts +74 -0
  60. package/src/client/conversations/ChatLayout.tsx +7 -3
  61. package/src/client/conversations/ConversationBrief.tsx +17 -6
  62. package/src/client/conversations/ConversationChildAgents.tsx +87 -58
  63. package/src/client/conversations/ConversationComposer.tsx +26 -12
  64. package/src/client/conversations/ConversationDetails.tsx +198 -95
  65. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  66. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  67. package/src/client/conversations/ConversationHeader.tsx +8 -3
  68. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  69. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  70. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  71. package/src/client/conversations/ConversationMeta.tsx +111 -92
  72. package/src/client/conversations/ConversationPage.tsx +241 -110
  73. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  74. package/src/client/conversations/ConversationUsageBreakdown.tsx +16 -3
  75. package/src/client/conversations/ConversationWatches.tsx +165 -0
  76. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  77. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  78. package/src/client/conversations/DetailsList.tsx +124 -0
  79. package/src/client/conversations/EventDetails.tsx +42 -8
  80. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  81. package/src/client/conversations/ObjectCard.tsx +20 -8
  82. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  83. package/src/client/conversations/README.md +93 -19
  84. package/src/client/conversations/ShareConversationButton.tsx +28 -3
  85. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  86. package/src/client/conversations/ToolFrame.tsx +36 -52
  87. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  88. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  89. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  90. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  91. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  92. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  93. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  94. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  95. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  96. package/src/client/conversations/TranscriptSubagentView.tsx +3 -3
  97. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  98. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  99. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  100. package/src/client/conversations/TranscriptView.tsx +63 -27
  101. package/src/client/conversations/eventLog.ts +57 -0
  102. package/src/client/conversations/eventTime.ts +60 -0
  103. package/src/client/conversations/queries.ts +29 -0
  104. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  105. package/src/client/format.ts +5 -0
  106. package/src/client/lib/utils.ts +1 -0
  107. package/src/client/pages/ConnectionsPage.tsx +2 -2
  108. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  109. package/src/client/pages/SettingsPage.tsx +9 -10
  110. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  111. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  112. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  113. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  114. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  115. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  116. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  117. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  118. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  119. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  120. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  121. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  122. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  123. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  124. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  125. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  126. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  127. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  128. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  129. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  130. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  131. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  132. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  133. package/src/client/pages/automations/README.md +57 -0
  134. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  135. package/src/client/pages/automations/automationFormat.ts +37 -23
  136. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  137. package/src/client/pages/code/CodePage.tsx +52 -55
  138. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  139. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  140. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  141. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  142. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  143. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  144. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  145. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  146. package/src/client/pages/memory/memoryRecord.ts +1 -1
  147. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  148. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  149. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  150. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  151. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  152. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  153. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  154. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  155. package/src/client/pages/system/README.md +23 -0
  156. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  157. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  158. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  159. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  160. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  161. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  162. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  163. package/src/client/pages/system/sandboxSize.ts +8 -0
  164. package/src/client/pages/system/workspaceQueries.ts +53 -0
  165. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  166. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  167. package/src/client/styles.ts +4 -0
  168. package/src/client.tsx +7 -1
  169. package/src/mock-reporting/automation-collection.ts +4 -2
  170. package/src/mock-reporting/fixtures.ts +23 -7
  171. package/src/mock-reporting/routes.ts +12 -0
  172. package/src/mock-reporting/watches.ts +94 -0
  173. package/src/shell.ts +4 -9
  174. package/src/tailwind.css +77 -10
  175. package/src/client/components/Button.tsx +0 -118
  176. package/src/client/components/Field.tsx +0 -53
  177. package/src/client/components/SegmentedTabs.tsx +0 -119
  178. package/src/client/components/Select.tsx +0 -52
  179. package/src/client/components/Sheet.LICENSE +0 -21
  180. package/src/client/components/Sheet.tsx +0 -141
  181. package/src/client/components/Skeleton.tsx +0 -17
  182. package/src/client/components/TextInput.tsx +0 -58
  183. package/src/client/components/Tooltip.tsx +0 -216
  184. package/src/client/components/charts/useChartLayout.ts +0 -27
  185. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -91
  186. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  187. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  188. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  189. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  190. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  191. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  192. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  193. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  194. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  195. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  196. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  197. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -0,0 +1,168 @@
1
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useId,
7
+ useState,
8
+ useSyncExternalStore,
9
+ type ComponentProps,
10
+ } from "react";
11
+ import { cn } from "@dashboard/lib/utils";
12
+
13
+ /** Open dashboard tooltips at once and keep them open briefly for the pointer to reach them. */
14
+ function TooltipProvider({
15
+ delay = 0,
16
+ closeDelay = 150,
17
+ ...props
18
+ }: TooltipPrimitive.Provider.Props) {
19
+ return (
20
+ <TooltipPrimitive.Provider
21
+ data-slot="tooltip-provider"
22
+ delay={delay}
23
+ closeDelay={closeDelay}
24
+ {...props}
25
+ />
26
+ );
27
+ }
28
+
29
+ /** Link the open popup to its trigger. Base UI leaves this to the app. */
30
+ const TooltipDescriptionContext = createContext<string | undefined>(undefined);
31
+
32
+ /**
33
+ * Wide screens with a fine hover pointer show tooltips. On smaller or touch
34
+ * screens, a tooltip can cover the menu that its trigger opens.
35
+ */
36
+ const TOOLTIP_MEDIA_QUERY =
37
+ "(min-width: 768px) and (hover: hover) and (pointer: fine)";
38
+
39
+ function subscribeToTooltipMedia(onChange: () => void) {
40
+ const media = window.matchMedia(TOOLTIP_MEDIA_QUERY);
41
+ media.addEventListener("change", onChange);
42
+ return () => media.removeEventListener("change", onChange);
43
+ }
44
+
45
+ function tooltipMediaMatches() {
46
+ return window.matchMedia(TOOLTIP_MEDIA_QUERY).matches;
47
+ }
48
+
49
+ /** Own one tooltip's open state. It stays off where tooltips are not shown. */
50
+ function Tooltip({
51
+ disabled,
52
+ onOpenChange,
53
+ open: openProp,
54
+ ...props
55
+ }: TooltipPrimitive.Root.Props) {
56
+ const available = useSyncExternalStore(
57
+ subscribeToTooltipMedia,
58
+ tooltipMediaMatches,
59
+ () => false,
60
+ );
61
+ const id = useId();
62
+ const [openState, setOpenState] = useState(props.defaultOpen ?? false);
63
+ const open = available && (openProp ?? openState);
64
+ return (
65
+ <TooltipDescriptionContext.Provider value={open ? id : undefined}>
66
+ <TooltipPrimitive.Root
67
+ data-slot="tooltip"
68
+ disabled={disabled || !available}
69
+ onOpenChange={(next, details) => {
70
+ setOpenState(next);
71
+ onOpenChange?.(next, details);
72
+ }}
73
+ open={openProp}
74
+ {...props}
75
+ />
76
+ </TooltipDescriptionContext.Provider>
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Show the tooltip on mouse hover or keyboard focus. Touch does not open it.
82
+ * A click keeps it open, so copy and share feedback stays visible.
83
+ */
84
+ function TooltipTrigger({
85
+ closeOnClick = false,
86
+ ...props
87
+ }: TooltipPrimitive.Trigger.Props) {
88
+ const describedBy = useContext(TooltipDescriptionContext);
89
+ return (
90
+ <TooltipPrimitive.Trigger
91
+ aria-describedby={describedBy}
92
+ closeOnClick={closeOnClick}
93
+ data-slot="tooltip-trigger"
94
+ {...props}
95
+ />
96
+ );
97
+ }
98
+
99
+ const tooltipContentVariants = cva(
100
+ "z-50 select-text rounded-md border border-dashboard-border-emphasis bg-dashboard-surface-raised font-sans text-xs shadow-2xl shadow-black/70 outline-none",
101
+ {
102
+ variants: {
103
+ size: {
104
+ /** Selectable detail cards, such as chart buckets and metric breakdowns. */
105
+ default:
106
+ "min-w-36 max-w-lg px-3 py-2 leading-relaxed text-dashboard-text-muted",
107
+ /** One short label for an icon-only control. */
108
+ compact: "px-2.5 py-1.5 leading-none text-dashboard-text",
109
+ },
110
+ },
111
+ defaultVariants: { size: "default" },
112
+ },
113
+ );
114
+
115
+ /** Show tooltip text beside its trigger and keep it inside the viewport. */
116
+ function TooltipContent({
117
+ className,
118
+ side = "top",
119
+ sideOffset = 10,
120
+ align = "center",
121
+ size,
122
+ children,
123
+ ...props
124
+ }: TooltipPrimitive.Popup.Props &
125
+ Pick<TooltipPrimitive.Positioner.Props, "align" | "side" | "sideOffset"> &
126
+ VariantProps<typeof tooltipContentVariants>) {
127
+ const id = useContext(TooltipDescriptionContext);
128
+ return (
129
+ <TooltipPrimitive.Portal>
130
+ <TooltipPrimitive.Positioner
131
+ align={align}
132
+ collisionPadding={8}
133
+ side={side}
134
+ sideOffset={sideOffset}
135
+ className="isolate z-50 data-anchor-hidden:hidden"
136
+ >
137
+ <TooltipPrimitive.Popup
138
+ data-slot="tooltip-content"
139
+ id={id}
140
+ role="tooltip"
141
+ className={cn(tooltipContentVariants({ size }), className)}
142
+ {...props}
143
+ >
144
+ {children}
145
+ </TooltipPrimitive.Popup>
146
+ </TooltipPrimitive.Positioner>
147
+ </TooltipPrimitive.Portal>
148
+ );
149
+ }
150
+
151
+ /** Name what a detail tooltip describes, such as a chart bucket date. */
152
+ function TooltipTitle({ className, ...props }: ComponentProps<"div">) {
153
+ return (
154
+ <div
155
+ className={cn("mb-1 font-semibold text-dashboard-text-muted", className)}
156
+ {...props}
157
+ />
158
+ );
159
+ }
160
+
161
+ export {
162
+ Tooltip,
163
+ TooltipTrigger,
164
+ TooltipContent,
165
+ TooltipTitle,
166
+ TooltipProvider,
167
+ tooltipContentVariants,
168
+ };
@@ -0,0 +1,96 @@
1
+ import * as React from "react";
2
+ import { cn } from "@dashboard/lib/utils";
3
+
4
+ import {
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ } from "@dashboard/components/ui/collapsible";
9
+ import { ChevronRightIcon } from "lucide-react";
10
+
11
+ function TraceIcon({ className, ...props }: React.ComponentProps<"span">) {
12
+ return (
13
+ <span
14
+ data-slot="trace-icon"
15
+ aria-hidden="true"
16
+ className={cn(
17
+ "flex size-4 shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ /** Keep one trace row and its details in one open state. */
26
+ function TraceCollapsible({
27
+ className,
28
+ ...props
29
+ }: Omit<React.ComponentProps<typeof Collapsible>, "open"> &
30
+ (
31
+ | { open?: undefined }
32
+ | {
33
+ open: boolean;
34
+ onOpenChange: NonNullable<
35
+ React.ComponentProps<typeof Collapsible>["onOpenChange"]
36
+ >;
37
+ }
38
+ )) {
39
+ return (
40
+ <Collapsible
41
+ data-slot="trace-collapsible"
42
+ className={cn("flex w-full min-w-0 flex-col", className)}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+
48
+ /** Render the row button with a chevron that turns while open. */
49
+ function TraceCollapsibleTrigger({
50
+ className,
51
+ children,
52
+ ...props
53
+ }: React.ComponentProps<typeof CollapsibleTrigger>) {
54
+ return (
55
+ <CollapsibleTrigger
56
+ data-slot="trace-collapsible-trigger"
57
+ className={cn(
58
+ "group/trace-collapsible-trigger flex w-fit max-w-full min-w-0 cursor-pointer items-center gap-1 rounded-md px-2 py-1.5 text-left text-dashboard-text-muted transition-colors outline-none hover:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus disabled:cursor-default disabled:hover:bg-transparent",
59
+ className,
60
+ )}
61
+ {...props}
62
+ >
63
+ {children}
64
+ <TraceIcon className="transition-transform group-aria-expanded/trace-collapsible-trigger:rotate-90 motion-reduce:transition-none">
65
+ <ChevronRightIcon className="size-3 text-dashboard-text-faint" />
66
+ </TraceIcon>
67
+ </CollapsibleTrigger>
68
+ );
69
+ }
70
+
71
+ /** Reveal row details in a card under the row title. */
72
+ function TraceCollapsibleContent({
73
+ className,
74
+ children,
75
+ ...props
76
+ }: React.ComponentProps<typeof CollapsibleContent>) {
77
+ return (
78
+ <CollapsibleContent
79
+ data-slot="trace-collapsible-content"
80
+ className="h-(--collapsible-panel-height) min-w-0 overflow-hidden transition-[height] duration-200 data-ending-style:h-0 data-starting-style:h-0 motion-reduce:transition-none"
81
+ {...props}
82
+ >
83
+ <div
84
+ data-slot="trace-collapsible-content-inner"
85
+ className={cn(
86
+ "mt-1.5 ml-6 min-w-0 overflow-hidden rounded-md border border-dashboard-border-subtle bg-dashboard-surface-panel",
87
+ className,
88
+ )}
89
+ >
90
+ {children}
91
+ </div>
92
+ </CollapsibleContent>
93
+ );
94
+ }
95
+
96
+ export { TraceCollapsible, TraceCollapsibleTrigger, TraceCollapsibleContent };
@@ -0,0 +1,74 @@
1
+ import type { Dialog } from "@base-ui/react/dialog";
2
+ import {
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ useSyncExternalStore,
7
+ type RefObject,
8
+ } from "react";
9
+
10
+ import {
11
+ acquireBodyScrollLock,
12
+ releaseBodyScrollLock,
13
+ } from "../../bodyScrollLock";
14
+
15
+ // A resize can mount conversation details below an already open side drawer.
16
+ // Document order keeps the front modal in charge across these separate roots.
17
+ function subscribeToSheets(onChange: () => void) {
18
+ const observer = new MutationObserver(onChange);
19
+ observer.observe(document.body, { childList: true, subtree: true });
20
+ return () => observer.disconnect();
21
+ }
22
+
23
+ /** Keep an app sheet mounted through dismissal and share the document scroll lock. */
24
+ export function useSheet(
25
+ onClose: () => void,
26
+ initialFocusRef?: RefObject<HTMLElement | null>,
27
+ ) {
28
+ const [open, setOpen] = useState(false);
29
+ const closingRef = useRef(false);
30
+ const popupRef = useRef<HTMLDivElement>(null);
31
+ const topmost = useSyncExternalStore(
32
+ subscribeToSheets,
33
+ () => {
34
+ const modals = document.querySelectorAll("[data-dashboard-modal]");
35
+ const last = modals[modals.length - 1];
36
+ return popupRef.current ? last === popupRef.current : !last;
37
+ },
38
+ () => true,
39
+ );
40
+ useEffect(() => {
41
+ acquireBodyScrollLock();
42
+ setOpen(true);
43
+ return releaseBodyScrollLock;
44
+ }, []);
45
+
46
+ return {
47
+ rootProps: {
48
+ open,
49
+ modal: topmost ? "trap-focus" : false,
50
+ disablePointerDismissal: !topmost,
51
+ onOpenChange(nextOpen, details) {
52
+ if (!nextOpen && !topmost) {
53
+ details.cancel();
54
+ return;
55
+ }
56
+ closingRef.current = !nextOpen;
57
+ setOpen(nextOpen);
58
+ },
59
+ onOpenChangeComplete(nextOpen) {
60
+ if (!nextOpen && closingRef.current) onClose();
61
+ },
62
+ } satisfies Dialog.Root.Props,
63
+ contentProps: {
64
+ ref: popupRef,
65
+ "aria-modal": topmost ? true : undefined,
66
+ finalFocus: () => Boolean(closingRef.current && topmost),
67
+ initialFocus: () => {
68
+ const modals = document.querySelectorAll("[data-dashboard-modal]");
69
+ if (modals[modals.length - 1] !== popupRef.current) return false;
70
+ return initialFocusRef?.current ?? true;
71
+ },
72
+ } satisfies Dialog.Popup.Props,
73
+ };
74
+ }
@@ -5,12 +5,14 @@ import { cn } from "../styles";
5
5
  /**
6
6
  * Own the conversation chat frame: one scroll region above one dock.
7
7
  *
8
- * Pages pass `scroll` and `dock` only. Do not invent footer padding, safe-area,
8
+ * Pages pass `header`, `scroll`, and `dock`. Do not invent footer padding, safe-area,
9
9
  * or shell height math outside this component and `ComposerDock`.
10
10
  */
11
11
  export function ChatLayout(props: {
12
12
  className?: string;
13
13
  dock?: ReactNode;
14
+ /** Keep conversation chrome outside the scrolling content. */
15
+ header?: ReactNode;
14
16
  /** When true, keep a taller minimum scroll region for long transcripts. */
15
17
  scrollMinTall?: boolean;
16
18
  scroll: ReactNode;
@@ -20,11 +22,13 @@ export function ChatLayout(props: {
20
22
  return (
21
23
  <div
22
24
  className={cn(
23
- "grid h-full min-h-0 min-w-0 grid-rows-[minmax(0,1fr)_minmax(0,auto)]",
24
- props.scrollMinTall && "grid-rows-[minmax(7rem,1fr)_minmax(0,auto)]",
25
+ "grid h-full min-h-0 min-w-0 grid-rows-[auto_minmax(0,1fr)_minmax(0,auto)]",
26
+ props.scrollMinTall &&
27
+ "grid-rows-[auto_minmax(7rem,1fr)_minmax(0,auto)]",
25
28
  props.className,
26
29
  )}
27
30
  >
31
+ <div className="min-w-0">{props.header}</div>
28
32
  <div
29
33
  aria-label={props.scrollAriaLabel}
30
34
  className={cn(
@@ -4,7 +4,13 @@ import type { ConversationDetailReport } from "@sentry/junior/api/schema";
4
4
 
5
5
  import { Detail, DetailList } from "../components/DetailList";
6
6
  import { StatusChip, type StatusChipTone } from "../components/StatusChip";
7
+ import {
8
+ Collapsible,
9
+ CollapsibleContent,
10
+ CollapsibleTrigger,
11
+ } from "../components/ui/collapsible";
7
12
  import { formatTime } from "../format";
13
+ import { cn, dashboardDisclosureChevronClass } from "../styles";
8
14
  import { MentionPeopleProvider, renderMentionText } from "./transcriptMentions";
9
15
 
10
16
  type ConversationBriefReport = NonNullable<ConversationDetailReport["brief"]>;
@@ -160,16 +166,21 @@ function BriefContentView(props: {
160
166
 
161
167
  function BriefSection(props: { children: ReactNode; title: string }) {
162
168
  return (
163
- <details className="group min-w-0 border-t border-dashboard-border pt-3">
164
- <summary className="flex cursor-pointer list-none items-center gap-2 text-sm font-medium text-dashboard-text [&::-webkit-details-marker]:hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus">
169
+ <Collapsible className="min-w-0 border-t border-dashboard-border pt-3">
170
+ <CollapsibleTrigger className="group flex w-full cursor-pointer items-center gap-2 text-left text-sm font-medium text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus">
165
171
  <ChevronRight
166
172
  aria-hidden="true"
167
- className="size-3.5 shrink-0 text-dashboard-text-muted transition-transform group-open:rotate-90"
173
+ className={cn(
174
+ dashboardDisclosureChevronClass,
175
+ "text-dashboard-text-muted",
176
+ )}
168
177
  />
169
178
  {props.title}
170
- </summary>
171
- <div className="mt-3">{props.children}</div>
172
- </details>
179
+ </CollapsibleTrigger>
180
+ <CollapsibleContent hiddenUntilFound>
181
+ <div className="mt-3">{props.children}</div>
182
+ </CollapsibleContent>
183
+ </Collapsible>
173
184
  );
174
185
  }
175
186
 
@@ -1,14 +1,24 @@
1
1
  import { useQuery } from "@tanstack/react-query";
2
- import { Bot, CircleAlert, LockKeyhole } from "lucide-react";
2
+ import {
3
+ Bot,
4
+ Circle,
5
+ CircleAlert,
6
+ CircleCheck,
7
+ CircleDot,
8
+ CirclePause,
9
+ CircleX,
10
+ LoaderCircle,
11
+ LockKeyhole,
12
+ type LucideIcon,
13
+ } from "lucide-react";
3
14
  import {
4
15
  conversationChildAgentsReportSchema,
5
16
  type ConversationChildAgent,
6
17
  } from "@sentry/junior/api/schema";
7
18
 
8
- import { StatusChip, type StatusChipTone } from "../components/StatusChip";
19
+ import type { StatusChipTone } from "../components/StatusChip";
9
20
  import {
10
21
  formatConversationCostTotal,
11
- formatRelativeTime,
12
22
  formatRuntime,
13
23
  formatTime,
14
24
  formatUsageTotal,
@@ -85,9 +95,12 @@ export function ConversationChildAgents(props: {
85
95
  }
86
96
 
87
97
  return (
88
- <ol aria-label="Subagents" className="m-0 grid list-none gap-2 p-0">
98
+ <ol
99
+ aria-label="Subagents"
100
+ className="m-0 grid list-none gap-px overflow-hidden rounded-lg border border-dashboard-border-subtle bg-dashboard-border-subtle p-0"
101
+ >
89
102
  {query.data.agents.map((agent) => (
90
- <li key={agent.conversationId}>
103
+ <li className="bg-dashboard-surface-panel" key={agent.conversationId}>
91
104
  <ChildAgentRow agent={agent} onOpen={props.onOpenAgent} />
92
105
  </li>
93
106
  ))}
@@ -95,9 +108,48 @@ export function ConversationChildAgents(props: {
95
108
  );
96
109
  }
97
110
 
111
+ const statusIcons: Partial<Record<string, LucideIcon>> = {
112
+ active: CircleDot,
113
+ blocked: CircleAlert,
114
+ completed: CircleCheck,
115
+ error: CircleX,
116
+ failed: CircleX,
117
+ paused: CirclePause,
118
+ running: LoaderCircle,
119
+ };
120
+
121
+ const statusIconToneClass: Record<StatusChipTone, string> = {
122
+ accent: "text-violet-300",
123
+ danger: "text-rose-300",
124
+ info: "text-cyan-300",
125
+ neutral: "text-dashboard-text-faint",
126
+ success: "text-emerald-300",
127
+ warning: "text-amber-300",
128
+ };
129
+
130
+ /** Mark a child agent status with one icon and an audible label. */
131
+ function ChildAgentStatusIcon(props: { status: string }) {
132
+ const Icon = statusIcons[props.status] ?? Circle;
133
+ return (
134
+ <span className="mt-px grid size-4 shrink-0 place-items-center">
135
+ <Icon
136
+ aria-hidden="true"
137
+ className={cn(
138
+ "size-3.5",
139
+ statusIconToneClass[subagentStatusTone(props.status)],
140
+ props.status === "running" &&
141
+ "animate-spin motion-reduce:animate-none",
142
+ )}
143
+ />
144
+ <span className="sr-only">{props.status}</span>
145
+ </span>
146
+ );
147
+ }
148
+
98
149
  /**
99
- * Show who ran, what it was asked to do, and what it cost. The row text is the
100
- * accessible name, so status, title, task, and error stay audible.
150
+ * Show one child agent as a dense row: status, task title, and run time first,
151
+ * then agent name and cost. The row text is the accessible name, so status,
152
+ * title, and error stay audible.
101
153
  */
102
154
  function ChildAgentRow(props: {
103
155
  agent: ConversationChildAgent;
@@ -107,24 +159,20 @@ function ChildAgentRow(props: {
107
159
  const tone = subagentStatusTone(agent.status);
108
160
  const task = agent.task?.trim();
109
161
  const headline = agent.title ?? task;
110
- // A title is often a short form of the task. Skip the task when it adds nothing.
111
- const detail = agent.title && task && task !== agent.title ? task : undefined;
112
162
  const runtime = formatRuntime(agent.cumulativeDurationMs);
163
+ const name = agent.redacted ? "Private subagent" : (agent.name ?? "subagent");
113
164
  // Totals include this subagent's own subagents. Say so, so the row does not
114
165
  // read as the work of this subagent alone.
115
166
  const nested = agent.childAgentTotals?.count;
116
- const stats = [
167
+ const meta = [
117
168
  formatUsageTotal(agent.cumulativeUsage),
118
169
  formatConversationCostTotal(agent.cumulativeUsage, agent.auxiliaryCosts),
119
- runtime ? `${runtime} runtime` : "",
120
- nested
121
- ? `incl. ${nested} nested ${nested === 1 ? "subagent" : "subagents"}`
122
- : "",
170
+ nested ? `+${nested} nested` : "",
123
171
  ].filter(Boolean);
124
172
 
125
173
  return (
126
174
  <button
127
- className="grid w-full min-w-0 cursor-pointer gap-1.5 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel px-3 py-2.5 text-left text-inherit transition-colors duration-200 hover:border-dashboard-border-emphasis hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus"
175
+ className="grid w-full min-w-0 cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-x-2.5 gap-y-0.5 px-3 py-2.5 text-left text-inherit transition-colors hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
128
176
  onClick={() =>
129
177
  props.onOpen({
130
178
  conversationId: agent.conversationId,
@@ -137,59 +185,40 @@ function ChildAgentRow(props: {
137
185
  status: agent.status,
138
186
  })
139
187
  }
188
+ title={[
189
+ task && task !== headline ? task : "",
190
+ `Updated ${formatTime(agent.updatedAt, {
191
+ dateStyle: "medium",
192
+ timeStyle: "short",
193
+ })}`,
194
+ ]
195
+ .filter(Boolean)
196
+ .join("\n")}
140
197
  type="button"
141
198
  >
142
- <span className="flex min-w-0 items-center gap-2 text-xs">
143
- {agent.redacted ? (
144
- <span className="flex min-w-0 items-center gap-1.5 text-dashboard-text-muted">
145
- <LockKeyhole aria-hidden="true" className="size-3 shrink-0" />
146
- Private subagent
147
- </span>
148
- ) : (
149
- <span className="min-w-0 truncate font-medium text-dashboard-text-muted">
150
- {agent.name ?? "subagent"}
151
- </span>
152
- )}
153
- <StatusChip className="ml-auto shrink-0" size="compact" tone={tone}>
154
- {agent.status}
155
- </StatusChip>
156
- <time
157
- className="shrink-0 text-dashboard-text-faint"
158
- dateTime={agent.updatedAt}
159
- title={`Updated ${formatTime(agent.updatedAt, {
160
- dateStyle: "medium",
161
- timeStyle: "short",
162
- })}`}
163
- >
164
- {formatRelativeTime(agent.updatedAt)}
165
- </time>
199
+ <ChildAgentStatusIcon status={agent.status} />
200
+ <span className="min-w-0 truncate text-sm font-medium leading-5 text-dashboard-text">
201
+ {headline ?? name}
166
202
  </span>
167
- {headline ? (
168
- <span className="line-clamp-2 break-words text-sm font-semibold leading-snug text-dashboard-text">
169
- {headline}
170
- </span>
171
- ) : null}
172
- {detail ? (
173
- <span className="line-clamp-2 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text-muted">
174
- {detail}
203
+ <span className="shrink-0 text-xs leading-5 tabular-nums text-dashboard-text-muted">
204
+ {runtime}
205
+ </span>
206
+ <span className="col-start-2 col-end-4 flex min-w-0 items-center gap-1.5 text-xs leading-4 text-dashboard-text-muted">
207
+ {agent.redacted ? (
208
+ <LockKeyhole aria-hidden="true" className="size-3 shrink-0" />
209
+ ) : null}
210
+ <span className="min-w-0 truncate">
211
+ {[headline ? name : "", ...meta].filter(Boolean).join(" · ")}
175
212
  </span>
176
- ) : null}
213
+ </span>
177
214
  {agent.error ? (
178
215
  <span
179
216
  className={cn(
180
- "flex min-w-0 items-start gap-1.5 text-xs leading-relaxed",
217
+ "col-start-2 col-end-4 line-clamp-2 min-w-0 whitespace-pre-wrap break-words pt-0.5 text-xs leading-4",
181
218
  errorToneClass[tone] ?? "text-dashboard-text-muted",
182
219
  )}
183
220
  >
184
- <CircleAlert aria-hidden="true" className="mt-0.5 size-3 shrink-0" />
185
- <span className="line-clamp-3 min-w-0 whitespace-pre-wrap break-words">
186
- {agent.error}
187
- </span>
188
- </span>
189
- ) : null}
190
- {stats.length > 0 ? (
191
- <span className="block text-xs tabular-nums text-dashboard-text-muted">
192
- {stats.join(" · ")}
221
+ {agent.error}
193
222
  </span>
194
223
  ) : null}
195
224
  </button>
@@ -12,7 +12,12 @@ import {
12
12
  } from "react";
13
13
  import { ImagePlus, Send, Square } from "lucide-react";
14
14
 
15
- import { Button } from "../components/Button";
15
+ import { Button } from "../components/ui/button";
16
+ import {
17
+ Tooltip,
18
+ TooltipContent,
19
+ TooltipTrigger,
20
+ } from "../components/ui/tooltip";
16
21
  import { useDashboardOnline } from "../connection";
17
22
  import { cn, dashboardComposerSurfaceClass } from "../styles";
18
23
 
@@ -447,16 +452,25 @@ export const ConversationComposer = memo(function ConversationComposer(
447
452
  )}
448
453
  >
449
454
  <div className="flex min-w-0 items-center gap-3">
450
- <Button
451
- aria-label="Attach images"
452
- disabled={sendLocked || readingImages}
453
- onClick={() => fileInputRef.current?.click()}
454
- size="icon"
455
- title="Attach images (up to 3 MB total)"
456
- type="button"
457
- >
458
- <ImagePlus aria-hidden="true" size={16} />
459
- </Button>
455
+ <Tooltip>
456
+ <TooltipTrigger
457
+ render={
458
+ <Button
459
+ focusableWhenDisabled
460
+ aria-label="Attach images"
461
+ disabled={sendLocked || readingImages}
462
+ onClick={() => fileInputRef.current?.click()}
463
+ size="icon"
464
+ type="button"
465
+ >
466
+ <ImagePlus aria-hidden="true" size={16} />
467
+ </Button>
468
+ }
469
+ />
470
+ <TooltipContent size="compact">
471
+ Attach images (up to 3 MB total)
472
+ </TooltipContent>
473
+ </Tooltip>
460
474
  {props.footerStart}
461
475
  {props.footerStart ? null : (
462
476
  <div className="hidden min-w-0 font-sans text-xs leading-relaxed text-dashboard-text-muted md:block">
@@ -488,7 +502,7 @@ export const ConversationComposer = memo(function ConversationComposer(
488
502
  readingImages
489
503
  }
490
504
  title={sendLocked ? "Sending message" : props.submitLabel}
491
- tone="primary"
505
+ variant="primary"
492
506
  type="submit"
493
507
  >
494
508
  <Send aria-hidden="true" size={14} />