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

Merge remote-tracking branch 'origin/main' into claude/diff-pane-empty-auto-openBrowse files
# Conflicts:
#	mods/diff/README.md
#	mods/diff/hooks/register.ts
#	mods/diff/tests/register.test.ts
Claude authored on Sep 19, 2026 parents d7f15cd + 92ec78f · commit d017bd665559
5 changed files+103 -12compared with d7f15cd
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
@@ -22,9 +22,10 @@
2222 a subagent's edit opens nothing, and where the surface does not say,
2323 nothing opens by itself. An edit to a file outside the repository, or one
2424 after which the diff lists nothing or cannot be read, opens nothing and
25-leaves the opening to a later edit. An open the engine leaves waiting
26-undrawn is withdrawn, so no later resize seats it, and the next edit asks
27-again.
25+leaves the opening to a later edit. A docked pane fetches before it opens,
26+as the built-in panel primes its data, so it never lands on
27+`Loading diff…`; an open the engine leaves waiting undrawn is withdrawn, so
28+no later resize seats it, and the next edit asks again.
2829
2930 Under the fullscreen layout a terminal under 110 columns gets the
3031 built-in's line asking for a wider one and nothing opens. Without that
@@ -35,8 +35,8 @@
3535 *
3636 * Git runs when the built-in's would: `session.start` binds the host and
3737 * registers `/diff`; `/diff` or the main loop's first checkpointed edit with
38- * room pins the backend, until `/clear`; a placed, open pane fetches, and so
39- * does such an edit inside the tree, until one lists a file to open on.
38+ * room pins the backend, until `/clear`; a docked pane fetches, then opens,
39+ * and such an edit inside the tree fetches until one lists a file to open on.
4040 *
4141 * @param on the engine's registrar
4242 */
@@ -430,8 +430,12 @@
430430
431431 dialogRows = isDialog ? Views.dialogRowsOf(model) : null
432432
433+ const landedBefore = landed
434+
433435 if (read) {
434436 model = PaneState.afterFetch(model, read)
437+ } else if (!isDialog) {
438+ await refresh(engine).catch(() => undefined)
435439 }
436440
437441 const opened = await engine.openPane(
@@ -457,8 +461,12 @@
457461 Record.recorderOf(engine).shown(trigger, Record.widthBucketOf(columns))
458462 }
459463
460- void refresh(engine, read)
464+ const isStale = isDialog || read !== null || landed !== landedBefore
461465
466+ if (isStale) {
467+ void refresh(engine, read)
468+ }
469+
462470 return true
463471 }
464472
@@ -953,7 +961,7 @@
953961 landed += 1
954962 }
955963
956- if (isPaneOpen && hasLanded) {
964+ if (hasLanded && isPaneOpen) {
957965 scheduleRefresh(engine)
958966 }
959967
@@ -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
@@ -784,10 +784,15 @@
784784
785785 expect(world.reads, "the edit's read is under way").toEqual([0])
786786
787- expect(await $.command.run(Fixtures.DIFF)).toEqual({
788- text: 'Diff panel shown',
789- })
787+ const shown = $.command.run(Fixtures.DIFF)
790788
789+ await world.clock.advance(1)
790+
791+ expect(
792+ await shown,
793+ '/diff reads before it opens, as the edit does',
794+ ).toEqual({ text: 'Diff panel shown' })
795+
791796 expect(await $.command.run(Fixtures.DIFF)).toEqual({
792797 text: 'Diff panel hidden',
793798 })
@@ -816,8 +821,12 @@
816821 })
817822
818823 await world.clock.settle()
824+
825+ const shown = $.command.run(Fixtures.DIFF)
826+
827+ await world.clock.advance(1)
828+ await shown
819829 await $.command.run(Fixtures.DIFF)
820- await $.command.run(Fixtures.DIFF)
821830 await $.tool.call({ tool: 'Bash', command: 'make' })
822831 await world.clock.advance(Fixtures.SETTLE_MS)
823832
@@ -843,8 +852,11 @@
843852 })
844853
845854 await world.clock.settle()
846- await $.command.run(Fixtures.DIFF)
855+
856+ const shown = $.command.run(Fixtures.DIFF)
857+
847858 await world.clock.advance(Fixtures.SETTLE_MS)
859+ await shown
848860
849861 expect(
850862 world.opened.map(pane => pane.id),
@@ -1030,6 +1042,12 @@
10301042 ).toEqual(['diff'])
10311043
10321044 expect(
1045+ world.runs.map(run => Fixtures.gitWordOf(run.argv)).slice(0, 2),
1046+ 'the repository was found and read once before the open, as the ' +
1047+ 'built-in primes; nothing polls for a pane no one sees',
1048+ ).toEqual(['rev-parse --show-toplevel', 'status'])
1049+
1050+ expect(
10331051 world.runs.map(run => Fixtures.gitWordOf(run.argv)),
10341052 'the repository was found and read once to decide, no hunks fetched ' +
10351053 'for a pane no one sees',
@@ -1041,6 +1059,11 @@
10411059 'ls-files',
10421060 ])
10431061
1062+ expect(
1063+ world.runs.map(run => Fixtures.gitWordOf(run.argv)),
1064+ 'no HEAD poll for a withdrawn pane',
1065+ ).not.toContain(Fixtures.POLL_WORD)
1066+
10441067 isNarrow = false
10451068
10461069 expect(
@@ -1094,6 +1117,59 @@
10941117 ).toEqual(['diff'])
10951118 })
10961119
1120+ test('a docked pane opens once its first fetch settled', async ($, on) => {
1121+ const opened: string[] = []
1122+ const clock = Fixtures.startsSession(on)
1123+
1124+ on('process.run', async ($, e) => {
1125+ if (e.argv.includes('--shortstat')) {
1126+ await clock.sleep(Fixtures.SLOW_DIFF_MS)
1127+ }
1128+
1129+ return { value: Fixtures.gitIn(e.argv) }
1130+ })
1131+
1132+ on('ui.open', ($, e) => {
1133+ opened.push(e.id)
1134+
1135+ return { value: undefined }
1136+ })
1137+
1138+ on('ui.close', () => ({ value: undefined }))
1139+ on('ui.invalidate', () => ({ value: undefined }))
1140+ on('ui.render', { component: 'PromptHint' }, () => Fixtures.HINT_DRAWN)
1141+ on('session.messages', () => ({ value: [] }))
1142+ on('settings.read', () => ({ value: {} }))
1143+ on('tool.call', () => ({ result: 'edited' }))
1144+ mock.store(on)
1145+ mock.env(on, {})
1146+
1147+ await $.session.start(Fixtures.SESSION)
1148+ await $.ui.render(Fixtures.HINT)
1149+
1150+ await $.tool.call({
1151+ tool: 'Edit',
1152+ file_path: '/work/app.ts',
1153+ old_string: '1',
1154+ new_string: '2',
1155+ })
1156+
1157+ await clock.advance(Fixtures.SLOW_DIFF_MS - 1)
1158+
1159+ expect(opened, 'git has not answered: no pane, no Loading frame').toEqual(
1160+ [],
1161+ )
1162+
1163+ await clock.advance(Fixtures.SETTLE_MS)
1164+
1165+ expect(opened, 'the fetch settled: the pane opens filled').toEqual(['diff'])
1166+
1167+ const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
1168+
1169+ expect(drawn).toContain('1 file changed')
1170+ expect(drawn).not.toContain('Loading diff')
1171+ })
1172+
10971173 test('/clear closes the pane it finds open', async ($, on) => {
10981174 const world = Fixtures.inRepository(on)
10991175