feat: persist scroll position per conversation tab

- Added scrollPosition to Conversation interface
- Save scroll position when switching away from a tab
- Restore exact scroll position when switching back
- Uses -1 to indicate auto-scroll mode (scroll to bottom)
- Prevents interference between scroll restore and auto-scroll
This commit is contained in:
2026-01-23 14:57:06 -08:00
committed by Naomi Carrigan
parent 371e4efde3
commit c088dc0096
3 changed files with 59 additions and 3 deletions
+37 -3
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { claudeStore, type TerminalLine } from "$lib/stores/claude";
import { afterUpdate } from "svelte";
import { afterUpdate, tick } from "svelte";
import ConversationTabs from "./ConversationTabs.svelte";
import Markdown from "./Markdown.svelte";
import HighlightedText from "./HighlightedText.svelte";
@@ -10,6 +10,8 @@
let shouldAutoScroll = true;
let lines: TerminalLine[] = [];
let currentSearchQuery = "";
let currentConversationId: string | null = null;
let isRestoringScroll = false;
searchQuery.subscribe((value) => {
currentSearchQuery = value;
@@ -19,14 +21,46 @@
lines = value;
});
claudeStore.activeConversationId.subscribe(async (newId) => {
if (!newId) return;
// Save current conversation's scroll position before switching
if (currentConversationId && currentConversationId !== newId && terminalElement) {
const position = shouldAutoScroll ? -1 : terminalElement.scrollTop;
claudeStore.saveScrollPosition(currentConversationId, position);
}
currentConversationId = newId;
// Restore scroll position for the new conversation after DOM updates
await tick();
if (terminalElement) {
const savedPosition = claudeStore.getScrollPosition(newId);
isRestoringScroll = true;
if (savedPosition === -1) {
// Auto-scroll to bottom
shouldAutoScroll = true;
terminalElement.scrollTop = terminalElement.scrollHeight;
} else {
// Restore to saved position
shouldAutoScroll = false;
terminalElement.scrollTop = savedPosition;
}
// Small delay to prevent the scroll handler from overriding our restore
setTimeout(() => {
isRestoringScroll = false;
}, 50);
}
});
function handleScroll() {
if (!terminalElement) return;
if (!terminalElement || isRestoringScroll) return;
const { scrollTop, scrollHeight, clientHeight } = terminalElement;
shouldAutoScroll = scrollHeight - scrollTop - clientHeight < 100;
}
afterUpdate(() => {
if (shouldAutoScroll && terminalElement) {
if (shouldAutoScroll && terminalElement && !isRestoringScroll) {
terminalElement.scrollTop = terminalElement.scrollHeight;
}
});
+2
View File
@@ -64,6 +64,8 @@ export const claudeStore = {
deleteConversation: conversationsStore.deleteConversation,
switchConversation: conversationsStore.switchConversation,
renameConversation: conversationsStore.renameConversation,
saveScrollPosition: conversationsStore.saveScrollPosition,
getScrollPosition: conversationsStore.getScrollPosition,
getGrantedTools: (): string[] => {
let tools: string[] = [];
+20
View File
@@ -21,6 +21,7 @@ export interface Conversation {
grantedTools: Set<string>;
pendingPermission: PermissionRequest | null;
pendingQuestion: UserQuestionEvent | null;
scrollPosition: number;
createdAt: Date;
lastActivityAt: Date;
}
@@ -55,6 +56,7 @@ function createConversationsStore() {
grantedTools: new Set(),
pendingPermission: null,
pendingQuestion: null,
scrollPosition: -1, // -1 means "scroll to bottom" (auto-scroll)
createdAt: new Date(),
lastActivityAt: new Date(),
};
@@ -106,6 +108,7 @@ function createConversationsStore() {
($conv) => $conv?.pendingPermission || null
);
const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null);
const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1);
return {
// Expose derived stores for compatibility
@@ -118,6 +121,7 @@ function createConversationsStore() {
isProcessing: { subscribe: isProcessing.subscribe },
grantedTools: { subscribe: grantedTools.subscribe },
pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe },
scrollPosition: { subscribe: scrollPosition.subscribe },
// New conversation-specific stores
conversations: { subscribe: conversations.subscribe },
@@ -325,6 +329,22 @@ function createConversationsStore() {
});
},
saveScrollPosition: (id: string, position: number) => {
conversations.update((convs) => {
const conv = convs.get(id);
if (conv) {
conv.scrollPosition = position;
}
return convs;
});
},
getScrollPosition: (id: string): number => {
const convs = get(conversations);
const conv = convs.get(id);
return conv?.scrollPosition ?? -1;
},
// Methods that operate on the active conversation
setSessionId: (id: string | null) => {
ensureInitialized();