Skip to content

fix(terminal): stop Snacks climbing cursor on hide/show toggle (#240, #183)#271

Merged
ThomasK33 merged 3 commits into
mainfrom
worktree-issue-240-cursor-toggle
Jun 9, 2026
Merged

fix(terminal): stop Snacks climbing cursor on hide/show toggle (#240, #183)#271
ThomasK33 merged 3 commits into
mainfrom
worktree-issue-240-cursor-toggle

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

What

Fixes the "climbing cursor" in the Snacks terminal: hiding and re-showing the Claude panel left the terminal cursor one row above Claude's prompt, so typed text landed on the wrong line and the box visually corrupted.

Closes #240 (vertical split)
Closes #183 (floating window — same root cause)

Root cause

Instrumented end-to-end (real Claude CLI driven via agent-tty). The trigger is not a pty resize — there is zero SIGWINCH on toggle. Instead:

  1. Snacks hides by closing the window (nvim_win_close) and shows by recreating it (open_win).
  2. On show, Neovim sends a focus-in (ESC[I) to the child (Claude enables DECSET ?1004).
  3. Claude Code (Ink) re-renders relative to the cursor on focus-in; the recreate shifted its cursor anchor by one row, so the UI climbs.

Controls confirm the layering: focus change alone (no hide/show) does not drift, and an absolute-positioning TUI does not drift under the same churn — only Claude does, consistent with the community report that downgrading Claude to 2.0.76 makes it disappear. The native provider does not exhibit it.

The fix

Manage hide/show without destroying the window, preserving the cursor anchor:

  • Float: park via nvim_win_set_config({ hide = true/false }) (window kept alive; requires Neovim ≥ 0.10).
  • Split: can't be config-hidden, so close on hide and recreate with vsplit + nvim_win_set_buf on show — the native-provider path, which doesn't drift. Sets full height, re-applies Snacks' window-local options, and reuses the buffer so the <S-CR> newline map survives. Works on all supported Neovim versions.
  • The Snacks instance hide/show/toggle are monkeypatched so user-wired Snacks keymaps (e.g. self:hide()) also benefit.
  • is_terminal_visible() now treats a config-hidden window as not visible, so ensure_visible / diff / @-mention-send re-show a parked float.

Verification

  • Harness gate (fixtures/cursor-toggle-repro/agent-repro.sh, real Claude 2.1.x): cursor-vs-prompt delta after each toggle — snacks split 0→0 (was 0→1), snacks float 0→0 + panel truly hides, native unchanged.
  • mise run all: 494 tests / 0 failures, luacheck + treefmt clean.
  • New unit tests (tests/unit/terminal/snacks_toggle_spec.lua): float/split hide-show, config-hidden visibility, E444-safe close, externally-closed-float-reopens-as-float, open-reuse.

Review

Ran a multi-dimension adversarial review (correctness / Snacks API / regression / portability / style) over the diff; applied the confirmed findings: float-reopens-as-float, E444-safe split close, full-height parity, fixbuf-autocmd ordering, re-applying window-local options, buffer-identity check, per-branch debug logging, plus this CHANGELOG entry. Re-gated green afterwards.

Notes / scope

🤖 Generated with Claude Code

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@claude review

ThomasK33 and others added 2 commits June 9, 2026 10:57
…183)

Hiding and re-showing the Claude panel with the Snacks provider left the
terminal cursor one row above Claude's prompt, so typed text landed on the
wrong line and the prompt box corrupted. Root cause: Snacks hides by closing
the window and shows by recreating it via open_win(); that recreate shifts the
cursor anchor Claude (Ink) re-renders against on the focus-in event Neovim
sends on show. It is not a pty resize.

Manage hide/show without destroying the window:
- float: park via nvim_win_set_config({hide=true/false}) (needs nvim-0.10)
- split: close on hide, recreate with vsplit + nvim_win_set_buf on show, like
  the native provider (which does not drift); set full height, re-apply Snacks'
  window-local options, and keep the buffer so the <S-CR> map survives

Also monkeypatch the Snacks instance hide/show/toggle so user-wired Snacks
keymaps (e.g. self:hide()) get the fix, and make is_terminal_visible() treat a
config-hidden window as not visible so ensure_visible/diff/send re-show a
parked float. Splits are fixed on all supported Neovim versions; the float fix
requires nvim-0.10 (pre-0.10 floats keep prior behavior).

Adds tests/unit/terminal/snacks_toggle_spec.lua covering the float/split
hide-show paths, config-hidden visibility, E444-safe close, and the
externally-closed-float-reopens-as-float case.

Change-Id: I71c521935460fc9fec0eaa45823a2c91002b4d8d
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Self-contained agent-tty reproduction of the Snacks climbing-cursor bug:
- init.lua: minimal LazyVim-style Snacks fixture (provider/position/cmd env knobs)
- box.py: auth-free instrument that enables focus reporting and logs every input
  byte + SIGWINCH, proving the trigger is focus churn on window recreate, not a
  pty resize
- agent-repro.sh: drives a real Claude CLI and prints the cursor-vs-prompt delta
  per toggle for snacks (split & float) vs native
- README.md: verdict, root-cause chain, and measured results

Change-Id: I2596d57c56b22d937e744e6be56f42a7735666fa
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f4d156b84

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lua/claudecode/terminal/snacks.lua Outdated
@ThomasK33 ThomasK33 force-pushed the worktree-issue-240-cursor-toggle branch from 0f4d156 to b258097 Compare June 9, 2026 09:00
…eview)

cc_show recreated every split as a vsplit driven by split_side, so a
snacks_win_opts.position = "top"/"bottom" terminal silently turned into a
left/right vertical split after the first hide/show. Recreate now honors the
resolved Snacks position: left/right -> vertical split, top/bottom ->
horizontal split sized from snacks_win_opts.height; float and any other
position delegate back to Snacks (which owns that geometry). Adds
resolve_split_size and unit tests for the horizontal-split and
unsupported-position paths.

Change-Id: I2087a82e04bd56cc85977051102aa722ba67bdcf
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

Addressed the horizontal-split feedback in ce288a4 (rebased on latest main). @codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@claude review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33 ThomasK33 merged commit d462006 into main Jun 9, 2026
2 checks passed
@ThomasK33 ThomasK33 deleted the worktree-issue-240-cursor-toggle branch June 9, 2026 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant