mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
chore: bump version to 0.1502
This commit is contained in:
@@ -25,7 +25,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.1501 (must match `package.json` for npm publish)
|
||||
**Version**: 0.1502 (must match `package.json` for npm publish)
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1501",
|
||||
"version": "0.1502",
|
||||
"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",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Take screenshots of the typing-visible-below bug.
|
||||
* Intercepts input to prevent damage to live sessions.
|
||||
*/
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true, args: ['--window-size=1280,800'] });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
// Intercept input to prevent sending to real sessions
|
||||
await page.route('**/api/sessions/*/input', route => route.fulfill({
|
||||
status: 200, contentType: 'application/json', body: '{"success":true}'
|
||||
}));
|
||||
|
||||
console.log('Opening Claudeman...');
|
||||
await page.goto('http://localhost:3000');
|
||||
await page.waitForSelector('.xterm-screen', { timeout: 10000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Screenshot 1: Before typing
|
||||
await page.screenshot({ path: '/tmp/before-typing.png' });
|
||||
console.log('Screenshot 1: /tmp/before-typing.png');
|
||||
|
||||
// Click on the terminal to focus it
|
||||
await page.click('.xterm-screen');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Type a short string
|
||||
await page.keyboard.type('hello world this is a test', { delay: 30 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: '/tmp/after-short-typing.png' });
|
||||
console.log('Screenshot 2: /tmp/after-short-typing.png');
|
||||
|
||||
// Type a LONG string that approaches the right edge
|
||||
await page.keyboard.type(' and now I am typing a very long line that should approach the right edge of the terminal to test whether text wraps incorrectly below the status bar area which would be the typing visible below bug', { delay: 15 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: '/tmp/after-long-typing.png' });
|
||||
console.log('Screenshot 3: /tmp/after-long-typing.png');
|
||||
|
||||
// Wait for flush + SIGWINCH to settle
|
||||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: '/tmp/after-settle.png' });
|
||||
console.log('Screenshot 4: /tmp/after-settle.png (after 1s settle)');
|
||||
|
||||
// Try typing even MORE to really push past the edge
|
||||
await page.keyboard.type(' AAAA BBBB CCCC DDDD EEEE FFFF GGGG HHHH IIII JJJJ KKKK LLLL MMMM NNNN OOOO PPPP', { delay: 10 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: '/tmp/after-overflow.png' });
|
||||
console.log('Screenshot 5: /tmp/after-overflow.png (past right edge)');
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: '/tmp/after-overflow-settle.png' });
|
||||
console.log('Screenshot 6: /tmp/after-overflow-settle.png (settled)');
|
||||
|
||||
await browser.close();
|
||||
console.log('Done');
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err); process.exit(1); });
|
||||
+3
-8
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Input echo test: Verifies that typing in the web UI doesn't produce
|
||||
* "visible below" artifacts (text mirrored onto Ink's status bar rows).
|
||||
*
|
||||
* Test approach:
|
||||
* 1. Creates a SHELL session (bash, not Claude) to avoid Ink complexity
|
||||
* 2. Types various input patterns and captures terminal state
|
||||
* 3. Verifies echo appears at correct cursor position, not duplicated
|
||||
*
|
||||
* Usage: node test/input-echo-test.mjs [--live]
|
||||
* --live Use existing sessions instead of creating new ones
|
||||
*/
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const PORT = 3000;
|
||||
const BASE = `http://localhost:${PORT}`;
|
||||
|
||||
async function createShellSession() {
|
||||
const resp = await fetch(`${BASE}/api/sessions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mode: 'shell', name: 'input-test' })
|
||||
});
|
||||
if (!resp.ok) throw new Error(`Failed to create session: ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
return data.session?.id || data.id;
|
||||
}
|
||||
|
||||
async function deleteSession(id) {
|
||||
await fetch(`${BASE}/api/sessions/${id}`, { method: 'DELETE' }).catch(() => {});
|
||||
}
|
||||
|
||||
async function waitForSessionReady(sessionId, timeoutMs = 10000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const resp = await fetch(`${BASE}/api/sessions/${sessionId}`);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
if (data.status === 'idle' || data.status === 'busy') return true;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getTerminalContent(page) {
|
||||
return page.evaluate(() => {
|
||||
const app = window.app;
|
||||
if (!app?.terminal) return null;
|
||||
const buf = app.terminal.buffer.active;
|
||||
const lines = [];
|
||||
for (let i = 0; i <= buf.baseY + buf.viewportY + app.terminal.rows; i++) {
|
||||
const line = buf.getLine(i);
|
||||
if (line) lines.push(line.translateToString(true));
|
||||
}
|
||||
return {
|
||||
lines,
|
||||
cursorX: buf.cursorX,
|
||||
cursorY: buf.cursorY,
|
||||
rows: app.terminal.rows,
|
||||
cols: app.terminal.cols,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const isLive = process.argv.includes('--live');
|
||||
let sessionId = null;
|
||||
|
||||
console.log('=== Input Echo Test ===\n');
|
||||
|
||||
// Create a shell session for testing
|
||||
if (!isLive) {
|
||||
console.log('Creating shell session...');
|
||||
sessionId = await createShellSession();
|
||||
console.log(`Session created: ${sessionId}`);
|
||||
|
||||
const ready = await waitForSessionReady(sessionId);
|
||||
if (!ready) {
|
||||
console.error('Session failed to become ready');
|
||||
await deleteSession(sessionId);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Session ready\n');
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--window-size=1280,800']
|
||||
});
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
// Collect console output for debugging
|
||||
const consoleLogs = [];
|
||||
page.on('console', msg => consoleLogs.push(`[${msg.type()}] ${msg.text()}`));
|
||||
|
||||
try {
|
||||
console.log('Opening Claudeman...');
|
||||
await page.goto(BASE);
|
||||
await page.waitForSelector('.xterm-screen', { timeout: 10000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Switch to our test session if not live
|
||||
if (sessionId) {
|
||||
// Click the test session tab
|
||||
const tabClicked = await page.evaluate((id) => {
|
||||
const tabs = document.querySelectorAll('.session-tab');
|
||||
for (const tab of tabs) {
|
||||
if (tab.dataset.sessionId === id) {
|
||||
tab.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, sessionId);
|
||||
if (tabClicked) {
|
||||
await page.waitForTimeout(1000);
|
||||
console.log('Switched to test session');
|
||||
}
|
||||
}
|
||||
|
||||
// Focus the terminal
|
||||
await page.click('.xterm-screen');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Take before screenshot
|
||||
await page.screenshot({ path: '/tmp/input-test-before.png' });
|
||||
console.log('Screenshot: /tmp/input-test-before.png (before typing)');
|
||||
|
||||
// Get terminal state before typing
|
||||
const beforeState = await getTerminalContent(page);
|
||||
const beforeCursorY = beforeState?.cursorY ?? -1;
|
||||
console.log(`Before: cursor at row ${beforeCursorY}, col ${beforeState?.cursorX}`);
|
||||
|
||||
// TEST 1: Type a short string
|
||||
console.log('\n--- Test 1: Short string ---');
|
||||
const testStr = 'echo hello-world-test-123';
|
||||
await page.keyboard.type(testStr, { delay: 30 });
|
||||
await page.waitForTimeout(600); // Wait for PTY echo round-trip
|
||||
|
||||
const afterShort = await getTerminalContent(page);
|
||||
await page.screenshot({ path: '/tmp/input-test-short.png' });
|
||||
console.log('Screenshot: /tmp/input-test-short.png');
|
||||
|
||||
// Check: cursor should still be on same row (or +1 for wrap)
|
||||
// and text should appear at/near the cursor position
|
||||
const cursorRowAfter = afterShort?.cursorY ?? -1;
|
||||
console.log(`After: cursor at row ${cursorRowAfter}, col ${afterShort?.cursorX}`);
|
||||
|
||||
// The echo should be on the cursor's row, not below
|
||||
const echoLine = afterShort?.lines?.[afterShort.cursorY + (afterShort.lines.length - afterShort.rows)] ?? '';
|
||||
console.log(`Echo line: "${echoLine.trim()}"`);
|
||||
if (echoLine.includes(testStr)) {
|
||||
console.log('PASS: Echo found at cursor row');
|
||||
} else {
|
||||
console.log('INFO: Echo may be on a different row (check screenshots)');
|
||||
}
|
||||
|
||||
// TEST 2: Press Enter and check response
|
||||
console.log('\n--- Test 2: Enter key ---');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const afterEnter = await getTerminalContent(page);
|
||||
await page.screenshot({ path: '/tmp/input-test-enter.png' });
|
||||
console.log('Screenshot: /tmp/input-test-enter.png');
|
||||
|
||||
// Check for "hello-world-test-123" in the output
|
||||
const allText = afterEnter?.lines?.join('\n') ?? '';
|
||||
if (allText.includes('hello-world-test-123')) {
|
||||
console.log('PASS: Command output visible');
|
||||
} else {
|
||||
console.log('WARN: Command output not found');
|
||||
}
|
||||
|
||||
// TEST 3: Type a LONG string that approaches right edge
|
||||
console.log('\n--- Test 3: Long string (near right edge) ---');
|
||||
const longStr = 'echo ' + 'A'.repeat(afterShort?.cols ? afterShort.cols - 10 : 150);
|
||||
await page.keyboard.type(longStr, { delay: 10 });
|
||||
await page.waitForTimeout(800);
|
||||
|
||||
const afterLong = await getTerminalContent(page);
|
||||
await page.screenshot({ path: '/tmp/input-test-long.png' });
|
||||
console.log('Screenshot: /tmp/input-test-long.png');
|
||||
console.log(`After long: cursor at row ${afterLong?.cursorY}, col ${afterLong?.cursorX}`);
|
||||
|
||||
// Check that no text leaked below the cursor row
|
||||
const cursorAbsRow = (afterLong?.lines?.length ?? 0) - (afterLong?.rows ?? 0) + (afterLong?.cursorY ?? 0);
|
||||
const rowsBelow = afterLong?.lines?.slice(cursorAbsRow + 1) ?? [];
|
||||
const leakedText = rowsBelow.filter(l => l.trim().length > 0 && l.includes('AAA'));
|
||||
if (leakedText.length === 0) {
|
||||
console.log('PASS: No text leaked below cursor row');
|
||||
} else {
|
||||
console.log(`FAIL: Text leaked below cursor: ${leakedText.map(l => `"${l.trim()}"`).join(', ')}`);
|
||||
}
|
||||
|
||||
// TEST 4: Rapid typing (paste-like)
|
||||
console.log('\n--- Test 4: Rapid input (paste simulation) ---');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(500);
|
||||
// Type rapidly (no delay between chars)
|
||||
await page.keyboard.type('echo rapid-test-no-delay-abcdefghij', { delay: 0 });
|
||||
await page.waitForTimeout(800);
|
||||
|
||||
await page.screenshot({ path: '/tmp/input-test-rapid.png' });
|
||||
console.log('Screenshot: /tmp/input-test-rapid.png');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const afterRapid = await getTerminalContent(page);
|
||||
const rapidText = afterRapid?.lines?.join('\n') ?? '';
|
||||
if (rapidText.includes('rapid-test-no-delay-abcdefghij')) {
|
||||
console.log('PASS: Rapid input command output visible');
|
||||
} else {
|
||||
console.log('WARN: Rapid input output not found (check screenshots)');
|
||||
}
|
||||
|
||||
// Final screenshot
|
||||
await page.screenshot({ path: '/tmp/input-test-final.png' });
|
||||
console.log('\nScreenshot: /tmp/input-test-final.png (final state)');
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Test Complete ===');
|
||||
console.log('Check screenshots in /tmp/input-test-*.png');
|
||||
console.log('Key check: no text should appear below the active cursor row');
|
||||
|
||||
} finally {
|
||||
await browser.close();
|
||||
if (sessionId && !isLive) {
|
||||
console.log(`\nCleaning up session ${sessionId}...`);
|
||||
await deleteSession(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Test failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user