Give the mouse back, retire the follow controls, update Claude per session #54

Merged
jknapp merged 3 commits from feat/mouse-release-retire-follow-update into main 2026-09-08 23:43:35 +00:00
Owner

Three terminal fixes, discussed and designed before implementation.

A program that grabs the mouse and dies used to freeze the tab

A TUI sets DECSET ?1000/?1002/?1003; if it exits without resetting them, xterm keeps routing clicks, drags and — under ?1003 — every pointer move to the PTY. Text selection dies and escape bytes flood the prompt. The only way out was closing the tab.

TerminalView now reconciles a flag against term.modes.mouseTrackingMode in the term.write() callback — the mode only changes because the container printed a sequence, so one check per write catches every transition with no polling. Ctrl+Shift+X, or a status-bar button, writes the resets back through term.write and never sendInput: the reset belongs to xterm's parser, and a still-live TUI told about it would just re-grab on its next repaint.

The control is in the status bar deliberately. Mouse tracking is the normal steady state of htop, vim, lazygit and Claude Code, so a badge painted over the terminal would be on screen for the whole life of those programs and would swallow clicks aimed at their own top-right corner, silently killing their mouse with no undo.

macOptionClickForcesSelection is also enabled now. xterm's force-select is Shift everywhere except macOS, where it is Option and gated behind that option, which defaults to false — so until this commit Mac users had no way at all to select text while a program held the mouse.

"Following" and "Jump to Current" are retired

Claude Code draws on the alternate screen, which has no scrollback, so viewportY always equalled baseY, isAtBottom was permanently true, and neither control could do anything — which is what made them look broken.

They did still work in bash tabs, on the normal buffer. Removing them is a real behaviour change there; the justification is that xterm's native follow already covers it, not that nothing was lost. The per-write scrollToBottom() went with them because it fought exactly that native behaviour. What remains — on activate and after a refit — now samples viewportY >= baseY before the fit(), so opening the Notes dock or dragging the sidebar no longer yanks a reader to the tail.

claude update runs before every Claude session

Containers here stop/start and often just keep running, so a long-lived one never re-checked between restarts. The entrypoint's copy stays for cold starts.

Both copies take the same flock on /tmp/.triple-c-claude-update.lock. That lock is load-bearing: the entrypoint prints "container ready" only after its own update finishes, so starting a project and immediately opening a tab would otherwise run two updaters against the same ~/.claude/bin, and || echo would hide a half-written install one line before exec claude ran it. -E 0 makes losing the race a success, since the holder just did the work.

This turns the non-Bedrock path from a bare argv into a bash -c wrapper, so flags and session names are shell-interpolated now and must go through shell_quote_arg. Bash sessions are untouched.

Verification

  • Frontend: 756/756 tests across 62 files, tsc --noEmit clean, npm run build OK
  • Rust: 545 + 3 tests pass, cargo check clean
  • bash -n container/entrypoint.sh OK

Worth reviewer attention

  • The flock behaviour on a genuine cold container start has not been exercised against a real container, only reasoned about and syntax-checked.
  • Whether the status-bar button reads as noise during normal Claude use is a judgment call; the docs say outright that seeing it is normal inside a TUI.
  • The drop-overlay regression test lost its fixture (it used the always-rendered Following toggle) and now uses the URL toast — real chrome painted over the pane, which is the property actually under test.

🤖 Generated with Claude Code

https://claude.ai/code/session_0145mQi9NZiCDrznBUEEDE4n

