mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
34eb3cd8fcf9fcb09bc9b0185ae90a17092fa0ff
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6c04cecbba | Fix ghost autocomplete after Topsec backspace echo | ||
|
|
f4aa6ddb46 |
fix #1013: stop ghost text from drawing over untracked echoed input (#1042)
Inline (ghost-text) suggestions render suggestion.substring(trackedInput.length) after the cursor, where trackedInput is a client-side reconstruction of the command line (buffer heuristics + keystroke prediction, to mask SSH echo latency). On hosts with non-standard echo — hardware bastion hosts / network OS like `ecOS#` (#1013, previously #756 / #906) — that reconstruction drifts and the ghost gets painted over characters the user already typed (`int` + ghost `terface` -> `intterface`). Add a fail-safe consistency check: on each post-echo render, if the real terminal line before the cursor contains the tracked input followed by more untracked, non-whitespace characters (reality is AHEAD of what we tracked), hide the ghost instead of drawing it over real text. SSH echo latency is the opposite case (the line is a prefix-behind of the tracked input) and is deliberately not flagged, so the ghost stays responsive on slow links. The check is ASCII-only (wide-char column mapping is ambiguous) and fail-open, so it can only ever suppress a ghost that would otherwise corrupt — never change correct behaviour. This converts the recurring "ghost shows already-typed characters" bug into "ghost simply doesn't show" on devices we can't track reliably. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
938d1ef48b |
Fix ghost text duplicating glyphs after Tab completion (#906)
The reliability gate at handleInput's adjustToInput call froze the ghost at its last show()-time tail in any path where the typed buffer becomes unreliable (Tab pass-through to shell, history recall, cursor moves). When the user kept typing into that gap, the next render advanced the cursor past the ghost's anchor while the ghost text stayed put — a → -accept then pasted the stale tail on top of the just-typed glyphs (e.g. "systemctl s" + typing "t" → screen showed "systemctl sttop firewalld"). Add GhostTextAddon.applyKeystroke so the ghost can evolve its own currentInput off raw keystrokes (printable / Backspace / Ctrl-W), seeded by whatever the last show() captured from the live xterm reading. handleInput now uses the existing adjustToInput on the reliable path (preserves multi-char paste re-alignment) and routes single-keystroke events through applyKeystroke on the unreliable path, fixing the visual misalignment and the duplication-on-accept in one shot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
73b27ad7c4 |
fix(autocomplete): sync ghost text to live input on every keystroke (#815)
* fix(autocomplete): sync ghost text to live input on every keystroke Ghost text was displayed based on whatever input was passed to GhostTextAddon.show() at fetch time. Between a user's keystroke and the next debounced fetchSuggestions firing (~100ms), the on-screen line had already advanced one character but ghost.getGhostText() still returned the pre-update tail. Pressing → during that window pasted the stale tail on top of the new char — e.g. type "do", suggestion shows "cker ls"; type "c", accept immediately → "doc" + "cker ls" lands as "doccker ls" instead of the expected "docker ls". Two-layer fix: 1. New GhostTextAddon.adjustToInput(newInput) that re-renders the ghost against a fresh input without waiting for a new fetch: shrinks / grows the tail if the suggestion still prefix-matches, hides otherwise. Called from handleInput after every buffer mutation (printable, backspace, Ctrl-W, paste tail) when the buffer is reliable. Unreliable-buffer paths skip the call to avoid making the ghost lie. 2. Defense-in-depth at both ghost-accept sites (→ and Ctrl-→): recompute the tail against the live typed buffer instead of trusting getGhostText's show()-time state. If the suggestion no longer prefixes the live buffer, hide without writing. Ctrl-→ additionally resyncs ghost.show() to the live buffer before picking the next word so getNextWord operates on an up-to-date tail. * fix(autocomplete): defer ghost text updates to the next xterm render The previous pass made adjustToInput re-show the ghost synchronously on every keystroke, but xterm hasn't echoed the triggering char yet at that moment — cursorX is still the pre-keystroke position. Painting the shrunken tail there left it visibly overlapping with the char xterm was about to draw, and the ghost only snapped to the right column on the next onRender tick. That one-frame overlap is the "jitter" the reporter still saw. Switch adjustToInput to a defer-and-reapply pattern: - On every keystroke that should re-align the ghost, stash the desired input in pendingInput and hide the element immediately. The transient blank frame is preferable to an overlap glyph. - The existing term.onRender listener now checks for a pending update first: by that tick xterm has processed the echo, cursorX has advanced, and we can paint the new tail at the correct column via applyInputUpdate. - New isActive() exposes "has a live suggestion even if hidden waiting for render" so a fast "type + →" / "type + Ctrl-→" sequence in the hide-until-render gap still hits the accept branch and grabs the recomputed tail from the live buffer. show() and hide() clear pendingInput so an explicit state change supersedes any queued adjust. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): restore ghost text, predict-anchor-shift on each keystroke The previous refactor broke inline completion entirely: 1. useTerminalAutocomplete force-disabled showGhostText whenever showPopupMenu was on — and both are true by default, so ghost never rendered. 2. GhostTextAddon put its overlay container *under* xterm's screen via insertBefore + no z-index. xterm's default renderer paints theme.background across every cell including empty ones, so the ghost was fully occluded by the canvas even when the hook *did* call show(). Fixes both issues and lands the correct per-keystroke strategy the jitter report was asking for: - Drop the showGhostText-vs-showPopupMenu gate; respect user settings. - Put the ghost container back on top of the screen (appendChild + z-index 1). - Track anchorInputLength at show() time. adjustToInput now advances the ghost's left by (newInput.length - anchorInputLength) cells *synchronously* — i.e. it predicts where xterm's cursor will land once the echo arrives, instead of re-reading the live cursorX that hasn't advanced yet. textContent is trimmed in the same call, so ghost + real-input stay aligned across SSH echo latency with no one-frame overlap or blank gap. - Updated GhostTextAddon.test.ts expectations for the new behavior (and cast the fake-document through unknown to fix the pre-existing TS error). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): address ghost text review feedback Follow-ups on the predict-anchor-shift from the previous commit, based on a code-reviewer pass: - Backspace / Ctrl-W de-sync: updatePosition's Math.max(0, ...) was clamping the delta to zero when newInput shrank below the show-time input length. The ghost then stayed pinned at the original anchor column while the real cursor walked back left, leaving a gap between the cursor and the ghost. Let the delta go negative so the ghost tracks the cursor backwards; clamp the resulting left at 0 instead of clamping the delta. - Resize staleness: onResize now also resets lastLeft/lastTop and re-renders, so the dedup cache in updatePosition doesn't hide a now-stale pixel coordinate after xterm recomputes cell dims. - Added a regression test for the backspace path covering both the step-back-below-anchor case and the clamp-at-0-on-overshoot case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): don't accept whole suggestion when buffer is unreliable Codex flagged (#815 P1 ×2) that the live-buffer recompute on → and Ctrl-→ falls into a degenerate path when typedBufferReliableRef is false. My previous cut used live = "" as the fallback, but fullSuggestion.startsWith("") is always true — so: - → would write the entire suggestion over whatever is on the line (post history-recall ↑, Ctrl-R reverse search, etc.). - Ctrl-→ would reanchor the ghost at the start and getNextWord would hand back the first token, duplicating leading content on top of the recalled command. When the buffer is unreliable, empty buffer ≠ empty line — the line has content we're not tracking. Fall back to the ghost's own cached state instead of recomputing: - → reliable: recompute tail vs live buffer, flip buffer to the accepted suggestion, reliability back on. - → unreliable: use ghost.getGhostText() (shown-at-show-time tail) and don't touch the buffer/reliability flag. - Ctrl-→ reliable: resync ghost to live, then proceed as before. - Ctrl-→ unreliable: skip the resync, derive the shrink baseline from fullSuggestion - current-ghost-tail so the next-word logic still works off whatever the ghost was actually showing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): hide ghost on single-byte cursor/recall control chars Reviewer caught that Ctrl-P / Ctrl-N / Ctrl-R / Ctrl-A / Ctrl-E and friends flip typedBufferReliableRef to false but don't hide the ghost — leaving it rendering a tail tied to the pre-recall line. The previous commit's unreliable-→ fallback then reads that stale tail via ghost.getGhostText() and writes it onto the recalled line, reproducing the very duplication class the fallback was meant to prevent (just triggered by Ctrl-P instead of ↑). Mirror what the escape-sequence branch already does: clearState() + return. Once the ghost is hidden, ghost.isActive() is false at the → and Ctrl-→ gates, so the accept-path doesn't fire at all until a fresh fetchSuggestions re-anchors it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): drop accepted-command cache on cursor/recall keys Reviewer pointed out that the early returns in the single-byte ctrl-char and escape-sequence branches leave lastAcceptedCommandRef untouched. If the user accepts a suggestion via → and then immediately hits Ctrl-R or ↑ to pick a different command, the fast Enter path (lines ~611-612) still reads the cached accepted command and records it — logging the old suggestion instead of whichever command the reverse-search or history-recall actually ran. Null lastAcceptedCommandRef at the top of both branches (same place we hide the ghost and flip reliability off) so accept + recall + Enter records the recalled command, not the stale accept. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): also null accepted-command cache on Ctrl-C / Ctrl-U Reviewer flagged this class of bug is still reachable via Ctrl-C / Ctrl-U. The branch handling those kills the zle line, but the early return leaves lastAcceptedCommandRef pointing at a command that is no longer on the line: accept "git status" via → → Ctrl-C to abandon → type "ls" → Enter logs "git status" via the fast path instead of "ls". Same one-liner as the other early-return branches: null the cache alongside clearState(). Now the cache's lifetime truly ends at any event that invalidates the accept. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): null accepted-command cache on bracketed paste too Fifth-pass reviewer caught the last symmetric gap: the bracketed-paste branch appends pasted bytes to the buffer but leaves lastAcceptedCommandRef set. Accept "git status" via → then bracketed-paste " --short" (no embedded newline), press Enter — the fast path at line 611 still reads "git status" and logs that instead of "git status --short". Mirror the non-bracketed paste branch: null the cache before clearState() returns. All handleInput paths that extend or invalidate the line now consistently end the cache's lifetime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): predict ghost column by cell width + wrap at EOL Review caught two geometry bugs in GhostTextAddon.updatePosition that only surfaced outside the ASCII happy path: - CJK / fullwidth / emoji glyphs occupy two xterm cells but the predictor advanced by one char-length per code unit, so ghost drifted one cell left for every wide char typed and visibly overlapped the user's glyph. - When the predicted column crossed term.cols the real cursor wrapped to the next row, but the predictor just piled more pixels onto `left` — ghost walked off the right edge instead of following onto the next line. Fix both by switching from code-unit count to a small EAW-style width classifier, then applying row wrapping via col = (anchorX + cellDelta) % cols rowOffset = Math.floor((anchorX + cellDelta) / cols) against the current term.cols. Fake terminal in the test suite now exposes cols/rows so the unit tests can exercise both invariants: - "advances the anchor by two cells when a CJK glyph is typed" - "wraps the ghost to the next row when the predicted column crosses cols" Known limitation the review already flagged: on backspace-after-wide we don't have per-grapheme widths to reverse exactly, so the negative delta falls back to code-unit width on the deleted slice. The slice is `currentSuggestion[currentInput.length..anchorInputLength]` which is the same text the user would have typed, so it's correct when only ASCII edits; wide-char backspace can still drift by one cell. Fixing this cleanly needs a per-grapheme buffer and is out of scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(autocomplete): honor showGhostText toggle while a ghost is on screen Codex flagged (#815 P2) that fetchSuggestions gates new ghost shows on settingsRef.current.showGhostText, but handleInput's adjustToInput call had no such guard. A ghost that was already active at the moment the user turned showGhostText off would keep tracking the typed buffer via adjustToInput on every keystroke, so the "disabled" setting only took hold after some unrelated path called clearState(). Two-part fix: - Add a useEffect watching settings.showGhostText. When it flips false, hide the active ghost immediately so the disabled setting applies to whatever was already on screen. - Gate the adjustToInput call in handleInput behind settingsRef.current.showGhostText too, so subsequent keystrokes under the disabled setting don't try to move or re-show a ghost. Codex's earlier P2 about wrap-at-EOL on line 236 is already resolved by |