Skip to content

fix(tui): fall back to CellMotion where AllMotion is unreliable - #872

Open
Vasanthdev2004 wants to merge 2 commits into
mainfrom
fix/870-mouse-mode-fallback
Open

fix(tui): fall back to CellMotion where AllMotion is unreliable#872
Vasanthdev2004 wants to merge 2 commits into
mainfrom
fix/870-mouse-mode-fallback

Conversation

@Vasanthdev2004

@Vasanthdev2004 Vasanthdev2004 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Fixes the scroll failure behind #870.

The problem

AllMotion (1003) buys exactly one thing over CellMotion (1002): hover highlighting. Wheel, click and drag all arrive under either. The asymmetry is in the failure: a terminal that does not implement 1003 does not fall back to 1002, it sends nothing at all. Since the TUI holds the alternate screen, the terminal's own scrollback and wheel are gone too, so the user cannot scroll by any means.

That is what #870 reported as a freeze. The screenshot shows a completed turn (Thought for 1.9s, answer rendered, composer back to its idle placeholder) with ↓ 59 more · PgDn in the footer. Nothing was hung. The output ran past the fold and no scroll input worked.

The code already conceded 1003 is fragile by carving out PRoot. The legacy Windows console is the same case and was not covered, so zero.exe run outside Windows Terminal asks for a mode its terminal never answers.

The change

mouseModeFor now makes the decision. It trusts terminals that identify themselves, WT_SESSION for Windows Terminal and TERM_PROGRAM for hosts like VS Code and mintty, and assumes an unidentified Windows terminal is the legacy console. Guessing wrong in that direction costs a hover highlight. Guessing wrong the other way costs every mouse event.

ZERO_MOUSE_MODE=all|cell|off overrides it, so anyone on a terminal we guessed wrong about can fix it without waiting for a release, and I can ask a reporter to test one specific mode. An unrecognised value is ignored rather than treated as "off", so a typo cannot silently kill the mouse.

Tests

mouse_mode_test.go covers linux, macOS, PRoot, legacy Windows console, Windows Terminal, VS Code on Windows, all three override values with case and whitespace variants, and an unknown override. Written against the old behaviour first: only legacy_Windows_console and the override cases failed, so they pin the gap rather than describe the fix.

go build ./..., GOOS=windows go build ./..., go vet and gofmt are clean.

Notes

The root cause is not confirmed with the reporter yet, since I have not heard back on which terminal they use. I would rather ship the safe default than wait, because the override gives us a one-line diagnostic to hand them either way.

Unrelated, TestAltScreenTranscriptScrollKeepsFooterFixed currently fails on pristine main at edf660a on Windows. Not touched by this PR, but somebody should look.

Summary by CodeRabbit

  • Bug Fixes
    • Improved terminal mouse handling across operating systems and environments.
    • Added compatibility for legacy Windows terminals, Windows Terminal, VS Code, and PRoot environments.
    • Added support for overriding mouse behavior through the ZERO_MOUSE_MODE setting.
    • Invalid mouse-mode overrides now safely fall back to automatic detection.
    • Improved reliability when terminal capabilities are inherited or unavailable.

AllMotion (1003) buys exactly one thing over CellMotion (1002): hover
highlighting. Wheel, click and drag all arrive under either. But a
terminal that does not implement 1003 does not degrade to 1002, it sends
nothing at all, and because the TUI holds the alternate screen the
terminal's own scrollback and wheel are gone too. The user is left
unable to scroll by any means, and a long answer then reads as a hang
rather than as a missing highlight. That is how #870 arrived: a
completed turn with "59 more" lines below the fold, reported as a
freeze.

The existing code already conceded 1003 is fragile by carving out PRoot.
The legacy Windows console is the same case and was not covered, so a
bare zero.exe outside Windows Terminal asked for a mode its terminal
does not answer.

Move the decision into mouseModeFor, which trusts terminals that
identify themselves (WT_SESSION for Windows Terminal, TERM_PROGRAM for
hosts like VS Code and mintty) and assumes an unidentified Windows
terminal is the legacy console. Guessing wrong that way costs a hover
highlight; guessing wrong the other way costs every mouse event.

ZERO_MOUSE_MODE=all|cell|off overrides it, so a user on a terminal we
guessed wrong about can fix it without waiting for a release. An
unrecognised value is ignored rather than treated as "off", so a typo
cannot silently kill the mouse.

Refs #870.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 36b9c1ea-5406-4711-8cc2-865a0c7278aa

📥 Commits

Reviewing files that changed from the base of the PR and between e4bc1f6 and 8adc625.

📒 Files selected for processing (2)
  • internal/tui/mouse.go
  • internal/tui/mouse_mode_test.go

Walkthrough

The TUI now centralizes mouse-mode selection. It supports environment overrides and selects compatible modes for PRoot, Windows terminals, and other platforms. Tests cover fallback behavior, terminal detection, override precedence, and invalid overrides.

