@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
@@ -0,0 +1,218 @@
1
+ import { Globe2, Lock, Trash2 } from "lucide-react";
2
+ import { useState, type ReactNode } from "react";
3
+
4
+ import { DetailsPanel } from "../../components/DetailsPanel";
5
+ import { PageContentSkeleton } from "../../components/PageContentSkeleton";
6
+ import {
7
+ AlertDialog,
8
+ AlertDialogAction,
9
+ AlertDialogCancel,
10
+ AlertDialogContent,
11
+ AlertDialogDescription,
12
+ AlertDialogFooter,
13
+ AlertDialogTitle,
14
+ } from "../../components/ui/alert-dialog";
15
+ import { Button } from "../../components/ui/button";
16
+ import {
17
+ Tooltip,
18
+ TooltipContent,
19
+ TooltipTrigger,
20
+ } from "../../components/ui/tooltip";
21
+ import { TranscriptText } from "../../conversations/TranscriptText";
22
+ import type {
23
+ PluginUserPageRecord,
24
+ PluginUserPageRecordAction,
25
+ } from "../user/pluginUserPageData";
26
+
27
+ // The memory plugin API does not resolve mentions yet, so memory text shows
28
+ // Slack ids instead of names.
29
+ const NO_MEMORY_MENTIONS: readonly [] = [];
30
+
31
+ // The heading, origin, and scope rows already show these values.
32
+ const SUMMARIZED_METADATA = new Set([
33
+ "Learned",
34
+ "Memory ID",
35
+ "Source",
36
+ "Type",
37
+ "Visibility",
38
+ ]);
39
+
40
+ /** Show one memory beside the library, or in a bottom sheet on small screens. */
41
+ export function MemoryDetails(props: {
42
+ forgetPending: boolean;
43
+ onClose(): void;
44
+ onForget(action: PluginUserPageRecordAction): void;
45
+ record: PluginUserPageRecord | undefined;
46
+ }) {
47
+ const { record } = props;
48
+ const [confirmOpen, setConfirmOpen] = useState(false);
49
+ const heading = record ? memoryHeading(record) : "Memory details";
50
+ const isPublic = record && metadataValue(record, "Visibility") === "Public";
51
+ const VisibilityIcon = isPublic ? Globe2 : Lock;
52
+ const forgetAction = record?.actions?.find(
53
+ (recordAction) => recordAction.tone === "danger",
54
+ );
55
+
56
+ return (
57
+ <DetailsPanel
58
+ label="Memory details"
59
+ onClose={props.onClose}
60
+ sheetTitle={heading}
61
+ title={
62
+ <h2 className="m-0 flex items-center gap-2 font-display text-base font-medium text-dashboard-text">
63
+ {record ? (
64
+ <VisibilityIcon
65
+ aria-hidden="true"
66
+ className="shrink-0 text-dashboard-text-muted"
67
+ size={14}
68
+ />
69
+ ) : null}
70
+ <span className="truncate">{heading}</span>
71
+ </h2>
72
+ }
73
+ >
74
+ <div className="grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto]">
75
+ <div
76
+ aria-label="Memory content"
77
+ role="region"
78
+ tabIndex={0}
79
+ className="grid min-h-0 min-w-0 content-start gap-4 overflow-y-auto overscroll-contain p-4 outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus [overflow-wrap:anywhere]"
80
+ >
81
+ {record ? (
82
+ <>
83
+ <div className="grid gap-3 text-sm leading-relaxed">
84
+ <TranscriptText
85
+ mentions={NO_MEMORY_MENTIONS}
86
+ text={record.title}
87
+ />
88
+ {record.description ? (
89
+ <TranscriptText
90
+ mentions={NO_MEMORY_MENTIONS}
91
+ text={record.description}
92
+ />
93
+ ) : null}
94
+ </div>
95
+ <MemoryFacts record={record} />
96
+ </>
97
+ ) : (
98
+ <PageContentSkeleton label="Loading memory" variant="panel" />
99
+ )}
100
+ </div>
101
+ {record && forgetAction ? (
102
+ // Keep the destructive action away from the title and close control.
103
+ <footer
104
+ aria-label="Memory actions"
105
+ className="flex items-center gap-2 border-t border-dashboard-border bg-dashboard-bg-elevated p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]"
106
+ >
107
+ <Tooltip>
108
+ <TooltipTrigger
109
+ render={
110
+ <Button
111
+ aria-label="Forget this memory"
112
+ className="min-h-11 min-w-11"
113
+ disabled={props.forgetPending}
114
+ onClick={() => {
115
+ if (forgetAction.confirmation) setConfirmOpen(true);
116
+ else props.onForget(forgetAction);
117
+ }}
118
+ size="icon"
119
+ variant="danger"
120
+ >
121
+ <Trash2 aria-hidden="true" size={16} />
122
+ </Button>
123
+ }
124
+ />
125
+ <TooltipContent size="compact">Forget this memory</TooltipContent>
126
+ </Tooltip>
127
+ </footer>
128
+ ) : null}
129
+ </div>
130
+ {forgetAction?.confirmation && record ? (
131
+ <AlertDialog onOpenChange={setConfirmOpen} open={confirmOpen}>
132
+ <AlertDialogContent>
133
+ <AlertDialogTitle>{forgetAction.confirmation}</AlertDialogTitle>
134
+ <AlertDialogDescription>{record.title}</AlertDialogDescription>
135
+ <AlertDialogFooter>
136
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
137
+ <AlertDialogAction
138
+ onClick={() => {
139
+ setConfirmOpen(false);
140
+ props.onForget(forgetAction);
141
+ }}
142
+ variant="danger"
143
+ >
144
+ {forgetAction.label}
145
+ </AlertDialogAction>
146
+ </AlertDialogFooter>
147
+ </AlertDialogContent>
148
+ </AlertDialog>
149
+ ) : null}
150
+ </DetailsPanel>
151
+ );
152
+ }
153
+
154
+ function MemoryFacts(props: { record: PluginUserPageRecord }) {
155
+ const { record } = props;
156
+ const learned = metadataValue(record, "Learned");
157
+ const source = metadataValue(record, "Source");
158
+ const visibility = metadataValue(record, "Visibility");
159
+ const isPublic = visibility === "Public";
160
+ const origin =
161
+ learned === "Automatic"
162
+ ? `From a ${source ?? "past"} conversation`
163
+ : learned === "Explicit"
164
+ ? isPublic
165
+ ? "Someone asked Junior"
166
+ : "You asked Junior"
167
+ : learned
168
+ ? "Before origin tracking"
169
+ : undefined;
170
+ const remembered = metadataValue(record, "Remembered");
171
+ // Most memories are saved when Junior sees them. Show one date in that case.
172
+ const dates = (record.metadata ?? []).filter(
173
+ (item) =>
174
+ !SUMMARIZED_METADATA.has(item.label) &&
175
+ !(item.label === "Observed" && item.value === remembered),
176
+ );
177
+ return (
178
+ <dl className="m-0 grid min-w-0 gap-2 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel p-3 text-xs leading-snug">
179
+ {origin ? <MemoryFact label="Learned">{origin}</MemoryFact> : null}
180
+ {visibility ? (
181
+ <MemoryFact label="Used in">
182
+ {isPublic ? "Future workspace channels" : "Your future conversations"}
183
+ </MemoryFact>
184
+ ) : null}
185
+ {dates.map((item) => (
186
+ <MemoryFact key={item.label} label={item.label}>
187
+ {item.value}
188
+ </MemoryFact>
189
+ ))}
190
+ </dl>
191
+ );
192
+ }
193
+
194
+ function MemoryFact(props: { children: ReactNode; label: string }) {
195
+ return (
196
+ <div className="flex min-w-0 items-baseline justify-between gap-4">
197
+ <dt className="shrink-0 text-dashboard-text-muted">{props.label}</dt>
198
+ <dd className="m-0 min-w-0 break-words text-right tabular-nums text-dashboard-text">
199
+ {props.children}
200
+ </dd>
201
+ </div>
202
+ );
203
+ }
204
+
205
+ /** Name a memory by visibility and type, such as "Private preference". */
206
+ function memoryHeading(record: PluginUserPageRecord): string {
207
+ const kind = metadataValue(record, "Type");
208
+ const visibility = metadataValue(record, "Visibility");
209
+ if (!kind) return visibility ? `${visibility} memory` : "Memory";
210
+ return visibility ? `${visibility} ${kind.toLowerCase()}` : kind;
211
+ }
212
+
213
+ function metadataValue(
214
+ record: PluginUserPageRecord,
215
+ label: string,
216
+ ): string | undefined {
217
+ return record.metadata?.find((item) => item.label === label)?.value;
218
+ }
@@ -8,7 +8,7 @@ import {
8
8
  Sparkles,
9
9
  UserRound,
10
10
  } from "lucide-react";
