cmux-tui: git.diff and git.status on the session host, with cmux git diff|status
Part of the cmux-next parity work listed in #16174 (comment)
The changes view in the agent pane can show only the last turn's edits, because nothing on the session host reads git. This PR adds that read. It implements the two operations agreed for the Changes view gap list (gap list), with the shapes in manaflow-ai/cmux-next-spec#2.
What it adds
A git_ops module in cmux-tui-core answers both ops. It holds no store state.
-
git.difflists one scope's changed files with their status and line counts. Scopes:uncommitted: the working tree against HEAD.unstaged: the working tree against the index.staged: the index against HEAD.committed: HEAD against its first parent.branch: the working tree against the merge base with origin's default branch (else main or master).
The working-tree scopes include untracked files. With
include_patch, each file carries its patch from the first@@line. -
git.statusreads branch, head, upstream, ahead and behind, and the base branch. -
Target. Either an absolute
path, or a workspace, screen, pane, tab or terminal selector. A selector names its current terminal, and the read uses that terminal's working directory (Surface::local_cwd). Giving both a path and a selector, or neither, isvalidation.invalid. -
CLI.
cmux git statusandcmux git diff [--scope] [--patch] [--max-patch-bytes] [--max-files] [<path>...]. The target is--path,--workspace,--screen,--pane,--tabor--terminal; without one, the CLI sends the current directory. -
Spec. Both ops are in the catalog, inventory, resource API lists and SDK manifests.
spec/cli.mddocuments the verbs.
Bounds
- Patches.
max_patch_bytes(262144 by default) cuts each patch at a line end, and one reply carries at most 8 MiB of patches. Cut files are markedpatch_truncated. - Files.
max_files(500) caps the list; the rest are counted infiles_omitted. At most 200 untracked files are line-counted; the rest are counted inuntracked_skipped. - Listings. A file listing over 8 MiB fails with
too_many_changesinstead of returning a partial list. - Patch runs. Patches run in batches of 256 paths, so a long list stays under the argument limit.
Errors
These ops use the session host's existing error codes. Everything specific is operation.failed with details.extra.code: not_a_repository, target_not_found, no_working_directory, no_base_branch, no_merge_base, too_many_changes or git_failed.
Safety
A read never runs a program that the repository or the daemon's environment chooses:
- Every
GIT_*variable is dropped. core.fsmonitoris off, and diffs run with--no-ext-diffand--no-textconv.- Every configured
filter.<driver>has itsclean,smudgeandprocessblanked andrequiredset to false. - Submodules are not entered for dirty checks.
- Pathspecs are literal, and
pathsmust stay inside the repository. - Untracked files open without following links or blocking on FIFOs.
- Each git run has a 20 s deadline and bounded output, and runs on the request's own connection thread.
The path target can read any repository the daemon's user can read, which is the same reach pane.run already has. Neither op is added to any remote relay allowlist.
Tests
git_ops/tests.rs drives the ops through handle_resource_message against real repositories, so each result is also checked against the catalog types. The tests cover:
- each scope;
- unborn HEAD and a root commit;
- renames and binaries;
- the bounds, the untracked cap and path filters;
- status with an upstream, detached HEAD and unborn HEAD;
- targets by path and by terminal and workspace selector;
- the failures;
- a repository whose filter driver would touch a marker file.
Parser unit tests cover name-status, numstat, patch splitting (including quoted and spaced names), truncation and the status headers. cli/command/git/tests.rs covers the CLI, and the CLI catalog coverage test includes both ops.
Commits
- Red: 8bcacac (wiring, a stub that refuses both ops, and the integration tests) plus 9061041 (format only). On hosted run 36983884479, all 11 integration tests fail on their assertions on Linux and macOS.
- Fix: 906d97d. Hosted full run: 36985941799.
Rust was built and tested only through the hosted verification, not on a local machine.
Not in this PR: the Last turn scope needs a turn boundary owned by the session host, and the agent pane's mock and page still use the old git.scope.diff name. Both follow in their own PRs.
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.
Summary by cubic
Adds git.diff and git.status as read operations answered by a new, stateless git_ops module on the session host, so the agent pane's changes view can show live repository state instead of just the last turn's edits. Adds a curated cmux git status|diff CLI that exposes both ops as read-only MCP tools as well, and fits the merged base's god-file budgets by moving the router tests, CLI transport coverage, help texts, flag table, wire-name tests, and mouse parameter rules into their own files.
Bounds and safety
- Each scope's file list, line counts, and optional patches are bounded by configurable
max_files,max_patch_bytes, and an 8 MiB reply budget; oversized listings fail withtoo_many_changes. - Reads run with no ambient
GIT_*environment, no fsmonitor, external diff, textconv, or configured filter drivers, literal pathspecs, a 20 s deadline, and bounded output, and never enter submodules. - The
pathtarget can read any repository the daemon user can read, matchingpane.run's reach, and neither op is in any remote relay allowlist.
Not in this PR: the Last turn scope, which needs a session-host-owned turn boundary, and the agent pane's mock and page still using the old git.scope.diff name.