fix(chat): rework prompt navigator rail into sliding tape with hover preview (#2185)
* fix(chat): rework prompt navigator rail into sliding tape with hover preview - Pin the active indicator to the target during programmatic scrolls so the scroll spy's intermediate reports don't drag it backwards mid-animation - Replace the visibility-ratio active-turn picker with a stable reading-line rule (last turn whose top is above the line), dropping IntersectionObserver - Replace the list panel with a Codex-style gutter: the whole strip is one hover/click target mapped to the nearest tick, with a per-prompt preview card that follows the cursor - Cap the rail at a fixed window of ticks; hovering the edges carousels through the rest, with gradient masks hinting at more content - Render ticks as a tape that glides to keep the active prompt centered, remounting on history prepend to avoid spurious slide animations - Keep load-earlier as a compact button aligned over the tick column * fix(chat): shrink navigator gutter when message column sits under it On narrow windows the centered message column extends under the rail's full-width invisible hover zone, which swallowed clicks on the right edge of user bubbles — including the expand/collapse control. Measure the column against the gutter and switch to a narrow hit zone when they overlap. * fix(sync): stop runaway history auto-load on sessions with empty assistant messages An assistant message fetched with zero parts (e.g. a run aborted before any output) was stored as absence — indistinguishable from parts that were never fetched. getSessionMaterializationStatus therefore reported the session as never renderable, so the ensure-renderable effects (ChatContainer, ModelControls) retried syncSession forever; each retry refetched the whole grown window and fired another background prepend, progressively loading the entire history of large sessions on open. Commit an explicit empty [] snapshot for assistant messages so fetched-empty counts as renderable, while non-assistant messages keep the absent representation and its no-op commit behavior. Reproduced and verified headless against a real 857-message session: before, 20 message fetches escalating to limit=857; after, one initial page and a single progressive-mount prepend.
This commit is contained in:
committed by
GitHub
parent
697b180532
commit
799904f0f4
@@ -76,6 +76,9 @@ const MOBILE_TURN_MODEL_CACHE_MAX = 4
|
||||
const MOBILE_TURN_MODEL_CACHE_MAX_MESSAGES = 30
|
||||
const HISTORY_RENDER_WAIT_TIMEOUT_MS = 250
|
||||
const HISTORY_INTERACTION_GUARD_MS = 2000
|
||||
// Long smooth scrolls across a big session can take a couple of seconds;
|
||||
// the pin releases early as soon as the spy reports the target turn.
|
||||
const SCROLL_PIN_TIMEOUT_MS = 2500
|
||||
const turnModelCache = new Map<string, { messages: ChatMessageEntry[]; model: TurnWindowModel }>()
|
||||
const getTurnModelCacheMax = () => {
|
||||
if (isVSCodeRuntime()) return VSCODE_TURN_MODEL_CACHE_MAX
|
||||
@@ -241,6 +244,7 @@ export const useChatTimelineController = ({
|
||||
const initializedSessionRef = React.useRef<string | null>(null);
|
||||
const pendingRenderResolversRef = React.useRef<Array<() => void>>([]);
|
||||
const pendingScrollRequestRef = React.useRef<PendingScrollRequest | null>(null);
|
||||
const scrollPinRef = React.useRef<{ turnId: string; expiresAt: number } | null>(null);
|
||||
const historyInteractionRef = React.useRef(false);
|
||||
const historyInteractionTimerRef = React.useRef<number | null>(null);
|
||||
|
||||
@@ -305,6 +309,7 @@ export const useChatTimelineController = ({
|
||||
initializedSessionRef.current = sessionId;
|
||||
setIsLoadingOlder(false);
|
||||
setPendingRevealWork(false);
|
||||
scrollPinRef.current = null;
|
||||
setActiveTurnId(null);
|
||||
}, [sessionId]);
|
||||
|
||||
@@ -362,6 +367,13 @@ export const useChatTimelineController = ({
|
||||
|
||||
if (didScroll) {
|
||||
if (pending.turnId) {
|
||||
// Pin the indicator to the target so the scroll spy's
|
||||
// intermediate reports during the smooth scroll don't drag
|
||||
// it backwards before the animation lands.
|
||||
scrollPinRef.current = {
|
||||
turnId: pending.turnId,
|
||||
expiresAt: Date.now() + SCROLL_PIN_TIMEOUT_MS,
|
||||
};
|
||||
setActiveTurnId(pending.turnId);
|
||||
}
|
||||
resolvePendingScrollRequest(true);
|
||||
@@ -890,6 +902,13 @@ export const useChatTimelineController = ({
|
||||
}, [goToBottom]);
|
||||
|
||||
const handleActiveTurnChange = React.useCallback((turnId: string | null) => {
|
||||
const pin = scrollPinRef.current;
|
||||
if (pin) {
|
||||
if (turnId !== pin.turnId && Date.now() < pin.expiresAt) {
|
||||
return;
|
||||
}
|
||||
scrollPinRef.current = null;
|
||||
}
|
||||
setActiveTurnId(turnId);
|
||||
}, []);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user