Changes

Mouse mode selection

Layer / File(s) Summary
Centralized mouse mode detection
internal/tui/mouse.go, internal/tui/model.go
mouseModeFor handles ZERO_MOUSE_MODE overrides and selects AllMotion or CellMotion from platform, terminal, and PRoot state. The model uses this selector.
Mouse mode selection tests
internal/tui/mouse_mode_test.go
Table-driven tests cover platform fallbacks, Windows terminal detection, inherited environment behavior, override precedence, normalization, and unknown overrides.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: anandh8x, gnanam1990

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: using CellMotion when AllMotion is unreliable.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/870-mouse-mode-fallback

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 6, 2026
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

@anandh8x @gnanam1990 small one, but there is a judgement call in it rather than a correctness question, so I would like a second opinion.

The mechanical part is straightforward. AllMotion (1003) buys only hover highlighting over CellMotion, and a terminal that does not implement it sends nothing at all rather than degrading, which combined with the alt screen leaves the user no way to scroll. That is #870: a completed turn with 59 lines below the fold, reported as a freeze.

The judgement is this. I treat an unidentified Windows terminal as the legacy console and drop it to CellMotion, so a Windows user outside Windows Terminal or VS Code loses hover highlighting. That is deliberate, since the opposite mistake costs every mouse event rather than one highlight, but it is a real behaviour change on Windows and I would rather two of you agreed with the trade than just me.

Worth knowing: the reporter has not told us which terminal they use yet, so the root cause is inferred, not confirmed. ZERO_MOUSE_MODE=all|cell|off is in the PR partly so we can settle that with them without shipping a build.

One unrelated thing I tripped over: TestAltScreenTranscriptScrollKeepsFooterFixed fails on pristine main at edf660a on Windows. Nothing to do with this PR, I checked against a clean worktree, but it needs an owner.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 8adc625d1688
Changed files (3): internal/tui/model.go, internal/tui/mouse.go, internal/tui/mouse_mode_test.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The mechanical change looks sound, and I agree with the judgement call in your comment. A few follow-ups are worth tracking, but none of them overturn the trade you are asking reviewers to sign off on.

Findings

  • [P3] Inherited WT_SESSION / TERM_PROGRAM can still defeat the legacy-console fallback
    internal/tui/mouse.go:57-59, internal/tui/mouse.go:42-44
    You treat unidentified Windows consoles (no WT_SESSION, no TERM_PROGRAM) as legacy conhost and fall back to CellMotion. That trade is separate from this edge case: on Windows, child processes inherit the parent environment, so a zero.exe session attached to legacy conhost can still carry WT_SESSION from Windows Terminal or TERM_PROGRAM from Git Bash / VS Code even though the active console host is not that parent emulator. In that case windowsTerminalReportsAllMotion returns true and AllMotion is requested on a host that may drop every mouse event — the same #870 failure class. This is outside the judgement call you raised, and it is probably not the reporter's path if they double-clicked zero.exe or ran from a plain cmd window with a clean env. I would still add a regression test (goos=windows, inherited WT_SESSION or TERM_PROGRAM, want CellMotion unless you intentionally trust inheritance). If it shows up in the #870 thread, ZERO_MOUSE_MODE=cell is already the right diagnostic. Detecting the actual console host would be nicer than env markers alone, but I would not block merge on it given your diagnostic workflow.

  • [P3] Ctrl+E "re-enabled" notice ignores a ZERO_MOUSE_MODE=off override
    internal/tui/model.go:1446-1451, internal/tui/mouse.go:36-37, internal/tui/model.go:2791-2799
    With ZERO_MOUSE_MODE=off or none, mouseModeFor always returns MouseModeNone, but pressing Ctrl+E again still posts "Mouse interaction re-enabled." even though View() keeps emitting MouseModeNone. Minor UX mismatch for anyone using the override deliberately; fine to fix in a follow-up.

On the trade you asked about

You framed the real question correctly: unidentified Windows should default to CellMotion because guessing the other way costs every mouse event, not just hover. I agree with that trade for the likely #870 paths (double-click / plain cmd.exe). Losing hover on capable third-party Windows terminals that do not set WT_SESSION or TERM_PROGRAM (some Alacritty / WezTerm installs) is an acceptable cost, and ZERO_MOUSE_MODE=all is a reasonable way to confirm that with the reporter before hardening detection further.

Root cause for #870 is still inferred, as you noted. Shipping the safe default now rather than waiting for the reporter's terminal answer is reasonable; the override gives you a one-line diagnostic either way.

gnanam1990
gnanam1990 previously approved these changes Aug 7, 2026

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: Approve

Checked out and probed rather than read. The reasoning that makes this correct is the asymmetry, and the PR states it plainly: AllMotion buys one thing (hover highlighting), while a terminal that does not implement 1003 sends nothing — and because the app holds the alternate screen, the terminal's own wheel and scrollback are gone too. So the failure is not "no highlight", it is "no way to scroll at all", which reads as a hang. Trading a highlight for that is obviously right.

