@sentry/junior-dashboard 0.276.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 +168 -33
- package/dist/client.js +88 -83
- package/dist/index.d.ts +1 -1
- package/dist/index.js +163 -31
- package/dist/tailwind.css +1 -1
- package/package.json +4 -4
- package/src/api/schema.ts +20 -2
- package/src/app.ts +80 -12
- 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/ConversationDetails.tsx +6 -3
- package/src/client/conversations/ConversationHeader.tsx +9 -0
- package/src/client/conversations/ConversationHeaderActions.tsx +23 -0
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationMemories.tsx +64 -67
- package/src/client/conversations/ConversationPage.tsx +21 -8
- package/src/client/conversations/ConversationSidebar.tsx +80 -24
- package/src/client/conversations/ConversationWatches.tsx +15 -21
- 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/TranscriptMemoryPopover.tsx +20 -25
- package/src/client/conversations/TranscriptTurnContextView.tsx +9 -5
- 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/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/queries.ts +2 -1
- package/src/client/types.ts +1 -0
- package/src/conversation-cache.ts +1 -1
- package/src/mock-reporting/fixtures.ts +103 -27
- package/src/mock-reporting/routes.ts +47 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { ChevronRight } from "lucide-react";
|
|
2
|
-
import type { ConversationDetailReport } from "@sentry/junior/api/schema";
|
|
1
|
+
import { ArrowRight, BookmarkPlus, Brain, ChevronRight } from "lucide-react";
|
|
3
2
|
|
|
4
3
|
import { dashboardDisclosureChevronClass } from "../styles";
|
|
5
4
|
import type {
|
|
@@ -7,6 +6,7 @@ import type {
|
|
|
7
6
|
ConversationMemoryList,
|
|
8
7
|
} from "./conversationMemory";
|
|
9
8
|
import { DetailsCard } from "./DetailsList";
|
|
9
|
+
import { PrivateMemoryIcon } from "./MemoryIcons";
|
|
10
10
|
|
|
11
11
|
/** Count the distinct memories that a Conversation recalled and captured. */
|
|
12
12
|
function conversationMemoryCounts(list: ConversationMemoryList) {
|
|
@@ -48,28 +48,38 @@ export function ConversationMemoryCard(props: {
|
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
51
|
+
/** One memory with its full text, its visibility, and a jump to its Turn. */
|
|
52
|
+
function MemoryCard(props: {
|
|
53
|
+
entry: ConversationMemoryEntry;
|
|
54
|
+
onSelect(entry: ConversationMemoryEntry & { turnId: string }): void;
|
|
55
|
+
}) {
|
|
56
|
+
const { entry } = props;
|
|
57
|
+
const { turnId } = entry;
|
|
58
|
+
return (
|
|
59
|
+
<li className="grid overflow-hidden rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel">
|
|
60
|
+
{/* Break long URLs so the card keeps the panel width. */}
|
|
61
|
+
<p className="m-0 whitespace-pre-wrap px-3 py-2.5 text-sm leading-relaxed text-dashboard-text [overflow-wrap:anywhere]">
|
|
62
|
+
{entry.memory.content}
|
|
63
|
+
</p>
|
|
64
|
+
<div className="flex min-h-8 items-center justify-between gap-2 border-t border-dashboard-border-subtle bg-dashboard-fill-faint px-3 py-1 text-xs text-dashboard-text-muted">
|
|
65
|
+
{entry.memory.visibility === "private" ? (
|
|
66
|
+
<PrivateMemoryIcon />
|
|
67
|
+
) : (
|
|
68
|
+
<span />
|
|
69
|
+
)}
|
|
70
|
+
{turnId ? (
|
|
71
|
+
<button
|
|
72
|
+
className="-mr-1.5 inline-flex cursor-pointer items-center gap-1 rounded-md border-0 bg-transparent px-1.5 py-1 font-sans text-xs font-medium text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
73
|
+
onClick={() => props.onSelect({ ...entry, turnId })}
|
|
74
|
+
type="button"
|
|
75
|
+
>
|
|
76
|
+
Go to message
|
|
77
|
+
<ArrowRight aria-hidden="true" size={13} />
|
|
78
|
+
</button>
|
|
79
|
+
) : null}
|
|
80
|
+
</div>
|
|
81
|
+
</li>
|
|
82
|
+
);
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
/**
|
|
@@ -79,52 +89,39 @@ export function turnPrompts(
|
|
|
79
89
|
export function ConversationMemoryEntries(props: {
|
|
80
90
|
entries: ConversationMemoryEntry[];
|
|
81
91
|
onSelect(entry: ConversationMemoryEntry & { turnId: string }): void;
|
|
82
|
-
prompts: Map<string, string>;
|
|
83
92
|
}) {
|
|
93
|
+
const groups = (["recalled", "captured"] as const)
|
|
94
|
+
.map((source) => ({
|
|
95
|
+
entries: props.entries.filter((entry) => entry.source === source),
|
|
96
|
+
source,
|
|
97
|
+
}))
|
|
98
|
+
.filter((group) => group.entries.length > 0);
|
|
84
99
|
return (
|
|
85
|
-
<
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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}
|
|
107
|
-
</span>
|
|
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>
|
|
100
|
+
<div className="grid gap-5">
|
|
101
|
+
{groups.map((group) => (
|
|
102
|
+
<section className="grid gap-2" key={group.source}>
|
|
103
|
+
<h3 className="m-0 flex items-center gap-1.5 font-sans text-xs font-semibold text-dashboard-text">
|
|
104
|
+
{group.source === "recalled" ? (
|
|
105
|
+
<Brain aria-hidden="true" size={13} strokeWidth={1.8} />
|
|
122
106
|
) : (
|
|
123
|
-
<
|
|
107
|
+
<BookmarkPlus aria-hidden="true" size={13} strokeWidth={1.8} />
|
|
124
108
|
)}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
109
|
+
{group.source === "recalled" ? "Recalled" : "Captured"}
|
|
110
|
+
<span className="font-normal tabular-nums text-dashboard-text-muted">
|
|
111
|
+
{group.entries.length}
|
|
112
|
+
</span>
|
|
113
|
+
</h3>
|
|
114
|
+
<ol className="m-0 grid list-none gap-2 p-0">
|
|
115
|
+
{group.entries.map((entry) => (
|
|
116
|
+
<MemoryCard
|
|
117
|
+
entry={entry}
|
|
118
|
+
key={`${entry.source}:${entry.memory.id}`}
|
|
119
|
+
onSelect={props.onSelect}
|
|
120
|
+
/>
|
|
121
|
+
))}
|
|
122
|
+
</ol>
|
|
123
|
+
</section>
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
129
126
|
);
|
|
130
127
|
}
|
|
@@ -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,7 +32,7 @@ import {
|
|
|
31
32
|
import { ConversationHeader } from "./ConversationHeader";
|
|
32
33
|
import { ConversationReplyFooter } from "./ConversationReplyFooter";
|
|
33
34
|
import { ConversationDetails } from "./ConversationDetails";
|
|
34
|
-
import {
|
|
35
|
+
import { AutoArchiveToggle } from "./AutoArchiveToggle";
|
|
35
36
|
import {
|
|
36
37
|
ConversationMemoryContext,
|
|
37
38
|
memoryAnchorKey,
|
|
@@ -208,6 +209,7 @@ export function ConversationPage(props: {
|
|
|
208
209
|
[memoryList, memoryPopover],
|
|
209
210
|
);
|
|
210
211
|
const archive = useArchiveConversation(conversationId);
|
|
212
|
+
const pin = usePinConversation(conversationId);
|
|
211
213
|
const markRead = useMarkConversationRead(conversationId);
|
|
212
214
|
const readAtOpen = useReadPositionAtOpen(detail.data, detail.fetchedDetail);
|
|
213
215
|
const feedConversation = conversations.find(
|
|
@@ -330,10 +332,6 @@ export function ConversationPage(props: {
|
|
|
330
332
|
}, [linkedTurnNotFound, linkedTurnSeq, setSelectedEventSeq]);
|
|
331
333
|
// Show a chosen memory at its transcript row. Wait for the first page, then
|
|
332
334
|
// load earlier pages until the row renders, or stop when history ends.
|
|
333
|
-
const memoryPrompts = useMemo(
|
|
334
|
-
() => turnPrompts(transcript?.events),
|
|
335
|
-
[transcript?.events],
|
|
336
|
-
);
|
|
337
335
|
useEffect(() => {
|
|
338
336
|
if (!memoryJump || !transcript) return;
|
|
339
337
|
const anchor = pageRef.current?.querySelector(
|
|
@@ -488,6 +486,14 @@ export function ConversationPage(props: {
|
|
|
488
486
|
<ConversationAnnotations detail={detail.data} />
|
|
489
487
|
) : null
|
|
490
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
|
+
}}
|
|
491
497
|
archive={{
|
|
492
498
|
archived: Boolean(conversation?.archivedAt),
|
|
493
499
|
disabled: !conversation || archive.isPending,
|
|
@@ -612,13 +618,20 @@ export function ConversationPage(props: {
|
|
|
612
618
|
{showDetails ? (
|
|
613
619
|
<ConversationMemoryContext value={memoryState}>
|
|
614
620
|
<ConversationDetails
|
|
621
|
+
autoArchive={
|
|
622
|
+
detail.data?.autoArchive === undefined ? null : (
|
|
623
|
+
<AutoArchiveToggle
|
|
624
|
+
autoArchive={detail.data.autoArchive}
|
|
625
|
+
conversationId={conversationId}
|
|
626
|
+
/>
|
|
627
|
+
)
|
|
628
|
+
}
|
|
615
629
|
brief={detail.data?.brief}
|
|
616
630
|
conversationId={conversationId}
|
|
617
631
|
highlightReview={reviewOpen}
|
|
618
632
|
identity={identity}
|
|
619
633
|
onClose={closeDetails}
|
|
620
634
|
onOpenSubagent={setSubagentTarget}
|
|
621
|
-
memoryPrompts={memoryPrompts}
|
|
622
635
|
onSelectMemory={(entry) => {
|
|
623
636
|
// Memory rows show only in the Conversation view, and search
|
|
624
637
|
// can hide them.
|
|
@@ -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
|
);
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
Timer,
|
|
16
16
|
Users,
|
|
17
17
|
} from "lucide-react";
|
|
18
|
-
import {
|
|
18
|
+
import type { ReactNode } from "react";
|
|
19
19
|
import {
|
|
20
20
|
conversationWatchesReportSchema,
|
|
21
21
|
type ConversationWatch,
|
|
@@ -24,6 +24,12 @@ import {
|
|
|
24
24
|
import { DisplayText } from "../display-text/DisplayText";
|
|
25
25
|
import { formatTime } from "../format";
|
|
26
26
|
import { fetchDashboardJson } from "../http";
|
|
27
|
+
import { conversationSideReadQueryKey } from "./queries";
|
|
28
|
+
import {
|
|
29
|
+
Collapsible,
|
|
30
|
+
CollapsibleContent,
|
|
31
|
+
CollapsibleTrigger,
|
|
32
|
+
} from "../components/ui/collapsible";
|
|
27
33
|
import { dashboardDisclosureChevronClass } from "../styles";
|
|
28
34
|
import { DetailsList, DetailsRow } from "./DetailsList";
|
|
29
35
|
|
|
@@ -36,7 +42,7 @@ export function useConversationWatches(conversationId: string) {
|
|
|
36
42
|
`/api/conversations/${encodeURIComponent(conversationId)}/watches`,
|
|
37
43
|
signal,
|
|
38
44
|
),
|
|
39
|
-
queryKey:
|
|
45
|
+
queryKey: conversationSideReadQueryKey(conversationId, "watches"),
|
|
40
46
|
retry: false,
|
|
41
47
|
});
|
|
42
48
|
}
|
|
@@ -76,7 +82,6 @@ const resourceIcons: Partial<Record<string, LucideIcon>> = {
|
|
|
76
82
|
*/
|
|
77
83
|
function WatchRow(props: { watch: ConversationWatch }) {
|
|
78
84
|
const { watch } = props;
|
|
79
|
-
const [expanded, setExpanded] = useState(false);
|
|
80
85
|
const Icon = watch.redacted
|
|
81
86
|
? LockKeyhole
|
|
82
87
|
: (resourceIcons[watch.resourceType] ?? Eye);
|
|
@@ -94,7 +99,6 @@ function WatchRow(props: { watch: ConversationWatch }) {
|
|
|
94
99
|
/>
|
|
95
100
|
</span>
|
|
96
101
|
);
|
|
97
|
-
const toggle = () => setExpanded((value) => !value);
|
|
98
102
|
const row = watch.url ? (
|
|
99
103
|
// The row opens the watched resource, like a linked work row. The
|
|
100
104
|
// chevron beside it shows the details.
|
|
@@ -102,32 +106,22 @@ function WatchRow(props: { watch: ConversationWatch }) {
|
|
|
102
106
|
<div className="min-w-0 flex-1">
|
|
103
107
|
<DetailsRow href={watch.url} icon={icon} title={title} />
|
|
104
108
|
</div>
|
|
105
|
-
<
|
|
106
|
-
aria-expanded={expanded}
|
|
109
|
+
<CollapsibleTrigger
|
|
107
110
|
aria-label={`Details for ${title}`}
|
|
108
111
|
className="group grid size-8 shrink-0 place-items-center rounded-md 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"
|
|
109
|
-
onClick={toggle}
|
|
110
|
-
type="button"
|
|
111
112
|
>
|
|
112
113
|
{chevron}
|
|
113
|
-
</
|
|
114
|
+
</CollapsibleTrigger>
|
|
114
115
|
</div>
|
|
115
116
|
) : (
|
|
116
|
-
<DetailsRow
|
|
117
|
-
expanded={expanded}
|
|
118
|
-
icon={icon}
|
|
119
|
-
meta={chevron}
|
|
120
|
-
onToggle={toggle}
|
|
121
|
-
title={title}
|
|
122
|
-
/>
|
|
117
|
+
<DetailsRow disclosure icon={icon} meta={chevron} title={title} />
|
|
123
118
|
);
|
|
124
|
-
if (!expanded) return row;
|
|
125
119
|
|
|
126
120
|
return (
|
|
127
|
-
|
|
121
|
+
<Collapsible className="min-w-0">
|
|
128
122
|
{row}
|
|
129
123
|
{/* Indent under the title: icon column, column gap, and row padding. */}
|
|
130
|
-
<
|
|
124
|
+
<CollapsibleContent className="grid min-w-0 gap-1.5 pb-2 pl-9 pr-1.5 pt-0.5 text-xs leading-4 text-dashboard-text-muted">
|
|
131
125
|
{watch.intent ? (
|
|
132
126
|
<p className="m-0 break-words">
|
|
133
127
|
<DisplayText inline text={watch.intent} />
|
|
@@ -166,8 +160,8 @@ function WatchRow(props: { watch: ConversationWatch }) {
|
|
|
166
160
|
</span>
|
|
167
161
|
</p>
|
|
168
162
|
) : null}
|
|
169
|
-
</
|
|
170
|
-
|
|
163
|
+
</CollapsibleContent>
|
|
164
|
+
</Collapsible>
|
|
171
165
|
);
|
|
172
166
|
}
|
|
173
167
|
|
|
@@ -11,14 +11,14 @@ import { pageCount, pageItems, PagePagination } from "../components/Pagination";
|
|
|
11
11
|
import { ConversationHomeList } from "./ConversationHomeList";
|
|
12
12
|
import { ConversationComposer } from "./ConversationComposer";
|
|
13
13
|
import {
|
|
14
|
-
useCreateConversation,
|
|
15
14
|
usePendingArchiveConversationUpdates,
|
|
16
15
|
type PendingArchiveConversationUpdate,
|
|
17
|
-
} from "./
|
|
16
|
+
} from "./conversationArchive";
|
|
17
|
+
import { useCreateConversation } from "./queries";
|
|
18
18
|
import { conversationPath, NEW_CONVERSATION_PATH } from "./conversationRoutes";
|
|
19
19
|
import {
|
|
20
20
|
attentionConversationIds,
|
|
21
|
-
|
|
21
|
+
pinnedAndAttentionFirst,
|
|
22
22
|
} from "./conversationSections";
|
|
23
23
|
import { buildConversations, getDashboardTimeZone } from "../format";
|
|
24
24
|
import { useDebouncedValue } from "../debouncedValue";
|
|
@@ -73,7 +73,7 @@ export function ConversationWorkspace() {
|
|
|
73
73
|
const pagedConversations = useMemo(
|
|
74
74
|
() =>
|
|
75
75
|
pageItems(
|
|
76
|
-
|
|
76
|
+
pinnedAndAttentionFirst(
|
|
77
77
|
conversations,
|
|
78
78
|
attentionConversationIds(
|
|
79
79
|
waitingConversationIds,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ChevronRight } from "lucide-react";
|
|
2
2
|
import { useState, type ReactNode } from "react";
|
|
3
3
|
|
|
4
|
+
import { CollapsibleTrigger } from "../components/ui/collapsible";
|
|
4
5
|
import { cn, dashboardDisclosureChevronClass } from "../styles";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -87,21 +88,20 @@ export function DetailsList<T>(props: {
|
|
|
87
88
|
/**
|
|
88
89
|
* Lay out one details list row: an icon, a title, and short trailing text on
|
|
89
90
|
* the first line, then optional lines under the title. A row with `href`
|
|
90
|
-
* opens that link in a new tab. A
|
|
91
|
-
*
|
|
91
|
+
* opens that link in a new tab. A `disclosure` row is the trigger of the
|
|
92
|
+
* enclosing `Collapsible`, so it shows or hides the content under it.
|
|
92
93
|
*/
|
|
93
94
|
export function DetailsRow(props: {
|
|
94
95
|
children?: ReactNode;
|
|
95
|
-
|
|
96
|
+
disclosure?: boolean;
|
|
96
97
|
href?: string;
|
|
97
98
|
icon: ReactNode;
|
|
98
99
|
meta?: ReactNode;
|
|
99
|
-
onToggle?(): void;
|
|
100
100
|
title: ReactNode;
|
|
101
101
|
/** Native hover text with detail that the row does not show. */
|
|
102
102
|
tooltip?: string;
|
|
103
103
|
}) {
|
|
104
|
-
const interactive = props.href !== undefined || props.
|
|
104
|
+
const interactive = props.href !== undefined || props.disclosure === true;
|
|
105
105
|
const className = cn(
|
|
106
106
|
"grid w-full min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-x-2.5 gap-y-1 rounded-md px-1.5 py-1.5 text-left text-inherit no-underline",
|
|
107
107
|
interactive &&
|
|
@@ -134,17 +134,11 @@ export function DetailsRow(props: {
|
|
|
134
134
|
</a>
|
|
135
135
|
);
|
|
136
136
|
}
|
|
137
|
-
if (props.
|
|
137
|
+
if (props.disclosure) {
|
|
138
138
|
return (
|
|
139
|
-
<
|
|
140
|
-
aria-expanded={props.expanded ?? false}
|
|
141
|
-
className={className}
|
|
142
|
-
onClick={props.onToggle}
|
|
143
|
-
title={props.tooltip}
|
|
144
|
-
type="button"
|
|
145
|
-
>
|
|
139
|
+
<CollapsibleTrigger className={className} title={props.tooltip}>
|
|
146
140
|
{content}
|
|
147
|
-
</
|
|
141
|
+
</CollapsibleTrigger>
|
|
148
142
|
);
|
|
149
143
|
}
|
|
150
144
|
return (
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ArrowUpRight, Lock } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
/** Mark a memory that only its owner can see. */
|
|
4
|
+
export function PrivateMemoryIcon() {
|
|
5
|
+
return <Lock aria-label="Private" role="img" size={13} strokeWidth={1.8} />;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* End a memory row that links to the memory's page. Its parent link needs
|
|
10
|
+
* the `group` class, so the arrow follows the row hover.
|
|
11
|
+
*/
|
|
12
|
+
export function MemoryLinkIcons(props: { private: boolean }) {
|
|
13
|
+
return (
|
|
14
|
+
<span className="flex items-center gap-1.5 pt-1 text-dashboard-text-faint">
|
|
15
|
+
{props.private ? <PrivateMemoryIcon /> : null}
|
|
16
|
+
<ArrowUpRight
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
className="transition-colors group-hover:text-dashboard-text"
|
|
19
|
+
size={14}
|
|
20
|
+
/>
|
|
21
|
+
</span>
|
|
22
|
+
);
|
|
23
|
+
}
|