chore: bump version to 0.1586

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-21 14:00:16 +01:00
co-authored by Claude Opus 4.6
parent 0fe6251424
commit 7fbc60db9b
3 changed files with 74 additions and 103 deletions
+1 -1
View File
@@ -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`) 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` 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.1585 (must match `package.json` for npm publish) **Version**: 0.1586 (must match `package.json` for npm publish)
## Project Overview ## Project Overview
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "claudeman", "name": "claudeman",
"version": "0.1585", "version": "0.1586",
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
+72 -101
View File
@@ -894,6 +894,7 @@ class LocalEchoOverlay {
} }
screen.appendChild(this.overlay); screen.appendChild(this.overlay);
this.pendingText = ''; this.pendingText = '';
this._flushedOffset = 0; // chars flushed to PTY during tab switch (echo may not have arrived yet)
this._storageKey = 'claudeman_local_echo_pending'; this._storageKey = 'claudeman_local_echo_pending';
// Restore unsent input from previous session (survives reload/disconnect) // Restore unsent input from previous session (survives reload/disconnect)
try { try {
@@ -960,6 +961,7 @@ class LocalEchoOverlay {
clear() { clear() {
this.pendingText = ''; this.pendingText = '';
this._flushedOffset = 0;
this._persist(); this._persist();
this._lastRenderKey = ''; this._lastRenderKey = '';
this._lastPromptPos = null; this._lastPromptPos = null;
@@ -1015,12 +1017,29 @@ class LocalEchoOverlay {
const cellW = dims.css.cell.width; const cellW = dims.css.cell.width;
const cellH = dims.css.cell.height; const cellH = dims.css.cell.height;
const totalCols = this.terminal.cols; const totalCols = this.terminal.cols;
// Position right after "❯ " (prompt col + 2) // Position after existing text on the prompt line. Normally this is
const startCol = Math.min(activePrompt.col + 2, totalCols - 1); // just "❯ " (col+2), but after a tab switch the flushed text is echoed
// by the PTY and sits in the terminal buffer — the overlay must start
// AFTER it to avoid covering it with its opaque background.
// _flushedOffset provides immediate offset even before echo arrives.
let startCol = activePrompt.col + 2 + (this._flushedOffset || 0);
try {
const buf = this.terminal.buffer.active;
const promptLine = buf.getLine(buf.viewportY + activePrompt.row);
if (promptLine) {
const existingEnd = promptLine.translateToString(true).trimEnd().length;
if (existingEnd > startCol) startCol = existingEnd;
// Once echo has arrived in the buffer, we can drop the offset
if (this._flushedOffset && existingEnd >= activePrompt.col + 2 + this._flushedOffset) {
this._flushedOffset = 0;
}
}
} catch {}
startCol = Math.min(startCol, totalCols - 1);
const firstLineCols = Math.max(1, totalCols - startCol); const firstLineCols = Math.max(1, totalCols - startCol);
// Skip redundant re-renders (e.g. from flushPendingWrites at 60fps) // Skip redundant re-renders (e.g. from flushPendingWrites at 60fps)
const renderKey = `${this.pendingText.length}:${activePrompt.row}:${activePrompt.col}:${totalCols}`; const renderKey = `${this.pendingText.length}:${startCol}:${activePrompt.row}:${activePrompt.col}:${totalCols}`;
if (renderKey === this._lastRenderKey && this.overlay.style.display !== 'none') return; if (renderKey === this._lastRenderKey && this.overlay.style.display !== 'none') return;
this._lastRenderKey = renderKey; this._lastRenderKey = renderKey;
@@ -2067,33 +2086,10 @@ class ClaudemanApp {
} }
}; };
// Background keystroke forwarding for local echo mode. // Local echo mode: buffer keystrokes locally (shown in overlay) and only
// Sends keystrokes to the PTY in the background (50ms debounce) so Tab // send to PTY on Enter. Avoids out-of-order delivery on high-latency
// completion works and tab-switching doesn't lose PTY state. // mobile connections. The overlay + localStorage persistence ensure input
this._localEchoBgBuffer = ''; // survives tab switches and reconnects.
this._localEchoBgTimer = null;
const drainBgBuffer = () => {
if (this._localEchoBgTimer) {
clearTimeout(this._localEchoBgTimer);
this._localEchoBgTimer = null;
}
const buf = this._localEchoBgBuffer;
this._localEchoBgBuffer = '';
return buf;
};
const scheduleBgFlush = () => {
if (this._localEchoBgTimer) return;
this._localEchoBgTimer = setTimeout(() => {
this._localEchoBgTimer = null;
const buf = this._localEchoBgBuffer;
this._localEchoBgBuffer = '';
if (buf && this.activeSessionId) {
this._sendInputAsync(this.activeSessionId, buf);
}
}, 50);
};
this.terminal.onData((data) => { this.terminal.onData((data) => {
if (this.activeSessionId) { if (this.activeSessionId) {
@@ -2104,55 +2100,47 @@ class ClaudemanApp {
if (/^\x1b\[[\?>=]?[\d;]*[cnR]$/.test(data)) return; if (/^\x1b\[[\?>=]?[\d;]*[cnR]$/.test(data)) return;
// ── Local Echo Mode ── // ── Local Echo Mode ──
// When enabled, keystrokes are captured locally in the overlay for // When enabled, keystrokes are buffered locally in the overlay for
// instant visual feedback AND sent to the PTY in the background // instant visual feedback. Nothing is sent to the PTY until Enter
// (50ms debounce) so Tab completion and tab-switching work. // (or a control char) is pressed — avoids out-of-order char delivery.
if (this._localEchoEnabled) { if (this._localEchoEnabled) {
if (data === '\x7f') { if (data === '\x7f') {
// Backspace: remove last char from overlay, send \x7f to PTY in background // Backspace: remove last char from overlay only (nothing sent to PTY yet)
this._localEchoOverlay?.removeChar(); this._localEchoOverlay?.removeChar();
this._localEchoBgBuffer += '\x7f';
scheduleBgFlush();
return; return;
} }
if (/^[\r\n]+$/.test(data)) { if (/^[\r\n]+$/.test(data)) {
// Enter: drain any background-buffered chars, then send \r after 120ms. // Enter: send full buffered text + \r to PTY in one shot
// PTY already has the text from background sends; drain catches any remainder. const text = this._localEchoOverlay?.pendingText || '';
this._localEchoOverlay?.clear(); this._localEchoOverlay?.clear();
// Clear flushed offset — Enter commits all text
this._flushedOffsets?.delete(this.activeSessionId);
if (this._inputFlushTimeout) { if (this._inputFlushTimeout) {
clearTimeout(this._inputFlushTimeout); clearTimeout(this._inputFlushTimeout);
this._inputFlushTimeout = null; this._inputFlushTimeout = null;
} }
const remainder = drainBgBuffer(); if (text) {
if (remainder) { this._pendingInput += text;
this._pendingInput += remainder;
flushInput(); flushInput();
} }
// Send \r after a short delay so text arrives first
setTimeout(() => { setTimeout(() => {
this._pendingInput += '\r'; this._pendingInput += '\r';
flushInput(); flushInput();
}, 120); }, 80);
return; return;
} }
if (data.length > 1 && data.charCodeAt(0) >= 32) { if (data.length > 1 && data.charCodeAt(0) >= 32) {
// Paste: append to overlay + send to PTY immediately // Paste: append to overlay only (sent on Enter)
this._localEchoOverlay?.appendText(data); this._localEchoOverlay?.appendText(data);
const remainder = drainBgBuffer();
if (remainder) this._pendingInput += remainder;
this._pendingInput += data;
if (this._inputFlushTimeout) {
clearTimeout(this._inputFlushTimeout);
this._inputFlushTimeout = null;
}
flushInput();
return; return;
} }
if (data.charCodeAt(0) < 32) { if (data.charCodeAt(0) < 32) {
// Control chars (Ctrl+C, escape sequences): clear overlay, drain bg, send immediately // Control chars (Ctrl+C, escape sequences): send buffered text + control char immediately
const text = this._localEchoOverlay?.pendingText || '';
this._localEchoOverlay?.clear(); this._localEchoOverlay?.clear();
const remainder = drainBgBuffer(); if (text) {
if (remainder) { this._pendingInput += text;
this._pendingInput += remainder;
} }
this._pendingInput += data; this._pendingInput += data;
if (this._inputFlushTimeout) { if (this._inputFlushTimeout) {
@@ -2163,10 +2151,8 @@ class ClaudemanApp {
return; return;
} }
if (data.length === 1 && data.charCodeAt(0) >= 32) { if (data.length === 1 && data.charCodeAt(0) >= 32) {
// Printable char: add to overlay + queue for background send to PTY // Printable char: add to overlay only (sent on Enter)
this._localEchoOverlay?.addChar(data); this._localEchoOverlay?.addChar(data);
this._localEchoBgBuffer += data;
scheduleBgFlush();
return; return;
} }
} }
@@ -2469,16 +2455,6 @@ class ClaudemanApp {
const echoEnabled = settings.localEchoEnabled ?? MobileDetection.isTouchDevice(); const echoEnabled = settings.localEchoEnabled ?? MobileDetection.isTouchDevice();
const shouldEnable = !!(echoEnabled && session); const shouldEnable = !!(echoEnabled && session);
if (this._localEchoEnabled && !shouldEnable) { if (this._localEchoEnabled && !shouldEnable) {
// Flush background-buffered keystrokes before disabling
if (this.activeSessionId && this._localEchoBgBuffer) {
const remainder = this._localEchoBgBuffer;
if (this._localEchoBgTimer) {
clearTimeout(this._localEchoBgTimer);
this._localEchoBgTimer = null;
}
this._localEchoBgBuffer = '';
this._sendInputAsync(this.activeSessionId, remainder);
}
this._localEchoOverlay?.clear(); this._localEchoOverlay?.clear();
} }
this._localEchoEnabled = shouldEnable; this._localEchoEnabled = shouldEnable;
@@ -4652,23 +4628,33 @@ class ClaudemanApp {
this.writeFrameScheduled = false; this.writeFrameScheduled = false;
this._isLoadingBuffer = false; this._isLoadingBuffer = false;
this._loadBufferQueue = null; this._loadBufferQueue = null;
// Flush any background-buffered keystrokes to the outgoing session's PTY // End any in-flight IME composition.
if (this.activeSessionId && this._localEchoBgBuffer) { // iOS Safari keeps autocorrect composing; switching tabs without ending it
const remainder = this._localEchoBgBuffer; // leaves xterm's _compositionHelper._isComposing stuck true, which blocks
if (this._localEchoBgTimer) { // keyboard input when the user returns to this tab.
clearTimeout(this._localEchoBgTimer); try {
this._localEchoBgTimer = null; const ch = this.terminal?._core?._compositionHelper;
if (ch?._isComposing) {
ch._isComposing = false;
// Also fire compositionend on the textarea so any other listeners reset
const ta = this.terminal?.element?.querySelector('.xterm-helper-textarea');
if (ta) ta.dispatchEvent(new CompositionEvent('compositionend', { data: '' }));
} }
this._localEchoBgBuffer = ''; } catch {}
this._sendInputAsync(this.activeSessionId, remainder);
} // Flush local echo text to PTY before switching tabs.
// Save local echo text for outgoing session before clear destroys it // Send as a single batch (no Enter) so it lands in the session's readline
// input buffer — avoids "old text resent on Enter" and overlay render bugs.
// Track flushed length so _render() offsets the overlay correctly even before
// the PTY echo arrives in the terminal buffer.
if (this.activeSessionId) { if (this.activeSessionId) {
const echoText = this._localEchoOverlay?.pendingText || ''; const echoText = this._localEchoOverlay?.pendingText || '';
if (echoText) { if (echoText) {
this.localEchoTextCache.set(this.activeSessionId, echoText); this._sendInputAsync(this.activeSessionId, echoText);
} else { // Store flushed length PER SESSION so it's available when switching back
this.localEchoTextCache.delete(this.activeSessionId); if (!this._flushedOffsets) this._flushedOffsets = new Map();
const prev = this._flushedOffsets.get(this.activeSessionId) || 0;
this._flushedOffsets.set(this.activeSessionId, prev + echoText.length);
} }
} }
this._localEchoOverlay?.clear(); this._localEchoOverlay?.clear();
@@ -4755,20 +4741,10 @@ class ClaudemanApp {
this.terminal.reset(); this.terminal.reset();
} }
// Restore local echo text for incoming session (saved during previous tab switch). // Restore flushed offset for this session so the overlay positions correctly
// Retry rerender at increasing delays — terminal render service may not be ready // even before the PTY echo arrives in the terminal buffer.
// immediately after buffer load (especially on slower mobile devices). if (this._flushedOffsets?.has(sessionId) && this._localEchoOverlay) {
const savedEcho = this.localEchoTextCache.get(sessionId); this._localEchoOverlay._flushedOffset = this._flushedOffsets.get(sessionId);
if (savedEcho && this._localEchoOverlay) {
this._localEchoOverlay.pendingText = savedEcho;
this._localEchoOverlay._persist();
for (const delay of [150, 500, 1000]) {
setTimeout(() => {
if (this.activeSessionId === sessionId && this._localEchoOverlay?.pendingText) {
this._localEchoOverlay.rerender();
}
}, delay);
}
} }
// Fire-and-forget resize — don't await to avoid blocking UI. // Fire-and-forget resize — don't await to avoid blocking UI.
@@ -4852,12 +4828,7 @@ class ClaudemanApp {
this.terminalBuffers.delete(sessionId); this.terminalBuffers.delete(sessionId);
this.terminalBufferCache.delete(sessionId); this.terminalBufferCache.delete(sessionId);
this.localEchoTextCache.delete(sessionId); this.localEchoTextCache.delete(sessionId);
// Cancel orphaned background echo timer (discard — session is gone) this._flushedOffsets?.delete(sessionId);
if (this._localEchoBgTimer) {
clearTimeout(this._localEchoBgTimer);
this._localEchoBgTimer = null;
this._localEchoBgBuffer = '';
}
this._inputQueue.delete(sessionId); this._inputQueue.delete(sessionId);
this.ralphStates.delete(sessionId); this.ralphStates.delete(sessionId);
this.ralphClosedSessions.delete(sessionId); this.ralphClosedSessions.delete(sessionId);