feat(llm,proxy): cursor restore lands on prompt-end col + DSR at key moments#102
Merged
Conversation
…moments PR 3 of 3 — consumes the col-tracking + DSR plumbing from PRs #100 / #101 to fix the user-reported "Ctrl+Up lands at col 1 not after PS1" bug AND ground-truth the tracker at sensitive points. ## Ctrl+Up column fix Snapshot now captures (row, col) on inline-panel open via the new `ctx.cursor_col` plumb. `inlineRestoreRow` renamed to `inlineRestorePos` returning both — paint CUPs `\x1B[<r>;<c>H` instead of `\x1B[<r>;1H`. Col-snapshot 0 falls back to col 1 defensively (matches prior behaviour for non-TTY tests). Mechanics: - `module.zig` — adds `Context.cursor_col` (companion to `cursor_row`). - `proxy.zig` — initial assign + the two refresh sites mirror the row plumb: `cursor_tracker.currentCol()`. - `modules/llm.zig` — adds `chat_open_cursor_col: u16 = 0` field. - `modules/llm/hooks.zig` — captures col alongside row on toggle. - `modules/llm/paint.zig` — restore helper returns `struct { row, col }`; both CUP call sites updated. ## DSR-6n at key moments `proxy.zig` now fires `\x1B[6n` at: - Inline-panel reservation grow/shrink edge (where the previous layout chaos lived) — `applyReserveRows` block writes DSR after the ghost-clear, before the reservation transition. The reply lands on stdin and updates `cursor_tracker` via the existing filter (PR #101). - OSC 133 `;D` (`cmd_end`) — command finished, its output may have scrolled the cursor unpredictably. Re-anchor before the next sensitive op. DSR-6n is a write-only, asynchronous fire-and-forget — the reply arrives on the next stdin tick via the filter and updates the tracker. No sync wait, no latency. ## Tests 620 pass total (2 new pin the col snapshot + restore: `(row, col) snapshot` test asserts `\x1B[8;4H` appears, `\x1B[8;1H` doesn't; the col-snapshot 0 fallback test confirms defensive col=1 path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Subagent review caught that the reservation-transition DSR fires AFTER the LLM module already captured the (row, col) snapshot at toggle dispatch — so the reply doesn't refresh THIS open's restore target. It's a downstream consumer resync (next paint, next prompt anchor) and the comment now says so. Also moves the DSR write to AFTER `applyReserveRows` (its actual purpose is to re-anchor after the DECSTBM + per-row erase sequence). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
Author
|
Subagent review complete — 2 rounds at the latest commit. Ready for human merge. Rounds:
Test plan:
The user-facing fix (Ctrl+Up parks cursor AFTER PS1, not col 1) is verified by the new tests; DSR resyncs are sub-perceptual best-effort improvements that don't have unit tests (would need an integration test with a fake terminal that replies to DSR — reasonable to defer). 🤖 Generated with Claude Code |
fentas
added a commit
that referenced
this pull request
May 18, 2026
…or (#103) * fix(proxy): scroll shell content up when inline panel grows past cursor Reported repro: prompt at the bottom row, Alt+C → panel opens but the prompt stays where it was, getting over-painted by the panel chrome. Ctrl+Up then parks the cursor invisibly inside the panel. Different from the earlier (reverted) attempt — this one runs ONLY when the cursor_tracker reports the cursor would land in the new reservation zone (`cur_row > new_bottom`), and uses the now- reliable column from the cursor-tracking series (#100/#101/#102) to CUP the cursor to the prompt's actual position (post-PS1) after the scroll. Mechanics: 1. Compute `new_bottom = sb.rows - want_reserve` (last row of the shrunken shell area). 2. If `cur_row > new_bottom`, emit `\n` × (cur_row - new_bottom) at the cursor's current row. Each LF at the bottom of the active DECSTBM region scrolls content up by 1. 3. CUP to (new_bottom, cur_col) so subsequent shell output continues from the prompt's relocated row + the column the user's typing was about to land at. 4. `applyReserveRows` follows immediately, shrinking DECSTBM and blanking the new reservation rows. 5. The DSR-6n fired afterwards re-anchors `cursor_tracker` once the terminal reports the actual post-scroll position. Bash never sees the `\n` bytes — they go to STDOUT, not pty.master. The shell's internal cursor model is unchanged; only the visible screen scrolls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(proxy): drop redundant SIGWINCH on reservation toggle — it undoes the scroll-up Cursor stays at the old row after the panel-grow scroll-up because the SIGWINCH sent right after triggers bash's readline redraw — and readline doesn't know we moved the visual prompt. It re-emits the prompt at bash's INTERNAL cursor row, which is unchanged. The SIGWINCH was a no-op for actual size: we pass the full terminal dimensions (DECSTBM clipping is atty-side), so the slave TIOCGWINSZ delta is zero. Real resize events are handled by the SIGWINCH-handler path that listens on the proxy's own signal pipe, so dropping the spurious bounce here doesn't lose anything. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(llm,proxy): defer chat snapshot to first paint + propagate post-scroll cursor The reservation-grow scroll-up moves the prompt UP but the LLM module's inline-panel snapshot was captured at toggle-action time — BEFORE the scroll. The panel's restore CUP then landed on the pre-scroll row, which is now inside the new panel zone, and bash's next prompt redraw chased the cursor up — the prompt visibly walked UP on every redraw. Two coordinated changes: 1. Proxy: after emitting the scroll-up sequence, set `cursor_tracker.setPosition(new_bottom, cur_col)` and refresh `ctx.cursor_row/col` so the upcoming paint sees post-scroll state without waiting for the DSR-6n reply (which arrives next tick). 2. LLM module: toggle resets the snapshot to 0 (defer marker) instead of eagerly capturing; the open path of `paintInlineChat` captures from `ctx.cursor_row/col` on the first paint. By then the proxy has propagated post-scroll position, so the restore CUP lands on the panel's actual top edge. Tests in `paint_tests.zig` updated to drive a paint between toggle and snapshot assertions — the deferred-capture semantics are explicit in the assertion sequence now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(llm): inline_chat_top_gap — blank spacer row between prompt and panel The inline panel divider used to sit directly against the bottom shell row. With cursor restore now landing correctly on the prompt, the prompt visually butted right up against the chat chrome — cramped, no breathing room. New `Config.inline_chat_top_gap` (default 1) reserves N extra rows on top of `inline_chat_rows` and the panel paint leaves them blank. At default the layout becomes: prompt row, 1 blank gap row, divider, scrollback, input row, statusbar. Set to 0 for the old flush layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(proxy): scroll-up LFs must be emitted at region bottom, not cur_row LFs only trigger DECSTBM scroll-up when emitted AT the bottom of the scroll region — at any other row they just advance the cursor. The scroll-up code was CUPing to `cur_row` first, so when the cursor sat one row above the region bottom (e.g. after 3 empty Enters from a post-chat-close prompt) the first LF only moved the cursor down instead of scrolling, and we lost one scroll. Result: the prompt landed one row higher than intended — an extra blank between the prompt and the panel's top spacer. CUP to `sb.effectiveRows()` (current region bottom) before emitting the LF run; every LF then scrolls and `scroll_n = cur_row - new_bottom` lands the prompt exactly on `new_bottom`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(llm,proxy): guard tiny-panel underflow; document scroll-up→DECRC contract Two findings from subagent review of PR #103: 1. Underflow guard. paintInlineChat computed `scrollback_rows = panel_rows - 2` later in the function; if a proxy clamp landed `panel_rows` on 1 (e.g. `inline_chat_rows=3`, top_gap=1, terminal small enough that `live_reserve` got clamped one row above `base + 3`), the u16 wrapped to ~65k and the per-row blank loop overflowed the small chat paint buffer — surfaced as "terminal too small" with the panel rolling back. The early-out at the top of the function now also bails when `live_reserve - base_reserve < top_gap + 3` so the subtraction below is provably safe. 2. Ordering contract comment. The scroll-up's final CUP at (new_bottom, cur_col) is what `applyReserveRows`'s DECSC will snapshot a few bytes later; its DECRC restores cursor there once the new reservation + erase have run. If `applyReserveRows` ever moves its DECSC earlier the panel paint's snapshot would drift. Documented inline so the dependency is visible at the call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR 3/3 of the cursor-position-tracking work. Consumes the foundations from #100 (col tracking + OSC 133 anchors) and #101 (DSR-6n parser) to fix the user-reported Ctrl+Up column bug and ground-truth the tracker at sensitive moments.
Ctrl+Up column fix
The snapshot captured on inline-panel open now includes `col` alongside `row`. `inlineRestoreRow` → `inlineRestorePos` returning both. Paint CUPs `\x1B[;H` instead of `\x1B[;1H`. Col snapshot of 0 falls back to col 1 (defensive — matches prior behaviour for non-TTY test fixtures).
Mechanics:
DSR-6n firings
`proxy.zig` now writes `\x1B[6n` at:
Fire-and-forget — reply lands async; no sync wait, no latency.
Stacks on master directly
PRs #100 + #101 are merged, so this branches off the new master.
Test plan
🤖 Generated with Claude Code