mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
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:
@@ -153,7 +153,7 @@ Step 3 handles tab completion and arrow-key edits: if the user tabs to complete
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `setFlushed(count, text)` | Mark characters as sent-but-unacknowledged. Triggers a render. |
|
||||
| `setFlushed(count, text, render?)` | Mark characters as sent-but-unacknowledged. Pass `render=false` when restoring during a tab switch before the new buffer has loaded (prevents stale prompt column locking). Default: `true`. |
|
||||
| `getFlushed()` | Returns `{ count: number, text: string }`. |
|
||||
| `clearFlushed()` | Clear flushed state (call when server echo has arrived). |
|
||||
|
||||
@@ -166,6 +166,7 @@ The overlay can scan the terminal buffer for text that already exists after the
|
||||
| `detectBufferText()` | Scan buffer for text after prompt. Returns the detected text string, or `null`. If found, sets it as flushed text. Guarded: only runs once per `clear()` cycle. |
|
||||
| `resetBufferDetection()` | Re-enable detection (e.g., after tab completion response arrives). |
|
||||
| `suppressBufferDetection()` | Prevent detection until next `clear()`. Use when switching to a session whose buffer has UI framework text (e.g., Ink status bars) after the prompt marker that would be falsely detected. |
|
||||
| `undoDetection()` | Undo the last `detectBufferText()` — clears flushed state and re-enables detection. Use when tab completion detection found text matching the pre-tab baseline (no real completion happened) and needs to retry. |
|
||||
|
||||
#### Rendering
|
||||
|
||||
@@ -251,12 +252,10 @@ function switchToSession(newSessionId: string) {
|
||||
loadSessionBuffer(newSessionId);
|
||||
|
||||
// 3. Restore overlay state for new session
|
||||
zerolag.suppressBufferDetection(); // prevent false detection of UI text
|
||||
const saved = savedFlushed.get(newSessionId);
|
||||
if (saved) {
|
||||
zerolag.suppressBufferDetection(); // prevent false detection of UI text
|
||||
zerolag.setFlushed(saved.count, saved.text);
|
||||
} else {
|
||||
zerolag.suppressBufferDetection(); // fresh session, no flushed state
|
||||
zerolag.setFlushed(saved.count, saved.text, false); // render=false: buffer not loaded yet
|
||||
}
|
||||
|
||||
// 4. Re-render after buffer loads
|
||||
@@ -281,6 +280,10 @@ zerolag.resetBufferDetection();
|
||||
const detected = zerolag.detectBufferText();
|
||||
if (detected && detected !== baseline) {
|
||||
// Tab completion occurred — overlay now shows the completed text
|
||||
zerolag.rerender();
|
||||
} else if (detected) {
|
||||
// Same text as before Tab — no real completion. Undo and retry next cycle.
|
||||
zerolag.undoDetection();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -271,11 +271,16 @@ export class ZerolagInputAddon implements XtermAddon {
|
||||
*
|
||||
* @param count - Number of characters flushed
|
||||
* @param text - The actual flushed text (avoids reading stale terminal buffer)
|
||||
* @param render - Whether to re-render immediately (default: `true`).
|
||||
* Pass `false` when restoring flushed state during a tab/session switch
|
||||
* before the new buffer has loaded — rendering against a stale buffer
|
||||
* would lock the prompt column to the wrong position. Call `rerender()`
|
||||
* explicitly after the buffer finishes loading.
|
||||
*/
|
||||
setFlushed(count: number, text: string): void {
|
||||
setFlushed(count: number, text: string, render = true): void {
|
||||
this._flushedOffset = count;
|
||||
this._flushedText = text;
|
||||
this._render();
|
||||
if (render) this._render();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,6 +352,20 @@ export class ZerolagInputAddon implements XtermAddon {
|
||||
this._bufferDetectDone = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last `detectBufferText()` call — clears flushed state and
|
||||
* re-enables detection.
|
||||
*
|
||||
* Use case: Tab completion detection found text that matches the
|
||||
* pre-tab baseline (no real completion happened). Call this to undo
|
||||
* the detection so it can retry on the next flush cycle.
|
||||
*/
|
||||
undoDetection(): void {
|
||||
this._flushedOffset = 0;
|
||||
this._flushedText = '';
|
||||
this._bufferDetectDone = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress buffer detection until the next `clear()` or
|
||||
* `resetBufferDetection()` call.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user