generated from nhcarrigan/template
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:
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { claudeStore, type TerminalLine } from "$lib/stores/claude";
|
import { claudeStore, type TerminalLine } from "$lib/stores/claude";
|
||||||
import { afterUpdate } from "svelte";
|
import { afterUpdate, tick } from "svelte";
|
||||||
import ConversationTabs from "./ConversationTabs.svelte";
|
import ConversationTabs from "./ConversationTabs.svelte";
|
||||||
import Markdown from "./Markdown.svelte";
|
import Markdown from "./Markdown.svelte";
|
||||||
import HighlightedText from "./HighlightedText.svelte";
|
import HighlightedText from "./HighlightedText.svelte";
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
let shouldAutoScroll = true;
|
let shouldAutoScroll = true;
|
||||||
let lines: TerminalLine[] = [];
|
let lines: TerminalLine[] = [];
|
||||||
let currentSearchQuery = "";
|
let currentSearchQuery = "";
|
||||||
|
let currentConversationId: string | null = null;
|
||||||
|
let isRestoringScroll = false;
|
||||||
|
|
||||||
searchQuery.subscribe((value) => {
|
searchQuery.subscribe((value) => {
|
||||||
currentSearchQuery = value;
|
currentSearchQuery = value;
|
||||||
@@ -19,14 +21,46 @@
|
|||||||
lines = value;
|
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() {
|
function handleScroll() {
|
||||||
if (!terminalElement) return;
|
if (!terminalElement || isRestoringScroll) return;
|
||||||
const { scrollTop, scrollHeight, clientHeight } = terminalElement;
|
const { scrollTop, scrollHeight, clientHeight } = terminalElement;
|
||||||
shouldAutoScroll = scrollHeight - scrollTop - clientHeight < 100;
|
shouldAutoScroll = scrollHeight - scrollTop - clientHeight < 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
afterUpdate(() => {
|
afterUpdate(() => {
|
||||||
if (shouldAutoScroll && terminalElement) {
|
if (shouldAutoScroll && terminalElement && !isRestoringScroll) {
|
||||||
terminalElement.scrollTop = terminalElement.scrollHeight;
|
terminalElement.scrollTop = terminalElement.scrollHeight;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export const claudeStore = {
|
|||||||
deleteConversation: conversationsStore.deleteConversation,
|
deleteConversation: conversationsStore.deleteConversation,
|
||||||
switchConversation: conversationsStore.switchConversation,
|
switchConversation: conversationsStore.switchConversation,
|
||||||
renameConversation: conversationsStore.renameConversation,
|
renameConversation: conversationsStore.renameConversation,
|
||||||
|
saveScrollPosition: conversationsStore.saveScrollPosition,
|
||||||
|
getScrollPosition: conversationsStore.getScrollPosition,
|
||||||
|
|
||||||
getGrantedTools: (): string[] => {
|
getGrantedTools: (): string[] => {
|
||||||
let tools: string[] = [];
|
let tools: string[] = [];
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface Conversation {
|
|||||||
grantedTools: Set<string>;
|
grantedTools: Set<string>;
|
||||||
pendingPermission: PermissionRequest | null;
|
pendingPermission: PermissionRequest | null;
|
||||||
pendingQuestion: UserQuestionEvent | null;
|
pendingQuestion: UserQuestionEvent | null;
|
||||||
|
scrollPosition: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
lastActivityAt: Date;
|
lastActivityAt: Date;
|
||||||
}
|
}
|
||||||
@@ -55,6 +56,7 @@ function createConversationsStore() {
|
|||||||
grantedTools: new Set(),
|
grantedTools: new Set(),
|
||||||
pendingPermission: null,
|
pendingPermission: null,
|
||||||
pendingQuestion: null,
|
pendingQuestion: null,
|
||||||
|
scrollPosition: -1, // -1 means "scroll to bottom" (auto-scroll)
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
lastActivityAt: new Date(),
|
lastActivityAt: new Date(),
|
||||||
};
|
};
|
||||||
@@ -106,6 +108,7 @@ function createConversationsStore() {
|
|||||||
($conv) => $conv?.pendingPermission || null
|
($conv) => $conv?.pendingPermission || null
|
||||||
);
|
);
|
||||||
const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null);
|
const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null);
|
||||||
|
const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Expose derived stores for compatibility
|
// Expose derived stores for compatibility
|
||||||
@@ -118,6 +121,7 @@ function createConversationsStore() {
|
|||||||
isProcessing: { subscribe: isProcessing.subscribe },
|
isProcessing: { subscribe: isProcessing.subscribe },
|
||||||
grantedTools: { subscribe: grantedTools.subscribe },
|
grantedTools: { subscribe: grantedTools.subscribe },
|
||||||
pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe },
|
pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe },
|
||||||
|
scrollPosition: { subscribe: scrollPosition.subscribe },
|
||||||
|
|
||||||
// New conversation-specific stores
|
// New conversation-specific stores
|
||||||
conversations: { subscribe: conversations.subscribe },
|
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
|
// Methods that operate on the active conversation
|
||||||
setSessionId: (id: string | null) => {
|
setSessionId: (id: string | null) => {
|
||||||
ensureInitialized();
|
ensureInitialized();
|
||||||
|
|||||||
Reference in New Issue
Block a user