Skip to content

feat(diff): add a Session / Last turn scope toggle to the changes panel (#1635)#2748

Open
fiorelorenzo wants to merge 6 commits into
generalaction:mainfrom
fiorelorenzo:feat/diff-last-turn
Open

feat(diff): add a Session / Last turn scope toggle to the changes panel (#1635)#2748
fiorelorenzo wants to merge 6 commits into
generalaction:mainfrom
fiorelorenzo:feat/diff-last-turn

Conversation

@fiorelorenzo

@fiorelorenzo fiorelorenzo commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a scope toggle to the Changes panel so you can flip the "Changed" section between the whole task diff (Session, the current behavior) and only what the most recent agent turn changed (Last turn). The two scopes stay cleanly separated, which is what people asked for in the issue.

How it works

Turn baseline (main). On each turn start (the provider-agnostic conversation:input-submitted event, which fires for both ACP and PTY agents) LastTurnBaselineService snapshots the task's worktree as a git tree and remembers it per workspace. "Last turn" is then the diff between that snapshot and the current worktree.

  • The snapshot captures the whole worktree, tracked and untracked, respecting .gitignore, via git write-tree against a throwaway index file. It never touches the real index, working tree, or commit history (validated in the unit test).
  • The baseline is kept in memory on purpose: it is a dangling tree that git gc could prune between sessions, and it is only meaningful for the current turn. It resets on restart and is re-captured on the next prompt.

Rendering (renderer). In last-turn scope the file list comes from a new gitWorktree.getLastTurnChanges RPC, and opening a file renders the immutable baseTree to headTree snapshot diff through the existing ref-to-ref git diff group. So there is no new live-working-tree diff mode and no changes to the diff-tab machinery or its staleness reconcile. The diff is a stable view of the turn, and it refreshes when the next turn is captured.

Last-turn is a read-only review view: stage, discard, and commit actions and selection are hidden, and the empty state explains when a turn made no changes.

Commits (one layer each)

  1. GitWorktree.snapshotWorktreeTree() and getChangedFilesBetweenTrees() in packages/core (plus the IGitWorktree interface): the snapshot and tree-diff primitives.
  2. LastTurnBaselineService, the getLastTurnChanges RPC, the git:last-turn-baseline event, and init wiring.
  3. The last-turn data source in the renderer worktree store.
  4. The Session / Last turn toggle and section wiring.
  5. The two new IGitWorktree methods implemented on the legacy SSH runtime, so remote worktrees compile and behave the same.

Related issues

Closes #1635.

Testing

  • packages/core git tests, including a new case: snapshot plus tree-diff with tracked, untracked, and gitignored files, non-destructive (the real index still sees the change as unstaged).
  • LastTurnBaselineService unit test: capture on turn start, overwrite per turn, ignore workspaceless tasks, clear on teardown.
  • Touched-area suites green: core git (54), main-process git (45), diff-view stores and manager (54, no regressions).
  • pnpm run format, pnpm run lint, and pnpm run typecheck clean on all changed files.

Screenshot/Recording

I develop on a headless box and cannot render the Electron UI here, so I have not visually verified the renderer wiring or captured a screenshot. The backend is fully unit-tested. If someone can run it and confirm the toggle and the read-only last-turn view render correctly, I will follow up on anything that needs adjusting.

Follow-ups (intentionally out of scope)

  • Persist the scope choice globally (it is per-task right now and resets on remount).
  • A browser test for the section's scope-aware rendering.
  • Optionally a live-working-tree "after" side, so an open last-turn diff updates as the turn continues instead of refreshing on the next turn.
Checklist
  • I kept this PR small and focused
  • I ran a self-review before opening this PR
  • I ran the relevant local checks or explained why not
  • I updated docs when behavior or setup changed
  • I added or updated tests when behavior changed, or explained why not
  • I only added comments where the logic is not obvious
  • I used Conventional Commits for commit messages and the PR title

…alaction#1635)

Foundation for the "changes since last turn" diff mode. Adds two methods to
GitWorktree / IGitWorktree:

- snapshotWorktreeTree(): captures the whole worktree (tracked + untracked,
  respecting .gitignore) as a git tree oid, non-destructively via a throwaway
  index file inside the git dir. Used to snapshot the worktree at a turn boundary.
- getChangedFilesBetweenTrees(base, head): lists files changed between two such
  snapshots, in the same GitChange[] shape as getChangedFiles.

Snapshots are serialized per worktree and never touch the real index or working
tree; the before-side content is served by the existing getFileAtRef via the tree
oid. Covered by a unit test (tracked + untracked + gitignore + non-destructive).
…changes (generalaction#1635)

Adds LastTurnBaselineService: on each turn start (the provider-agnostic
conversation:input-submitted event, which fires for both ACP and PTY agents) it
snapshots the task's worktree as a git tree and remembers it per workspace,
emitting git:last-turn-baseline. The baseline is kept in memory on purpose (a
dangling tree can be pruned by git gc across sessions, and it is only meaningful
for the current turn), so it resets on restart and is re-captured on the next prompt.

New RPC gitWorktree.getLastTurnChanges(projectId, workspaceId) returns the files
changed between that baseline and the current worktree, or { baseline: null } when
nothing has been captured yet, so the renderer can fall back to the session diff.
Covered by a service unit test.
…eneralaction#1635)

The renderer git worktree store now tracks the "last turn" changes returned by
gitWorktree.getLastTurnChanges: the worktree snapshot taken at the start of the
most recent turn (baseTree) plus the files changed since. It refreshes on the
git:last-turn-baseline event for its workspace, backing the upcoming
Session / Last turn scope toggle in the changes panel.
…el (generalaction#1635)

Wires the "last turn" diff into the Changes panel UI:

- A history-icon toggle in the "Changed" section header switches between Session
  (the whole task diff, current behavior) and Last turn (only the most recent
  agent turn's changes). The label becomes "Last turn" while active.
- In last-turn scope the file list comes from getLastTurnChanges, and opening a
  file renders the immutable baseTree -> headTree snapshot diff via the existing
  ref-to-ref 'git' diff group, so no live working-tree diff mode was needed. The
  RPC now also returns headTree for the modified side.
- Last-turn is a read-only review view: stage/discard/commit actions and
  selection are hidden, and the empty state explains no turn changes yet.

Scope is per-task (resets on remount); global persistence is a follow-up.
…me (generalaction#1635)

Adding snapshotWorktreeTree / getChangedFilesBetweenTrees to IGitWorktree broke
LegacySshGitWorktree, which also implements the interface. The legacy SSH
execution context cannot pass GIT_INDEX_FILE through its exec, so the
non-destructive worktree snapshot is unavailable there: snapshotWorktreeTree
throws (LastTurnBaselineService treats it as no baseline and the diff view falls
back to the session scope) and getChangedFilesBetweenTrees returns []. The native
runtime implements both fully.
@fiorelorenzo fiorelorenzo marked this pull request as ready for review July 2, 2026 14:11
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a Session / Last turn scope toggle to the Changes panel, letting users flip the "Changed" section between the entire task diff and only what the most recent agent turn modified. The three issues flagged in the previous review (prefetch group mismatch, empty-state wording, and SSH stub parameter names) are all correctly addressed in this iteration.

  • Core git primitives (snapshotWorktreeTree, getChangedFilesBetweenTrees) are added to GitWorktree with a serialization chain (snapshotChain) that keeps the shared throwaway index file race-free; the SSH runtime stubs throw/return-empty so the feature degrades gracefully on the deprecated runtime.
  • LastTurnBaselineService captures a worktree tree OID at each turn start via the provider-agnostic conversation:input-submitted event, stores it in-memory per workspace, and emits a git:last-turn-baseline event that the renderer store subscribes to.
  • Renderer-side, GitWorktreeStore exposes lastTurnChanges / refreshLastTurn, and UnstagedSection now selects the correct diff group ('git' for last-turn, 'disk' for session), uses scope-aware prefetch warm-up, hides mutation actions in last-turn mode, and shows distinct empty-state labels for "no turn yet" vs. "turn made no changes".

Confidence Score: 5/5

Safe to merge. The three issues from the prior review are correctly resolved, all new code paths are covered by unit and integration tests, and the legacy SSH runtime degrades cleanly without blocking the rest of the feature.

The baseline-capture service, the new git primitives, and the renderer wiring are all implemented correctly. The snapshot chain serializes concurrent calls to the shared throwaway index file, so the race-free guarantee holds. Empty-state messages now properly distinguish no baseline yet from this turn changed nothing. The prefetch hooks mirror the correct diff group per scope. No correctness bugs were found in this iteration.

controller.ts (getLastTurnChanges) is worth revisiting if snapshot-related git object accumulation becomes observable in practice — a future optimization could cache the headTree between status-revision changes to reduce redundant git operations.

Important Files Changed

Filename Overview
packages/core/src/git/git-worktree.ts Adds snapshotWorktreeTree (serialized via snapshotChain to protect the shared throwaway index) and getChangedFilesBetweenTrees (reuses parseNumstat/name-status pattern consistent with existing getChangedFiles); both are well-tested.
apps/emdash-desktop/src/main/core/git/last-turn-baseline-service.ts Captures per-workspace baseline OID on every conversation:input-submitted, clears it on task:torn-down, and emits the git:last-turn-baseline channel event; lifecycle (initialize/dispose) and tests are thorough.
apps/emdash-desktop/src/main/core/git/worktree/controller.ts Adds getLastTurnChanges RPC; calls snapshotWorktreeTree() on every invocation to get a fresh headTree — accumulation of dangling objects is a known trade-off documented in a prior review comment.
apps/emdash-desktop/src/renderer/features/tasks/stores/git-worktree-store.ts Adds _lastTurn observable, lastTurnChanges computed getter, refreshLastTurn async method, and event subscription in start(); dispose() correctly cleans up the subscription and the MobX observable is properly registered.
apps/emdash-desktop/src/renderer/features/tasks/diff-view/changes-panel/unstaged-section.tsx Scope-aware rendering is correct: separate diskPrefetch/gitPrefetch hooks properly warm the right model group per scope; mutation actions are correctly gated behind !isLastTurn; empty-state messages now distinguish no-baseline from zero-changes-this-turn.
apps/emdash-desktop/src/main/core/runtime/legacy/ssh-git.ts snapshotWorktreeTree throws to degrade cleanly on the legacy SSH runtime; getChangedFilesBetweenTrees returns [] and now carries the interface parameter names as requested in the previous review.
apps/emdash-desktop/src/renderer/features/tasks/diff-view/changes-panel/components/changes-scope-toggle.tsx New toggle component with controlled tooltip that stays open after click; aria-pressed and aria-label are set correctly for accessibility.
apps/emdash-desktop/src/main/core/git/last-turn-baseline-service.test.ts Unit tests cover: no baseline before first turn, capture+emit on input-submitted, overwrite per turn, workspace-less task skip, and baseline cleanup on task teardown.
packages/core/src/git/git-worktree.test.ts Integration test validates snapshot + diff with tracked, untracked, and gitignored files, confirms non-destructive behavior (real index unchanged), and baseline content preservation for the before-side of the diff.

Reviews (2): Last reviewed commit: "fix(diff): prefetch the correct models i..." | Re-trigger Greptile

Comment thread apps/emdash-desktop/src/main/core/runtime/legacy/ssh-git.ts Outdated
Address the Greptile review on generalaction#2748:

- The Changed panel prefetched only the 'disk'/HEAD models, but last-turn
  files open on the 'git' group against the immutable baseTree -> headTree
  snapshot, so every last-turn hover missed the model cache. Add a git-group
  prefetch keyed by commitRef(baseTree)/commitRef(headTree) and pick it when
  the scope is last-turn.
- Split the last-turn empty state: before the first turn (no baseline yet) it
  no longer claims 'the most recent turn made no file changes', which only
  applies once a baseline exists.
- Name the getChangedFilesBetweenTrees parameters on the legacy SSH stub so it
  matches the IGitWorktree interface signature.
@fiorelorenzo

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review. I pushed the three fixes from your review in 3d4e70e:

  1. Prefetch group mismatch: the Changed panel now selects a 'git'-group prefetch keyed by commitRef(baseTree) / commitRef(headTree) when the scope is last-turn, so it warms the same models openChange opens instead of the 'disk'/HEAD ones.
  2. Empty state: it now tells apart "no turn recorded yet" (no baseline before the first prompt) from "the most recent turn made no file changes".
  3. Legacy SSH stub: getChangedFilesBetweenTrees(_baseTree, _headTree) now carries the interface parameter names.

CI is green on the new commit.

@rabanspiegel

Copy link
Copy Markdown
Contributor

Thank you for your contribution @fiorelorenzo !

@arnestrickmann arnestrickmann requested a review from jschwxrz July 3, 2026 09:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Diff mode to view changes since last turn

2 participants