Skip to content

Commit 8a7420a

Browse files
konsta95claude
andcommitted
Merge anthropics#98357 (fb1e991) into the F1-F3 bundle
poteat's HEAD watch from the pin, quiet polling on unusual branch names, and the /clear shown mark, fitted into the bundle's ownership model: - register(): poteat's doc wording, with anthropics#94847's in-tree fetch clause kept. - probeBackend: startPoll runs right after the pin, as poteat's does after `backend ??= probed`. Here the pin follows the owner check, so a probe overtaken while it reads the store starts no watch. - refresh: a listed read no longer starts the poll (poteat). - placePane: poteat's keepBaseline() on open. The shown mark stays with the bundle's recordShown, which is fenced by epoch and close. - openOnRestore: the bundle's epoch guard is kept. poteat's change there only restyles the same read. - /clear under an open pane (confirmSessionChange): the pane counts as shown for the new conversation with trigger 'manual', and its HEAD baseline is kept after the pin (poteat's two additions). - register.test.ts: anthropics#94847's resumed-session test is kept, and poteat's retitle is taken. Co-authored with a harness running Claude and Codex teams. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: 16cb7844-98c6-4af7-b67a-3e9d16705e7f
2 parents e116d5a + fb1e991 commit 8a7420a

4 files changed

Lines changed: 218 additions & 37 deletions

File tree

‎mods/diff/hooks/git/probes/head-key-of/head-key-of.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { stampProbeOf } from '../stamp-probe-of'
99
* A string that changes when HEAD moves: HEAD's text and the timestamps
1010
* of HEAD, its ref file and packed-refs, each a listed real file.
1111
*
12-
* A HEAD that is not a plain file, or a `ref:` outside the safe refname
13-
* grammar, is read through `git rev-parse` instead and nothing is stat'ed;
14-
* every directory above a stat'ed file was listed as a real directory.
12+
* A `ref:` outside the safe refname grammar is dated by HEAD's own log, the
13+
* listed real file `logs/HEAD`; without one, or where HEAD is not a plain
14+
* file, `git rev-parse` answers instead.
1515
*
1616
* @param deps git, listings, timestamps, a file read
1717
* @param repository the repository's directories
@@ -54,13 +54,16 @@ export async function headKeyOf(
5454
const isSymbolic = ref !== ''
5555

5656
const isUnsafeRef = isSymbolic && !isSafeRefName(ref)
57+
const logged = isUnsafeRef ? await stampIfFile(own, 'logs/HEAD') : null
58+
const isUndated = isUnsafeRef && typeof logged !== 'number'
59+
const isRefDated = isSymbolic && !isUnsafeRef
5760

58-
return isUnsafeRef
61+
return isUndated
5962
? viaGit()
6063
: [
6164
head,
6265
await stampIfFile(own, 'HEAD'),
63-
isSymbolic ? await stampIfFile(common, ref) : null,
66+
isRefDated ? await stampIfFile(common, ref) : logged,
6467
await stampIfFile(common, 'packed-refs'),
6568
].join('|')
6669
}

‎mods/diff/hooks/register.ts‎

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,10 @@ import Views from './views'
3434
* Registers the diff pane: `/diff` once the built-in stands down, the
3535
* pane's drawing and refresh, its opening on Claude's first edit, the ask.
3636
*
37-
* Git runs when the built-in's would: `session.start` binds the host and
38-
* registers `/diff`, and off its dispatch reads the transcript, so a resumed
39-
* session whose turns already edited opens as its first edit would; `/diff`
40-
* or the main loop's first checkpointed edit with room pins the backend,
41-
* until `/clear`, which reads afresh under a pane it leaves open; a docked
42-
* pane fetches, then opens, and such an edit inside the tree fetches until
43-
* one lists a file to open on.
37+
* Git runs when the built-in's would: none at the start; `/diff` or the main
38+
* loop's first checkpointed edit with room pins the backend, until `/clear`,
39+
* which reads afresh under a pane it leaves open; such an edit inside the
40+
* tree fetches until one lists a file to open on.
4441
*
4542
* @param on the engine's registrar
4643
*/
@@ -207,6 +204,7 @@ export function register(on: On) {
207204

208205
if (!lifecycle.isCurrent(owner)) return false
209206
backend = probed
207+
startPoll(engine, probed)
210208

211209
const mode = stored && probed.baseModes.includes(stored) ? stored : null
212210

@@ -389,6 +387,25 @@ export function register(on: On) {
389387
).includes(null)
390388
}
391389

390+
function keepBaseline() {
391+
const pinned = backend
392+
393+
if (!pinned || polled.headKey !== '') {
394+
return
395+
}
396+
397+
void pinned
398+
.headKeyOf()
399+
.catch(() => '')
400+
.then(key => {
401+
const isFirst = backend === pinned && polled.headKey === ''
402+
403+
if (isFirst) {
404+
polled.headKey = key
405+
}
406+
})
407+
}
408+
392409
function startPoll(engine: Host, pinned: Backend.Backend) {
393410
if (polled.toplevel === pinned.repository.toplevel) {
394411
return
@@ -495,7 +512,6 @@ export function register(on: On) {
495512
break
496513
case 'data':
497514
generation += 1
498-
if (pinned) startPoll(engine, pinned)
499515
break
500516
}
501517

@@ -652,6 +668,7 @@ export function register(on: On) {
652668
}
653669

654670
isPaneOpen = true
671+
keepBaseline()
655672
paneTrigger = trigger
656673
void recordShown(engine)
657674

@@ -1143,8 +1160,7 @@ export function register(on: On) {
11431160

11441161
on('ui.render', { component: 'PromptHint' }, ($, e, next) => {
11451162
if (isOnPaneSurface(e)) {
1146-
const viewport: { columns?: number; isFullscreen?: boolean } | undefined =
1147-
e.viewport
1163+
const { viewport } = e
11481164

11491165
const isFirstMeasure = columns === null && viewport?.columns !== undefined
11501166

@@ -1419,9 +1435,13 @@ export function register(on: On) {
14191435
change.release(true)
14201436

14211437
if (isPaneOpen) {
1438+
if (change.reason === 'clear') paneTrigger = 'manual'
14221439
void recordShown(engine)
14231440
await pinBackend(engine)
1424-
if (lifecycle.isSession(change.owner) && isPaneOpen) void refresh(engine)
1441+
if (lifecycle.isSession(change.owner) && isPaneOpen) {
1442+
keepBaseline()
1443+
void refresh(engine)
1444+
}
14251445
}
14261446
if (lifecycle.isSession(change.owner) && change.reason === 'resume') {
14271447
void openOnRestore(engine).catch(() => undefined)

‎mods/diff/tests/git/probes/head-key-of.test.ts‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe('head-key-of', () => {
1616
function recorded(
1717
listings: Readonly<Record<string, ReadonlyMap<string, Git.EntryKind>>>,
1818
head: string,
19+
stampOf: (path: string) => number = () => Fixtures.STAMP,
1920
): Recorded {
2021
const statted: string[] = []
2122
const read: string[] = []
@@ -30,7 +31,7 @@ describe('head-key-of', () => {
3031
mtimeOf: path => {
3132
statted.push(path)
3233

33-
return Promise.resolve(Fixtures.STAMP)
34+
return Promise.resolve(stampOf(path))
3435
},
3536
readFile: path => {
3637
read.push(path)
@@ -57,6 +58,27 @@ describe('head-key-of', () => {
5758
'/r/.git/refs/heads': heads,
5859
}
5960

61+
const logged = {
62+
'/r/.git': new Map<string, Git.EntryKind>([
63+
...plainGitDir,
64+
['logs', 'dir'],
65+
]),
66+
'/r/.git/logs': new Map<string, Git.EntryKind>([['HEAD', 'file']]),
67+
}
68+
69+
async function idleOn(branch: string) {
70+
const probe = recorded(logged, `ref: refs/heads/${branch}\n`)
71+
72+
return {
73+
keys: new Set([
74+
await Git.headKeyOf(probe.deps, Fixtures.CHECKOUT),
75+
await Git.headKeyOf(probe.deps, Fixtures.CHECKOUT),
76+
await Git.headKeyOf(probe.deps, Fixtures.CHECKOUT),
77+
]).size,
78+
ran: probe.ran,
79+
}
80+
}
81+
6082
test('a plain checkout reads HEAD, its ref file, packed-refs', async () => {
6183
const probe = recorded(healthy, 'ref: refs/heads/main\n')
6284
const key = await Git.headKeyOf(probe.deps, Fixtures.CHECKOUT)
@@ -79,6 +101,37 @@ describe('head-key-of', () => {
79101
expect(key).toBe(`rev:${Fixtures.HEAD_SHA}`)
80102
})
81103

104+
test('a branch named with a # starts no git while idle', async () => {
105+
expect(await idleOn('fix/#12')).toEqual({ ran: [], keys: 1 })
106+
})
107+
108+
test('a branch named outside ASCII starts no git while idle', async () => {
109+
expect(await idleOn('höher')).toEqual({ ran: [], keys: 1 })
110+
})
111+
112+
test('on such a branch a moved HEAD is seen at the next look', async () => {
113+
let moves = 0
114+
115+
const probe = recorded(logged, 'ref: refs/heads/fix/#12\n', path =>
116+
path.endsWith('/logs/HEAD') ? Fixtures.STAMP + moves : Fixtures.STAMP,
117+
)
118+
119+
const before = await Git.headKeyOf(probe.deps, Fixtures.CHECKOUT)
120+
121+
moves += 1
122+
123+
expect(await Git.headKeyOf(probe.deps, Fixtures.CHECKOUT)).not.toBe(before)
124+
expect(probe.ran, 'dated by the log of HEAD, no git started').toEqual([])
125+
})
126+
127+
test('with no log of HEAD such a branch is read through git', async () => {
128+
const probe = recorded(healthy, 'ref: refs/heads/fix/#12\n')
129+
const key = await Git.headKeyOf(probe.deps, Fixtures.CHECKOUT)
130+
131+
expect(probe.statted).toEqual([])
132+
expect(key).toBe(`rev:${Fixtures.HEAD_SHA}`)
133+
})
134+
82135
test('a HEAD that is a symbolic link is never read or stat-ed', async () => {
83136
const probe = recorded(
84137
{

0 commit comments

Comments
 (0)