11
- import { useEffect, useState } from "react";
11
+ import { useEffect, useRef, useState } from "react";
12
12
  import { Navigate, useLocation, useNavigate, useParams } from "react-router";
13
13
  import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
14
14
 
@@ -25,8 +25,10 @@ import {
25
25
  } from "../../components/controls/TimeRangeSelector";
26
26
  import { Card } from "../../components/layout/Card";
27
27
  import { PageHeader } from "../../components/layout/PageHeader";
28
+ import { PageLayout } from "../../components/layout/PageLayout";
28
29
  import {
29
30
  type PluginUserPageRecord,
31
+ type PluginUserPageRecordAction,
30
32
  usePluginUserPageData,
31
33
  } from "../user/pluginUserPageData";
32
34
  import { pathWithSearch } from "../../searchParams";
@@ -36,7 +38,7 @@ import {
36
38
  useMemoryDashboardData,
37
39
  useMemorySearchCounts,
38
40
  } from "./memoryDashboard";
39
- import { MemoryDetailsDrawer } from "./MemoryDetailsDrawer";
41
+ import { MemoryDetails } from "./MemoryDetails";
40
42
  import { MemoryPageLayout } from "./MemoryPageLayout";
41
43
  import { MemorySearchHighlight } from "./MemorySearchHighlight";
42
44
  import { MemoryTimeline } from "./MemoryTimeline";
@@ -64,13 +66,16 @@ export function MemoryPage(props: { page: PluginUserPageLink }) {
64
66
 
65
67
  return (
66
68
  <MemoryPageLayout>
67
- <PageHeader
68
- description={props.page.description}
69
- {...(overview ? { onRangeChange: setRange, range } : {})}
70
- title={props.page.label}
71
- />
72
69
  {overview ? (
73
- <MemoryOverview range={range} />
70
+ <>
71
+ <PageHeader
72
+ description={props.page.description}
73
+ onRangeChange={setRange}
74
+ range={range}
75
+ title={props.page.label}
76
+ />
77
+ <MemoryOverview range={range} />
78
+ </>
74
79
  ) : (
75
80
  <MemoryLibrary libraryPath={libraryPath} page={props.page} />
76
81
  )}
@@ -161,11 +166,34 @@ function MemoryLibrary(props: {
161
166
  const navigate = useNavigate();
162
167
  const location = useLocation();
163
168
  const { memoryId } = useParams();
169
+ const listRef = useRef<HTMLDivElement>(null);
164
170
  const memoryQuery = useMemoryRecord(memoryId);
165
171
  const selectedRecord =
166
172
  memoryQuery.data ?? records.find((record) => record.id === memoryId);
167
173
  const memoryPath = (pathname: string) =>
168
174
  pathWithSearch(pathname, location.search);
175
+ const closeDetails = () => {
176
+ const trigger = memoryId
177
+ ? listRef.current?.querySelector<HTMLButtonElement>(
178
+ `button[data-memory-id="${CSS.escape(memoryId)}"]`,
179
+ )
180
+ : undefined;
181
+ navigate(memoryPath(props.libraryPath));
182
+ (trigger ?? listRef.current)?.focus();
183
+ };
184
+ const forget = (recordAction: PluginUserPageRecordAction) => {
185
+ // The forgotten row leaves the list, so focus the list instead.
186
+ navigate(memoryPath(props.libraryPath));
187
+ listRef.current?.focus();
188
+ runAction(recordAction);
189
+ };
190
+
191
+ useEffect(() => {
192
+ // Forget leaves the shared mutation in success or error. Clear it when
193
+ // another memory opens, so old action feedback does not stay visible.
194
+ if (memoryId) action.reset();
195
+ // oxlint-disable-next-line react/exhaustive-deps -- reset only on open id change
196
+ }, [memoryId]);
169
197
 
170
198
  useEffect(() => {
171
199
  // Drop stale permalinks after a forget/archive or unknown id once the
@@ -198,116 +226,143 @@ function MemoryLibrary(props: {
198
226
  const loading = !query.data && !query.error;
199
227
 
200
228
  return (
201
- <section className="grid gap-4" aria-labelledby="memory-library-title">
202
- <div className="flex flex-wrap items-end justify-between gap-4">
203
- <div>
204
- <h2
205
- className="m-0 font-display text-xl font-medium tracking-[-0.02em] text-dashboard-text"
206
- id="memory-library-title"
207
- >
208
- {searchQuery ? "Search results" : "What Junior remembers"}
209
- </h2>
210
- </div>
211
- {content?.searchPlaceholder ? (
212
- <SearchInput
213
- className="w-full max-w-md sm:w-auto"
214
- label={content.searchPlaceholder}
215
- onChange={setSearchText}
216
- placeholder={content.searchPlaceholder}
217
- size="default"
218
- value={searchText}
229
+ <div
230
+ className={cn(
231
+ "grid h-full min-h-0 min-w-0",
232
+ memoryId && "xl:grid-cols-[minmax(0,1fr)_25rem]",
233
+ )}
234
+ >
235
+ <div
236
+ aria-label="Memories list"
237
+ className="min-h-0 overflow-y-auto overscroll-contain outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
238
+ ref={listRef}
239
+ role="region"
240
+ tabIndex={0}
241
+ >
242
+ <PageLayout className="gap-6 sm:gap-8">
243
+ <PageHeader
244
+ description={props.page.description}
245
+ title={props.page.label}
219
246
  />
220
- ) : null}
221
- </div>
222
-
223
- <FilterTabList
224
- ariaLabel="Memory collections"
225
- items={[
226
- {
227
- count: counts?.active,
228
- label: "All",
229
- value: "",
230
- },
231
- {
232
- count: counts?.personal,
233
- label: "Private",
234
- value: "private",
235
- },
236
- {
237
- count: counts?.public,
238
- label: "Public",
239
- value: "public",
240
- },
241
- ]}
242
- onChange={setFilter}
243
- value={filter}
244
- />
247
+ <section
248
+ className="grid gap-4"
249
+ aria-labelledby="memory-library-title"
250
+ >
251
+ <div className="flex flex-wrap items-end justify-between gap-4">
252
+ <div>
253
+ <h2
254
+ className="m-0 font-display text-xl font-medium tracking-[-0.02em] text-dashboard-text"
255
+ id="memory-library-title"
256
+ >
257
+ {searchQuery ? "Search results" : "What Junior remembers"}
258
+ </h2>
259
+ </div>
260
+ {content?.searchPlaceholder ? (
261
+ <SearchInput
262
+ className="w-full max-w-md sm:w-auto"
263
+ label={content.searchPlaceholder}
264
+ onChange={setSearchText}
265
+ placeholder={content.searchPlaceholder}
266
+ size="default"
267
+ value={searchText}
268
+ />
269
+ ) : null}
270
+ </div>
245
271
 
246
- {loading ? (
247
- <PageContentSkeleton label="Loading memories" variant="panel" />
248
- ) : query.error ? (
249
- <Card className="flex items-center gap-3 border-rose-300/20 p-5 text-sm text-rose-200">
250
- <CircleAlert aria-hidden="true" size={18} />
251
- {query.error.message}
252
- </Card>
253
- ) : records.length === 0 ? (
254
- <Card className="grid min-h-56 place-items-center p-8 text-center">
255
- <div>
256
- <Database
257
- aria-hidden="true"
258
- className="mx-auto text-dashboard-text-muted"
259
- size={26}
272
+ <FilterTabList
273
+ ariaLabel="Memory collections"
274
+ items={[
275
+ {
276
+ count: counts?.active,
277
+ label: "All",
278
+ value: "",
279
+ },
280
+ {
281
+ count: counts?.personal,
282
+ label: "Private",
283
+ value: "private",
284
+ },
285
+ {
286
+ count: counts?.public,
287
+ label: "Public",
288
+ value: "public",
289
+ },
290
+ ]}
291
+ onChange={setFilter}
292
+ value={filter}
260
293
  />
261
- <p className="mt-4 mb-0 text-sm text-dashboard-text-muted">
262
- {content?.emptyText ?? "No memories yet."}
263
- </p>
264
- </div>
265
- </Card>
266
- ) : (
267
- <div className="grid gap-3">
268
- <Card padding="none">
269
- <MemoryListHeader />
270
- {records.map((record, index) => (
271
- <MemoryRow
272
- first={index === 0}
273
- key={record.id}
274
- onSelect={() =>
275
- navigate(
276
- memoryPath(
277
- memoryId === record.id
278
- ? props.libraryPath
279
- : `/memories/${encodeURIComponent(record.id)}`,
280
- ),
281
- )
282
- }
283
- record={record}
284
- searchQuery={searchQuery}
285
- selected={record.id === memoryId}
286
- />
287
- ))}
288
- </Card>
289
- <LoadMorePagination
290
- className="mt-2"
291
- hasMore={!query.isPlaceholderData && Boolean(query.hasNextPage)}
292
- loading={query.isFetchingNextPage}
293
- onLoadMore={() => void query.fetchNextPage()}
294
- />
295
- {action.error ? (
296
- <InlineError className="text-center">
297
- Could not complete this action. Try again.
298
- </InlineError>
299
- ) : null}
300
- </div>
301
- )}
302
- {!loading ? (
303
- <MemoryDetailsDrawer
304
- action={action}
305
- onAction={runAction}
306
- onClose={() => navigate(memoryPath(props.libraryPath))}
294
+
295
+ {loading ? (
296
+ <PageContentSkeleton label="Loading memories" variant="panel" />
297
+ ) : query.error ? (
298
+ <Card className="flex items-center gap-3 border-rose-300/20 p-5 text-sm text-rose-200">
299
+ <CircleAlert aria-hidden="true" size={18} />
300
+ {query.error.message}
301
+ </Card>
302
+ ) : records.length === 0 ? (
303
+ <Card className="grid min-h-56 place-items-center p-8 text-center">
304
+ <div>
305
+ <Database
306
+ aria-hidden="true"
307
+ className="mx-auto text-dashboard-text-muted"
308
+ size={26}
309
+ />
310
+ <p className="mt-4 mb-0 text-sm text-dashboard-text-muted">
311
+ {content?.emptyText ?? "No memories yet."}
312
+ </p>
313
+ </div>
314
+ </Card>
315
+ ) : (
316
+ <div className="grid gap-3">
317
+ <Card padding="none">
318
+ <MemoryListHeader />
319
+ {records.map((record, index) => (
320
+ <MemoryRow
321
+ first={index === 0}
322
+ key={record.id}
323
+ onSelect={() =>
324
+ navigate(
325
+ memoryPath(
326
+ memoryId === record.id
327
+ ? props.libraryPath
328
+ : `/memories/${encodeURIComponent(record.id)}`,
329
+ ),
330
+ )
331
+ }
332
+ record={record}
333
+ searchQuery={searchQuery}
334
+ selected={record.id === memoryId}
335
+ />
336
+ ))}
337
+ </Card>
338
+ <LoadMorePagination
339
+ className="mt-2"
340
+ hasMore={
341
+ !query.isPlaceholderData && Boolean(query.hasNextPage)
342
+ }
343
+ loading={query.isFetchingNextPage}
344
+ onLoadMore={() => void query.fetchNextPage()}
345
+ />
346
+ {action.error ? (
347
+ <InlineError className="text-center">
348
+ Could not complete this action. Try again.
349
+ </InlineError>
350
+ ) : null}
351
+ </div>
352
+ )}
353
+ </section>
354
+ </PageLayout>
355
+ </div>
356
+ {memoryId ? (
357
+ <MemoryDetails
358
+ forgetPending={action.isPending}
359
+ key={memoryId}
360
+ onClose={closeDetails}
361
+ onForget={forget}
307
362
  record={selectedRecord}
308
363
  />
309
364
  ) : null}
310
- </section>
365
+ </div>
311
366
  );
312
367
  }
313
368
 
@@ -500,6 +555,7 @@ function MemoryRow(props: {
500
555
  <button
501
556
  aria-expanded={props.selected}
502
557
  aria-label={`View memory details: ${props.record.title}`}
558
+ data-memory-id={props.record.id}
503
559
  className={cn(
504
560
  "grid min-w-0 flex-1 cursor-pointer items-center gap-3 border-0 bg-transparent px-4 py-3 text-left",
505
561
  MEMORY_GRID,
@@ -10,7 +10,8 @@ const MEMORY_BASE_PATH = "/memories";
10
10
  const MEMORY_LIBRARY_PATH = `${MEMORY_BASE_PATH}/library`;
11
11
  const RESERVED_MEMORY_SEGMENTS = new Set(["library"]);
12
12
 
13
- function isMemoriesLibraryPath(pathname: string): boolean {
13
+ /** Identify library and memory routes that keep the list beside its details. */
14
+ export function isMemoryLibraryPath(pathname: string): boolean {
14
15
  if (
15
16
  pathname === MEMORY_LIBRARY_PATH ||
16
17
  pathname.startsWith(`${MEMORY_LIBRARY_PATH}/`)
@@ -21,21 +22,34 @@ function isMemoriesLibraryPath(pathname: string): boolean {
21
22
  return Boolean(match?.[1] && !RESERVED_MEMORY_SEGMENTS.has(match[1]));
22
23
  }
23
24
 
24
- /** Place the shared secondary navigation above one Memories page. */
25
+ /**
26
+ * Place the shared secondary navigation above one Memories page. Library
27
+ * children fill the remaining height and own their scroll frames.
28
+ */
25
29
  export function MemoryPageLayout(props: { children: ReactNode }) {
30
+ const library = isMemoryLibraryPath(useLocation().pathname);
26
31
  return (
27
32
  <SecondaryNavigation
28
33
  ariaLabel="Memory navigation"
34
+ className={
35
+ library
36
+ ? "grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)] md:grid-rows-[minmax(0,1fr)]"
37
+ : undefined
38
+ }
29
39
  items={[
30
40
  { end: true, label: "Overview", to: MEMORY_BASE_PATH },
31
41
  {
32
- isActive: isMemoriesLibraryPath,
42
+ isActive: isMemoryLibraryPath,
33
43
  label: "Memories",
34
44
  to: MEMORY_LIBRARY_PATH,
35
45
  },
36
46
  ]}
37
47
  >
38
- <PageLayout className="gap-6 sm:gap-8">{props.children}</PageLayout>
48
+ {library ? (
49
+ <div className="min-h-0">{props.children}</div>
50
+ ) : (
51
+ <PageLayout className="gap-6 sm:gap-8">{props.children}</PageLayout>
52
+ )}
39
53
  </SecondaryNavigation>
40
54
  );
41
55
  }
@@ -46,10 +60,9 @@ export function MemoryRouteLoading(props: {
46
60
  label: string;
47
61
  title?: string;
48
62
  }) {
49
- const location = useLocation();
50
- const overview = location.pathname === MEMORY_BASE_PATH;
51
- return (
52
- <MemoryPageLayout>
63
+ const library = isMemoryLibraryPath(useLocation().pathname);
64
+ const content = (
65
+ <>
53
66
  <PageHeader
54
67
  description={
55
68
  props.description ??
@@ -59,8 +72,19 @@ export function MemoryRouteLoading(props: {
59
72
  />
60
73
  <PageContentSkeleton
61
74
  label={props.label}
62
- variant={overview ? "overview" : "library"}
75
+ variant={library ? "library" : "overview"}
63
76
  />
77
+ </>
78
+ );
79
+ return (
80
+ <MemoryPageLayout>
81
+ {library ? (
82
+ <div className="h-full overflow-y-auto">
83
+ <PageLayout className="gap-6 sm:gap-8">{content}</PageLayout>
84
+ </div>
85
+ ) : (
86
+ content
87
+ )}
64
88
  </MemoryPageLayout>
65
89
  );
66
90
  }