Additivity verified directly. I asserted mouseModeFor on darwin/linux/freebsd with no env and no PRoot: all still AllMotion. So every platform that worked before is untouched, and the change is confined to Windows-without-a-known-terminal and PRoot — the two cases that were already broken.

The risk direction is the safe one. An unidentified Windows terminal is assumed to be the legacy console host, which is exactly what a user gets double-clicking zero.exe. Guessing wrong that way costs a highlight; guessing wrong the other way costs every mouse event. Correct default for a case you cannot detect.

Edge cases I probed, all correct:

  • a typo in ZERO_MOUSE_MODE (celll, allmotion, true, 1, yes) is ignored, not read as "off" — a mistyped variable cannot silently kill the mouse, which matters for a knob whose failure mode is invisible
  • the override wins even where the heuristic would fall back (windows + PRoot + all → AllMotion), so a user on a terminal you guessed wrong about can recover without editing config
  • a blank-but-present WT_SESSION (" ") does not count as a real terminal — the TrimSpace is load-bearing, not decoration

No per-render cost: isRunningUnderPRoot is a sync.OnceValue, so hoisting it into the argument list does not add a /proc read per frame, and os.Getenv per frame is negligible.

The six-case table (linux, macOS, PRoot, legacy console, Windows Terminal, VS Code) covers the matrix, and the env-override table covers casing and whitespace. go test ./internal/tui/ green on the branch.

Non-blocking: ZERO_MOUSE_MODE is a real escape hatch for a failure the user cannot otherwise diagnose — worth a line in the docs or zero doctor output, so someone with a dead mouse can find it without reading source.

WT_SESSION and TERM_PROGRAM are inherited, so they answer for whichever
process set them rather than for the console host attached now. A shell
started from Windows Terminal or Git Bash that later runs zero.exe
against the legacy console still carries them, and mouseModeFor then
asks for AllMotion on a host that may drop every mouse event: the #870
failure class through a narrower door.

Not fixed, and deliberately so. Telling an inherited variable from a live
one needs the console host rather than the environment, and erring the
other way costs every Windows Terminal user their hover highlighting for
a case that takes an unusual launch path to reach.

So the limit is written down in the code and pinned by a test that also
asserts ZERO_MOUSE_MODE=cell still overrides it, since that override is
the only recourse an affected user has. If real host detection lands
later the test fails by design, and its message says to delete it.

Raised by @jatmn on #872.
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

@jatmn 8adc625. You are right about the mechanism, and I have not fixed it, deliberately. Here is the reasoning so you can push back if you disagree.

Telling an inherited WT_SESSION from a live one needs the console host rather than the environment. Getting that check wrong in the other direction costs every Windows Terminal user their hover highlighting, which is the common case, to protect a case that needs an unusual launch path. That trade looked worse than the one already in the PR.

So the limit is now written down where the decision is made, and TestInheritedWindowsTerminalEnvStillAsksForAllMotion pins it, including that ZERO_MOUSE_MODE=cell still overrides it, since that is the only recourse an affected user has. If someone lands real host detection the test fails by design and its message says to delete it.

If you would rather I go the other way and default Windows to CellMotion outright, accepting the lost hover everywhere, say so and I will. It is a smaller diff than host detection and it has no false-positive surface at all.

@Vasanthdev2004
Vasanthdev2004 requested a review from jatmn August 7, 2026 15:37

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approving after 8adc625d. My earlier approval was auto-dismissed by the push.

Verified the new commit changes no behavior: git show 8adc625d -- internal/tui/mouse.go is comment-only once blank and // lines are filtered out. The +13 is the KNOWN LIMIT block; the executable part is the test.

The limit itself is worth calling out for anyone reading this later, because the commit is choosing to leave a narrow instance of the bug the PR fixes. WT_SESSION/TERM_PROGRAM are inherited, so they answer for the process that set them, not for the console host attached right now — a shell launched from Windows Terminal that later runs zero.exe against the legacy console still carries them, and mouseModeFor asks for AllMotion on a host that may drop every event. That is #870 arriving by a narrower door, and the comment says so plainly rather than hoping nobody notices.

Leaving it is the right call on the trade the comment describes: deciding it properly means asking the console host instead of the environment, and erring the other way costs every Windows Terminal user their hover highlighting to cover a case that needs an unusual launch path to reach. ZERO_MOUSE_MODE=cell is a real recourse in the meantime.

TestInheritedWindowsTerminalEnvStillAsksForAllMotion passes and pins it. Worth being explicit that this test asserts behavior we know is imperfect — if the host-query fix ever lands, this test is the thing to change, not a regression to debug. The comment naming the test makes that discoverable, which is exactly what a pinned limit needs.

go test ./internal/tui/ green. Approving.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants