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.mouseTrackingModein 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 >= baseYbefore 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.
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
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
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
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 main2026-09-08 23:43:35 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.TerminalViewnow reconciles a flag againstterm.modes.mouseTrackingModein theterm.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 throughterm.writeand neversendInput: 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.
macOptionClickForcesSelectionis also enabled now. xterm's force-select is Shift everywhere except macOS, where it is Option and gated behind that option, which defaults tofalse— 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
viewportYalways equalledbaseY,isAtBottomwas permanently true, and neither control could do anything — which is what made them look broken.They did still work in
bashtabs, 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-writescrollToBottom()went with them because it fought exactly that native behaviour. What remains — on activate and after a refit — now samplesviewportY >= baseYbefore thefit(), so opening the Notes dock or dragging the sidebar no longer yanks a reader to the tail.claude updateruns before every Claude sessionContainers 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
flockon/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|| echowould hide a half-written install one line beforeexec clauderan it.-E 0makes 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 -cwrapper, so flags and session names are shell-interpolated now and must go throughshell_quote_arg. Bash sessions are untouched.Verification
tsc --noEmitclean,npm run buildOKcargo checkcleanbash -n container/entrypoint.shOKWorth reviewer attention
flockbehaviour on a genuine cold container start has not been exercised against a real container, only reasoned about and syntax-checked.🤖 Generated with Claude Code
https://claude.ai/code/session_0145mQi9NZiCDrznBUEEDE4n