@sentry/junior-dashboard 0.276.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.
- package/dist/app.js +196 -72
- package/dist/client.js +94 -89
- package/dist/index.d.ts +1 -1
- package/dist/index.js +191 -70
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +20 -2
- package/src/app.ts +80 -12
- package/src/auth.ts +5 -1
- package/src/client/App.tsx +9 -4
- package/src/client/components/DetailsPanel.tsx +1 -1
- package/src/client/components/FormNotice.tsx +11 -14
- package/src/client/components/ProfileMenu.tsx +5 -6
- package/src/client/components/ui/resizable.tsx +50 -0
- package/src/client/conversations/AutoArchiveToggle.tsx +71 -0
- package/src/client/conversations/ConversationArchiveNotices.tsx +1 -1
- package/src/client/conversations/ConversationChildAgents.tsx +2 -1
- package/src/client/conversations/ConversationDetails.tsx +6 -3
- package/src/client/conversations/ConversationHeader.tsx +15 -2
- package/src/client/conversations/ConversationHeaderActions.tsx +27 -0
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationMemories.tsx +64 -67
- package/src/client/conversations/ConversationMeta.tsx +2 -7
- package/src/client/conversations/ConversationPage.tsx +175 -79
- package/src/client/conversations/ConversationSidebar.tsx +82 -25
- package/src/client/conversations/ConversationTranscript.tsx +11 -8
- package/src/client/conversations/ConversationWatches.tsx +15 -21
- package/src/client/conversations/ConversationWorkspace.tsx +132 -44
- package/src/client/conversations/ConversationWorkspaceLoading.tsx +17 -3
- package/src/client/conversations/DetailsList.tsx +8 -14
- package/src/client/conversations/EventDetails.tsx +2 -62
- package/src/client/conversations/MemoryIcons.tsx +23 -0
- package/src/client/conversations/TranscriptActivityGroup.tsx +36 -14
- package/src/client/conversations/TranscriptContextEventView.tsx +7 -18
- package/src/client/conversations/TranscriptMemoryPopover.tsx +20 -25
- package/src/client/conversations/TranscriptRailEvent.tsx +0 -8
- package/src/client/conversations/TranscriptTurnContextView.tsx +9 -5
- package/src/client/conversations/TranscriptView.tsx +89 -20
- package/src/client/conversations/autoArchiveQueries.ts +53 -0
- package/src/client/conversations/conversationArchive.ts +255 -0
- package/src/client/conversations/conversationMemory.ts +2 -1
- package/src/client/conversations/conversationSections.ts +24 -18
- package/src/client/conversations/conversationSidebarLayout.ts +61 -0
- package/src/client/conversations/eventLog.ts +2 -31
- package/src/client/conversations/eventTranscript.ts +9 -42
- package/src/client/conversations/queries.ts +121 -250
- package/src/client/conversations/transcript.ts +46 -16
- package/src/client/conversations/transcriptBottomPinning.ts +40 -24
- package/src/client/conversations/transcriptReadPosition.ts +31 -7
- package/src/client/conversations/transcriptRenderModel.ts +1 -14
- package/src/client/conversations/transcriptSearch.tsx +3 -11
- package/src/client/conversations/transcriptUnreadHistory.ts +248 -0
- package/src/client/conversations/usePinConversation.ts +88 -0
- package/src/client/display-text/DisplayText.tsx +7 -2
- package/src/client/display-text/codeBlocks.ts +92 -0
- package/src/client/format.ts +3 -67
- package/src/client/http.ts +5 -5
- package/src/client/markdownExport.ts +32 -42
- package/src/client/pages/code/RepositoryTabs.tsx +2 -2
- package/src/client/pages/memory/MemoryDetails.tsx +6 -33
- package/src/client/pages/memory/MemoryPage.tsx +7 -92
- package/src/client/pages/memory/MemoryTimeline.tsx +16 -32
- package/src/client/pages/memory/memoryDashboard.ts +2 -38
- package/src/client/pages/memory/memoryRecord.ts +1 -6
- package/src/client/pages/settings/AutoArchiveSettings.tsx +145 -0
- package/src/client/pages/settings/SettingsPage.tsx +2 -0
- package/src/client/review-desk/ConversationReviewItems.tsx +7 -9
- package/src/client/review-desk/queries.ts +2 -1
- package/src/client/types.ts +19 -33
- package/src/conversation-cache.ts +7 -9
- package/src/mock-reporting/fixtures.ts +126 -60
- package/src/mock-reporting/routes.ts +50 -1
- package/src/tailwind.css +30 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { BundledLanguage } from "shiki/bundle/web";
|
|
2
|
+
|
|
3
|
+
import { dashboardCodeLanguages } from "../code-languages";
|
|
4
|
+
import { detectOutputLanguage, prettyJsonData } from "../format";
|
|
5
|
+
import type { CodeBlock } from "../types";
|
|
6
|
+
|
|
7
|
+
// Shiki has no language detection, and agents often leave fences untagged.
|
|
8
|
+
// Only clear signals select a language, so prose in a fence stays plain.
|
|
9
|
+
const SCRIPT_PATTERN =
|
|
10
|
+
/^javascript:|=>|\b(?:const|let|var)\s+[\w$]+\s*=|\bfunction\b\s*[\w$]*\s*\(|^\s*(?:import\s.+\sfrom\s|export\s+(?:(?:async|class|const|default|enum|function|interface|let|type|var)\s|[{*]))/m;
|
|
11
|
+
const PYTHON_PATTERN =
|
|
12
|
+
/^\s*(?:(?:async\s+)?def\s+\w+\s*\(.*\)\s*(?:->.*)?:|class\s+\w+(?:\([^)]*\))?\s*:|from\s+[\w.]+\s+import\s|import\s+[\w.]+\s*$)/m;
|
|
13
|
+
const SHELL_PATTERN =
|
|
14
|
+
/^\s*(?:\$\s|(?:bun|brew|cargo|cat|cd|cp|curl|docker|echo|export|gh|git|go|grep|kubectl|ls|make|mkdir|mv|npm|npx|pip|pnpm|rm|ssh|sudo|uv|wget|yarn)\s)/;
|
|
15
|
+
|
|
16
|
+
const LANGUAGE_ALIASES: Record<string, BundledLanguage> = {
|
|
17
|
+
console: "shellscript",
|
|
18
|
+
htm: "html",
|
|
19
|
+
js: "javascript",
|
|
20
|
+
jsonl: "json",
|
|
21
|
+
md: "markdown",
|
|
22
|
+
ndjson: "json",
|
|
23
|
+
sh: "shellscript",
|
|
24
|
+
text: "markdown",
|
|
25
|
+
txt: "markdown",
|
|
26
|
+
xml: "xml",
|
|
27
|
+
yml: "yaml",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Parse transcript prose as Markdown or JSON while preserving fenced code. */
|
|
31
|
+
export function parseMarkdownBlocks(text: string): CodeBlock[] {
|
|
32
|
+
const blocks: CodeBlock[] = [];
|
|
33
|
+
// The info string can hold more than the language, such as `c++` or
|
|
34
|
+
// `js title="x"`. Only its first word names the language.
|
|
35
|
+
const fence = /^([ \t]*)```([^\n`]*)\n([\s\S]*?)```/gm;
|
|
36
|
+
let cursor = 0;
|
|
37
|
+
let match: RegExpExecArray | null;
|
|
38
|
+
while ((match = fence.exec(text))) {
|
|
39
|
+
const prose = text.slice(cursor, match.index).trim();
|
|
40
|
+
if (prose) blocks.push(proseBlock(prose));
|
|
41
|
+
const tag = match[2]?.trim().split(/\s+/)[0];
|
|
42
|
+
const code = stripFenceIndent(match[3] ?? "", match[1]?.length ?? 0);
|
|
43
|
+
const language = tag ? normalizeLanguage(tag) : guessLanguage(code);
|
|
44
|
+
blocks.push({
|
|
45
|
+
code: formatCodeBlock(code, language),
|
|
46
|
+
fenced: true,
|
|
47
|
+
language,
|
|
48
|
+
});
|
|
49
|
+
cursor = match.index + match[0].length;
|
|
50
|
+
}
|
|
51
|
+
const rest = text.slice(cursor).trim();
|
|
52
|
+
if (rest) blocks.push(proseBlock(rest));
|
|
53
|
+
return blocks.length > 0 ? blocks : [proseBlock(text)];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function proseBlock(text: string): CodeBlock {
|
|
57
|
+
const language = detectOutputLanguage(text);
|
|
58
|
+
return { code: formatCodeBlock(text, language), fenced: false, language };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeLanguage(tag: string): BundledLanguage {
|
|
62
|
+
const normalized = tag.toLowerCase();
|
|
63
|
+
const candidate = LANGUAGE_ALIASES[normalized] ?? normalized;
|
|
64
|
+
return Object.hasOwn(dashboardCodeLanguages, candidate)
|
|
65
|
+
? (candidate as BundledLanguage)
|
|
66
|
+
: "markdown";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function guessLanguage(code: string): BundledLanguage {
|
|
70
|
+
if (detectOutputLanguage(code) === "json") return "json";
|
|
71
|
+
if (SCRIPT_PATTERN.test(code)) return "tsx";
|
|
72
|
+
if (PYTHON_PATTERN.test(code)) return "python";
|
|
73
|
+
if (SHELL_PATTERN.test(code)) return "shellscript";
|
|
74
|
+
return "markdown";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A fence in a list item is indented, and so are its code lines. Remove that
|
|
78
|
+
// indent, as CommonMark does.
|
|
79
|
+
function stripFenceIndent(code: string, indent: number): string {
|
|
80
|
+
if (indent === 0) return code;
|
|
81
|
+
return code
|
|
82
|
+
.split("\n")
|
|
83
|
+
.map((line) => {
|
|
84
|
+
const lead = line.length - line.trimStart().length;
|
|
85
|
+
return line.slice(Math.min(indent, lead));
|
|
86
|
+
})
|
|
87
|
+
.join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatCodeBlock(code: string, language: BundledLanguage): string {
|
|
91
|
+
return language === "json" ? (prettyJsonData(code) ?? code) : code;
|
|
92
|
+
}
|
package/src/client/format.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { BundledLanguage } from "shiki/bundle/web";
|
|
2
|
-
import { dashboardCodeLanguages } from "./code-languages";
|
|
3
2
|
import type {
|
|
4
3
|
ActorIdentity,
|
|
5
4
|
ConversationAuxiliaryCosts,
|
|
@@ -8,7 +7,6 @@ import type {
|
|
|
8
7
|
ConversationUsage,
|
|
9
8
|
} from "@sentry/junior/api/schema";
|
|
10
9
|
import type {
|
|
11
|
-
CodeBlock,
|
|
12
10
|
Conversation,
|
|
13
11
|
ConversationTranscript,
|
|
14
12
|
TranscriptViewMessage,
|
|
@@ -815,28 +813,6 @@ export function automationPath(
|
|
|
815
813
|
return tab === "overview" ? base : `${base}/${tab}`;
|
|
816
814
|
}
|
|
817
815
|
|
|
818
|
-
function normalizeLanguage(language: string | undefined): BundledLanguage {
|
|
819
|
-
const normalized = language?.trim().toLowerCase();
|
|
820
|
-
if (!normalized) return "markdown";
|
|
821
|
-
const aliases: Record<string, BundledLanguage> = {
|
|
822
|
-
console: "shellscript",
|
|
823
|
-
htm: "html",
|
|
824
|
-
js: "javascript",
|
|
825
|
-
jsonl: "json",
|
|
826
|
-
md: "markdown",
|
|
827
|
-
ndjson: "json",
|
|
828
|
-
sh: "shellscript",
|
|
829
|
-
text: "markdown",
|
|
830
|
-
txt: "markdown",
|
|
831
|
-
xml: "xml",
|
|
832
|
-
yml: "yaml",
|
|
833
|
-
};
|
|
834
|
-
const candidate = aliases[normalized] ?? normalized;
|
|
835
|
-
return Object.hasOwn(dashboardCodeLanguages, candidate)
|
|
836
|
-
? (candidate as BundledLanguage)
|
|
837
|
-
: "markdown";
|
|
838
|
-
}
|
|
839
|
-
|
|
840
816
|
function prettyJson(text: string): string | undefined {
|
|
841
817
|
const trimmed = text.trim();
|
|
842
818
|
if (!trimmed) return undefined;
|
|
@@ -863,14 +839,11 @@ function prettyJsonl(text: string): string | undefined {
|
|
|
863
839
|
return formatted.join("\n");
|
|
864
840
|
}
|
|
865
841
|
|
|
866
|
-
|
|
842
|
+
/** Pretty-print JSON or JSONL text, or return undefined for other text. */
|
|
843
|
+
export function prettyJsonData(text: string): string | undefined {
|
|
867
844
|
return prettyJson(text) ?? prettyJsonl(text);
|
|
868
845
|
}
|
|
869
846
|
|
|
870
|
-
function formatCodeBlock(code: string, language: BundledLanguage): string {
|
|
871
|
-
return language === "json" ? (prettyJsonData(code) ?? code) : code;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
847
|
/**
|
|
875
848
|
* Detect the language for LLM text output prose: json if the text is valid
|
|
876
849
|
* JSON or JSONL, markdown otherwise. Never auto-detects XML, HTML, TypeScript,
|
|
@@ -889,44 +862,6 @@ export function detectOutputLanguage(text: string): BundledLanguage {
|
|
|
889
862
|
return "markdown";
|
|
890
863
|
}
|
|
891
864
|
|
|
892
|
-
/** Parse transcript prose as Markdown or JSON while preserving fenced code. */
|
|
893
|
-
export function parseMarkdownBlocks(text: string): CodeBlock[] {
|
|
894
|
-
const blocks: CodeBlock[] = [];
|
|
895
|
-
const fence = /```([A-Za-z0-9_-]+)?\n([\s\S]*?)```/g;
|
|
896
|
-
let cursor = 0;
|
|
897
|
-
let match: RegExpExecArray | null;
|
|
898
|
-
while ((match = fence.exec(text))) {
|
|
899
|
-
const prose = text.slice(cursor, match.index).trim();
|
|
900
|
-
if (prose) {
|
|
901
|
-
const language = detectOutputLanguage(prose);
|
|
902
|
-
blocks.push({
|
|
903
|
-
code: formatCodeBlock(prose, language),
|
|
904
|
-
fenced: false,
|
|
905
|
-
language,
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
const language = normalizeLanguage(match[1]);
|
|
909
|
-
blocks.push({
|
|
910
|
-
code: formatCodeBlock(match[2] ?? "", language),
|
|
911
|
-
fenced: true,
|
|
912
|
-
language,
|
|
913
|
-
});
|
|
914
|
-
cursor = match.index + match[0].length;
|
|
915
|
-
}
|
|
916
|
-
const rest = text.slice(cursor).trim();
|
|
917
|
-
if (rest) {
|
|
918
|
-
const language = detectOutputLanguage(rest);
|
|
919
|
-
blocks.push({
|
|
920
|
-
code: formatCodeBlock(rest, language),
|
|
921
|
-
fenced: false,
|
|
922
|
-
language,
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
if (blocks.length > 0) return blocks;
|
|
926
|
-
const language = detectOutputLanguage(text);
|
|
927
|
-
return [{ code: formatCodeBlock(text, language), fenced: false, language }];
|
|
928
|
-
}
|
|
929
|
-
|
|
930
865
|
/** Convert SQL conversation summaries into dashboard rows. */
|
|
931
866
|
export function buildConversations(
|
|
932
867
|
summaries: ConversationSummaryReport[],
|
|
@@ -937,6 +872,7 @@ export function buildConversations(
|
|
|
937
872
|
annotations: summary.annotations,
|
|
938
873
|
sidebarAnnotations: summary.sidebarAnnotations,
|
|
939
874
|
archivedAt: summary.archivedAt,
|
|
875
|
+
pinnedAt: summary.pinnedAt,
|
|
940
876
|
auxiliaryCosts: summary.auxiliaryCosts,
|
|
941
877
|
channel: summary.channel,
|
|
942
878
|
channelName: summary.channelName,
|
package/src/client/http.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
...(
|
|
187
|
-
? { cache: "no-store" as const, headers:
|
|
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 && !(
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
addMetaLine(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
"
|
|
294
|
-
String(event.details.
|
|
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);
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "../../components/controls/TimeRangeSelector";
|
|
17
17
|
import { Card } from "../../components/layout/Card";
|
|
18
18
|
import { ConversationHomeList } from "../../conversations/ConversationHomeList";
|
|
19
|
-
import {
|
|
19
|
+
import { pinnedAndAttentionFirst } from "../../conversations/conversationSections";
|
|
20
20
|
import { buildConversations, getDashboardTimeZone } from "../../format";
|
|
21
21
|
import { useDebouncedValue } from "../../debouncedValue";
|
|
22
22
|
import { useWaitingConversationIds } from "../../review-desk/queries";
|
|
@@ -54,7 +54,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
|
|
|
54
54
|
) : (
|
|
55
55
|
<ConversationHomeList
|
|
56
56
|
conversations={pageItems(
|
|
57
|
-
|
|
57
|
+
pinnedAndAttentionFirst(conversations, waitingConversationIds),
|
|
58
58
|
page,
|
|
59
59
|
PAGE_SIZE,
|
|
60
60
|
)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
?
|
|
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
|
|
173
|
+
/** Name a memory by type, such as "Preference". */
|
|
198
174
|
function memoryHeading(record: PluginUserPageRecord): string {
|
|
199
|
-
|
|
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
|
|
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) => ({
|
|
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: "
|
|
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.
|
|
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
|
|
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="
|
|
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
|
}
|