003 — Diff Window: Plan #
| Status | Implemented (retrospective) |
| Anchor date | 2026-03-06 |
| Documented | 2026-07-12 (backfilled) |
| Primary PRs | #1, #10, #45 (follow-up waves: #449, #529, #536, #537, #540) |
| Sources | PR descriptions (#1, #10, #45, #449, #529, #536, #537, #540); fork change-list entries for commits 0d03848–8985fc2, 1b32a26 (ledger lives at 017-upstream-sync-process/upstream-ledger.md) |
| Related | 012-keybinding-system, 037-line-diff-tracking, docs/components/diff-view.md |
Background #
Prowl's core loop is "let an agent work in a worktree, then review what it did". Before this feature there was no in-app way to inspect a worktree's uncommitted changes — reviewing an agent's output meant switching to a terminal or an external tool. The fork wanted a fast, local diff viewer reachable directly from the worktree row.
This is a fork-only feature (upstream supacode has no equivalent), built on
YiTong (https://github.com/onevcat/YiTong), onevcat's own WKWebView-backed
diff-rendering library.
Goals #
- A standalone diff window showing all changes in the selected worktree's working directory vs HEAD — tracked changes and untracked new files.
- File tree sidebar (left) + rendered diff (right) via
NavigationSplitView, with YiTong'sDiffViewas the renderer. - Instant file switching: preload all file contents concurrently when the window opens.
- Openable from the worktree row's diff badge, a keyboard shortcut, and a "Show Diff" menu item.
- Toolbar with sidebar toggle and a split/unified diff style picker persisted
across launches;
Cmd+Wcloses the window; window frame persisted. - Singleton window that refreshes its content when it regains focus.
Non-goals (initial scope)
- Diff against a base branch or a PR — this is strictly working-tree vs HEAD.
- External diff tools (added later, see amendment 002).
- Staging/committing from the diff window.
Design / Approach #
As shipped in #1 (2026-03-06):
- Git layer — new
GitClientoperations:git diff HEAD --name-status(changed-file list),git ls-files --others --exclude-standard(untracked files), andgit show HEAD:<path>(old file contents). New/deleted/renamed files map to empty-vs-disk, HEAD-vs-empty, and old-path-vs-new-path pairs. - Model —
DiffChangedFileparses the--name-statusoutput (M/A/D/R/C status plus paths). - State —
DiffWindowState, an@Observableclass holding the file list, selection, and a per-fileDiffDocumentcache filled by concurrent preloading, so selecting a file renders from cache. - Window —
DiffWindowManager, a singletonNSWindowmanager following the existingSettingsWindowManagerpattern: one window app-wide,setFrameAutosaveNamefor frame persistence, a localkeyDownevent monitor to interceptCmd+W, and refresh-on-focus. - View —
DiffWindowContentView:NavigationSplitViewwith the file list sidebar and YiTongDiffViewdetail; toolbar hosts the sidebar toggle and a split/unified style picker persisted viaUserDefaults(@AppStorage("diffViewStyle")).
Two small fixes were planned/landed as part of the initial arc: unicode
(Chinese) filenames were invisible because git's default core.quotePath=true
octal-escapes non-ASCII paths — fixed by passing -c core.quotePath=false to
both listing commands (#10); and the YiTong dependency moved from
branch-tracking (master) to a semver pin at 0.2.0, whose optimized web bundle
cut the embedded asset from 9.3 MB to 2.7 MB (−7 MB on the .app) (#45).
Alternatives & decisions #
- Standalone
NSWindow, not a SwiftUIWindowGroupscene — deliberately followed the existingSettingsWindowManagersingleton pattern. Consequence: the window does not inherit SwiftUI environment appearance, which later required explicit appearance plumbing (amendment 004). - Preload everything on open rather than load-on-select — chosen for instant file switching; acceptable because worktree diffs are typically small. The concurrent task group updates the cache per-file as results arrive, so early selections don't wait for the whole set.
- YiTong pinned by semver (0.2.0) instead of tracking
master(#45) — reproducible release builds and a measured −71% web-bundle size. - Diff basis is HEAD, not the base branch — the window answers "what did
the agent change that isn't committed yet"; PR-level review is delegated to
code hosts (see
docs/components/github-pull-requests.md).
Amendments #
- Updated 2026-06-14: configurable external diff tools (Hunk, FileMerge, Kaleidoscope, custom command) — see 002-external-diff-tools.md
- Updated 2026-07-03: render pipeline hardening — stale-cache race, select
debounce, render-error recovery,
Debouncerextraction +RenderStateenum (#529/#536/#537) — see 003-render-pipeline-hardening.md - Updated 2026-07-08: diff window follows app appearance instead of system (#540) — see 004-appearance-follows-app.md
- Updated 2026-07-14: built-in outgoing changes for an identified pull request, using its target remote and merge-base semantics — see 005-outgoing-changes.md
- Updated 2026-07-24: hardening plan for outgoing changes — fully-qualified base refs, labeled no-PR fallback ladder, distinct resolution errors, focus-refresh fix — see 006-outgoing-changes-hardening.md