Skip to content

fix(vcs): collapse review diff modes to git + branch + turn#1044

Merged
Astro-Han merged 5 commits into
devfrom
claude/vcs-collapse
Jun 1, 2026
Merged

fix(vcs): collapse review diff modes to git + branch + turn#1044
Astro-Han merged 5 commits into
devfrom
claude/vcs-collapse

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Jun 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Collapses the four review-panel modes (unstaged / staged / branch / turn) into three (git / branch / turn), aligned with upstream OpenCode's two-mode shape plus PawWork's turn snapshot.
  • Fixes the latent bug in branch mode: git.diffHead ran git diff <mergeBase> HEAD, which silently dropped every staged and working-tree change. branch mode now uses git diff <mergeBase> (working tree vs ref), so the full branch delta reaches the user.
  • Net delete of ~270 lines: removes the per-mode Git helpers (diffUnstaged/diffStaged/diffHead/statsUnstaged/statsStaged/statsHead/statusUnstaged/patchStaged/patchUnstaged/patchHead/patchStagedAll) in favour of the existing generic diff(cwd, ref) / stats(cwd, ref) / patch(cwd, ref, file).

Why

The split into unstaged/staged/branch surfaced Git's internal index/HEAD concepts as the product's main path. Each tab was internally consistent in its own narrow Git semantics but never gave the user a complete answer:

  • unstaged hid staged work
  • staged hid working-tree work
  • branch (the most-clicked) hid every uncommitted change because of the spurious HEAD endref

With the new shape, every tab answers a coherent product question: git = "what have I changed since the last commit", branch = "what does this branch accumulate against main", turn = "what did the agent change this round".

Changes

  • packages/opencode/src/git/index.ts: drop the per-mode helpers, keep only the generic diff / stats / patch / patchAll / patchUntracked / statUntracked.
  • packages/opencode/src/project/vcs.ts: Mode = ["git", "branch"]. Both routes go through one track(git, cwd, ref?) helper that merges git.diff(cwd, ref) + status ?? entries (and falls back to status-only when there is no HEAD yet). diffRaw simplifies in the same direction.
  • packages/opencode/src/server/instance/index.ts: /vcs/diff description updated to match the new modes.
  • packages/sdk/js: regenerated; VcsDiffData["query"]["mode"] is now "git" | "branch".
  • packages/app/src/pages/session/review-change-mode.ts + use-session-review-state.ts: ReviewChangeMode = "git" | "branch" | "turn", cache store shape collapses to two VCS slots, options array becomes ["git", "branch", "turn"].
  • packages/app/src/pages/session/review-panel-view.tsx: empty-state branches collapse to git / branch.
  • packages/app/src/i18n/{zh,en}.ts + parity.test.ts: drop *.title.unstaged / *.title.staged / noUnstagedChanges / noStagedChanges, add *.title.git / noGitChanges.
  • Tests: git.test.ts, vcs.test.ts, review-change-mode.test.ts, use-session-review-state.test.ts, execution-scope.test.ts updated. New regression coverage in vcs.test.ts asserts diff('branch') includes staged + unstaged + untracked work on top of branch commits — the case the old diffHead form silently dropped.

Verification

  • bun run typecheck (packages/opencode, packages/sdk/js, packages/app)
  • bun test (packages/opencode: 3416 pass / 9 skip / 1 todo; packages/app: 1628 pass)
  • bun run lint
  • Scratch repo in /tmp confirmed that git diff <mergeBase> reports the staged add and the working-tree edit the old git diff <mergeBase> HEAD form was hiding.

Test plan

  • Maintainer manual: open a session with both staged and unstaged work on a non-default branch. Confirm the Changes tab lists every modification (no more cross-tab archaeology). Confirm the Branch tab additionally shows commits that have already landed on the branch. Confirm the Last Turn tab still tracks the agent snapshot exactly as before.

Related

Touches the same code path that #1019 references (Status panel branch stats reuse).

Summary by CodeRabbit

Release Notes

  • Changes
    • Simplified git review modes by consolidating separate unstaged/staged views into a unified "git" mode that shows all working tree changes (staged, unstaged, and untracked files) alongside a dedicated "branch" comparison mode.
    • Updated version control API modes to reflect the new git and branch change comparison options.

The review panel exposed four diff modes (unstaged / staged / branch /
turn) backed by three independent slice helpers per ref. Each slice was
internally consistent but never showed the user the full picture:

