@sentry/junior-dashboard 0.264.0 → 0.266.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -5
  3. package/dist/app.js +119 -35
  4. package/dist/client.js +145 -136
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +119 -35
  7. package/dist/tailwind.css +1 -1
  8. package/package.json +5 -4
  9. package/src/api/schema.ts +0 -1
  10. package/src/app.ts +5 -20
  11. package/src/client/App.tsx +29 -31
  12. package/src/client/api.ts +7 -0
  13. package/src/client/components/ActorAvatar.tsx +10 -22
  14. package/src/client/components/BottomSheet.tsx +12 -2
  15. package/src/client/components/ConnectionBanner.tsx +4 -3
  16. package/src/client/components/CopyButton.tsx +82 -0
  17. package/src/client/components/DetailsPanel.tsx +133 -0
  18. package/src/client/components/Drawer.tsx +15 -24
  19. package/src/client/components/EmptyTelemetry.tsx +5 -3
  20. package/src/client/components/FilterBar.tsx +16 -31
  21. package/src/client/components/ImageAttachment.tsx +49 -63
  22. package/src/client/components/InlineError.tsx +8 -4
  23. package/src/client/components/Metric.tsx +33 -23
  24. package/src/client/components/Notice.tsx +19 -26
  25. package/src/client/components/PageContentSkeleton.tsx +1 -1
  26. package/src/client/components/Pagination.tsx +27 -27
  27. package/src/client/components/ParticipantAvatarStack.tsx +43 -39
  28. package/src/client/components/ProfileMenu.tsx +27 -93
  29. package/src/client/components/README.md +103 -78
  30. package/src/client/components/SearchInput.tsx +51 -41
  31. package/src/client/components/SegmentedNav.tsx +1 -1
  32. package/src/client/components/ShimmerText.tsx +2 -6
  33. package/src/client/components/ThemeSwitcher.tsx +19 -15
  34. package/src/client/components/VersionDriftBanner.tsx +9 -7
  35. package/src/client/components/charts/ActivityChart.tsx +173 -288
  36. package/src/client/components/charts/CacheWrites.tsx +59 -37
  37. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  38. package/src/client/components/charts/README.md +4 -4
  39. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  40. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  41. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  42. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  43. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  44. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  45. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  46. package/src/client/components/ui/accordion.tsx +77 -0
  47. package/src/client/components/ui/alert-dialog.tsx +89 -0
  48. package/src/client/components/ui/alert.tsx +64 -0
  49. package/src/client/components/ui/avatar.tsx +53 -0
  50. package/src/client/components/ui/button.tsx +86 -0
  51. package/src/client/components/ui/chart.tsx +74 -0
  52. package/src/client/components/ui/checkbox.tsx +26 -0
  53. package/src/client/components/ui/collapsible.tsx +22 -0
  54. package/src/client/components/ui/dialog.tsx +66 -0
  55. package/src/client/components/ui/empty.tsx +32 -0
  56. package/src/client/components/ui/field.tsx +49 -0
  57. package/src/client/components/ui/input-group.tsx +96 -0
  58. package/src/client/components/ui/input.tsx +35 -0
  59. package/src/client/components/ui/native-select.tsx +57 -0
  60. package/src/client/components/ui/pagination.tsx +60 -0
  61. package/src/client/components/ui/popover.tsx +67 -0
  62. package/src/client/components/ui/radio-group.tsx +32 -0
  63. package/src/client/components/ui/sheet.tsx +13 -3
  64. package/src/client/components/ui/skeleton.tsx +19 -0
  65. package/src/client/components/ui/spinner.tsx +21 -0
  66. package/src/client/components/ui/table.tsx +101 -0
  67. package/src/client/components/ui/tabs.tsx +75 -0
  68. package/src/client/components/ui/textarea.tsx +17 -0
  69. package/src/client/components/ui/toast.tsx +215 -0
  70. package/src/client/components/ui/toggle-group.tsx +36 -0
  71. package/src/client/components/ui/toggle.tsx +37 -0
  72. package/src/client/components/ui/tooltip.tsx +168 -0
  73. package/src/client/components/ui/trace.tsx +96 -0
  74. package/src/client/conversations/ChatLayout.tsx +7 -3
  75. package/src/client/conversations/ConversationBrief.tsx +17 -6
  76. package/src/client/conversations/ConversationChildAgents.tsx +5 -5
  77. package/src/client/conversations/ConversationComposer.tsx +26 -12
  78. package/src/client/conversations/ConversationDetails.tsx +147 -62
  79. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  80. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  81. package/src/client/conversations/ConversationHeader.tsx +13 -7
  82. package/src/client/conversations/ConversationHeaderActions.tsx +97 -93
  83. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  84. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  85. package/src/client/conversations/ConversationMeta.tsx +55 -88
  86. package/src/client/conversations/ConversationPage.tsx +220 -101
  87. package/src/client/conversations/ConversationSidebar.tsx +49 -29
  88. package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
  89. package/src/client/conversations/ConversationWatches.tsx +173 -0
  90. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  91. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  92. package/src/client/conversations/DetailsList.tsx +139 -0
  93. package/src/client/conversations/EventDetails.tsx +42 -8
  94. package/src/client/conversations/EventUsage.tsx +99 -74
  95. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  96. package/src/client/conversations/ObjectCard.tsx +20 -8
  97. package/src/client/conversations/PendingMailboxStack.tsx +52 -38
  98. package/src/client/conversations/README.md +89 -13
  99. package/src/client/conversations/ShareConversationButton.tsx +28 -17
  100. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  101. package/src/client/conversations/ToolFrame.tsx +36 -52
  102. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  103. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  104. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  105. package/src/client/conversations/TranscriptEventView.tsx +35 -16
  106. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  107. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  108. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  109. package/src/client/conversations/TranscriptMessageContextView.tsx +35 -18
  110. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  111. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  112. package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
  113. package/src/client/conversations/TranscriptSummary.tsx +34 -2
  114. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  115. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  116. package/src/client/conversations/TranscriptTurnContextView.tsx +144 -65
  117. package/src/client/conversations/TranscriptView.tsx +63 -27
  118. package/src/client/conversations/eventLog.ts +58 -0
  119. package/src/client/conversations/eventTime.ts +60 -0
  120. package/src/client/conversations/queries.ts +29 -0
  121. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  122. package/src/client/conversations/transcriptMentions.tsx +18 -5
  123. package/src/client/format.ts +5 -0
  124. package/src/client/pages/ConnectionsPage.tsx +2 -2
  125. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  126. package/src/client/pages/SettingsPage.tsx +9 -10
  127. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  128. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  129. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  130. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  131. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  132. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  133. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  134. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  135. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  136. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  137. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  138. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  139. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  140. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  141. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  142. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  143. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  144. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  145. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  146. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  147. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  148. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  149. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  150. package/src/client/pages/automations/README.md +57 -0
  151. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  152. package/src/client/pages/automations/automationFormat.ts +37 -23
  153. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  154. package/src/client/pages/code/CodePage.tsx +52 -55
  155. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  156. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  157. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  158. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  159. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  160. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  161. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  162. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  163. package/src/client/pages/memory/memoryRecord.ts +1 -1
  164. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  165. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  166. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  167. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  168. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  169. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  170. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  171. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  172. package/src/client/pages/system/README.md +23 -0
  173. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  174. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  175. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  176. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  177. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  178. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  179. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  180. package/src/client/pages/system/sandboxSize.ts +8 -0
  181. package/src/client/pages/system/workspaceQueries.ts +53 -0
  182. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  183. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  184. package/src/client/styles.ts +4 -0
  185. package/src/client.tsx +7 -1
  186. package/src/mock-reporting/automation-collection.ts +4 -2
  187. package/src/mock-reporting/fixtures.ts +23 -7
  188. package/src/mock-reporting/routes.ts +12 -0
  189. package/src/mock-reporting/watches.ts +94 -0
  190. package/src/shell.ts +4 -9
  191. package/src/tailwind.css +0 -10
  192. package/src/client/components/Button.tsx +0 -118
  193. package/src/client/components/Field.tsx +0 -53
  194. package/src/client/components/SegmentedTabs.tsx +0 -119
  195. package/src/client/components/Select.tsx +0 -52
  196. package/src/client/components/Skeleton.tsx +0 -17
  197. package/src/client/components/TextInput.tsx +0 -58
  198. package/src/client/components/Tooltip.tsx +0 -216
  199. package/src/client/components/charts/useChartLayout.ts +0 -27
  200. package/src/client/components/ui/sheet.LICENSE +0 -21
  201. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  202. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  203. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  204. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  205. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  206. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  207. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  208. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  209. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  210. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  211. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  212. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  213. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -10,8 +10,13 @@ import {
10
10
  X,
11
11
  } from "lucide-react";
