@sentry/junior-dashboard 0.277.0 → 0.278.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/app.js +30 -41
  2. package/dist/client.js +90 -90
  3. package/dist/index.js +30 -41
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +5 -4
  6. package/src/app.ts +1 -1
  7. package/src/client/App.tsx +9 -4
  8. package/src/client/components/DetailsPanel.tsx +1 -1
  9. package/src/client/components/ui/resizable.tsx +50 -0
  10. package/src/client/conversations/AutoArchiveToggle.tsx +50 -14
  11. package/src/client/conversations/ConversationHeader.tsx +6 -2
  12. package/src/client/conversations/ConversationHeaderActions.tsx +4 -0
  13. package/src/client/conversations/ConversationMeta.tsx +2 -7
  14. package/src/client/conversations/ConversationPage.tsx +155 -72
  15. package/src/client/conversations/ConversationSidebar.tsx +2 -1
  16. package/src/client/conversations/ConversationTranscript.tsx +11 -8
  17. package/src/client/conversations/ConversationWorkspace.tsx +129 -41
  18. package/src/client/conversations/ConversationWorkspaceLoading.tsx +17 -3
  19. package/src/client/conversations/EventDetails.tsx +2 -62
  20. package/src/client/conversations/TranscriptActivityGroup.tsx +36 -14
  21. package/src/client/conversations/TranscriptContextEventView.tsx +7 -18
  22. package/src/client/conversations/TranscriptRailEvent.tsx +0 -8
  23. package/src/client/conversations/TranscriptView.tsx +89 -20
  24. package/src/client/conversations/conversationSidebarLayout.ts +61 -0
  25. package/src/client/conversations/eventLog.ts +2 -31
  26. package/src/client/conversations/eventTranscript.ts +9 -42
  27. package/src/client/conversations/queries.ts +76 -15
  28. package/src/client/conversations/transcript.ts +46 -16
  29. package/src/client/conversations/transcriptBottomPinning.ts +40 -24
  30. package/src/client/conversations/transcriptReadPosition.ts +31 -7
  31. package/src/client/conversations/transcriptRenderModel.ts +1 -14
  32. package/src/client/conversations/transcriptSearch.tsx +3 -11
  33. package/src/client/conversations/transcriptUnreadHistory.ts +248 -0
  34. package/src/client/http.ts +5 -5
  35. package/src/client/markdownExport.ts +32 -42
  36. package/src/client/pages/memory/MemoryDetails.tsx +6 -33
  37. package/src/client/pages/memory/MemoryPage.tsx +7 -92
  38. package/src/client/pages/memory/MemoryTimeline.tsx +16 -32
  39. package/src/client/pages/memory/memoryDashboard.ts +2 -38
  40. package/src/client/pages/memory/memoryRecord.ts +1 -6
  41. package/src/client/types.ts +18 -33
  42. package/src/conversation-cache.ts +6 -8
  43. package/src/mock-reporting/fixtures.ts +23 -33
  44. package/src/mock-reporting/routes.ts +3 -1
  45. package/src/tailwind.css +30 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Open a long Conversation at the reader's read position.
