mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 07:29:42 +02:00
chore: bump version to 0.1502
This commit is contained in:
+10
-102
@@ -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) || '';
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user