@sentry/junior-dashboard 0.275.0 → 0.277.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 +194 -40
- package/dist/client.js +120 -92
- package/dist/index.d.ts +1 -1
- package/dist/index.js +189 -38
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +20 -2
- package/src/app.ts +88 -15
- package/src/auth.ts +5 -1
- package/src/client/components/FormNotice.tsx +11 -14
- package/src/client/components/ProfileMenu.tsx +5 -6
- package/src/client/conversations/AutoArchiveToggle.tsx +35 -0
- package/src/client/conversations/ConversationArchiveNotices.tsx +1 -1
- package/src/client/conversations/ConversationChildAgents.tsx +2 -1
- package/src/client/conversations/ConversationComposer.tsx +42 -26
- package/src/client/conversations/ConversationDetails.tsx +66 -35
- package/src/client/conversations/ConversationHeader.tsx +19 -2
- package/src/client/conversations/ConversationHeaderActions.tsx +23 -0
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationListStatusIcon.tsx +16 -11
- package/src/client/conversations/ConversationMemories.tsx +114 -77
- package/src/client/conversations/ConversationMeta.tsx +47 -42
- package/src/client/conversations/ConversationPage.tsx +183 -60
- package/src/client/conversations/ConversationSidebar.tsx +80 -24
- package/src/client/conversations/ConversationTranscript.tsx +37 -8
- package/src/client/conversations/ConversationWatches.tsx +42 -26
- package/src/client/conversations/ConversationWorkspace.tsx +4 -4
- package/src/client/conversations/DetailsList.tsx +8 -14
- package/src/client/conversations/MemoryIcons.tsx +23 -0
- 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 +174 -0
- package/src/client/conversations/TranscriptMessageView.tsx +46 -16
- package/src/client/conversations/TranscriptRows.tsx +9 -2
- package/src/client/conversations/TranscriptTurnContextView.tsx +9 -5
- package/src/client/conversations/UsageCard.tsx +3 -3
- package/src/client/conversations/autoArchiveQueries.ts +53 -0
- package/src/client/conversations/conversationArchive.ts +255 -0
- package/src/client/conversations/conversationMemory.ts +139 -0
- package/src/client/conversations/conversationSections.ts +24 -18
- package/src/client/conversations/eventTranscript.ts +8 -0
- package/src/client/conversations/queries.ts +48 -238
- 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/pages/code/RepositoryTabs.tsx +2 -2
- 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/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/queries.ts +2 -1
- package/src/client/review-desk/reviewItemFormat.ts +16 -0
- package/src/client/sentry.ts +77 -0
- package/src/client/types.ts +7 -0
- package/src/client.tsx +3 -0
- package/src/conversation-cache.ts +1 -1
- package/src/mock-reporting/fixtures.ts +103 -27
- package/src/mock-reporting/routes.ts +47 -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
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
CircleX,
|
|
7
7
|
GitMerge,
|
|
8
8
|
GitPullRequest,
|
|
9
|
-
Globe2,
|
|
10
9
|
LockKeyhole,
|
|
11
10
|
TriangleAlert,
|
|
12
11
|
} from "lucide-react";
|
|
@@ -72,35 +71,24 @@ export function PendingAuthorization(props: {
|
|
|
72
71
|
);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Mark a private conversation with a lock icon. Public is the default, so it
|
|
76
|
+
* gets no marker, which keeps the title bar short.
|
|
77
|
+
*/
|
|
76
78
|
export function ConversationPrivacyChip(props: {
|
|
77
79
|
visibility: Conversation["visibility"];
|
|
78
80
|
}) {
|
|
79
|
-
if (
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
const shortLabel = isPublic ? "Public" : "Private";
|
|
83
|
-
const fullLabel = isPublic ? "Public conversation" : "Private conversation";
|
|
84
|
-
const detail = isPublic
|
|
85
|
-
? "Anyone in this workspace can see this transcript."
|
|
86
|
-
: "Only members of this conversation can see this transcript.";
|
|
81
|
+
if (props.visibility !== "private") return null;
|
|
82
|
+
const label =
|
|
83
|
+
"Private conversation. Only members of this conversation can see this transcript.";
|
|
87
84
|
return (
|
|
88
85
|
<span
|
|
89
|
-
className=
|
|
90
|
-
isPublic
|
|
91
|
-
? "inline-flex max-w-full items-center gap-1 rounded-full border border-dashboard-border-emphasis bg-dashboard-fill-soft px-2 py-0.5 font-sans text-2xs font-medium text-dashboard-text-muted"
|
|
92
|
-
: "inline-flex max-w-full items-center gap-1 rounded-full border border-dashboard-border-strong bg-dashboard-fill-soft px-2 py-0.5 font-sans text-2xs font-medium text-dashboard-text-muted"
|
|
93
|
-
}
|
|
86
|
+
className="inline-flex items-center text-dashboard-text-muted"
|
|
94
87
|
role="note"
|
|
95
|
-
title={
|
|
88
|
+
title={label}
|
|
96
89
|
>
|
|
97
|
-
<
|
|
98
|
-
<span
|
|
99
|
-
{shortLabel}
|
|
100
|
-
</span>
|
|
101
|
-
<span className="sr-only">
|
|
102
|
-
{fullLabel}. {detail}
|
|
103
|
-
</span>
|
|
90
|
+
<LockKeyhole aria-hidden="true" className="size-3.5 shrink-0" />
|
|
91
|
+
<span className="sr-only">{label}</span>
|
|
104
92
|
</span>
|
|
105
93
|
);
|
|
106
94
|
}
|
|
@@ -568,6 +556,37 @@ export function conversationLocation(
|
|
|
568
556
|
);
|
|
569
557
|
}
|
|
570
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Show the estimated cost of a conversation with its breakdown tooltip.
|
|
561
|
+
* Returns null until there is a cost or a live run to show.
|
|
562
|
+
*/
|
|
563
|
+
export function conversationCost(
|
|
564
|
+
conversation: Conversation | undefined,
|
|
565
|
+
detail: ConversationDetailReport | undefined,
|
|
566
|
+
tooltipPlacement?: "above" | "below",
|
|
567
|
+
): ReactNode {
|
|
568
|
+
if (!conversation) return null;
|
|
569
|
+
const costSummary = summarizeCost(
|
|
570
|
+
detail?.cumulativeUsage ?? conversation.cumulativeUsage,
|
|
571
|
+
);
|
|
572
|
+
const auxiliaryCosts = detail?.auxiliaryCosts ?? conversation.auxiliaryCosts;
|
|
573
|
+
const live = (detail?.status ?? conversation.status) === "active";
|
|
574
|
+
if (!costSummary && !auxiliaryCosts && !live) return null;
|
|
575
|
+
return (
|
|
576
|
+
<CostMetric
|
|
577
|
+
auxiliaryCosts={auxiliaryCosts}
|
|
578
|
+
childAgentTotals={
|
|
579
|
+
detail?.childAgentTotals ?? conversation.childAgentTotals
|
|
580
|
+
}
|
|
581
|
+
live={live}
|
|
582
|
+
liveModelId={liveModelId(detail)}
|
|
583
|
+
modelUsage={detail?.modelUsage}
|
|
584
|
+
summary={costSummary}
|
|
585
|
+
tooltipPlacement={tooltipPlacement}
|
|
586
|
+
/>
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
571
590
|
function conversationStatItems(props: {
|
|
572
591
|
conversation: Conversation | undefined;
|
|
573
592
|
detail?: ConversationDetailReport;
|
|
@@ -580,7 +599,6 @@ function conversationStatItems(props: {
|
|
|
580
599
|
props.detail?.cumulativeUsage ?? props.conversation.cumulativeUsage;
|
|
581
600
|
const tokenSummary =
|
|
582
601
|
summarizeModelUsage(props.detail?.modelUsage) ?? summarizeUsage(usage);
|
|
583
|
-
const costSummary = summarizeCost(usage);
|
|
584
602
|
const durationLabel = formatConversationDuration(props.conversation);
|
|
585
603
|
const live = (props.detail?.status ?? props.conversation.status) === "active";
|
|
586
604
|
const activeModelId = liveModelId(props.detail);
|
|
@@ -589,8 +607,9 @@ function conversationStatItems(props: {
|
|
|
589
607
|
props.detail?.childAgentTotals ?? props.conversation.childAgentTotals;
|
|
590
608
|
const lastActivityAt = props.conversation.lastSeenAt;
|
|
591
609
|
const location = conversationLocation(props.conversation, props.detail);
|
|
610
|
+
const cost = conversationCost(props.conversation, props.detail);
|
|
592
611
|
const rawStats: Array<ConversationStatItem | undefined> = [
|
|
593
|
-
// The desktop title bar shows the location. Mobile hides that bar.
|
|
612
|
+
// The desktop title bar shows the location and cost. Mobile hides that bar.
|
|
594
613
|
location
|
|
595
614
|
? {
|
|
596
615
|
content: location,
|
|
@@ -660,26 +679,12 @@ function conversationStatItems(props: {
|
|
|
660
679
|
label: "Tokens",
|
|
661
680
|
}
|
|
662
681
|
: undefined,
|
|
663
|
-
|
|
664
|
-
props.detail?.auxiliaryCosts ||
|
|
665
|
-
props.conversation.auxiliaryCosts ||
|
|
666
|
-
live
|
|
682
|
+
cost
|
|
667
683
|
? {
|
|
668
|
-
content:
|
|
669
|
-
<CostMetric
|
|
670
|
-
auxiliaryCosts={
|
|
671
|
-
props.detail?.auxiliaryCosts ??
|
|
672
|
-
props.conversation.auxiliaryCosts
|
|
673
|
-
}
|
|
674
|
-
childAgentTotals={childAgentTotals}
|
|
675
|
-
live={live}
|
|
676
|
-
liveModelId={activeModelId}
|
|
677
|
-
modelUsage={props.detail?.modelUsage}
|
|
678
|
-
summary={costSummary}
|
|
679
|
-
/>
|
|
680
|
-
),
|
|
684
|
+
content: cost,
|
|
681
685
|
key: "cost",
|
|
682
686
|
label: "Cost",
|
|
687
|
+
mobileOnly: true,
|
|
683
688
|
}
|
|
684
689
|
: undefined,
|
|
685
690
|
childAgentTotals
|
|
@@ -14,9 +14,10 @@ import type {
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
useArchiveConversation,
|
|
17
|
-
useConversationData,
|
|
18
17
|
type PendingArchiveConversationUpdate,
|
|
19
|
-
} from "./
|
|
18
|
+
} from "./conversationArchive";
|
|
19
|
+
import { useConversationData } from "./queries";
|
|
20
|
+
import { usePinConversation } from "./usePinConversation";
|
|
20
21
|
import { conversationIsResponding } from "./transcript";
|
|
21
22
|
import { buildConversationMarkdown } from "../markdownExport";
|
|
22
23
|
import { CopyButton } from "../components/CopyButton";
|
|
@@ -31,6 +32,14 @@ import {
|
|
|
31
32
|
import { ConversationHeader } from "./ConversationHeader";
|
|
32
33
|
import { ConversationReplyFooter } from "./ConversationReplyFooter";
|
|
33
34
|
import { ConversationDetails } from "./ConversationDetails";
|
|
35
|
+
import { AutoArchiveToggle } from "./AutoArchiveToggle";
|
|
36
|
+
import {
|
|
37
|
+
ConversationMemoryContext,
|
|
38
|
+
memoryAnchorKey,
|
|
39
|
+
useConversationMemories,
|
|
40
|
+
type ConversationMemoryState,
|
|
41
|
+
type OpenMemoryPopover,
|
|
42
|
+
} from "./conversationMemory";
|
|
34
43
|
import { ConversationEventDetails } from "./ConversationEventDetails";
|
|
35
44
|
import { ConversationEventLogLoading } from "./ConversationEventLog";
|
|
36
45
|
import {
|
|
@@ -50,6 +59,7 @@ import { ConversationAgentTree } from "./ConversationAgentTree";
|
|
|
50
59
|
import { conversationLinks } from "./ConversationLinkedWork";
|
|
51
60
|
import {
|
|
52
61
|
ConversationAnnotations,
|
|
62
|
+
conversationCost,
|
|
53
63
|
conversationLocation,
|
|
54
64
|
ConversationPrivacyChip,
|
|
55
65
|
ConversationStats,
|
|
@@ -74,6 +84,11 @@ import {
|
|
|
74
84
|
ParticipantAvatarStack,
|
|
75
85
|
} from "../components/ParticipantAvatarStack";
|
|
76
86
|
import { ChatLayout } from "./ChatLayout";
|
|
87
|
+
import {
|
|
88
|
+
QuickQuestionPanel,
|
|
89
|
+
QuickQuestionSelectionButton,
|
|
90
|
+
useQuickQuestionThread,
|
|
91
|
+
} from "./QuickQuestion";
|
|
77
92
|
import { latestReviewItemId } from "../review-desk/reviewItemFormat";
|
|
78
93
|
import { Transcript } from "./TranscriptView";
|
|
79
94
|
import {
|
|
@@ -155,7 +170,24 @@ export function ConversationPage(props: {
|
|
|
155
170
|
);
|
|
156
171
|
}, [setSearchParams]);
|
|
157
172
|
const [turnContext, setTurnContext] = useState<TurnContextSelection>();
|
|
158
|
-
const
|
|
173
|
+
const [memoryPopover, setMemoryPopover] = useState<OpenMemoryPopover>();
|
|
174
|
+
// A memory chosen in details waits here until its transcript row renders.
|
|
175
|
+
const [memoryJump, setMemoryJump] = useState<OpenMemoryPopover>();
|
|
176
|
+
// The page stays mounted across Conversations. Memory state belongs to one.
|
|
177
|
+
const [memoryConversationId, setMemoryConversationId] = useState(
|
|
178
|
+
props.conversationId,
|
|
179
|
+
);
|
|
180
|
+
if (memoryConversationId !== props.conversationId) {
|
|
181
|
+
setMemoryConversationId(props.conversationId);
|
|
182
|
+
setMemoryPopover(undefined);
|
|
183
|
+
setMemoryJump(undefined);
|
|
184
|
+
}
|
|
185
|
+
const quickQuestion = useQuickQuestionThread(props.conversationId);
|
|
186
|
+
// Event details and turn context also show over a Quick question.
|
|
187
|
+
const showQuickQuestion =
|
|
188
|
+
Boolean(quickQuestion.thread) && !eventOpen && !turnContext;
|
|
189
|
+
const showDetails =
|
|
190
|
+
detailsOpen && !eventOpen && !turnContext && !quickQuestion.thread;
|
|
159
191
|
const [viewState, setView] = useState<TranscriptViewMode>(() =>
|
|
160
192
|
eventOpen ? "raw" : "rich",
|
|
161
193
|
);
|
|
@@ -167,7 +199,17 @@ export function ConversationPage(props: {
|
|
|
167
199
|
const summaries = props.data?.conversations.conversations ?? [];
|
|
168
200
|
const conversations = buildConversations(summaries);
|
|
169
201
|
const detail = useConversationData(conversationId);
|
|
202
|
+
const memoryList = useConversationMemories(conversationId).data;
|
|
203
|
+
const memoryState = useMemo<ConversationMemoryState>(
|
|
204
|
+
() => ({
|
|
205
|
+
list: memoryList,
|
|
206
|
+
open: memoryPopover,
|
|
207
|
+
setOpen: setMemoryPopover,
|
|
208
|
+
}),
|
|
209
|
+
[memoryList, memoryPopover],
|
|
210
|
+
);
|
|
170
211
|
const archive = useArchiveConversation(conversationId);
|
|
212
|
+
const pin = usePinConversation(conversationId);
|
|
171
213
|
const markRead = useMarkConversationRead(conversationId);
|
|
172
214
|
const readAtOpen = useReadPositionAtOpen(detail.data, detail.fetchedDetail);
|
|
173
215
|
const feedConversation = conversations.find(
|
|
@@ -288,6 +330,34 @@ export function ConversationPage(props: {
|
|
|
288
330
|
if (linkedTurnSeq !== undefined) setSelectedEventSeq(linkedTurnSeq);
|
|
289
331
|
else if (linkedTurnNotFound) setSelectedEventSeq(undefined);
|
|
290
332
|
}, [linkedTurnNotFound, linkedTurnSeq, setSelectedEventSeq]);
|
|
333
|
+
// Show a chosen memory at its transcript row. Wait for the first page, then
|
|
334
|
+
// load earlier pages until the row renders, or stop when history ends.
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
if (!memoryJump || !transcript) return;
|
|
337
|
+
const anchor = pageRef.current?.querySelector(
|
|
338
|
+
`[data-memory-anchor="${CSS.escape(memoryAnchorKey(memoryJump))}"]`,
|
|
339
|
+
);
|
|
340
|
+
if (anchor) {
|
|
341
|
+
anchor.scrollIntoView({ block: "center" });
|
|
342
|
+
setMemoryPopover(memoryJump);
|
|
343
|
+
setMemoryJump(undefined);
|
|
344
|
+
} else if (
|
|
345
|
+
detail.hasPreviousPage &&
|
|
346
|
+
!detail.isLoadingPreviousPage &&
|
|
347
|
+
!detail.historyError
|
|
348
|
+
) {
|
|
349
|
+
loadPreviousPageRef.current();
|
|
350
|
+
} else if (!detail.isLoadingPreviousPage) {
|
|
351
|
+
setMemoryJump(undefined);
|
|
352
|
+
}
|
|
353
|
+
}, [
|
|
354
|
+
detail.hasPreviousPage,
|
|
355
|
+
detail.historyError,
|
|
356
|
+
detail.isLoadingPreviousPage,
|
|
357
|
+
memoryJump,
|
|
358
|
+
transcript,
|
|
359
|
+
view,
|
|
360
|
+
]);
|
|
291
361
|
// Keep the panel while the transcript or a linked event loads, and for the
|
|
292
362
|
// render before a found Turn link becomes an event link.
|
|
293
363
|
const eventLoading =
|
|
@@ -362,7 +432,7 @@ export function ConversationPage(props: {
|
|
|
362
432
|
ref={pageRef}
|
|
363
433
|
className={cn(
|
|
364
434
|
"grid h-full min-h-0 min-w-0",
|
|
365
|
-
(showDetails || showEvent || turnContext) &&
|
|
435
|
+
(showDetails || showEvent || turnContext || showQuickQuestion) &&
|
|
366
436
|
"xl:grid-cols-[minmax(0,1fr)_25rem]",
|
|
367
437
|
)}
|
|
368
438
|
>
|
|
@@ -373,10 +443,12 @@ export function ConversationPage(props: {
|
|
|
373
443
|
agentTree ? <ConversationAgentTree nodes={agentTree} /> : null
|
|
374
444
|
}
|
|
375
445
|
conversationId={conversationId}
|
|
446
|
+
cost={conversationCost(conversation, detail.data, "below")}
|
|
376
447
|
detailsOpen={showDetails}
|
|
377
448
|
onDetailsClick={() => {
|
|
378
449
|
if (eventOpen) setSelectedEventSeq(undefined);
|
|
379
450
|
setTurnContext(undefined);
|
|
451
|
+
quickQuestion.close();
|
|
380
452
|
if (showDetails) closeDetails();
|
|
381
453
|
else setDetailsChoice(true);
|
|
382
454
|
}}
|
|
@@ -414,6 +486,14 @@ export function ConversationPage(props: {
|
|
|
414
486
|
<ConversationAnnotations detail={detail.data} />
|
|
415
487
|
) : null
|
|
416
488
|
}
|
|
489
|
+
pin={{
|
|
490
|
+
// The API lets only participants pin, the same as archive.
|
|
491
|
+
disabled:
|
|
492
|
+
!conversation || !detail.data?.isParticipant || pin.isPending,
|
|
493
|
+
error: Boolean(pin.error),
|
|
494
|
+
onClick: () => pin.mutate(!conversation?.pinnedAt),
|
|
495
|
+
pinned: Boolean(conversation?.pinnedAt),
|
|
496
|
+
}}
|
|
417
497
|
archive={{
|
|
418
498
|
archived: Boolean(conversation?.archivedAt),
|
|
419
499
|
disabled: !conversation || archive.isPending,
|
|
@@ -470,31 +550,35 @@ export function ConversationPage(props: {
|
|
|
470
550
|
data.
|
|
471
551
|
</div>
|
|
472
552
|
) : null}
|
|
473
|
-
<
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
553
|
+
<ConversationMemoryContext value={memoryState}>
|
|
554
|
+
<Transcript
|
|
555
|
+
hasPreviousPage={detail.hasPreviousPage}
|
|
556
|
+
historyError={detail.historyError}
|
|
557
|
+
historyVersion={detail.historyVersion}
|
|
558
|
+
lastReadSeq={readAtOpen.lastReadSeq}
|
|
559
|
+
live={live}
|
|
560
|
+
loadingPreviousPage={detail.isLoadingPreviousPage}
|
|
561
|
+
onLoadPreviousPage={detail.loadPreviousPage}
|
|
562
|
+
onRead={markRead}
|
|
563
|
+
onSelectedEventSeqChange={setSelectedEventSeq}
|
|
564
|
+
pendingMessages={detail.inlineMessages}
|
|
565
|
+
pinRequestVersion={pinRequestVersion}
|
|
566
|
+
responding={
|
|
567
|
+
!detail.error && conversationIsResponding(transcript)
|
|
568
|
+
}
|
|
569
|
+
onOpenSubagentTranscript={onOpenSubagentTranscript}
|
|
570
|
+
search={search}
|
|
571
|
+
onSearchChange={setSearch}
|
|
572
|
+
selectedEventSeq={selectedEventSeq}
|
|
573
|
+
startAtUnread={
|
|
574
|
+
readConversationStartPosition() === "unread"
|
|
575
|
+
}
|
|
576
|
+
transcript={transcript}
|
|
577
|
+
turnContextPanel={turnContextPanel}
|
|
578
|
+
unread={readAtOpen.unread}
|
|
579
|
+
view={view}
|
|
580
|
+
/>
|
|
581
|
+
</ConversationMemoryContext>
|
|
498
582
|
</>
|
|
499
583
|
)}
|
|
500
584
|
</div>
|
|
@@ -532,37 +616,58 @@ export function ConversationPage(props: {
|
|
|
532
616
|
}
|
|
533
617
|
/>
|
|
534
618
|
{showDetails ? (
|
|
535
|
-
<
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
619
|
+
<ConversationMemoryContext value={memoryState}>
|
|
620
|
+
<ConversationDetails
|
|
621
|
+
autoArchive={
|
|
622
|
+
detail.data?.autoArchive === undefined ? null : (
|
|
623
|
+
<AutoArchiveToggle
|
|
624
|
+
autoArchive={detail.data.autoArchive}
|
|
625
|
+
conversationId={conversationId}
|
|
626
|
+
/>
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
brief={detail.data?.brief}
|
|
630
|
+
conversationId={conversationId}
|
|
631
|
+
highlightReview={reviewOpen}
|
|
632
|
+
identity={identity}
|
|
633
|
+
onClose={closeDetails}
|
|
634
|
+
onOpenSubagent={setSubagentTarget}
|
|
635
|
+
onSelectMemory={(entry) => {
|
|
636
|
+
// Memory rows show only in the Conversation view, and search
|
|
637
|
+
// can hide them.
|
|
638
|
+
if (view !== "rich") onViewChange("rich");
|
|
639
|
+
setSearch("");
|
|
640
|
+
setMemoryJump({
|
|
641
|
+
memoryId: entry.memory.id,
|
|
642
|
+
source: entry.source,
|
|
643
|
+
turnId: entry.turnId,
|
|
644
|
+
});
|
|
645
|
+
}}
|
|
646
|
+
links={conversationLinks(detail.data)}
|
|
647
|
+
overview={
|
|
648
|
+
hasConversationStats({
|
|
649
|
+
conversation,
|
|
650
|
+
detail: detail.data,
|
|
651
|
+
}) ? (
|
|
652
|
+
<ConversationStats
|
|
653
|
+
conversation={conversation}
|
|
654
|
+
detail={detail.data}
|
|
655
|
+
/>
|
|
656
|
+
) : null
|
|
657
|
+
}
|
|
658
|
+
usage={
|
|
659
|
+
hasConversationUsageBreakdown({
|
|
660
|
+
conversation,
|
|
661
|
+
detail: detail.data,
|
|
662
|
+
}) ? (
|
|
663
|
+
<ConversationUsageBreakdown
|
|
664
|
+
conversation={conversation}
|
|
665
|
+
detail={detail.data}
|
|
666
|
+
/>
|
|
667
|
+
) : null
|
|
668
|
+
}
|
|
669
|
+
/>
|
|
670
|
+
</ConversationMemoryContext>
|
|
566
671
|
) : null}
|
|
567
672
|
{showEvent ? (
|
|
568
673
|
<ConversationEventDetails
|
|
@@ -584,6 +689,24 @@ export function ConversationPage(props: {
|
|
|
584
689
|
/>
|
|
585
690
|
</TranscriptSearchProvider>
|
|
586
691
|
) : null}
|
|
692
|
+
{showQuickQuestion && quickQuestion.thread ? (
|
|
693
|
+
<QuickQuestionPanel
|
|
694
|
+
conversationId={conversationId}
|
|
695
|
+
error={quickQuestion.error}
|
|
696
|
+
onClose={quickQuestion.close}
|
|
697
|
+
onSubmit={quickQuestion.submit}
|
|
698
|
+
pending={quickQuestion.pending}
|
|
699
|
+
thread={quickQuestion.thread}
|
|
700
|
+
/>
|
|
701
|
+
) : null}
|
|
702
|
+
<QuickQuestionSelectionButton
|
|
703
|
+
onAsk={(selection) => {
|
|
704
|
+
if (eventOpen) setSelectedEventSeq(undefined);
|
|
705
|
+
setTurnContext(undefined);
|
|
706
|
+
quickQuestion.open(selection);
|
|
707
|
+
}}
|
|
708
|
+
rootRef={pageRef}
|
|
709
|
+
/>
|
|
587
710
|
<SubagentTranscriptDrawer
|
|
588
711
|
onClose={() => setSubagentTarget(undefined)}
|
|
589
712
|
target={subagentTarget}
|
|
@@ -6,11 +6,13 @@ import {
|
|
|
6
6
|
CircleAlert,
|
|
7
7
|
Inbox,
|
|
8
8
|
LockKeyhole,
|
|
9
|
+
Pin,
|
|
9
10
|
SquarePen,
|
|
10
11
|
} from "lucide-react";
|
|
11
12
|
import { Link } from "react-router";
|
|
12
13
|
|
|
13
|
-
import { useArchiveConversation } from "./
|
|
14
|
+
import { useArchiveConversation } from "./conversationArchive";
|
|
15
|
+
import { usePinConversation } from "./usePinConversation";
|
|
14
16
|
import { conversationPath } from "./conversationRoutes";
|
|
15
17
|
import {
|
|
16
18
|
conversationDisplayTitle,
|
|
@@ -51,9 +53,12 @@ type ConversationSidebarEntry =
|
|
|
51
53
|
}
|
|
52
54
|
| { conversation: Conversation; key: string; kind: "conversation" };
|
|
53
55
|
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
type
|
|
56
|
+
// The title names the attempted action, so a failed archive or pin never
|
|
57
|
+
// reads the row's already-optimistic state.
|
|
58
|
+
type RowActionError = { conversation: Conversation; title: string };
|
|
59
|
+
|
|
60
|
+
const ROW_ACTION_BUTTON_CLASS =
|
|
61
|
+
"absolute top-0.5 z-10 grid size-7 cursor-pointer place-items-center rounded-md bg-dashboard-surface-active text-dashboard-text-muted transition hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus sm:pointer-events-none sm:opacity-0 sm:focus:pointer-events-auto sm:focus:opacity-100 sm:group-focus-within:pointer-events-auto sm:group-focus-within:opacity-100 sm:group-hover:pointer-events-auto sm:group-hover:opacity-100 disabled:cursor-not-allowed";
|
|
57
62
|
|
|
58
63
|
const conversationEntryKey = (entry: ConversationSidebarEntry) => entry.key;
|
|
59
64
|
|
|
@@ -81,23 +86,42 @@ export function ConversationSidebar(props: {
|
|
|
81
86
|
const isLanding = variant === "landing";
|
|
82
87
|
const [archivedConversation, setArchivedConversation] =
|
|
83
88
|
useState<Conversation>();
|
|
84
|
-
const [
|
|
89
|
+
const [rowError, setRowError] = useState<RowActionError>();
|
|
85
90
|
const dismissArchivedConversation = useCallback(
|
|
86
91
|
() => setArchivedConversation(undefined),
|
|
87
92
|
[],
|
|
88
93
|
);
|
|
89
94
|
const handleArchiveError = useCallback(
|
|
90
95
|
(conversation: Conversation, wasArchiving: boolean) => {
|
|
91
|
-
|
|
96
|
+
setRowError({
|
|
97
|
+
conversation,
|
|
98
|
+
title: wasArchiving ? "Could not archive" : "Could not restore",
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
[],
|
|
102
|
+
);
|
|
103
|
+
const handlePinError = useCallback(
|
|
104
|
+
(conversation: Conversation, wasPinning: boolean) => {
|
|
105
|
+
setRowError({
|
|
106
|
+
conversation,
|
|
107
|
+
title: wasPinning ? "Could not pin" : "Could not unpin",
|
|
108
|
+
});
|
|
92
109
|
},
|
|
93
110
|
[],
|
|
94
111
|
);
|
|
95
|
-
|
|
96
|
-
|
|
112
|
+
// A later success on the same row replaces its error notice.
|
|
113
|
+
const clearRowError = useCallback((conversation: Conversation) => {
|
|
114
|
+
setRowError((current) =>
|
|
97
115
|
current?.conversation.id === conversation.id ? undefined : current,
|
|
98
116
|
);
|
|
99
|
-
setArchivedConversation(conversation);
|
|
100
117
|
}, []);
|
|
118
|
+
const handleArchived = useCallback(
|
|
119
|
+
(conversation: Conversation) => {
|
|
120
|
+
clearRowError(conversation);
|
|
121
|
+
setArchivedConversation(conversation);
|
|
122
|
+
},
|
|
123
|
+
[clearRowError],
|
|
124
|
+
);
|
|
101
125
|
const { collapsedKeys, toggle: toggleSection } =
|
|
102
126
|
useCollapsedConversationSections();
|
|
103
127
|
// Rebuild section rows only when their inputs change. Avoid fresh Date.now()
|
|
@@ -217,6 +241,8 @@ export function ConversationSidebar(props: {
|
|
|
217
241
|
conversation={entry.conversation}
|
|
218
242
|
onArchiveError={handleArchiveError}
|
|
219
243
|
onArchived={handleArchived}
|
|
244
|
+
onPinError={handlePinError}
|
|
245
|
+
onPinned={clearRowError}
|
|
220
246
|
selected={entry.conversation.id === props.selectedId}
|
|
221
247
|
unread={props.finishedConversationIds.has(
|
|
222
248
|
entry.conversation.id,
|
|
@@ -231,7 +257,7 @@ export function ConversationSidebar(props: {
|
|
|
231
257
|
/>
|
|
232
258
|
)}
|
|
233
259
|
</div>
|
|
234
|
-
{archivedConversation ||
|
|
260
|
+
{archivedConversation || rowError ? (
|
|
235
261
|
<div
|
|
236
262
|
className={cn(
|
|
237
263
|
// Landing is content-tall (parent scrolls). Keep notices on the
|
|
@@ -241,11 +267,11 @@ export function ConversationSidebar(props: {
|
|
|
241
267
|
: "absolute bottom-[max(0.75rem,env(safe-area-inset-bottom))] left-3 right-3 z-20 grid gap-2",
|
|
242
268
|
)}
|
|
243
269
|
>
|
|
244
|
-
{
|
|
245
|
-
<
|
|
246
|
-
conversation={
|
|
247
|
-
onDismiss={() =>
|
|
248
|
-
|
|
270
|
+
{rowError ? (
|
|
271
|
+
<ConversationActionErrorNotice
|
|
272
|
+
conversation={rowError.conversation}
|
|
273
|
+
onDismiss={() => setRowError(undefined)}
|
|
274
|
+
title={rowError.title}
|
|
249
275
|
/>
|
|
250
276
|
) : null}
|
|
251
277
|
{archivedConversation ? (
|
|
@@ -317,6 +343,8 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(props: {
|
|
|
317
343
|
conversation: Conversation;
|
|
318
344
|
onArchiveError(conversation: Conversation, wasArchiving: boolean): void;
|
|
319
345
|
onArchived(conversation: Conversation): void;
|
|
346
|
+
onPinError(conversation: Conversation, wasPinning: boolean): void;
|
|
347
|
+
onPinned(conversation: Conversation): void;
|
|
320
348
|
selected: boolean;
|
|
321
349
|
unread: boolean;
|
|
322
350
|
waiting?: WaitingConversation;
|
|
@@ -334,6 +362,11 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(props: {
|
|
|
334
362
|
if (archived) props.onArchived(props.conversation);
|
|
335
363
|
},
|
|
336
364
|
});
|
|
365
|
+
const pin = usePinConversation(props.conversation.id, {
|
|
366
|
+
onError: (pinned) => props.onPinError(props.conversation, pinned),
|
|
367
|
+
onSuccess: () => props.onPinned(props.conversation),
|
|
368
|
+
});
|
|
369
|
+
const pinned = Boolean(props.conversation.pinnedAt);
|
|
337
370
|
const status = visualStatusForConversation(props.conversation);
|
|
338
371
|
const title = conversationDisplayTitle(props.conversation);
|
|
339
372
|
// Unread and waiting rows read like unread mail: a dot and a bold title.
|
|
@@ -405,12 +438,39 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(props: {
|
|
|
405
438
|
/>
|
|
406
439
|
</Link>
|
|
407
440
|
) : null}
|
|
441
|
+
{/* Mobile rows pin from the conversation menu, so only the archive
|
|
442
|
+
button takes row space there. Archived rows restore before they pin. */}
|
|
443
|
+
{props.conversation.archivedAt ? null : (
|
|
444
|
+
<Tooltip>
|
|
445
|
+
<TooltipTrigger
|
|
446
|
+
render={
|
|
447
|
+
<button
|
|
448
|
+
aria-label={`${pinned ? "Unpin" : "Pin"} ${title}`}
|
|
449
|
+
aria-pressed={pinned}
|
|
450
|
+
className={cn(ROW_ACTION_BUTTON_CLASS, "right-9 max-sm:hidden")}
|
|
451
|
+
disabled={pin.isPending}
|
|
452
|
+
onClick={() => pin.mutate(!pinned)}
|
|
453
|
+
type="button"
|
|
454
|
+
>
|
|
455
|
+
<Pin
|
|
456
|
+
aria-hidden="true"
|
|
457
|
+
fill={pinned ? "currentColor" : "none"}
|
|
458
|
+
size={14}
|
|
459
|
+
/>
|
|
460
|
+
</button>
|
|
461
|
+
}
|
|
462
|
+
/>
|
|
463
|
+
<TooltipContent size="compact">
|
|
464
|
+
{pinned ? "Unpin" : "Pin"}
|
|
465
|
+
</TooltipContent>
|
|
466
|
+
</Tooltip>
|
|
467
|
+
)}
|
|
408
468
|
<Tooltip>
|
|
409
469
|
<TooltipTrigger
|
|
410
470
|
render={
|
|
411
471
|
<button
|
|
412
472
|
aria-label={`${props.conversation.archivedAt ? "Restore" : "Archive"} ${title}`}
|
|
413
|
-
className="
|
|
473
|
+
className={cn(ROW_ACTION_BUTTON_CLASS, "right-1.5")}
|
|
414
474
|
disabled={archive.isPending}
|
|
415
475
|
onClick={() =>
|
|
416
476
|
archive.mutate({
|
|
@@ -459,15 +519,11 @@ function ConversationRowState(props: {
|
|
|
459
519
|
);
|
|
460
520
|
}
|
|
461
521
|
|
|
462
|
-
function
|
|
522
|
+
function ConversationActionErrorNotice(props: {
|
|
463
523
|
conversation: Conversation;
|
|
464
524
|
onDismiss(): void;
|
|
465
|
-
|
|
525
|
+
title: string;
|
|
466
526
|
}) {
|
|
467
|
-
const title = conversationDisplayTitle(props.conversation);
|
|
468
|
-
const actionTitle = props.wasArchiving
|
|
469
|
-
? "Could not archive"
|
|
470
|
-
: "Could not restore";
|
|
471
527
|
return (
|
|
472
528
|
<Notice
|
|
473
529
|
action={
|
|
@@ -475,9 +531,9 @@ function ArchiveConversationErrorNotice(props: {
|
|
|
475
531
|
Dismiss
|
|
476
532
|
</NoticeAction>
|
|
477
533
|
}
|
|
478
|
-
detail={
|
|
534
|
+
detail={conversationDisplayTitle(props.conversation)}
|
|
479
535
|
icon={CircleAlert}
|
|
480
|
-
title={
|
|
536
|
+
title={props.title}
|
|
481
537
|
tone="error"
|
|
482
538
|
/>
|
|
483
539
|
);
|