@sentry/junior-dashboard 0.264.0 → 0.266.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/LICENSE +201 -0
- package/README.md +2 -5
- package/dist/app.js +119 -35
- package/dist/client.js +145 -136
- package/dist/index.d.ts +0 -1
- package/dist/index.js +119 -35
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +0 -1
- package/src/app.ts +5 -20
- package/src/client/App.tsx +29 -31
- package/src/client/api.ts +7 -0
- package/src/client/components/ActorAvatar.tsx +10 -22
- package/src/client/components/BottomSheet.tsx +12 -2
- package/src/client/components/ConnectionBanner.tsx +4 -3
- package/src/client/components/CopyButton.tsx +82 -0
- package/src/client/components/DetailsPanel.tsx +133 -0
- package/src/client/components/Drawer.tsx +15 -24
- package/src/client/components/EmptyTelemetry.tsx +5 -3
- package/src/client/components/FilterBar.tsx +16 -31
- package/src/client/components/ImageAttachment.tsx +49 -63
- package/src/client/components/InlineError.tsx +8 -4
- package/src/client/components/Metric.tsx +33 -23
- package/src/client/components/Notice.tsx +19 -26
- package/src/client/components/PageContentSkeleton.tsx +1 -1
- package/src/client/components/Pagination.tsx +27 -27
- package/src/client/components/ParticipantAvatarStack.tsx +43 -39
- package/src/client/components/ProfileMenu.tsx +27 -93
- package/src/client/components/README.md +103 -78
- package/src/client/components/SearchInput.tsx +51 -41
- package/src/client/components/SegmentedNav.tsx +1 -1
- package/src/client/components/ShimmerText.tsx +2 -6
- package/src/client/components/ThemeSwitcher.tsx +19 -15
- package/src/client/components/VersionDriftBanner.tsx +9 -7
- package/src/client/components/charts/ActivityChart.tsx +173 -288
- package/src/client/components/charts/CacheWrites.tsx +59 -37
- package/src/client/components/charts/InputCacheChart.tsx +137 -131
- package/src/client/components/charts/README.md +4 -4
- package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
- package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
- package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
- package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
- package/src/client/components/layout/DashboardHeader.tsx +44 -90
- package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
- package/src/client/components/layout/VisualViewportShell.tsx +2 -2
- package/src/client/components/ui/accordion.tsx +77 -0
- package/src/client/components/ui/alert-dialog.tsx +89 -0
- package/src/client/components/ui/alert.tsx +64 -0
- package/src/client/components/ui/avatar.tsx +53 -0
- package/src/client/components/ui/button.tsx +86 -0
- package/src/client/components/ui/chart.tsx +74 -0
- package/src/client/components/ui/checkbox.tsx +26 -0
- package/src/client/components/ui/collapsible.tsx +22 -0
- package/src/client/components/ui/dialog.tsx +66 -0
- package/src/client/components/ui/empty.tsx +32 -0
- package/src/client/components/ui/field.tsx +49 -0
- package/src/client/components/ui/input-group.tsx +96 -0
- package/src/client/components/ui/input.tsx +35 -0
- package/src/client/components/ui/native-select.tsx +57 -0
- package/src/client/components/ui/pagination.tsx +60 -0
- package/src/client/components/ui/popover.tsx +67 -0
- package/src/client/components/ui/radio-group.tsx +32 -0
- package/src/client/components/ui/sheet.tsx +13 -3
- package/src/client/components/ui/skeleton.tsx +19 -0
- package/src/client/components/ui/spinner.tsx +21 -0
- package/src/client/components/ui/table.tsx +101 -0
- package/src/client/components/ui/tabs.tsx +75 -0
- package/src/client/components/ui/textarea.tsx +17 -0
- package/src/client/components/ui/toast.tsx +215 -0
- package/src/client/components/ui/toggle-group.tsx +36 -0
- package/src/client/components/ui/toggle.tsx +37 -0
- package/src/client/components/ui/tooltip.tsx +168 -0
- package/src/client/components/ui/trace.tsx +96 -0
- package/src/client/conversations/ChatLayout.tsx +7 -3
- package/src/client/conversations/ConversationBrief.tsx +17 -6
- package/src/client/conversations/ConversationChildAgents.tsx +5 -5
- package/src/client/conversations/ConversationComposer.tsx +26 -12
- package/src/client/conversations/ConversationDetails.tsx +147 -62
- package/src/client/conversations/ConversationEventDetails.tsx +141 -0
- package/src/client/conversations/ConversationEventLog.tsx +150 -129
- package/src/client/conversations/ConversationHeader.tsx +13 -7
- package/src/client/conversations/ConversationHeaderActions.tsx +97 -93
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
- package/src/client/conversations/ConversationMeta.tsx +55 -88
- package/src/client/conversations/ConversationPage.tsx +220 -101
- package/src/client/conversations/ConversationSidebar.tsx +49 -29
- package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
- package/src/client/conversations/ConversationWatches.tsx +173 -0
- package/src/client/conversations/ConversationWorkspace.tsx +12 -22
- package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
- package/src/client/conversations/DetailsList.tsx +139 -0
- package/src/client/conversations/EventDetails.tsx +42 -8
- package/src/client/conversations/EventUsage.tsx +99 -74
- package/src/client/conversations/ForkConversationButton.tsx +27 -16
- package/src/client/conversations/ObjectCard.tsx +20 -8
- package/src/client/conversations/PendingMailboxStack.tsx +52 -38
- package/src/client/conversations/README.md +89 -13
- package/src/client/conversations/ShareConversationButton.tsx +28 -17
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
- package/src/client/conversations/ToolFrame.tsx +36 -52
- package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
- package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
- package/src/client/conversations/TranscriptEventView.tsx +35 -16
- package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
- package/src/client/conversations/TranscriptLoading.tsx +1 -1
- package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
- package/src/client/conversations/TranscriptMessageContextView.tsx +35 -18
- package/src/client/conversations/TranscriptMessageView.tsx +6 -1
- package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
- package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
- package/src/client/conversations/TranscriptSummary.tsx +34 -2
- package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
- package/src/client/conversations/TranscriptToolView.tsx +1 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +144 -65
- package/src/client/conversations/TranscriptView.tsx +63 -27
- package/src/client/conversations/eventLog.ts +58 -0
- package/src/client/conversations/eventTime.ts +60 -0
- package/src/client/conversations/queries.ts +29 -0
- package/src/client/conversations/transcriptBottomPinning.ts +34 -0
- package/src/client/conversations/transcriptMentions.tsx +18 -5
- package/src/client/format.ts +5 -0
- package/src/client/pages/ConnectionsPage.tsx +2 -2
- package/src/client/pages/PersonalTokensPage.tsx +4 -4
- package/src/client/pages/SettingsPage.tsx +9 -10
- package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
- package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
- package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
- package/src/client/pages/automations/AutomationDetails.tsx +193 -0
- package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
- package/src/client/pages/automations/AutomationEditor.tsx +41 -29
- package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
- package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
- package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
- package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
- package/src/client/pages/automations/AutomationFilters.tsx +189 -74
- package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
- package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
- package/src/client/pages/automations/AutomationRow.tsx +12 -138
- package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
- package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
- package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
- package/src/client/pages/automations/AutomationStatus.tsx +201 -0
- package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
- package/src/client/pages/automations/AutomationsPage.tsx +80 -68
- package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
- package/src/client/pages/automations/README.md +57 -0
- package/src/client/pages/automations/TriggerFilters.tsx +22 -0
- package/src/client/pages/automations/automationFormat.ts +37 -23
- package/src/client/pages/code/CodeActivityChart.tsx +59 -101
- package/src/client/pages/code/CodePage.tsx +52 -55
- package/src/client/pages/code/RepositoryPage.tsx +6 -8
- package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
- package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
- package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
- package/src/client/pages/memory/MemoryDetails.tsx +218 -0
- package/src/client/pages/memory/MemoryPage.tsx +168 -112
- package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
- package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
- package/src/client/pages/memory/memoryRecord.ts +1 -1
- package/src/client/pages/people/ContributionGrid.tsx +23 -16
- package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
- package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
- package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
- package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
- package/src/client/pages/system/GuardianActivity.tsx +1 -2
- package/src/client/pages/system/PluginBarChart.tsx +106 -87
- package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
- package/src/client/pages/system/README.md +23 -0
- package/src/client/pages/system/SystemNavigation.tsx +5 -1
- package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
- package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
- package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
- package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
- package/src/client/pages/system/WorkspaceList.tsx +20 -30
- package/src/client/pages/system/WorkspacesPage.tsx +117 -119
- package/src/client/pages/system/sandboxSize.ts +8 -0
- package/src/client/pages/system/workspaceQueries.ts +53 -0
- package/src/client/pages/user/PluginUserPage.tsx +51 -2
- package/src/client/pages/user/pluginUserPageData.ts +1 -6
- package/src/client/styles.ts +4 -0
- package/src/client.tsx +7 -1
- package/src/mock-reporting/automation-collection.ts +4 -2
- package/src/mock-reporting/fixtures.ts +23 -7
- package/src/mock-reporting/routes.ts +12 -0
- package/src/mock-reporting/watches.ts +94 -0
- package/src/shell.ts +4 -9
- package/src/tailwind.css +0 -10
- package/src/client/components/Button.tsx +0 -118
- package/src/client/components/Field.tsx +0 -53
- package/src/client/components/SegmentedTabs.tsx +0 -119
- package/src/client/components/Select.tsx +0 -52
- package/src/client/components/Skeleton.tsx +0 -17
- package/src/client/components/TextInput.tsx +0 -58
- package/src/client/components/Tooltip.tsx +0 -216
- package/src/client/components/charts/useChartLayout.ts +0 -27
- package/src/client/components/ui/sheet.LICENSE +0 -21
- package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
- package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
- package/src/client/pages/dev/ChartFixtures.tsx +0 -325
- package/src/client/pages/dev/ComponentsPage.tsx +0 -899
- package/src/client/pages/dev/ConversationFixture.tsx +0 -231
- package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
- package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
- package/src/client/pages/dev/SelectFixtures.tsx +0 -62
- package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
- package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
- package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
- package/src/client/pages/system/SnapshotSummary.tsx +0 -78
|
@@ -1,66 +1,108 @@
|
|
|
1
1
|
import { Brain, Braces, ChevronRight, Layers } from "lucide-react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useId,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
3
9
|
import { createPortal } from "react-dom";
|
|
4
10
|
|
|
11
|
+
import { DetailsPanel } from "../components/DetailsPanel";
|
|
12
|
+
import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
|
|
5
13
|
import { Drawer } from "../components/Drawer";
|
|
6
14
|
import type { TranscriptViewTurnContext } from "../types";
|
|
7
15
|
import { formatMessageTimestamp, formatTime } from "../format";
|
|
8
16
|
import { memoryRecallContent, type MemoryRecallContent } from "./turnContext";
|
|
9
|
-
import { cn } from "../styles";
|
|
17
|
+
import { cn, dashboardDisclosureChevronClass } from "../styles";
|
|
10
18
|
import { HighlightText } from "./transcriptSearch";
|
|
11
|
-
import {
|
|
19
|
+
import { TranscriptSummaryTrigger } from "./TranscriptSummary";
|
|
12
20
|
|
|
13
|
-
/**
|
|
21
|
+
/** The turn context that a page panel shows, keyed by its message. */
|
|
22
|
+
export type TurnContextSelection = {
|
|
23
|
+
contexts: TranscriptViewTurnContext[];
|
|
24
|
+
id: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Let the page that owns a panel show turn context there. */
|
|
28
|
+
export type TurnContextPanel = {
|
|
29
|
+
openId?: string;
|
|
30
|
+
onToggle(selection: TurnContextSelection): void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const TurnContextPanelContext = createContext<TurnContextPanel | undefined>(
|
|
34
|
+
undefined,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Route turn context in one transcript to its owner's panel. Without a
|
|
39
|
+
* `panel`, turn context opens in a modal drawer. Each transcript sets this, so
|
|
40
|
+
* a nested transcript does not use the panel of the page behind it.
|
|
41
|
+
*/
|
|
42
|
+
export function TurnContextPanelProvider(props: {
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
panel?: TurnContextPanel;
|
|
45
|
+
}) {
|
|
46
|
+
return (
|
|
47
|
+
<TurnContextPanelContext value={props.panel}>
|
|
48
|
+
{props.children}
|
|
49
|
+
</TurnContextPanelContext>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Show structured context attached to one transcript user message. The panel
|
|
55
|
+
* keys context by `messageId`, so every control for one message, including a
|
|
56
|
+
* remounted one, shows the same open state.
|
|
57
|
+
*/
|
|
14
58
|
export function TranscriptTurnContextView(props: {
|
|
15
59
|
contexts: TranscriptViewTurnContext[];
|
|
60
|
+
messageId?: string;
|
|
16
61
|
}) {
|
|
62
|
+
const panel = useContext(TurnContextPanelContext);
|
|
17
63
|
const [open, setOpen] = useState(false);
|
|
18
|
-
const
|
|
64
|
+
const instanceId = useId();
|
|
65
|
+
const id = props.messageId ?? instanceId;
|
|
66
|
+
const expanded = panel ? panel.openId === id : open;
|
|
19
67
|
|
|
20
68
|
return (
|
|
21
69
|
<>
|
|
22
70
|
<button
|
|
23
|
-
aria-haspopup="dialog"
|
|
24
|
-
aria-expanded={
|
|
71
|
+
aria-haspopup={panel ? undefined : "dialog"}
|
|
72
|
+
aria-expanded={expanded}
|
|
25
73
|
aria-label="View turn context"
|
|
26
74
|
className={cn(
|
|
27
75
|
"inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus",
|
|
28
|
-
|
|
76
|
+
expanded && "bg-dashboard-fill-hover text-dashboard-text",
|
|
29
77
|
)}
|
|
30
|
-
|
|
78
|
+
data-turn-context-id={id}
|
|
79
|
+
onClick={() => {
|
|
80
|
+
if (!panel) return setOpen(true);
|
|
81
|
+
panel.onToggle({ contexts: props.contexts, id });
|
|
82
|
+
}}
|
|
31
83
|
title="View context"
|
|
32
84
|
type="button"
|
|
33
85
|
>
|
|
34
86
|
<Layers aria-hidden="true" size={14} strokeWidth={1.8} />
|
|
35
87
|
</button>
|
|
36
88
|
|
|
37
|
-
{open
|
|
89
|
+
{!panel && open
|
|
38
90
|
? createPortal(
|
|
39
91
|
<Drawer
|
|
40
92
|
closeLabel="Close turn context"
|
|
41
93
|
header={
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
</h2>
|
|
49
|
-
<p className="m-0 mt-1 text-xs text-dashboard-text-muted">
|
|
50
|
-
Context supplied with this message
|
|
51
|
-
</p>
|
|
52
|
-
</>
|
|
94
|
+
<h2
|
|
95
|
+
className="m-0 text-sm font-semibold text-dashboard-text"
|
|
96
|
+
id={instanceId}
|
|
97
|
+
>
|
|
98
|
+
Turn context
|
|
99
|
+
</h2>
|
|
53
100
|
}
|
|
54
101
|
onClose={() => setOpen(false)}
|
|
55
|
-
openKey={
|
|
56
|
-
titleId={
|
|
102
|
+
openKey={instanceId}
|
|
103
|
+
titleId={instanceId}
|
|
57
104
|
>
|
|
58
|
-
{props.contexts
|
|
59
|
-
<TurnContext
|
|
60
|
-
context={context}
|
|
61
|
-
key={`${context.pluginName}:${context.kind}:${context.version}:${index}`}
|
|
62
|
-
/>
|
|
63
|
-
))}
|
|
105
|
+
<TurnContextList contexts={props.contexts} />
|
|
64
106
|
</Drawer>,
|
|
65
107
|
document.body,
|
|
66
108
|
)
|
|
@@ -69,6 +111,37 @@ export function TranscriptTurnContextView(props: {
|
|
|
69
111
|
);
|
|
70
112
|
}
|
|
71
113
|
|
|
114
|
+
/** Show turn context in the page panel: an aside, or a sheet on small screens. */
|
|
115
|
+
export function TurnContextDetails(props: {
|
|
116
|
+
contexts: TranscriptViewTurnContext[];
|
|
117
|
+
onClose(): void;
|
|
118
|
+
}) {
|
|
119
|
+
return (
|
|
120
|
+
<DetailsPanel
|
|
121
|
+
label="Turn context"
|
|
122
|
+
onClose={props.onClose}
|
|
123
|
+
title={
|
|
124
|
+
<h2 className="m-0 text-base font-semibold text-dashboard-text">
|
|
125
|
+
Turn context
|
|
126
|
+
</h2>
|
|
127
|
+
}
|
|
128
|
+
>
|
|
129
|
+
<div className="px-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
|
|
130
|
+
<TurnContextList contexts={props.contexts} />
|
|
131
|
+
</div>
|
|
132
|
+
</DetailsPanel>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function TurnContextList(props: { contexts: TranscriptViewTurnContext[] }) {
|
|
137
|
+
return props.contexts.map((context, index) => (
|
|
138
|
+
<TurnContext
|
|
139
|
+
context={context}
|
|
140
|
+
key={`${context.pluginName}:${context.kind}:${context.version}:${index}`}
|
|
141
|
+
/>
|
|
142
|
+
));
|
|
143
|
+
}
|
|
144
|
+
|
|
72
145
|
function TurnContext(props: { context: TranscriptViewTurnContext }) {
|
|
73
146
|
const memory = memoryRecallContent(props.context);
|
|
74
147
|
|
|
@@ -117,15 +190,17 @@ function MemoryRecall(props: {
|
|
|
117
190
|
return (
|
|
118
191
|
<div className="overflow-hidden rounded-lg border border-dashboard-border-strong">
|
|
119
192
|
{props.memories.map((memory, index) => (
|
|
120
|
-
<
|
|
121
|
-
className="
|
|
193
|
+
<Collapsible
|
|
194
|
+
className="border-t border-dashboard-border-strong first:border-t-0"
|
|
122
195
|
key={memory.id}
|
|
123
196
|
>
|
|
124
|
-
<
|
|
197
|
+
<TranscriptSummaryTrigger className="group flex items-start gap-2.5">
|
|
125
198
|
<ChevronRight
|
|
126
199
|
aria-hidden="true"
|
|
127
|
-
className=
|
|
128
|
-
|
|
200
|
+
className={cn(
|
|
201
|
+
dashboardDisclosureChevronClass,
|
|
202
|
+
"mt-0.5 text-dashboard-text-muted",
|
|
203
|
+
)}
|
|
129
204
|
/>
|
|
130
205
|
<span className="min-w-0 flex-1">
|
|
131
206
|
<span className="flex items-center justify-between gap-3">
|
|
@@ -140,39 +215,43 @@ function MemoryRecall(props: {
|
|
|
140
215
|
<HighlightText text={memory.content} />
|
|
141
216
|
</span>
|
|
142
217
|
</span>
|
|
143
|
-
</
|
|
144
|
-
|
|
145
|
-
<div className="border-t border-dashboard-border bg-dashboard-fill-faint px-4 py-4">
|
|
146
|
-
<div className="whitespace-pre-wrap text-sm leading-6 text-dashboard-text">
|
|
147
|
-
<HighlightText text={memory.content} />
|
|
148
|
-
</div>
|
|
218
|
+
</TranscriptSummaryTrigger>
|
|
149
219
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<
|
|
154
|
-
<HighlightText text={memory.id} />
|
|
155
|
-
</dd>
|
|
156
|
-
</div>
|
|
157
|
-
<div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
|
|
158
|
-
<dt className="text-dashboard-text-muted">Observed</dt>
|
|
159
|
-
<dd className="m-0 text-dashboard-text-muted">
|
|
160
|
-
{formatTime(new Date(memory.observedAtMs).toISOString())}
|
|
161
|
-
</dd>
|
|
162
|
-
</div>
|
|
163
|
-
<div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
|
|
164
|
-
<dt className="text-dashboard-text-muted">Scope</dt>
|
|
165
|
-
<dd className="m-0 text-dashboard-text-muted">
|
|
166
|
-
{memory.scope}
|
|
167
|
-
</dd>
|
|
168
|
-
</div>
|
|
169
|
-
<div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
|
|
170
|
-
<dt className="text-dashboard-text-muted">Kind</dt>
|
|
171
|
-
<dd className="m-0 text-dashboard-text-muted">{memory.kind}</dd>
|
|
220
|
+
<CollapsibleContent hiddenUntilFound>
|
|
221
|
+
<div className="border-t border-dashboard-border bg-dashboard-fill-faint px-4 py-4">
|
|
222
|
+
<div className="whitespace-pre-wrap text-sm leading-6 text-dashboard-text">
|
|
223
|
+
<HighlightText text={memory.content} />
|
|
172
224
|
</div>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
225
|
+
|
|
226
|
+
<dl className="mt-4 grid gap-2 text-xs">
|
|
227
|
+
<div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
|
|
228
|
+
<dt className="text-dashboard-text-muted">Memory ID</dt>
|
|
229
|
+
<dd className="m-0 break-all font-mono text-dashboard-text-muted">
|
|
230
|
+
<HighlightText text={memory.id} />
|
|
231
|
+
</dd>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
|
|
234
|
+
<dt className="text-dashboard-text-muted">Observed</dt>
|
|
235
|
+
<dd className="m-0 text-dashboard-text-muted">
|
|
236
|
+
{formatTime(new Date(memory.observedAtMs).toISOString())}
|
|
237
|
+
</dd>
|
|
238
|
+
</div>
|
|
239
|
+
<div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
|
|
240
|
+
<dt className="text-dashboard-text-muted">Scope</dt>
|
|
241
|
+
<dd className="m-0 text-dashboard-text-muted">
|
|
242
|
+
{memory.scope}
|
|
243
|
+
</dd>
|
|
244
|
+
</div>
|
|
245
|
+
<div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
|
|
246
|
+
<dt className="text-dashboard-text-muted">Kind</dt>
|
|
247
|
+
<dd className="m-0 text-dashboard-text-muted">
|
|
248
|
+
{memory.kind}
|
|
249
|
+
</dd>
|
|
250
|
+
</div>
|
|
251
|
+
</dl>
|
|
252
|
+
</div>
|
|
253
|
+
</CollapsibleContent>
|
|
254
|
+
</Collapsible>
|
|
176
255
|
))}
|
|
177
256
|
<p className="m-0 border-t border-dashboard-border-strong px-3 py-2 text-xs text-dashboard-text-muted">
|
|
178
257
|
{props.memories.length}{" "}
|
|
@@ -1,20 +1,31 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import { ArrowDownToLine } from "lucide-react";
|
|
2
3
|
|
|
3
4
|
import type {
|
|
4
5
|
ConversationTranscript,
|
|
5
6
|
TranscriptViewSubagentPart,
|
|
6
7
|
} from "../types";
|
|
7
|
-
import { Button } from "../components/
|
|
8
|
+
import { Button } from "../components/ui/button";
|
|
9
|
+
import {
|
|
10
|
+
Tooltip,
|
|
11
|
+
TooltipContent,
|
|
12
|
+
TooltipTrigger,
|
|
13
|
+
} from "../components/ui/tooltip";
|
|
8
14
|
import { ConversationTranscriptView } from "./ConversationTranscript";
|
|
9
15
|
import { ConversationEventLog } from "./ConversationEventLog";
|
|
10
16
|
import {
|
|
11
17
|
transcriptBottomVersion,
|
|
12
18
|
transcriptJuniorMessageVersion,
|
|
19
|
+
useAutoLoadPreviousPage,
|
|
13
20
|
usePinnedTranscriptBottom,
|
|
14
21
|
} from "./transcriptBottomPinning";
|
|
15
22
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
16
23
|
import { transcriptEmptyClass } from "./transcriptStyles";
|
|
17
24
|
import { TranscriptSearchProvider } from "./transcriptSearch";
|
|
25
|
+
import {
|
|
26
|
+
TurnContextPanelProvider,
|
|
27
|
+
type TurnContextPanel,
|
|
28
|
+
} from "./TranscriptTurnContextView";
|
|
18
29
|
|
|
19
30
|
/** Render one conversation transcript as ordered message and tool events. */
|
|
20
31
|
export function Transcript(props: {
|
|
@@ -28,16 +39,20 @@ export function Transcript(props: {
|
|
|
28
39
|
onSelectedEventSeqChange?(seq: number | undefined): void;
|
|
29
40
|
responding?: boolean;
|
|
30
41
|
search?: string;
|
|
42
|
+
onSearchChange?(value: string): void;
|
|
31
43
|
selectedEventSeq?: number;
|
|
32
44
|
onOpenSubagentTranscript?: (args: {
|
|
33
45
|
part: TranscriptViewSubagentPart;
|
|
34
46
|
conversation: ConversationTranscript;
|
|
35
47
|
}) => void;
|
|
36
48
|
transcript?: ConversationTranscript;
|
|
49
|
+
/** Show turn context in the owner's panel. Without it, a drawer opens. */
|
|
50
|
+
turnContextPanel?: TurnContextPanel;
|
|
37
51
|
view?: TranscriptViewMode;
|
|
38
52
|
}) {
|
|
39
53
|
const view = props.view ?? "rich";
|
|
40
|
-
const
|
|
54
|
+
const [localSearch, setLocalSearch] = useState("");
|
|
55
|
+
const search = props.search ?? localSearch;
|
|
41
56
|
const historyLabel = view === "raw" ? "events" : "messages";
|
|
42
57
|
const redacted = props.transcript?.eventHistory.status === "redacted";
|
|
43
58
|
const bottomPinning = usePinnedTranscriptBottom({
|
|
@@ -54,6 +69,21 @@ export function Transcript(props: {
|
|
|
54
69
|
},
|
|
55
70
|
});
|
|
56
71
|
|
|
72
|
+
const loadPreviousPage = () => {
|
|
73
|
+
bottomPinning.preserveViewportForPrepend();
|
|
74
|
+
props.onLoadPreviousPage?.();
|
|
75
|
+
};
|
|
76
|
+
const previousPageRef = useAutoLoadPreviousPage({
|
|
77
|
+
enabled: Boolean(
|
|
78
|
+
props.onLoadPreviousPage &&
|
|
79
|
+
props.hasPreviousPage &&
|
|
80
|
+
!props.loadingPreviousPage &&
|
|
81
|
+
!props.historyError,
|
|
82
|
+
),
|
|
83
|
+
historyVersion: props.historyVersion ?? "empty",
|
|
84
|
+
onLoad: loadPreviousPage,
|
|
85
|
+
});
|
|
86
|
+
|
|
57
87
|
if (!props.transcript) {
|
|
58
88
|
return (
|
|
59
89
|
<div className={transcriptEmptyClass()}>
|
|
@@ -71,16 +101,13 @@ export function Transcript(props: {
|
|
|
71
101
|
</div>
|
|
72
102
|
) : null}
|
|
73
103
|
{props.hasPreviousPage || props.loadingPreviousPage ? (
|
|
74
|
-
<div className="mb-3 flex items-center gap-3">
|
|
104
|
+
<div className="mb-3 flex items-center gap-3" ref={previousPageRef}>
|
|
75
105
|
<span className="h-px min-w-4 flex-1 bg-dashboard-fill-strong" />
|
|
76
106
|
<button
|
|
77
107
|
aria-label="Load earlier events"
|
|
78
108
|
className="shrink-0 cursor-pointer border-0 bg-transparent px-1 py-0.5 font-sans text-xs font-medium text-dashboard-text-muted transition-colors hover:text-dashboard-text focus-visible:outline focus-visible:outline-1 focus-visible:outline-dashboard-focus/55 disabled:cursor-not-allowed disabled:opacity-50"
|
|
79
109
|
disabled={props.loadingPreviousPage}
|
|
80
|
-
onClick={
|
|
81
|
-
bottomPinning.preserveViewportForPrepend();
|
|
82
|
-
props.onLoadPreviousPage?.();
|
|
83
|
-
}}
|
|
110
|
+
onClick={loadPreviousPage}
|
|
84
111
|
title="Load earlier events"
|
|
85
112
|
type="button"
|
|
86
113
|
>
|
|
@@ -103,15 +130,18 @@ export function Transcript(props: {
|
|
|
103
130
|
<ConversationEventLog
|
|
104
131
|
key={props.transcript.conversationId}
|
|
105
132
|
conversation={props.transcript}
|
|
133
|
+
onSearchChange={props.onSearchChange ?? setLocalSearch}
|
|
106
134
|
onSelectedSeqChange={props.onSelectedEventSeqChange}
|
|
107
135
|
selectedSeq={props.selectedEventSeq}
|
|
108
136
|
/>
|
|
109
137
|
) : (
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
<TurnContextPanelProvider panel={props.turnContextPanel}>
|
|
139
|
+
<ConversationTranscriptView
|
|
140
|
+
onOpenSubagentTranscript={props.onOpenSubagentTranscript}
|
|
141
|
+
conversation={props.transcript}
|
|
142
|
+
responding={props.responding ?? props.live ?? false}
|
|
143
|
+
/>
|
|
144
|
+
</TurnContextPanelProvider>
|
|
115
145
|
)}
|
|
116
146
|
<div
|
|
117
147
|
aria-hidden="true"
|
|
@@ -141,21 +171,27 @@ function JumpToLatestButton(props: {
|
|
|
141
171
|
|
|
142
172
|
return (
|
|
143
173
|
<div className="pointer-events-none sticky bottom-3 z-20 flex h-0 justify-center px-3 md:fixed md:inset-x-auto md:bottom-6 md:right-8 md:h-auto md:justify-end md:px-0">
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
<Tooltip>
|
|
175
|
+
<TooltipTrigger
|
|
176
|
+
render={
|
|
177
|
+
<Button
|
|
178
|
+
aria-label={label}
|
|
179
|
+
className="pointer-events-auto relative -translate-y-full rounded-lg !border !border-dashboard-border-emphasis !bg-dashboard-surface-active text-dashboard-text shadow-[0_6px_24px_rgba(0,0,0,0.36)] hover:!border-dashboard-border-interactive hover:!bg-dashboard-surface-hover hover:!text-dashboard-text md:translate-y-0"
|
|
180
|
+
onClick={props.onClick}
|
|
181
|
+
size="icon"
|
|
182
|
+
>
|
|
183
|
+
<ArrowDownToLine aria-hidden="true" size={16} strokeWidth={2} />
|
|
184
|
+
{props.hasPendingUpdate ? (
|
|
185
|
+
<span
|
|
186
|
+
aria-hidden="true"
|
|
187
|
+
className="absolute right-1.5 top-1.5 size-2 bg-emerald-300"
|
|
188
|
+
/>
|
|
189
|
+
) : null}
|
|
190
|
+
</Button>
|
|
191
|
+
}
|
|
192
|
+
/>
|
|
193
|
+
<TooltipContent size="compact">{label}</TooltipContent>
|
|
194
|
+
</Tooltip>
|
|
159
195
|
</div>
|
|
160
196
|
);
|
|
161
197
|
}
|
|
@@ -111,6 +111,7 @@ export function eventLogTone(data: ConversationReportEventData): string {
|
|
|
111
111
|
}
|
|
112
112
|
if (
|
|
113
113
|
(data.type === "guardian_action_reviewed" && data.decision === "ask") ||
|
|
114
|
+
(data.type === "turn_lifecycle" && data.state === "blocked") ||
|
|
114
115
|
(data.type === "subagent" &&
|
|
115
116
|
(data.status === "aborted" || data.status === "blocked"))
|
|
116
117
|
) {
|
|
@@ -118,3 +119,60 @@ export function eventLogTone(data: ConversationReportEventData): string {
|
|
|
118
119
|
}
|
|
119
120
|
return "text-dashboard-text-subtle";
|
|
120
121
|
}
|
|
122
|
+
|
|
123
|
+
/** One event log entry with its display text. */
|
|
124
|
+
export type EventLogRow = {
|
|
125
|
+
event: ConversationReportEvent;
|
|
126
|
+
summary: string;
|
|
127
|
+
metadata: string;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Build the display text for each event log entry. */
|
|
131
|
+
export function eventLogRows(
|
|
132
|
+
events: readonly ConversationReportEvent[],
|
|
133
|
+
): EventLogRow[] {
|
|
134
|
+
return events.map((event) => {
|
|
135
|
+
const model = eventLogModel(event);
|
|
136
|
+
return {
|
|
137
|
+
event,
|
|
138
|
+
summary: eventLogSummary(event.data).replace(/\s+/g, " ").slice(0, 300),
|
|
139
|
+
metadata: [
|
|
140
|
+
model?.modelId,
|
|
141
|
+
model?.modelProfile,
|
|
142
|
+
model?.reasoningLevel && `${model.reasoningLevel} reasoning`,
|
|
143
|
+
eventLogUsage(event),
|
|
144
|
+
]
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.join(" · "),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Keep the rows that match a trimmed, lowercase search query. The match
|
|
153
|
+
* includes the full event payload, so large payloads are serialized here.
|
|
154
|
+
*/
|
|
155
|
+
export function filterEventLogRows(
|
|
156
|
+
rows: EventLogRow[],
|
|
157
|
+
normalizedQuery: string,
|
|
158
|
+
): EventLogRow[] {
|
|
159
|
+
if (!normalizedQuery) return rows;
|
|
160
|
+
return rows.filter(({ event, summary, metadata }) =>
|
|
161
|
+
`${summary} ${metadata} ${JSON.stringify(event)}`
|
|
162
|
+
.toLowerCase()
|
|
163
|
+
.includes(normalizedQuery),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Find the visible rows before and after the selected event. */
|
|
168
|
+
export function adjacentEventLogSeqs(
|
|
169
|
+
rows: readonly EventLogRow[],
|
|
170
|
+
seq: number | undefined,
|
|
171
|
+
): { previous?: number; next?: number } {
|
|
172
|
+
const index = rows.findIndex((row) => row.event.seq === seq);
|
|
173
|
+
if (index === -1) return {};
|
|
174
|
+
return {
|
|
175
|
+
previous: rows[index - 1]?.event.seq,
|
|
176
|
+
next: rows[index + 1]?.event.seq,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useLocalStorage } from "@uidotdev/usehooks";
|
|
3
|
+
|
|
4
|
+
const storageKey = "junior:event-log:time-mode";
|
|
5
|
+
|
|
6
|
+
/** Share the browser-only UTC/local preference between event rows and details. */
|
|
7
|
+
export function useEventTime() {
|
|
8
|
+
const [mode, setMode] = useLocalStorage<"utc" | "local">(storageKey, "utc");
|
|
9
|
+
const local = mode === "local";
|
|
10
|
+
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
11
|
+
const timeZone = local ? localTimeZone : "UTC";
|
|
12
|
+
const formatters = useMemo(() => {
|
|
13
|
+
const clock: Intl.DateTimeFormatOptions = {
|
|
14
|
+
timeZone,
|
|
15
|
+
hour: "2-digit",
|
|
16
|
+
minute: "2-digit",
|
|
17
|
+
second: "2-digit",
|
|
18
|
+
fractionalSecondDigits: 3,
|
|
19
|
+
hourCycle: "h23",
|
|
20
|
+
numberingSystem: "latn",
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
clock: new Intl.DateTimeFormat("en-GB", clock),
|
|
24
|
+
timestamp: new Intl.DateTimeFormat(undefined, {
|
|
25
|
+
...clock,
|
|
26
|
+
year: "numeric",
|
|
27
|
+
month: "short",
|
|
28
|
+
day: "numeric",
|
|
29
|
+
timeZoneName: "short",
|
|
30
|
+
}),
|
|
31
|
+
// A log can span both standard time and daylight-saving time.
|
|
32
|
+
zone: new Intl.DateTimeFormat(undefined, {
|
|
33
|
+
timeZone,
|
|
34
|
+
timeZoneName: "shortGeneric",
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
}, [timeZone]);
|
|
38
|
+
const label = local
|
|
39
|
+
? (formatters.zone
|
|
40
|
+
.formatToParts()
|
|
41
|
+
.find((part) => part.type === "timeZoneName")?.value ?? localTimeZone)
|
|
42
|
+
: "UTC";
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
local,
|
|
46
|
+
label,
|
|
47
|
+
title: local
|
|
48
|
+
? `${localTimeZone} · Switch to UTC`
|
|
49
|
+
: `UTC · Switch to local time (${localTimeZone})`,
|
|
50
|
+
toggle() {
|
|
51
|
+
setMode(local ? "utc" : "local");
|
|
52
|
+
},
|
|
53
|
+
formatClock(value: string) {
|
|
54
|
+
return formatters.clock.format(new Date(value));
|
|
55
|
+
},
|
|
56
|
+
formatTimestamp(value: string) {
|
|
57
|
+
return local ? formatters.timestamp.format(new Date(value)) : value;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -663,6 +663,35 @@ export function useConversationData(conversationId: string | undefined) {
|
|
|
663
663
|
},
|
|
664
664
|
);
|
|
665
665
|
}, [conversationId, detailData, mailbox, outbox.data, queryClient]);
|
|
666
|
+
// The title is generated after the first message, and only the open
|
|
667
|
+
// Conversation polls. Copy a newer title into loaded feeds, such as the
|
|
668
|
+
// sidebar and the home list, so they do not keep the old one.
|
|
669
|
+
const displayTitle = detail.data?.displayTitle;
|
|
670
|
+
const detailUpdatedAt = detail.dataUpdatedAt;
|
|
671
|
+
useEffect(() => {
|
|
672
|
+
if (!conversationId || !displayTitle) return;
|
|
673
|
+
const feeds = queryClient
|
|
674
|
+
.getQueryCache()
|
|
675
|
+
.findAll({ queryKey: ["dashboard", "conversations"] });
|
|
676
|
+
for (const query of feeds) {
|
|
677
|
+
const feed = query.state.data as ConversationFeed | undefined;
|
|
678
|
+
if (!feed || query.state.dataUpdatedAt > detailUpdatedAt) continue;
|
|
679
|
+
const stale = feed.conversations.some(
|
|
680
|
+
(conversation) =>
|
|
681
|
+
conversation.conversationId === conversationId &&
|
|
682
|
+
conversation.displayTitle !== displayTitle,
|
|
683
|
+
);
|
|
684
|
+
if (!stale) continue;
|
|
685
|
+
queryClient.setQueryData<ConversationFeed>(query.queryKey, {
|
|
686
|
+
...feed,
|
|
687
|
+
conversations: feed.conversations.map((conversation) =>
|
|
688
|
+
conversation.conversationId === conversationId
|
|
689
|
+
? { ...conversation, displayTitle }
|
|
690
|
+
: conversation,
|
|
691
|
+
),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}, [conversationId, detailUpdatedAt, displayTitle, queryClient]);
|
|
666
695
|
const historyStatus = detailData?.eventHistory.status;
|
|
667
696
|
const historyQueryKey = useMemo(
|
|
668
697
|
() => ["conversation", conversationId, "history", historyStatus] as const,
|
|
@@ -15,6 +15,7 @@ import type { ConversationTranscript } from "../types";
|
|
|
15
15
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
16
16
|
|
|
17
17
|
const BOTTOM_PROXIMITY_PX = 96;
|
|
18
|
+
const PREVIOUS_PAGE_PROXIMITY_PX = 600;
|
|
18
19
|
const USER_SCROLL_DELTA_PX = 2;
|
|
19
20
|
|
|
20
21
|
type ScrollRoot = HTMLElement | Window;
|
|
@@ -650,6 +651,39 @@ export function usePinnedTranscriptBottom(input: {
|
|
|
650
651
|
);
|
|
651
652
|
}
|
|
652
653
|
|
|
654
|
+
/** Load the previous history page when the reader scrolls near the top. */
|
|
655
|
+
export function useAutoLoadPreviousPage(input: {
|
|
656
|
+
enabled: boolean;
|
|
657
|
+
historyVersion: string;
|
|
658
|
+
onLoad: () => void;
|
|
659
|
+
}): RefCallback<HTMLElement> {
|
|
660
|
+
const [sentinel, setSentinel] = useState<HTMLElement | null>(null);
|
|
661
|
+
const onLoadRef = useRef(input.onLoad);
|
|
662
|
+
onLoadRef.current = input.onLoad;
|
|
663
|
+
|
|
664
|
+
useEffect(() => {
|
|
665
|
+
if (!input.enabled || !sentinel) return;
|
|
666
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
667
|
+
|
|
668
|
+
const root = scrollRootFor(sentinel);
|
|
669
|
+
const observer = new IntersectionObserver(
|
|
670
|
+
(entries) => {
|
|
671
|
+
if (!entries.some((entry) => entry.isIntersecting)) return;
|
|
672
|
+
observer.disconnect();
|
|
673
|
+
onLoadRef.current();
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
root: root && !isWindowRoot(root) ? root : null,
|
|
677
|
+
rootMargin: `${PREVIOUS_PAGE_PROXIMITY_PX}px 0px 0px 0px`,
|
|
678
|
+
},
|
|
679
|
+
);
|
|
680
|
+
observer.observe(sentinel);
|
|
681
|
+
return () => observer.disconnect();
|
|
682
|
+
}, [input.enabled, input.historyVersion, sentinel]);
|
|
683
|
+
|
|
684
|
+
return setSentinel;
|
|
685
|
+
}
|
|
686
|
+
|
|
653
687
|
/** Keep the previously visible content at the same viewport offset after a prepend. */
|
|
654
688
|
export function scrollTopAfterPrepend(
|
|
655
689
|
previous: Pick<ScrollSnapshot, "scrollHeight" | "scrollTop">,
|