Skip to content

(panel): always offer Changes, and say what the directory is - #310

Merged
jbr-sekoia merged 2 commits into
mainfrom
feat/changes-tab-always-available
Sep 18, 2026
Merged

jbr-sekoia merged 2 commits into
mainfrom
feat/changes-tab-always-available

Conversation

@jbr-sekoia

Copy link
Copy Markdown
Collaborator

The behaviour

Every local session offers the Changes control. Opening it shows, in plain language, whichever of these is true:

state what the panel says
work tree, with changes the file list, under N files changed +A −B
work tree, clean No changes
no work tree This directory is not a git repository. — a note, not an error colour
a repository git refuses to open git's own message, bounded as it already was

A repository deleted under an open tab is reported into that tab on the next refresh; a git init under a session that had none is listed on the next refresh too. Nothing polls.

Remote sessions are unchanged: with no local filesystem to corroborate against, a remote 128 always reports git's message rather than the plain line.

Kept

The distinction between no repository and a repository git refuses is untouched. Exit 128 is git's generic fatal code — dubious ownership, unreadable .git permissions, an unsupported core.repositoryformatversion, a .git file whose gitdir is gone — so "no repository" is still established by gitEntryAtOrAbove, an fs.lstat walk for a .git entry up to the root, never by an exit status and never by a message match. status() still returns reason: 'not-a-repo' only on that positive evidence, and isWorkTree() keeps its full table. All of test/git-changes-runner-real-git.test.js's fixtures for the refused-repository cases are unchanged and still pin that none of them produces not-a-repo.

What changed is only what the renderer does with that answer: it picks the wording instead of deciding whether a button exists.

Removed, and the evidence it was dead

The eager availability probe existed for one purpose — to decide whether to render the button before the user could click it. With the button unconditional that purpose is gone, and status() already asks the same question at the moment the tab needs it, on the failure path it was going to take anyway.

Removed, in full:

  • git-changes-available (main handler + preload binding) — the only caller was refreshChangesAvailability; nothing else in the repo referenced the channel or gitChangesAvailable.
  • refreshChangesAvailability, updateChangesToggle, noteChangesUnavailable, CHANGES_UNANSWERED, changesAvailabilityInFlight, and the changesAvailable field on filePanelState — the memo, the in-flight dedupe, the three-way memo state, the stale-reply guard and the withdrawal path through toggleChangesTab. All of it existed to make a per-activation probe affordable and to undo a button that had already been drawn.
  • isWorkTree() stays on the runner's interface: status() calls it through cwdHasNoWorkTree(), and it is what the real-git tests assert the 128-corroboration table against.

Nothing is half-deleted: switchPanel no longer touches the Changes control at all, and the only remaining decision in this area is the one branch in refreshChanges that picks the wording.

Tests

The tests that pinned the hiding behaviour are deleted — the probe memo/dedupe/stale-reply suite, the withdrawal suite, and the dirty-buffer withdrawal cases. New tests cover: the control present with no work tree; the control present with a clean work tree; each of the four states' text reaching the DOM; a refused repository showing git's message and not the no-repository text; a repository disappearing under an open tab reporting rather than closing; a repository disappearing under unsaved edits asking nothing and keeping the buffer; and a git failure after a no-repository answer rendering as a failure.

Mutations run against the guards added, each caught:

mutation red
drop the reason === 'not-a-repo' check 2
treat every status failure as "no repository" 2
render the no-repository state with the error colour 2
drop the notARepo reset on the git-failure branch 1

npx eslint . — 0 errors, 333 warnings (unchanged from c939748).
npm test — batch 1: 1882 tests / 1879 pass / 1 fail / 2 skipped; batch 2: 120 tests / 119 pass / 0 fail / 1 skipped. The single failure is the pre-existing environmental one, test/ipc-path-validator.test.js "allows files under ~/.claude/". The pre-commit hook was bypassed for that reason; the numbers above are from a manual full run.

A control that vanishes explains nothing: its absence is
indistinguishable from a bug, and it gives the user nowhere to ask why.
The Changes button is now unconditional, and the panel it opens reports
whichever of four answers is true — the file list, "No changes", "This
directory is not a git repository.", or git's own bounded message when
there is a repository it refuses to open.

Telling those last two apart is kept exactly as it was: exit 128 is
git's generic fatal code, so "no repository" is still established by a
filesystem walk for a .git entry (gitEntryAtOrAbove), never by a message
match or an exit status, and status() still returns reason: 'not-a-repo'
only on that positive evidence. Only what the renderer does with the
answer changed.

With the button unconditional there is nothing left to decide before a
switch, so the eager availability probe goes with it: the
git-changes-available IPC, its preload binding, the three-way memo, the
in-flight dedupe, the stale-reply guard and the withdrawal path through
toggleChangesTab. status() already asks the same question at the moment
the tab needs the answer, and isWorkTree() stays on the runner for it.
…failure

With a file open, the notice above the editor borrowed both the wording
and the colour of a refresh that failed — the same sentence the list
below deliberately renders as a neutral note. A directory that has no
repository did not fail at anything, so the notice now states the fact
itself and takes the neutral colour, while every genuine failure keeps
"The file list could not be refreshed" and the error colour.

The dirty-buffer case runs over the three ways the tree can stop
answering under an open editor — the repository removed, the directory
gone, and a repository git refuses — rather than only the first.

@devsuitup devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 3940b67.

The delta since 871ca31 is scoped to the notice bar: notARepo no longer counts as alarming, the no-repository case gets its own text instead of "The file list could not be refreshed", and .ai/contexts/changes-view.md now describes the same neutral treatment for the list and the notice bar. The three tree-lost payloads (repo removed, directory gone, git refuses) keep the dirty buffer without a prompt; a genuine refresh failure still lands in the red notice. test/dom-file-panel-changes.test.js passes on this head (94/94, replayed locally), and all CI jobs are green on 3940b67.

Nothing else moved: main.js, preload.js, git-changes-runner.js, docs/changes-view.md and test/panel-terminal.test.js are identical to the previous head, so the earlier findings on those files stand as already noted.

No blocking finding. Approving.

@jbr-sekoia
jbr-sekoia merged commit 3b456bc into main Sep 18, 2026
10 checks passed
@jbr-sekoia
jbr-sekoia deleted the feat/changes-tab-always-available branch September 18, 2026 16:56
@jbr-sekoia jbr-sekoia mentioned this pull request Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants