spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

diff: a docked pane reads the repository before it opens, so it never lands on Loading diffBrowse files
poteat authored on Sep 19, 2026 parent 2287e5d · commit c97f2932af00
5 changed files+92 -10compared with 2287e5d
Mmods/diff/README.md
Mmods/diff/hooks/register.ts
Mmods/diff/tests/fixtures/beneath/index.ts
Amods/diff/tests/fixtures/beneath/slow-diff-ms.ts
Mmods/diff/tests/register.test.ts
@@ -20,8 +20,10 @@
2020 the person never chose, 110 when they kept it open before; a person who
2121 closed it is left alone) and file checkpointing is on; a subagent's edit
2222 opens nothing, and where the surface does not say, nothing opens by
23-itself. An open the engine leaves waiting undrawn is withdrawn, so no
24-later resize seats it, and the next edit asks again.
23+itself. A docked pane fetches before it opens, as the built-in panel
24+primes its data, so it never lands on `Loading diff…`; an open the engine
25+leaves waiting undrawn is withdrawn, so no later resize seats it, and the
26+next edit asks again.
2527
2628 Under the fullscreen layout a terminal under 110 columns gets the
2729 built-in's line asking for a wider one and nothing opens. Without that
@@ -34,7 +34,7 @@
3434 *
3535 * Git runs when the built-in's would: `session.start` binds the host and
3636 * registers `/diff`; `/diff` or the main loop's first checkpointed edit with
37- * room pins the backend, until `/clear`; only a placed, open pane fetches.
37+ * room pins the backend, until `/clear`; a docked pane fetches, then opens.
3838 *
3939 * @param on the engine's registrar
4040 */
@@ -53,6 +53,7 @@
5353 let isRefreshing = false
5454 let isRefreshQueued = false
5555 let generation = 0
56+ let landed = 0
5657 let bodyStamp: string | null = null
5758 let bodyBase: string | null = null
5859
@@ -409,6 +410,12 @@
409410
410411 dialogRows = isDialog ? Views.dialogRowsOf(model) : null
411412
413+ const landedBefore = landed
414+
415+ if (!isDialog) {
416+ await refresh(engine).catch(() => undefined)
417+ }
418+
412419 const opened = await engine.openPane(
413420 isDialog
414421 ? { ...dialogPane(), focus: true }
@@ -432,8 +439,12 @@
432439 Record.recorderOf(engine).shown(trigger, Record.widthBucketOf(columns))
433440 }
434441
435- void refresh(engine)
442+ const isStale = isDialog || landed !== landedBefore
436443
444+ if (isStale) {
445+ void refresh(engine)
446+ }
447+
437448 return true
438449 }
439450
@@ -860,11 +871,15 @@
860871 result.deny === undefined &&
861872 result.isError !== true
862873
863- const isStale =
864- isPaneOpen &&
865- (isEdit ? hasEdited : result === undefined || result.deny === undefined)
874+ const hasLanded = isEdit
875+ ? hasEdited
876+ : result === undefined || result.deny === undefined
866877
867- if (isStale) {
878+ if (hasLanded) {
879+ landed += 1
880+ }
881+
882+ if (hasLanded && isPaneOpen) {
868883 scheduleRefresh(engine)
869884 }
870885
@@ -1,4 +1,5 @@
11 export type * from './beneath.js'
22 export * from './left-waiting.js'
3+export * from './slow-diff-ms.js'
34
45 export * as default from '.'
@@ -1,0 +1,5 @@
1+/**
2+ * How long a slow repository takes to answer `git diff` in the tests that
3+ * time the pane's opening against its first fetch: well inside git's timeout.
4+ */
5+export const SLOW_DIFF_MS = 3000
@@ -565,9 +565,15 @@
565565 ).toEqual(['diff'])
566566
567567 expect(
568+ world.runs.map(run => Fixtures.gitWordOf(run.argv)).slice(0, 2),
569+ 'the repository was found and read once before the open, as the ' +
570+ 'built-in primes; nothing polls for a pane no one sees',
571+ ).toEqual(['rev-parse --show-toplevel', 'status'])
572+
573+ expect(
568574 world.runs.map(run => Fixtures.gitWordOf(run.argv)),
569- 'the repository was found, nothing fetched for a pane no one sees',
570- ).toEqual(['rev-parse --show-toplevel'])
575+ 'no HEAD poll for a withdrawn pane',
576+ ).not.toContain(Fixtures.POLL_WORD)
571577
572578 isNarrow = false
573579
@@ -622,6 +628,59 @@
622628 ).toEqual(['diff'])
623629 })
624630
631+ test('a docked pane opens once its first fetch settled', async ($, on) => {
632+ const opened: string[] = []
633+ const clock = Fixtures.startsSession(on)
634+
635+ on('process.run', async ($, e) => {
636+ if (e.argv.includes('--shortstat')) {
637+ await clock.sleep(Fixtures.SLOW_DIFF_MS)
638+ }
639+
640+ return { value: Fixtures.gitIn(e.argv) }
641+ })
642+
643+ on('ui.open', ($, e) => {
644+ opened.push(e.id)
645+
646+ return { value: undefined }
647+ })
648+
649+ on('ui.close', () => ({ value: undefined }))
650+ on('ui.invalidate', () => ({ value: undefined }))
651+ on('ui.render', { component: 'PromptHint' }, () => Fixtures.HINT_DRAWN)
652+ on('session.messages', () => ({ value: [] }))
653+ on('settings.read', () => ({ value: {} }))
654+ on('tool.call', () => ({ result: 'edited' }))
655+ mock.store(on)
656+ mock.env(on, {})
657+
658+ await $.session.start(Fixtures.SESSION)
659+ await $.ui.render(Fixtures.HINT)
660+
661+ await $.tool.call({
662+ tool: 'Edit',
663+ file_path: '/work/app.ts',
664+ old_string: '1',
665+ new_string: '2',
666+ })
667+
668+ await clock.advance(Fixtures.SLOW_DIFF_MS - 1)
669+
670+ expect(opened, 'git has not answered: no pane, no Loading frame').toEqual(
671+ [],
672+ )
673+
674+ await clock.advance(Fixtures.SETTLE_MS)
675+
676+ expect(opened, 'the fetch settled: the pane opens filled').toEqual(['diff'])
677+
678+ const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
679+
680+ expect(drawn).toContain('1 file changed')
681+ expect(drawn).not.toContain('Loading diff')
682+ })
683+
625684 test('/clear closes the pane it finds open', async ($, on) => {
626685 const world = Fixtures.inRepository(on)
627686