- unstaged hid staged work, staged hid unstaged work
- branch ran `git diff <mergeBase> HEAD`, silently dropping every
  uncommitted (staged + working-tree) change

Collapse to upstream's two-mode shape plus PawWork's turn snapshot:

- `git`  = working tree vs HEAD (covers staged + unstaged + untracked)
- `branch` = working tree vs merge-base (the full accumulated branch
            delta, now including uncommitted work)
- `turn`  = session snapshot, unchanged

Mechanical drop of the per-mode Git helpers (diffUnstaged / diffStaged /
diffHead / statsUnstaged / statsStaged / statsHead / statusUnstaged /
patchStaged / patchUnstaged / patchHead / patchStagedAll) in favour of
the existing generic `diff(cwd, ref)`, `stats(cwd, ref)`, and
`patch(cwd, ref, file)`. `Vcs.diff` routes both modes through a single
`track()` helper; `Vcs.diffRaw` keeps PawWork's no-HEAD branch via
`patchUntracked` on every status entry.

Front-end follows: `ReviewChangeMode` becomes `git | branch | turn`,
the cache store loses the unstaged/staged slots, i18n keys land at
`ui.sessionReview.title.git` and `session.review.noGitChanges`, and the
SDK regenerates the new mode union.

Verification:
- bun run typecheck (packages/opencode, packages/sdk/js, packages/app)
- bun test (packages/opencode: 3416 pass / 9 skip / 1 todo; packages/app:
  1628 pass)
- bun run lint
- /tmp scratch repo confirmed `git diff main` reports the staged + worktree
  delta the old `git diff main HEAD` was hiding
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Astro-Han, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 52 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 13928943-1559-4169-84d4-7d8753420f7e

📥 Commits

Reviewing files that changed from the base of the PR and between 91edafc and 5313228.

📒 Files selected for processing (4)
  • packages/opencode/src/git/index.ts
  • packages/opencode/src/project/vcs.ts
  • packages/opencode/src/server/instance/index.ts
  • packages/opencode/test/project/vcs.test.ts
📝 Walkthrough

Walkthrough

This PR consolidates VCS diff modes from unstaged/staged to a unified git mode with optional cached flag. Review mode types, session state, Git service interface, and Vcs diff implementation are refactored throughout packages/app and packages/opencode, with corresponding test coverage updates.

Changes

Git VCS Mode Consolidation

Layer / File(s) Summary
UI strings and review mode type contract
packages/app/src/i18n/en.ts, packages/app/src/i18n/zh.ts, packages/app/src/i18n/parity.test.ts, packages/app/src/pages/session/review-change-mode.ts, packages/app/src/pages/session/review-change-mode.test.ts
Introduces translation keys for git-focused review states (noGitChanges, noBranchChanges, git tab title) and establishes ReviewChangeMode type as git | branch | turn, with isVcsReviewMode and reviewChangeOptions() reflecting the new consolidated mode set.
Session review state management for new modes
packages/app/src/pages/session/use-session-review-state.ts, packages/app/src/pages/session/use-session-review-state.test.ts, packages/app/src/pages/session/review-panel-view.tsx, packages/app/src/pages/session/execution-scope.test.ts, packages/app/src/pages/session/review-change-mode.test.ts
Updates VCS state initialization to use git and branch keys only, uses new mode labels in empty-state UI text, adjusts task key generation to distinguish git diffs across execution scopes, and updates test fixtures to use unified git mode instead of unstaged/staged.
OpenCode Git interface simplification
packages/opencode/src/git/index.ts
Consolidates diff, stats, patch, and patchAll into unified signatures with optional DiffOptions/PatchOptions (including cached flag), removes unstaged/staged/head-specific method variants from the interface, adds internal cached() helper for --cached injection, and updates Service wiring to export only the unified methods.
Vcs diff implementation refactoring
packages/opencode/src/project/vcs.ts
Replaces staged/unstaged patching with unified track(git, cwd, ref) implementation that uses cached and head stat composition, per-file patch strategy selection with head/cached fallback, updates Mode schema to git | branch only, and rewrites diff() and diffRaw() control flow to diffs against HEAD (for git mode) or merge base (for branch mode).
API contract and endpoint documentation
packages/opencode/src/server/instance/index.ts, packages/sdk/openapi.json
Updates GET /vcs/diff endpoint description and OpenAPI spec to clarify that git mode compares working tree vs HEAD (including staged/unstaged/untracked), and branch mode compares working tree vs merge base with the default branch.
Git service tests for consolidated modes
packages/opencode/test/git/git.test.ts
Removes statusUnstaged() test, validates diff() against HEAD merges staged, unstaged, and committed-since-ref changes into one unified view, and adds diff() against merge-base test for branch-mode validation with modified/added status assertions.
Vcs integration tests for git and branch modes
packages/opencode/test/project/vcs.test.ts
Replaces diff('unstaged') tests with comprehensive diff('git') coverage including staged, unstaged, and untracked file combinations with patch assertions, strengthens diff('branch') assertions to require working-tree edits alongside committed branch changes, and adds regression tests for edge cases such as staged changes matching HEAD.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • Astro-Han/pawwork#335: This PR directly addresses the core issue by consolidating VCS diff modes from unstaged/staged variants into a unified git mode and refactoring the Vcs.diff implementation with a new track() approach for handling staged, unstaged, and branch comparisons.

