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;