fix(llm): inline-chat paint CUP-restores cursor to shell row#79
Conversation
The Alt+C open-paint left the real terminal cursor parked at the chat input row (right after the block-cursor glyph), so bash's echo of any subsequent byte — typed char, prompt redraw, exec injection — landed AT the chat input row. Observable as "command shows up in the chat panel AND in the shell prompt" and "Alt+C, Alt+C, Enter moves the prompt down to where the chat input was". Why the previous DECSC/DECRC pair didn't catch this: - The open-paint emits DECSC (`\x1B[s`) at its start. - The proxy's `applyReserveRows` then runs its OWN DECSC/DECRC pair around the row-erase, OVERWRITING the open-time slot. - After applyReserveRows, the slot points at wherever the chat panel last left the cursor — i.e. the input row — not the shell prompt. Fix: - Runtime.chat_open_cursor_row: snapshot of `ctx.cursor_row` taken when the toggle opens the panel. - inlineRestoreRow helper: returns the snapshot or shell_bottom (last row of the scroll region) as a clamped fallback. - paintInlineChat OPEN + CLOSE paths both end with an explicit CUP `\x1B[<row>;1H` to the helper's row — survives any DECSC overwrites upstream of us. Tests: - Existing toggle test now also pins the open-paint's CUP-to-row-21 fallback (terminal_rows=24, base_reserve=3, no cursor snapshot). - New test: `ctx.cursor_row=8` → open + close both CUP to row 8. - New test: bogus snapshot row 23 (inside reservation) clamps to 21. 514/514 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes the "Alt+C → Alt+C → Enter moves the prompt down" bug by ensuring the inline chat paint always ends with an explicit CUP back to the shell prompt row, rather than leaving the real terminal cursor at the chat input row where bare DECRC could be invalidated by the proxy's applyReserveRows DECSC/DECRC pair.
Changes:
- Add
Runtime.chat_open_cursor_rowsnapshot captured fromctx.cursor_rowwhen Alt+C opens the panel. - Introduce
inlineRestoreRowhelper with defensive clamp toshell_bottom; emit explicit CUP at end of both open and close paint paths. - Extend toggle paint test and add two new tests (snapshot-based restore + clamp-on-overshoot).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… test - Reframe `chat_open_cursor_row` doc + paint comments as state invariants, not historical fix narrative (CLAUDE.md "WHY only, never reference the current task/fix/callers"). - Add test pinning that re-open with `ctx.cursor_row = null` falls back to shell_bottom (not the stale snapshot from the previous open). The open branch already does `orelse 0` — this just locks the invariant in. - 515/515 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- chat_open_cursor_row doc: dropped misleading "Reset on close" line (the field is intentionally preserved through close so the close paint can target it). Reframe as "Intentionally preserved through close; the next open overwrites unconditionally." - inlineRestoreRow helper: add a one-line note on why it anchors to `total_rows - base_reserve` (stable across panel open/close) — DO NOT switch to live_reserve. - New test: re-open at a DIFFERENT non-null cursor_row overwrites the previous snapshot and the paint uses the new row. Symmetric to the null-cursor_row test. - New test: live ctx.cursor_row drift between paints does NOT shift the closing CUP — uses std.mem.endsWith to check the final CUP precisely (an internal panel CUP can incidentally hit any row inside the reservation; only the trailing bytes matter). - 517/517 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The field doc + helper doc already explain why DECSC is unreliable upstream. The paint-site blocks were paraphrasing the same story; collapse them to one-liners pointing at inlineRestoreRow so the canonical explanation lives in one place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Review loop complete — 3 rounds (Copilot + subagent, parallel). Round 1 (subagent): comment scrub for CLAUDE.md "WHY only", added re-open-with-null test, deferred SIGWINCH staleness (rare, clamp covers common case). Copilot: 0 comments. Stopping condition met (zero new Copilot comments for 3 consecutive rounds, subagent ship verdict). Final state: 517/517 tests pass, 5 new tests pin the snapshot invariants. Manual verification items added to issue #80. Ready for squash-merge. |
Summary
Fixes the user-reported "Alt+C → Alt+C → Enter moves the prompt down to where the chat input was" bug. The Alt+C open-paint left the real terminal cursor parked at the chat input row, so bash's echo of any subsequent byte landed AT the input row instead of the shell prompt.
Why the previous DECSC/DECRC pair didn't catch this
\x1B[s) at its start.applyReserveRowsthen runs its OWN DECSC/DECRC pair around the row-erase, OVERWRITING the open-time slot.applyReserveRows, bare DECRC restores to the cursor's position WHENapplyReserveRowsran — wherever the chat panel last left it (the input row), NOT the shell prompt.PR #78 fixed the close path with CUP to
shell_bottom. This PR fixes the missing other half: the open path's CUP-restore.Approach
Runtime.chat_open_cursor_row: snapshot ofctx.cursor_rowtaken when the toggle OPENS the panel.inlineRestoreRow(rt, total_rows, base_reserve)helper: returns the snapshot, falling back toshell_bottom(last row of the scroll region) when the snapshot is unknown or overshoots the shell area (defensive againstcursor_trackerdrift / SIGWINCH races).paintInlineChatOPEN + CLOSE paths both end with an explicit CUP\x1B[<row>;1H— survives any DECSC overwrites upstream.Test plan
zig build test -Dtarget=x86_64-linux-gnu— 514/514 passzig fmt --check src/ build.zig— cleanctx.cursor_row=8→ open + close paint both CUP to row 8 (not the fallback)🤖 Generated with Claude Code