@sentry/junior-dashboard 0.275.0 → 0.276.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 +28 -9
- package/dist/client.js +115 -92
- package/dist/index.js +28 -9
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/app.ts +8 -3
- package/src/client/conversations/ConversationComposer.tsx +42 -26
- package/src/client/conversations/ConversationDetails.tsx +63 -35
- package/src/client/conversations/ConversationHeader.tsx +10 -2
- package/src/client/conversations/ConversationListStatusIcon.tsx +16 -11
- package/src/client/conversations/ConversationMemories.tsx +118 -78
- package/src/client/conversations/ConversationMeta.tsx +47 -42
- package/src/client/conversations/ConversationPage.tsx +168 -58
- package/src/client/conversations/ConversationTranscript.tsx +37 -8
- package/src/client/conversations/ConversationWatches.tsx +37 -15
- package/src/client/conversations/PendingMailboxStack.tsx +11 -1
- package/src/client/conversations/QuickQuestion.tsx +476 -0
- package/src/client/conversations/TelemetryMetrics.tsx +2 -1
- package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
- package/src/client/conversations/TranscriptEventView.tsx +25 -2
- package/src/client/conversations/TranscriptMemoryPopover.tsx +179 -0
- package/src/client/conversations/TranscriptMessageView.tsx +46 -16
- package/src/client/conversations/TranscriptRows.tsx +9 -2
- package/src/client/conversations/UsageCard.tsx +3 -3
- package/src/client/conversations/conversationMemory.ts +138 -0
- package/src/client/conversations/eventTranscript.ts +8 -0
- package/src/client/review-desk/ReviewItemActions.tsx +1 -1
- package/src/client/review-desk/ReviewItemChangeRow.tsx +85 -0
- package/src/client/review-desk/ReviewItemContext.tsx +41 -23
- package/src/client/review-desk/reviewItemFormat.ts +16 -0
- package/src/client/sentry.ts +77 -0
- package/src/client/types.ts +6 -0
- package/src/client.tsx +3 -0
- package/src/mock-reporting/watches.ts +2 -0
- package/src/shell.ts +27 -2
- package/src/tailwind.css +8 -19
- package/src/client/components/ActiveIndicator.tsx +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.276.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@base-ui/react": "^1.9.0",
|
|
26
|
+
"@sentry/browser": "11.10.0",
|
|
26
27
|
"@tanstack/react-query": "^5.100.14",
|
|
27
28
|
"@uidotdev/usehooks": "2.4.1",
|
|
28
29
|
"class-variance-authority": "^0.7.1",
|
|
@@ -39,10 +40,10 @@
|
|
|
39
40
|
"remark-gfm": "^4.0.1",
|
|
40
41
|
"shiki": "4.1.0",
|
|
41
42
|
"zod": "^4.5.4",
|
|
42
|
-
"@sentry/junior-plugin-api": "0.
|
|
43
|
+
"@sentry/junior-plugin-api": "0.276.0"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
|
-
"@sentry/junior": "0.
|
|
46
|
+
"@sentry/junior": "0.276.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@oxlint/plugins": "1.79.0",
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
"tw-animate-css": "^1.4.0",
|
|
64
65
|
"typescript": "^7.0.2",
|
|
65
66
|
"vitest": "^4.1.12",
|
|
66
|
-
"@sentry/junior": "0.
|
|
67
|
+
"@sentry/junior": "0.276.0"
|
|
67
68
|
},
|
|
68
69
|
"scripts": {
|
|
69
70
|
"build": "pnpm --filter @sentry/junior-plugin-api build && pnpm --filter @sentry/junior build && pnpm run build:client && pnpm run build:css && pnpm run build:server",
|
package/src/app.ts
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
type JuniorApiVariables,
|
|
12
12
|
} from "@sentry/junior/api";
|
|
13
13
|
import { apiErrorSchema } from "@sentry/junior/api/schema";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getBrowserSentryConfig,
|
|
16
|
+
initSentry,
|
|
17
|
+
} from "@sentry/junior/instrumentation";
|
|
15
18
|
import { JUNIOR_VERSION } from "@sentry/junior/version";
|
|
16
19
|
import { DASHBOARD_VERSION_HEADER } from "./dashboard-version";
|
|
17
20
|
import { revalidateConversation } from "./conversation-cache";
|
|
@@ -527,10 +530,12 @@ export function createDashboardApp(
|
|
|
527
530
|
}
|
|
528
531
|
}
|
|
529
532
|
|
|
533
|
+
const renderPage = () =>
|
|
534
|
+
renderDashboard(basePath, agentName, getBrowserSentryConfig());
|
|
530
535
|
for (const { nested, path } of dashboardPagePaths(basePath)) {
|
|
531
|
-
app.get(path,
|
|
536
|
+
app.get(path, renderPage);
|
|
532
537
|
if (nested) {
|
|
533
|
-
app.get(`${path}/*`,
|
|
538
|
+
app.get(`${path}/*`, renderPage);
|
|
534
539
|
}
|
|
535
540
|
}
|
|
536
541
|
for (const route of options.pluginRoutes ?? []) {
|
|
@@ -81,7 +81,10 @@ type ConversationComposerProps = {
|
|
|
81
81
|
* crushed beside the mobile send button.
|
|
82
82
|
*/
|
|
83
83
|
footerStart?: ReactNode;
|
|
84
|
+
/** Text only, for a narrow panel: no images, no key hint, icon-only send. */
|
|
85
|
+
compact?: boolean;
|
|
84
86
|
label: string;
|
|
87
|
+
placeholder?: string;
|
|
85
88
|
/**
|
|
86
89
|
* Restore the submitted text after a failed accept when there is no mailbox
|
|
87
90
|
* outbox (new conversation create). Existing conversations keep failures in
|
|
@@ -433,7 +436,12 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
433
436
|
)}
|
|
434
437
|
onDragOver={(event) => {
|
|
435
438
|
if (!event.dataTransfer.types.includes("Files")) return;
|
|
439
|
+
// Always cancel a file drop, or the browser opens the file.
|
|
436
440
|
event.preventDefault();
|
|
441
|
+
if (props.compact) {
|
|
442
|
+
event.dataTransfer.dropEffect = "none";
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
437
445
|
event.dataTransfer.dropEffect = "copy";
|
|
438
446
|
setDragging(true);
|
|
439
447
|
}}
|
|
@@ -444,12 +452,13 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
444
452
|
onDrop={(event) => {
|
|
445
453
|
if (!event.dataTransfer.types.includes("Files")) return;
|
|
446
454
|
event.preventDefault();
|
|
455
|
+
if (props.compact) return;
|
|
447
456
|
setDragging(false);
|
|
448
457
|
void addImages(Array.from(event.dataTransfer.files));
|
|
449
458
|
}}
|
|
450
459
|
onPaste={(event) => {
|
|
451
460
|
const files = Array.from(event.clipboardData.files);
|
|
452
|
-
if (!files.length) return;
|
|
461
|
+
if (props.compact || !files.length) return;
|
|
453
462
|
event.preventDefault();
|
|
454
463
|
void addImages(files);
|
|
455
464
|
}}
|
|
@@ -510,7 +519,7 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
510
519
|
}}
|
|
511
520
|
onFocus={props.onFocus}
|
|
512
521
|
onKeyDown={handleKeyDown}
|
|
513
|
-
placeholder="Message Junior…"
|
|
522
|
+
placeholder={props.placeholder ?? "Message Junior…"}
|
|
514
523
|
ref={textareaRef}
|
|
515
524
|
rows={1}
|
|
516
525
|
spellCheck={autocorrect}
|
|
@@ -524,27 +533,29 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
524
533
|
)}
|
|
525
534
|
>
|
|
526
535
|
<div className="flex min-w-0 items-center gap-3">
|
|
527
|
-
|
|
528
|
-
<
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
536
|
+
{props.compact ? null : (
|
|
537
|
+
<Tooltip>
|
|
538
|
+
<TooltipTrigger
|
|
539
|
+
render={
|
|
540
|
+
<Button
|
|
541
|
+
focusableWhenDisabled
|
|
542
|
+
aria-label="Attach images"
|
|
543
|
+
disabled={sendLocked || readingImages}
|
|
544
|
+
onClick={() => fileInputRef.current?.click()}
|
|
545
|
+
size="icon"
|
|
546
|
+
type="button"
|
|
547
|
+
>
|
|
548
|
+
<ImagePlus aria-hidden="true" size={16} />
|
|
549
|
+
</Button>
|
|
550
|
+
}
|
|
551
|
+
/>
|
|
552
|
+
<TooltipContent size="compact">
|
|
553
|
+
Attach images (up to 3 MB total)
|
|
554
|
+
</TooltipContent>
|
|
555
|
+
</Tooltip>
|
|
556
|
+
)}
|
|
546
557
|
{props.footerStart}
|
|
547
|
-
{props.footerStart ? null : (
|
|
558
|
+
{props.footerStart || props.compact ? null : (
|
|
548
559
|
<div className="hidden min-w-0 font-sans text-xs leading-relaxed text-dashboard-text-muted md:block">
|
|
549
560
|
Enter to send · Shift+Enter for a new line
|
|
550
561
|
</div>
|
|
@@ -565,7 +576,10 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
565
576
|
) : null}
|
|
566
577
|
<Button
|
|
567
578
|
aria-label={sendLocked ? "Sending message" : props.submitLabel}
|
|
568
|
-
className=
|
|
579
|
+
className={cn(
|
|
580
|
+
"disabled:!border-transparent disabled:!bg-dashboard-fill-hover disabled:!text-dashboard-text-muted disabled:!opacity-100",
|
|
581
|
+
props.compact && "w-9 px-0",
|
|
582
|
+
)}
|
|
569
583
|
disabled={
|
|
570
584
|
(!canSend && !images.length) ||
|
|
571
585
|
!online ||
|
|
@@ -576,9 +590,11 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
576
590
|
type="submit"
|
|
577
591
|
>
|
|
578
592
|
<Send aria-hidden="true" size={14} />
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
593
|
+
{props.compact ? null : (
|
|
594
|
+
<span className="hidden md:inline">
|
|
595
|
+
{sendLocked ? "Sending…" : props.submitLabel}
|
|
596
|
+
</span>
|
|
597
|
+
)}
|
|
582
598
|
</Button>
|
|
583
599
|
</div>
|
|
584
600
|
</div>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ArrowLeft } from "lucide-react";
|
|
2
2
|
import { useState, type ReactNode } from "react";
|
|
3
3
|
import type { ConversationDetailReport } from "@sentry/junior/api/schema";
|
|
4
4
|
|
|
5
5
|
import { DetailsPanel, useDetailsPanel } from "../components/DetailsPanel";
|
|
6
6
|
import { formatRelativeTime, formatTime } from "../format";
|
|
7
7
|
import { ConversationReviewItems } from "../review-desk/ConversationReviewItems";
|
|
8
|
-
import {
|
|
8
|
+
import { Button } from "../components/ui/button";
|
|
9
9
|
import { ConversationBrief } from "./ConversationBrief";
|
|
10
10
|
import {
|
|
11
11
|
ConversationChildAgents,
|
|
@@ -16,9 +16,14 @@ import {
|
|
|
16
16
|
type ConversationLink,
|
|
17
17
|
} from "./ConversationLinkedWork";
|
|
18
18
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
ConversationMemoryCard,
|
|
20
|
+
ConversationMemoryEntries,
|
|
21
21
|
} from "./ConversationMemories";
|
|
22
|
+
import {
|
|
23
|
+
conversationMemoryEntries,
|
|
24
|
+
useConversationMemoryState,
|
|
25
|
+
type ConversationMemoryEntry,
|
|
26
|
+
} from "./conversationMemory";
|
|
22
27
|
import {
|
|
23
28
|
ConversationWatches,
|
|
24
29
|
useConversationWatches,
|
|
@@ -43,15 +48,63 @@ export function ConversationDetails(props: {
|
|
|
43
48
|
onClose(): void;
|
|
44
49
|
/** Open one child agent transcript. A details sheet closes first. */
|
|
45
50
|
onOpenSubagent(target: SubagentTranscriptTarget): void;
|
|
51
|
+
/** Show one memory where its Turn recalled or captured it. */
|
|
52
|
+
onSelectMemory(entry: ConversationMemoryEntry & { turnId: string }): void;
|
|
53
|
+
/** The prompt that started each loaded Turn. */
|
|
54
|
+
memoryPrompts: Map<string, string>;
|
|
46
55
|
overview: ReactNode;
|
|
47
56
|
usage: ReactNode;
|
|
48
57
|
}) {
|
|
49
58
|
const panel = useDetailsPanel();
|
|
50
|
-
const
|
|
59
|
+
const memoryList = useConversationMemoryState()?.list;
|
|
60
|
+
const [memoriesOpen, setMemoriesOpen] = useState(false);
|
|
61
|
+
const [memoryConversationId, setMemoryConversationId] = useState(
|
|
62
|
+
props.conversationId,
|
|
63
|
+
);
|
|
64
|
+
if (memoryConversationId !== props.conversationId) {
|
|
65
|
+
setMemoryConversationId(props.conversationId);
|
|
66
|
+
setMemoriesOpen(false);
|
|
67
|
+
}
|
|
51
68
|
const agents = useConversationChildAgents(props.conversationId).data?.agents;
|
|
52
69
|
const watches =
|
|
53
70
|
useConversationWatches(props.conversationId).data?.watches ?? [];
|
|
54
71
|
|
|
72
|
+
if (memoriesOpen && memoryList) {
|
|
73
|
+
// The list replaces details in place, so nothing stacks over the panel.
|
|
74
|
+
return (
|
|
75
|
+
<DetailsPanel
|
|
76
|
+
label="Conversation details"
|
|
77
|
+
onClose={props.onClose}
|
|
78
|
+
title={
|
|
79
|
+
<div className="-ml-2 flex min-w-0 items-center gap-1">
|
|
80
|
+
<Button
|
|
81
|
+
aria-label="Back to details"
|
|
82
|
+
onClick={() => setMemoriesOpen(false)}
|
|
83
|
+
size="icon"
|
|
84
|
+
>
|
|
85
|
+
<ArrowLeft aria-hidden="true" size={15} />
|
|
86
|
+
</Button>
|
|
87
|
+
<h2 className="m-0 truncate font-sans text-sm font-semibold text-dashboard-text">
|
|
88
|
+
Memories
|
|
89
|
+
</h2>
|
|
90
|
+
</div>
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
<div className="px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-3">
|
|
94
|
+
<ConversationMemoryEntries
|
|
95
|
+
entries={conversationMemoryEntries(memoryList)}
|
|
96
|
+
onSelect={(entry) => {
|
|
97
|
+
// The transcript sits under a details sheet. Close it first.
|
|
98
|
+
if (!panel) props.onClose();
|
|
99
|
+
props.onSelectMemory(entry);
|
|
100
|
+
}}
|
|
101
|
+
prompts={props.memoryPrompts}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</DetailsPanel>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
55
108
|
return (
|
|
56
109
|
<DetailsPanel
|
|
57
110
|
label="Conversation details"
|
|
@@ -135,38 +188,13 @@ export function ConversationDetails(props: {
|
|
|
135
188
|
{props.identity != null ? (
|
|
136
189
|
<DetailsCard title="Participants">{props.identity}</DetailsCard>
|
|
137
190
|
) : null}
|
|
138
|
-
{
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</DetailsCard>
|
|
191
|
+
{memoryList?.events.length ? (
|
|
192
|
+
<ConversationMemoryCard
|
|
193
|
+
list={memoryList}
|
|
194
|
+
onOpen={() => setMemoriesOpen(true)}
|
|
195
|
+
/>
|
|
144
196
|
) : null}
|
|
145
197
|
</div>
|
|
146
198
|
</DetailsPanel>
|
|
147
199
|
);
|
|
148
200
|
}
|
|
149
|
-
|
|
150
|
-
/** Hide long, rarely read content behind one show/hide row. */
|
|
151
|
-
function Disclosure(props: { children: ReactNode; label: string }) {
|
|
152
|
-
const [open, setOpen] = useState(false);
|
|
153
|
-
return (
|
|
154
|
-
<div className="grid min-w-0 gap-2">
|
|
155
|
-
<button
|
|
156
|
-
aria-expanded={open}
|
|
157
|
-
className="group -mx-1.5 -my-1 flex min-h-8 items-center gap-2.5 rounded-md px-1.5 py-1.5 text-left text-xs font-medium leading-4 text-dashboard-text-muted transition-colors hover:bg-dashboard-surface-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
158
|
-
onClick={() => setOpen((value) => !value)}
|
|
159
|
-
type="button"
|
|
160
|
-
>
|
|
161
|
-
<span className="grid size-5 shrink-0 place-items-center">
|
|
162
|
-
<ChevronRight
|
|
163
|
-
aria-hidden="true"
|
|
164
|
-
className={dashboardDisclosureChevronClass}
|
|
165
|
-
/>
|
|
166
|
-
</span>
|
|
167
|
-
{open ? `Hide ${props.label}` : `Show ${props.label}`}
|
|
168
|
-
</button>
|
|
169
|
-
{open ? props.children : null}
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
@@ -20,14 +20,17 @@ import { ShareConversationButton } from "./ShareConversationButton";
|
|
|
20
20
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Show the conversation title bar: where it runs, the title, and
|
|
24
|
-
*
|
|
23
|
+
* Show the conversation title bar: where it runs, the title, its cost, and
|
|
24
|
+
* actions. Other run facts, linked work, and the Brief live in Conversation
|
|
25
|
+
* details.
|
|
25
26
|
*/
|
|
26
27
|
export function ConversationHeader(props: {
|
|
27
28
|
/** Parent chain of a child Conversation. Root Conversations omit it. */
|
|
28
29
|
agentTree?: ReactNode;
|
|
29
30
|
/** A `menu` layout copy action for the `⋯` menu. */
|
|
30
31
|
copyAction?: ReactNode;
|
|
32
|
+
/** Estimated cost. Desktop only; mobile shows it in Conversation details. */
|
|
33
|
+
cost?: ReactNode;
|
|
31
34
|
archive: ConversationArchiveAction;
|
|
32
35
|
conversationId: string;
|
|
33
36
|
detailsOpen: boolean;
|
|
@@ -179,6 +182,11 @@ export function ConversationHeader(props: {
|
|
|
179
182
|
<span className="shrink-0">{props.privacy}</span>
|
|
180
183
|
</div>
|
|
181
184
|
<div className="flex shrink-0 items-center gap-0.5">
|
|
185
|
+
{props.cost ? (
|
|
186
|
+
<span className="mr-2 text-xs tabular-nums text-dashboard-text-muted">
|
|
187
|
+
{props.cost}
|
|
188
|
+
</span>
|
|
189
|
+
) : null}
|
|
182
190
|
{props.view === "raw" ? (
|
|
183
191
|
// Event log is a debug view reached from the menu. Say which
|
|
184
192
|
// view this is, and give one click back.
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { LockKeyhole } from "lucide-react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Spinner } from "../components/ui/spinner";
|
|
4
4
|
import { cn } from "../styles";
|
|
5
5
|
import type { VisualStatus } from "../types";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Render a conversation status marker. "Waiting on you" replaces every other
|
|
9
|
-
* marker,
|
|
9
|
+
* marker. A running conversation shows a spinner, so it never looks like an
|
|
10
|
+
* ended one.
|
|
10
11
|
*/
|
|
11
12
|
export function ConversationListStatusIcon(props: {
|
|
12
13
|
finishedSinceSeen: boolean;
|
|
@@ -24,6 +25,10 @@ export function ConversationListStatusIcon(props: {
|
|
|
24
25
|
);
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
if (props.status === "active") {
|
|
29
|
+
return <Spinner aria-label="Running" className="size-3 text-emerald-300" />;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
if (props.finishedSinceSeen) {
|
|
28
33
|
return (
|
|
29
34
|
<span
|
|
@@ -40,8 +45,6 @@ export function ConversationListStatusIcon(props: {
|
|
|
40
45
|
aria-label="Private conversation"
|
|
41
46
|
className={cn(
|
|
42
47
|
"size-3 shrink-0",
|
|
43
|
-
props.status === "active" &&
|
|
44
|
-
"animate-[junior-active-indicator_1.8s_ease-in-out_infinite] text-emerald-300 drop-shadow-[0_0_6px_rgba(110,231,183,0.55)] motion-reduce:animate-none",
|
|
45
48
|
props.status === "failed" && "text-rose-300",
|
|
46
49
|
props.status === "idle" && "text-dashboard-text-muted",
|
|
47
50
|
)}
|
|
@@ -49,18 +52,20 @@ export function ConversationListStatusIcon(props: {
|
|
|
49
52
|
);
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
if (props.status === "
|
|
53
|
-
return
|
|
55
|
+
if (props.status === "failed") {
|
|
56
|
+
return (
|
|
57
|
+
<span
|
|
58
|
+
aria-label="Failed"
|
|
59
|
+
className="size-1.5 shrink-0 rounded-full bg-rose-300"
|
|
60
|
+
role="img"
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
return (
|
|
57
66
|
<span
|
|
58
67
|
aria-hidden="true"
|
|
59
|
-
className=
|
|
60
|
-
"size-1.5 shrink-0 rounded-full",
|
|
61
|
-
props.status === "failed" && "bg-rose-300",
|
|
62
|
-
props.status === "idle" && "bg-dashboard-text-faint",
|
|
63
|
-
)}
|
|
68
|
+
className="size-1.5 shrink-0 rounded-full bg-dashboard-text-faint"
|
|
64
69
|
/>
|
|
65
70
|
);
|
|
66
71
|
}
|
|
@@ -1,90 +1,130 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { z } from "zod";
|
|
1
|
+
import { ChevronRight } from "lucide-react";
|
|
2
|
+
import type { ConversationDetailReport } from "@sentry/junior/api/schema";
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { dashboardDisclosureChevronClass } from "../styles";
|
|
5
|
+
import type {
|
|
6
|
+
ConversationMemoryEntry,
|
|
7
|
+
ConversationMemoryList,
|
|
8
|
+
} from "./conversationMemory";
|
|
9
|
+
import { DetailsCard } from "./DetailsList";
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
visibility: z.enum(["private", "public"]),
|
|
18
|
-
})
|
|
19
|
-
.strict(),
|
|
20
|
-
),
|
|
21
|
-
})
|
|
22
|
-
.strict();
|
|
23
|
-
|
|
24
|
-
/** Load the memories captured from one Conversation. */
|
|
25
|
-
export function useConversationMemories(conversationId: string) {
|
|
26
|
-
return useQuery({
|
|
27
|
-
queryFn: ({ signal }) =>
|
|
28
|
-
fetchDashboardJson(
|
|
29
|
-
conversationMemoryListSchema,
|
|
30
|
-
`/api/plugins/memory/conversations/${encodeURIComponent(conversationId)}/memories`,
|
|
31
|
-
signal,
|
|
32
|
-
),
|
|
33
|
-
queryKey: ["conversation", conversationId, "memories"],
|
|
34
|
-
retry: false,
|
|
35
|
-
});
|
|
11
|
+
/** Count the distinct memories that a Conversation recalled and captured. */
|
|
12
|
+
function conversationMemoryCounts(list: ConversationMemoryList) {
|
|
13
|
+
const ids = (source: "captured" | "recalled") =>
|
|
14
|
+
new Set(
|
|
15
|
+
list.events
|
|
16
|
+
.filter((event) => event.source === source)
|
|
17
|
+
.flatMap((event) => event.memoryIds),
|
|
18
|
+
).size;
|
|
19
|
+
return { captured: ids("captured"), recalled: ids("recalled") };
|
|
36
20
|
}
|
|
37
21
|
|
|
38
|
-
/** Show
|
|
39
|
-
export function
|
|
40
|
-
|
|
22
|
+
/** Show memory counts in one row that opens the full memory list. */
|
|
23
|
+
export function ConversationMemoryCard(props: {
|
|
24
|
+
list: ConversationMemoryList;
|
|
25
|
+
onOpen(): void;
|
|
26
|
+
}) {
|
|
27
|
+
const counts = conversationMemoryCounts(props.list);
|
|
28
|
+
const summary = [
|
|
29
|
+
counts.recalled ? `${counts.recalled} recalled` : undefined,
|
|
30
|
+
counts.captured ? `${counts.captured} captured` : undefined,
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(" · ");
|
|
34
|
+
return (
|
|
35
|
+
<DetailsCard title="Memories">
|
|
36
|
+
<button
|
|
37
|
+
className="group -mx-1.5 -my-1 flex min-h-8 w-[calc(100%+0.75rem)] items-center justify-between gap-2.5 rounded-md px-1.5 py-1.5 text-left text-sm tabular-nums text-dashboard-text transition-colors hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
38
|
+
onClick={props.onOpen}
|
|
39
|
+
type="button"
|
|
40
|
+
>
|
|
41
|
+
{summary}
|
|
42
|
+
<ChevronRight
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
className={dashboardDisclosureChevronClass}
|
|
45
|
+
/>
|
|
46
|
+
</button>
|
|
47
|
+
</DetailsCard>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
);
|
|
51
|
+
/**
|
|
52
|
+
* Map each loaded Turn to the prompt that started it, so a memory can say
|
|
53
|
+
* where it came from. A Turn starts from its last user input message.
|
|
54
|
+
*/
|
|
55
|
+
export function turnPrompts(
|
|
56
|
+
events: ConversationDetailReport["events"] | undefined,
|
|
57
|
+
): Map<string, string> {
|
|
58
|
+
const texts = new Map<string, string>();
|
|
59
|
+
const prompts = new Map<string, string>();
|
|
60
|
+
for (const { data } of events ?? []) {
|
|
61
|
+
if (data.type === "message" && data.role === "user" && data.text) {
|
|
62
|
+
texts.set(data.messageId, data.text);
|
|
63
|
+
}
|
|
64
|
+
if (data.type === "turn_lifecycle" && data.state === "started") {
|
|
65
|
+
const prompt = data.inputMessageIds
|
|
66
|
+
?.map((id) => texts.get(id))
|
|
67
|
+
.filter((text) => text !== undefined)
|
|
68
|
+
.at(-1);
|
|
69
|
+
if (prompt) prompts.set(data.turnId, prompt);
|
|
70
|
+
}
|
|
63
71
|
}
|
|
72
|
+
return prompts;
|
|
73
|
+
}
|
|
64
74
|
|
|
75
|
+
/**
|
|
76
|
+
* List every memory that a Conversation recalled or captured, with its full
|
|
77
|
+
* text. Selecting one shows it in the transcript.
|
|
78
|
+
*/
|
|
79
|
+
export function ConversationMemoryEntries(props: {
|
|
80
|
+
entries: ConversationMemoryEntry[];
|
|
81
|
+
onSelect(entry: ConversationMemoryEntry & { turnId: string }): void;
|
|
82
|
+
prompts: Map<string, string>;
|
|
83
|
+
}) {
|
|
65
84
|
return (
|
|
66
|
-
<ol className="m-0 grid list-none gap-
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
<ol className="m-0 grid list-none gap-2 p-0">
|
|
86
|
+
{props.entries.map((entry) => {
|
|
87
|
+
const { turnId } = entry;
|
|
88
|
+
const prompt = turnId ? props.prompts.get(turnId) : undefined;
|
|
89
|
+
const source =
|
|
90
|
+
entry.source === "recalled"
|
|
91
|
+
? prompt
|
|
92
|
+
? `Recalled for “${prompt}”`
|
|
93
|
+
: "Recalled"
|
|
94
|
+
: prompt
|
|
95
|
+
? `Captured after “${prompt}”`
|
|
96
|
+
: "Captured";
|
|
97
|
+
const body = (
|
|
98
|
+
<>
|
|
99
|
+
<p className="m-0 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text">
|
|
100
|
+
{entry.memory.content}
|
|
101
|
+
</p>
|
|
102
|
+
<span className="flex min-w-0 items-center gap-2 text-xs text-dashboard-text-muted">
|
|
103
|
+
<span className="min-w-0 truncate">{source}</span>
|
|
104
|
+
{entry.memory.visibility === "private" ? (
|
|
105
|
+
<span className="shrink-0">· Private</span>
|
|
106
|
+
) : null}
|
|
81
107
|
</span>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
const className =
|
|
111
|
+
"grid w-full gap-1.5 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel px-3 py-2.5 text-left font-sans";
|
|
112
|
+
return (
|
|
113
|
+
<li key={`${entry.source}:${entry.memory.id}`}>
|
|
114
|
+
{turnId ? (
|
|
115
|
+
<button
|
|
116
|
+
className={`${className} cursor-pointer transition-colors hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus`}
|
|
117
|
+
onClick={() => props.onSelect({ ...entry, turnId })}
|
|
118
|
+
type="button"
|
|
119
|
+
>
|
|
120
|
+
{body}
|
|
121
|
+
</button>
|
|
122
|
+
) : (
|
|
123
|
+
<div className={className}>{body}</div>
|
|
124
|
+
)}
|
|
125
|
+
</li>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
88
128
|
</ol>
|
|
89
129
|
);
|
|
90
130
|
}
|