@sentry/junior-dashboard 0.279.0 → 0.281.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 +105 -43
- package/dist/client-manifest.json +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +105 -43
- package/dist/public/chunks/AdminPage-M5RBVZFN.js +1 -0
- package/dist/public/chunks/AutomationPage-WALCREDT.js +3 -0
- package/dist/public/chunks/AutomationRunsPage-CV47E4UX.js +1 -0
- package/dist/public/chunks/AutomationsPage-57L3AQVJ.js +1 -0
- package/dist/public/chunks/CodePage-XJUYMFJH.js +1 -0
- package/dist/public/chunks/ConnectionsPage-3QQAGYWF.js +1 -0
- package/dist/public/chunks/DrawerContent-RYYS57ZH.js +1 -0
- package/dist/public/chunks/LocationDetailPage-4IL5HQML.js +1 -0
- package/dist/public/chunks/LocationsPage-D2DUQAHJ.js +1 -0
- package/dist/public/chunks/McpSettingsPage-3RT7JPEK.js +1 -0
- package/dist/public/chunks/PeoplePage-6F5GPQ7C.js +1 -0
- package/dist/public/chunks/PersonProfilePage-SPAYQPTD.js +1 -0
- package/dist/public/chunks/PersonalTokensPage-SHAUJ74M.js +1 -0
- package/dist/public/chunks/PluginUserPage-64FIWD7M.js +1 -0
- package/dist/public/chunks/RepositoryPage-L7IHOXSN.js +1 -0
- package/dist/public/chunks/SettingsPage-U7WG4FHE.js +1 -0
- package/dist/public/chunks/SystemPage-ZBC43OJH.js +1 -0
- package/dist/public/chunks/WorkspaceFormPage-3NVIIZ4P.js +2 -0
- package/dist/public/chunks/WorkspacesPage-G73HPBDD.js +1 -0
- package/dist/public/chunks/chunk-2BCS5AQL.js +1 -0
- package/dist/public/chunks/chunk-2J3LGF5Q.js +1 -0
- package/dist/public/chunks/chunk-2NNKHSZE.js +1 -0
- package/dist/public/chunks/chunk-3Y3ICT6K.js +1 -0
- package/dist/public/chunks/chunk-4AKUZW6R.js +1 -0
- package/dist/public/chunks/chunk-4TQSTCIN.js +13 -0
- package/dist/public/chunks/chunk-4UGZNVJM.js +1 -0
- package/dist/public/chunks/chunk-564V7YHG.js +1 -0
- package/dist/public/chunks/chunk-5ISD3QSC.js +1 -0
- package/dist/public/chunks/chunk-5LNGNJ3P.js +1 -0
- package/dist/public/chunks/chunk-5ZA674OU.js +1 -0
- package/dist/public/chunks/chunk-64GPEGGE.js +1 -0
- package/dist/public/chunks/chunk-6FHY65EQ.js +1 -0
- package/dist/public/chunks/chunk-6FSJ3W2W.js +1 -0
- package/dist/public/chunks/chunk-6PHJZPWD.js +19 -0
- package/dist/public/chunks/chunk-B5ZN3SWQ.js +14 -0
- package/dist/public/chunks/chunk-BKOACADP.js +1 -0
- package/dist/public/chunks/chunk-CQY6BYUB.js +1 -0
- package/dist/public/chunks/chunk-CR2ICLSH.js +1 -0
- package/dist/public/chunks/chunk-DEEO4D4N.js +1 -0
- package/dist/public/chunks/chunk-DZB4LPI6.js +1 -0
- package/dist/public/chunks/chunk-FE522UEA.js +1 -0
- package/dist/public/chunks/chunk-FSGVY7HB.js +1 -0
- package/dist/public/chunks/chunk-FVX42XAD.js +1 -0
- package/dist/public/chunks/chunk-G5NWYJTV.js +1 -0
- package/dist/public/chunks/chunk-GMZIELFU.js +1 -0
- package/dist/public/chunks/chunk-GOTQ4D6W.js +1 -0
- package/dist/public/chunks/chunk-GYRM5ZIV.js +1 -0
- package/dist/public/chunks/chunk-GZBMHYYU.js +1 -0
- package/dist/public/chunks/chunk-H2ZU34CP.js +1 -0
- package/dist/public/chunks/chunk-H76B43OQ.js +1 -0
- package/dist/public/chunks/chunk-HCLRG2EC.js +1 -0
- package/dist/public/chunks/chunk-II4NXLSQ.js +2 -0
- package/dist/public/chunks/chunk-ILH47V7O.js +1 -0
- package/dist/public/chunks/chunk-IZWTFLCC.js +1 -0
- package/dist/public/chunks/chunk-J37MXUED.js +1 -0
- package/dist/public/chunks/chunk-JAZABOBA.js +0 -0
- package/dist/public/chunks/chunk-JPL2NRWK.js +1 -0
- package/dist/public/chunks/chunk-JVJ5UWIK.js +1 -0
- package/dist/public/chunks/chunk-K4TYMAA3.js +63 -0
- package/dist/public/chunks/chunk-KDUTK357.js +1 -0
- package/dist/public/chunks/chunk-L2H3ZWZN.js +1 -0
- package/dist/public/chunks/chunk-LBELRO5P.js +1 -0
- package/dist/public/chunks/chunk-LKW7MULF.js +1 -0
- package/dist/public/chunks/chunk-LZYAHJVQ.js +1 -0
- package/dist/public/chunks/chunk-MEHLQZDG.js +3 -0
- package/dist/public/chunks/chunk-MKKFXRYO.js +1 -0
- package/dist/public/chunks/chunk-MTBUV2GJ.js +1 -0
- package/dist/public/chunks/chunk-NGQRNS3C.js +1 -0
- package/dist/public/chunks/chunk-ONDKE72P.js +1 -0
- package/dist/public/chunks/chunk-PG7M7L24.js +1 -0
- package/dist/public/chunks/chunk-PQPAPPEI.js +1 -0
- package/dist/public/chunks/chunk-PXHLJIOT.js +1 -0
- package/dist/public/chunks/chunk-QOX5RMLM.js +1 -0
- package/dist/public/chunks/chunk-QPBLZSNX.js +1 -0
- package/dist/public/chunks/chunk-QQUFROWT.js +1 -0
- package/dist/public/chunks/chunk-RB5FBDEY.js +1 -0
- package/dist/public/chunks/chunk-RGXXD4DM.js +41 -0
- package/dist/public/chunks/chunk-RHMZNAJX.js +1 -0
- package/dist/public/chunks/chunk-SIOW7OOW.js +1 -0
- package/dist/public/chunks/chunk-SUX4QBVF.js +1 -0
- package/dist/public/chunks/chunk-T4M53QQT.js +1 -0
- package/dist/public/chunks/chunk-TC7TU4GK.js +1 -0
- package/dist/public/chunks/chunk-TFJOEF3Q.js +1 -0
- package/dist/public/chunks/chunk-UJD3MBMB.js +1 -0
- package/dist/public/chunks/chunk-UK2V4UBT.js +1 -0
- package/dist/public/chunks/chunk-UT6HZPGL.js +1 -0
- package/dist/public/chunks/chunk-UZ44J2LH.js +1 -0
- package/dist/public/chunks/chunk-VC6IS564.js +1 -0
- package/dist/public/chunks/chunk-WLIOZO3W.js +1 -0
- package/dist/public/chunks/chunk-WRXMQB7X.js +25 -0
- package/dist/public/chunks/chunk-XDUSY26N.js +55 -0
- package/dist/public/chunks/chunk-XNR4RZEC.js +1 -0
- package/dist/public/chunks/chunk-YPMUHXYT.js +147 -0
- package/dist/public/chunks/chunk-ZIJ6SJDN.js +1 -0
- package/dist/public/chunks/code-highlighter-Q7WC7AEF.js +12 -0
- package/dist/public/client-4OAAOHF4.js +68 -0
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +1 -1
- package/src/assets.ts +4 -1
- package/src/client/App.tsx +100 -23
- package/src/client/TracedRoutes.tsx +8 -4
- package/src/client/code-highlighter.ts +26 -0
- package/src/client/code.tsx +2 -22
- package/src/client/components/BottomSheet.tsx +1 -1
- package/src/client/components/DetailsPanel.tsx +2 -1
- package/src/client/components/Drawer.tsx +12 -71
- package/src/client/components/DrawerContent.tsx +76 -0
- package/src/client/components/ProfileMenu.tsx +12 -3
- package/src/client/components/UserInputForm.tsx +28 -5
- package/src/client/conversations/ConversationBrief.tsx +129 -65
- package/src/client/conversations/ConversationCatchUp.tsx +107 -0
- package/src/client/conversations/ConversationContextTags.tsx +110 -0
- package/src/client/conversations/ConversationDetails.tsx +13 -14
- package/src/client/conversations/ConversationEventLog.tsx +135 -70
- package/src/client/conversations/ConversationHeader.tsx +74 -65
- package/src/client/conversations/ConversationHeaderActions.tsx +36 -38
- package/src/client/conversations/ConversationHomeList.tsx +41 -3
- package/src/client/conversations/ConversationLinkedWork.tsx +30 -10
- package/src/client/conversations/ConversationMeta.tsx +21 -27
- package/src/client/conversations/ConversationOverflowMenu.tsx +15 -4
- package/src/client/conversations/ConversationPage.tsx +219 -157
- package/src/client/conversations/ConversationTranscript.tsx +234 -186
- package/src/client/conversations/ConversationUsageBreakdown.tsx +142 -64
- package/src/client/conversations/DetailsList.tsx +7 -1
- package/src/client/conversations/PendingMailboxStack.tsx +88 -44
- package/src/client/conversations/ShareConversationButton.tsx +5 -50
- package/src/client/conversations/SlackMark.tsx +16 -4
- package/src/client/conversations/TranscriptActivityGroup.tsx +7 -14
- package/src/client/conversations/TranscriptEventView.tsx +7 -3
- package/src/client/conversations/TranscriptReasoningView.tsx +4 -2
- package/src/client/conversations/TranscriptRows.tsx +61 -16
- package/src/client/conversations/TranscriptView.tsx +25 -13
- package/src/client/conversations/conversationArchive.ts +22 -0
- package/src/client/conversations/conversationCatchUpState.ts +96 -0
- package/src/client/conversations/conversationMemory.ts +8 -1
- package/src/client/conversations/queries.ts +85 -46
- package/src/client/conversations/{transcriptUnreadHistory.ts → transcriptAroundHistory.ts} +48 -42
- package/src/client/conversations/transcriptBottomPinning.ts +40 -8
- package/src/client/conversations/transcriptListRows.ts +215 -0
- package/src/client/conversations/transcriptReadPosition.ts +24 -2
- package/src/client/conversations/turnContext.ts +1 -1
- package/src/client/conversations/virtualRows.ts +168 -0
- package/src/client/http.ts +6 -5
- package/src/client/pages/admin/AdminPage.tsx +1 -1
- package/src/client/pages/admin/ServiceAccountsSection.tsx +1 -1
- package/src/client/pages/memory/memoryDashboard.ts +1 -1
- package/src/client/pages/memory/memoryRecord.ts +1 -1
- package/src/client/pages/settings/ConnectionsPage.tsx +1 -1
- package/src/client/pages/user/PluginUserPage.tsx +0 -6
- package/src/client/pages/user/pluginUserPageRoutes.ts +5 -0
- package/src/client/primaryNav.ts +1 -1
- package/src/client/review-desk/reviewItemFormat.ts +1 -1
- package/src/client/sentry.ts +53 -3
- package/src/conversation-cache.ts +8 -10
- package/src/mock-reporting/fixtures.ts +43 -8
- package/src/mock-reporting/routes.ts +40 -3
- package/src/public-assets.ts +28 -20
- package/src/shell.ts +12 -2
- package/dist/public/client-ZCZXZBTL.js +0 -460
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useCallback,
|
|
3
3
|
useEffect,
|
|
4
|
+
useEffectEvent,
|
|
5
|
+
useLayoutEffect,
|
|
4
6
|
useMemo,
|
|
5
7
|
useRef,
|
|
6
8
|
useState,
|
|
7
9
|
useSyncExternalStore,
|
|
8
10
|
type ReactNode,
|
|
9
11
|
} from "react";
|
|
10
|
-
import { useSearchParams } from "react-router";
|
|
12
|
+
import { useNavigate, useSearchParams } from "react-router";
|
|
11
13
|
import type {
|
|
12
14
|
ConversationDetailReport,
|
|
13
15
|
ConversationFeed,
|
|
16
|
+
ConversationReportEvent,
|
|
14
17
|
} from "@sentry/junior/api/schema";
|
|
15
18
|
|
|
16
19
|
import type { ConversationData } from "./queries";
|
|
17
20
|
import {
|
|
18
21
|
useArchiveConversation,
|
|
22
|
+
type ArchivedFromConversationState,
|
|
19
23
|
type PendingArchiveConversationUpdate,
|
|
20
24
|
} from "./conversationArchive";
|
|
21
25
|
import { usePinConversation } from "./usePinConversation";
|
|
22
26
|
import { conversationIsResponding } from "./transcript";
|
|
27
|
+
import {
|
|
28
|
+
ConversationCatchUpContext,
|
|
29
|
+
useConversationCatchUp,
|
|
30
|
+
} from "./conversationCatchUpState";
|
|
23
31
|
import { buildConversationMarkdown } from "../markdownExport";
|
|
24
32
|
import { CopyButton } from "../components/CopyButton";
|
|
25
33
|
import {
|
|
@@ -39,6 +47,7 @@ import {
|
|
|
39
47
|
memoryAnchorKey,
|
|
40
48
|
useConversationMemories,
|
|
41
49
|
type ConversationMemoryState,
|
|
50
|
+
type MemoryJump,
|
|
42
51
|
type OpenMemoryPopover,
|
|
43
52
|
} from "./conversationMemory";
|
|
44
53
|
import { ConversationEventDetails } from "./ConversationEventDetails";
|
|
@@ -57,14 +66,13 @@ import {
|
|
|
57
66
|
import { cn } from "../styles";
|
|
58
67
|
import { ConversationForkLinks } from "./ConversationForkLinks";
|
|
59
68
|
import { ConversationAgentTree } from "./ConversationAgentTree";
|
|
69
|
+
import { conversationContextTags } from "./ConversationContextTags";
|
|
60
70
|
import { conversationLinks } from "./ConversationLinkedWork";
|
|
61
71
|
import {
|
|
62
72
|
ConversationAnnotations,
|
|
63
73
|
conversationCost,
|
|
64
|
-
conversationLocation,
|
|
65
74
|
ConversationPrivacyChip,
|
|
66
75
|
ConversationStats,
|
|
67
|
-
hasConversationAnnotations,
|
|
68
76
|
hasConversationStats,
|
|
69
77
|
} from "./ConversationMeta";
|
|
70
78
|
import {
|
|
@@ -100,9 +108,10 @@ import {
|
|
|
100
108
|
import { TranscriptLoading } from "./TranscriptLoading";
|
|
101
109
|
import { recordTranscriptShown } from "../sentry";
|
|
102
110
|
import {
|
|
103
|
-
|
|
111
|
+
useHistoryAround,
|
|
112
|
+
type HistoryStart,
|
|
104
113
|
type TranscriptHistory,
|
|
105
|
-
} from "./
|
|
114
|
+
} from "./transcriptAroundHistory";
|
|
106
115
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
107
116
|
import {
|
|
108
117
|
SubagentTranscriptDrawer,
|
|
@@ -143,6 +152,7 @@ export function ConversationPage(props: {
|
|
|
143
152
|
() => false,
|
|
144
153
|
);
|
|
145
154
|
const pageRef = useRef<HTMLDivElement>(null);
|
|
155
|
+
const focusDetailsOnOpen = useRef(false);
|
|
146
156
|
const [subagentTarget, setSubagentTarget] =
|
|
147
157
|
useState<SubagentTranscriptTarget>();
|
|
148
158
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
@@ -184,7 +194,11 @@ export function ConversationPage(props: {
|
|
|
184
194
|
const [turnContext, setTurnContext] = useState<TurnContextSelection>();
|
|
185
195
|
const [memoryPopover, setMemoryPopover] = useState<OpenMemoryPopover>();
|
|
186
196
|
// A memory chosen in details waits here until its transcript row renders.
|
|
187
|
-
const [memoryJump, setMemoryJump] = useState<
|
|
197
|
+
const [memoryJump, setMemoryJump] = useState<MemoryJump>();
|
|
198
|
+
// The transcript mounts only rows near the viewport. The rows of the last
|
|
199
|
+
// memory jump stay mounted, so the jump finds its row and the popover
|
|
200
|
+
// keeps it.
|
|
201
|
+
const [revealSeq, setRevealSeq] = useState<number>();
|
|
188
202
|
// The page stays mounted across Conversations. Memory state belongs to one.
|
|
189
203
|
const [memoryConversationId, setMemoryConversationId] = useState(
|
|
190
204
|
props.conversationId,
|
|
@@ -193,6 +207,7 @@ export function ConversationPage(props: {
|
|
|
193
207
|
setMemoryConversationId(props.conversationId);
|
|
194
208
|
setMemoryPopover(undefined);
|
|
195
209
|
setMemoryJump(undefined);
|
|
210
|
+
setRevealSeq(undefined);
|
|
196
211
|
}
|
|
197
212
|
const quickQuestion = useQuickQuestionThread(props.conversationId);
|
|
198
213
|
// Event details and turn context also show over a Quick question.
|
|
@@ -200,6 +215,14 @@ export function ConversationPage(props: {
|
|
|
200
215
|
Boolean(quickQuestion.thread) && !eventOpen && !turnContext;
|
|
201
216
|
const showDetails =
|
|
202
217
|
detailsOpen && !eventOpen && !turnContext && !quickQuestion.thread;
|
|
218
|
+
useLayoutEffect(() => {
|
|
219
|
+
if (!showDetails || !focusDetailsOnOpen.current) return;
|
|
220
|
+
focusDetailsOnOpen.current = false;
|
|
221
|
+
// The inline opener leaves the tab order while details are visible.
|
|
222
|
+
pageRef.current
|
|
223
|
+
?.querySelector<HTMLButtonElement>('[data-slot="details-panel-close"]')
|
|
224
|
+
?.focus({ preventScroll: true });
|
|
225
|
+
}, [showDetails]);
|
|
203
226
|
const [viewState, setView] = useState<TranscriptViewMode>(() =>
|
|
204
227
|
eventOpen ? "raw" : "rich",
|
|
205
228
|
);
|
|
@@ -214,11 +237,33 @@ export function ConversationPage(props: {
|
|
|
214
237
|
const [startAtUnread] = useState(
|
|
215
238
|
() => readConversationStartPosition() === "unread",
|
|
216
239
|
);
|
|
240
|
+
// A link at open shows its row, so the transcript does not open at the
|
|
241
|
+
// latest message or the read position, unless the link fails first.
|
|
242
|
+
const [startAtLink, setStartAtLink] = useState(
|
|
243
|
+
() => selectedEventSeq !== undefined || linkedTurnId !== undefined,
|
|
244
|
+
);
|
|
217
245
|
const detail = props.detail;
|
|
218
246
|
// Jump to latest, a send, or the mobile keyboard skips the newer pages for
|
|
219
247
|
// the rest of the visit. It can come before the unread page loads.
|
|
220
248
|
const [showLatest, setShowLatest] = useState(false);
|
|
221
|
-
|
|
249
|
+
// A link at open wins over the read position. When the loaded rows hold
|
|
250
|
+
// the link, the transcript keeps them and does not page to the read
|
|
251
|
+
// position.
|
|
252
|
+
const opensAtUnread = startAtUnread && !showLatest && !startAtLink;
|
|
253
|
+
// A linked row that the transcript does not hold. The transcript opens at
|
|
254
|
+
// the page around it until the reader jumps to the latest. A link at open
|
|
255
|
+
// starts there, so the first render reads no other page.
|
|
256
|
+
const [linkStart, setLinkStart] = useState(() =>
|
|
257
|
+
linkedTurnId !== undefined
|
|
258
|
+
? unheldLinkStart(detail.data?.events, { turn: linkedTurnId })
|
|
259
|
+
: selectedEventSeq === undefined
|
|
260
|
+
? undefined
|
|
261
|
+
: unheldLinkStart(detail.data?.events, { event: selectedEventSeq }),
|
|
262
|
+
);
|
|
263
|
+
const showLatestPage = useCallback(() => {
|
|
264
|
+
setShowLatest(true);
|
|
265
|
+
setLinkStart(undefined);
|
|
266
|
+
}, []);
|
|
222
267
|
const memoryList = useConversationMemories(conversationId).data;
|
|
223
268
|
const memoryState = useMemo<ConversationMemoryState>(
|
|
224
269
|
() => ({
|
|
@@ -229,41 +274,78 @@ export function ConversationPage(props: {
|
|
|
229
274
|
[memoryList, memoryPopover],
|
|
230
275
|
);
|
|
231
276
|
const archive = useArchiveConversation(conversationId);
|
|
277
|
+
const navigate = useNavigate();
|
|
232
278
|
const pin = usePinConversation(conversationId);
|
|
233
279
|
const markRead = useMarkConversationRead(conversationId);
|
|
234
280
|
const readAtOpen = useReadPositionAtOpen(detail.data, detail.fetchedDetail);
|
|
235
|
-
const
|
|
281
|
+
const dropLink = useCallback(() => {
|
|
282
|
+
setLinkStart(undefined);
|
|
283
|
+
setStartAtLink(false);
|
|
284
|
+
setMemoryJump(undefined);
|
|
285
|
+
setSearchParams(
|
|
286
|
+
(current) => {
|
|
287
|
+
const next = new URLSearchParams(current);
|
|
288
|
+
next.delete(CONVERSATION_EVENT_PARAM);
|
|
289
|
+
next.delete(CONVERSATION_TURN_PARAM);
|
|
290
|
+
return next;
|
|
291
|
+
},
|
|
292
|
+
{ replace: true },
|
|
293
|
+
);
|
|
294
|
+
}, [setSearchParams]);
|
|
295
|
+
const unreadCursor = opensAtUnread ? readAtOpen.unreadCursor : undefined;
|
|
296
|
+
const aroundHistory = useHistoryAround({
|
|
236
297
|
conversationId,
|
|
237
298
|
// Cached earlier pages from a past visit would look like newer events.
|
|
238
299
|
detail: detail.latest,
|
|
239
|
-
onShowLatest:
|
|
240
|
-
|
|
241
|
-
|
|
300
|
+
onShowLatest: showLatestPage,
|
|
301
|
+
// A link that fails to load is dropped. The transcript goes back to
|
|
302
|
+
// where it was.
|
|
303
|
+
...(linkStart
|
|
304
|
+
? { onStartFailed: dropLink, start: linkStart }
|
|
305
|
+
: unreadCursor
|
|
306
|
+
? {
|
|
307
|
+
start: { around: unreadCursor },
|
|
308
|
+
...(readAtOpen.unreadPage
|
|
309
|
+
? { startPage: readAtOpen.unreadPage }
|
|
310
|
+
: undefined),
|
|
311
|
+
}
|
|
312
|
+
: undefined),
|
|
242
313
|
});
|
|
243
|
-
// The
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
314
|
+
// The transcript on screen, so it can stay while the next one loads. The
|
|
315
|
+
// page is keyed by Conversation, so at open this is the cached copy.
|
|
316
|
+
const shownRef = useRef(detail.data);
|
|
317
|
+
// A reader far behind sees the events from their read position, and a
|
|
318
|
+
// link shows the events around its row. Others see the latest page and
|
|
319
|
+
// the pages before it. The transcript on screen stays until this visit's
|
|
320
|
+
// fetch tells where to open and the page around that row loads. It loads
|
|
321
|
+
// no earlier pages and shows no marker: at open, both would come from the
|
|
322
|
+
// cached read position. It reports no reads, because it can sit at rows
|
|
323
|
+
// the reader has not read yet. Then the transcript opens at the row, if
|
|
324
|
+
// the reader has not moved.
|
|
325
|
+
const holdShown = Boolean(
|
|
326
|
+
shownRef.current &&
|
|
327
|
+
(aroundHistory?.isPending ||
|
|
328
|
+
(opensAtUnread && !linkStart && !detail.fetchedAtOpen)),
|
|
255
329
|
);
|
|
256
|
-
const
|
|
330
|
+
const transcriptPending =
|
|
331
|
+
detail.isPending || Boolean(aroundHistory?.isPending && !shownRef.current);
|
|
332
|
+
const history: TranscriptHistory = holdShown
|
|
257
333
|
? {
|
|
258
334
|
...detail,
|
|
259
|
-
data:
|
|
335
|
+
data: shownRef.current,
|
|
260
336
|
hasPreviousPage: false,
|
|
261
337
|
isLoadingPreviousPage: false,
|
|
262
338
|
loadPreviousPage: () => {},
|
|
263
339
|
}
|
|
264
|
-
: (
|
|
265
|
-
|
|
266
|
-
|
|
340
|
+
: (aroundHistory ?? detail);
|
|
341
|
+
if (history.data) shownRef.current = history.data;
|
|
342
|
+
const catchUp = useConversationCatchUp({
|
|
343
|
+
conversationId,
|
|
344
|
+
events: history.data?.events,
|
|
345
|
+
hasNewerPage: Boolean(history.newer),
|
|
346
|
+
hasPreviousPage: history.hasPreviousPage,
|
|
347
|
+
unread: holdShown ? undefined : readAtOpen.unread,
|
|
348
|
+
});
|
|
267
349
|
const feedConversation = conversations.find(
|
|
268
350
|
(item) => item.id === conversationId,
|
|
269
351
|
);
|
|
@@ -277,6 +359,11 @@ export function ConversationPage(props: {
|
|
|
277
359
|
<ParticipantAvatarStack participants={participants} size="detail" />
|
|
278
360
|
) : null;
|
|
279
361
|
const conversationDetail = detail.data;
|
|
362
|
+
const links = conversationLinks(conversationDetail);
|
|
363
|
+
const showUsageBreakdown = hasConversationUsageBreakdown({
|
|
364
|
+
conversation,
|
|
365
|
+
detail: conversationDetail,
|
|
366
|
+
});
|
|
280
367
|
const title =
|
|
281
368
|
conversationDetailTitle(detail.data) ??
|
|
282
369
|
conversationDisplayTitle(conversation);
|
|
@@ -342,53 +429,29 @@ export function ConversationPage(props: {
|
|
|
342
429
|
},
|
|
343
430
|
[eventOpen, setSelectedEventSeq],
|
|
344
431
|
);
|
|
345
|
-
// A linked event, Turn, or memory can be outside the loaded
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
? undefined
|
|
362
|
-
: selectedEventSeq < oldestLoadedSeq
|
|
363
|
-
? "earlier"
|
|
364
|
-
: selectedEventSeq > newestLoadedSeq &&
|
|
365
|
-
(history.newer || holdCachedTranscript)
|
|
366
|
-
? "newer"
|
|
367
|
-
: undefined;
|
|
368
|
-
// Replace a found Turn link with a link to its first event, or drop it
|
|
369
|
-
// when history has no such event.
|
|
432
|
+
// A linked event, Turn, or memory can be outside the loaded rows. Then the
|
|
433
|
+
// transcript opens at the page around it, which the server finds by the
|
|
434
|
+
// event seq or Turn id. Only a new link does this, so a reader who jumps to
|
|
435
|
+
// the latest stays there. Returns false when the loaded rows hold the link.
|
|
436
|
+
const openLink = useEffectEvent((link: ConversationLink) => {
|
|
437
|
+
const start = unheldLinkStart(history.data?.events, link);
|
|
438
|
+
if (start) setLinkStart(start);
|
|
439
|
+
return Boolean(start);
|
|
440
|
+
});
|
|
441
|
+
useEffect(() => {
|
|
442
|
+
if (linkedTurnId !== undefined) openLink({ turn: linkedTurnId });
|
|
443
|
+
else if (selectedEventSeq !== undefined) {
|
|
444
|
+
openLink({ event: selectedEventSeq });
|
|
445
|
+
}
|
|
446
|
+
}, [linkedTurnId, selectedEventSeq]);
|
|
447
|
+
// Replace a found Turn link with a link to its first event.
|
|
370
448
|
const linkedTurnSeq = linkedTurnId
|
|
371
449
|
? turnStartedEventSeq(transcript?.events, linkedTurnId)
|
|
372
450
|
: undefined;
|
|
373
|
-
const linkedTurnMissing =
|
|
374
|
-
linkedTurnId !== undefined &&
|
|
375
|
-
linkedTurnSeq === undefined &&
|
|
376
|
-
transcript !== undefined;
|
|
377
|
-
const linkedSearch = linkedEventDirection
|
|
378
|
-
? searchHistory(linkedEventDirection)
|
|
379
|
-
: linkedTurnMissing
|
|
380
|
-
? searchHistory("any")
|
|
381
|
-
: undefined;
|
|
382
|
-
useEffect(() => loadToward(linkedSearch), [linkedSearch, loadToward]);
|
|
383
|
-
const linkedTurnNotFound = linkedTurnMissing && linkedSearch === "end";
|
|
384
451
|
useEffect(() => {
|
|
385
452
|
if (linkedTurnSeq !== undefined) setSelectedEventSeq(linkedTurnSeq);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
// Show a chosen memory at its transcript row. Wait for the first page, then
|
|
389
|
-
// load pages toward the row until it renders, or stop when history ends.
|
|
390
|
-
const memorySearch =
|
|
391
|
-
memoryJump && transcript ? searchHistory("any") : undefined;
|
|
453
|
+
}, [linkedTurnSeq, setSelectedEventSeq]);
|
|
454
|
+
// Show a chosen memory at its transcript row, once the row renders.
|
|
392
455
|
useEffect(() => {
|
|
393
456
|
if (!memoryJump || !transcript) return;
|
|
394
457
|
const anchor = pageRef.current?.querySelector(
|
|
@@ -398,22 +461,17 @@ export function ConversationPage(props: {
|
|
|
398
461
|
anchor.scrollIntoView({ block: "center" });
|
|
399
462
|
setMemoryPopover(memoryJump);
|
|
400
463
|
setMemoryJump(undefined);
|
|
401
|
-
} else if (
|
|
464
|
+
} else if (!holdShown && !openLink({ event: memoryJump.seq })) {
|
|
465
|
+
// The loaded rows hold the event, but no row shows the memory.
|
|
402
466
|
setMemoryJump(undefined);
|
|
403
|
-
} else {
|
|
404
|
-
loadToward(memorySearch);
|
|
405
467
|
}
|
|
406
|
-
}, [
|
|
468
|
+
}, [holdShown, memoryJump, transcript, view]);
|
|
407
469
|
// Keep the panel while the transcript or a linked event loads, and for the
|
|
408
470
|
// render before a found Turn link becomes an event link.
|
|
409
471
|
const eventLoading =
|
|
410
472
|
eventOpen &&
|
|
411
473
|
!selectedEvent &&
|
|
412
|
-
(transcriptPending ||
|
|
413
|
-
linkedTurnSeq !== undefined ||
|
|
414
|
-
linkedSearch === "load-earlier" ||
|
|
415
|
-
linkedSearch === "load-newer" ||
|
|
416
|
-
linkedSearch === "wait");
|
|
474
|
+
(transcriptPending || holdShown || linkedTurnId !== undefined);
|
|
417
475
|
const showEvent = Boolean(selectedEvent) || eventLoading;
|
|
418
476
|
// Step through the same filtered rows that the event log shows.
|
|
419
477
|
const events = transcript?.events;
|
|
@@ -436,9 +494,9 @@ export function ConversationPage(props: {
|
|
|
436
494
|
[turnContext?.id],
|
|
437
495
|
);
|
|
438
496
|
const requestPin = useCallback(() => {
|
|
439
|
-
|
|
497
|
+
showLatestPage();
|
|
440
498
|
setPinRequestVersion((version) => version + 1);
|
|
441
|
-
}, []);
|
|
499
|
+
}, [showLatestPage]);
|
|
442
500
|
const onOpenSubagentTranscript = useCallback(
|
|
443
501
|
({
|
|
444
502
|
part,
|
|
@@ -480,7 +538,7 @@ export function ConversationPage(props: {
|
|
|
480
538
|
className={cn(
|
|
481
539
|
"grid h-full min-h-0 min-w-0",
|
|
482
540
|
(showDetails || showEvent || turnContext || showQuickQuestion) &&
|
|
483
|
-
"xl:grid-cols-[minmax(0,1fr)_25rem]",
|
|
541
|
+
"xl:grid-cols-[minmax(0,1fr)_22rem] 2xl:grid-cols-[minmax(0,1fr)_25rem]",
|
|
484
542
|
)}
|
|
485
543
|
>
|
|
486
544
|
<ChatLayout
|
|
@@ -489,15 +547,20 @@ export function ConversationPage(props: {
|
|
|
489
547
|
agentTree={
|
|
490
548
|
agentTree ? <ConversationAgentTree nodes={agentTree} /> : null
|
|
491
549
|
}
|
|
550
|
+
contextTags={conversationContextTags(conversation)}
|
|
492
551
|
conversationId={conversationId}
|
|
493
|
-
cost={conversationCost(conversation,
|
|
552
|
+
cost={conversationCost(conversation, conversationDetail, "below")}
|
|
494
553
|
detailsOpen={showDetails}
|
|
495
554
|
onDetailsClick={() => {
|
|
496
555
|
if (eventOpen) setSelectedEventSeq(undefined);
|
|
497
556
|
setTurnContext(undefined);
|
|
498
557
|
quickQuestion.close();
|
|
499
|
-
if (showDetails)
|
|
500
|
-
|
|
558
|
+
if (showDetails) {
|
|
559
|
+
closeDetails();
|
|
560
|
+
} else {
|
|
561
|
+
focusDetailsOnOpen.current = true;
|
|
562
|
+
setDetailsChoice(true);
|
|
563
|
+
}
|
|
501
564
|
}}
|
|
502
565
|
forks={
|
|
503
566
|
detail.data?.forkedFromConversationId ||
|
|
@@ -529,9 +592,7 @@ export function ConversationPage(props: {
|
|
|
529
592
|
/>
|
|
530
593
|
}
|
|
531
594
|
linkedWork={
|
|
532
|
-
|
|
533
|
-
<ConversationAnnotations detail={detail.data} />
|
|
534
|
-
) : null
|
|
595
|
+
links.length ? <ConversationAnnotations links={links} /> : null
|
|
535
596
|
}
|
|
536
597
|
pin={{
|
|
537
598
|
// The API lets only participants pin, the same as archive.
|
|
@@ -545,15 +606,25 @@ export function ConversationPage(props: {
|
|
|
545
606
|
archived: Boolean(conversation?.archivedAt),
|
|
546
607
|
disabled: !conversation || archive.isPending,
|
|
547
608
|
error: Boolean(archive.error),
|
|
548
|
-
onClick: () =>
|
|
609
|
+
onClick: () => {
|
|
610
|
+
const archived = !conversation!.archivedAt;
|
|
549
611
|
archive.mutate({
|
|
550
|
-
archived
|
|
612
|
+
archived,
|
|
551
613
|
lastSeenAt: conversation!.lastSeenAt,
|
|
552
|
-
})
|
|
614
|
+
});
|
|
615
|
+
// Archiving hides this conversation, so go home at once.
|
|
616
|
+
// The mutation keeps running after this page unmounts.
|
|
617
|
+
if (archived) {
|
|
618
|
+
navigate("/", {
|
|
619
|
+
state: {
|
|
620
|
+
archivedConversation: conversation!,
|
|
621
|
+
} satisfies ArchivedFromConversationState,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
},
|
|
553
625
|
pending: archive.isPending,
|
|
554
626
|
}}
|
|
555
627
|
live={live}
|
|
556
|
-
location={conversationLocation(conversation, detail.data)}
|
|
557
628
|
onSearchChange={setSearch}
|
|
558
629
|
onViewChange={onViewChange}
|
|
559
630
|
privacy={
|
|
@@ -600,39 +671,41 @@ export function ConversationPage(props: {
|
|
|
600
671
|
</div>
|
|
601
672
|
) : null}
|
|
602
673
|
<ConversationMemoryContext value={memoryState}>
|
|
603
|
-
<
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
674
|
+
<ConversationCatchUpContext value={catchUp}>
|
|
675
|
+
<Transcript
|
|
676
|
+
hasPreviousPage={history.hasPreviousPage}
|
|
677
|
+
historyError={history.historyError}
|
|
678
|
+
historyVersion={history.historyVersion}
|
|
679
|
+
lastReadSeq={readAtOpen.lastReadSeq}
|
|
680
|
+
live={live}
|
|
681
|
+
loadingPreviousPage={history.isLoadingPreviousPage}
|
|
682
|
+
newerHistory={history.newer}
|
|
683
|
+
onLoadPreviousPage={history.loadPreviousPage}
|
|
684
|
+
onRead={holdShown ? undefined : markRead}
|
|
685
|
+
onSelectedEventSeqChange={setSelectedEventSeq}
|
|
686
|
+
// Sends and the thinking row belong after the latest event.
|
|
687
|
+
pendingMessages={
|
|
688
|
+
history.newer ? undefined : detail.inlineMessages
|
|
689
|
+
}
|
|
690
|
+
pinRequestVersion={pinRequestVersion}
|
|
691
|
+
revealSeq={revealSeq}
|
|
692
|
+
responding={
|
|
693
|
+
!detail.error &&
|
|
694
|
+
!history.newer &&
|
|
695
|
+
conversationIsResponding(detail.data)
|
|
696
|
+
}
|
|
697
|
+
onOpenSubagentTranscript={onOpenSubagentTranscript}
|
|
698
|
+
search={search}
|
|
699
|
+
onSearchChange={setSearch}
|
|
700
|
+
selectedEventSeq={selectedEventSeq}
|
|
701
|
+
startAtLink={startAtLink}
|
|
702
|
+
startAtUnread={startAtUnread}
|
|
703
|
+
transcript={transcript}
|
|
704
|
+
turnContextPanel={turnContextPanel}
|
|
705
|
+
unread={holdShown ? undefined : readAtOpen.unread}
|
|
706
|
+
view={view}
|
|
707
|
+
/>
|
|
708
|
+
</ConversationCatchUpContext>
|
|
636
709
|
</ConversationMemoryContext>
|
|
637
710
|
</>
|
|
638
711
|
)}
|
|
@@ -695,29 +768,30 @@ export function ConversationPage(props: {
|
|
|
695
768
|
// can hide them.
|
|
696
769
|
if (view !== "rich") onViewChange("rich");
|
|
697
770
|
setSearch("");
|
|
771
|
+
setRevealSeq(entry.seq);
|
|
698
772
|
setMemoryJump({
|
|
699
773
|
memoryId: entry.memory.id,
|
|
774
|
+
seq: entry.seq,
|
|
700
775
|
source: entry.source,
|
|
701
776
|
turnId: entry.turnId,
|
|
702
777
|
});
|
|
703
778
|
}}
|
|
704
|
-
links={
|
|
779
|
+
links={links}
|
|
705
780
|
overview={
|
|
706
781
|
hasConversationStats({
|
|
707
782
|
conversation,
|
|
708
783
|
detail: detail.data,
|
|
784
|
+
showUsageBreakdown,
|
|
709
785
|
}) ? (
|
|
710
786
|
<ConversationStats
|
|
711
787
|
conversation={conversation}
|
|
712
788
|
detail={detail.data}
|
|
789
|
+
showUsageBreakdown={showUsageBreakdown}
|
|
713
790
|
/>
|
|
714
791
|
) : null
|
|
715
792
|
}
|
|
716
793
|
usage={
|
|
717
|
-
|
|
718
|
-
conversation,
|
|
719
|
-
detail: detail.data,
|
|
720
|
-
}) ? (
|
|
794
|
+
showUsageBreakdown ? (
|
|
721
795
|
<ConversationUsageBreakdown
|
|
722
796
|
conversation={conversation}
|
|
723
797
|
detail={detail.data}
|
|
@@ -812,32 +886,20 @@ function committedMessageIds(
|
|
|
812
886
|
|
|
813
887
|
const EMPTY_MESSAGE_IDS: readonly string[] = [];
|
|
814
888
|
|
|
815
|
-
type
|
|
816
|
-
type HistorySearch = "end" | "failed" | "load-earlier" | "load-newer" | "wait";
|
|
889
|
+
type ConversationLink = { event: number } | { turn: string };
|
|
817
890
|
|
|
818
|
-
/**
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
)
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
if (direction !== "earlier" && history.newer && !history.newer.error) {
|
|
833
|
-
return "load-newer";
|
|
834
|
-
}
|
|
835
|
-
if (
|
|
836
|
-
direction !== "newer" &&
|
|
837
|
-
history.hasPreviousPage &&
|
|
838
|
-
!history.historyError
|
|
839
|
-
) {
|
|
840
|
-
return "load-earlier";
|
|
841
|
-
}
|
|
842
|
-
return history.historyError || history.newer?.error ? "failed" : "end";
|
|
891
|
+
/** The page to open for a link, or nothing when `events` hold its row. */
|
|
892
|
+
function unheldLinkStart(
|
|
893
|
+
events: readonly ConversationReportEvent[] | undefined,
|
|
894
|
+
link: ConversationLink,
|
|
895
|
+
): HistoryStart | undefined {
|
|
896
|
+
const seq =
|
|
897
|
+
"event" in link ? link.event : turnStartedEventSeq(events, link.turn);
|
|
898
|
+
const held =
|
|
899
|
+
seq !== undefined &&
|
|
900
|
+
events !== undefined &&
|
|
901
|
+
seq >= (events[0]?.seq ?? Infinity) &&
|
|
902
|
+
seq <= (events.at(-1)?.seq ?? -Infinity);
|
|
903
|
+
if (held) return undefined;
|
|
904
|
+
return "event" in link ? { event: String(link.event) } : { turn: link.turn };
|
|
843
905
|
}
|