feat(webui): segment transcript storage
This commit is contained in:
@@ -38,11 +38,16 @@ interface ThreadViewportProps {
|
||||
cliApps?: CliAppInfo[];
|
||||
mcpPresets?: McpPresetInfo[];
|
||||
forkBoundaryMessageCount?: number | null;
|
||||
hasMoreBefore?: boolean;
|
||||
loadingOlder?: boolean;
|
||||
userMessageOffset?: number;
|
||||
onLoadOlder?: () => Promise<void> | void;
|
||||
onOpenFilePreview?: (path: string) => void;
|
||||
onForkFromMessage?: (beforeUserIndex: number) => void;
|
||||
}
|
||||
|
||||
const NEAR_BOTTOM_PX = 48;
|
||||
const NEAR_TOP_PX = 96;
|
||||
const DEFAULT_SCROLL_BUTTON_BOTTOM_PX = 192;
|
||||
const SCROLL_BUTTON_COMPOSER_GAP_PX = 16;
|
||||
export const INITIAL_HISTORY_WINDOW = 160;
|
||||
@@ -72,6 +77,10 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
cliApps = [],
|
||||
mcpPresets = [],
|
||||
forkBoundaryMessageCount = null,
|
||||
hasMoreBefore = false,
|
||||
loadingOlder = false,
|
||||
userMessageOffset = 0,
|
||||
onLoadOlder,
|
||||
onOpenFilePreview,
|
||||
onForkFromMessage,
|
||||
}, ref) {
|
||||
@@ -99,9 +108,10 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
);
|
||||
const hiddenMessageCount = messages.length - visibleMessages.length;
|
||||
const hiddenUserMessageCount =
|
||||
hiddenMessageCount > 0
|
||||
userMessageOffset
|
||||
+ (hiddenMessageCount > 0
|
||||
? messages.slice(0, hiddenMessageCount).filter((message) => message.role === "user").length
|
||||
: 0;
|
||||
: 0);
|
||||
const visibleForkBoundaryMessageCount =
|
||||
forkBoundaryMessageCount !== null && forkBoundaryMessageCount > hiddenMessageCount
|
||||
? forkBoundaryMessageCount - hiddenMessageCount
|
||||
@@ -126,6 +136,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
} else if (el) {
|
||||
el.scrollTo({ top: el.scrollHeight, behavior });
|
||||
}
|
||||
userReadingHistoryRef.current = false;
|
||||
setAtBottom(true);
|
||||
}, []);
|
||||
|
||||
@@ -159,10 +170,26 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
}
|
||||
userReadingHistoryRef.current = true;
|
||||
setAtBottom(false);
|
||||
setVisibleMessageCount((count) =>
|
||||
Math.min(messages.length, count + HISTORY_WINDOW_INCREMENT),
|
||||
);
|
||||
}, [messages.length]);
|
||||
if (hiddenMessageCount > 0) {
|
||||
setVisibleMessageCount((count) =>
|
||||
Math.min(messages.length, count + HISTORY_WINDOW_INCREMENT),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (hasMoreBefore && onLoadOlder && !loadingOlder) {
|
||||
setVisibleMessageCount((count) => count + HISTORY_WINDOW_INCREMENT);
|
||||
void onLoadOlder();
|
||||
}
|
||||
}, [hasMoreBefore, hiddenMessageCount, loadingOlder, messages.length, onLoadOlder]);
|
||||
|
||||
const maybeLoadEarlierFromScroll = useCallback(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el || !hasMessages || pendingConversationScrollRef.current) return;
|
||||
if (!userReadingHistoryRef.current) return;
|
||||
if (el.scrollTop > NEAR_TOP_PX) return;
|
||||
if (hiddenMessageCount <= 0 && !hasMoreBefore) return;
|
||||
loadEarlierMessages();
|
||||
}, [hasMessages, hasMoreBefore, hiddenMessageCount, loadEarlierMessages]);
|
||||
|
||||
const jumpToUserPrompt = useCallback((promptId: string) => {
|
||||
const scrollEl = scrollRef.current;
|
||||
@@ -218,8 +245,17 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
restoreScrollAfterPrependRef.current = null;
|
||||
if (!el) return;
|
||||
const delta = el.scrollHeight - pending.height;
|
||||
el.scrollTop = pending.top + delta;
|
||||
}, [visibleMessages.length]);
|
||||
const nextTop = pending.top + delta;
|
||||
try {
|
||||
el.scrollTop = nextTop;
|
||||
} catch {
|
||||
try {
|
||||
el.scrollTo?.({ top: nextTop, behavior: "auto" });
|
||||
} catch {
|
||||
// Test DOMs can expose read-only scrollTop; browsers keep this writable.
|
||||
}
|
||||
}
|
||||
}, [visibleMessages.length, messages.length]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const promptId = pendingPromptJumpRef.current;
|
||||
@@ -271,17 +307,19 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const onScroll = () => {
|
||||
const onScroll = (allowHistoryLoad = true) => {
|
||||
const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
const near = distance < NEAR_BOTTOM_PX;
|
||||
setAtBottom(near);
|
||||
userReadingHistoryRef.current = !near;
|
||||
if (allowHistoryLoad && !near) maybeLoadEarlierFromScroll();
|
||||
};
|
||||
|
||||
onScroll();
|
||||
el.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => el.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
onScroll(false);
|
||||
const handleScroll = () => onScroll(true);
|
||||
el.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => el.removeEventListener("scroll", handleScroll);
|
||||
}, [maybeLoadEarlierFromScroll]);
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-0 flex-1 overflow-hidden">
|
||||
@@ -302,9 +340,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
<ThreadMessages
|
||||
messages={visibleMessages}
|
||||
isStreaming={isStreaming}
|
||||
hiddenMessageCount={hiddenMessageCount}
|
||||
hiddenUserMessageCount={hiddenUserMessageCount}
|
||||
onLoadEarlier={loadEarlierMessages}
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
forkBoundaryMessageCount={visibleForkBoundaryMessageCount}
|
||||
|
||||
Reference in New Issue
Block a user