Possibly related PRs

  • Astro-Han/pawwork#338: Both PRs update session review change modes and VCS i18n; this PR renames the modes from unstaged/staged (introduced in #338) to the unified git/branch model.
  • Astro-Han/pawwork#436: Both PRs refactor the session VCS-diff pipeline in use-session-review-state.ts and adjust vcsTaskKey generation, with this PR shifting modes while #436 adjusts task gating and directory-scoped keying.
  • Astro-Han/pawwork#956: Both PRs refactor Git service APIs (patch/diff interface in git/index.ts) and Vcs diff plumbing in vcs.ts, so the git mode consolidation is coupled with raw-diff and patch-generation changes.

Poem

🐰 From unstaged and staged, a clarity emerges—
One git mode to rule them all, as the branch diverges.
Cached flags whisper where --quiet once spoke,
And track() dances between HEAD and the folk.
A simpler API, a cleaner refrain,
Makes version control dreams less arcane!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(vcs): collapse review diff modes to git + branch + turn' directly and clearly summarizes the main change: collapsing four review modes into three specific modes.
Description check ✅ Passed The description covers all required template sections: Summary, Why, Related Issue context, Human Review Status pending, Review Focus implied through detailed changes, Risk Notes (none), How To Verify (verification steps and test results provided), and all required checklist items are addressed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/vcs-collapse

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added app Application behavior and product flows ui Design system and user interface harness Model harness, prompts, tool descriptions, and session mechanics P2 Medium priority labels Jun 1, 2026
@Astro-Han Astro-Han added bug Something isn't working P1 High priority labels Jun 1, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested priority: P2 (includes user-path files (packages/app/src/i18n/en.ts, packages/app/src/i18n/parity.test.ts, packages/app/src/i18n/zh.ts, packages/app/src/pages/session/execution-scope.test.ts, packages/app/src/pages/session/review-change-mode.test.ts, packages/app/src/pages/session/review-change-mode.ts, packages/app/src/pages/session/review-panel-view.tsx, packages/app/src/pages/session/use-session-review-state.test.ts, packages/app/src/pages/session/use-session-review-state.ts)).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

@github-actions github-actions Bot removed the P2 Medium priority label Jun 1, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request simplifies the version control system (VCS) diff logic by consolidating the separate 'unstaged' and 'staged' modes into a single 'git' mode across translation files, frontend components, API routes, and the generated SDK. It also refactors the underlying Git utilities to use a unified tracking helper. A high-severity issue was identified in Vcs.diff where Git operations are run against Instance.directory instead of checking for a separate worktree via Instance.worktree ?? Instance.directory, which could lead to incorrect or missing diffs when a worktree is active.

Comment thread packages/opencode/src/project/vcs.ts
Astro-Han added 2 commits June 1, 2026 17:17
The previous commit changed the server-side `Vcs.Mode` schema to
`["git", "branch"]` but only regenerated the TypeScript SDK in
packages/sdk/js/src/v2/gen/. The checked-in OpenAPI source under
packages/sdk/openapi.json still declared the old `unstaged | staged |
branch` enum, so any client generated from that file would send
requests the server now rejects with a query validation error.

