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 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-25 08:30:42 +01:00
co-authored by Claude Opus 4.5
parent fc97f6a41a
commit 9d42ff5bb6
3 changed files with 25 additions and 2 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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",
+23
View File
@@ -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;
});