generated from nhcarrigan/template
fix: correct autoscroll race conditions and lazy load trigger
- Move isRestoringScroll = true before await tick() so scroll events fired during the DOM transition cannot incorrectly set shouldAutoScroll to false, breaking autoscroll for all tabs except the first - Guard the windowStart reactive block with isSwitchingConversation so stale lines from the previous conversation cannot override the correct windowStart after a tab switch - Fix lazy load threshold to trigger relative to the visible window top (windowStart * AVG_LINE_HEIGHT + 300) rather than the absolute scroll container top, so older messages load without scrolling through the spacer
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { claudeStore, type TerminalLine } from "$lib/stores/claude";
|
||||
import { afterUpdate, tick, onMount, onDestroy } from "svelte";
|
||||
import { get } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import ConversationTabs from "./ConversationTabs.svelte";
|
||||
@@ -25,6 +26,7 @@
|
||||
let isRestoringScroll = false;
|
||||
let windowStart = 0;
|
||||
let isLoadingMore = false;
|
||||
let isSwitchingConversation = false;
|
||||
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
searchQuery.subscribe((value) => {
|
||||
@@ -59,24 +61,39 @@
|
||||
|
||||
currentConversationId = newId;
|
||||
|
||||
// Peek at the saved position to set windowStart before the first tick,
|
||||
// preventing a stale windowStart from a previous conversation leaving
|
||||
// visibleLines empty (windowStart >= lines.length).
|
||||
// Guard the $: reactive auto-scroll block from firing with stale `lines`
|
||||
// (the old conversation's data) during the switch. Without this, Svelte's
|
||||
// reactive system can re-run the window-advance block before `terminalLines`
|
||||
// has recomputed for the new conversation, overriding our correct windowStart.
|
||||
isSwitchingConversation = true;
|
||||
|
||||
// Read the new conversation's lines directly from the store — the derived
|
||||
// `terminalLines` store (and thus `lines`) may not have recomputed yet when
|
||||
// this subscriber fires, so using `lines` here would give stale data.
|
||||
const newConvLines = get(claudeStore.conversations).get(newId)?.terminalLines ?? [];
|
||||
const savedPosition = claudeStore.getScrollPosition(newId);
|
||||
if (savedPosition === -1) {
|
||||
// Will auto-scroll: pin the window to the tail of the new conversation
|
||||
shouldAutoScroll = true;
|
||||
windowStart = Math.max(0, lines.length - WINDOW_SIZE);
|
||||
windowStart = Math.max(0, newConvLines.length - WINDOW_SIZE);
|
||||
} else {
|
||||
// Will restore a specific position: always start from the top of history
|
||||
shouldAutoScroll = false;
|
||||
windowStart = 0;
|
||||
}
|
||||
|
||||
// Restore scroll position for the new conversation after DOM updates
|
||||
// Block the scroll handler during the entire DOM transition — scroll events
|
||||
// can fire mid-tick when the content changes, and handleScroll would see
|
||||
// scrollTop not at the bottom yet and set shouldAutoScroll = false, breaking
|
||||
// autoscroll for the new conversation permanently.
|
||||
isRestoringScroll = true;
|
||||
|
||||
// Restore scroll position for the new conversation after DOM updates.
|
||||
// Clear the switch guard first so the $: block can react to new lines
|
||||
// arriving after the switch settles.
|
||||
await tick();
|
||||
isSwitchingConversation = false;
|
||||
if (terminalElement) {
|
||||
isRestoringScroll = true;
|
||||
if (savedPosition === -1) {
|
||||
terminalElement.scrollTop = terminalElement.scrollHeight;
|
||||
} else {
|
||||
@@ -94,8 +111,10 @@
|
||||
const { scrollTop, scrollHeight, clientHeight } = terminalElement;
|
||||
shouldAutoScroll = scrollHeight - scrollTop - clientHeight < 100;
|
||||
|
||||
// Load older lines when the user scrolls near the top
|
||||
if (scrollTop < 300 && windowStart > 0 && !isLoadingMore) {
|
||||
// Load older lines when the user scrolls near the top of the visible window.
|
||||
// Use windowStart * AVG_LINE_HEIGHT (the spacer height) as the baseline so
|
||||
// we trigger at the top of the rendered content, not the absolute container top.
|
||||
if (scrollTop < windowStart * AVG_LINE_HEIGHT + 300 && windowStart > 0 && !isLoadingMore) {
|
||||
isLoadingMore = true;
|
||||
const prevScrollHeight = terminalElement.scrollHeight;
|
||||
const prevScrollTop = terminalElement.scrollTop;
|
||||
@@ -182,8 +201,10 @@
|
||||
// Height of the invisible spacer above the visible window
|
||||
$: topSpacerHeight = windowStart * AVG_LINE_HEIGHT;
|
||||
|
||||
// Advance the window forward when auto-scrolling and new lines overflow it
|
||||
$: if (shouldAutoScroll && lines.length > windowStart + WINDOW_SIZE) {
|
||||
// Advance the window forward when auto-scrolling and new lines overflow it.
|
||||
// Skip during conversation switches — `lines` may still hold the previous
|
||||
// conversation's data, which would push windowStart past the new conv's end.
|
||||
$: if (shouldAutoScroll && !isSwitchingConversation && lines.length > windowStart + WINDOW_SIZE) {
|
||||
windowStart = Math.max(0, lines.length - WINDOW_SIZE);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user