Patch the enum and the operation description in place. A full regen of
this file pulls in unrelated drift from schema-tool behaviour changes
(stray pattern fields, leading shell echo); keeping the diff surgical
isolates the contract change to the VCS endpoint.

Verification:
- git diff --stat packages/sdk/openapi.json shows +2/-3 (mode enum +
  description only)
- Found by codex review on PR #1044
`git diff <ref>` only walks worktree vs ref, so when a file is staged and
the worktree is then restored to match the ref (e.g. edit → `git add` →
overwrite back to HEAD), porcelain status shows `MM` but the diff returns
nothing. The previous track() merged only `git.diff(...)` + untracked
status entries, so the new git mode silently dropped this state — the
file was about to be committed and the review panel was empty.

Carry `--cached` through the existing helpers instead of adding parallel
ones:

- Git.DiffOptions { cached } and PatchOptions extending it
- diff / stats / patch / patchAll thread `--cached` when requested
- track() folds three sources into the file list: tracked (worktree-vs-ref),
  staged-only entries detected via porcelain status (index-vs-ref where
  the worktree happens to match), and untracked entries
- stat map blends head-vs-ref over cached-vs-ref so worktree edits keep
  their additions/deletions while staged-only files inherit the cached
  numbers
- per-file patch falls back to `git.patch(..., { cached: true })` when
  the worktree-vs-ref patch is empty but the index still differs

Same helper surface, one extra `cached` boolean. branch mode benefits
from the same fix when a feature-branch file is staged then reverted to
the merge base.

Verification:
- bun run typecheck (opencode, sdk/js, app)
- bun test (opencode: 3417 pass / 9 skip / 1 todo; targeted git+vcs+routes: 54 pass)
- bun run lint
- New regression test in test/project/vcs.test.ts pins the stage→restore
  case to the new surfaced-with-cached behaviour
