feat(ui): git status indicator in the bottom bar, with a panel of uncommitted and unpushed work - #537
Conversation
…ommitted and unpushed work Optional and per-device (showGitStatus, default off). GET /api/sessions/:id/git-status is read-only and offline (no fetch, --no-optional-locks), skips remote and Docker sessions, caps its lists, and single-flights concurrent polls. The toolbar indicator shows uncommitted files, commits not pushed, or a check; clicking opens a draggable panel in the style of the Files window. Which repositories: the enclosing one when there is one; otherwise every repository up to two levels below the working directory (capped, skipping dot-folders and node_modules, never following symlinks), each in a collapsible section, with the indicator summing them. A repository that merely sits above the workspace and is the home folder or higher (a dotfiles repo) is ignored. Git-supplied text is only ever written with textContent. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
Rows open an in-panel diff (staged, not staged, untracked as additions, deleted as removals) via GET /api/sessions/:id/git-diff, with Back and Open file. The route matches repo and path against the current status, runs git diff read-only (--no-ext-diff --no-textconv), and caps output at 400 KB. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
|
Added a follow-up (cd9218c): clicking a file in the Git panel now shows what changed in it (an in-panel diff with Back and Open file) instead of just opening the file. It uses a new read-only 🤖 Generated with Claude Code |
…ing, default on) The Git window shows each group's files under their folders, collapsed until clicked, with single-child folder chains merged and open folders surviving the refresh. App Settings → Bottom bar → 'Git status: group files by folder' (per device) switches back to the flat list. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
…ed repositories README, Working With Files (new Git changes section), Settings Reference, The Dashboard and the changeset. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
|
Follow-ups pushed to this PR:
Browser test file passes (13). 🤖 Generated with Claude Code |
…und-trip Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
|
Thanks @opticon454 for this. It adds an opt-in Git indicator to the bottom bar for the active session's repository, with a draggable panel of uncommitted files, unpushed commits and per-file diffs, backed by two read-only routes. The hardening is careful and the tests (real git, real Chromium) are excellent, and everything is green here. A few things need to change before merge: 1. The Docker exclusion is keyed on the session, not the path ( 2. A branch whose upstream is gone reports everything pushed ( 3. Turning the setting off during a poll leaves it dead after turning it back on ( 4. The docs overstate what the flags prevent ( 5. CLAUDE.md's frontend load order ( 6. The changeset ( Smaller things, fold them in if you like or I will pick them up at merge:
Once 1 to 6 are in, I will merge. Thanks again, this fills a real gap. |
…stream, in-flight reset, docs) - never inspect a repository at or inside a Docker case workspace (walk-up, scan, diff route): git would run its clean filters on the host - a branch whose upstream was deleted and pruned reports upstreamGone and falls back to commits on no remote, instead of green - turning the setting off during a poll releases the in-flight flag - log.showSignature=false; reword the docs: clean filters still run - CLAUDE.md frontend load order, changeset names git-diff - discovery reads a bounded, sorted directory listing; leading-dash paths allowed; diff 500 redacts credentials - keyboard focus survives the poll re-render; panel stays on screen on narrow viewports; aria-expanded visible on light skins Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
|
Thanks, all six are in (0c4bb51), plus the smaller items.
Smaller items, all done: discovery now reads a bounded (5000) listing, sorts, then takes 300; a leading Unit/route/browser tests for the module pass (88 + 16), typecheck clean. 🤖 Generated with Claude Code |
- A cached list of repositories below a folder is re-checked against the Docker case workspaces as they are now, so a repository linked as a Docker workspace within the 30 s list cache is no longer inspected. - A diff past runGit's 8 MB output bound is cut short from git's partial output instead of failing with a 500. - The browser test waits for its slow route handler on unroute (unrouteAll behavior 'wait'), so a late route.continue() cannot fail the run. - "Upstream is gone" now reads "Upstream not on remote", true for a branch that was never pushed as well as one deleted on the remote; docs mirrored. - The diff route checks the repository against the workspace's own cached repository list (findWorkspaceRepo) and refreshes only that repository, instead of a fresh status of every repository in the folder. - CLAUDE.md: a Key Patterns entry for the git read surface and its rules. - The enclosing repository is identified with one cached rev-parse before any full status, so an unrelated repository above the workspace costs one process and its failure no longer hides the repositories below. - Wiki: the bottom-bar indicator moves out of the header-controls table. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Merged in 1.35.0, thanks @opticon454! This one is going to get a lot of use, and you turned all six items from the last round around in twenty minutes. Applied on the way in (2c38e77):
|
What
An optional Git status indicator at the right of the bottom bar, for when an agent leaves work uncommitted or unpushed.
For the active session's repository it shows
● 3(uncommitted files),↑ 2(commits not pushed), or✓(everything committed and pushed): amber when there is something outstanding, red on a merge conflict, muted green when clean. Click it and a draggable window opens (same look as the Files window, default spot just to its left so both can be open) listing exactly:new ← old. Click a file to preview it (deleted files and untracked folders are not clickable).main → origin/main,↑ ahead,↓ behind, stash count) and a footer saying it is read-only.Optional and per-device: Settings → Header & Panels → Bottom bar → Git status, default off. While it is off nothing polls and the button never shows. Phones are unaffected (the toolbar's right group is already hidden there).
How it works
GET /api/sessions/:id/git-status(src/git-workspace-status.ts,routes/git-status-routes.ts). Read-only and offline: it never fetches, pulls, commits or writes.git statusruns with--no-optional-locks(it does not even refresh the index, so polling cannot contend with the agent's own git commands) andcore.fsmonitor=false. "Behind" is therefore as of the lastgit fetch, and the panel footer says so; "ahead" and the unpushed list are exact against the remote-tracking refs on disk.cwd. Lists are capped (300 files, 50 commits) while the counts stay exact. Concurrent polls of one folder share one in-flight computation and a result under 4 s old is reused;?fresh=1(the panel's Refresh button and opening the panel) skips that reuse.state: 'unsupported', no indicator): a Docker workspace is writable from inside its sandbox, and running git on it would run on the host. A local session already runs as the same OS user, so polling adds no privilege. Ownership goes throughfindSessionOrFail, so another user's session is a 404.redactGitCredentialsbefore they reach a client.git-status-ui.js, a new mixin): polls every 15 s while the page is visible, immediately on a session switch or window focus, and stops completely when the setting is turned off (no reload needed). Everything git supplies (file names, commit subjects, authors) is written withtextContentonly. Stale responses (another session, setting turned off mid-request) are dropped.displayKeys, stripped from the strictPUT /api/settings, not inSettingsUpdateSchema.Which repositories
git finds a repository by walking up from the working directory, so the rules are explicit (and tested against real repos):
node_modules,dist,build,target,vendor,venvand__pycache__are skipped, symlinks are never followed, and a repository's own contents are not searched. The list is re-scanned at most every 30 s.$HOME, or/): without this, a workspace under~/codeman-caseswould report the dirty files of your dotfiles repo. A workspace that is that repo's root is not ignored. A worktree (.gitis a file) counts as a repository. A submodule's own uncommitted files are not reported (only a changed pointer).Tests
test/git-workspace-status.test.ts(50): the porcelain-v2 parser (branch/upstream/ahead-behind, detached, staged+modified = two rows, renames, unmerged, untracked with spaces/quotes/newlines); real git: clean/dirty/staged/renames/conflicts/odd names/stashes/detached/the file cap, a bare remote for in-sync/ahead/no-upstream/pushed-branch, behind only after a fetch (it never fetches); it only reads (the index bytes are unchanged and no lock is left, and a repo-configuredcore.fsmonitorhook is not run, each verified to fail without the hardening flags); cache single-flight/TTL/fresh; error mapping, credential redaction, partial failures; no write or network verb is ever issued. Overview: folder of several repos (sorted, own status each), depth 2 found / depth 3 not, subfolder reports the enclosing repo, no scan inside a repo, a session inside a nested repo, the$HOMEdotfiles repo and a repo above home ignored (verified to fail with the guard disabled) while a workspace that is the root is not,node_modules/dot-folders/symlinks skipped, worktrees counted, the 12-repo cap, no repo at all, git failure as an error, and the discovery cache withfresh.test/routes/git-status-routes.test.ts(10): envelope, not-a-repo, 404, runs in the session's working directory, remote and Docker never run git,?fresh=1, multi-user ownership.test/git-status.browser.test.ts(11, real Chromium + real server + a real repo with a remote): off by default with no request to the route; turning it on through the real Settings save (HTTP 200, so the key stays out of the strict PUT); counts and tooltip; sits at the right of the bottom bar before the version; the panel contents; hostile file names render as text; file click previews by absolute path; drag; clean state after a push (and a manual refresh shows it at once); a non-repo session hides the button and the open panel stops showing the previous repo; a folder holding two repos shows two sections (the dirty one open, the clean one collapsed) and the indicator adds them up; turning it off hides everything and stops polling; the amber colour is the computed one (skin rules out-rank a bare class).Merge order
Rebased onto 1.34.0 (the earlier
config/test-suites.tsoverlap with the merged PRs is resolved). Independent of my other open PRs apart from adjacent insertions inconfig/test-suites.tsanddocs/api-reference.md, which will need a trivial rebase for whichever lands second. I will do that as they merge.🤖 Generated with Claude Code
https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS