diff --git a/CLAUDE.md b/CLAUDE.md index 2d62e9d4..cfb35319 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ When user says "COM": 1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`) 2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web` -**Version**: 0.1512 (must match `package.json` for npm publish) +**Version**: 0.1513 (must match `package.json` for npm publish) ## Project Overview diff --git a/package.json b/package.json index 02d31b80..8b993a82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1512", + "version": "0.1513", "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 c2e77bc6..2f52806f 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -342,13 +342,18 @@ const KeyboardHandler = { KeyboardAccessoryBar.show(); } - // Scroll active terminal to bottom so input line is visible - // Delay to let layout settle after keyboard resizes the viewport + // Refit terminal locally AND send resize to server so Claude Code (Ink) + // knows the actual terminal dimensions. Without this, Ink redraws at the + // old (larger) row count when the user types, causing content to scroll + // off the visible area with each keystroke. + // Note: the throttledResize handler still suppresses ongoing resize events + // while keyboard is up — this one-shot resize on open/close is sufficient. setTimeout(() => { if (typeof app !== 'undefined' && app.terminal) { - // Refit terminal to the now-smaller container, then scroll if (app.fitAddon) try { app.fitAddon.fit(); } catch {} app.terminal.scrollToBottom(); + // Send resize to server so PTY dimensions match xterm + this._sendTerminalResize(); } }, 150); }, @@ -362,14 +367,34 @@ const KeyboardHandler = { this.resetLayout(); - // Refit terminal to the restored (larger) container to prevent black gap + // Refit terminal and send resize to restore original dimensions setTimeout(() => { if (typeof app !== 'undefined' && app.fitAddon) { try { app.fitAddon.fit(); } catch {} + // Send resize to server to restore full terminal size + this._sendTerminalResize(); } }, 100); }, + /** Send current terminal dimensions to the server (one-shot, for keyboard open/close) */ + _sendTerminalResize() { + if (typeof app === 'undefined' || !app.activeSessionId || !app.fitAddon) return; + try { + const dims = app.fitAddon.proposeDimensions(); + if (dims) { + const cols = Math.max(dims.cols, 40); + const rows = Math.max(dims.rows, 10); + app._lastResizeDims = { cols, rows }; + fetch(`/api/sessions/${app.activeSessionId}/resize`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cols, rows }) + }); + } + } catch {} + }, + /** Check if element is an input that triggers keyboard (excludes terminal) */ isInputElement(el) { if (!el) return false; @@ -500,6 +525,16 @@ const KeyboardAccessoryBar = { this.element = document.createElement('div'); this.element.className = 'keyboard-accessory-bar'; this.element.innerHTML = ` + + @@ -526,8 +561,9 @@ const KeyboardAccessoryBar = { const action = btn.dataset.action; this.handleAction(action, btn); - // For double-tap buttons, refocus terminal so keyboard stays open during confirm state - if ((action === 'clear' || action === 'compact') && this._confirmAction) { + // Refocus terminal so keyboard stays open (tap blurs terminal → keyboard dismisses → toolbar shifts) + if ((action === 'scroll-up' || action === 'scroll-down') || + ((action === 'clear' || action === 'compact') && this._confirmAction)) { if (typeof app !== 'undefined' && app.terminal) { app.terminal.focus(); } @@ -549,6 +585,12 @@ const KeyboardAccessoryBar = { if (typeof app === 'undefined' || !app.activeSessionId) return; switch (action) { + case 'scroll-up': + this.sendKey('\x1b[A'); + break; + case 'scroll-down': + this.sendKey('\x1b[B'); + break; case 'init': this.sendCommand('/init'); break; @@ -613,6 +655,18 @@ const KeyboardAccessoryBar = { setTimeout(() => app.sendInput('\r'), 120); }, + /** Send a special key (arrow, escape, etc.) directly to the PTY. + * Bypasses tmux send-keys -l (literal mode) since escape sequences + * must be written raw to be interpreted as key presses by Ink. */ + sendKey(escapeSequence) { + if (!app.activeSessionId) return; + fetch(`/api/sessions/${app.activeSessionId}/input`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ input: escapeSequence }) + }); + }, + /** Read clipboard and send contents as input */ /** Show a paste overlay with a textarea for iOS compatibility */ pasteFromClipboard() { @@ -1590,7 +1644,11 @@ class ClaudemanApp { this._resizeTimeout = null; if (this.fitAddon) { this.fitAddon.fit(); - if (this.activeSessionId) { + // Skip server resize while mobile keyboard is visible — sending SIGWINCH + // causes Ink to re-render at the new row count, garbling terminal output. + // Local fit() still runs so xterm knows the viewport size for scrolling. + const keyboardUp = typeof KeyboardHandler !== 'undefined' && KeyboardHandler.keyboardVisible; + if (this.activeSessionId && !keyboardUp) { const dims = this.fitAddon.proposeDimensions(); // Enforce minimum dimensions to prevent layout issues const cols = dims ? Math.max(dims.cols, MIN_COLS) : MIN_COLS; diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css index 81607b08..9bc17e83 100644 --- a/src/web/public/mobile.css +++ b/src/web/public/mobile.css @@ -526,6 +526,17 @@ height: 14px; } + .accessory-btn-arrow { + padding: 6px 10px; + background: #1e3a5f; + border-color: rgba(59, 130, 246, 0.3); + color: #93c5fd; + } + + .accessory-btn-arrow:active { + background: #2563eb; + } + .accessory-btn-dismiss { margin-left: auto; padding: 6px 10px;