@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
|
@@ -22,6 +22,7 @@ import { TranscriptSubagentView } from "./TranscriptSubagentView";
|
|
|
22
22
|
import { TranscriptContextEventView } from "./TranscriptContextEventView";
|
|
23
23
|
import {
|
|
24
24
|
isCollapsibleActivityEntry,
|
|
25
|
+
quietTurnEntryKeys,
|
|
25
26
|
TranscriptActivityGroup,
|
|
26
27
|
} from "./TranscriptActivityGroup";
|
|
27
28
|
import { TranscriptToolView } from "./TranscriptToolView";
|
|
@@ -232,9 +233,7 @@ const VisibleTranscriptEntries = memo(function VisibleTranscriptEntries(props: {
|
|
|
232
233
|
keyPrefix={props.conversation.conversationId}
|
|
233
234
|
unread={props.unread}
|
|
234
235
|
renderContext={(entry) => (
|
|
235
|
-
<TranscriptRailEvent
|
|
236
|
-
kind={entry.part.event.type === "handoff" ? "handoff" : "compaction"}
|
|
237
|
-
>
|
|
236
|
+
<TranscriptRailEvent kind="compaction">
|
|
238
237
|
<TranscriptContextEventView
|
|
239
238
|
part={entry.part}
|
|
240
239
|
timestamp={entry.timestamp}
|
|
@@ -359,6 +358,12 @@ function TranscriptEntryList(props: {
|
|
|
359
358
|
}) {
|
|
360
359
|
const search = useTranscriptSearch();
|
|
361
360
|
const activityKeys = useRef(new Map<string, string>());
|
|
361
|
+
const quietKeys = useMemo(
|
|
362
|
+
() => quietTurnEntryKeys(props.entries),
|
|
363
|
+
[props.entries],
|
|
364
|
+
);
|
|
365
|
+
const collapsible = (entry: TranscriptEntry) =>
|
|
366
|
+
isCollapsibleActivityEntry(entry) || quietKeys.has(entry.key);
|
|
362
367
|
const claimedActivityKeys = new Set<string>();
|
|
363
368
|
const rows: ReactNode[] = [];
|
|
364
369
|
// Search results are not a reading order, so they get no marker.
|
|
@@ -394,11 +399,11 @@ function TranscriptEntryList(props: {
|
|
|
394
399
|
for (let index = 0; index < props.entries.length; ) {
|
|
395
400
|
const entry = props.entries[index]!;
|
|
396
401
|
|
|
397
|
-
if (
|
|
402
|
+
if (collapsible(entry)) {
|
|
398
403
|
const activityEntries: RenderedTranscriptEntry[] = [];
|
|
399
404
|
while (
|
|
400
405
|
index < props.entries.length &&
|
|
401
|
-
|
|
406
|
+
collapsible(props.entries[index]!)
|
|
402
407
|
) {
|
|
403
408
|
activityEntries.push(props.entries[index]!);
|
|
404
409
|
index += 1;
|
|
@@ -523,9 +528,7 @@ function RedactedTranscriptView(props: {
|
|
|
523
528
|
entries={groupTranscriptMessages(props.messages)}
|
|
524
529
|
keyPrefix={`${props.conversation.conversationId}:redacted`}
|
|
525
530
|
renderContext={(entry) => (
|
|
526
|
-
<TranscriptRailEvent
|
|
527
|
-
kind={entry.part.event.type === "handoff" ? "handoff" : "compaction"}
|
|
528
|
-
>
|
|
531
|
+
<TranscriptRailEvent kind="compaction">
|
|
529
532
|
<TranscriptContextEventView
|
|
530
533
|
part={entry.part}
|
|
531
534
|
timestamp={entry.timestamp}
|
|
@@ -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
|
|
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
import type { InputImage } from "@sentry/junior/api/schema";
|
|
2
|
-
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { Globe2, LockKeyhole } from "lucide-react";
|
|
2
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Globe2, LockKeyhole, PanelLeft } from "lucide-react";
|
|
4
4
|
import { useNavigate, useParams } from "react-router";
|
|
5
|
+
import type { PanelImperativeHandle } from "react-resizable-panels";
|
|
5
6
|
|
|
6
7
|
import { useConversationsData } from "../api";
|
|
7
8
|
import { ConversationSidebar } from "./ConversationSidebar";
|
|
8
9
|
import { SearchInput } from "../components/SearchInput";
|
|
10
|
+
import {
|
|
11
|
+
ResizableHandle,
|
|
12
|
+
ResizablePanel,
|
|
13
|
+
ResizablePanelGroup,
|
|
14
|
+
} from "../components/ui/resizable";
|
|
9
15
|
import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group";
|
|
10
16
|
import { pageCount, pageItems, PagePagination } from "../components/Pagination";
|
|
11
17
|
import { ConversationHomeList } from "./ConversationHomeList";
|
|
12
18
|
import { ConversationComposer } from "./ConversationComposer";
|
|
19
|
+
import { HeaderIconButton } from "./ConversationHeaderActions";
|
|
13
20
|
import {
|
|
14
|
-
useCreateConversation,
|
|
15
21
|
usePendingArchiveConversationUpdates,
|
|
16
22
|
type PendingArchiveConversationUpdate,
|
|
17
|
-
} from "./
|
|
23
|
+
} from "./conversationArchive";
|
|
24
|
+
import { useCreateConversation, type ConversationData } from "./queries";
|
|
18
25
|
import { conversationPath, NEW_CONVERSATION_PATH } from "./conversationRoutes";
|
|
19
26
|
import {
|
|
20
27
|
attentionConversationIds,
|
|
21
|
-
|
|
28
|
+
pinnedAndAttentionFirst,
|
|
22
29
|
} from "./conversationSections";
|
|
23
30
|
import { buildConversations, getDashboardTimeZone } from "../format";
|
|
24
31
|
import { useDebouncedValue } from "../debouncedValue";
|
|
32
|
+
import { cn } from "../styles";
|
|
25
33
|
import type { Conversation } from "../types";
|
|
26
34
|
import { ConversationPage } from "./ConversationPage";
|
|
27
35
|
import {
|
|
@@ -30,13 +38,29 @@ import {
|
|
|
30
38
|
} from "../review-desk/queries";
|
|
31
39
|
import { UnsentAnswersProvider } from "../review-desk/UnsentAnswers";
|
|
32
40
|
import { useConversationFinishedIndicators } from "./useConversationFinishedIndicators";
|
|
41
|
+
import {
|
|
42
|
+
readSidebarWidth,
|
|
43
|
+
saveSidebarWidth,
|
|
44
|
+
SIDEBAR_WIDTH,
|
|
45
|
+
useConversationSidebarOpen,
|
|
46
|
+
useSidebarFits,
|
|
47
|
+
} from "./conversationSidebarLayout";
|
|
33
48
|
|
|
34
49
|
const CONVERSATION_PAGE_SIZE = 20;
|
|
50
|
+
const SIDEBAR_PANEL_ID = "conversation-list";
|
|
35
51
|
|
|
36
52
|
/** Render the conversation home page or one selected conversation. */
|
|
37
|
-
export function ConversationWorkspace(
|
|
53
|
+
export function ConversationWorkspace(props: {
|
|
54
|
+
/** The selected Conversation's data, from the shell. */
|
|
55
|
+
detail: ConversationData;
|
|
56
|
+
}) {
|
|
38
57
|
const [page, setPage] = useState(1);
|
|
39
58
|
const [query, setQuery] = useState("");
|
|
59
|
+
const [sidebarOpen, setSidebarOpen] = useConversationSidebarOpen();
|
|
60
|
+
const sidebarFits = useSidebarFits();
|
|
61
|
+
const sidebarPanel = useRef<PanelImperativeHandle>(null);
|
|
62
|
+
const sidebarGroup = useRef<HTMLDivElement>(null);
|
|
63
|
+
const sidebarId = useId();
|
|
40
64
|
const search = useDebouncedValue(query.trim());
|
|
41
65
|
const params = useParams();
|
|
42
66
|
const navigate = useNavigate();
|
|
@@ -73,7 +97,7 @@ export function ConversationWorkspace() {
|
|
|
73
97
|
const pagedConversations = useMemo(
|
|
74
98
|
() =>
|
|
75
99
|
pageItems(
|
|
76
|
-
|
|
100
|
+
pinnedAndAttentionFirst(
|
|
77
101
|
conversations,
|
|
78
102
|
attentionConversationIds(
|
|
79
103
|
waitingConversationIds,
|
|
@@ -146,45 +170,109 @@ export function ConversationWorkspace() {
|
|
|
146
170
|
);
|
|
147
171
|
}
|
|
148
172
|
|
|
173
|
+
const toggleSidebar = () => {
|
|
174
|
+
const panel = sidebarPanel.current;
|
|
175
|
+
if (!panel) return;
|
|
176
|
+
if (panel.isCollapsed()) panel.resize(readSidebarWidth());
|
|
177
|
+
else panel.collapse();
|
|
178
|
+
};
|
|
179
|
+
|
|
149
180
|
return (
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
<ResizablePanelGroup
|
|
182
|
+
className="min-h-0 overflow-hidden bg-dashboard-bg"
|
|
183
|
+
elementRef={sidebarGroup}
|
|
184
|
+
onLayoutChanged={(layout) => {
|
|
185
|
+
// Save after the drag ends. A drag that collapses the list passes
|
|
186
|
+
// the minimum width first, and that width is not the reader's pick.
|
|
187
|
+
const percent = layout[SIDEBAR_PANEL_ID] ?? 0;
|
|
188
|
+
const groupWidth = sidebarGroup.current?.offsetWidth ?? 0;
|
|
189
|
+
if (percent > 0) saveSidebarWidth((percent / 100) * groupWidth);
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
{sidebarFits ? (
|
|
193
|
+
<>
|
|
194
|
+
<ResizablePanel
|
|
195
|
+
collapsible
|
|
196
|
+
defaultSize={sidebarOpen ? readSidebarWidth() : 0}
|
|
197
|
+
groupResizeBehavior="preserve-pixel-size"
|
|
198
|
+
id={SIDEBAR_PANEL_ID}
|
|
199
|
+
maxSize={SIDEBAR_WIDTH.max}
|
|
200
|
+
minSize={SIDEBAR_WIDTH.min}
|
|
201
|
+
onResize={(size) => {
|
|
202
|
+
// Dragging past the minimum width collapses the list too, so the
|
|
203
|
+
// panel size is the one source for the open state.
|
|
204
|
+
const open = size.inPixels > 0;
|
|
205
|
+
if (open !== sidebarOpen) setSidebarOpen(open);
|
|
206
|
+
}}
|
|
207
|
+
panelRef={sidebarPanel}
|
|
208
|
+
>
|
|
209
|
+
<div
|
|
210
|
+
className={cn(
|
|
211
|
+
"h-full min-h-0 overflow-hidden",
|
|
212
|
+
!sidebarOpen && "hidden",
|
|
213
|
+
)}
|
|
214
|
+
id={sidebarId}
|
|
215
|
+
>
|
|
216
|
+
<ConversationSidebar
|
|
217
|
+
conversations={conversations}
|
|
218
|
+
error={feed.error?.message}
|
|
219
|
+
finishedConversationIds={finishedConversationIds}
|
|
220
|
+
heldConversationId={heldConversationId}
|
|
221
|
+
loading={feed.isPending}
|
|
222
|
+
onNewConversation={openCreate}
|
|
223
|
+
onQueryChange={setQuery}
|
|
224
|
+
query={query}
|
|
225
|
+
selectedId={selectedId}
|
|
226
|
+
timeZone={getDashboardTimeZone()}
|
|
227
|
+
waitingConversations={waitingConversations}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
</ResizablePanel>
|
|
231
|
+
<ResizableHandle
|
|
232
|
+
aria-label="Resize conversation list"
|
|
233
|
+
className="w-px bg-transparent transition-colors after:w-2 focus-visible:ring-dashboard-border-interactive data-[separator=active]:bg-dashboard-border-interactive data-[separator=hover]:bg-dashboard-border-interactive"
|
|
184
234
|
/>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
235
|
+
</>
|
|
236
|
+
) : null}
|
|
237
|
+
<ResizablePanel id="selected-conversation">
|
|
238
|
+
<section
|
|
239
|
+
aria-label="Selected conversation"
|
|
240
|
+
className="grid h-full min-h-0 grid-rows-[minmax(0,1fr)] overflow-hidden bg-dashboard-bg"
|
|
241
|
+
>
|
|
242
|
+
<UnsentAnswersProvider conversationId={selectedId} key={selectedId}>
|
|
243
|
+
<ConversationPage
|
|
244
|
+
conversationId={selectedId}
|
|
245
|
+
detail={props.detail}
|
|
246
|
+
data={
|
|
247
|
+
feed.data
|
|
248
|
+
? {
|
|
249
|
+
conversations: feed.data,
|
|
250
|
+
}
|
|
251
|
+
: undefined
|
|
252
|
+
}
|
|
253
|
+
onRead={markRead}
|
|
254
|
+
pendingArchiveUpdate={pendingArchiveUpdates.find(
|
|
255
|
+
(update) => update.conversationId === selectedId,
|
|
256
|
+
)}
|
|
257
|
+
sidebarToggle={
|
|
258
|
+
<HeaderIconButton
|
|
259
|
+
controls={sidebarId}
|
|
260
|
+
expanded={sidebarOpen}
|
|
261
|
+
label={
|
|
262
|
+
sidebarOpen
|
|
263
|
+
? "Hide conversation list"
|
|
264
|
+
: "Show conversation list"
|
|
265
|
+
}
|
|
266
|
+
onClick={toggleSidebar}
|
|
267
|
+
>
|
|
268
|
+
<PanelLeft aria-hidden="true" size={15} strokeWidth={2} />
|
|
269
|
+
</HeaderIconButton>
|
|
270
|
+
}
|
|
271
|
+
/>
|
|
272
|
+
</UnsentAnswersProvider>
|
|
273
|
+
</section>
|
|
274
|
+
</ResizablePanel>
|
|
275
|
+
</ResizablePanelGroup>
|
|
188
276
|
);
|
|
189
277
|
}
|
|
190
278
|
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
|
|
1
3
|
import { Skeleton } from "../components/ui/skeleton";
|
|
2
4
|
import { dashboardContainerClass, cn } from "../styles";
|
|
3
5
|
import { ChatLayout } from "./ChatLayout";
|
|
4
6
|
import { ConversationHomeListLoading } from "./ConversationHomeList";
|
|
7
|
+
import {
|
|
8
|
+
readSidebarWidth,
|
|
9
|
+
useConversationSidebarOpen,
|
|
10
|
+
} from "./conversationSidebarLayout";
|
|
5
11
|
|
|
6
12
|
/** Keep the conversation workspace geometry stable while its first data loads. */
|
|
7
13
|
export function ConversationWorkspaceLoading(props: { detail: boolean }) {
|
|
14
|
+
const [sidebarOpen] = useConversationSidebarOpen();
|
|
8
15
|
if (!props.detail) return <ConversationHomeLoading />;
|
|
9
16
|
return (
|
|
10
17
|
<div
|
|
@@ -12,12 +19,19 @@ export function ConversationWorkspaceLoading(props: { detail: boolean }) {
|
|
|
12
19
|
aria-live="polite"
|
|
13
20
|
className={cn(
|
|
14
21
|
dashboardContainerClass,
|
|
15
|
-
"grid h-full min-h-0
|
|
22
|
+
"grid h-full min-h-0 grid-cols-[minmax(0,1fr)] overflow-hidden",
|
|
23
|
+
sidebarOpen &&
|
|
24
|
+
"md:grid-cols-[var(--conversation-sidebar-width)_minmax(0,1fr)]",
|
|
16
25
|
)}
|
|
17
26
|
role="status"
|
|
27
|
+
style={
|
|
28
|
+
{
|
|
29
|
+
"--conversation-sidebar-width": `${readSidebarWidth()}px`,
|
|
30
|
+
} as CSSProperties
|
|
31
|
+
}
|
|
18
32
|
>
|
|
19
33
|
<span className="sr-only">Loading conversation</span>
|
|
20
|
-
<ConversationSidebarLoading />
|
|
34
|
+
{sidebarOpen ? <ConversationSidebarLoading /> : null}
|
|
21
35
|
<ConversationDetailLoading />
|
|
22
36
|
</div>
|
|
23
37
|
);
|
|
@@ -90,7 +104,7 @@ function ConversationDetailLoading() {
|
|
|
90
104
|
>
|
|
91
105
|
<ChatLayout
|
|
92
106
|
scrollAriaLabel="Conversation transcript"
|
|
93
|
-
scrollClassName="px-3 pb-1.5 md:px-7 md:pb-2"
|
|
107
|
+
scrollClassName="thin-scrollbar px-3 pb-1.5 md:px-7 md:pb-2"
|
|
94
108
|
scroll={
|
|
95
109
|
<section className="min-w-0">
|
|
96
110
|
<div className="mb-4 grid gap-3 border-b border-dashboard-border-subtle py-4">
|
|
@@ -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 (
|
|
@@ -14,67 +14,17 @@ import { dashboardDisclosureChevronClass } from "../styles";
|
|
|
14
14
|
import { RedactedMarker } from "./TranscriptRedacted";
|
|
15
15
|
import { DetailsCard } from "./DetailsList";
|
|
16
16
|
import { DisplayText } from "../display-text/DisplayText";
|
|
17
|
-
import { eventLogModel } from "./eventLog";
|
|
18
|
-
import { formatCostBreakdown } from "../format";
|
|
19
17
|
import { EventUsage } from "./EventUsage";
|
|
20
18
|
|
|
21
19
|
/** Present event content as readable sections, with the exact report available on demand. */
|
|
22
20
|
export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
23
21
|
const data = event.data;
|
|
24
22
|
const mentions = event.mentions ?? [];
|
|
25
|
-
const model =
|
|
23
|
+
const model = event.model;
|
|
26
24
|
const { usage, ...callAttributes } = event.modelCall ?? {};
|
|
27
25
|
let content: ReactNode;
|
|
28
26
|
|
|
29
27
|
switch (data.type) {
|
|
30
|
-
case "turn_routed": {
|
|
31
|
-
const {
|
|
32
|
-
type: _,
|
|
33
|
-
modelId: _modelId,
|
|
34
|
-
modelProfile: _profile,
|
|
35
|
-
reasoningLevel: _reasoning,
|
|
36
|
-
costUsd,
|
|
37
|
-
...metadata
|
|
38
|
-
} = data;
|
|
39
|
-
content = (
|
|
40
|
-
<DetailSection title="Route selection">
|
|
41
|
-
<DetailValue
|
|
42
|
-
value={{
|
|
43
|
-
...metadata,
|
|
44
|
-
...(costUsd !== undefined
|
|
45
|
-
? { routerCostUsd: formatCostBreakdown({ total: costUsd }) }
|
|
46
|
-
: undefined),
|
|
47
|
-
}}
|
|
48
|
-
/>
|
|
49
|
-
</DetailSection>
|
|
50
|
-
);
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
case "handoff": {
|
|
54
|
-
const {
|
|
55
|
-
type: _,
|
|
56
|
-
modelId: _modelId,
|
|
57
|
-
modelProfile: _profile,
|
|
58
|
-
reasoningLevel: _reasoning,
|
|
59
|
-
summary,
|
|
60
|
-
...metadata
|
|
61
|
-
} = data;
|
|
62
|
-
content = (
|
|
63
|
-
<>
|
|
64
|
-
{Object.keys(metadata).length > 0 ? (
|
|
65
|
-
<DetailSection>
|
|
66
|
-
<DetailValue value={metadata} />
|
|
67
|
-
</DetailSection>
|
|
68
|
-
) : null}
|
|
69
|
-
{summary ? (
|
|
70
|
-
<DetailSection title="Continuation summary">
|
|
71
|
-
<DisplayText mentions={mentions} text={summary} />
|
|
72
|
-
</DetailSection>
|
|
73
|
-
) : null}
|
|
74
|
-
</>
|
|
75
|
-
);
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
28
|
case "message": {
|
|
79
29
|
const { type: _, text, redacted, ...metadata } = data;
|
|
80
30
|
content = (
|
|
@@ -172,20 +122,10 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
172
122
|
|
|
173
123
|
return (
|
|
174
124
|
<div className="@container grid min-w-0 gap-3">
|
|
175
|
-
<DetailSection
|
|
176
|
-
title={
|
|
177
|
-
data.type === "handoff" ? "Target configuration" : "Configuration"
|
|
178
|
-
}
|
|
179
|
-
>
|
|
125
|
+
<DetailSection title="Configuration">
|
|
180
126
|
<DetailValue
|
|
181
127
|
value={{
|
|
182
128
|
model: model?.modelId ?? "Not recorded",
|
|
183
|
-
...(model
|
|
184
|
-
? {
|
|
185
|
-
modelProfile: model.modelProfile ?? "Not recorded",
|
|
186
|
-
reasoningLevel: model.reasoningLevel ?? "Not recorded",
|
|
187
|
-
}
|
|
188
|
-
: undefined),
|
|
189
129
|
...callAttributes,
|
|
190
130
|
}}
|
|
191
131
|
/>
|
|
@@ -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
|
+
}
|
|
@@ -42,6 +42,33 @@ export function isCollapsibleActivityEntry(
|
|
|
42
42
|
return true;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Keys of message rows that fold into activity: each quiet reply and the
|
|
47
|
+
* Event messages that started its Turn. Event messages wait for the next
|
|
48
|
+
* reply; a message from a person clears them.
|
|
49
|
+
*/
|
|
50
|
+
export function quietTurnEntryKeys(
|
|
51
|
+
entries: readonly RenderedTranscriptEntry[],
|
|
52
|
+
): Set<string> {
|
|
53
|
+
const keys = new Set<string>();
|
|
54
|
+
let eventKeys: string[] = [];
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (entry.kind !== "message") continue;
|
|
57
|
+
const { message } = entry;
|
|
58
|
+
if (message.role === "user") {
|
|
59
|
+
eventKeys = message.eventType ? [...eventKeys, entry.key] : [];
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (message.role !== "assistant") continue;
|
|
63
|
+
if (message.quiet) {
|
|
64
|
+
keys.add(entry.key);
|
|
65
|
+
for (const key of eventKeys) keys.add(key);
|
|
66
|
+
}
|
|
67
|
+
eventKeys = [];
|
|
68
|
+
}
|
|
69
|
+
return keys;
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
function entryStartMs(entry: RenderedTranscriptEntry): number | undefined {
|
|
46
73
|
if (entry.kind === "tool") {
|
|
47
74
|
return entry.part.startedTimestamp ?? entry.timestamp;
|
|
@@ -104,25 +131,18 @@ export function activityGroupSummary(
|
|
|
104
131
|
(entry) =>
|
|
105
132
|
entry.kind === "context" && entry.part.event.type === "compaction",
|
|
106
133
|
).length;
|
|
107
|
-
const handoffs = entries.flatMap((entry) =>
|
|
108
|
-
entry.kind === "context" && entry.part.event.type === "handoff"
|
|
109
|
-
? [entry.part.event]
|
|
110
|
-
: [],
|
|
111
|
-
);
|
|
112
134
|
const structuredCount = entries.filter(
|
|
113
135
|
(entry) => entry.kind === "structured_event",
|
|
114
136
|
).length;
|
|
115
137
|
const attachmentsCount = entries.filter(
|
|
116
138
|
(entry) => entry.kind === "attachments_delivered",
|
|
117
139
|
).length;
|
|
118
|
-
const eventCount = entries.filter(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
? countLabel(handoffs.length, "model handoff", "model handoffs")
|
|
125
|
-
: undefined;
|
|
140
|
+
const eventCount = entries.filter(
|
|
141
|
+
(entry) => entry.kind === "message" && entry.message.role !== "assistant",
|
|
142
|
+
).length;
|
|
143
|
+
const quietReplyCount = entries.filter(
|
|
144
|
+
(entry) => entry.kind === "message" && entry.message.role === "assistant",
|
|
145
|
+
).length;
|
|
126
146
|
|
|
127
147
|
const parts = [
|
|
128
148
|
toolCount > 0
|
|
@@ -134,7 +154,6 @@ export function activityGroupSummary(
|
|
|
134
154
|
compactionCount > 0
|
|
135
155
|
? countLabel(compactionCount, "context compacted", "context compacted")
|
|
136
156
|
: undefined,
|
|
137
|
-
handoffSummary,
|
|
138
157
|
structuredCount > 0
|
|
139
158
|
? countLabel(structuredCount, "1 structured event", "structured events")
|
|
140
159
|
: undefined,
|
|
@@ -142,6 +161,9 @@ export function activityGroupSummary(
|
|
|
142
161
|
? countLabel(attachmentsCount, "1 file delivery", "file deliveries")
|
|
143
162
|
: undefined,
|
|
144
163
|
eventCount > 0 ? countLabel(eventCount, "1 event", "events") : undefined,
|
|
164
|
+
quietReplyCount > 0
|
|
165
|
+
? countLabel(quietReplyCount, "1 quiet reply", "quiet replies")
|
|
166
|
+
: undefined,
|
|
145
167
|
].filter((value): value is string => value !== undefined);
|
|
146
168
|
|
|
147
169
|
return parts.length > 0 ? parts.join(" · ") : activityGroupLabel(entries);
|