12
12
 
13
- import { Button, ToggleButton } from "../components/Button";
14
- import { IconButtonTooltip } from "../components/Tooltip";
13
+ import { Button } from "../components/ui/button";
14
+ import {
15
+ Tooltip,
16
+ TooltipContent,
17
+ TooltipTrigger,
18
+ } from "../components/ui/tooltip";
19
+ import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group";
15
20
  import { cn } from "../styles";
16
21
  import type { TranscriptViewMode } from "./transcriptRenderModel";
17
22
  import { ShareConversationButton } from "./ShareConversationButton";
@@ -43,17 +48,19 @@ export function ConversationHeaderActions(props: {
43
48
  if (layout === "menu") {
44
49
  return (
45
50
  <div className="grid gap-0.5">
46
- <MenuActionButton
47
- label={props.searchOpen ? "Hide search" : "Search transcript"}
48
- onClick={props.onSearchClick}
49
- pressed={props.searchOpen}
50
- >
51
- {props.searchOpen ? (
52
- <X aria-hidden="true" size={16} strokeWidth={2} />
53
- ) : (
54
- <Search aria-hidden="true" size={16} strokeWidth={2} />
55
- )}
56
- </MenuActionButton>
51
+ {props.view === "rich" ? (
52
+ <MenuActionButton
53
+ label={props.searchOpen ? "Hide search" : "Search transcript"}
54
+ onClick={props.onSearchClick}
55
+ pressed={props.searchOpen}
56
+ >
57
+ {props.searchOpen ? (
58
+ <X aria-hidden="true" size={16} strokeWidth={2} />
59
+ ) : (
60
+ <Search aria-hidden="true" size={16} strokeWidth={2} />
61
+ )}
62
+ </MenuActionButton>
63
+ ) : null}
57
64
  <MenuActionButton
58
65
  label="Conversation"
59
66
  onClick={() => props.onViewChange("rich")}
@@ -88,10 +95,12 @@ export function ConversationHeaderActions(props: {
88
95
 
89
96
  return (
90
97
  <div className="flex shrink-0 items-center gap-0.5">
91
- <TranscriptSearchToggle
92
- onClick={props.onSearchClick}
93
- open={props.searchOpen}
94
- />
98
+ {props.view === "rich" ? (
99
+ <TranscriptSearchToggle
100
+ onClick={props.onSearchClick}
101
+ open={props.searchOpen}
102
+ />
103
+ ) : null}
95
104
  {props.copyAction}
96
105
  <ArchiveConversationButton {...props.archive} layout="bar" />
97
106
  <Button
@@ -115,67 +124,55 @@ export function TranscriptViewToggle(props: {
115
124
  value: TranscriptViewMode;
116
125
  }) {
117
126
  return (
118
- <div
127
+ <ToggleGroup
119
128
  aria-label="Transcript view"
120
129
  className={
121
130
  props.labelled
122
- ? "inline-flex items-center gap-6"
123
- : "inline-flex items-center gap-0.5 rounded-md border border-dashboard-border bg-dashboard-overlay-soft p-0.5"
131
+ ? "inline-flex gap-6"
132
+ : "inline-flex gap-0.5 rounded-md border border-dashboard-border bg-dashboard-overlay-soft p-0.5"
124
133
  }
125
- role="group"
134
+ onValueChange={([next]) => {
135
+ if (next) props.onChange(next);
136
+ }}
137
+ value={[props.value]}
126
138
  >
127
- <ViewModeButton
128
- active={props.value === "rich"}
129
- label="Conversation"
130
- labelled={props.labelled}
131
- onClick={() => props.onChange("rich")}
132
- >
139
+ <ViewModeItem label="Conversation" labelled={props.labelled} value="rich">
133
140
  <MessagesSquare aria-hidden="true" size={14} strokeWidth={2} />
134
- </ViewModeButton>
135
- <ViewModeButton
136
- active={props.value === "raw"}
137
- label="Event log"
138
- labelled={props.labelled}
139
- onClick={() => props.onChange("raw")}
140
- >
141
+ </ViewModeItem>
142
+ <ViewModeItem label="Event log" labelled={props.labelled} value="raw">
141
143
  <ScrollText aria-hidden="true" size={14} strokeWidth={2} />
142
- </ViewModeButton>
143
- </div>
144
+ </ViewModeItem>
145
+ </ToggleGroup>
144
146
  );
145
147
  }
146
148
 
147
- function ViewModeButton(props: {
148
- active: boolean;
149
+ function ViewModeItem(props: {
149
150
  children: ReactNode;
150
151
  label: string;
151
152
  labelled?: boolean;
152
- onClick(): void;
153
+ value: TranscriptViewMode;
153
154
  }) {
154
- const button = (
155
- <ToggleButton
155
+ const item = (
156
+ <ToggleGroupItem
156
157
  aria-label={props.label}
157
- className={cn(
158
- "!no-underline",
158
+ className={
159
159
  props.labelled
160
- ? "relative flex h-11 items-center gap-2 rounded-none px-0 py-0 text-sm after:absolute after:inset-x-0 after:bottom-0 after:h-0.5"
161
- : "grid size-7 place-items-center rounded-md px-0 py-0",
162
- props.active &&
163
- (props.labelled
164
- ? "text-dashboard-text after:bg-dashboard-text-solid"
165
- : "bg-dashboard-fill-hover text-dashboard-text"),
166
- )}
167
- onClick={props.onClick}
168
- pressed={props.active}
169
- variant="text"
160
+ ? "relative h-11 min-w-0 gap-2 rounded-none border-0 px-0 text-sm hover:bg-transparent after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 data-pressed:border-0 data-pressed:bg-transparent data-pressed:after:bg-dashboard-text-solid"
161
+ : "size-7 min-w-0 border-0 px-0 data-pressed:border-0 data-pressed:bg-dashboard-fill-hover"
162
+ }
163
+ value={props.value}
170
164
  >
171
165
  {props.children}
172
166
  {props.labelled ? props.label : null}
173
- </ToggleButton>
167
+ </ToggleGroupItem>
174
168
  );
175
169
  return props.labelled ? (
176
- button
170
+ item
177
171
  ) : (
178
- <IconButtonTooltip label={props.label}>{button}</IconButtonTooltip>
172
+ <Tooltip>
173
+ <TooltipTrigger render={item} />
174
+ <TooltipContent size="compact">{props.label}</TooltipContent>
175
+ </Tooltip>
179
176
  );
180
177
  }
181
178
 
@@ -188,21 +185,26 @@ export function HeaderIconButton(props: {
188
185
  pressed?: boolean;
189
186
  }) {
190
187
  return (
191
- <IconButtonTooltip label={props.label}>
192
- <Button
193
- ref={props.ref}
194
- aria-label={props.label}
195
- aria-pressed={props.pressed}
196
- className={cn(
197
- "text-dashboard-text-muted",
198
- props.pressed && "bg-dashboard-fill-strong text-dashboard-text",
199
- )}
200
- onClick={props.onClick}
201
- size="icon"
202
- >
203
- {props.children}
204
- </Button>
205
- </IconButtonTooltip>
188
+ <Tooltip>
189
+ <TooltipTrigger
190
+ render={
191
+ <Button
192
+ ref={props.ref}
193
+ aria-label={props.label}
194
+ aria-pressed={props.pressed}
195
+ className={cn(
196
+ "text-dashboard-text-muted",
197
+ props.pressed && "bg-dashboard-fill-strong text-dashboard-text",
198
+ )}
199
+ onClick={props.onClick}
200
+ size="icon"
201
+ >
202
+ {props.children}
203
+ </Button>
204
+ }
205
+ />
206
+ <TooltipContent size="compact">{props.label}</TooltipContent>
207
+ </Tooltip>
206
208
  );
207
209
  }
208
210
 
@@ -233,19 +235,16 @@ function MenuActionButton(props: {
233
235
  pressed?: boolean;
234
236
  }) {
235
237
  return (
236
- <button
238
+ <Button
237
239
  aria-label={props.label}
238
240
  aria-pressed={props.pressed}
239
- className={cn(
240
- "flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text transition-colors hover:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-strong focus-visible:outline-none",
241
- props.pressed && "bg-dashboard-fill-strong",
242
- )}
241
+ className={props.pressed ? "bg-dashboard-fill-strong" : undefined}
243
242
  onClick={props.onClick}
244
- type="button"
243
+ size="menu"
245
244
  >
246
245
  {props.children}
247
246
  <span>{props.label}</span>
248
- </button>
247
+ </Button>
249
248
  );
250
249
  }
251
250
 
@@ -260,29 +259,34 @@ function ArchiveConversationButton(
260
259
  const Icon = props.archived ? ArchiveRestore : Archive;
261
260
  if (props.layout === "menu") {
262
261
  return (
263
- <button
262
+ <Button
264
263
  aria-label={label}
265
- className="flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text transition-colors hover:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-strong focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
266
264
  disabled={props.disabled}
267
265
  onClick={props.onClick}
268
- type="button"
266
+ size="menu"
269
267
  >
270
268
  <Icon aria-hidden="true" size={16} strokeWidth={2} />
271
269
  <span>{label}</span>
272
- </button>
270
+ </Button>
273
271
  );
274
272
  }
275
273
  return (
276
- <IconButtonTooltip label={label}>
277
- <Button
278
- aria-label={label}
279
- className="hidden shrink-0 text-dashboard-text-muted md:grid"
280
- disabled={props.disabled}
281
- onClick={props.onClick}
282
- size="icon"
283
- >
284
- <Icon aria-hidden="true" size={15} strokeWidth={2} />
285
- </Button>
286
- </IconButtonTooltip>
274
+ <Tooltip>
275
+ <TooltipTrigger
276
+ render={
277
+ <Button
278
+ focusableWhenDisabled
279
+ aria-label={label}
280
+ className="hidden shrink-0 text-dashboard-text-muted md:grid"
281
+ disabled={props.disabled}
282
+ onClick={props.onClick}
283
+ size="icon"
284
+ >
285
+ <Icon aria-hidden="true" size={15} strokeWidth={2} />
286
+ </Button>
287
+ }
288
+ />
289
+ <TooltipContent size="compact">{label}</TooltipContent>
290
+ </Tooltip>
287
291
  );
288
292
  }
@@ -9,7 +9,7 @@ import {
9
9
  visualStatusForConversation,
10
10
  } from "../format";
11
11
  import { EmptyTelemetry } from "../components/EmptyTelemetry";
12
- import { Skeleton } from "../components/Skeleton";
12
+ import { Skeleton } from "../components/ui/skeleton";
13
13
  import {
14
14
  conversationParticipants,
15
15
  ParticipantAvatarStack,
@@ -0,0 +1,118 @@
1
+ import type { ObjectType } from "@sentry/junior-plugin-api";
2
+ import type { ConversationDetailReport } from "@sentry/junior/api/schema";
3
+
4
+ import { ObjectIcon } from "../components/ObjectIcon";
5
+ import { DetailsList, DetailsRow, DetailsRowLine } from "./DetailsList";
6
+
7
+ type ConversationAnnotation = NonNullable<
8
+ ConversationDetailReport["annotations"]
9
+ >[number];
10
+
11
+ /** One annotation that links to work outside Junior. */
12
+ export type ConversationLink = {
13
+ description?: string;
14
+ facts?: { type: string };
15
+ key: string;
16
+ label: string;
17
+ objectType: ObjectType;
18
+ plugin: string;
19
+ status?: string;
20
+ url: string;
21
+ };
22
+
23
+ const resourceLinkStatusLabels: Record<string, string> = {
24
+ closed: "Closed",
25
+ draft: "Draft",
26
+ merged: "Merged",
27
+ open: "Open",
28
+ warning: "Needs attention",
29
+ };
30
+
31
+ /** Name a link status for a reader, such as "Merged" for `merged`. */
32
+ export function linkStatusLabel(status: string | undefined): string {
33
+ if (!status) return "";
34
+ return (
35
+ resourceLinkStatusLabels[status] ??
36
+ status.charAt(0).toUpperCase() + status.slice(1)
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Pick the annotations that link to outside work, newest first as saved. A
42
+ * resource link without an object type takes the type of its sidebar
43
+ * annotation.
44
+ */
45
+ export function conversationLinks(
46
+ detail: ConversationDetailReport | undefined,
47
+ ): ConversationLink[] {
48
+ return (detail?.annotations ?? []).flatMap(
49
+ (annotation: ConversationAnnotation): ConversationLink[] => {
50
+ if (!annotation.url) return [];
51
+ const objectType =
52
+ annotation.objectType ??
53
+ detail?.sidebarAnnotations?.find(
54
+ (sidebar) => sidebar.key === annotation.key,
55
+ )?.objectType ??
56
+ "item";
57
+ const description =
58
+ annotation.kind === "object"
59
+ ? annotation.title !== annotation.label
60
+ ? annotation.title
61
+ : annotation.description
62
+ : annotation.description;
63
+ return [
64
+ {
65
+ ...(description ? { description } : undefined),
66
+ ...(annotation.kind === "object" && annotation.facts
67
+ ? { facts: annotation.facts }
68
+ : undefined),
69
+ key: `${annotation.plugin}:${annotation.key}`,
70
+ label: annotation.label,
71
+ objectType,
72
+ plugin: annotation.plugin,
73
+ ...(annotation.status ? { status: annotation.status } : undefined),
74
+ url: annotation.url,
75
+ },
76
+ ];
77
+ },
78
+ );
79
+ }
80
+
81
+ /** Show the outside work that a Conversation links to, one row each. */
82
+ export function ConversationLinkedWork(props: { links: ConversationLink[] }) {
83
+ return (
84
+ <DetailsList
85
+ itemKey={(link) => link.key}
86
+ items={props.links}
87
+ renderItem={(link) => {
88
+ const status = linkStatusLabel(link.status);
89
+ return (
90
+ <DetailsRow
91
+ href={link.url}
92
+ icon={
93
+ // The row text names the status, so the icon stays silent.
94
+ <ObjectIcon
95
+ decorative
96
+ facts={link.facts}
97
+ objectType={link.objectType}
98
+ size={14}
99
+ status={link.status}
100
+ />
101
+ }
102
+ meta={status}
103
+ title={link.label}
104
+ tooltip={[link.label, link.plugin, status, link.description]
105
+ .filter(Boolean)
106
+ .join(" · ")}
107
+ >
108
+ {link.description ? (
109
+ <DetailsRowLine className="line-clamp-2 break-words">
110
+ {link.description}
111
+ </DetailsRowLine>
112
+ ) : null}
113
+ </DetailsRow>
114
+ );
115
+ }}
116
+ />
117
+ );
118
+ }
@@ -24,7 +24,12 @@ import {
24
24
  summarizeUsage,
25
25
  automationPath,
26
26
  } from "../format";
27
- import { Tooltip } from "../components/Tooltip";
27
+ import {
28
+ Tooltip,
29
+ TooltipContent,
30
+ TooltipTitle,
31
+ TooltipTrigger,
32
+ } from "../components/ui/tooltip";
28
33
  import { MetricList, type MetricListItem } from "../components/Metric";
29
34
  import { cn, dashboardInlineLinkClass } from "../styles";
30
35
  import {
@@ -39,6 +44,7 @@ import {
39
44
  type SidebarAnnotation,
40
45
  type SidebarAnnotationBadgeGroup,
41
46
  } from "./sidebarAnnotationBadges";
47
+ import { conversationLinks, linkStatusLabel } from "./ConversationLinkedWork";
42
48
 
43
49
  const MOBILE_MEDIA_QUERY = "(max-width: 767px)";
44
50
 
@@ -100,13 +106,6 @@ export function ConversationPrivacyChip(props: {
100
106
  );
101
107
  }
102
108
 
103
- type ResourceLinkStatus = NonNullable<
104
- Extract<
105
- NonNullable<ConversationDetailReport["annotations"]>[number],
106
- { kind: "resource_link" }
107
- >["status"]
108
- >;
109
-
110
109
  /** True when the conversation has at least one resource-link annotation. */
111
110
  export function hasConversationAnnotations(
112
111
  annotations: ConversationDetailReport["annotations"] | undefined,
@@ -136,9 +135,36 @@ export function ConversationSidebarAnnotations(props: {
136
135
  : [];
137
136
 
138
137
  return (
139
- <Tooltip
140
- align="left"
141
- content={
138
+ <Tooltip>
139
+ <TooltipTrigger
140
+ render={
141
+ <span
142
+ aria-label={`Linked work, newest first: ${details.join(", ")}`}
143
+ className="inline-flex min-w-0 max-w-full items-center"
144
+ >
145
+ {useMobileFacepile ? (
146
+ <SidebarAnnotationIconFacepile
147
+ annotations={mobileFacepileAnnotations}
148
+ // Discs sit on the sidebar panel / row surface.
149
+ cutoutColor="var(--color-dashboard-surface-panel)"
150
+ />
151
+ ) : (
152
+ <span className="inline-flex min-w-0 items-center gap-1">
153
+ {badges.labeledGroups.map((group) => (
154
+ <SidebarAnnotationGroupChip group={group} key={group.label} />
155
+ ))}
156
+ {badges.overflowGroupCount > 0 ? (
157
+ <span className="inline-flex h-5 min-w-7 shrink-0 items-center justify-center rounded-full border border-dashboard-border-strong bg-dashboard-control px-1.5 font-sans text-2xs tabular-nums leading-none text-dashboard-text-muted">
158
+ +{badges.overflowGroupCount}
159
+ </span>
160
+ ) : null}
161
+ </span>
162
+ )}
163
+ </span>
164
+ }
165
+ />
166
+ <TooltipContent align="start">
167
+ <TooltipTitle>Linked work</TooltipTitle>
142
168
  <ul className="m-0 list-none space-y-1 p-0">
143
169
  {annotations.map((annotation, index) => (
144
170
  <li
@@ -158,33 +184,7 @@ export function ConversationSidebarAnnotations(props: {
158
184
  </li>
159
185
  ))}
160
186
  </ul>
161
- }
162
- label="Linked work"
163
- triggerClassName="min-w-0"
164
- >
165
- <span
166
- aria-label={`Linked work, newest first: ${details.join(", ")}`}
167
- className="inline-flex min-w-0 max-w-full items-center"
168
- >
169
- {useMobileFacepile ? (
170
- <SidebarAnnotationIconFacepile
171
- annotations={mobileFacepileAnnotations}
172
- // Discs sit on the sidebar panel / row surface.
173
- cutoutColor="var(--color-dashboard-surface-panel)"
174
- />
175
- ) : (
176
- <span className="inline-flex min-w-0 items-center gap-1">
177
- {badges.labeledGroups.map((group) => (
178
- <SidebarAnnotationGroupChip group={group} key={group.label} />
179
- ))}
180
- {badges.overflowGroupCount > 0 ? (
181
- <span className="inline-flex h-5 min-w-7 shrink-0 items-center justify-center rounded-full border border-dashboard-border-strong bg-dashboard-control px-1.5 font-sans text-2xs tabular-nums leading-none text-dashboard-text-muted">
182
- +{badges.overflowGroupCount}
183
- </span>
184
- ) : null}
185
- </span>
186
- )}
187
- </span>
187
+ </TooltipContent>
188
188
  </Tooltip>
189
189
  );
190
190
  }
@@ -412,48 +412,35 @@ function SidebarAnnotationIcon(props: {
412
412
  );
413
413
  }
414
414
 
415
- /** Render resource-link annotations under the conversation title. */
415
+ /** Render linked work as a compact strip under the conversation title. */
416
416
  export function ConversationAnnotations(props: {
417
417
  detail: ConversationDetailReport | undefined;
418
- layout?: "list" | "strip";
419
418
  }) {
420
- const links =
421
- props.detail?.annotations?.filter(
422
- (annotation) => annotation.url !== null,
423
- ) ?? [];
419
+ const links = conversationLinks(props.detail);
424
420
  if (links.length === 0) return null;
425
421
  return (
426
- <div
427
- className={cn(
428
- "flex gap-x-4 gap-y-2",
429
- props.layout === "strip" ? "overflow-x-auto" : "flex-col",
430
- )}
431
- >
422
+ <div className="flex gap-x-4 gap-y-2 overflow-x-auto">
432
423
  {links.map((link) => (
433
424
  <a
434
- className={cn(
435
- "inline-flex min-w-0 max-w-full shrink-0 items-center gap-2 rounded-md font-sans leading-snug text-dashboard-text no-underline transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus",
436
- props.layout === "strip"
437
- ? "px-1 py-0.5 text-xs hover:text-dashboard-accent-text"
438
- : "min-h-10 border border-dashboard-border px-3 py-2 text-sm hover:border-dashboard-border-interactive hover:bg-dashboard-surface-hover",
439
- )}
440
- href={link.url ?? undefined}
441
- key={`${link.plugin}:${link.key}`}
425
+ className="inline-flex min-w-0 max-w-full shrink-0 items-center gap-2 rounded-md px-1 py-0.5 font-sans text-xs leading-snug text-dashboard-text no-underline transition-colors hover:text-dashboard-accent-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
426
+ href={link.url}
427
+ key={link.key}
442
428
  rel="noreferrer"
443
429
  target="_blank"
444
- title={link.kind === "object" ? link.title : resourceLinkTitle(link)}
430
+ title={[
431
+ link.label,
432
+ link.plugin,
433
+ linkStatusLabel(link.status),
434
+ link.description,
435
+ ]
436
+ .filter(Boolean)
437
+ .join(" · ")}
445
438
  >
446
439
  <ObjectIcon
447
- objectType={
448
- link.objectType ??
449
- props.detail?.sidebarAnnotations?.find(
450
- (annotation) => annotation.key === link.key,
451
- )?.objectType ??
452
- "item"
453
- }
454
- status={link.status}
455
- facts={link.kind === "object" ? link.facts : undefined}
440
+ facts={link.facts}
441
+ objectType={link.objectType}
456
442
  size={16}
443
+ status={link.status}
457
444
  />
458
445
  <span className="min-w-0 break-words">{link.label}</span>
459
446
  </a>
@@ -462,26 +449,6 @@ export function ConversationAnnotations(props: {
462
449
  );
463
450
  }
464
451
 
465
- function resourceLinkTitle(link: {
466
- description?: string;
467
- label: string;
468
- plugin: string;
469
- status?: ResourceLinkStatus;
470
- }): string {
471
- const statusLabel = link.status
472
- ? {
473
- open: "Open",
474
- draft: "Draft",
475
- closed: "Closed",
476
- merged: "Merged",
477
- warning: "Needs attention",
478
- }[link.status]
479
- : undefined;
480
- return [link.label, link.plugin, statusLabel, link.description]
481
- .filter(Boolean)
482
- .join(" · ");
483
- }
484
-
485
452
  function LocationLink(props: { label: string; locationUrl?: string }) {
486
453
  return props.locationUrl ? (
487
454
  <a