Three terminal fixes, discussed and designed before implementation. ### A program that grabs the mouse and dies used to freeze the tab A TUI sets DECSET `?1000`/`?1002`/`?1003`; if it exits without resetting them, xterm keeps routing clicks, drags and — under `?1003` — every pointer *move* to the PTY. Text selection dies and escape bytes flood the prompt. The only way out was closing the tab. `TerminalView` now reconciles a flag against `term.modes.mouseTrackingMode` **in the `term.write()` callback** — the mode only changes because the container printed a sequence, so one check per write catches every transition with no polling. `Ctrl+Shift+X`, or a status-bar button, writes the resets back through `term.write` and **never `sendInput`**: the reset belongs to xterm's parser, and a still-live TUI told about it would just re-grab on its next repaint. The control is in the status bar deliberately. Mouse tracking is the *normal* steady state of htop, vim, lazygit and Claude Code, so a badge painted over the terminal would be on screen for the whole life of those programs and would swallow clicks aimed at their own top-right corner, silently killing their mouse with no undo. `macOptionClickForcesSelection` is also enabled now. xterm's force-select is Shift everywhere except macOS, where it is Option and gated behind that option, which defaults to `false` — so until this commit Mac users had no way at all to select text while a program held the mouse. ### "Following" and "Jump to Current" are retired Claude Code draws on the alternate screen, which has no scrollback, so `viewportY` always equalled `baseY`, `isAtBottom` was permanently true, and neither control could do anything — which is what made them look broken. They **did** still work in `bash` tabs, on the normal buffer. Removing them is a real behaviour change there; the justification is that xterm's native follow already covers it, not that nothing was lost. The per-write `scrollToBottom()` went with them because it fought exactly that native behaviour. What remains — on activate and after a refit — now samples `viewportY >= baseY` *before* the `fit()`, so opening the Notes dock or dragging the sidebar no longer yanks a reader to the tail. ### `claude update` runs before every Claude session Containers here stop/start and often just keep running, so a long-lived one never re-checked between restarts. The entrypoint's copy stays for cold starts. Both copies take the same `flock` on `/tmp/.triple-c-claude-update.lock`. That lock is load-bearing: the entrypoint prints "container ready" only *after* its own update finishes, so starting a project and immediately opening a tab would otherwise run two updaters against the same `~/.claude/bin`, and `|| echo` would hide a half-written install one line before `exec claude` ran it. `-E 0` makes losing the race a success, since the holder just did the work. This turns the non-Bedrock path from a bare argv into a `bash -c` wrapper, so flags and session names are shell-interpolated now and must go through `shell_quote_arg`. Bash sessions are untouched. ### Verification - Frontend: 756/756 tests across 62 files, `tsc --noEmit` clean, `npm run build` OK - Rust: 545 + 3 tests pass, `cargo check` clean - `bash -n container/entrypoint.sh` OK ### Worth reviewer attention - The `flock` behaviour on a genuine cold container start has not been exercised against a real container, only reasoned about and syntax-checked. - Whether the status-bar button reads as noise during normal Claude use is a judgment call; the docs say outright that seeing it is normal inside a TUI. - The drop-overlay regression test lost its fixture (it used the always-rendered Following toggle) and now uses the URL toast — real chrome painted over the pane, which is the property actually under test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0145mQi9NZiCDrznBUEEDE4n
jknapp added 1 commit 2026-09-08 18:33:01 +00:00
Give the mouse back, retire the follow controls, update Claude per session
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m44s
Build App (Preview) / build-linux (pull_request) Successful in 5m8s
Build App (Preview) / build-windows (pull_request) Successful in 6m28s
Build App (Preview) / prune-previews (pull_request) Successful in 2s
Build Container / build-container (pull_request) Failing after 14m49s
c0e4c87cec
Three things the terminal was getting wrong.

**A program that grabs the mouse and dies used to freeze the tab.** A TUI sets
DECSET ?1000/?1002/?1003; if it exits without resetting them, xterm keeps
routing clicks, drags and — under ?1003 — every pointer *move* to the PTY.
Text selection dies and escape bytes flood the prompt. The only exit was
closing the tab. `TerminalView` now reconciles a flag against
`term.modes.mouseTrackingMode` in the `term.write()` callback — the mode only
changes because the container printed a sequence, so one check per write
catches every transition with no polling — and `Ctrl+Shift+X` or a status-bar
button writes the resets back through `term.write`, never `sendInput`: the
reset belongs to xterm's parser, and a still-live TUI told about it would just
re-grab on its next repaint.

