@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,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
+ }
@@ -16,13 +16,20 @@ import type { ConversationDetailReport } from "@sentry/junior/api/schema";
16
16
 
17
17
  import {
18
18
  formatConversationDuration,
19
+ formatRelativeTime,
20
+ formatTime,
19
21
  slackLocationLabel,
20
22
  summarizeCost,
21
23
  summarizeModelUsage,
22
24
  summarizeUsage,
23
25
  automationPath,
24
26
  } from "../format";
25
- import { Tooltip } from "../components/Tooltip";
27
+ import {
28
+ Tooltip,
29
+ TooltipContent,
30
+ TooltipTitle,
31
+ TooltipTrigger,
32
+ } from "../components/ui/tooltip";
26
33
  import { MetricList, type MetricListItem } from "../components/Metric";
27
34
  import { cn, dashboardInlineLinkClass } from "../styles";
28
35
  import {
@@ -37,6 +44,7 @@ import {
37
44
  type SidebarAnnotation,
38
45
  type SidebarAnnotationBadgeGroup,
39
46
  } from "./sidebarAnnotationBadges";
47
+ import { conversationLinks, linkStatusLabel } from "./ConversationLinkedWork";
40
48
 
41
49
  const MOBILE_MEDIA_QUERY = "(max-width: 767px)";
42
50
 
@@ -98,13 +106,6 @@ export function ConversationPrivacyChip(props: {
98
106
  );
99
107
  }
100
108
 
101
- type ResourceLinkStatus = NonNullable<
102
- Extract<
103
- NonNullable<ConversationDetailReport["annotations"]>[number],
104
- { kind: "resource_link" }
105
- >["status"]
106
- >;
107
-
108
109
  /** True when the conversation has at least one resource-link annotation. */
109
110
  export function hasConversationAnnotations(
110
111
  annotations: ConversationDetailReport["annotations"] | undefined,
@@ -134,9 +135,36 @@ export function ConversationSidebarAnnotations(props: {
134
135
  : [];
135
136
 
136
137
  return (
137
- <Tooltip
138
- align="left"
139
- 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>
140
168
  <ul className="m-0 list-none space-y-1 p-0">
141
169
  {annotations.map((annotation, index) => (
142
170
  <li
@@ -156,33 +184,7 @@ export function ConversationSidebarAnnotations(props: {
156
184
  </li>
157
185
  ))}
158
186
  </ul>
159
- }
160
- label="Linked work"
161
- triggerClassName="min-w-0"
162
- >
163
- <span
164
- aria-label={`Linked work, newest first: ${details.join(", ")}`}
165
- className="inline-flex min-w-0 max-w-full items-center"
166
- >
167
- {useMobileFacepile ? (
168
- <SidebarAnnotationIconFacepile
169
- annotations={mobileFacepileAnnotations}
170
- // Discs sit on the sidebar panel / row surface.
171
- cutoutColor="var(--color-dashboard-surface-panel)"
172
- />
173
- ) : (
174
- <span className="inline-flex min-w-0 items-center gap-1">
175
- {badges.labeledGroups.map((group) => (
176
- <SidebarAnnotationGroupChip group={group} key={group.label} />
177
- ))}
178
- {badges.overflowGroupCount > 0 ? (
179
- <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">
180
- +{badges.overflowGroupCount}
181
- </span>
182
- ) : null}
183
- </span>
184
- )}
185
- </span>
187
+ </TooltipContent>
186
188
  </Tooltip>
187
189
  );
188
190
  }
@@ -410,48 +412,35 @@ function SidebarAnnotationIcon(props: {
410
412
  );
411
413
  }
412
414
 
413
- /** Render resource-link annotations under the conversation title. */
415
+ /** Render linked work as a compact strip under the conversation title. */
414
416
  export function ConversationAnnotations(props: {
415
417
  detail: ConversationDetailReport | undefined;
416
- layout?: "list" | "strip";
417
418
  }) {
418
- const links =
419
- props.detail?.annotations?.filter(
420
- (annotation) => annotation.url !== null,
421
- ) ?? [];
419
+ const links = conversationLinks(props.detail);
422
420
  if (links.length === 0) return null;
423
421
  return (
424
- <div
425
- className={cn(
426
- "flex gap-x-4 gap-y-2",
427
- props.layout === "strip" ? "overflow-x-auto" : "flex-col",
428
- )}
429
- >
422
+ <div className="flex gap-x-4 gap-y-2 overflow-x-auto">
430
423
  {links.map((link) => (
431
424
  <a
432
- className={cn(
433
- "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",
434
- props.layout === "strip"
435
- ? "px-1 py-0.5 text-xs hover:text-dashboard-accent-text"
436
- : "min-h-10 border border-dashboard-border px-3 py-2 text-sm hover:border-dashboard-border-interactive hover:bg-dashboard-surface-hover",
437
- )}
438
- href={link.url ?? undefined}
439
- 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}
440
428
  rel="noreferrer"
441
429
  target="_blank"
442
- 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(" · ")}
443
438
  >
444
439
  <ObjectIcon
445
- objectType={
446
- link.objectType ??
447
- props.detail?.sidebarAnnotations?.find(
448
- (annotation) => annotation.key === link.key,
449
- )?.objectType ??
450
- "item"
451
- }
452
- status={link.status}
453
- facts={link.kind === "object" ? link.facts : undefined}
440
+ facts={link.facts}
441
+ objectType={link.objectType}
454
442
  size={16}
443
+ status={link.status}
455
444
  />
456
445
  <span className="min-w-0 break-words">{link.label}</span>
457
446
  </a>
@@ -460,26 +449,6 @@ export function ConversationAnnotations(props: {
460
449
  );
461
450
  }
462
451
 
463
- function resourceLinkTitle(link: {
464
- description?: string;
465
- label: string;
466
- plugin: string;
467
- status?: ResourceLinkStatus;
468
- }): string {
469
- const statusLabel = link.status
470
- ? {
471
- open: "Open",
472
- draft: "Draft",
473
- closed: "Closed",
474
- merged: "Merged",
475
- warning: "Needs attention",
476
- }[link.status]
477
- : undefined;
478
- return [link.label, link.plugin, statusLabel, link.description]
479
- .filter(Boolean)
480
- .join(" · ");
481
- }
482
-
483
452
  function LocationLink(props: { label: string; locationUrl?: string }) {
484
453
  return props.locationUrl ? (
485
454
  <a
@@ -547,7 +516,13 @@ export function hasConversationStats(props: {
547
516
  return conversationStatItems(props).length > 0;
548
517
  }
549
518
 
550
- /** Render runtime, source, token, and cost metadata under the conversation title. */
519
+ type ConversationStatItem = MetricListItem & { label: string };
520
+
521
+ /**
522
+ * Render runtime, source, token, and cost metadata. The compact variant is one
523
+ * line under the conversation title. The full variant is a labeled list for
524
+ * conversation details.
525
+ */
551
526
  export function ConversationStats(props: {
552
527
  conversation: Conversation | undefined;
553
528
  detail?: ConversationDetailReport;
@@ -556,6 +531,24 @@ export function ConversationStats(props: {
556
531
  const stats = conversationStatItems(props);
557
532
  if (stats.length === 0) return null;
558
533
 
534
+ if ((props.variant ?? "full") === "full") {
535
+ return (
536
+ <dl className="m-0 grid min-w-0 gap-2 text-xs leading-snug">
537
+ {stats.map((item) => (
538
+ <div
539
+ className="flex min-w-0 items-baseline justify-between gap-4"
540
+ key={item.key}
541
+ >
542
+ <dt className="shrink-0 text-dashboard-text-muted">{item.label}</dt>
543
+ <dd className="m-0 min-w-0 break-words text-right tabular-nums text-dashboard-text">
544
+ {item.content}
545
+ </dd>
546
+ </div>
547
+ ))}
548
+ </dl>
549
+ );
550
+ }
551
+
559
552
  return (
560
553
  <MetricList
561
554
  className="break-words text-xs leading-[1.45] text-dashboard-text-muted"
@@ -568,7 +561,7 @@ function conversationStatItems(props: {
568
561
  conversation: Conversation | undefined;
569
562
  detail?: ConversationDetailReport;
570
563
  variant?: "compact" | "full";
571
- }): MetricListItem[] {
564
+ }): ConversationStatItem[] {
572
565
  if (!props.conversation) return [];
573
566
  const variant = props.variant ?? "full";
574
567
  const completeDetail = props.detail?.previousCursor
@@ -590,17 +583,37 @@ function conversationStatItems(props: {
590
583
  const sourceTask = props.detail?.sourceTask;
591
584
  const childAgentTotals =
592
585
  props.detail?.childAgentTotals ?? props.conversation.childAgentTotals;
593
- const rawStats: Array<MetricListItem | undefined> = [
586
+ const lastActivityAt = props.conversation.lastSeenAt;
587
+ const rawStats: Array<ConversationStatItem | undefined> = [
594
588
  location
595
589
  ? {
596
590
  content: <LocationLink label={location} locationUrl={locationUrl} />,
597
591
  key: "location",
592
+ label: "Location",
598
593
  }
599
594
  : undefined,
600
595
  variant === "full" && sourceTask
601
596
  ? {
602
597
  content: <SourceTask sourceTask={sourceTask} />,
603
598
  key: "source-task",
599
+ label: "Source",
600
+ }
601
+ : undefined,
602
+ variant === "full" && lastActivityAt
603
+ ? {
604
+ content: (
605
+ <time
606
+ dateTime={lastActivityAt}
607
+ title={formatTime(lastActivityAt, {
608
+ dateStyle: "medium",
609
+ timeStyle: "short",
610
+ })}
611
+ >
612
+ {formatRelativeTime(lastActivityAt)}
613
+ </time>
614
+ ),
615
+ key: "last-activity",
616
+ label: "Last activity",
604
617
  }
605
618
  : undefined,
606
619
  durationLabel !== "none"
@@ -615,6 +628,7 @@ function conversationStatItems(props: {
615
628
  />
616
629
  ),
617
630
  key: "duration",
631
+ label: "Runtime",
618
632
  }
619
633
  : undefined,
620
634
  tokenSummary
@@ -637,6 +651,7 @@ function conversationStatItems(props: {
637
651
  />
638
652
  ),
639
653
  key: "tokens",
654
+ label: "Tokens",
640
655
  }
641
656
  : undefined,
642
657
  costSummary ||
@@ -659,14 +674,18 @@ function conversationStatItems(props: {
659
674
  />
660
675
  ),
661
676
  key: "cost",
677
+ label: "Cost",
662
678
  }
663
679
  : undefined,
664
680
  childAgentTotals
665
681
  ? {
666
682
  content: <SubagentsMetric childAgentTotals={childAgentTotals} />,
667
683
  key: "subagents",
684
+ label: "Subagents",
668
685
  }
669
686
  : undefined,
670
687
  ];
671
- return rawStats.filter((item): item is MetricListItem => item !== undefined);
688
+ return rawStats.filter(
689
+ (item): item is ConversationStatItem => item !== undefined,
690
+ );
672
691
  }