@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,4 +1,4 @@
|
|
|
1
|
-
import { Fragment,
|
|
1
|
+
import { Fragment, type ReactNode } from "react";
|
|
2
2
|
import { ChevronRight } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
import { ShimmerText } from "../components/ShimmerText";
|
|
@@ -15,6 +15,7 @@ import type { RenderedTranscriptEntry } from "./transcriptRenderModel";
|
|
|
15
15
|
import { cn } from "../styles";
|
|
16
16
|
import { useTranscriptSearch } from "./transcriptSearch";
|
|
17
17
|
import { TranscriptActivityNesting } from "./TranscriptRailEvent";
|
|
18
|
+
import { useTranscriptRowOpen } from "./TranscriptRows";
|
|
18
19
|
import { TranscriptSummaryTrigger } from "./TranscriptSummary";
|
|
19
20
|
|
|
20
21
|
function countLabel(count: number, singular: string, plural: string): string {
|
|
@@ -175,18 +176,16 @@ function hasLiveActivity(entries: RenderedTranscriptEntry[]): boolean {
|
|
|
175
176
|
);
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
/** Keep activity collapsed until the reader explicitly opens it. */
|
|
179
|
-
export function activityGroupOpen(userOpen: boolean | null): boolean {
|
|
180
|
-
return userOpen ?? false;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
179
|
/** Collapse completed non-message activity so chat messages stay primary. */
|
|
184
180
|
export function TranscriptActivityGroup(props: {
|
|
185
181
|
entries: RenderedTranscriptEntry[];
|
|
182
|
+
/** Keeps the group open while it unmounts outside the viewport. */
|
|
183
|
+
openKey?: string;
|
|
186
184
|
renderEntry: (entry: RenderedTranscriptEntry) => ReactNode;
|
|
187
185
|
}) {
|
|
188
186
|
const { active: searchActive } = useTranscriptSearch();
|
|
189
|
-
|
|
187
|
+
// Activity stays collapsed until the reader opens it.
|
|
188
|
+
const [open, setOpen] = useTranscriptRowOpen(props.openKey);
|
|
190
189
|
const rows = props.entries.map((entry) => (
|
|
191
190
|
<Fragment key={entry.key}>{props.renderEntry(entry)}</Fragment>
|
|
192
191
|
));
|
|
@@ -202,14 +201,8 @@ export function TranscriptActivityGroup(props: {
|
|
|
202
201
|
);
|
|
203
202
|
}
|
|
204
203
|
|
|
205
|
-
const open = activityGroupOpen(userOpen);
|
|
206
|
-
|
|
207
204
|
return (
|
|
208
|
-
<Collapsible
|
|
209
|
-
className="min-w-0 py-0.5"
|
|
210
|
-
onOpenChange={setUserOpen}
|
|
211
|
-
open={open}
|
|
212
|
-
>
|
|
205
|
+
<Collapsible className="min-w-0 py-0.5" onOpenChange={setOpen} open={open}>
|
|
213
206
|
<TranscriptSummaryTrigger
|
|
214
207
|
flush
|
|
215
208
|
className={cn(
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ChevronRight } from "lucide-react";
|
|
2
|
-
import { useState } from "react";
|
|
3
2
|
|
|
4
3
|
import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
|
|
5
4
|
import { cn, dashboardDisclosureChevronClass } from "../styles";
|
|
@@ -9,6 +8,7 @@ import { messageRawText } from "./transcriptRenderModel";
|
|
|
9
8
|
import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
|
|
10
9
|
import { RedactedMarker } from "./TranscriptRedacted";
|
|
11
10
|
import type { TranscriptViewMessage } from "../types";
|
|
11
|
+
import { useTranscriptRowOpen } from "./TranscriptRows";
|
|
12
12
|
import { TranscriptSummaryTrigger } from "./TranscriptSummary";
|
|
13
13
|
|
|
14
14
|
function EventTypeChip(props: { eventType: string }) {
|
|
@@ -28,9 +28,13 @@ function batchSummary(events: readonly { label?: string }[]): string {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** Render an Event Message whose heading expands its full text. */
|
|
31
|
-
export function TranscriptEventView(props: {
|
|
31
|
+
export function TranscriptEventView(props: {
|
|
32
|
+
message: TranscriptViewMessage;
|
|
33
|
+
/** Keeps the text open while the row unmounts outside the viewport. */
|
|
34
|
+
openKey?: string;
|
|
35
|
+
}) {
|
|
32
36
|
const { active: searchActive } = useTranscriptSearch();
|
|
33
|
-
const [open, setOpen] =
|
|
37
|
+
const [open, setOpen] = useTranscriptRowOpen(props.openKey);
|
|
34
38
|
const text = messageRawText(props.message);
|
|
35
39
|
const redacted = props.message.parts.some(
|
|
36
40
|
(part) => part.type === "text" && part.redacted,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
1
|
import { Lightbulb } from "lucide-react";
|
|
3
2
|
|
|
4
3
|
import {
|
|
@@ -13,15 +12,18 @@ import {
|
|
|
13
12
|
TranscriptHeadingMeta,
|
|
14
13
|
TranscriptHeadingRow,
|
|
15
14
|
} from "./TranscriptHeadingRow";
|
|
15
|
+
import { useTranscriptRowOpen } from "./TranscriptRows";
|
|
16
16
|
import { useTranscriptSearch } from "./transcriptSearch";
|
|
17
17
|
|
|
18
18
|
/** Render reasoning collapsed with a short preview until expanded or searched. */
|
|
19
19
|
export function TranscriptReasoningView(props: {
|
|
20
|
+
/** Keeps the entry open while it unmounts outside the viewport. */
|
|
21
|
+
openKey?: string;
|
|
20
22
|
part: TranscriptViewReasoningPart;
|
|
21
23
|
timestamp?: number;
|
|
22
24
|
}) {
|
|
23
25
|
const { active: searchActive } = useTranscriptSearch();
|
|
24
|
-
const [open, setOpen] =
|
|
26
|
+
const [open, setOpen] = useTranscriptRowOpen(props.openKey);
|
|
25
27
|
const expanded = searchActive || open;
|
|
26
28
|
const rendered = props.part.redacted ? "Reasoning redacted" : props.part.text;
|
|
27
29
|
const timestamp = formatMessageTimestamp(props.timestamp);
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
type ComponentPropsWithoutRef,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
2
8
|
|
|
3
9
|
import { cn } from "../styles";
|
|
10
|
+
import {
|
|
11
|
+
ConversationCatchUpButton,
|
|
12
|
+
ConversationCatchUpCard,
|
|
13
|
+
} from "./ConversationCatchUp";
|
|
4
14
|
|
|
5
15
|
/** Keep row padding and the gap between transcript entries in one place. */
|
|
6
16
|
export function TranscriptRows({
|
|
@@ -20,28 +30,63 @@ export function TranscriptRows({
|
|
|
20
30
|
*/
|
|
21
31
|
export function TranscriptUnreadMarker() {
|
|
22
32
|
return (
|
|
23
|
-
<div
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
<div className="grid gap-2 py-1" data-transcript-unread-marker="">
|
|
34
|
+
{/* Equal side columns keep "New" centered when the catch-up offer shows. */}
|
|
35
|
+
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-3">
|
|
36
|
+
<span
|
|
37
|
+
aria-label="New messages"
|
|
38
|
+
className="h-px bg-dashboard-accent/50"
|
|
39
|
+
role="separator"
|
|
40
|
+
/>
|
|
41
|
+
<span
|
|
42
|
+
aria-hidden="true"
|
|
43
|
+
className="font-sans text-xs font-semibold text-dashboard-accent-text"
|
|
44
|
+
>
|
|
45
|
+
New
|
|
46
|
+
</span>
|
|
47
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
48
|
+
<span className="h-px flex-1 bg-dashboard-accent/50" />
|
|
49
|
+
<ConversationCatchUpButton />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<ConversationCatchUpCard />
|
|
34
53
|
</div>
|
|
35
54
|
);
|
|
36
55
|
}
|
|
37
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Open rows by key, for the rows of one transcript list. Rows far from the
|
|
59
|
+
* viewport unmount, so a row that opens again reads its state here.
|
|
60
|
+
*/
|
|
61
|
+
export const TranscriptOpenRows = createContext<Map<string, boolean> | null>(
|
|
62
|
+
null,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Hold whether a transcript row part is open. With a key, the state stays
|
|
67
|
+
* while the row unmounts outside the viewport.
|
|
68
|
+
*/
|
|
69
|
+
export function useTranscriptRowOpen(
|
|
70
|
+
key: string | undefined,
|
|
71
|
+
): [boolean, (open: boolean) => void] {
|
|
72
|
+
const openRows = useContext(TranscriptOpenRows);
|
|
73
|
+
const [open, setOpenState] = useState(
|
|
74
|
+
() => (key !== undefined && openRows?.get(key)) || false,
|
|
75
|
+
);
|
|
76
|
+
const setOpen = (value: boolean) => {
|
|
77
|
+
setOpenState(value);
|
|
78
|
+
if (key !== undefined) openRows?.set(key, value);
|
|
79
|
+
};
|
|
80
|
+
return [open, setOpen];
|
|
81
|
+
}
|
|
82
|
+
|
|
38
83
|
/**
|
|
39
84
|
* Lay out one transcript row at its real size.
|
|
40
85
|
*
|
|
41
|
-
* Rows do not use `content-visibility: auto`.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
86
|
+
* Rows do not use `content-visibility: auto`. The list mounts only the rows
|
|
87
|
+
* near the viewport, and a row that mounts above the reader can measure a
|
|
88
|
+
* different size than its estimate. The scroll owner corrects the scroll
|
|
89
|
+
* position for that, so keep estimates close and overscan large.
|
|
45
90
|
*/
|
|
46
91
|
export function TranscriptRow(props: {
|
|
47
92
|
/** Stable row id that holds the reader's place while layout changes. */
|
|
@@ -12,6 +12,7 @@ import { cn } from "../styles";
|
|
|
12
12
|
import { ConversationTranscriptView } from "./ConversationTranscript";
|
|
13
13
|
import { ConversationEventLog } from "./ConversationEventLog";
|
|
14
14
|
import {
|
|
15
|
+
TranscriptJumpToLatestContext,
|
|
15
16
|
transcriptBottomVersion,
|
|
16
17
|
transcriptJuniorMessageVersion,
|
|
17
18
|
useAutoLoadHistoryPage,
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
type TranscriptUnreadRange,
|
|
24
25
|
} from "./transcriptReadPosition";
|
|
25
26
|
import { TranscriptSearchProvider } from "./transcriptSearch";
|
|
26
|
-
import type { TranscriptHistory } from "./
|
|
27
|
+
import type { TranscriptHistory } from "./transcriptAroundHistory";
|
|
27
28
|
import {
|
|
28
29
|
TurnContextPanelProvider,
|
|
29
30
|
type TurnContextPanel,
|
|
@@ -51,6 +52,8 @@ export function Transcript(props: {
|
|
|
51
52
|
/** The reader's own sends to show at the end until history has them. */
|
|
52
53
|
pendingMessages?: readonly ConversationMailboxMessage[];
|
|
53
54
|
responding?: boolean;
|
|
55
|
+
/** Keep the rows around this event seq mounted, so the page can show it. */
|
|
56
|
+
revealSeq?: number;
|
|
54
57
|
search?: string;
|
|
55
58
|
onSearchChange?(value: string): void;
|
|
56
59
|
selectedEventSeq?: number;
|
|
@@ -61,6 +64,8 @@ export function Transcript(props: {
|
|
|
61
64
|
transcript?: ConversationTranscript;
|
|
62
65
|
/** Show turn context in the owner's panel. Without it, a drawer opens. */
|
|
63
66
|
turnContextPanel?: TurnContextPanel;
|
|
67
|
+
/** Open where a link shows its row instead of the latest message. */
|
|
68
|
+
startAtLink?: boolean;
|
|
64
69
|
/** Open at the "New" marker instead of the latest message. */
|
|
65
70
|
startAtUnread?: boolean;
|
|
66
71
|
/** Rows that were unread when the Conversation opened get a marker. */
|
|
@@ -79,6 +84,7 @@ export function Transcript(props: {
|
|
|
79
84
|
historyVersion: props.historyVersion ?? "empty",
|
|
80
85
|
juniorMessageVersion: transcriptJuniorMessageVersion(props.transcript),
|
|
81
86
|
pinRequestVersion: props.pinRequestVersion,
|
|
87
|
+
startAtLink: props.startAtLink,
|
|
82
88
|
startAtUnread: view === "rich" && props.startAtUnread,
|
|
83
89
|
view,
|
|
84
90
|
versions: {
|
|
@@ -114,6 +120,11 @@ export function Transcript(props: {
|
|
|
114
120
|
historyVersion: props.historyVersion ?? "empty",
|
|
115
121
|
onLoad: () => newer?.load(),
|
|
116
122
|
});
|
|
123
|
+
const jumpToLatest = () => {
|
|
124
|
+
// The latest page is in the detail, so the jump needs no request.
|
|
125
|
+
newer?.showLatest();
|
|
126
|
+
bottomPinning.jumpToBottom();
|
|
127
|
+
};
|
|
117
128
|
|
|
118
129
|
if (!props.transcript) {
|
|
119
130
|
return (
|
|
@@ -162,17 +173,22 @@ export function Transcript(props: {
|
|
|
162
173
|
conversation={props.transcript}
|
|
163
174
|
onSearchChange={props.onSearchChange ?? setLocalSearch}
|
|
164
175
|
onSelectedSeqChange={props.onSelectedEventSeqChange}
|
|
176
|
+
scrollAnchorKey={bottomPinning.scrollAnchorKey}
|
|
165
177
|
selectedSeq={props.selectedEventSeq}
|
|
166
178
|
/>
|
|
167
179
|
) : (
|
|
168
180
|
<TurnContextPanelProvider panel={props.turnContextPanel}>
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
<TranscriptJumpToLatestContext value={jumpToLatest}>
|
|
182
|
+
<ConversationTranscriptView
|
|
183
|
+
onOpenSubagentTranscript={props.onOpenSubagentTranscript}
|
|
184
|
+
conversation={props.transcript}
|
|
185
|
+
pendingMessages={props.pendingMessages}
|
|
186
|
+
responding={props.responding ?? props.live ?? false}
|
|
187
|
+
revealSeq={props.revealSeq}
|
|
188
|
+
scrollAnchorKey={bottomPinning.scrollAnchorKey}
|
|
189
|
+
unread={props.unread}
|
|
190
|
+
/>
|
|
191
|
+
</TranscriptJumpToLatestContext>
|
|
176
192
|
</TurnContextPanelProvider>
|
|
177
193
|
)}
|
|
178
194
|
{newer ? (
|
|
@@ -206,11 +222,7 @@ export function Transcript(props: {
|
|
|
206
222
|
/>
|
|
207
223
|
<JumpToLatestButton
|
|
208
224
|
hasPendingUpdate={bottomPinning.hasPendingUpdate}
|
|
209
|
-
onClick={
|
|
210
|
-
// The latest page is in the detail, so the jump needs no request.
|
|
211
|
-
newer?.showLatest();
|
|
212
|
-
bottomPinning.jumpToBottom();
|
|
213
|
-
}}
|
|
225
|
+
onClick={jumpToLatest}
|
|
214
226
|
visible={bottomPinning.showJumpToLatest}
|
|
215
227
|
/>
|
|
216
228
|
</div>
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
import { archiveConversationResponseSchema } from "@sentry/junior/api/schema";
|
|
12
12
|
|
|
13
13
|
import { patch } from "../http";
|
|
14
|
+
import type { Conversation } from "../types";
|
|
14
15
|
import {
|
|
15
16
|
conversationDetailQueryKey,
|
|
16
17
|
type ConversationSnapshot,
|
|
@@ -82,6 +83,27 @@ export function usePendingArchiveConversationUpdates() {
|
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Router state for home after a conversation archives itself. Home reports
|
|
88
|
+
* the background archive result, because the conversation page is gone.
|
|
89
|
+
*/
|
|
90
|
+
export type ArchivedFromConversationState = {
|
|
91
|
+
archivedConversation: Conversation;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Read the newest archive or restore status for one conversation. */
|
|
95
|
+
export function useLatestArchiveStatus(conversationId: string | undefined) {
|
|
96
|
+
const statuses = useMutationState({
|
|
97
|
+
filters: {
|
|
98
|
+
exact: true,
|
|
99
|
+
mutationKey: [...archiveConversationMutationKey, conversationId],
|
|
100
|
+
},
|
|
101
|
+
select: (mutation) => mutation.state.status,
|
|
102
|
+
});
|
|
103
|
+
// The mutation cache keeps insertion order, so the last entry is newest.
|
|
104
|
+
return conversationId ? statuses.at(-1) : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
/** Archive or restore one conversation with an immediate reversible cache update. */
|
|
86
108
|
export function useArchiveConversation(
|
|
87
109
|
conversationId: string,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offer a catch-up when a reader comes back to a lot of unread activity. The
|
|
3
|
+
* reader asks for it, because each summary costs a model call and a short
|
|
4
|
+
* wait.
|
|
5
|
+
*/
|
|
6
|
+
import { useMutation } from "@tanstack/react-query";
|
|
7
|
+
import { createContext, useContext } from "react";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
conversationCatchUpSchema,
|
|
11
|
+
type ConversationCatchUp,
|
|
12
|
+
type ConversationDetailReport,
|
|
13
|
+
} from "@sentry/junior/api/schema";
|
|
14
|
+
import { post } from "../http";
|
|
15
|
+
import type { TranscriptUnreadRange } from "./transcriptReadPosition";
|
|
16
|
+
|
|
17
|
+
/** Fewer unread Turns than this are quicker to read than a summary. */
|
|
18
|
+
const CATCH_UP_MIN_TURNS = 2;
|
|
19
|
+
|
|
20
|
+
/** The catch-up offer for the unread range of one Conversation visit. */
|
|
21
|
+
export type ConversationCatchUpState = {
|
|
22
|
+
data?: ConversationCatchUp;
|
|
23
|
+
error: boolean;
|
|
24
|
+
pending: boolean;
|
|
25
|
+
request(): void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Count the Turns that finished in the unread range. When the range goes past
|
|
30
|
+
* the loaded rows, the reader has at least a page of unread rows.
|
|
31
|
+
*/
|
|
32
|
+
function hasEnoughUnread(
|
|
33
|
+
events: ConversationDetailReport["events"],
|
|
34
|
+
unread: TranscriptUnreadRange,
|
|
35
|
+
pages: { hasNewerPage: boolean; hasPreviousPage: boolean },
|
|
36
|
+
): boolean {
|
|
37
|
+
if (pages.hasNewerPage) return true;
|
|
38
|
+
const first = events[0];
|
|
39
|
+
if (pages.hasPreviousPage && first && first.seq > unread.afterSeq) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
let turns = 0;
|
|
43
|
+
for (const event of events) {
|
|
44
|
+
if (event.seq <= unread.afterSeq || event.seq > unread.throughSeq) continue;
|
|
45
|
+
if (
|
|
46
|
+
event.data.type === "turn_lifecycle" &&
|
|
47
|
+
event.data.state !== "started"
|
|
48
|
+
) {
|
|
49
|
+
turns += 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return turns >= CATCH_UP_MIN_TURNS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Offer a catch-up only when enough is unread. */
|
|
56
|
+
export function useConversationCatchUp(input: {
|
|
57
|
+
conversationId: string;
|
|
58
|
+
events?: ConversationDetailReport["events"];
|
|
59
|
+
hasNewerPage: boolean;
|
|
60
|
+
hasPreviousPage: boolean;
|
|
61
|
+
unread?: TranscriptUnreadRange;
|
|
62
|
+
}): ConversationCatchUpState | undefined {
|
|
63
|
+
const { unread } = input;
|
|
64
|
+
const mutation = useMutation({
|
|
65
|
+
mutationFn: (range: TranscriptUnreadRange) =>
|
|
66
|
+
post(
|
|
67
|
+
conversationCatchUpSchema,
|
|
68
|
+
`/api/conversations/${encodeURIComponent(input.conversationId)}/catch-up`,
|
|
69
|
+
range,
|
|
70
|
+
),
|
|
71
|
+
});
|
|
72
|
+
// Once asked, keep the answer even if the loaded rows change.
|
|
73
|
+
const asked = !mutation.isIdle;
|
|
74
|
+
if (
|
|
75
|
+
!unread ||
|
|
76
|
+
(!asked && (!input.events || !hasEnoughUnread(input.events, unread, input)))
|
|
77
|
+
) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
data: mutation.data,
|
|
82
|
+
error: mutation.isError,
|
|
83
|
+
pending: mutation.isPending,
|
|
84
|
+
request: () => mutation.mutate(unread),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Only the Conversation page sets it, so nested transcripts offer nothing. */
|
|
89
|
+
export const ConversationCatchUpContext = createContext<
|
|
90
|
+
ConversationCatchUpState | undefined
|
|
91
|
+
>(undefined);
|
|
92
|
+
|
|
93
|
+
/** Read the catch-up offer of the current Conversation page. */
|
|
94
|
+
export function useConversationCatchUpState() {
|
|
95
|
+
return useContext(ConversationCatchUpContext);
|
|
96
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
2
|
import { createContext, useContext } from "react";
|
|
3
|
-
import
|
|
3
|
+
import * as z from "zod";
|
|
4
4
|
|
|
5
5
|
import { fetchDashboardJson } from "../http";
|
|
6
6
|
import { conversationSideReadQueryKey } from "./queries";
|
|
@@ -11,6 +11,7 @@ const conversationMemoryListSchema = z
|
|
|
11
11
|
z
|
|
12
12
|
.object({
|
|
13
13
|
memoryIds: z.array(z.string().min(1)).min(1),
|
|
14
|
+
seq: z.number().int().min(0),
|
|
14
15
|
source: z.enum(["captured", "recalled"]),
|
|
15
16
|
turnId: z.string().min(1).optional(),
|
|
16
17
|
})
|
|
@@ -76,6 +77,8 @@ export function anchorMemories(
|
|
|
76
77
|
/** One row of the Conversation memory list. */
|
|
77
78
|
export type ConversationMemoryEntry = {
|
|
78
79
|
memory: ConversationMemory;
|
|
80
|
+
/** The recall or capture event, where the transcript opens. */
|
|
81
|
+
seq: number;
|
|
79
82
|
source: MemorySource;
|
|
80
83
|
turnId?: string;
|
|
81
84
|
};
|
|
@@ -99,6 +102,7 @@ export function conversationMemoryEntries(
|
|
|
99
102
|
seen.add(key);
|
|
100
103
|
entries.push({
|
|
101
104
|
memory,
|
|
105
|
+
seq: event.seq,
|
|
102
106
|
source: event.source,
|
|
103
107
|
...(event.turnId ? { turnId: event.turnId } : undefined),
|
|
104
108
|
});
|
|
@@ -110,6 +114,9 @@ export function conversationMemoryEntries(
|
|
|
110
114
|
/** The open memory popover, and the memory it highlights. */
|
|
111
115
|
export type OpenMemoryPopover = MemoryAnchor & { memoryId?: string };
|
|
112
116
|
|
|
117
|
+
/** A memory chosen in details, and the event where the transcript opens. */
|
|
118
|
+
export type MemoryJump = OpenMemoryPopover & { seq: number };
|
|
119
|
+
|
|
113
120
|
/** Shared memory popover state for one Conversation page. */
|
|
114
121
|
export type ConversationMemoryState = {
|
|
115
122
|
list?: ConversationMemoryList;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import * as z from "zod";
|
|
2
3
|
import {
|
|
3
4
|
type QueryClient,
|
|
4
5
|
queryOptions,
|
|
@@ -22,13 +23,14 @@ import {
|
|
|
22
23
|
conversationDetailReportSchema,
|
|
23
24
|
conversationDetailWithMailboxReportSchema,
|
|
24
25
|
conversationEventPageSchema,
|
|
25
|
-
|
|
26
|
+
conversationUnchangedDetailWithMailboxSchema,
|
|
26
27
|
forkConversationResponseSchema,
|
|
27
28
|
steerConversationPendingMessagesResponseSchema,
|
|
28
29
|
stopConversationTurnResponseSchema,
|
|
29
30
|
} from "@sentry/junior/api/schema";
|
|
30
31
|
|
|
31
32
|
import { REVIEW_ITEMS_QUERY_KEY } from "../review-desk/queries";
|
|
33
|
+
import { readResponseJson } from "../sentry";
|
|
32
34
|
import {
|
|
33
35
|
DashboardApiError,
|
|
34
36
|
del,
|
|
@@ -102,29 +104,6 @@ function isOpenPrefetch(snapshot: ConversationSnapshot | undefined) {
|
|
|
102
104
|
);
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
/** Read the mailbox before history so acknowledged input is already committed. */
|
|
106
|
-
async function readConversationSnapshot(
|
|
107
|
-
conversationId: string,
|
|
108
|
-
signal?: AbortSignal,
|
|
109
|
-
previous?: ConversationSnapshot,
|
|
110
|
-
unreadPage?: boolean,
|
|
111
|
-
): Promise<ConversationSnapshot> {
|
|
112
|
-
// With no cached copy, the transcript waits for this read. The server reads
|
|
113
|
-
// both in the same order in one request, which saves a round trip. Polls
|
|
114
|
-
// read the mailbox apart, so the detail can still answer 304.
|
|
115
|
-
if (!previous) {
|
|
116
|
-
return readConversationData(conversationId, signal, undefined, {
|
|
117
|
-
mailbox: true,
|
|
118
|
-
unreadPage,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
const mailbox = await readConversationPendingMessages(conversationId, signal);
|
|
122
|
-
const detail = await readConversationData(conversationId, signal, previous, {
|
|
123
|
-
unreadPage,
|
|
124
|
-
});
|
|
125
|
-
return { ...detail, mailbox };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
107
|
const CONVERSATION_SIDE_READS = ["agents", "memories", "watches"] as const;
|
|
129
108
|
|
|
130
109
|
/**
|
|
@@ -185,11 +164,13 @@ export function conversationDetailQueryOptions(
|
|
|
185
164
|
const previous = client.getQueryData<ConversationSnapshot>(
|
|
186
165
|
conversationDetailQueryKey(conversationId),
|
|
187
166
|
);
|
|
188
|
-
|
|
167
|
+
// The server reads the mailbox before history, so acknowledged input
|
|
168
|
+
// is in at least one of them.
|
|
169
|
+
const next = await readConversationData(
|
|
189
170
|
conversationId!,
|
|
190
171
|
signal,
|
|
191
172
|
previous,
|
|
192
|
-
options?.unreadPage,
|
|
173
|
+
{ mailbox: true, unreadPage: options?.unreadPage },
|
|
193
174
|
);
|
|
194
175
|
refreshSideReadsOnChange(client, conversationId!, previous, next);
|
|
195
176
|
return next;
|
|
@@ -221,7 +202,9 @@ export function conversationDetailQueryOptions(
|
|
|
221
202
|
/**
|
|
222
203
|
* Read positions only move forward. A poll that started before a read report
|
|
223
204
|
* finished must not put an older position back in the cache, nor the unread
|
|
224
|
-
* cursor of that older position.
|
|
205
|
+
* cursor of that older position. The cache already holds the cursor for the
|
|
206
|
+
* newer position: a read does not change the detail version, so no later poll
|
|
207
|
+
* would bring it back.
|
|
225
208
|
*/
|
|
226
209
|
function keepLatestReadPosition(
|
|
227
210
|
previous: ConversationSnapshot,
|
|
@@ -230,8 +213,14 @@ function keepLatestReadPosition(
|
|
|
230
213
|
const kept = previous.readPosition;
|
|
231
214
|
if (!kept) return next;
|
|
232
215
|
if ((next.readPosition?.lastReadSeq ?? -1) >= kept.lastReadSeq) return next;
|
|
233
|
-
const { unreadCursor: _olderCursor, ...rest } = next;
|
|
234
|
-
return {
|
|
216
|
+
const { unreadCursor: _olderCursor, unreadPage: _olderPage, ...rest } = next;
|
|
217
|
+
return {
|
|
218
|
+
...rest,
|
|
219
|
+
readPosition: kept,
|
|
220
|
+
...(previous.unreadCursor
|
|
221
|
+
? { unreadCursor: previous.unreadCursor, unreadPage: previous.unreadPage }
|
|
222
|
+
: undefined),
|
|
223
|
+
};
|
|
235
224
|
}
|
|
236
225
|
|
|
237
226
|
/** Create one dashboard conversation and refresh the personal feed. */
|
|
@@ -858,6 +847,10 @@ export async function readConversationData(
|
|
|
858
847
|
const query = new URLSearchParams();
|
|
859
848
|
if (options?.unreadPage) query.set("unreadPage", "true");
|
|
860
849
|
if (options?.mailbox) query.set("mailbox", "true");
|
|
850
|
+
// A poll asks only for the events after the cached ones. The unread page
|
|
851
|
+
// needs the latest page, so a read that asks for it reads that page.
|
|
852
|
+
const after = options?.unreadPage ? undefined : previous?.appendCursor;
|
|
853
|
+
if (after) query.set("after", after);
|
|
861
854
|
const search = query.size ? `?${query}` : "";
|
|
862
855
|
const response = await readDashboardResponse(
|
|
863
856
|
`/api/conversations/${encodeURIComponent(conversationId)}${search}`,
|
|
@@ -867,11 +860,69 @@ export async function readConversationData(
|
|
|
867
860
|
: undefined,
|
|
868
861
|
);
|
|
869
862
|
if (response.status === 304 && previous) return previous;
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
863
|
+
if (!options?.mailbox) {
|
|
864
|
+
return withDetailEtag(
|
|
865
|
+
withCachedEvents(
|
|
866
|
+
await readResponseJson(response, conversationDetailReportSchema),
|
|
867
|
+
previous,
|
|
868
|
+
),
|
|
869
|
+
response,
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
const report = await readResponseJson(
|
|
873
|
+
response,
|
|
874
|
+
z.union([
|
|
875
|
+
conversationUnchangedDetailWithMailboxSchema,
|
|
876
|
+
conversationDetailWithMailboxReportSchema,
|
|
877
|
+
]),
|
|
878
|
+
);
|
|
879
|
+
if (!("unchanged" in report)) {
|
|
880
|
+
return withDetailEtag(withCachedEvents(report, previous), response);
|
|
881
|
+
}
|
|
882
|
+
// Only a request that sent a version can get this answer.
|
|
883
|
+
if (!previous) throw new Error("Unchanged detail without a cached copy");
|
|
884
|
+
return { ...previous, mailbox: report.mailbox };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Put the cached events before the newer events of an `appended` report.
|
|
889
|
+
* The cached detail keeps its cursors: they still point before its events.
|
|
890
|
+
*/
|
|
891
|
+
function withCachedEvents(
|
|
892
|
+
report: ConversationSnapshot,
|
|
893
|
+
previous: ConversationSnapshot | undefined,
|
|
894
|
+
): ConversationSnapshot {
|
|
895
|
+
if (!report.appended) return report;
|
|
896
|
+
// Only a request that sent an `appendCursor` can get this answer.
|
|
897
|
+
if (!previous) throw new Error("Appended detail without a cached copy");
|
|
898
|
+
const { appended: _appended, ...rest } = report;
|
|
899
|
+
const lastSeq = previous.events.at(-1)?.seq ?? -1;
|
|
900
|
+
const firstSeq = previous.events[0]?.seq;
|
|
901
|
+
// Like the server, keep the unread cursor while the reader is behind the
|
|
902
|
+
// first event.
|
|
903
|
+
const behind =
|
|
904
|
+
rest.readPosition !== undefined &&
|
|
905
|
+
firstSeq !== undefined &&
|
|
906
|
+
rest.readPosition.lastReadSeq + 1 < firstSeq;
|
|
907
|
+
return {
|
|
908
|
+
...rest,
|
|
909
|
+
events: [
|
|
910
|
+
...previous.events,
|
|
911
|
+
...report.events.filter((event) => event.seq > lastSeq),
|
|
912
|
+
],
|
|
913
|
+
...(previous.previousCursor
|
|
914
|
+
? { previousCursor: previous.previousCursor }
|
|
915
|
+
: undefined),
|
|
916
|
+
...(behind && previous.unreadCursor
|
|
917
|
+
? { unreadCursor: previous.unreadCursor, unreadPage: previous.unreadPage }
|
|
918
|
+
: undefined),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function withDetailEtag<T extends ConversationDetailReport>(
|
|
923
|
+
report: T,
|
|
924
|
+
response: Response,
|
|
925
|
+
): T & { detailEtag?: string } {
|
|
875
926
|
return {
|
|
876
927
|
...report,
|
|
877
928
|
detailEtag:
|
|
@@ -879,18 +930,6 @@ export async function readConversationData(
|
|
|
879
930
|
};
|
|
880
931
|
}
|
|
881
932
|
|
|
882
|
-
/** Read accepted mailbox messages that have not reached durable history yet. */
|
|
883
|
-
export function readConversationPendingMessages(
|
|
884
|
-
conversationId: string,
|
|
885
|
-
signal?: AbortSignal,
|
|
886
|
-
): Promise<ConversationPendingMessagesReport> {
|
|
887
|
-
return fetchDashboardJson(
|
|
888
|
-
conversationPendingMessagesReportSchema,
|
|
889
|
-
`/api/conversations/${encodeURIComponent(conversationId)}/pending-messages`,
|
|
890
|
-
signal,
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
933
|
/** Read one bounded page of events before the supplied history cursor. */
|
|
895
934
|
export function readConversationEvents(
|
|
896
935
|
conversationId: string,
|