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: the first edit opens the pane only where the layout docks it (#94653)Browse files
* mods: the declarations are 2.1.273's, which add the vscode surface, plus a render viewport's layout

* diff: the first edit opens the pane only where the layout docks it beside the transcript
Alice T'Poteat authored on Sep 16, 2026 · GitHub committed on Sep 16, 2026 parent 7dd0636 · commit b782847db9a1
10 changed files+117 -12compared with 7dd0636
Mmods/diff/README.md
Mmods/diff/hooks/pane-state/pane-model/pane-model.ts
Mmods/diff/hooks/register.ts
Mmods/diff/tests/fixtures/hint-at.ts
Mmods/diff/tests/fixtures/hint.ts
Mmods/diff/tests/fixtures/index.ts
Amods/diff/tests/fixtures/main-screen-hint.ts
Amods/diff/tests/fixtures/unsaid-hint.ts
Mmods/diff/tests/register.test.ts
Mmods/types/claude-code.d.ts
@@ -14,8 +14,11 @@
1414 as Claude edits and runs shell commands, and while it is open it polls
1515 the repository's HEAD so a commit or checkout made elsewhere shows too.
1616 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).
17+layout docks it beside the transcript (the fullscreen layout, which each
18+drawing's `viewport` says) and the terminal is wide enough (144 columns
19+when the person never chose, 110 when they kept it open before; a person
20+who closed it is left alone); where the surface does not say, nothing
21+opens by itself.
1922
2023 Under the fullscreen layout a terminal under 110 columns gets the
2124 built-in's line asking for a wider one and nothing opens. Without that
@@ -58,7 +61,7 @@
5861 | event | what the hook does |
5962 | --- | --- |
6063 | `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. |
61-| `ui.render` of `PromptHint` | Reads the terminal's width, which decides whether the first edit opens the pane. |
64+| `ui.render` of `PromptHint` | Reads the terminal's width and whether its layout docks a pane, which decide whether the first edit opens the pane. |
6265 | `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. |
6366 | `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. |
6467 | `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. |
@@ -9,8 +9,9 @@
99 * Everything one drawing of the pane reads.
1010 *
1111 * The last good fetch, what the person picked, the bodies read so far, the
12- * turns and todos, how the surface seated it, whether fullscreen (once
13- * `/diff` said), the dialog's view, and the docked scroll in its last box.
12+ * turns and todos, how the surface seated it, whether the layout docks a
13+ * pane (once a drawing or `/diff` said), the dialog's view, and the docked
14+ * scroll in its last box.
1415 */
1516 export type PaneModel = {
1617 words: Backend.BackendWords
@@ -452,7 +452,7 @@
452452
453453 const hasRoom =
454454 preference !== false &&
455- model.isFullscreen !== false &&
455+ model.isFullscreen === true &&
456456 columns !== null &&
457457 columns >= floor
458458
@@ -622,6 +622,11 @@
622622 on('ui.render', { component: 'PromptHint' }, ($, e, next) => {
623623 if (isOnPaneSurface(e)) {
624624 columns = e.viewport?.columns ?? columns
625+
626+ model = {
627+ ...model,
628+ isFullscreen: e.viewport?.isFullscreen ?? model.isFullscreen,
629+ }
625630 }
626631
627632 return next(e)
@@ -3,13 +3,17 @@
33 import { HINT } from './hint.js'
44
55 /**
6- * The prompt's hint on a terminal so many columns wide: drawing it is how
7- * the plugin learns the width.
6+ * The prompt's hint on a fullscreen terminal so many columns wide: drawing
7+ * it is how the plugin learns the width.
88 *
99 * @param columns the terminal's width
1010 * @returns the `ui.render` input
1111 */
1212 export const hintAt = (columns: number): RenderInput<'PromptHint'> => ({
1313 ...HINT,
14- viewport: { columns, rows: HINT.viewport?.rows ?? columns },
14+ viewport: {
15+ columns,
16+ rows: HINT.viewport?.rows ?? columns,
17+ isFullscreen: true,
18+ },
1519 })
@@ -1,13 +1,14 @@
11 import type { RenderInput } from 'claude-code'
22
33 /**
4- * The prompt's hint on a 160-column terminal: drawing it is how the plugin
5- * learns how wide the terminal is.
4+ * The prompt's hint on a 160-column terminal under the fullscreen layout:
5+ * drawing it is how the plugin learns how wide the terminal is and whether a
6+ * pane docks beside the transcript there.
67 */
78 export const HINT: RenderInput<'PromptHint'> = {
89 component: 'PromptHint',
910 surface: 'terminal',
1011 requestId: 'hint',
11- viewport: { columns: 160, rows: 40 },
12+ viewport: { columns: 160, rows: 40, isFullscreen: true },
1213 props: { isDraft: false, isWorking: false, hint: '' },
1314 }
@@ -25,6 +25,7 @@
2525 export * from './inline-pane.js'
2626 export * from './keeping.js'
2727 export * from './kind-walk-of'
28+export * from './main-screen-hint.js'
2829 export * from './many-file-count.js'
2930 export * from './many-files.js'
3031 export * from './mode-label-of'
@@ -51,6 +52,7 @@
5152 export * from './turn-diffs-of'
5253 export * from './two-files.js'
5354 export * from './typed-prompt-of.js'
55+export * from './unsaid-hint.js'
5456 export * from './vs-main.js'
5557 export * from './wheel-over-list.js'
5658 export * from './wheel-tick.js'
@@ -1,0 +1,12 @@
1+import type { RenderInput } from 'claude-code'
2+
3+import { HINT } from './hint.js'
4+
5+/**
6+ * The prompt's hint on the same wide terminal on its main screen
7+ * (`CLAUDE_CODE_NO_FLICKER=0`), where nothing docks and a pane opens inline.
8+ */
9+export const MAIN_SCREEN_HINT: RenderInput<'PromptHint'> = {
10+ ...HINT,
11+ viewport: { columns: 160, rows: 40, isFullscreen: false },
12+}
@@ -1,0 +1,13 @@
1+import type { RenderInput } from 'claude-code'
2+
3+import { HINT } from './hint.js'
4+
5+/**
6+ * The prompt's hint from a surface that measured its size but did not say
7+ * whether it docks a pane: an engine that predates the word, or a remote
8+ * surface that has not reported it.
9+ */
10+export const UNSAID_HINT: RenderInput<'PromptHint'> = {
11+ ...HINT,
12+ viewport: { columns: 160, rows: 40 },
13+}
@@ -348,6 +348,70 @@
348348 expect(world.opened.map(pane => pane.id)).toEqual(['diff'])
349349 })
350350
351+ test('on the main screen the first edit opens nothing, and /diff still opens the dialog', async ($, on) => {
352+ const world = Fixtures.inRepository(on)
353+
354+ on('tool.call', () => ({ result: 'edited' }))
355+
356+ await $.session.start(Fixtures.SESSION)
357+ await $.ui.render(Fixtures.MAIN_SCREEN_HINT)
358+
359+ await $.tool.call({
360+ tool: 'Edit',
361+ file_path: '/work/app.ts',
362+ old_string: '1',
363+ new_string: '2',
364+ })
365+
366+ await world.clock.advance(Fixtures.SETTLE_MS)
367+
368+ expect(world.opened, 'a pane there would be an unasked dialog').toEqual([])
369+ expect(world.runs, 'so nothing was asked of the repository').toEqual([])
370+
371+ expect(await $.command.run(Fixtures.DIALOG_DIFF)).toEqual({})
372+
373+ expect(
374+ world.opened[0],
375+ "the person's /diff opens the dialog, not dismisses one",
376+ ).toMatchObject({ id: 'diff', focus: true })
377+ })
378+
379+ test('a surface that does not say whether it docks a pane opens nothing at the first edit', async ($, on) => {
380+ const world = Fixtures.inRepository(on)
381+
382+ on('tool.call', () => ({ result: 'edited' }))
383+
384+ await $.session.start(Fixtures.SESSION)
385+ await $.ui.render(Fixtures.UNSAID_HINT)
386+
387+ await $.tool.call({
388+ tool: 'Edit',
389+ file_path: '/work/app.ts',
390+ old_string: '1',
391+ new_string: '2',
392+ })
393+
394+ await world.clock.advance(Fixtures.SETTLE_MS)
395+
396+ expect(world.opened, 'unknown is not a dock').toEqual([])
397+
398+ await $.ui.render(Fixtures.HINT)
399+
400+ await $.tool.call({
401+ tool: 'Edit',
402+ file_path: '/work/app.ts',
403+ old_string: '2',
404+ new_string: '3',
405+ })
406+
407+ await world.clock.advance(Fixtures.SETTLE_MS)
408+
409+ expect(
410+ world.opened.map(pane => pane.id),
411+ 'once a drawing says the layout docks, the next edit opens it',
412+ ).toEqual(['diff'])
413+ })
414+
351415 test('an edit that failed or was refused opens nothing', async ($, on) => {
352416 const world = Fixtures.inRepository(on)
353417
Large file, diff not shown.