- Raised by codex review on PR #1044

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/src/project/vcs.ts`:
- Around line 343-359: diffRaw currently builds tracked patches using
git.patchAll(..., "HEAD") which misses index-only (staged-only) changes; update
diffRaw to prefer the provided stagedAgainstRef when present (use
git.patchAll(worktree, stagedAgainstRef, ... ) instead of "HEAD") and if that
still misses staged-only deltas call the cached/index patch fallback (via
rawPatch calling git.patchCached or git.patchIndex for the same ref) and merge
that into the tracked variable before appending untracked extras; modify the
tracked assignment (and keep rawPatch, git.patchAll, git.patchUntracked,
stagedAgainstRef, and diffRaw identifiers) so staged-only changes are included.
- Around line 325-334: The git-path resolution in the git branch of the
conditional uses Instance.directory directly, which can point at the base
checkout instead of an attached worktree; update calls to use the resolved
worktree path (Instance.worktree ?? Instance.directory) whenever calling
git.hasHead, git.mergeBase, and track so status/diff/apply semantics match other
methods (change the ref variable computation and the three calls:
git.hasHead(Instance.directory) -> git.hasHead(worktree),
git.mergeBase(Instance.directory, ...) -> git.mergeBase(worktree, ...), and
track(git, Instance.directory, ref) -> track(git, worktree, ref)).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a1b9a3a-b790-4174-8efa-346ae46835fe

📥 Commits

Reviewing files that changed from the base of the PR and between 3ade93a and 91edafc.

⛔ Files ignored due to path filters (2)
  • packages/sdk/js/src/v2/gen/sdk.gen.ts is excluded by !**/gen/**
  • packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
📒 Files selected for processing (15)
  • packages/app/src/i18n/en.ts
  • packages/app/src/i18n/parity.test.ts
  • packages/app/src/i18n/zh.ts
  • packages/app/src/pages/session/execution-scope.test.ts
  • packages/app/src/pages/session/review-change-mode.test.ts
  • packages/app/src/pages/session/review-change-mode.ts
  • packages/app/src/pages/session/review-panel-view.tsx
  • packages/app/src/pages/session/use-session-review-state.test.ts
  • packages/app/src/pages/session/use-session-review-state.ts
  • packages/opencode/src/git/index.ts
  • packages/opencode/src/project/vcs.ts
  • packages/opencode/src/server/instance/index.ts
  • packages/opencode/test/git/git.test.ts
  • packages/opencode/test/project/vcs.test.ts
  • packages/sdk/openapi.json

Comment thread packages/opencode/src/project/vcs.ts Outdated
Comment thread packages/opencode/src/project/vcs.ts Outdated
Astro-Han added 2 commits June 1, 2026 17:46
Vcs.status, Vcs.diffRaw, and Vcs.apply all resolve the working directory
via Instance.worktree ?? Instance.directory; only Vcs.diff still pointed
at Instance.directory, so the review panel showed the wrong (or empty)
diff whenever a session ran inside a separate worktree.
In a pre-first-commit repo a path can be staged-added and then deleted from
the worktree (status "AD"), which the collapsed diffRaw lost: tracked was
empty because hasHead was false, and patchUntracked could not read the
already-deleted file. Restore the pre-collapse three-section layout for
the no-HEAD path — staged-vs-empty + worktree-vs-index + untracked — and
add patchAllUnstaged() so the worktree-vs-index slice has a dedicated git
helper. The result is review-oriented unified text, not git-apply-clean
for mixed index/worktree states; the route description and an inline
comment make that contract explicit.
@Astro-Han Astro-Han merged commit 973943b into dev Jun 1, 2026
38 of 40 checks passed
@Astro-Han Astro-Han deleted the claude/vcs-collapse branch June 1, 2026 13:27
Astro-Han added a commit that referenced this pull request Jun 2, 2026
dev #1044 collapsed Vcs.Mode to git|branch; the route test still queried
mode=unstaged, which the validator now rejects with 400 after rebase. Use
mode=git (working-tree vs HEAD), the direct equivalent of the old unstaged.
Astro-Han added a commit that referenced this pull request Jun 2, 2026
Migrate the JSON route handlers in the instance root router (packages/opencode/src/server/instance/index.ts) to the Effect runtime path via `AppRuntime.runPromise` + `Effect.gen`: POST /instance/dispose, GET /path, GET /vcs, GET /vcs/diff, GET /vcs/status, GET /command, GET /agent, GET /skill, GET /lsp, GET /formatter. Part of #936 Hono-to-Effect route migration; no route, schema, or response-shape change. WebSocket and non-JSON surfaces in this file are untouched.

Change boundary:
- packages/opencode/src/server/instance/index.ts: handler bodies only. Each handler resolves the relevant Service (Vcs/Command/Agent/Skill/LSP/Format) or runs existing logic inside `AppRuntime.runPromise`. Route registrations, validators, the `Vcs.Mode` (git|branch) query schema, OpenAPI descriptions, and JSON responses are unchanged.
- packages/opencode/test/server/instance-root-routes.test.ts: new tests for /path + metadata routes (/agent /skill /command /lsp /formatter), VCS routes (/vcs, /vcs/diff, /vcs/status), and /instance/dispose through the real Server runtime.

Verification (isolated worktree, rebased onto origin/dev and pushed so PR CI runs the integrated state):
- bun install --frozen-lockfile
- bun test test/server/instance-root-routes.test.ts -> 3 pass
- bun run typecheck (opencode) -> clean
- codex review --base origin/dev -> no actionable issues
- full PR CI green (ci + windows-advisory)

Review follow-up:
- Two Gemini "high" comments warned about AsyncLocalStorage context loss when reading Instance.current / Instance.worktree / Instance.directory inside AppRuntime effects after async yields. Verified empirically as a non-issue here: attach() (run-service.ts) captures the instance into the Effect-side InstanceRef synchronously at runPromise time, and Node ALS is also preserved across Effect.promise yields under our runtime (probed: Instance.directory resolves correctly before AND after a yield, 3/3 runs). Instance.dispose() additionally reads ctx synchronously at its first statement. On Effect.promise vs Effect.tryPromise: a rejection becomes an Effect defect that still rejects runPromise and is surfaced by Hono as 500, the same observable behavior as the prior await. Both threads resolved with rationale; no code change.
- Rebase integration fix: dev #1044 collapsed Vcs.Mode to git|branch; the new /vcs/diff test queried mode=unstaged, now rejected with 400. Updated the test to mode=git (working-tree vs HEAD, the direct equivalent of the old unstaged). Test-only change.

Linked: #936
Residual risk: low. Read routes preserved; ALS safety verified empirically; VCS mode contract follows current dev (#1044).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app Application behavior and product flows bug Something isn't working harness Model harness, prompts, tool descriptions, and session mechanics P1 High priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant