# Feature specification — Git diff panel

## Source

No formal upstream PRD. Authored from a direct request (2026-06-02): "add a git diff panel that can be
shown instead of the file browser, similar to Paseo," plus a read-only review of the Paseo reference at
`/opt/forks/paseo`. Ground-truth conventions drawn from the project's own docs and existing surfaces.

## Outcome

A person working in a session can review the uncommitted (and on-branch) changes of the project's
repository in a diff view that lives in the same right-side panel slot as the file browser, switching
between the two with a tab. From that view they can read each changed file's diff, stage and unstage
files, commit staged work with a message, and discard a file's changes — without leaving the session or
opening a terminal. The view tells them, at a glance, which files changed and by how much, and stays
current as agents and the user make edits.

## Actors and triggers

- **The session user** (single user) opens the right-side file panel and switches its tab from Files to
  Git. The Git tab is the trigger; switching back to Files restores the file tree in the same slot
  ([D1](artifacts/decision-log.md#d1--placement-a-tab-inside-the-file-browser)).
- The panel is available in any session that has the file panel, because the stage / commit / discard
  actions are the human user's own UI actions, not assistant tools — the AI has no access to these
  endpoints, and the artifact sandbox prevents a rendered artifact from reaching them
  ([D8](artifacts/decision-log.md#d8--git-write-is-a-user-action-not-an-assistant-tool),
  [D12](artifacts/decision-log.md#d12--git-write-security-posture)).
- An ambient indicator on the file-panel header signals the repository is dirty, making the Git tab
  findable without opening it ([D17](artifacts/decision-log.md#d17--ambient-dirty-indicator-and-empty-state-hint)).

## Primary flow

1. The user opens the right-side file panel and selects the **Git** tab.
2. The panel shows the changes of the **project's repository**, defaulting its comparison mode by the
   repository state on first open: **Uncommitted** (working tree vs. the last commit) when the working
   tree is dirty, **Committed** (the current branch vs. its base) when it is clean. Once the user
   selects a mode explicitly, that choice is pinned for the session and subsequent refreshes do not
   override it; if a refresh would change the auto-selected mode (e.g. the tree went clean while
   Uncommitted was pinned), the panel briefly notes the change rather than swapping silently
   ([D2](artifacts/decision-log.md#d2--scope-the-project-repository-with-two-comparison-modes),
   [D3](artifacts/decision-log.md#d3--mode-auto-selection-and-session-pinning),
   [D14](artifacts/decision-log.md#d14--mode-pinning-and-first-open-auto-selection)).
3. The panel presents a list of changed files, each with its path, change type (added / modified /
   deleted / renamed / untracked), and an added/removed line count. In Committed mode the header labels
   the base used ("Git — branch vs &lt;base&gt;")
   ([D13](artifacts/decision-log.md#d13--committed-mode-base-resolution-and-labeling)).
4. The user expands a file to read its diff inline; collapsing hides it again. A control expands or
   collapses all files at once.
5. The user **stages** or **unstages** individual files. Staged and unstaged files are grouped into
   separate sections and distinguished by both a label/icon and grouping — not color alone — and each
   stage/unstage control carries an accessible name that includes the file path.
6. The user writes a commit message and **commits** the staged files. The commit's author and committer
   identity is derived from a server-side source; the request cannot set or influence it
   ([D12](artifacts/decision-log.md#d12--git-write-security-posture)). On success the committed files
   leave the list, the panel refreshes to the new repository state, and a brief non-blocking confirmation
   is shown ([D6](artifacts/decision-log.md#d6--v1-actions-stage-unstage-commit-discard-no-push)).
7. The user may switch the comparison mode explicitly (Uncommitted ↔ Committed) at any time; the file
   list and counts update to the selected mode, and the choice is pinned for the remainder of the session.

## Alternate flows and states

- **Loading:** while the first difference is being computed the panel shows a brief loading indicator in
  place of the file list.
- **Empty (no changes):** when the selected mode has no changes the panel shows a mode-specific empty
  message ("No uncommitted changes" / "No changes vs. the base branch") instead of a file list. When
  the Git view is empty but the session has unapplied pending changes, the empty state hints that those
  live in the pending-changes panel
  ([D17](artifacts/decision-log.md#d17--ambient-dirty-indicator-and-empty-state-hint)).
- **Discard a file:** the user discards a single file's changes from an overflow or secondary affordance,
  separated from the Stage/Unstage controls (not an equal-weight sibling). Because discard is
  irrecoverable, the panel asks for a plain confirmation before acting, with wording that distinguishes
  the two cases: "Discard changes to X?" for a tracked file (which returns to its committed content) and
  "Delete X? It has never been committed and cannot be recovered" for an untracked file (which is
  permanently removed). On confirmation the file's changes are applied and the list refreshes
  ([D7](artifacts/decision-log.md#d7--discard-requires-a-plain-confirmation),
  [D15](artifacts/decision-log.md#d15--discard-is-irrecoverable-tracked-vs-untracked-confirmation-separated-affordance)).
- **Commit with an empty message or nothing staged:** the commit control is unavailable until at least one
  file is staged and a non-empty message is present.
- **Refresh:** the panel re-reads the repository state when the Git tab is opened, after any stage /
  unstage / commit / discard it performs, after an agent turn completes, after the user applies or
  discards a queued change in the pending-changes panel, and on an explicit Refresh control. Concurrent
  refresh triggers are coalesced — a refresh already in flight absorbs later triggers rather than
  spawning a second concurrent read, so the panel settles to a single final snapshot
  ([D10](artifacts/decision-log.md#d10--refresh-on-open-on-mutation-on-turn-completion-on-demand-with-coalescence)).

## Edge cases and failure modes

- **Not a git repository:** when the project's path is not a git repository the Git tab is not offered;
  the file panel stays on Files only.
- **Binary files:** a changed binary file appears in the list with its path and change type but its body
  shows a "Binary file" placeholder instead of a diff.
- **Very large diffs:** a file whose diff exceeds a display threshold appears in the list with its path and
  counts but its body shows a "Diff too large to display" placeholder; a git read that does not complete
  within a deadline exits the loading state, shows an error, and offers Refresh; the overall response is
  bounded so a huge change set cannot stall the panel
  ([D5](artifacts/decision-log.md#d5--binary-and-large-file-handling)).
- **A git action fails:** the panel surfaces the failure as an inline error — commit-area errors appear
  near the commit control; per-file action errors appear in the affected file row. The panel leaves the
  repository as close to its pre-action state as the git layer allows; the list refreshes to reflect the
  repository's true state.
- **Repository busy (index locked):** when a write fails because the repository's index is locked by
  another process (e.g. a concurrent agent turn), the panel communicates "the repository is busy, try
  again" rather than a raw error.
- **In-progress git operations:** when the repository is mid-operation (merge, rebase, cherry-pick, or
  bisect), the panel disables its write affordances and shows the repository's current state, rather than
  allowing stage / commit / discard to fail with raw errors.
- **Concurrent edits during a read:** the displayed diff is a snapshot at read time; a later edit is picked
  up on the next refresh rather than mutating the view mid-read.
- **The base branch cannot be resolved** (Committed mode, no discoverable base): the panel falls back to
  showing uncommitted changes and labels the mode as a fallback, rather than silently swapping or erroring
  ([D13](artifacts/decision-log.md#d13--committed-mode-base-resolution-and-labeling)).

## User interactions

- A **Files / Git** tab switch in the file panel header. The diff view occupies the same slot as the
  file tree; only one is shown at a time. The tab strip and header fit on one line without horizontal
  scroll or wrapping; all interactive controls meet the app's existing mobile tap-target minimum. The
  tab affordance and the panel work the same on mobile (where the panel is a slide-in drawer) as on
  desktop ([D16](artifacts/decision-log.md#d16--tab-named-git),
  [D18](artifacts/decision-log.md#d18--mobile-tap-target-and-header-fit)).
- An **ambient dirty indicator** on the file-panel toggle/header when the repository is dirty
  ([D17](artifacts/decision-log.md#d17--ambient-dirty-indicator-and-empty-state-hint)).
- A **comparison-mode** selector (Uncommitted / Committed) at the top of the Git view.
- Per-file rows showing path, change type, and an added/removed count, each expandable to reveal a
  syntax-highlighted unified diff, with an expand-all / collapse-all control.
- Files grouped into **staged** and **unstaged** sections; each section is labeled and the grouping
  itself is the primary distinction (supplemented by a per-file label/icon), not color alone; each
  stage/unstage control carries an accessible name including the file path.
- Per-file **Stage / Unstage** affordances and a **Discard** affordance in an overflow or secondary
  position (not an equal-weight sibling of Stage/Unstage), and a **commit message** field with a
  **Commit** action. Stage / Unstage / Commit / Discard are available only in Uncommitted mode;
  Committed mode is read-only review
  ([D6](artifacts/decision-log.md#d6--v1-actions-stage-unstage-commit-discard-no-push),
  [D15](artifacts/decision-log.md#d15--discard-is-irrecoverable-tracked-vs-untracked-confirmation-separated-affordance)).
- A **Refresh** control.
- Diffs render in a single (unified) layout; additions and removals are visually distinguished with line
  numbers ([D9](artifacts/decision-log.md)).

## Coordinations

- **The file panel** hosts the view and owns the Files ↔ Git tab state.
- **The project repository** is the single source of truth for the diff and the target of stage / commit /
  discard. The repository root is derived server-side from the session's project record; per-file
  arguments are validated to resolve inside the repository and rejected if they escape it; user-supplied
  text (commit message, file targets) is passed as discrete arguments and never interpreted as commands.
  The git-write actions are never registered as assistant tools; the artifact sandbox prevents a rendered
  artifact from invoking them
  ([D11](artifacts/decision-log.md#d11--all-git-operations-scoped-to-the-project-repository-path),
  [D12](artifacts/decision-log.md#d12--git-write-security-posture)).
- **The pending-changes panel** remains the place where unapplied agent edits (held in a separate working
  copy) are reviewed and applied; the diff panel reflects the project repository's real state, so applying
  or discarding a pending change is one of the events that refreshes the diff panel. The two panels are
  complementary, not duplicates ([D2](artifacts/decision-log.md#d2--scope-the-project-repository-with-two-comparison-modes)).
- **Agent turns and the user's own edits** change the repository; turn completion is a refresh trigger.

## Out of scope

- Pushing, pulling, creating pull requests, merging, or any operation that talks to a remote.
- Per-line or per-hunk review comments and "send selected lines to an agent" — that is a separate feature
  (the diff-line re-prompt item), and this panel deliberately does not build a line-selection/commenting
  surface.
- Side-by-side (split) diff layout.
- Staging or discarding individual hunks/lines (stage and discard operate at whole-file granularity).
- A live file-system watcher that streams diffs as files change on disk; refresh is event- and
  demand-driven, not continuous.
- Showing the session agent's separate working-copy diff in this panel; that remains the pending-changes
  panel's job.
- Renaming the existing pending-changes panel; naming and scope changes to that panel are out of scope
  for this feature.

## Deferred (YAGNI)

- **Push / pull / pull-request / merge actions.** Deferred — not requested (the request was "stage/commit"),
  and the assistant-level no-remote-write rule signals remotes are out of band for now. Reopening trigger: a
  stated need to publish commits from the panel. Evidence gate failed: no user-described need.
- **Side-by-side diff layout.** Deferred — the primary surface is mobile-first and unified reads well there;
  a split layout is a desktop-only enhancement. Reopening trigger: a request to compare wide files
  side-by-side on desktop. Evidence gate: simpler unified version satisfies the stated need.
- **Per-hunk staging / discarding.** Deferred — whole-file granularity covers the stated stage/commit need.
  Reopening trigger: a request to commit part of a file.
- **Continuous file-watch streaming of the diff.** Deferred — event- and demand-driven refresh covers a
  single-user workflow without a watcher's cost. Reopening trigger: the diff is observed to feel stale
  between refresh events in practice.

## Open items

- None. Commit author/committer identity is settled: derived server-side from the host git configuration;
  the request cannot set or influence it (F3, [D12](artifacts/decision-log.md#d12--git-write-security-posture)).

## Summary

A Files / Git tab in the right-side file panel that shows the project repository's diff in two modes
(uncommitted vs. HEAD, and the branch vs. its upstream/default base, auto-selected by repo state on
first open and then pinned to the user's choice), lets the user stage, unstage, commit (with
server-derived identity), and discard whole files (with irrecoverable-discard confirmation distinguishing
tracked and untracked cases), and stays current via coalesced event- and demand-driven refresh. Single
actor (the session user); the panel is complementary to the existing pending-changes panel. The tab is
named "Git" (Files / Git), distinct from the pending-changes panel. An ambient dirty indicator makes it
findable. Write affordances are disabled during in-progress git operations. 18 decisions recorded. Four
items deferred under YAGNI (remote actions, split layout, per-hunk granularity, file-watch streaming).
Review team: junior-developer, user-experience-designer, adversarial-security-analyst, on-call-engineer.
No load-bearing mechanics required a technical-notes file (the git mechanics are discoverable from the
codebase's existing git-metadata path).
