chore: bump version to 0.1502

This commit is contained in:
arkon
2026-02-13 20:25:56 +01:00
parent 3523bbe3af
commit d63fe2063f
8 changed files with 334 additions and 125 deletions
+3 -8
View File
@@ -883,15 +883,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
const textPart = input.replace(/\r/g, '').replace(/\n/g, '').trimEnd();
if (textPart && hasCarriageReturn) {
// Send text + Enter in a single command
// -l flag = literal text (no special key interpretation)
// 'Enter' after -l text = Enter key
// Send text + Enter in a single tmux invocation (one child process).
// tmux's \; separator chains commands within one process spawn.
await execAsync(
`tmux send-keys -t "${session.muxName}" -l ${shellescape(textPart)}`,
{ timeout: EXEC_TIMEOUT_MS }
);
await execAsync(
`tmux send-keys -t "${session.muxName}" Enter`,
`tmux send-keys -t "${session.muxName}" -l ${shellescape(textPart)} \\; send-keys -t "${session.muxName}" Enter`,
{ timeout: EXEC_TIMEOUT_MS }
);
} else if (textPart) {
+10 -102
View File
@@ -1242,15 +1242,10 @@ class ClaudemanApp {
// Sequential input send chain — ensures keystroke ordering across async fetches
this._inputSendChain = Promise.resolve();
// Local echo with output hold — shows typed chars instantly on high-latency connections.
// Instead of stripping echoed chars from Ink output (fragile), we simply hold ALL server
// output while typing and flush it when the user pauses. Ink's final redraw replaces
// any locally-echoed chars with the correct server state.
this._localEchoActive = false; // Whether we have unconfirmed locally-echoed chars
this._localEchoCount = 0; // Number of unconfirmed chars
this._localEchoHoldBuffer = []; // Server output held during typing
this._localEchoFlushTimer = null; // Timer to flush after typing pause
this._localEchoFlushDelay = 200; // ms after last keystroke before flushing server output
// No local echo — let PTY/Ink handle all character echoing.
// Local echo was removed because it's fundamentally incompatible with Ink's
// terminal management (caused "typing visible below" bug where locally-echoed
// chars leaked onto Ink's status bar rows).
// Accessibility: Focus trap for modals
this.activeFocusTrap = null;
@@ -1550,25 +1545,17 @@ class ClaudemanApp {
this.terminalResizeObserver = new ResizeObserver(throttledResize);
this.terminalResizeObserver.observe(container);
// Handle keyboard input with batching for rapid keystrokes
// Handle keyboard input — send to PTY immediately, no local echo.
// PTY/Ink handles all character echoing to avoid desync ("typing visible below" bug).
this._pendingInput = '';
this._inputFlushTimeout = null;
this._inputFlushDelay = 8; // Flush at ~120fps for snappy input
const flushInput = () => {
// Clear timeout immediately so new keystrokes can schedule fresh flushes.
// Previously this was set AFTER the await fetch(), which meant the stale
// timer ID blocked new keystrokes from scheduling — serializing all input
// behind the previous fetch's full network round-trip.
this._inputFlushTimeout = null;
if (this._pendingInput && this.activeSessionId) {
const input = this._pendingInput;
const sessionId = this.activeSessionId;
this._pendingInput = '';
// Fire-and-forget: don't block the flush cycle waiting for network.
// This prevents keystroke serialization behind fetch RTT.
this._sendInputAsync(sessionId, input);
}
};
@@ -1581,36 +1568,9 @@ class ClaudemanApp {
// Patterns: \x1b[?...c (DA1), \x1b[>...c (DA2), \x1b[...R (CPR), \x1b[...n (DSR)
if (/^\x1b\[[\?>=]?[\d;]*[cnR]$/.test(data)) return;
// Local echo: show printable chars instantly during idle state.
// Reduces perceived latency on remote connections (e.g., 100-300ms RTT).
// Server output is held while typing and flushed when user pauses.
const session = this.sessions.get(this.activeSessionId);
const isIdle = session && session.status === 'idle';
if (isIdle && data.length === 1) {
const code = data.charCodeAt(0);
if (code >= 32 && code < 127) {
// Printable ASCII — echo immediately
this.terminal.write(data);
this._localEchoActive = true;
this._localEchoCount++;
this._resetLocalEchoFlush();
} else if ((code === 0x7f || code === 0x08) && this._localEchoCount > 0) {
// Backspace — undo last local echo char
this.terminal.write('\b \b');
this._localEchoCount--;
if (this._localEchoCount === 0) this._localEchoActive = false;
this._resetLocalEchoFlush();
}
}
// Control chars (Enter, Ctrl+C) clear local echo immediately
if (data.charCodeAt(0) < 32 || data.length > 1) {
this._clearLocalEcho();
}
this._pendingInput += data;
// Flush immediately for control characters (Enter, Ctrl+C, etc.)
// Control chars (Enter, Ctrl+C, escape sequences) — flush immediately
if (data.charCodeAt(0) < 32 || data.length > 1) {
if (this._inputFlushTimeout) {
clearTimeout(this._inputFlushTimeout);
@@ -1620,9 +1580,10 @@ class ClaudemanApp {
return;
}
// Batch regular input at high frequency for responsiveness
// Regular chars — flush on next microtask to coalesce rapid keystrokes
// (e.g. paste) while keeping latency near-zero for single keystrokes.
if (!this._inputFlushTimeout) {
this._inputFlushTimeout = setTimeout(flushInput, this._inputFlushDelay);
this._inputFlushTimeout = setTimeout(flushInput, 0);
}
}
});
@@ -1754,15 +1715,6 @@ class ClaudemanApp {
}
batchTerminalWrite(data) {
// Local echo output hold: while user is actively typing (local echo active),
// buffer server output instead of writing it. This prevents Ink's intermediate
// redraws from overwriting locally-echoed characters. The buffer is flushed
// when typing pauses (200ms), giving the server time to catch up.
if (this._localEchoActive) {
this._localEchoHoldBuffer.push(data);
return;
}
// 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) {
@@ -2296,10 +2248,6 @@ class ClaudemanApp {
const session = this.sessions.get(data.id);
if (session) {
session.status = 'busy';
// Clear local echo — server is now generating output
if (data.id === this.activeSessionId) {
this._clearLocalEcho();
}
// Only clear tab alert if no pending hooks (permission_prompt, elicitation_dialog, etc.)
if (!this.pendingHooks.has(data.id)) {
this.tabAlerts.delete(data.id);
@@ -3044,46 +2992,6 @@ class ClaudemanApp {
});
}
/**
* Reset the local echo flush timer. Called on each keystroke.
* After the user stops typing for _localEchoFlushDelay ms, flushes
* held server output to the terminal.
*/
_resetLocalEchoFlush() {
if (this._localEchoFlushTimer) clearTimeout(this._localEchoFlushTimer);
this._localEchoFlushTimer = setTimeout(() => {
this._flushLocalEcho();
}, this._localEchoFlushDelay);
}
/**
* Flush held server output and clear local echo state.
* Called when typing pauses, session goes busy, or control char sent.
*/
_flushLocalEcho() {
if (this._localEchoFlushTimer) {
clearTimeout(this._localEchoFlushTimer);
this._localEchoFlushTimer = null;
}
this._localEchoActive = false;
this._localEchoCount = 0;
// Flush all held server output through the normal write path
if (this._localEchoHoldBuffer.length > 0) {
const held = this._localEchoHoldBuffer;
this._localEchoHoldBuffer = [];
for (const chunk of held) {
this.batchTerminalWrite(chunk);
}
}
}
/**
* Clear local echo state immediately (on Enter, Ctrl+C, session busy).
*/
_clearLocalEcho() {
this._flushLocalEcho();
}
_enqueueInput(sessionId, input) {
const existing = this._inputQueue.get(sessionId) || '';
+5 -3
View File
@@ -1669,9 +1669,11 @@ body {
flex: 1;
background: #0d0d0d;
overflow: hidden;
/* Performance: isolate layout and paint operations */
contain: strict;
will-change: contents;
position: relative;
/* Performance: isolate layout and paint operations.
Do NOT use contain:strict — size containment breaks xterm.js
FitAddon dimension calculations. */
contain: layout paint;
}
/* Tab switch: green-tinted overlay fades in over old content, fades out to reveal new.
Animated via Web Animations API (not CSS keyframes) for reliable GPU compositing.
+12 -10
View File
@@ -1526,21 +1526,23 @@ export class WebServer extends EventEmitter {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Input exceeds maximum length (${MAX_INPUT_LENGTH} bytes)`);
}
// Use writeViaScreen for programmatic input (more reliable for mux sessions)
let success = false;
// Write input to PTY. Direct write is synchronous; writeViaScreen
// (tmux send-keys) is fire-and-forget to avoid blocking the HTTP response.
if (useScreen) {
success = await session.writeViaScreen(inputStr);
if (!success) {
console.warn(`[Server] writeViaScreen failed for session ${id}, falling back to direct write`);
// Fallback to direct write if screen write fails
// Fire-and-forget: don't block HTTP response on tmux child process.
// Fallback to direct write on failure.
session.writeViaScreen(inputStr).then(ok => {
if (!ok) {
console.warn(`[Server] writeViaScreen failed for session ${id}, falling back to direct write`);
session.write(inputStr);
}
}).catch(() => {
session.write(inputStr);
success = true; // Direct write doesn't return status, assume success
}
});
} else {
session.write(inputStr);
success = true;
}
return { success };
return { success: true };
});
// Resize session terminal