The control is in the status bar deliberately. Mouse tracking is the *normal*
state of htop, vim, lazygit and Claude Code, so a badge over the terminal
would be on screen for the whole life of those programs and would swallow
clicks aimed at their own top-right corner. `macOptionClickForcesSelection` is
also on now: xterm's force-select is Shift everywhere except macOS, where it
is Option and is gated behind that option, which defaults to false — so until
now Mac users had no way to select text while a program held the mouse.

**"Following" and "Jump to Current" are gone.** Claude Code draws on the
alternate screen, which has no scrollback, so `viewportY` always equalled
`baseY` and neither control could do anything. They did still work in bash
tabs; xterm's native follow covers that, and the per-write `scrollToBottom()`
went with them because it fought exactly that. What remains, on activate and
after a refit, now samples `viewportY >= baseY` *before* the fit, so opening
the Notes dock no longer yanks a reader to the tail.

**`claude update` runs before every Claude session, not just at container
start.** Containers here stop/start and often just keep running, so a
long-lived one never re-checked. Both copies take the same flock: the
entrypoint prints "container ready" only after its own update finishes, so
opening a tab immediately would otherwise run two updaters against the same
~/.claude/bin, with `|| echo` hiding a half-written install one line before
`exec claude` ran it.

This turns the non-Bedrock path from a bare argv into a `bash -c` wrapper, so
flags and session names are shell-interpolated now and must go through
`shell_quote_arg`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145mQi9NZiCDrznBUEEDE4n
jknapp added 1 commit 2026-09-08 19:48:43 +00:00
Never fail a container build because the cache was unreachable
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m41s
Build App (Preview) / build-windows (pull_request) Successful in 4m52s
Build App (Preview) / build-linux (pull_request) Successful in 8m0s
Build App (Preview) / prune-previews (pull_request) Successful in 2s
Build Container / build-container (pull_request) Successful in 10m21s
c02c02cbfc
Every layer of both architectures built. The job then died exporting to
act_runner's emulated GitHub Actions cache service, which it could not route
to: `GetCacheEntryDownloadURL ... dial tcp 192.168.1.126:40649: no route to
host`.

On a pull_request `push:` is false, so this job pushes nothing and the cache
is its only output — which means a network problem between the buildx
`docker-container` builder and the runner host threw away a complete,
successful validation of the Dockerfile on linux/amd64 and linux/arm64. A
cache is an optimisation; it must degrade to "slow", never to "red".

Only the exporter needs the flag. The import is already non-fatal — the build
ran all 37 layers after warning it could not read the cache.

This does not fix the routing itself, so builds stay uncached until that is
sorted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145mQi9NZiCDrznBUEEDE4n
jknapp added 1 commit 2026-09-08 22:30:31 +00:00
Give BuildKit the host's network, so it can reach the runner's cache
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 6s
Secret Scan / scan (pull_request) Successful in 6s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m45s
Build App (Preview) / build-windows (pull_request) Successful in 4m45s
Build App (Preview) / build-linux (pull_request) Successful in 7m38s
Build App (Preview) / prune-previews (pull_request) Successful in 4s
Build Container / build-container (pull_request) Successful in 14m48s
5d16b5713d
The multi-arch build needs the `docker-container` driver — the plain `docker`
driver cannot do linux/amd64+linux/arm64 — and that driver runs BuildKit in
its own container on Docker's default bridge. act_runner advertises
ACTIONS_CACHE_URL as an address the *job* container can reach, and nothing
teaches the BuildKit container about it. So the job could reach
192.168.1.126:40649 while the container actually making the cache request
could not.

That is also why no other workflow here hit this: it is the only one using
buildx. The rest make their cache calls from the job container act_runner set
up.

`no route to host` is EHOSTUNREACH — a firewall rejecting, not a missing route
— which is what a default firewalld zone does to traffic from the docker
bridge, and the runner registers under the stock RHEL/Fedora hostname.
Sharing the host's namespace sidesteps it: the cache address becomes local to
BuildKit. No effect on runners where this already worked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145mQi9NZiCDrznBUEEDE4n
jknapp merged commit 37bbf181c9 into main 2026-09-08 23:43:35 +00:00
jknapp deleted branch feat/mouse-release-retire-follow-update 2026-09-08 23:43:38 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: CyberCoveLLC/Triple-C#54