feat(diff): add a Session / Last turn scope toggle to the changes panel (#1635)#2748
feat(diff): add a Session / Last turn scope toggle to the changes panel (#1635)#2748fiorelorenzo wants to merge 6 commits into
Conversation
…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.
Greptile SummaryThis 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.
Confidence Score: 5/5Safe 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.
|
| 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
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.
|
@greptileai please re-review. I pushed the three fixes from your review in 3d4e70e:
CI is green on the new commit. |
|
Thank you for your contribution @fiorelorenzo ! |
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-submittedevent, which fires for both ACP and PTY agents)LastTurnBaselineServicesnapshots 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..gitignore, viagit write-treeagainst a throwaway index file. It never touches the real index, working tree, or commit history (validated in the unit test).git gccould 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.getLastTurnChangesRPC, and opening a file renders the immutablebaseTreetoheadTreesnapshot diff through the existing ref-to-refgitdiff 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)
GitWorktree.snapshotWorktreeTree()andgetChangedFilesBetweenTrees()inpackages/core(plus theIGitWorktreeinterface): the snapshot and tree-diff primitives.LastTurnBaselineService, thegetLastTurnChangesRPC, thegit:last-turn-baselineevent, and init wiring.IGitWorktreemethods implemented on the legacy SSH runtime, so remote worktrees compile and behave the same.Related issues
Closes #1635.
Testing
packages/coregit 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).LastTurnBaselineServiceunit test: capture on turn start, overwrite per turn, ignore workspaceless tasks, clear on teardown.pnpm run format,pnpm run lint, andpnpm run typecheckclean 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)
Checklist