webui: preserve chat scroll position when switching tabs
Fix scroll position preservation in SketchAppShell to maintain chat timeline
scroll position when navigating between tabs, preventing automatic scroll
to top when returning to the chat view.
Implementation Changes:
1. Scroll Position Storage:
- Add _chatScrollPosition state property to track chat scroll position
- Store scroll position when leaving chat view in toggleViewMode()
- Only store position if content is scrollable and user has actually scrolled
- Validate scrollHeight > clientHeight and scrollTop > 0 before storing
2. Scroll Position Restoration:
- Restore scroll position when returning to chat view
- Use requestAnimationFrame to ensure DOM is ready before restoration
- Add safety checks to verify view mode and container connection
- Validate container is still connected before applying scroll position
3. Smart Reset Logic:
- Reset stored scroll position when new messages arrive and user is near bottom
- Check if user is within 50px of bottom (isNearBottom) before resetting
- Allow timeline auto-scroll behavior for new messages when user is following
- Preserve position when user has scrolled up to read older messages
4. Defensive Programming:
- Add comprehensive validation for scroll container existence
- Check scrollHeight, clientHeight, and scrollTop before calculations
- Validate viewMode matches expected state during restoration
- Ensure container.isConnected before DOM manipulation
Technical Details:
- Uses existing scrollContainerRef for scroll container access
- Maintains compatibility with existing scroll behavior in sketch-timeline
- Preserves timeline auto-scroll for new messages when user is at bottom
- Only stores meaningful scroll positions (not zero or invalid values)
- Handles edge cases like rapid tab switching or container changes
User Experience:
- Chat tab now remembers scroll position when switching to diff/terminal tabs
- Reading older messages no longer interrupted by tab navigation
- New messages still auto-scroll when user is following conversation
- Smooth restoration without visible jumps or layout shifts
This resolves the issue where the chat timeline would always scroll to the
oldest message when navigating back from other tabs, significantly improving
the user experience when reviewing conversation history.
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s3a52c0413098ade3k
diff --git a/webui/src/web-components/sketch-app-shell.ts b/webui/src/web-components/sketch-app-shell.ts
index e2d27a2..d3bf00d 100644
--- a/webui/src/web-components/sketch-app-shell.ts
+++ b/webui/src/web-components/sketch-app-shell.ts
@@ -488,6 +488,10 @@
@state()
private _todoPanelVisible: boolean = false;
+ // Store scroll position for the chat view to preserve it when switching tabs
+ @state()
+ private _chatScrollPosition: number = 0;
+
// ResizeObserver for tracking chat input height changes
private chatInputResizeObserver: ResizeObserver | null = null;
@@ -754,6 +758,19 @@
// Don't do anything if the mode is already active
if (this.viewMode === mode) return;
+ // Store scroll position if we're leaving the chat view
+ if (this.viewMode === "chat" && this.scrollContainerRef.value) {
+ // Only store scroll position if we actually have meaningful content
+ const scrollTop = this.scrollContainerRef.value.scrollTop;
+ const scrollHeight = this.scrollContainerRef.value.scrollHeight;
+ const clientHeight = this.scrollContainerRef.value.clientHeight;
+
+ // Store position only if we have scrollable content and have actually scrolled
+ if (scrollHeight > clientHeight && scrollTop > 0) {
+ this._chatScrollPosition = scrollTop;
+ }
+ }
+
// Update the view mode
this.viewMode = mode;
@@ -788,6 +805,22 @@
switch (mode) {
case "chat":
chatView?.classList.add("view-active");
+ // Restore scroll position if we're switching back to chat
+ if (this.scrollContainerRef.value && this._chatScrollPosition > 0) {
+ // Use requestAnimationFrame to ensure DOM is ready
+ requestAnimationFrame(() => {
+ if (this.scrollContainerRef.value) {
+ // Double-check that we're still in chat mode and the container is available
+ if (
+ this.viewMode === "chat" &&
+ this.scrollContainerRef.value.isConnected
+ ) {
+ this.scrollContainerRef.value.scrollTop =
+ this._chatScrollPosition;
+ }
+ }
+ });
+ }
break;
case "diff2":
@@ -997,8 +1030,25 @@
}
// Update messages
+ const oldMessageCount = this.messages.length;
this.messages = aggregateAgentMessages(this.messages, newMessages);
+ // If new messages were added and we're in chat view, reset stored scroll position
+ // so the timeline can auto-scroll to bottom for new content
+ if (this.messages.length > oldMessageCount && this.viewMode === "chat") {
+ // Only reset if we were near the bottom (indicating user wants to follow new messages)
+ if (this.scrollContainerRef.value) {
+ const scrollTop = this.scrollContainerRef.value.scrollTop;
+ const scrollHeight = this.scrollContainerRef.value.scrollHeight;
+ const clientHeight = this.scrollContainerRef.value.clientHeight;
+ const isNearBottom = scrollTop + clientHeight >= scrollHeight - 50; // 50px tolerance
+
+ if (isNearBottom) {
+ this._chatScrollPosition = 0; // Reset stored position to allow auto-scroll
+ }
+ }
+ }
+
// Process new messages to find commit messages
// Update last commit info via container status component
if (this.containerStatusElement) {