From 9d42ff5bb6ef5c49e98abad75e333c23b8eb5cbe Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 25 Jan 2026 08:30:42 +0100 Subject: [PATCH] fix: sticky scroll on tab switch - keep terminal at bottom after new output Add isTerminalAtBottom() helper and modify batchTerminalWrite() to implement sticky scroll behavior. When SSE terminal data arrives after tab switch, terminal now stays scrolled to Claude input if user was already at bottom. Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 2 +- package.json | 2 +- src/web/public/app.js | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 78aff0c2..22706ace 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs. -**Version**: 0.1349 +**Version**: 0.1350 **Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty diff --git a/package.json b/package.json index 033ab6b8..096cf3c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1349", + "version": "0.1350", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/src/web/public/app.js b/src/web/public/app.js index 84181a3e..600cd373 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -380,6 +380,7 @@ class ClaudemanApp { // Terminal write batching this.pendingWrites = ''; this.writeFrameScheduled = false; + this._wasAtBottomBeforeWrite = true; // Default to true for sticky scroll // Render debouncing this.renderSessionTabsTimeout = null; @@ -564,7 +565,25 @@ class ClaudemanApp { this.terminal.writeln(''); } + /** + * Check if terminal viewport is at or near the bottom. + * Used to implement "sticky scroll" behavior - keep user at bottom if they were there. + */ + isTerminalAtBottom() { + if (!this.terminal) return true; + const buffer = this.terminal.buffer.active; + // viewportY is the top line of the viewport, baseY is where scrollback starts + // If viewportY >= baseY, we're showing the latest content (at bottom) + // Allow 2 lines tolerance for edge cases + return buffer.viewportY >= buffer.baseY - 2; + } + batchTerminalWrite(data) { + // Check if at bottom BEFORE adding data (captures user's scroll position) + // Only update if not already scheduled (preserve the first check's result) + if (!this.writeFrameScheduled) { + this._wasAtBottomBeforeWrite = this.isTerminalAtBottom(); + } this.pendingWrites += data; if (!this.writeFrameScheduled) { this.writeFrameScheduled = true; @@ -572,6 +591,10 @@ class ClaudemanApp { if (this.pendingWrites && this.terminal) { this.terminal.write(this.pendingWrites); this.pendingWrites = ''; + // Sticky scroll: if user was at bottom, keep them there after new output + if (this._wasAtBottomBeforeWrite) { + this.terminal.scrollToBottom(); + } } this.writeFrameScheduled = false; });