fix: eliminate 3 migration pitfalls in xterm-zerolag-input

Pitfall 3 (REAL BUG): setFlushed() called _render() unconditionally.
During tab switch, rendering against a stale buffer found the old
session's prompt and locked the column position via the flushed-text
column lock. After the new buffer loaded, rerender() kept the stale
column. Fix: add render parameter (default true), pass false during
tab-switch restore to defer rendering until buffer is loaded.

Pitfall 2: Tab completion "undo" required calling clearFlushed() +
resetBufferDetection() separately — easy to get wrong. Add
undoDetection() convenience method that atomically clears flushed
state and re-enables detection.

Pitfall 1: Backspace Map sync is app-specific (can't be fixed in lib),
but removeChar() return value + getFlushed() make the pattern clean.
Updated tab-switch test to demonstrate the correct pattern.

Tests: 78 total (+4 new)
- setFlushed render=false prevents stale column lock
- setFlushed render=false keeps overlay hidden
- undoDetection clears flushed + re-enables detection
- undoDetection preserves pending text

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-22 11:16:12 +01:00
co-authored by Claude Opus 4.6
parent 36f0772c2f
commit 8dfcfe8d62
3 changed files with 82 additions and 8 deletions
@@ -160,6 +160,30 @@ describe('ZerolagInputAddon', () => {
expect(addon.hasPending).toBe(true);
});
it('setFlushed with render=false does not render', () => {
const { addon } = tracked();
addon.setFlushed(3, 'abc', false);
expect(addon.getFlushed()).toEqual({ count: 3, text: 'abc' });
expect(addon.hasPending).toBe(true);
// Overlay should still be hidden (no render triggered)
expect(addon.state.visible).toBe(false);
});
it('setFlushed with render=false prevents stale column lock', () => {
const { addon, mock } = tracked(['$ old session text']);
// Simulate: clear overlay, then restore flushed WITHOUT render
addon.clear();
addon.setFlushed(5, 'hello', false);
// lastPromptPos should still be null (no render happened)
expect(addon.state.promptPosition).toBeNull();
// Now simulate buffer load and rerender
mock.setLines(['$ ']);
addon.rerender();
// Prompt should be freshly scanned, no stale column lock
const pos = addon.state.promptPosition;
expect(pos).not.toBeNull();
});
it('clearFlushed resets flushed state', () => {
const { addon } = tracked();
addon.setFlushed(3, 'abc');
@@ -291,6 +315,34 @@ describe('ZerolagInputAddon', () => {
expect(addon.getFlushed().count).toBe(0);
});
it('undoDetection clears flushed and re-enables detection', () => {
const { addon } = tracked(['$ completed']);
// Detect buffer text (simulates tab completion detection)
addon.detectBufferText();
expect(addon.getFlushed().count).toBe(9); // 'completed'
// Undo because it matched baseline (no real completion)
addon.undoDetection();
expect(addon.getFlushed().count).toBe(0);
expect(addon.getFlushed().text).toBe('');
// Detection should work again (guard reset)
const text = addon.detectBufferText();
expect(text).toBe('completed');
});
it('undoDetection does not hide overlay or clear pending', () => {
const { addon } = tracked(['$ buffer']);
addon.detectBufferText(); // sets flushed
addon.addChar('x'); // adds pending on top
addon.undoDetection();
// Flushed cleared, but pending preserved
expect(addon.pendingText).toBe('x');
expect(addon.getFlushed().count).toBe(0);
expect(addon.hasPending).toBe(true);
});
it('clear resets suppression (re-enables detection)', () => {
const { addon } = tracked(['$ text']);
addon.suppressBufferDetection();
@@ -394,7 +446,7 @@ describe('ZerolagInputAddon', () => {
// Tab switch back: restore as flushed (text is now in PTY)
addon.suppressBufferDetection(); // prevent false Ink detection
addon.setFlushed(totalCount, totalText);
addon.setFlushed(totalCount, totalText, false); // no render — buffer not loaded yet
expect(addon.getFlushed()).toEqual({ count: 2, text: 'hi' });
expect(addon.hasPending).toBe(true); // has flushed content