Diff Workspace
The multi-file diff review panel: a changed-files sidebar, per-file viewed markers, image compare, full-file view, and keyboard stepping across a whole change set.
Overview
The diff workspace is where you actually read a change set. It is a diff panel (a first-class built-in panel kind, peer to terminal, review, file viewer, file browser, browser, and dev preview) opened against a specific file and, usually, seeded with every other changed file alongside it.
Inside the app the panel is called Diff Viewer. The docs call it the diff workspace, because a single-file diff and a whole change set with a sidebar are the same panel wearing two faces, and the second one is what most reviews look like.
The layout is a changed-files sidebar on the left and the diff canvas on the right, with a footer carrying file stepping and the viewed toggle. When it is presented as a dialog the frame is pinned at its maximum height rather than sized to content: otherwise stepping from a two-line change to a two-hundred-line one would resize and re-center the whole window under your cursor.
Opening it
Six of the seven ways in do not involve Review Hub at all. That is the main reason this is its own page.
Open changes: the whole change set at once
Shift-Command-D Control-Shift-D Control-Shift-D opens the working-tree diff for the focused worktree. The same action is Open changes in the worktree three-dot and right-click menus, sitting directly above Review & Commit, and it appears in the command palette under that name.
It opens on the highest-churn changed file (the one with the most inserted plus deleted lines), seeded with the full change set, so the sidebar can step through everything from there. It reads the change set straight from the worktree store rather than from a rendered list, which is why it works from a collapsed worktree card where no Changed Files list exists. In the command palette the row is disabled with "No changes in the focused worktree" when there is nothing to open.
The Changed Files list on a worktree card
Expand a worktree card and click any row in its Changed Files list. It opens the diff seeded with that card's whole change set and keeps the panel's set in step with the live worktree poll, so files appearing and disappearing under an active agent do not strand the sidebar. See Worktree Cards.
The shared file-row menu
Right-click a file row anywhere it appears (the worktree card's Changed Files list, the file browser, Review Hub's staged and unstaged sections, and the diff workspace's own sidebar), and the same menu opens with Open diff at the top. One menu across four surfaces, so the gesture means the same thing everywhere.
The item is hidden for directories, and for files with no change to show. A deleted file keeps it (its diff is exactly what you want there) while "Open file" and "Open in editor" drop away, because there is nothing on disk left to open.
When the menu is opened from the diff sidebar itself, Open diff steps that sidebar's own viewer to the file rather than stacking a second diff dialog on top.
The file.openDiff action
Agents, plugins, and anything else dispatching actions can open a diff by path. It takes a required path (absolute or repo-relative), an optional worktreePath, and an optional git status, and returns the id of the panel it opened. The dialog it produces is deliberately ephemeral: never persisted, never counted toward the panel limit, never restored on restart. Use Open as panel when it should stay in the grid.
File Viewer's Diff mode
The File Viewer has a third view mode, Diff, alongside Source and Rendered. It is offered only when the file being viewed actually has a local change, and it shows that one file's working-tree diff inside the File Viewer panel: a single file, no sidebar, no stepping. A persisted mode whose capability disappears falls back to Source without overwriting your choice, so a poll that transiently drops the change does not erase what you picked.
Review Hub
Clicking a file row in Review Hub, or pressing Enter on a focused one, layers the diff workspace over the hub with the hub's whole change set attached. The diff follows the hub's selection: stage a file while its diff is up and the diff re-fetches under its new status rather than continuing to render the old one.
The changed-files sidebar
The sidebar appears whenever the panel was seeded with more than one file. It has four parts, top to bottom.
- Change set summary: the file count, plus total insertions in green and deletions in red across the whole set.
- Review progress: "N of M viewed", with a thin progress track beneath it. The track only appears once you have marked something: an empty full-width strip at zero progress reads as stray chrome.
- Filter: a substring match on the path. Escape in the filter clears it rather than bubbling out and closing the whole workspace. A filter that matches nothing offers Clear filter.
- File list: grouped by directory, with a folder header per group (deep paths truncate to their last three segments, and repo-root files sit under "(root)"). Each row shows a status letter, the filename, and that file's own insertion and deletion counts. Viewed files dim.
The open file's row is kept scrolled into view as you step, and re-revealed when a filter that was hiding it is cleared. Rows carry the shared file-row context menu.
The footer's sidebar toggle hides and shows the shelf, and that choice is a persisted preference rather than per-panel state: hide it once and every diff you open afterwards opens without it.
Keyboard model
Three keys, no modifiers, scoped to the focused panel so a background one stays inert. They are ignored while you are typing in an input, a textarea, or any editable field, including the sidebar filter.
| Key | Action |
|---|---|
| [ | Previous file in the change set |
| ] | Next file in the change set |
| v | Toggle the current file's viewed marker |
The footer mirrors all three: chevron buttons for previous and next (their tooltips name the bracket keys), a "N of M" position indicator between them, and a Viewed toggle on the right.
How stepping actually resolves
Two details are worth knowing, because they are what keeps stepping stable while an agent is writing.
First, v marks the file viewed and stops there. It does not advance. Marking and moving are separate gestures, so you can mark a file, keep reading it, and step when you are ready.
Second, the current file is resolved by identity on every render rather than stored as an index. A change set rebuilt from a later poll can reorder or drop files, and a remembered index would then point at the wrong one. Resolution tries the file's viewed key first (the only field that separates the staged and unstaged copies of a partially staged file, which share both path and status), then path plus status, then path alone. Stepping is clamped against the live length, and if the open file drops out of the set entirely, the next step resumes from where you were rather than jumping back to the top.
Stepping forward is prefetched: the next file's diff is warmed while you read the current one, so ] renders from cache.
The diff toolbar
| Control | What it does |
|---|---|
| Unified / Split | Diff layout. Persisted; Split is the default. |
| Changes / Full file | Content scope: changed hunks with context, or the whole file. Persisted. |
| File path | Click to copy the path. |
| Wrap long lines | Soft-wrap instead of horizontal scroll. Persisted. |
| Refresh | Re-fetch the diff, and re-request the file for media and PDF previews. |
| Reveal in Finder / Explorer / file manager | Show the file in your platform's file manager. |
| Open in editor | Open the file in your configured external editor. |
Reveal and Open in editor appear only when the panel can resolve an absolute path for the file. If either fails, an error banner with a Retry outranks the ambient notices below it (you asked for something and it did not happen, so that gets the space), and each button clears only its own failure.
The layout and wrap controls are hidden for images, video, audio, and PDFs, where they mean nothing.
Full file vs changed hunks
By default a diff shows changed regions with their surrounding context. The Full file segment expands every hunk to cover the whole file, so you can read a change in the context of everything around it. Layout and scope are independent: full file stays meaningful in both unified and split.
The segment is always visible, so the option is discoverable, and explains itself when it cannot be used. The reason rides an accessible description as well as a tooltip, because a disabled segment takes no focus and a hover-only explanation would never reach a keyboard or screen-reader user.
| Situation | Why full file is unavailable |
|---|---|
| Added or untracked file | The diff already shows the file's full contents. |
| Deleted file | There is no current version to read. |
| Base-branch diff | The file at that ref is not what is on disk. |
| Staged diff | Staged content lives in the index, not on disk. |
| Image, video, audio, PDF | The view already shows the whole asset. |
| Diff not loaded yet | There is nothing to expand yet. |
Full file can also be requested and then quietly fall short, in which case a "Showing changed lines only" notice names the reason: the file changed after the diff loaded (offering a retry), the file runs past 5,000 lines and stays on changed lines to keep the diff responsive, or the whole file cannot be shown for this particular diff. Only the first of those is recoverable, so only that one gets a button: a rename with no hunks never grows hunks, and an oversized file never shrinks.
The stale-diff banner
A diff is a snapshot. When the worktree watcher sees the open file change after the shown diff was fetched, an info banner appears above the canvas: "File changed since this diff loaded", with a Refresh action. The diff no longer silently shows content that has moved on.
Staleness is only claimed when there is a real signal. An unknown freshness key (an untracked worktree, a host with no worktree store, a store that had not hydrated when the diff was fetched) never flags, because no signal is not the same as fresh. The banner suppresses the full-file notice while it is up, since both would be pointing at the same underlying change and offering the same refresh.
Full file is also switched off automatically while a diff is stale. The mismatch check inside the renderer only covers lines the hunks name, so a change in a hidden gap would otherwise be painted as unchanged context, which is worse than showing less.
Comparing changed images
An image with a working-tree change gets a real comparison rather than a text diff. Three modes sit in a segmented toggle above the canvas:
- Two-up: HEAD and working tree side by side, each labeled.
- Swipe: one image over the other with a draggable divider. The divider is keyboard-operable.
- Onion skin: the two stacked, with a Working tree opacity slider from 0 to 100 to fade between them.
Added and deleted images show the one side that exists, captioned "Added: no previous version" or "Deleted: no working version". If one side fails to decode, the layout locks to two-up and the compare modes are hidden: swipe and onion skin have no way to show a per-side error, so the mode that can is the one you get.
Stepping between images holds the previous frame until the next one is ready to paint, so a walk through a directory of assets never tears down to a skeleton between files.
Video, audio, and PDFs
These render the current working-tree version inline rather than a comparison: the media pipeline has no cheap old-versus-new view. That applies to every diff source, base-branch included. A deleted one shows "No working-tree version to play" (or "to preview") rather than an empty player. When a supported container turns out to hold a codec Chromium lacks, you get a named error and a Refresh instead of a dead native control.
Viewed markers
The check on each sidebar row, the v key, and the footer's Viewed toggle all write to the same place: one store keyed per worktree, shared with Review Hub's file list. Check a file off in either surface and it reads as reviewed in both, and the sidebar's "N of M viewed" progress counts them.
Keys are namespaced by the surface that owns the change set. Review Hub uses staged: and unstaged:, a worktree card's change list uses the file's status, and base-branch review uses base:. That is what lets the two halves of a partially staged file be reviewed independently.
Markers are session-scoped. They are not written to disk, so a fresh app session starts a fresh review, and a successful commit in Review Hub clears them for that worktree: a commit starts a new review, and stale markers must not stick to files that change again afterwards.
Empty and unavailable states
| State | What you see |
|---|---|
| No file selected | "Pick a changed file to see its diff" |
| Worktree could not be resolved | "Diff unavailable: This panel's worktree couldn't be resolved; it may have been moved or removed." |
| Base-branch diff missing a ref | "Diff unavailable: The base or current branch couldn't be resolved, so there's no comparison to make." |
| Diff still loading | A shape-matched skeleton, gated so sub-400ms loads render nothing at all |
The unavailable states exist because the alternative is a skeleton that spins forever: with no subject to fetch, there is nothing for Refresh to retry either, so the panel says so instead of pretending to load.
What persists
Six preferences survive a restart, and they are global rather than per panel: every diff you open afterwards, from any entry point, honors them.
| Preference | Default |
|---|---|
| Diff layout (Split / Unified) | Split |
| Wrap long lines | Off |
| Ignore whitespace | Off |
| Show the changed-files sidebar | On |
| Content scope (Changes / Full file) | Changes |
| Diff text size (S / M / L) | M |
The text size preference drives the rendered diff's font size (11px, 12px, or 14px) through a CSS custom property on the panel body.
Everything else is per panel and per session: the sidebar filter, which file is open, and the position within the change set.