Skip to content

Commit 7dd0636

Browse files
authored
diff: run git when the built-in panel would, never at the session's start (#94594)
* diff: the session's start asks nothing of the repository; /diff or the first edit pins it when it comes * diff: git runs when the built-in panel's would: one rev-parse at first need, the tree walked only for an open pane, nothing after a turn or a refused tool * diff: a shell command that failed or was cut short still refreshes an open pane, an edit that did opens nothing, and a probe /clear overtook pins nothing
1 parent aad35ba commit 7dd0636

12 files changed

Lines changed: 443 additions & 123 deletions

File tree

‎mods/diff/README.md‎

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ click puts that file's hunks at the top; the list also scrolls under the
1111
built-in's list keys (`ctrl+up`/`ctrl+down`, `opt+up`/`opt+down`), and
1212
`ctrl+x b` moves the comparison base on, as the built-in's chord does: both
1313
through Buttons that declare the engine's own actions. The pane refreshes
14-
as Claude edits, runs shell commands and finishes turns, and while it is
15-
open it polls the repository's HEAD so a commit or checkout made elsewhere
16-
shows too. The first successful edit of a session opens the pane by itself
17-
where the terminal is wide enough (144 columns when the person never chose,
18-
110 when they kept it open before; a person who closed it is left alone).
14+
as Claude edits and runs shell commands, and while it is open it polls
15+
the repository's HEAD so a commit or checkout made elsewhere shows too.
16+
The first successful edit of a session opens the pane by itself where the
17+
terminal is wide enough (144 columns when the person never chose, 110 when
18+
they kept it open before; a person who closed it is left alone).
1919

2020
Under the fullscreen layout a terminal under 110 columns gets the
2121
built-in's line asking for a wider one and nothing opens. Without that
@@ -36,28 +36,37 @@ the default branch; the base line under the header names a base other than
3636
the session's, and the choice is kept per repository in the plugin's store.
3737
A picker shows one earlier turn's edits instead of the working tree, read
3838
from the session's messages. Files that changed before the session started
39-
(by their timestamp, among the paths already dirty at the start), and noise
40-
(lockfiles, generated and test files), are listed apart and folded until
41-
asked for; a rename lists as git prints it. Outside a git repository
42-
`/diff` says so and does nothing else.
39+
(by their timestamp, among the paths already dirty when the pane first
40+
read the repository), and noise (lockfiles, generated and test files), are
41+
listed apart and folded until asked for; a rename lists as git prints it.
42+
Outside a git repository `/diff` says so and does nothing else.
43+
44+
Git runs when the built-in panel's would: nothing at the session's start;
45+
one `git rev-parse`, in the directory the session started in, when `/diff`
46+
or the first edit a pane has room to open on first needs the repository
47+
(an answer of no repository is kept too, until `/clear` or `/resume`
48+
forgets it); and the working tree is read only by a fetch for a pane that
49+
is open, after an edit that landed or a shell command that ran. The one
50+
read the built-in has no counterpart for is a `git status` at a pane's
51+
first fetch, which stands in for the change time the built-in dates a
52+
moved file by.
4353

4454
`hooks/register.ts` is the module; everything under `hooks/` is its parts.
4555

4656
## What it hooks
4757

4858
| event | what the hook does |
4959
| --- | --- |
50-
| `session.start` | Binds the engine once, registers `/diff` (a session where another `/diff` is listed leaves the plugin idle), and pins the repository. |
60+
| `session.start` | Binds the engine once and registers `/diff` (a session where another `/diff` is listed leaves the plugin idle); asks nothing of the repository, which `/diff` or the first edit pins when it comes. |
5161
| `ui.render` of `PromptHint` | Reads the terminal's width, which decides whether the first edit opens the pane. |
5262
| `ui.render` of `Pane` | Draws the pane: docked, the header, base line, source picker, file list and toggles over the window of hunks; inline, the dialog. |
53-
| `command.run` of `diff` | Opens or closes the pane (focused and closing on Escape without the fullscreen layout), says which, and remembers the choice. |
63+
| `command.run` of `diff` | Pins the repository when none is, opens or closes the pane (focused and closing on Escape without the fullscreen layout), says which, and remembers the choice. |
5464
| `ui.close` of the pane | Backs out of the dialog's detail view instead of closing; else remembers the person's close as `/diff`'s. |
5565
| `ui.scroll` of the pane | Docked, moves the hunks under the pinned header and list (three rows a wheel tick, a page a page key), or the list when the wheel is over it, and keeps the engine's window still. |
5666
| `ui.focus` in the pane | In the dialog's list, selects the file the ring lands on, re-centres the five rows on it, and lands the ring where that row now sits. |
57-
| `command.run` of `clear`, `resume` | Closes the pane and forgets the session's state. |
58-
| `tool.call` of `Edit`, `Write`, `NotebookEdit` | After the edit, refreshes an open pane; the session's first successful edit opens it. |
59-
| `tool.call` of `Bash`, `PowerShell` | After the command, refreshes an open pane. |
60-
| `turn.complete` | Refreshes an open pane. |
67+
| `command.run` of `clear`, `resume` | Closes the pane and forgets the session's state, the pinned repository with it. |
68+
| `tool.call` of `Edit`, `Write`, `NotebookEdit` | After an edit that landed (not refused, not failed), refreshes an open pane; the session's first such edit opens it, pinning the repository then if the terminal has the room. |
69+
| `tool.call` of `Bash`, `PowerShell` | After a command that was not refused, failed and interrupted ones too, refreshes an open pane. |
6170
| `prompt.submit` | Adds the armed file's hunks to the prompt's context and disarms. |
6271