3
+ *
4
+ * The detail holds the latest page of events. When the read position is
5
+ * older than that page, the transcript starts with the page around the read
6
+ * position instead: the last read events and the unread events after them.
7
+ * The first detail fetch of a visit brings that page, so it needs no second
8
+ * request.
9
+ * Like earlier pages, newer pages load when the reader scrolls near them, so
10
+ * the server reads only the pages that the reader reaches.
11
+ *
12
+ * The latest page joins the transcript only after the newer pages reach it,
13
+ * so the transcript never has a gap. Until then the transcript ends with a
14
+ * row that loads the next newer page. A reader who jumps to the latest skips
15
+ * the newer pages: the transcript shows the latest page from the detail at
16
+ * once, and earlier pages load above it as the reader scrolls up. When a
17
+ * poll skips past the newer pages after they join the latest page, or past a
18
+ * newer page that has nothing after it, the transcript shows the latest page
19
+ * in the same way, so it never drops the skipped events.
20
+ */
21
+ import { useEffect, useMemo, useRef } from "react";
22
+ import { useInfiniteQuery } from "@tanstack/react-query";
23
+ import {
24
+ conversationEventPageSchema,
25
+ type ConversationDetailReport,
26
+ type ConversationEventPage,
27
+ type ConversationReportEvent,
28
+ } from "@sentry/junior/api/schema";
29
+
30
+ import { fetchDashboardJson } from "../http";
31
+ import { readConversationEvents } from "./queries";
32
+ import {
33
+ buildUnreadConversationTranscript,
34
+ conversationHistoryVersion,
35
+ } from "./transcript";
36
+
37
+ /** The transcript to show and how to load more of it. */
38
+ export type TranscriptHistory = {
39
+ data: ConversationDetailReport | undefined;
40
+ hasPreviousPage: boolean;
41
+ historyError: Error | null;
42
+ historyVersion: string;
43
+ isLoadingPreviousPage: boolean;
44
+ loadPreviousPage: () => void;
45
+ /** Set while newer pages have not reached the latest page. */
46
+ newer?: {
47
+ error: Error | null;
48
+ /** Load the next newer page, or retry a failed one. */
49
+ load: () => void;
50
+ loading: boolean;
51
+ /** Skip the newer pages and show the latest page. */
52
+ showLatest: () => void;
53
+ };
54
+ };
55
+
56
+ /**
57
+ * Load the transcript from `unreadCursor`. Returns `undefined` when there is
58
+ * no cursor, or when the first page fails, so the caller shows the latest
59
+ * page instead.
60
+ */
61
+ export function useUnreadHistory(input: {
62
+ conversationId: string;
63
+ /** The latest page, from the polled detail. */
64
+ detail: ConversationDetailReport | undefined;
65
+ /** `unreadCursor` from the detail when the Conversation opened. */
66
+ unreadCursor: string | undefined;
67
+ /** Skip the newer pages. The caller then passes no `unreadCursor`. */
68
+ onShowLatest: () => void;
69
+ /** The page around `unreadCursor`, when the detail brought it. */
70
+ unreadPage?: ConversationEventPage;
71
+ }): (TranscriptHistory & { isPending: boolean }) | undefined {
72
+ const { conversationId, detail, onShowLatest, unreadCursor, unreadPage } =
73
+ input;
74
+ const latestEventsRef = useRef<ConversationReportEvent[]>([]);
75
+ const latestEvents = useMemo(() => {
76
+ const kept = keepLatestEvents(latestEventsRef.current, detail?.events);
77
+ latestEventsRef.current = kept;
78
+ return kept;
79
+ }, [detail?.events]);
80
+ const latestFirstSeq = latestEvents[0]?.seq;
81
+ const joinedRef = useRef(false);
82
+ const newer = useInfiniteQuery({
83
+ // Only the page around the read position loads by itself. The data never
84
+ // goes stale, so a refetch cannot reload every newer page at once.
85
+ enabled: Boolean(unreadCursor),
86
+ staleTime: Infinity,
87
+ // Each visit starts again from the page around the read position. Pages
88
+ // from an earlier visit can end at an old latest event, which would
89
+ // leave a gap before the latest page.
90
+ gcTime: 0,
91
+ queryKey: ["conversation", conversationId, "unread", unreadCursor, "newer"],
92
+ queryFn: ({ pageParam, signal }) =>
93
+ readConversationEventsFrom(conversationId, pageParam, signal),
94
+ initialPageParam: { around: unreadCursor ?? "" } as NewerPageParam,
95
+ // The page that came with the detail. Without it, the query reads it.
96
+ ...(unreadPage
97
+ ? {
98
+ initialData: {
99
+ pageParams: [{ around: unreadCursor ?? "" }],
100
+ pages: [unreadPage],
101
+ },
102
+ }
103
+ : undefined),
104
+ getNextPageParam: (page): NewerPageParam | undefined =>
105
+ reachesLatest(page, latestFirstSeq) || !page.nextCursor
106
+ ? undefined
107
+ : { after: page.nextCursor },
108
+ retry: false,
109
+ });
110
+ const firstNewerPage = newer.data?.pages[0];
111
+ const earlier = useInfiniteQuery({
112
+ enabled: false,
113
+ queryKey: [
114
+ "conversation",
115
+ conversationId,
116
+ "unread",
117
+ unreadCursor,
118
+ "earlier",
119
+ ],
120
+ queryFn: ({ pageParam, signal }) =>
121
+ readConversationEvents(conversationId, pageParam, signal),
122
+ initialPageParam: firstNewerPage?.previousCursor ?? "",
123
+ getNextPageParam: (page) => page.previousCursor,
124
+ retry: false,
125
+ });
126
+
127
+ const newerPages = newer.data?.pages;
128
+ const earlierPages = earlier.data?.pages;
129
+ const lastNewerPage = newerPages?.at(-1);
130
+ const reached = lastNewerPage
131
+ ? reachesLatest(lastNewerPage, latestFirstSeq)
132
+ : false;
133
+ if (reached) joinedRef.current = true;
134
+ // No newer page can fill the gap before the latest page, or the latest
135
+ // events already on screen would leave until one does.
136
+ const skipped =
137
+ lastNewerPage !== undefined &&
138
+ !reached &&
139
+ (joinedRef.current || !lastNewerPage.nextCursor);
140
+ useEffect(() => {
141
+ if (skipped) onShowLatest();
142
+ }, [onShowLatest, skipped]);
143
+ const pages = useMemo(
144
+ () => [...(earlierPages ?? []), ...(newerPages ?? [])],
145
+ [earlierPages, newerPages],
146
+ );
147
+ const data = useMemo(
148
+ () =>
149
+ detail && newerPages
150
+ ? buildUnreadConversationTranscript(
151
+ detail,
152
+ pages,
153
+ reached ? latestEvents : [],
154
+ )
155
+ : undefined,
156
+ [detail, latestEvents, newerPages, pages, reached],
157
+ );
158
+
159
+ if (!unreadCursor || skipped || (newer.error && !newerPages)) {
160
+ return undefined;
161
+ }
162
+
163
+ const hasPreviousPage = earlier.data
164
+ ? earlier.hasNextPage
165
+ : Boolean(firstNewerPage?.previousCursor);
166
+ // `cancelRefetch: false` lets a second call join the page read in flight.
167
+ // By default it cancels that read and starts the same read again.
168
+ return {
169
+ data,
170
+ hasPreviousPage,
171
+ historyError: earlier.error,
172
+ historyVersion: conversationHistoryVersion(pages),
173
+ isLoadingPreviousPage: earlier.isFetchingNextPage,
174
+ isPending: !newerPages,
175
+ loadPreviousPage: () => {
176
+ if (hasPreviousPage) void earlier.fetchNextPage({ cancelRefetch: false });
177
+ },
178
+ ...(reached
179
+ ? undefined
180
+ : {
181
+ newer: {
182
+ error: newer.error,
183
+ load: () => {
184
+ if (newer.hasNextPage) {
185
+ void newer.fetchNextPage({ cancelRefetch: false });
186
+ }
187
+ },
188
+ loading: newer.isFetchingNextPage,
189
+ showLatest: onShowLatest,
190
+ },
191
+ }),
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Keep the events that new events push out of the latest page. Without them,
197
+ * the latest page would start after the newer pages again, and the transcript
198
+ * would drop its latest events until one more page loads. A poll that skips
199
+ * past the kept events starts over from its own events.
200
+ */
201
+ function keepLatestEvents(
202
+ kept: ConversationReportEvent[],
203
+ events: ConversationReportEvent[] | undefined,
204
+ ): ConversationReportEvent[] {
205
+ const firstSeq = events?.[0]?.seq;
206
+ const keptLastSeq = kept.at(-1)?.seq;
207
+ if (!events || firstSeq === undefined) return kept;
208
+ if (keptLastSeq === undefined || firstSeq > keptLastSeq + 1) return events;
209
+ const slid = kept.filter((event) => event.seq < firstSeq);
210
+ return slid.length ? [...slid, ...events] : events;
211
+ }
212
+
213
+ /**
214
+ * The newer pages reach the latest page when the latest page starts right
215
+ * after them. Without latest events, they reach it when nothing follows them.
216
+ */
217
+ function reachesLatest(
218
+ page: ConversationEventPage,
219
+ latestFirstSeq: number | undefined,
220
+ ): boolean {
221
+ const lastSeq = page.events.at(-1)?.seq;
222
+ if (lastSeq === undefined || latestFirstSeq === undefined) {
223
+ return !page.nextCursor;
224
+ }
225
+ return lastSeq + 1 >= latestFirstSeq;
226
+ }
227
+
228
+ /** The first page is around the read position. The rest follow it. */
229
+ type NewerPageParam = { around: string } | { after: string };
230
+
231
+ /** Read one bounded page of events around or after a history cursor. */
232
+ function readConversationEventsFrom(
233
+ conversationId: string,
234
+ param: NewerPageParam,
235
+ signal?: AbortSignal,
236
+ ): Promise<ConversationEventPage> {
237
+ const query = new URLSearchParams(param);
238
+ return fetchDashboardJson(
239
+ conversationEventPageSchema,
240
+ `/api/conversations/${encodeURIComponent(conversationId)}/events?${query}`,
241
+ signal,
242
+ ).then((page) => {
243
+ if ("after" in param && page.nextCursor === param.after) {
244
+ throw new Error("Conversation history cursor did not advance");
245
+ }
246
+ return page;
247
+ });
248
+ }
@@ -174,21 +174,21 @@ export async function fetchDashboardJson<T>(
174
174
  return schema.parse(await response.json());
175
175
  }
176
176
 
177
- /** Read an authenticated response, allowing 304 only for an explicit validator. */
177
+ /** Read an authenticated response, allowing 304 only for explicit validator headers. */
178
178
  export async function readDashboardResponse(
179
179
  path: string,
180
180
  signal?: AbortSignal,
181
- etag?: string,
181
+ validator?: Record<string, string>,
182
182
  ): Promise<Response> {
183
183
  const response = await fetchDashboard(path, {
184
184
  credentials: "same-origin",
185
185
  ...(signal ? { signal } : undefined),
186
- ...(etag
187
- ? { cache: "no-store" as const, headers: { "if-none-match": etag } }
186
+ ...(validator
187
+ ? { cache: "no-store" as const, headers: validator }
188
188
  : undefined),
189
189
  });
190
190
  if (response.status === 401) restartDashboardSignIn();
191
- if (!response.ok && !(etag && response.status === 304)) {
191
+ if (!response.ok && !(validator && response.status === 304)) {
192
192
  await throwDashboardApiError(path, response);
193
193
  }
194
194
  return response;
@@ -247,53 +247,43 @@ function appendContextEvent(
247
247
  timestamp: number | undefined,
248
248
  ): void {
249
249
  const event = part.event;
250
- lines.push(
251
- "",
252
- event.type === "handoff" ? "### Model handoff" : "### Context compacted",
253
- );
250
+ lines.push("", "### Context compacted");
254
251
  addEventMeta(lines, conversationTranscript, timestamp);
255
- if (event.type === "handoff") {
256
- addMetaLine(lines, "Model", event.modelId);
257
- addMetaLine(lines, "Profile", event.modelProfile);
258
- addMetaLine(lines, "Reasoning", event.reasoningLevel);
259
- } else {
260
- addMetaLine(lines, "Profile", event.modelProfile);
261
- addMetaLine(lines, "Model", event.modelId);
262
- if (event.details) {
263
- addMetaLine(
264
- lines,
265
- "Estimated input tokens",
266
- String(event.details.estimatedInputTokens),
267
- );
268
- if (event.details.replacementInputTokens !== undefined) {
269
- addMetaLine(
270
- lines,
271
- "Replacement input tokens",
272
- String(event.details.replacementInputTokens),
273
- );
274
- }
275
- addMetaLine(
276
- lines,
277
- "Compaction trigger",
278
- String(event.details.triggerTokens),
279
- );
280
- addMetaLine(lines, "Input limit", String(event.details.inputLimitTokens));
281
- addMetaLine(
282
- lines,
283
- "Input messages",
284
- String(event.details.inputMessageCount),
285
- );
286
- addMetaLine(
287
- lines,
288
- "Retained messages",
289
- String(event.details.retainedMessageCount),
290
- );
252
+ addMetaLine(lines, "Model", event.modelId);
253
+ if (event.details) {
254
+ addMetaLine(
255
+ lines,
256
+ "Estimated input tokens",
257
+ String(event.details.estimatedInputTokens),
258
+ );
259
+ if (event.details.replacementInputTokens !== undefined) {
291
260
  addMetaLine(
292
261
  lines,
293
- "Summary characters",
294
- String(event.details.summaryChars),
262
+ "Replacement input tokens",
263
+ String(event.details.replacementInputTokens),
295
264
  );
296
265
  }
266
+ addMetaLine(
267
+ lines,
268
+ "Compaction trigger",
269
+ String(event.details.triggerTokens),
270
+ );
271
+ addMetaLine(lines, "Input limit", String(event.details.inputLimitTokens));
272
+ addMetaLine(
273
+ lines,
274
+ "Input messages",
275
+ String(event.details.inputMessageCount),
276
+ );
277
+ addMetaLine(
278
+ lines,
279
+ "Retained messages",
280
+ String(event.details.retainedMessageCount),
281
+ );
282
+ addMetaLine(
283
+ lines,
284
+ "Summary characters",
285
+ String(event.details.summaryChars),
286
+ );
297
287
  }
298
288
  if (event.summary) {
299
289
  lines.push("", "#### Continuation summary", "", event.summary);
@@ -1,4 +1,4 @@
1
- import { Globe2, Lock, Trash2 } from "lucide-react";
1
+ import { Trash2 } from "lucide-react";
2
2
  import { useState, type ReactNode } from "react";
3
3
 
4
4
  import { DetailsPanel } from "../../components/DetailsPanel";
@@ -24,14 +24,8 @@ import type {
24
24
  PluginUserPageRecordAction,
25
25
  } from "../user/pluginUserPageData";
26
26
 
27
- // The heading, origin, and scope rows already show these values.
28
- const SUMMARIZED_METADATA = new Set([
29
- "Learned",
30
- "Memory ID",
31
- "Source",
32
- "Type",
33
- "Visibility",
34
- ]);
27
+ // The heading and origin rows already show these values.
28
+ const SUMMARIZED_METADATA = new Set(["Learned", "Memory ID", "Source", "Type"]);
35
29
 
36
30
  /** Show one memory beside the library, or in a bottom sheet on small screens. */
37
31
  export function MemoryDetails(props: {
@@ -43,8 +37,6 @@ export function MemoryDetails(props: {
43
37
  const { record } = props;
44
38
  const [confirmOpen, setConfirmOpen] = useState(false);
45
39
  const heading = record ? memoryHeading(record) : "Memory details";
46
- const isPublic = record && metadataValue(record, "Visibility") === "Public";
47
- const VisibilityIcon = isPublic ? Globe2 : Lock;
48
40
  const forgetAction = record?.actions?.find(
49
41
  (recordAction) => recordAction.tone === "danger",
50
42
  );
@@ -56,13 +48,6 @@ export function MemoryDetails(props: {
56
48
  sheetTitle={heading}
57
49
  title={
58
50
  <h2 className="m-0 flex items-center gap-2 font-display text-base font-medium text-dashboard-text">
59
- {record ? (
60
- <VisibilityIcon
61
- aria-hidden="true"
62
- className="shrink-0 text-dashboard-text-muted"
63
- size={14}
64
- />
65
- ) : null}
66
51
  <span className="truncate">{heading}</span>
67
52
  </h2>
68
53
  }
@@ -147,15 +132,11 @@ function MemoryFacts(props: { record: PluginUserPageRecord }) {
147
132
  const { record } = props;
148
133
  const learned = metadataValue(record, "Learned");
149
134
  const source = metadataValue(record, "Source");
150
- const visibility = metadataValue(record, "Visibility");
151
- const isPublic = visibility === "Public";
152
135
  const origin =
153
136
  learned === "Automatic"
154
137
  ? `From a ${source ?? "past"} conversation`
155
138
  : learned === "Explicit"
156
- ? isPublic
157
- ? "Someone asked Junior"
158
- : "You asked Junior"
139
+ ? "Someone asked Junior"
159
140
  : learned
160
141
  ? "Before origin tracking"
161
142
  : undefined;
@@ -169,11 +150,6 @@ function MemoryFacts(props: { record: PluginUserPageRecord }) {
169
150
  return (
170
151
  <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">
171
152
  {origin ? <MemoryFact label="Learned">{origin}</MemoryFact> : null}
172
- {visibility ? (
173
- <MemoryFact label="Used in">
174
- {isPublic ? "Future workspace channels" : "Your future conversations"}
175
- </MemoryFact>
176
- ) : null}
177
153
  {dates.map((item) => (
178
154
  <MemoryFact key={item.label} label={item.label}>
179
155
  {item.value}
@@ -194,12 +170,9 @@ function MemoryFact(props: { children: ReactNode; label: string }) {
194
170
  );
195
171
  }
196
172
 
197
- /** Name a memory by visibility and type, such as "Private preference". */
173
+ /** Name a memory by type, such as "Preference". */
198
174
  function memoryHeading(record: PluginUserPageRecord): string {
199
- const kind = metadataValue(record, "Type");
200
- const visibility = metadataValue(record, "Visibility");
201
- if (!kind) return visibility ? `${visibility} memory` : "Memory";
202
- return visibility ? `${visibility} ${kind.toLowerCase()}` : kind;
175
+ return metadataValue(record, "Type") ?? "Memory";
203
176
  }
204
177
 
205
178
  function metadataValue(
@@ -3,8 +3,6 @@ import {
3
3
  BrainCircuit,
4
4
  CircleAlert,
5
5
  Database,
6
- Globe2,
7
- Lock,
8
6
  Sparkles,
9
7
  UserRound,
10
8
  } from "lucide-react";
@@ -12,7 +10,6 @@ import { useEffect, useRef, useState } from "react";
12
10
  import { Navigate, useLocation, useNavigate, useParams } from "react-router";
13
11
  import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
14
12
 
15
- import { FilterTabList } from "../../components/FilterBar";
16
13
  import { InlineError } from "../../components/InlineError";
17
14
  import { PageContentSkeleton } from "../../components/PageContentSkeleton";
18
15
  import { LoadMorePagination } from "../../components/Pagination";
@@ -36,7 +33,6 @@ import { cn } from "../../styles";
36
33
  import {
37
34
  type MemoryDashboardData,
38
35
  useMemoryDashboardData,
39
- useMemorySearchCounts,
40
36
  } from "./memoryDashboard";
41
37
  import { MemoryDetails } from "./MemoryDetails";
42
38
  import { MemoryPageLayout } from "./MemoryPageLayout";
@@ -47,11 +43,9 @@ import { useMemoryRecord } from "./memoryRecord";
47
43
 
48
44
  /**
49
45
  * Leading memory column flexes; type and date columns stay equal fixed
50
- * widths on `sm` and up. Below `sm` those columns are hidden, so the grid
51
- * collapses to just the label and trailing visibility icon.
46
+ * widths on `sm` and up. Below `sm` those columns are hidden.
52
47
  */
53
- const MEMORY_GRID =
54
- "grid-cols-[minmax(0,1fr)_auto] sm:grid-cols-[minmax(0,1fr)_7rem_9rem_auto]";
48
+ const MEMORY_GRID = "grid-cols-1 sm:grid-cols-[minmax(0,1fr)_7rem_9rem]";
55
49
 
56
50
  /** Render the temporary first-class dashboard experience for memory. */
57
51
  export function MemoryPage(props: { page: PluginUserPageLink }) {
@@ -111,7 +105,7 @@ function MemoryOverview(props: { range: TimeRangeDays }) {
111
105
  days: dashboardQuery.data.days,
112
106
  hours: dashboardQuery.data.hours,
113
107
  range: props.range,
114
- emptySixHour: (date) => ({ date, personal: 0, public: 0 }),
108
+ emptySixHour: (date) => ({ count: 0, date }),
115
109
  })}
116
110
  range={props.range}
117
111
  />
@@ -148,21 +142,13 @@ function MemoryLibrary(props: {
148
142
  const {
149
143
  action,
150
144
  content,
151
- filter,
152
145
  query,
153
146
  records,
154
147
  runAction,
155
148
  searchQuery,
156
149
  searchText,
157
- setFilter,
158
150
  setSearchText,
159
151
  } = usePluginUserPageData(props.page);
160
- const dashboardQuery = useMemoryDashboardData();
161
- const searchCountsQuery = useMemorySearchCounts(searchQuery);
162
- // Tab totals follow the search. Without a search, the summary owns them.
163
- const counts = searchQuery
164
- ? searchCountsQuery.data
165
- : dashboardQuery.data?.stats;
166
152
  const navigate = useNavigate();
167
153
  const location = useLocation();
168
154
  const { memoryId } = useParams();
@@ -269,29 +255,6 @@ function MemoryLibrary(props: {
269
255
  ) : null}
270
256
  </div>
271
257
 
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}
293
- />
294
-
295
258
  {loading ? (
296
259
  <PageContentSkeleton label="Loading memories" variant="panel" />
297
260
  ) : query.error ? (
@@ -378,7 +341,6 @@ function MemoryListHeader() {
378
341
  <div>Memory</div>
379
342
  <div>Type</div>
380
343
  <div>Learned</div>
381
- <div className="sr-only">Visibility</div>
382
344
  </div>
383
345
  );
384
346
  }
@@ -387,20 +349,10 @@ function MemorySummary(props: { data: MemoryDashboardData }) {
387
349
  const { stats } = props.data;
388
350
  const items = [
389
351
  {
390
- detail: "personal + public",
352
+ detail: "shared across the workspace",
391
353
  label: "Total active",
392
- value: stats.active.toLocaleString("en-US"),
393
- },
394
- {
395
- detail: "private to you",
396
- label: "Personal",
397
- value: stats.personal.toLocaleString("en-US"),
398
- },
399
- {
400
- detail: "workspace shareable",
401
- label: "Public",
402
354
  tone: "text-dashboard-accent-text",
403
- value: stats.public.toLocaleString("en-US"),
355
+ value: stats.active.toLocaleString("en-US"),
404
356
  },
405
357
  {
406
358
  detail: "in the last 30 days",
@@ -411,7 +363,7 @@ function MemorySummary(props: { data: MemoryDashboardData }) {
411
363
  return (
412
364
  <section
413
365
  aria-label="Memory summary"
414
- className="grid grid-cols-2 gap-px border-y border-dashboard-border-subtle bg-dashboard-fill-hover lg:grid-cols-4"
366
+ className="grid grid-cols-2 gap-px border-y border-dashboard-border-subtle bg-dashboard-fill-hover"
415
367
  >
416
368
  {items.map((item) => (
417
369
  <div className="bg-dashboard-bg px-4 py-4 sm:px-5" key={item.label}>
@@ -442,12 +394,9 @@ function MemoryKindPanel(props: { data: MemoryDashboardData }) {
442
394
  <h2 className="m-0 font-display text-xl font-medium text-dashboard-text">
443
395
  What Junior remembers
444
396
  </h2>
445
- <p className="mt-1 mb-0 font-sans text-xs leading-relaxed text-dashboard-text-muted">
446
- Across personal and public scopes.
447
- </p>
448
397
  <div className="mt-5 grid gap-px overflow-hidden rounded-md border border-dashboard-border-subtle bg-dashboard-fill-hover">
449
398
  <OverviewBreakdownRow
450
- detail="Almost entirely personal"
399
+ detail="How people like to work"
451
400
  icon={UserRound}
452
401
  label="Preferences"
453
402
  value={stats.preference}
@@ -541,8 +490,6 @@ function MemoryRow(props: {
541
490
  const kind = metadataValue(props.record, "Type");
542
491
  const remembered = metadataValue(props.record, "Remembered");
543
492
  const source = metadataValue(props.record, "Source");
544
- const visibility = metadataValue(props.record, "Visibility");
545
- const isPublic = visibility === "Public";
546
493
  return (
547
494
  <SelectableRow
548
495
  className={cn(
@@ -565,11 +512,6 @@ function MemoryRow(props: {
565
512
  >
566
513
  <div className="min-w-0 overflow-hidden">
567
514
  <div className="flex min-w-0 items-center gap-2 overflow-hidden">
568
- <VisibilityIcon
569
- className="sm:hidden"
570
- isPublic={isPublic}
571
- size={14}
572
- />
573
515
  <h3 className="m-0 truncate font-display text-base font-medium leading-snug text-dashboard-text">
574
516
  <MemorySearchHighlight
575
517
  query={props.searchQuery}
@@ -591,38 +533,11 @@ function MemoryRow(props: {
591
533
  <span className="hidden truncate font-mono text-xs text-dashboard-text sm:block">
592
534
  {shortDate(remembered)}
593
535
  </span>
594
- <VisibilityIcon
595
- className="hidden justify-self-center sm:inline-flex"
596
- isPublic={isPublic}
597
- size={15}
598
- />
599
536
  </button>
600
537
  </SelectableRow>
601
538
  );
602
539
  }
603
540
 
604
- /** Render the lock (private) or globe (public) mark for one memory's visibility. */
605
- function VisibilityIcon(props: {
606
- className?: string;
607
- isPublic: boolean;
608
- size: number;
609
- }) {
610
- const Icon = props.isPublic ? Globe2 : Lock;
611
- const label = props.isPublic ? "Public" : "Private";
612
- return (
613
- <span
614
- className={cn(
615
- "inline-flex shrink-0 items-center text-dashboard-text-muted",
616
- props.className,
617
- )}
618
- title={label}
619
- >
620
- <Icon aria-hidden="true" size={props.size} />
621
- <span className="sr-only">{label}</span>
622
- </span>
623
- );
624
- }
625
-
626
541
  function metadataValue(record: PluginUserPageRecord, label: string): string {
627
542
  return record.metadata?.find((item) => item.label === label)?.value ?? "—";
628
543
  }