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; });