mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(predictive-echo): anchor hold after unpredicted wire edits (review findings)
Independent post-build review found three gaps, all one family: input that changes the composer without a prediction leaves the DISPLAYED cursor stale for one RTT, and anchoring a new run on it painted ghosts one cell off (blank-neutral, so they lived out the full TTL: "tehh" on backspace-then-retype, exactly on the links the feature targets). Fix: the addon now HOLDS new predictions after any such edit (backspace with nothing outstanding = deleting echoed text, clearPredictions, and now also IME/plain-paste 'text' commits, which the hook clears like 'clear') until the next PARSED write releases the hold. The inline predictChar reconcile deliberately does not count: only the emitter pass or the public reconcile() is the display-caught-up contract. Worst case is exactly one unpredicted keystroke, whose own echo releases the hold. Also patched the one bypass path the PR had missed: _handleCjkInput now clears predictions like insertTerminalText and the other bypass sends. Package suite 230, vm gating 85, E2E 10/10 all green after the change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
55bff4a4bf
commit
9e7c537e14
9 files changed
+83
-10
No files matched your search
@@ -22,7 +22,7 @@ Implementation detail extracted from `CLAUDE.md` so that file stays small enough
|
||||
|
||||
**External CLI modes (OpenCode, Codex, Gemini, Antigravity)**: `isExternalCliMode()` in `session.ts` (`mode === 'opencode' || 'codex' || 'gemini' || 'antigravity'`) gates Claude-specific behavior — Ralph tracker, BashToolParser, token/CLI-info parsing, and ❯-prompt readiness detection are all skipped (these CLIs render their own TUIs; readiness = output stabilization instead). All four modes **require tmux — no direct PTY fallback** — because secrets are injected via `tmux setenv` (socket-scoped `${this.tmux()} setenv`, never on the spawn command line): OpenCode gets `OPENCODE_CONFIG_CONTENT` etc., Codex gets `OPENAI_API_KEY`/`CODEX_API_KEY`/`CODEX_HOME` (`setCodexEnvVars`), Gemini gets `GEMINI_API_KEY`/`GOOGLE_API_KEY`/`GOOGLE_CLOUD_PROJECT`/`GOOGLE_APPLICATION_CREDENTIALS`/`GOOGLE_GENAI_USE_VERTEXAI` etc. (`setGeminiEnvVars`, all in `tmux-manager.ts`). Codex specifics: command built by `buildCodexCommand()` (`--model`, `resume <id>`, `--dangerously-bypass-approvals-and-sandbox` from the `codexConfig` payload / `codexDangerouslyBypassApprovals` app setting; `renderMode` is schema-coerced to `'hybrid'`, the only supported mode). Gemini specifics: command built by `buildGeminiCommand()` (`--skip-trust` always, `--approval-mode <default|auto_edit|yolo|plan>` defaulting to `yolo` for parity with Claude's `--dangerously-skip-permissions`, `--model`, `--resume` from the `geminiConfig` payload); availability via `GET /api/gemini/status` — session/quick-start routes fail with `OPERATION_FAILED` + install hint (`npm install -g @google/gemini-cli`) when missing. Codex AND Gemini export `COLORTERM=truecolor` + unset `NO_COLOR` (other modes unset `COLORTERM`); Gemini joins `isAltScreenStripMode()` (Codex/Claude/Gemini are Ink TUIs that repaint inline → strip alt-screen/`3J` so scrollback survives). Codex availability via `GET /api/codex/status`. Antigravity specifics: command built by `buildAntigravityCommand()` (`--model`, `--conversation <id>` resume, `--dangerously-skip-permissions` from the `antigravityConfig` payload); availability via `GET /api/antigravity/status` — routes fail with `OPERATION_FAILED` + install hint (`curl -fsSL https://antigravity.google/cli/install.sh | bash`) when missing. Unlike the other three it is NOT an npm package (standalone binary, `~/.local/bin/agy`), which is why `docker/agent.Dockerfile` installs it with its own `--dir /usr/local/bin` step rather than in the `npm install -g` line, and why it does NOT join `isAltScreenStripMode()`. Frontend: run-mode dropdown → `runCodex()`/`runGemini()` in `session-ui.js` ("Run CX"/"Run GM" labels), App Settings → Codex CLI tab; Respawn/Ralph options are Claude-only, so session options open on the Summary tab for external CLI sessions. ⚠️ `run*()` MUST unwrap the `{success,data}` envelope (`(await res.json()).data.available` / `data.data.sessionId`) — reading the raw shape silently breaks the run. Tests: `test/run-mode-ui.test.ts` + `test/gemini-mode.test.ts` (vm-sandbox harness, no real DOM).
|
||||
|
||||
**Codex input path (issues #218/#219/#220/#222)**: codex-mode sessions use **predictive write-through echo, never the buffer overlay**. The buffer overlay stays disabled exactly as 1.12.2 left it (`_updateLocalEchoState` in terminal-ui.js, same branch as shell; `_localEchoEnabled` remains false for codex), and the additive `_localEchoPolicy` field selects `'predict'` for codex when `localEchoEnabled` is on. Codex's composer is interactive per keystroke: typing "/" pops a live-filtering command picker (#222 was "picker never appears" because the "/" sat in the overlay until Enter), the composer grows/rewraps as it fills (#220: a long typed prompt existed ONLY in the overlay DOM, so codex never grew the composer), arrows and Ctrl+Backspace edit server-side state (#218: arrows were forwarded to an EMPTY composer while the typed text sat pending; the `\x08` control-char flush then left the overlay stateless so `\x7f` was swallowed as "nothing to remove"), and pastes arrive bracketed (#219: `terminal.paste()` wraps in `\x1b[200~..201~`, which the multi-byte-ESC branch forwarded WITHOUT flushing pending text, so the paste landed before it). The shared overlay branch (claude/gemini/opencode still buffer) gained three fixes: bracketed pastes flush pending text first, composer nav keys (`isComposerNavKey` allowlist in `CodemanTerminalInput` — arrows/Home/End/Delete/PgUp/PgDn incl. modifiers, deliberately excluding DA/CPR/DSR query responses) flush and hand the session to **pass-through** (plain PTY echo until Enter/Ctrl+C, because after cursor movement the append-only overlay cannot track edits), and a backspace that finds no overlay state is FORWARDED instead of swallowed. ⚠️ **Codex drops keystrokes that arrive in the same PTY read as a bracketed paste** (upstream `bottom_pane/paste_burst.rs` holds rapid chars for paste classification; verified against codex 0.147.0 by writing `hello\x1b[200~PASTED\x1b[201~` into the tmux client PTY in one write → composer shows only `PASTED`, while a 100ms gap yields `helloPASTED`), so the flush sends the typed text immediately and delays the paste sequence by 80ms — the same two-phase shape as the Enter branch's delayed `\r`. Related protocol fact: xterm.js sends `0x08` for Ctrl+Backspace, which codex's keymap binds to delete-ONE-char (`ctrl(Char('h'))`); real word-delete needs the kitty CSI-u encoding (`\x1b[127;5u`), which xterm.js 6.0.0 cannot emit (kitty support lands in 6.1.0-beta) — an upstream limitation, not a Codeman bug. E2E technique: codex 0.147 reaches its composer with any dummy key in `$CODEX_HOME/auth.json` (`{"OPENAI_API_KEY":"sk-test-..."}`), so a real TUI can be driven headlessly (envOverrides `CODEX_HOME` rides the `CODEX_*` allowlist) without real credentials. **Predictive write-through echo invariants** (the codex echo mode, `PredictiveEchoAddon` in `packages/xterm-zerolag-input`): (1) the onData hook `_predictHookOnData` is a PLAIN STATEMENT between the buffer block and Normal Mode — no `return`, try/catch-wrapped, never touches `_pendingInput` — so the wire path is byte-identical with the predictor active, absent or throwing (pinned at vm level and by an end-to-end trace-equality E2E); (2) it ships as a SEPARATE bundle `vendor/xterm-predictive-echo.js` so the zerolag bundle stays byte-identical, and a missing/broken bundle degrades codex to plain 1.12.2 echo (`typeof PredictiveEchoOverlay !== 'undefined'` guard); (3) predictions paint only while the cursor sits on the measured composer row (`isCodexComposerRow`, `CODEX_COMPOSER_ROW_RE = /^› /` — matches the empty-composer placeholder, typing, and the slash picker; rejects modal rows and 2-space wrapped continuation rows, the #220 ghost zone, which deliberately fall back to real echo); (4) reconciliation reads the PARSED buffer with `baseY + row` (xterm's `cursorY` is baseY-relative; `viewportY` only coincides while scrolled to bottom), confirms prefix-only on cell match PLUS cursor advance, cascades only on TWO consecutive foreign NON-BLANK passes (blanks are neutral: codex clears its placeholder on first echo), and TTL-bounds the rest; (5) the per-device `localEchoEnabled` toggle is the kill switch returning exact 1.12.2 behavior. Measured constants + fixtures: `docs/predictive-echo-plan.md`, recorded via `scripts/dev/record-codex-frames.mjs` through the production tmux+strip pipeline. Tests: `test/local-echo-codex-gating.test.ts` (vm harness: nav-key + predict classifier truth tables, policy matrix, wire-neutrality pins), `packages/xterm-zerolag-input/test/` (addon laws, real-fixture replay, seeded fuzz), `test/codex-predictive-echo.test.ts` (E2E vs real codex incl. byte-identity + 300ms-RTT).
|
||||
**Codex input path (issues #218/#219/#220/#222)**: codex-mode sessions use **predictive write-through echo, never the buffer overlay**. The buffer overlay stays disabled exactly as 1.12.2 left it (`_updateLocalEchoState` in terminal-ui.js, same branch as shell; `_localEchoEnabled` remains false for codex), and the additive `_localEchoPolicy` field selects `'predict'` for codex when `localEchoEnabled` is on. Codex's composer is interactive per keystroke: typing "/" pops a live-filtering command picker (#222 was "picker never appears" because the "/" sat in the overlay until Enter), the composer grows/rewraps as it fills (#220: a long typed prompt existed ONLY in the overlay DOM, so codex never grew the composer), arrows and Ctrl+Backspace edit server-side state (#218: arrows were forwarded to an EMPTY composer while the typed text sat pending; the `\x08` control-char flush then left the overlay stateless so `\x7f` was swallowed as "nothing to remove"), and pastes arrive bracketed (#219: `terminal.paste()` wraps in `\x1b[200~..201~`, which the multi-byte-ESC branch forwarded WITHOUT flushing pending text, so the paste landed before it). The shared overlay branch (claude/gemini/opencode still buffer) gained three fixes: bracketed pastes flush pending text first, composer nav keys (`isComposerNavKey` allowlist in `CodemanTerminalInput` — arrows/Home/End/Delete/PgUp/PgDn incl. modifiers, deliberately excluding DA/CPR/DSR query responses) flush and hand the session to **pass-through** (plain PTY echo until Enter/Ctrl+C, because after cursor movement the append-only overlay cannot track edits), and a backspace that finds no overlay state is FORWARDED instead of swallowed. ⚠️ **Codex drops keystrokes that arrive in the same PTY read as a bracketed paste** (upstream `bottom_pane/paste_burst.rs` holds rapid chars for paste classification; verified against codex 0.147.0 by writing `hello\x1b[200~PASTED\x1b[201~` into the tmux client PTY in one write → composer shows only `PASTED`, while a 100ms gap yields `helloPASTED`), so the flush sends the typed text immediately and delays the paste sequence by 80ms — the same two-phase shape as the Enter branch's delayed `\r`. Related protocol fact: xterm.js sends `0x08` for Ctrl+Backspace, which codex's keymap binds to delete-ONE-char (`ctrl(Char('h'))`); real word-delete needs the kitty CSI-u encoding (`\x1b[127;5u`), which xterm.js 6.0.0 cannot emit (kitty support lands in 6.1.0-beta) — an upstream limitation, not a Codeman bug. E2E technique: codex 0.147 reaches its composer with any dummy key in `$CODEX_HOME/auth.json` (`{"OPENAI_API_KEY":"sk-test-..."}`), so a real TUI can be driven headlessly (envOverrides `CODEX_HOME` rides the `CODEX_*` allowlist) without real credentials. **Predictive write-through echo invariants** (the codex echo mode, `PredictiveEchoAddon` in `packages/xterm-zerolag-input`): (1) the onData hook `_predictHookOnData` is a PLAIN STATEMENT between the buffer block and Normal Mode — no `return`, try/catch-wrapped, never touches `_pendingInput` — so the wire path is byte-identical with the predictor active, absent or throwing (pinned at vm level and by an end-to-end trace-equality E2E); (2) it ships as a SEPARATE bundle `vendor/xterm-predictive-echo.js` so the zerolag bundle stays byte-identical, and a missing/broken bundle degrades codex to plain 1.12.2 echo (`typeof PredictiveEchoOverlay !== 'undefined'` guard); (3) predictions paint only while the cursor sits on the measured composer row (`isCodexComposerRow`, `CODEX_COMPOSER_ROW_RE = /^› /` — matches the empty-composer placeholder, typing, and the slash picker; rejects modal rows and 2-space wrapped continuation rows, the #220 ghost zone, which deliberately fall back to real echo); (4) reconciliation reads the PARSED buffer with `baseY + row` (xterm's `cursorY` is baseY-relative; `viewportY` only coincides while scrolled to bottom), confirms prefix-only on cell match PLUS cursor advance, cascades only on TWO consecutive foreign NON-BLANK passes (blanks are neutral: codex clears its placeholder on first echo), and TTL-bounds the rest; (5) after an UNPREDICTED wire edit (backspace into echoed text, any 'clear'-classified input, an IME/plain-paste 'text' commit, or every bypass send incl. `_handleCjkInput`) the addon holds new predictions until the next PARSED write: the displayed cursor is stale for one RTT and anchoring on it paints ghosts one cell off; (6) the per-device `localEchoEnabled` toggle is the kill switch returning exact 1.12.2 behavior. Measured constants + fixtures: `docs/predictive-echo-plan.md`, recorded via `scripts/dev/record-codex-frames.mjs` through the production tmux+strip pipeline. Tests: `test/local-echo-codex-gating.test.ts` (vm harness: nav-key + predict classifier truth tables, policy matrix, wire-neutrality pins), `packages/xterm-zerolag-input/test/` (addon laws, real-fixture replay, seeded fuzz), `test/codex-predictive-echo.test.ts` (E2E vs real codex incLine truncated
|
||||
|
||||
### Remote sessions over SSH
|
||||
|
||||
|
||||
@@ -83,6 +83,14 @@ rules and why each exists:
|
||||
- **No drop on baseY change**: codex streams push lines to history while the
|
||||
composer stays viewport-pinned; predictions are row-relative to the pinned
|
||||
composer and remain valid (measured above).
|
||||
- **Anchor hold** (added by the independent post-build review): after any wire
|
||||
input whose cursor effect the display has not shown yet (backspace with
|
||||
nothing outstanding = deleting echoed text, every 'clear'-classified input,
|
||||
an IME/plain-paste 'text' commit, and the bypass send paths), new
|
||||
predictions are suppressed until the next PARSED write. Anchoring on the
|
||||
stale cursor painted ghosts one cell off ("tehh" on backspace-then-retype
|
||||
within RTT), blank-neutral and therefore TTL-lived. Worst case is exactly
|
||||
one unpredicted keystroke: its own echo is a write, which releases the hold.
|
||||
- **predictBackspace()** pops the newest outstanding record (informational
|
||||
return; the consumer forwards `\x7f` unconditionally). Deleting already-echoed
|
||||
text renders at RTT in v1.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
### Minor Changes
|
||||
|
||||
- **New addon: `PredictiveEchoAddon`, mosh-style write-through prediction.** The second echo mode for per-keystroke TUIs (OpenAI Codex's composer, live pickers) that buffer-until-Enter starves. Every keystroke is sent by the consumer immediately and unchanged; the addon paints the predicted glyph at the predicted cell and reconciles against the PARSED terminal buffer: confirmation requires the cell match plus a cursor advance past the record, foreign non-blank content on two consecutive passes cascades a drop, blank cells are neutral, a TTL bounds everything, and scroll/resize/sustained cursor moves clear the run. Visual-only by construction; it cannot gate, delay or rewrite input.
|
||||
- Anchor-hold rule: after an unpredicted wire edit (backspace into echoed text, cleared input, an IME text commit) new predictions hold until the next parsed write, so a stale displayed cursor can never mis-anchor a run (worst case: exactly one unpredicted keystroke).
|
||||
- New exports: `PredictiveEchoAddon`, `PredictiveEchoOptions`, `PredictionState`, plus the long-intended `charCellWidth` / `stringCellWidth` helpers.
|
||||
- `XtermTerminal` type gains OPTIONAL members (`buffer.active.cursorX/cursorY`, `getLine().getCell?`, `onWriteParsed?`, `onResize?`). Additive only: existing consumers and mocks are unaffected.
|
||||
- IIFE build exposes `window.PredictiveEchoAddon` and a self-activating `window.PredictiveEchoOverlay`, alongside the unchanged `ZerolagInputAddon` / `LocalEchoOverlay` globals.
|
||||
|
||||
@@ -333,7 +333,12 @@ identical in-place repaint, never false-confirms). A cell showing foreign
|
||||
non-blank content on two consecutive passes drops that prediction and all
|
||||
later ones (one pass tolerates half-parsed frames). Blank cells are neutral:
|
||||
they are what "not yet echoed" looks like. Whatever remains is dropped by TTL.
|
||||
Scrolling up, resizing, or a sustained cursor move clears the run.
|
||||
Scrolling up, resizing, or a sustained cursor move clears the run. After a
|
||||
backspace into already-echoed text, a cleared input, or a multi-char commit,
|
||||
the addon **holds** new predictions until the next parsed write: the displayed
|
||||
cursor is stale for one round trip, and anchoring on it would paint ghosts one
|
||||
cell off (worst case: exactly one unpredicted keystroke, whose own echo
|
||||
releases the hold).
|
||||
|
||||
### API
|
||||
|
||||
|
||||
@@ -88,6 +88,14 @@ export class PredictiveEchoAddon implements XtermAddon {
|
||||
private _confirmedTotal = 0;
|
||||
private _droppedTotal = 0;
|
||||
private _ttlTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
/** Anchor hold: set after an unpredicted wire edit (backspace into echoed
|
||||
* text, any cleared input, an IME text commit). While held, new
|
||||
* predictions are suppressed: the displayed cursor is stale until the
|
||||
* next parsed write, and anchoring on it paints ghosts one cell off
|
||||
* (found by review: backspace-then-retype within RTT). Cleared by the
|
||||
* onWriteParsed pass and by public reconcile(), never by the inline
|
||||
* predictChar pass (which runs before the display could catch up). */
|
||||
private _anchorHold = false;
|
||||
private _reconcileScheduled = false;
|
||||
private _disposables: Array<{ dispose(): void }> = [];
|
||||
private _predictWhen: ((terminal: XtermTerminal) => boolean) | null;
|
||||
@@ -141,6 +149,7 @@ export class PredictiveEchoAddon implements XtermAddon {
|
||||
this._reconcileScheduled = true;
|
||||
queueMicrotask(() => {
|
||||
this._reconcileScheduled = false;
|
||||
this._anchorHold = false; // a parse pass ran: the display caught up
|
||||
this._safeReconcile();
|
||||
});
|
||||
})
|
||||
@@ -183,6 +192,7 @@ export class PredictiveEchoAddon implements XtermAddon {
|
||||
predictChar(ch: string): boolean {
|
||||
try {
|
||||
this._reconcile();
|
||||
if (this._anchorHold) return false; // display has not caught up with a wire edit
|
||||
|
||||
const t = this._terminal;
|
||||
if (!t || !this._container) return false;
|
||||
@@ -247,7 +257,12 @@ export class PredictiveEchoAddon implements XtermAddon {
|
||||
predictBackspace(): boolean {
|
||||
try {
|
||||
const rec = this._outstanding.pop();
|
||||
if (!rec) return false;
|
||||
if (!rec) {
|
||||
// \x7f goes to the wire and will delete ECHOED text: the cursor is
|
||||
// about to move in a way we cannot see yet
|
||||
this._anchorHold = true;
|
||||
return false;
|
||||
}
|
||||
removePredictionSpan(this._spans, rec.seq);
|
||||
if (this._outstanding.length === 0) this._resetRun();
|
||||
return true;
|
||||
@@ -256,9 +271,12 @@ export class PredictiveEchoAddon implements XtermAddon {
|
||||
}
|
||||
}
|
||||
|
||||
/** Drop every outstanding prediction and its spans. */
|
||||
/** Drop every outstanding prediction and its spans. Also arms the anchor
|
||||
* hold: consumers clear on inputs (Enter, Esc, arrows, pastes) whose
|
||||
* cursor effect is unknown until the next parsed write. */
|
||||
clearPredictions(): void {
|
||||
try {
|
||||
this._anchorHold = true;
|
||||
this._droppedTotal += this._outstanding.length;
|
||||
this._outstanding = [];
|
||||
clearAllSpans(this._spans);
|
||||
@@ -268,8 +286,10 @@ export class PredictiveEchoAddon implements XtermAddon {
|
||||
}
|
||||
}
|
||||
|
||||
/** Manual reconcile pass, for consumers without onWriteParsed. */
|
||||
/** Manual reconcile pass, for consumers without onWriteParsed. By contract
|
||||
* it is called after writes parsed, so it also releases the anchor hold. */
|
||||
reconcile(): void {
|
||||
this._anchorHold = false;
|
||||
this._safeReconcile();
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ async function replay(name: string) {
|
||||
let painted = false;
|
||||
if (kind === 'char') painted = addon.predictChar(line.data);
|
||||
else if (kind === 'backspace') addon.predictBackspace();
|
||||
else if (kind === 'clear') addon.clearPredictions();
|
||||
else addon.clearPredictions(); // 'clear' AND 'text', like the terminal-ui hook
|
||||
// Span/record parity and grid bounds hold at every step
|
||||
expect(rt.spanCount()).toBe(addon.state.outstanding);
|
||||
assertSpansInGrid(rt);
|
||||
|
||||
@@ -335,6 +335,7 @@ describe('PredictiveEchoAddon', () => {
|
||||
|
||||
it('predictBackspace pops newest, returns false when empty, never touches confirmed', async () => {
|
||||
expect(addon.predictBackspace()).toBe(false);
|
||||
addon.reconcile(); // the empty pop armed the anchor hold; release it
|
||||
addon.predictChar('a');
|
||||
addon.predictChar('b');
|
||||
expect(addon.predictBackspace()).toBe(true);
|
||||
@@ -478,6 +479,7 @@ describe('PredictiveEchoAddon', () => {
|
||||
rows.style.color = 'rgb(255, 0, 0)'; // skin change
|
||||
a.refreshFont();
|
||||
a.clearPredictions();
|
||||
a.reconcile(); // release the anchor hold armed by the clear
|
||||
a.predictChar('v');
|
||||
const span2 = themed.terminal.element.querySelector('.xterm-screen span') as HTMLSpanElement;
|
||||
expect(span2.style.color).toBe('rgb(255, 0, 0)');
|
||||
@@ -485,6 +487,37 @@ describe('PredictiveEchoAddon', () => {
|
||||
themed.cleanup();
|
||||
});
|
||||
|
||||
it('anchor hold: backspace into echoed text suppresses prediction until a write parses', async () => {
|
||||
// \x7f went to the wire with nothing outstanding: the cursor will move
|
||||
// in a way the display has not shown, so anchoring now paints one cell
|
||||
// off (review finding: "tehh" ghosts on backspace-then-retype at RTT)
|
||||
expect(addon.predictBackspace()).toBe(false);
|
||||
expect(addon.predictChar('x')).toBe(false);
|
||||
expect(spansOf(mock)).toHaveLength(0);
|
||||
mock.fireWriteParsed(); // the display caught up
|
||||
await flushMicrotasks();
|
||||
expect(addon.predictChar('x')).toBe(true);
|
||||
});
|
||||
|
||||
it('anchor hold: clearPredictions suppresses until a write parses (or manual reconcile)', async () => {
|
||||
addon.predictChar('a');
|
||||
addon.clearPredictions(); // consumer saw Enter/Esc/arrow/paste
|
||||
expect(addon.predictChar('b')).toBe(false);
|
||||
mock.fireWriteParsed();
|
||||
await flushMicrotasks();
|
||||
expect(addon.predictChar('b')).toBe(true);
|
||||
});
|
||||
|
||||
it('anchor hold: the inline predictChar reconcile does NOT release it', () => {
|
||||
addon.clearPredictions();
|
||||
// Several keystrokes in a row before any echo: all suppressed, because
|
||||
// predictChar's inline pass must not count as the display catching up
|
||||
expect(addon.predictChar('a')).toBe(false);
|
||||
expect(addon.predictChar('b')).toBe(false);
|
||||
addon.reconcile(); // public/manual pass IS the caught-up contract
|
||||
expect(addon.predictChar('c')).toBe(true);
|
||||
});
|
||||
|
||||
it('state getter reports outstanding/confirmedTotal/droppedTotal/anchor', async () => {
|
||||
expect(addon.state).toEqual({ outstanding: 0, confirmedTotal: 0, droppedTotal: 0, anchor: null });
|
||||
addon.predictChar('a');
|
||||
|
||||
@@ -2563,8 +2563,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
const kind = window.CodemanTerminalInput.classifyPredictInput(data);
|
||||
if (kind === 'char') this._predictiveEcho.predictChar(data);
|
||||
else if (kind === 'backspace') this._predictiveEcho.predictBackspace();
|
||||
else if (kind === 'clear') this._predictiveEcho.clearPredictions();
|
||||
// kind === 'text' (plain multi-char paste): wire only, no visual
|
||||
// 'clear' AND 'text' (plain paste, IME word commits) both change the
|
||||
// composer in ways the display has not shown yet: clear the run and let
|
||||
// the addon's anchor hold suppress prediction until the echo catches up
|
||||
else this._predictiveEcho.clearPredictions();
|
||||
} catch {
|
||||
/* predictions must never block the wire */
|
||||
}
|
||||
@@ -2577,6 +2579,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
_crashDiag.log(`CJK send DROP no-session len=${text.length}`);
|
||||
return;
|
||||
}
|
||||
// Bypasses onData (like insertTerminalText): predictions cannot see this
|
||||
if (this._localEchoPolicy === 'predict') this._predictiveEcho?.clearPredictions();
|
||||
_crashDiag.log(`CJK send→${this.activeSessionId.slice(0, 8)} len=${text.length}`);
|
||||
this._sendInputAsync(this.activeSessionId, text);
|
||||
},
|
||||
|
||||
@@ -411,11 +411,13 @@ describe('_predictHookOnData (wire neutrality)', () => {
|
||||
expect(app._predictiveEcho!.clearPredictions).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("kind 'text' (plain paste) takes no visual action", () => {
|
||||
it("kind 'text' (plain paste, IME commit) clears the run like 'clear'", () => {
|
||||
// Review finding: an IME word-commit changes the composer without a
|
||||
// prediction; new predictions after it would mis-anchor until cascade.
|
||||
const app = makePredictApp();
|
||||
app._predictHookOnData('pasted text');
|
||||
expect(app._predictiveEcho!.predictChar).not.toHaveBeenCalled();
|
||||
expect(app._predictiveEcho!.clearPredictions).not.toHaveBeenCalled();
|
||||
expect(app._predictiveEcho!.clearPredictions).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('never touches _pendingInput and never sends (visual-only pin)', () => {
|
||||
|
||||
Reference in new issue
Block a user