chore: bump version to 0.1575

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-20 12:53:02 +01:00
co-authored by Claude Opus 4.6
parent 2528d98dce
commit 06c4e3933b
3 changed files with 116 additions and 44 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.1574 (must match `package.json` for npm publish) **Version**: 0.1575 (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.1574", "version": "0.1575",
"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",
+114 -42
View File
@@ -885,12 +885,15 @@ class LocalEchoOverlay {
} }
screen.appendChild(this.overlay); screen.appendChild(this.overlay);
this.pendingText = ''; this.pendingText = '';
this._cursorPos = 0; // Cursor position within pendingText (0 = before first char)
this._mutationId = 0; // Increments on every content/cursor change, used for renderKey dedup
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 {
const saved = localStorage.getItem(this._storageKey); const saved = localStorage.getItem(this._storageKey);
if (saved) { if (saved) {
this.pendingText = saved; this.pendingText = saved;
this._cursorPos = saved.length;
this._render(); this._render();
} }
} catch {} } catch {}
@@ -925,14 +928,20 @@ class LocalEchoOverlay {
} }
addChar(char) { addChar(char) {
this.pendingText += char; // Insert at cursor position, not always at end
this.pendingText = this.pendingText.slice(0, this._cursorPos) + char + this.pendingText.slice(this._cursorPos);
this._cursorPos++;
this._mutationId++;
this._persist(); this._persist();
this._render(); this._render();
} }
removeChar() { removeChar() {
if (this.pendingText.length > 0) { // Delete char before cursor position (like real backspace)
this.pendingText = this.pendingText.slice(0, -1); if (this._cursorPos > 0) {
this.pendingText = this.pendingText.slice(0, this._cursorPos - 1) + this.pendingText.slice(this._cursorPos);
this._cursorPos--;
this._mutationId++;
this._persist(); this._persist();
if (this.pendingText.length > 0) { if (this.pendingText.length > 0) {
this._render(); this._render();
@@ -942,15 +951,34 @@ class LocalEchoOverlay {
} }
} }
moveCursorLeft() {
if (this._cursorPos > 0) {
this._cursorPos--;
this._mutationId++;
this._render();
}
}
moveCursorRight() {
if (this._cursorPos < this.pendingText.length) {
this._cursorPos++;
this._mutationId++;
this._render();
}
}
appendText(text) { appendText(text) {
if (!text) return; if (!text) return;
this.pendingText += text; this.pendingText = this.pendingText.slice(0, this._cursorPos) + text + this.pendingText.slice(this._cursorPos);
this._cursorPos += text.length;
this._mutationId++;
this._persist(); this._persist();
this._render(); this._render();
} }
clear() { clear() {
this.pendingText = ''; this.pendingText = '';
this._cursorPos = 0;
this._persist(); this._persist();
this._lastRenderKey = ''; this._lastRenderKey = '';
this._lastPromptPos = null; this._lastPromptPos = null;
@@ -1001,8 +1029,10 @@ class LocalEchoOverlay {
const startCol = Math.min(activePrompt.col + 2, totalCols - 1); const startCol = Math.min(activePrompt.col + 2, 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}`; // _mutationId increments on every content/cursor change; prompt position
// is included so moves from Ink redraws also trigger re-render.
const renderKey = `${this._mutationId}:${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;
@@ -1036,15 +1066,24 @@ class LocalEchoOverlay {
this.overlay.appendChild(lineEl); this.overlay.appendChild(lineEl);
} }
// Block cursor at end of last line // Block cursor at cursor position (not always end of text)
const lastLine = lines[lines.length - 1]; // Map _cursorPos to visual line/col position
const lastLineLeft = lines.length === 1 ? startCol : 0; let cursorVisualPos = this._cursorPos;
const cursorCol = lastLineLeft + lastLine.length; let cursorLine = 0;
if (cursorVisualPos <= firstLineCols) {
cursorLine = 0;
} else {
cursorVisualPos -= firstLineCols;
cursorLine = 1 + Math.floor(cursorVisualPos / totalCols);
cursorVisualPos = cursorVisualPos % totalCols;
}
const cursorLineLeft = cursorLine === 0 ? startCol : 0;
const cursorCol = cursorLineLeft + cursorVisualPos;
if (cursorCol < totalCols) { if (cursorCol < totalCols) {
const cursor = document.createElement('span'); const cursor = document.createElement('span');
cursor.style.cssText = 'position:absolute;display:inline-block'; cursor.style.cssText = 'position:absolute;display:inline-block';
cursor.style.left = (cursorCol * cellW) + 'px'; cursor.style.left = (cursorCol * cellW) + 'px';
cursor.style.top = ((lines.length - 1) * cellH) + 'px'; cursor.style.top = (cursorLine * cellH) + 'px';
cursor.style.width = cellW + 'px'; cursor.style.width = cellW + 'px';
cursor.style.height = cellH + 'px'; cursor.style.height = cellH + 'px';
cursor.style.backgroundColor = this.terminal.options?.theme?.cursor || '#e0e0e0'; cursor.style.backgroundColor = this.terminal.options?.theme?.cursor || '#e0e0e0';
@@ -1689,8 +1728,13 @@ class ClaudemanApp {
this._inputQueueMaxBytes = 64 * 1024; // 64KB cap per session this._inputQueueMaxBytes = 64 * 1024; // 64KB cap per session
this._connectionStatus = 'connected'; this._connectionStatus = 'connected';
// Sequential input send chain — ensures keystroke ordering across async fetches // Serialized input sender — at most one fetch in-flight at a time.
this._inputSendChain = Promise.resolve(); // New input arriving while a send is in-flight merges into a single
// pending buffer (never a growing queue). Guarantees ordering without
// blocking the UI — typing and overlay rendering remain fully instant.
this._inputSendActive = false; // true while a fetch is in-flight
this._inputSendPending = ''; // chars waiting behind the in-flight request
this._inputSendPendingSession = null;
// Local echo overlay — DOM overlay positioned at the visible ❯ prompt // Local echo overlay — DOM overlay positioned at the visible ❯ prompt
// (not at buffer.cursorY, which reflects Ink's internal cursor position) // (not at buffer.cursorY, which reflects Ink's internal cursor position)
@@ -2084,7 +2128,7 @@ class ClaudemanApp {
// (50ms debounce) so Tab completion and tab-switching work. // (50ms debounce) so Tab completion and tab-switching work.
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: delete char at cursor position, send \x7f to PTY in background
this._localEchoOverlay?.removeChar(); this._localEchoOverlay?.removeChar();
this._localEchoBgBuffer += '\x7f'; this._localEchoBgBuffer += '\x7f';
scheduleBgFlush(); scheduleBgFlush();
@@ -2093,6 +2137,9 @@ class ClaudemanApp {
if (/^[\r\n]+$/.test(data)) { if (/^[\r\n]+$/.test(data)) {
// Enter: drain any background-buffered chars, then send \r after 120ms. // Enter: drain any background-buffered chars, then send \r after 120ms.
// PTY already has the text from background sends; drain catches any remainder. // PTY already has the text from background sends; drain catches any remainder.
// Capture sessionId NOW — the setTimeout fires 120ms later and activeSessionId
// could change if the user switches tabs during that window.
const enterSessionId = this.activeSessionId;
this._localEchoOverlay?.clear(); this._localEchoOverlay?.clear();
if (this._inputFlushTimeout) { if (this._inputFlushTimeout) {
clearTimeout(this._inputFlushTimeout); clearTimeout(this._inputFlushTimeout);
@@ -2104,8 +2151,9 @@ class ClaudemanApp {
flushInput(); flushInput();
} }
setTimeout(() => { setTimeout(() => {
this._pendingInput += '\r'; if (enterSessionId) {
flushInput(); this._sendInputAsync(enterSessionId, '\r');
}
}, 120); }, 120);
return; return;
} }
@@ -2122,8 +2170,21 @@ class ClaudemanApp {
flushInput(); flushInput();
return; return;
} }
// Arrow left/right: move cursor within overlay text, send to PTY in background
if (data === '\x1b[D') {
this._localEchoOverlay?.moveCursorLeft();
this._localEchoBgBuffer += data;
scheduleBgFlush();
return;
}
if (data === '\x1b[C') {
this._localEchoOverlay?.moveCursorRight();
this._localEchoBgBuffer += data;
scheduleBgFlush();
return;
}
if (data.charCodeAt(0) < 32) { if (data.charCodeAt(0) < 32) {
// Control chars (Ctrl+C, escape sequences): clear overlay, drain bg, send immediately // Other control chars (Ctrl+C, up/down arrows, etc.): clear overlay, drain bg, send immediately
this._localEchoOverlay?.clear(); this._localEchoOverlay?.clear();
const remainder = drainBgBuffer(); const remainder = drainBgBuffer();
if (remainder) { if (remainder) {
@@ -2138,7 +2199,7 @@ 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: insert at cursor position + queue for background send to PTY
this._localEchoOverlay?.addChar(data); this._localEchoOverlay?.addChar(data);
this._localEchoBgBuffer += data; this._localEchoBgBuffer += data;
scheduleBgFlush(); scheduleBgFlush();
@@ -3719,32 +3780,41 @@ class ClaudemanApp {
return; return;
} }
// Chain on dispatch only — wait for the previous request to be sent before // Merge into the pending buffer. If a request is already in-flight,
// dispatching the next one (preserves keystroke ordering), but don't wait // this just accumulates — the send loop will pick it up when the
// for the server's response. The server handles writeViaMux as // current request completes. No growing queue, no blocked typing.
// fire-and-forget anyway, so the HTTP response carries no useful data this._inputSendPending += input;
// beyond success/failure for retry purposes. this._inputSendPendingSession = sessionId;
this._inputSendChain = this._inputSendChain.then(() => {
const fetchPromise = fetch(`/api/sessions/${sessionId}/input`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input }),
keepalive: input.length < 65536,
});
// Handle response asynchronously — don't block next keystroke on response // If already sending, the loop will drain the pending buffer — done.
fetchPromise.then(resp => { if (this._inputSendActive) return;
if (!resp.ok) {
this._enqueueInput(sessionId, input); // Start the send loop. Runs entirely in the background; callers
} else { // (onData, bgBuffer timer, Enter handler) return immediately.
this.clearPendingHooks(sessionId); this._inputSendActive = true;
(async () => {
while (this._inputSendPending) {
const batch = this._inputSendPending;
const batchSession = this._inputSendPendingSession;
this._inputSendPending = '';
try {
const resp = await fetch(`/api/sessions/${batchSession}/input`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: batch }),
keepalive: batch.length < 65536,
});
if (!resp.ok) {
this._enqueueInput(batchSession, batch);
} else {
this.clearPendingHooks(batchSession);
}
} catch {
this._enqueueInput(batchSession, batch);
} }
}).catch(() => { }
this._enqueueInput(sessionId, input); this._inputSendActive = false;
}); })();
// Return immediately after fetch is dispatched (don't await response)
});
} }
@@ -4050,6 +4120,7 @@ class ClaudemanApp {
// selectSession loads the terminal buffer, so we schedule rerender after it settles. // selectSession loads the terminal buffer, so we schedule rerender after it settles.
if (savedEchoText && this._localEchoOverlay) { if (savedEchoText && this._localEchoOverlay) {
this._localEchoOverlay.pendingText = savedEchoText; this._localEchoOverlay.pendingText = savedEchoText;
this._localEchoOverlay._cursorPos = savedEchoText.length;
this._localEchoOverlay._persist(); this._localEchoOverlay._persist();
// Delay rerender until buffer load completes and prompt is visible // Delay rerender until buffer load completes and prompt is visible
setTimeout(() => this._localEchoOverlay?.rerender(), 500); setTimeout(() => this._localEchoOverlay?.rerender(), 500);
@@ -4718,6 +4789,7 @@ class ClaudemanApp {
const savedEcho = this.localEchoTextCache.get(sessionId); const savedEcho = this.localEchoTextCache.get(sessionId);
if (savedEcho && this._localEchoOverlay) { if (savedEcho && this._localEchoOverlay) {
this._localEchoOverlay.pendingText = savedEcho; this._localEchoOverlay.pendingText = savedEcho;
this._localEchoOverlay._cursorPos = savedEcho.length;
this._localEchoOverlay._persist(); this._localEchoOverlay._persist();
for (const delay of [150, 500, 1000]) { for (const delay of [150, 500, 1000]) {
setTimeout(() => { setTimeout(() => {