Remote agent status + git/PR in the sidebar for cmux ssh-tmux mirrors

MX
1mo ago
+1754-0

Draft β€” remote agent status + git/PR for cmux ssh-tmux mirrors. Builds on the merged remote-tmux work; opt-in behind the existing Remote tmux beta.

What you get

When you attach a remote session with cmux ssh-tmux <host> and run Claude Code or Codex in a mirrored pane, the left sidebar now shows the agent's live status, the git branch, and a clickable PR link β€” the same rows a local workspace shows, for an agent running on the remote host.

In the mirror workspace's sidebar row you see, for an agent + repo that live entirely on the remote host:

my-workspace
  🟦 Claude Code idle
  βŽ‡  <branch>  Β·  /path/to/repo
  ⇄  <owner>/<repo> #<n>  open      ← clickable, opens the PR

Why this was missing β€” local vs remote

The sidebar's agent/branch/PR rows are driven by three local producers, and all three are structurally blind to a remote agent:

ProducerHow it works locallyWhy it fails for an ssh-tmux mirror
Agent status (SharedLiveAgentIndex + tab.statusEntries)a hook injected into the locally-launched agent reports over the cmux control socket; liveness gated on a local PIDthe agent runs on the SSH host β€” not in the local process table, and the mirror's panes have no local PTY
Git branch (CmuxSidebarGit poller)runs git in the workspace's local working dirguard isRemoteWorkspace == false else { return } β€” the poller bails; and the repo is on the remote anyway
PR (CmuxGit poller)local git remote slug β†’ GitHub API / ghsame: no local repo to resolve a slug from

So everything assumed local repo + local cmux-instrumented shell. A mirror has neither β€” which is exactly why this needed a new channel.

How it works under the hood

The insight: the mirror already holds a live tmux -CC control stream to the host, and tmux can subscribe a client to a user-option value (refresh-client -B). So the remote agent's own lifecycle hook publishes status into a tmux user option, and cmux receives it as a %subscription-changed line on the stream it's already reading. No remote cmux CLI, no socket, no reverse relay, no auth.

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ your Mac (cmux) ─────────────────────┐
  β”‚  sidebar rows  ◀─ workspace.statusEntries / gitBranch /   β”‚
  β”‚                     pullRequest                           β”‚
  β”‚        β–²                                                  β”‚
  β”‚  RemoteTmuxSessionMirror ── parse @cmux_agent / @cmux_git β”‚
  β”‚        β–²                                                  β”‚
  β”‚  RemoteTmuxControlConnection  (subscribes #{@cmux_agent}, β”‚
  β”‚        β”‚                       #{@cmux_git} per pane)     β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚  %subscription-changed  (live tmux -CC stream)
  ═════════β•ͺ═══════════ SSH ControlMaster ════════════════════
           β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ remote host ─────────────────────────┐
  β”‚  agent lifecycle hook (installed by cmux on attach):      β”‚
  │    SessionStart→running  UserPromptSubmit→working         │
  │    Stop→idle                                              │
  β”‚      └─ tmux set -t "$TMUX_PANE" @cmux_agent '{…}'         β”‚
  β”‚      └─ ( git rev-parse / git diff / gh pr view           β”‚
  β”‚           β†’ tmux set @cmux_git '{branch,dirty,pr}' ) &     β”‚
  β”‚  claude / codex  ── the real agent                        β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

On attach (RemoteTmuxController.installRemoteAgentStatusHooks, once per host, fire-and-forget): cmux writes a dependency-free hook into the remote ~/.claude/settings.json and ~/.codex/hooks.json over the open ControlMaster (a python3 merger that preserves your other hooks and replaces only cmux-marked ones).

On each agent event the hook (running in the agent's pane + cwd):

  • writes @cmux_agent {agent,state,model?} synchronously (cheap), and
  • runs git/gh in a detached background subshell (so latency never delays the agent) and writes @cmux_git {branch,dirty,pr?}.

Both are scoped to $TMUX_PANE, self-disable outside tmux, and print {} for valid blocking-hook stdout.

cmux side: per-pane subscriptions #{@cmux_agent} / #{@cmux_git} (next to the existing cmux_cwd_/cmux_reflow_ ones) β†’ parsed by RemoteTmuxAgentStatus / RemoteTmuxGitStatus β†’ written to the real sidebar models (statusEntries, per-panel gitBranch/pullRequest). No renderer changes.

How this compares to the existing local path

Local agentRemote mirror (this PR)
transportcmux control socket (unix)tmux @-option over the existing -CC stream
who reportshook β†’ cmux hooks claude …hook β†’ tmux set @cmux_agent …
installPATH shim / wrapper at launchcmux writes remote settings.json/hooks.json on attach
livenesslocal PID scanthe hook's Stop event + chip-clear on teardown
deps on remoteβ€”just tmux (+ git/gh for the PR row)

It deliberately reuses the same sidebar models as local, so the rows render identically β€” it only swaps how the data crosses the boundary.

Fallbacks (when there's no hook)

For an agent that was already running before attach (its hooks aren't loaded), or a host where the hook can't be installed, cmux degrades gracefully to two coarser signals it can read without the hook:

  • Foreground command (pane_current_command β†’ "Claude Code running"), and
  • Transcript poll β€” reads the remote ~/.claude transcript mtime+model over the master for busy/idle (Claude only).

The hook path (Option C) takes precedence when present.

Proof (validated live against a real AL2023 host, tmux 3.6a)

  • tmux set @cmux_agent/@cmux_git pushes %subscription-changed cmux_agent_<pane> / cmux_git_<pane> on the control stream βœ…
  • cmux's generated install command merges cleanly into the remote settings.json βœ…
  • the installed hook, run exactly as Claude invokes it (event JSON on stdin) from a repo cwd, set:
    • @cmux_agent β†’ {"agent":"claude","state":"working","model":"claude-opus-4-8[1m]"}
    • @cmux_git β†’ {"branch":"<branch>","dirty":0,"pr":{"number":<n>,"state":"MERGED","url":"<pr-url>"}}
  • both parse to the sidebar models (model [1m] suffix stripped; PR branch-matched) βœ…
  • and the screenshot above is the rendered result.

Tests & docs

  • Pure parsers/builders fully unit-tested: RemoteTmuxAgentStatusTests, RemoteTmuxGitStatusTests, RemoteTmuxAgentHookInstallerTests, RemoteTmuxAgentProbeTests, subscription-command tests. All green; test files wired into the pbxproj.
  • DEBUG checkpoints (remote.agent.hookinstall / .sub / .hook / .sidebar, remote.git.sub / .git) for live diagnosis.
  • docs/investigations/remote-agent-status-sidebar.md (design + ranked options + empirical findings) and …-test-plan.md (manual test plan A–E + a no-app probe).

Notes / limits

  • Hooks load at agent startup β†’ only agents started after attach get the hook path; earlier ones use the fallback chip.
  • The git/PR row needs gh installed + authed on the remote and a GitHub remote; refreshes on hook events, not on an idle git checkout.
  • All user-facing strings localized (en + ja).

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Shows live remote agent status and git/PR info in the left sidebar for cmux ssh-tmux mirrors using tmux option subscriptions. Opt-in behind the existing Remote tmux beta.

  • New Features

    • Live remote agent chip (running/working/idle + model) via @cmux_agent over tmux -CC.
    • Remote git branch and clickable PR via @cmux_git (branch/dirty/PR).
    • One-time, per-host hook install into ~/.claude and ~/.codex that publishes @cmux_agent/@cmux_git; scoped to the pane and non-blocking.
    • Graceful fallbacks without hooks: foreground command detection and Claude transcript probe for busy/idle.
    • No UI changes; writes to existing sidebar models. Tests added for parsers and control parsing.
  • Migration

    • Enable the Remote tmux beta and connect with cmux ssh-tmux <host>.
    • Ensure tmux is present on the remote. For PR info, install and auth git and gh on the remote.
    • Start the agent after attach so the hook loads.

Summary by CodeRabbit

  • New Features
    • Remote tmux sessions now show supported AI agents’ running, working, or idle status in the sidebar, with model details when available.
    • Remote Git branch and pull-request information is displayed alongside session tabs.
    • Added English and Japanese translations for agent status labels.