6372
## What it calls on `$`

‎mods/diff/hooks/git/git-backend-of.ts‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type Types from './types'
1515
*
1616
* Each child runs with `--git-dir`, `--work-tree` and cwd the top, under
1717
* the C locale and the fetch's timeout (the built-in's execPinnedGit); the
18-
* paths dirty at the start are read once, first (Probes.dirtyPathsOf).
18+
* first fetch reads the paths dirty then, once (Probes.dirtyPathsOf).
1919
*
2020
* @param host the bound host's runner and probes
2121
* @returns the backend, or null outside a git working tree
@@ -47,25 +47,33 @@ export async function gitBackendOf(
4747
}
4848

4949
const run = runOf(repository)
50-
const baseline = await Probes.dirtyPathsOf(run)
5150

52-
const depsOf = (): Types.GitDeps => ({
51+
let baseline: Promise<ReadonlySet<string> | null> | null = null
52+
53+
const depsOf = (dirty: ReadonlySet<string> | null): Types.GitDeps => ({
5354
run,
5455
repository,
5556
mtimeOf: host.mtimeOf,
5657
entryKindsOf: host.entryKindsOf,
5758
sessionStartMs: host.sessionStartMsOf(),
58-
baseline,
59+
baseline: dirty,
5960
onBranchBase: host.onBranchBase,
6061
})
6162

6263
return {
6364
repository,
6465
baseModes: GIT_BASE_MODES,
6566
words: GIT_WORDS,
66-
fetchDiff: mode => fetchDiff(depsOf(), mode),
67+
fetchDiff: async mode => {
68+
baseline ??= Probes.dirtyPathsOf(run)
69+
70+
return fetchDiff(depsOf(await baseline), mode)
71+
},
6772
fetchFileHunks: (data, file) => fetchFileHunks(run, data, file),
6873
headKeyOf: () =>
69-
Probes.headKeyOf({ ...depsOf(), readFile: host.readFile }, repository),
74+
Probes.headKeyOf(
75+
{ ...depsOf(null), readFile: host.readFile },
76+
repository,
77+
),
7078
}
7179
}

‎mods/diff/hooks/git/probes/dirty-paths-of/dirty-paths-of.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import { STATUS_PATH_OFFSET } from './status-path-offset'
77
* Every path dirty in the working tree right now, tracked changes and
88
* untracked files alike, as `git status -z` lists them; null unread.
99
*
10-
* Read once when the backend is pinned, so a fetch can tell a path that
11-
* was already dirty when the session began from one that turned up since
12-
* (GitDeps `baseline`). Renames are not paired: each side is its own path.
10+
* Read once, by the backend's first fetch (it walks the whole tree), so a
11+
* later fetch can tell a path that was dirty then from one that turned up
12+
* since (GitDeps `baseline`). Renames unpaired: each side is its own path.
1313
*
1414
* @param run runs git against the pinned repository
1515
* @returns the paths, root-relative, or null on a failed or cut listing

‎mods/diff/hooks/git/probes/was-dirty-at-start/was-dirty-at-start.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type Types from '../../types'
22

33
/**
44
* Whether a path may predate the session at all: it was dirty when the
5-
* session began, or nothing says otherwise (no baseline was read).
5+
* baseline was read (the first fetch), or nothing says otherwise (none was).
66
*
77
* A path missing from the baseline turned up since, so its old timestamp
88
* (a rename keeps the file's, a move keeps the source's) does not date it,
@@ -12,7 +12,5 @@ import type Types from '../../types'
1212
* @param path the root-relative path
1313
* @returns false only when a baseline exists and lacks the path
1414
*/
15-
export const wasDirtyAtStart = (
16-
context: Types.DatingContext,
17-
path: string,
18-
): boolean => context.deps.baseline?.has(path) ?? true
15+
export const wasDirtyAtStart = (context: Types.DatingContext, path: string) =>
16+
context.deps.baseline?.has(path) ?? true

‎mods/diff/hooks/git/types/git-deps/git-deps.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ export type GitDeps = {
3333
sessionStartMs: number
3434

3535
/**
36-
* The paths dirty when the backend was pinned, at the session's start
37-
* (Probes.dirtyPathsOf); null when unlisted, absent where none is kept.
36+
* The paths dirty when the backend first fetched (Probes.dirtyPathsOf),
37+
* read then and kept; null when unlisted, absent where none is kept.
3838
*
39-
* A path absent from it turned up during the session (a rename's new
40-
* name, a file moved in), whatever timestamp it carries.
39+
* A path absent from it turned up since (a rename's new name, a file
40+
* moved in), whatever timestamp it carries.
4141
*/
4242
baseline?: ReadonlySet<string> | null
4343

0 commit comments

Comments
 (0)