# Decision log — Git diff panel

Decisions behind [`feature-specification.md`](../feature-specification.md). Full decisions carry rationale,
evidence, and rejected alternatives; trivial decisions are one-liners. Shared D# counter.

## Full decisions

### D1 — Placement: a tab inside the file browser
**Question:** Where does the diff view live in the workspace?
**Decision:** The diff view lives in the right-side file panel as a Files / Git tab, occupying the same
slot as the file tree, rather than as a new standalone workspace pane.
**Rationale:** The request was "instead of the file browser," and the file browser is a right-side sidebar,
not a workspace-grid pane. The reference design (Paseo) puts Changes / Files tabs in one sidebar slot. A
new workspace pane would require new pane-kind plumbing for an affordance the user described as a
replacement, not an addition.
**Evidence:** User answer (2026-06-02). Codebase: the file browser is the right-rail sidebar; the legacy
"file_browser" pane kind is unused. Paseo `explorer-sidebar.tsx` (Changes/Files tabs in one slot).
**Rejected alternatives:**
- A standalone git-diff workspace pane — rejected: it is an addition, not a replacement, and adds pane
  plumbing the user did not ask for.
**Driven by findings:** —
**Linked technical notes:** —
**Dependent decisions:** D9, D10.
**Referenced in spec:** Actors and triggers, Primary flow, User interactions.

### D2 — Scope: the project repository, with two comparison modes
**Question:** What repository and comparison modes does the panel cover?
**Decision:** The panel shows the **project repository's** changes, with a selector between **Uncommitted**
(working tree vs. last commit) and **Committed** (current branch vs. its upstream tracking branch when
set, otherwise the repository's default branch). It does not show the session agent's separate
working-copy diff — that remains the pending-changes panel's job. In Committed mode the view labels the
base it resolved ("Git — branch vs &lt;base&gt;"); when no base resolves the panel falls back to
Uncommitted and labels the mode as a fallback.
**Rationale:** The file browser is scoped to the project repository, so the diff "instead of" it should
share that scope. The user chose both comparison modes (Paseo-style) over a single mode. The agent
working-copy diff is already surfaced by the pending-changes panel; duplicating it here would create two
overlapping surfaces. Labeling the base prevents silent ambiguity (F11).
**Evidence:** User answer (2026-06-02, "Both, with a selector"). Codebase: the file browser and project
git-metadata are scoped to the project path; agent worktree diffs already flow to the pending-changes
panel. Paseo uncommitted/committed mode selector. F11 (base unlabeled finding).
**Rejected alternatives:**
- Project working tree only — rejected: user wanted both modes.
- Session agent worktree — rejected: overlaps the pending-changes panel and is not the file browser's scope.
- No base labeling — rejected (F11): the base is ambiguous between upstream tracking branch and default
  branch; unlabeled output invites confusion.
**Driven by findings:** F11.
**Linked technical notes:** —
**Dependent decisions:** D3, D4, D5, D11, D13.
**Referenced in spec:** Outcome, Primary flow, Edge cases and failure modes, Coordinations.

### D3 — Mode auto-selection and session pinning
**Question:** How is the initial mode chosen and how does it behave across refreshes?
**Decision:** Auto mode-selection applies on first open only: Uncommitted when the working tree is dirty,
Committed when it is clean. Once the user selects a mode explicitly it is pinned for the session;
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.
**Rationale:** Paseo's convention is auto-select by state. However, a refresh-triggered silent mode swap
would dislocate the user's view without warning — they could be mid-review and suddenly see a different
file list (F12). Pinning after explicit selection preserves the user's intent.
**Evidence:** Paseo convention (auto-select by dirty state). F12 (silent-dislocation finding;
design-judgment resolution).
**Rejected alternatives:**
- Always auto-select on every refresh — rejected (F12): silently dislocates the view when the tree state
  changes mid-session.
- No auto-selection (always start in a fixed mode) — rejected: ignores the most useful starting point.
**Driven by findings:** F12.
**Linked technical notes:** —
**Dependent decisions:** D14.
**Referenced in spec:** Primary flow, Alternate flows and states.

### D5 — Binary and large-file handling
**Question:** What does the panel show for files it cannot diff or whose diff is too large?
**Decision:** Binary files show a "Binary file" placeholder instead of a diff body. Files over a display-
size threshold show "Diff too large to display" in place of the diff body. A git read that does not
complete within a deadline exits the loading state, shows an error, and offers Refresh. The total
response payload is bounded so a huge change set cannot stall the panel.
**Rationale:** Paseo-style caps prevent the panel from hanging or overflowing on large repos. The read-
deadline (F7) is a distinct concern from the large-result cap: a slow git process can stall the panel
even when individual files are small.
**Evidence:** Paseo codebase (display-size caps). F7 (hanging git-read finding; evidence-backed addition).
**Rejected alternatives:**
- No cap — rejected: a huge change set can stall or overflow the panel.
- Combine deadline and size cap into one mechanism — rejected (F7): they address different failure modes
  (slow process vs. large output); both are needed.
**Driven by findings:** F7.
**Linked technical notes:** —
**Dependent decisions:** —
**Referenced in spec:** Edge cases and failure modes.

### D6 — v1 actions: stage, unstage, commit, discard (no push)
**Question:** Which write actions are included in v1, and in which modes are they available?
**Decision:** v1 includes staging/unstaging files, committing staged files with a message, and discarding a
file's changes — all available only in Uncommitted mode. Committed mode is read-only review with no write
actions. Pushing, pulling, PRs, and merges are excluded. Commit author/committer identity is derived
server-side; the request cannot set or influence it.
**Rationale:** The user chose to include stage/commit over a read-only view. Remote operations were not
requested and the assistant-level rule already treats remote writes as out of band, so v1 stops at local
history. Allowing write actions in Committed mode would mean reverting committed history (per-file resets
of committed commits), which was not requested and creates a different risk profile. Committer identity
must be server-derived to prevent the request body from spoofing authorship (F3).
**Evidence:** User answer (2026-06-02, "Include stage/commit"). Convention: the assistant cannot push to
remotes (project docs) — signals remotes are deliberately out of band. F3 (committer-identity
finding). F14 (mode-scoping finding; design-judgment resolution).
**Rejected alternatives:**
- Read-only review — rejected by the user.
- Full git actions incl. push/pull/PR (Paseo's set) — deferred (YAGNI): not requested.
- Write actions in Committed mode too — rejected (F14): reverting committed history is a distinct,
  unrequested capability with a different risk profile.
- Request-supplied commit identity — rejected (F3): allows spoofing; server-derived identity is the only
  safe source.
**Driven by findings:** F3, F14.
**Linked technical notes:** —
**Dependent decisions:** D7, D8, D12.
**Referenced in spec:** Primary flow, User interactions.

### D7 — Discard requires a plain confirmation
**Question:** What confirmation does discard require, and what wording?
**Decision:** Discarding a file's changes prompts a plain Cancel / Discard confirmation with wording that
distinguishes the two cases: "Discard changes to X?" for a tracked file (reverts to committed content)
and "Delete X? It has never been committed and cannot be recovered" for an untracked file (permanently
removed). Stage, unstage, and commit do not prompt.
**Rationale:** Discard is the only irreversible action in the set; a confirmation guards an accidental tap,
especially on mobile. The project's stated preference is plain confirm dialogs, never type-the-name
patterns. A tracked revert and an untracked permanent delete are different losses — the user deserves to
know which one they are confirming (F4).
**Evidence:** Convention: destructive actions use plain Cancel/Confirm dialogs (no type-to-confirm).
Stage/unstage/commit are reversible (commits can be amended/reset), so they need no prompt. F4
(tracked-vs-untracked and affordance-separation finding; design-judgment resolution).
**Rejected alternatives:**
- No confirmation — rejected: irreversible data loss on a stray tap.
- Type-the-filename-to-confirm — rejected: against the project's confirmation convention.
- Single generic confirmation for tracked and untracked — rejected (F4): hides the difference in
  consequence (revert vs. permanent delete) from the user.
**Driven by findings:** F4.
**Linked technical notes:** —
**Dependent decisions:** D15.
**Referenced in spec:** Alternate flows and states.

### D8 — Git write is a user action, not an assistant tool
**Question:** Are the panel's write actions gated by session type (e.g. read-only-assistant sessions)?
**Decision:** The diff panel's write actions (stage/commit/discard) are available wherever the file panel
appears, including read-only-assistant sessions, because they are the human user's own UI actions, not
the AI's. The git-write endpoints are never registered as assistant tools, and the artifact sandbox
prevents a rendered artifact from invoking them.
**Rationale:** The "read-only" rule constrains what the AI assistant's tools may do. A human committing
their own repository through a panel is a different actor; gating the panel by session type would be a
category error and produce inconsistent behavior across sessions. The artifact-sandbox commitment (F1)
closes the indirect path an artifact might otherwise exploit.
**Evidence:** Convention: the read-only invariant is defined over the assistant's tool surface; the file
browser (also user-driven) already appears in all sessions. F1 (artifact-sandbox finding; evidenced by
`connect-src 'none'` in the artifact iframe sandbox per BOOCHAT.md output-format section).
**Rejected alternatives:**
- Restrict the write actions to write-capable (coder) sessions only — rejected: conflates the assistant's
  tool permissions with the user's UI affordances; produces inconsistent behavior.
**Driven by findings:** F1.
**Linked technical notes:** —
**Dependent decisions:** D12.
**Referenced in spec:** Actors and triggers, Coordinations.

### D10 — Refresh on open, on mutation, on turn completion, on demand, with coalescence
**Question:** When does the panel re-read the repository, and how are concurrent triggers handled?
**Decision:** 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. Continuous file-watching is
excluded.
**Rationale:** These triggers cover every event that can change the project repository's state within the
app's single-user workflow. Coalescence (F8) prevents a burst of triggers (e.g. multiple rapid mutations)
from causing redundant concurrent reads or a stale intermediate result overwriting a fresher one. A
continuous file watcher adds cost without a multi-user need.
**Evidence:** Convention: event-driven refresh follows the session-event / broker model already used for
other panels. F8 (concurrent-refresh finding; evidence-backed addition).
**Rejected alternatives:**
- Continuous file-watch stream — rejected (YAGNI): event- and demand-driven covers the single-user case;
  deferred under YAGNI.
- No coalescence (each trigger spawns its own read) — rejected (F8): can produce concurrent reads and
  stale-snapshot overwrites.
**Driven by findings:** F8.
**Linked technical notes:** —
**Dependent decisions:** —
**Referenced in spec:** Alternate flows and states.

### D11 — All git operations scoped to the project repository path
**Question:** How is the git operation target scoped and validated?
**Decision:** Every read and write the panel performs is confined to the project's own repository. The
repository root is derived server-side from the session's project record — never from the request. Per-
file arguments are validated as repo-relative paths and rejected if they escape the repository root.
User-supplied text (commit message, file arguments) is passed as discrete arguments and never
interpolated into a shell string.
**Rationale:** The panel acts on the project repo only. Deriving the root server-side and validating
per-file arguments closes the path-escape and command-injection vectors (F2). Passing text as discrete
arguments (not a shell string) ensures user-supplied content cannot be interpreted as git flags or shell
syntax.
**Evidence:** Convention: project file operations resolve and scope to the project path via the existing
path-scoping guard; git-metadata reads already do this. F2 (derivation + argument-safety finding;
evidenced by the existing path-scoping guard).
**Rejected alternatives:**
- Accept a caller-supplied repository path — rejected: needless write surface, no use case.
- Validate path only at the root level (not per-file arguments) — rejected (F2): per-file arguments can
  escape the repo root via `../` traversal if not independently validated.
- Build git invocations as a shell string — rejected (F2): user-supplied content (commit message, file
  names with special characters) can be interpreted as flags or shell syntax.
**Driven by findings:** F2.
**Linked technical notes:** —
**Dependent decisions:** D12.
**Referenced in spec:** Coordinations.

### D12 — Git-write security posture
**Question:** What are the combined security commitments for the git-write surface?
**Decision:** The git-write surface (stage / commit / discard) has three security commitments: (1) these
actions are user-initiated UI actions only and are never registered as assistant tools; the artifact
sandbox prevents a rendered artifact from invoking them; (2) all git operations target only the project's
own repository, with the root derived server-side and per-file paths validated inside it; (3) commit
author/committer identity is derived from a server-side source (host git configuration) and cannot be set
by the request.
**Rationale:** F1, F2, and F3 each attacked a distinct vector — artifact-driven invocation, path/argument
injection, and identity spoofing — that D8 and D11 individually did not close. D12 records all three
commitments together as the complete security posture of the write surface.
**Evidence:** F1 (artifact-sandbox; `connect-src 'none'` per BOOCHAT.md output-format section).
F2 (path-scoping guard in the codebase; derivation and validation commitments). F3 (server-derived
identity commitment; design-judgment that no request field should influence authorship).
**Rejected alternatives:**
- Trust the client-supplied repository path — rejected (F2): see D11.
- Allow request-supplied commit identity — rejected (F3): allows spoofing; no legitimate use case in a
  single-user app.
- Rely on session-type gating instead of endpoint-level exclusion from tool registry — rejected (F1):
  session type is the wrong layer; artifact-sandbox closes the actual indirect path.
**Driven by findings:** F1, F2, F3.
**Linked technical notes:** —
**Dependent decisions:** —
**Referenced in spec:** Actors and triggers, Coordinations.

### D13 — Committed-mode base resolution and labeling
**Question:** What is the base for Committed mode, and how is it surfaced when resolution fails?
**Decision:** Committed mode compares the current branch against its upstream tracking branch when one is
set, falling back to the repository's default branch (main/master). The panel labels the base it used in
the mode header ("Git — branch vs &lt;base&gt;"). When no base resolves (no tracking branch and no
discoverable default branch), the panel falls back to showing uncommitted changes and labels the mode as
a fallback, rather than erroring or silently swapping.
**Rationale:** "Base" was undefined in D2, leaving the committed comparison ambiguous (F11). The tracking-
branch-first resolution matches git's own upstream model and is the most useful default for contributors
tracking a remote. Labeling the resolved base makes the comparison unambiguous to the user. A labeled
fallback is more informative than an error and does not leave the panel empty.
**Evidence:** F11 (base-unlabeled finding; UX-002, JD-002; design-judgment resolution). Git upstream
model (tracking branch as natural "base" for a contributor's branch).
**Rejected alternatives:**
- Always compare against the default branch, ignoring tracking — rejected (F11): wrong for contributors
  whose tracking branch is a personal fork or a PR target branch, not the default.
- Error when no base resolves — rejected: leaves the panel useless; an unlabeled fallback is more
  helpful.
- Silently swap to uncommitted without a label — rejected (F11): the original spec's behavior; confusing
  because the mode selector still shows "Committed".
**Driven by findings:** F11.
**Linked technical notes:** —
**Dependent decisions:** —
**Referenced in spec:** Primary flow, Edge cases and failure modes, User interactions.

### D14 — Mode pinning and first-open auto-selection
**Question:** Does auto mode-selection persist across refreshes after the user has acted?
**Decision:** Auto mode-selection applies on first open only. Once the user selects a mode explicitly (via
the selector), that choice is pinned for the session. Refreshes do not override a pinned mode. If a
refresh would change the auto-selected mode (e.g. the tree transitioned from dirty to clean while
Uncommitted was pinned), the panel briefly notes the change rather than swapping silently.
**Rationale:** Auto-select on every refresh would dislocate the user mid-review without warning —
illustrated by the scenario where the tree goes clean while the user is reading the uncommitted diff (F12).
A brief note on a state change preserves awareness without overriding intent.
**Evidence:** F12 (silent-dislocation finding; design-judgment resolution). D3 (auto-selection origin).
**Rejected alternatives:**
- Re-run auto-selection on every refresh — rejected (F12): dislocates the user's active view.
- No notification on a would-be mode change — rejected: leaves the user unaware that the repository
  state changed in a way that would normally affect the view.
**Driven by findings:** F12.
**Linked technical notes:** —
**Dependent decisions:** —
**Referenced in spec:** Primary flow.

### D15 — Discard is irrecoverable; tracked vs. untracked confirmation; separated affordance
**Question:** What are the full discard semantics and the UI placement of the discard control?
**Decision:** Discard is hard-delete and irrecoverable. The confirmation dialog uses two distinct wordings:
"Discard changes to X?" for a tracked file (which reverts to its committed content; the work is lost but
the file remains in history) and "Delete X? It has never been committed and cannot be recovered" for an
untracked file (permanent deletion with no recovery path). The Discard affordance is placed in an
overflow or secondary position rather than as an equal-weight sibling of Stage/Unstage.
**Rationale:** The spec previously called discard "irrecoverable" but left the git mechanic ambiguous.
Owning the word and spelling out the two cases (F4) ensures the confirmation is honest. Separating the
affordance from Stage/Unstage reduces the risk of an accidental tap on mobile (F4, UX concern).
**Evidence:** F4 (discard-semantics and affordance-separation finding; on-call-engineer OCE-002,
UX-005, adversarial-security-analyst; design-judgment resolution). Convention: plain
Cancel/Confirm dialogs.
**Rejected alternatives:**
- Single generic confirmation for tracked and untracked cases — rejected (F4): obscures the difference
  in consequence.
- Discard at equal weight alongside Stage/Unstage — rejected (F4): accidental-tap risk on mobile.
**Driven by findings:** F4.
**Linked technical notes:** —
**Dependent decisions:** —
**Referenced in spec:** Alternate flows and states, User interactions.

### D16 — Tab named "Git"
**Question:** What is the new tab called?
**Decision:** The new tab is named **Git**, giving a Files / Git tab pair. The existing "Pending Changes"
panel is not renamed; that rename is out of scope.
**Rationale:** "Changes" (the working name in the initial spec) collides with "Pending Changes" — the name
of an existing distinct panel — creating discoverability confusion (F10, UX-001, UX-008, JD-001). "Git"
is shorter, unambiguous, and describes the surface (the project's git state) without implying overlap
with the pending-changes panel.
**Evidence:** F10 (naming-collision finding; design-judgment resolution). Existing surface name: "Pending
Changes" panel in the codebase.
**Rejected alternatives:**
- "Changes" — rejected (F10): collides with "Pending Changes"; confusion in context-switching.
- Rename "Pending Changes" to disambiguate — rejected (F10): out of scope; would require changes to an
  existing surface the user did not ask to rename.
**Driven by findings:** F10.
**Linked technical notes:** —
**Dependent decisions:** —
**Referenced in spec:** Actors and triggers, User interactions, Out of scope.

### D17 — Ambient dirty indicator and empty-state hint
**Question:** How does the user discover the Git tab when the panel defaults to Files?
**Decision:** An ambient indicator on the file-panel toggle/header signals the repository is dirty (derived
from the refresh data already gathered), making the Git tab findable without opening it. 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.
**Rationale:** Without a visual signal the Git tab is invisible until the user already knows to look for it
(F10, UX-001). The indicator reuses state already gathered by the refresh cycle — no additional read
needed. The empty-state hint prevents the user from concluding the panel is broken when what they are
looking for is actually in the adjacent pending-changes panel.
**Evidence:** F10 (discoverability finding; design-judgment resolution). Refresh cycle already produces
dirty/clean state (D10).
**Rejected alternatives:**
- No ambient indicator (rely on the user knowing the tab exists) — rejected (F10): undiscoverable by
  new users.
- Always show dirty indicator (not just when dirty) — rejected: misleading on clean repos.
**Driven by findings:** F10.
**Linked technical notes:** —
**Dependent decisions:** —
**Referenced in spec:** Actors and triggers, Alternate flows and states.

### D18 — Mobile tap-target and header-fit
**Question:** What are the layout and accessibility constraints for the new tab and controls?
**Decision:** All interactive controls in the diff panel follow the app's existing mobile tap-target
minimum. The Files / Git tab strip and header fit on one line without horizontal scroll or wrapping;
existing header elements are condensed if needed to maintain fit.
**Rationale:** The app has an existing toolbar-fit rule (no scroll/wrap on crowded control bars) and a
mobile-first posture. The new Git tab and its in-panel controls must not break either. Condensing
existing elements rather than scrolling is the project's established pattern (F15, UX-009, JD-008).
**Evidence:** F15 (mobile-fit finding; convention). Project convention: toolbars must fit one line (no
scroll or wrapping); MEMORY.md toolbar-fit rule.
**Rejected alternatives:**
- Allow horizontal scroll if the header gets crowded — rejected: against the project's toolbar-fit rule.
- Wrap the header to a second line — rejected: against the project's toolbar-fit rule.
**Driven by findings:** F15.
**Linked technical notes:** —
**Dependent decisions:** —
**Referenced in spec:** User interactions.

## Trivial decisions

- D4: Untracked files included in Uncommitted view — untracked files appear in the Uncommitted file list as additions (considered tracked-only; rejected because the user's new files are part of "what changed"). — Referenced in spec: Primary flow.
- D9: Unified layout, syntax-highlighted — diffs render in a single-column unified layout reusing the existing code highlighter (considered side-by-side; deferred under YAGNI as a desktop-only enhancement). — Referenced in spec: User interactions.
