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

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
This commit is contained in:
2026-09-08 11:02:33 -07:00
co-authored by Claude Opus 5
parent 3aec2998d8
commit c0e4c87cec
14 changed files with 518 additions and 207 deletions
+20
View File
@@ -413,6 +413,26 @@ container is created once by a very long function where a dropped capability is
existing toggle: the label fingerprints *the setting*, not the set of things the setting drives,
so a project already at `true` gets no recreation at all on upgrade.
### Keeping Claude Code current
`claude update` runs in **two** places, and both are needed:
- `container/entrypoint.sh` runs it once per container start, before any session exists.
- `commands/terminal_commands.rs` (and its twin in `web_terminal/ws_handler.rs`) prepend it to the
command every Claude session launches with, because containers use a stop/start model and a
long-lived one would otherwise never re-check.
Both are `timeout`-bounded and `|| echo`'d, so an offline or slow network delays a tab rather than
failing it, and **both take the same `flock` on `/tmp/.triple-c-claude-update.lock`**. That lock is
not tidiness: the entrypoint prints "container ready" only after its own update finishes, so
starting a project and immediately opening a tab — or opening two tabs at once — otherwise runs two
updaters against the same `~/.claude/bin`, and `|| echo` would hide a half-written install behind a
friendly message one line before `exec claude` ran it. `-E 0` makes losing the race a success,
because the holder just did the work. The per-session copy is what forced the non-Bedrock path from a bare `["claude", ...]`
argv into a `bash -c` wrapper — the flags and the session name are interpolated into a shell
string now, so **anything added there must go through `shell_quote_arg`**. Bash sessions are
deliberately untouched.
### Container Lifecycle
Containers use a **stop/start** model (not create/destroy). Installed packages persist across stops. The `.claude` config dir uses a named Docker volume (`triple-c-claude-config-{projectId}`), nested inside the home volume (`triple-c-home-{projectId}`), so OAuth tokens and Claude Code config survive container stop/start *and* container recreation.