Skip to content

Commit b782847

Browse files
authored
diff: the first edit opens the pane only where the layout docks it (#94653)
* 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
1 parent 7dd0636 commit b782847

10 files changed

Lines changed: 185 additions & 44 deletions

File tree

‎mods/diff/README.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ through Buttons that declare the engine's own actions. The pane refreshes
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 @@ moved file by.
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. |

‎mods/diff/hooks/pane-state/pane-model/pane-model.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import type { Source } from '../source'
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

‎mods/diff/hooks/register.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ export function register(on: On) {
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 @@ export function register(on: On) {
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)

‎mods/diff/tests/fixtures/hint-at.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import type { RenderInput } from 'claude-code'
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
})

‎mods/diff/tests/fixtures/hint.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

‎mods/diff/tests/fixtures/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export * from './in-repository.js'
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 @@ export * from './truncate-start'
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'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,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+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,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+
}

‎mods/diff/tests/register.test.ts‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,70 @@ describe('register', () => {
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

‎mods/types/claude-code.d.ts‎

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Written by Claude Code 2.1.271.
1+
// Written by Claude Code 2.1.273.
22
// Claude Code function hooks: the plugin API's TypeScript declarations.
33
//
44
// EARLY ACCESS: this surface may change between releases without notice.
@@ -634,9 +634,9 @@ declare module 'claude-code' {
634634
* The props of `Button`, every surface's pressable leaf: an address, a
635635
* label, the closure a press runs, and the label styles a hover overrides.
636636
*
637-
* The terminal draws `[ label ]` (or `1: label` when `plain`), a desktop a
638-
* native button; a click, a `hotkey`, the chord for its `action`, or Enter
639-
* while it has the focus raises `ui.press`, whose bottom is `onPress`.
637+
* The terminal draws `[ label ]` (when `plain`, `1: label` or the label
638+
* alone), a desktop a native button; a click, a `hotkey`, the chord for its
639+
* `action`, or Enter under the focus raises `ui.press`, its bottom `onPress`.
640640
*/
641641
export type ButtonProps = {
642642
/**
@@ -668,7 +668,11 @@ declare module 'claude-code' {
668668
action?: string;
669669
/**
670670
* Drawn without chrome: the hotkey in the accent color, a colon, the
671-
* label (`1: Yes`), as a survey's row reads.
671+
* label (`1: Yes`), as a survey's row reads; no `hotkey`, the label alone.
672+
*
673+
* A one-glyph label (`'\u{1F50A}'`, a speaker) is then a control by
674+
* itself: the focus and the pointer still invert it, `dimColor` and `hover`
675+
* still apply. A desktop draws its native button either way.
672676
*/
673677
plain?: true;
674678
/**
@@ -2147,7 +2151,7 @@ declare module 'claude-code' {
21472151
repo: () => Promise<SessionRepo | null>;
21482152
/**
21492153
* Returns every surface the session draws on, each once: `terminal` under
2150-
* the REPL first, then `desktop` and `mobile` in the order they attached.
2154+
* the REPL first, then the remote ones in the order they attached.
21512155
*
21522156
* A session may draw on several at once (a terminal and two phones):
21532157
* clients attach (`session.attach`) and detach, and a render hook still
@@ -2714,9 +2718,9 @@ declare module 'claude-code' {
27142718
* The element constructors each surface draws, by `e.surface`: what
27152719
* `$.ui.resolve(e)` returns and a `ui.resolve` hook passes on; no globals.
27162720
*
2717-
* All carry `Box`, `Text`, `Button`, `Link`, `Code`; terminal and desktop add
2718-
* `Input`, `Select`, `Client`; desktop and mobile `Svg`; terminal `Raster`.
2719-
* Narrowed on `e.surface`, that table; unnarrowed, the union; else fragments.
2721+
* All carry `Box`, `Text`, `Button`, `Link`, `Code`; every remote surface
2722+
* `Svg`; all but mobile `Input` and `Select`; terminal and desktop `Client`;
2723+
* terminal `Raster`. Narrowed on `e.surface`, that table; else the union.
27202724
*/
27212725
export type Elements = {
27222726
terminal: {
@@ -2755,6 +2759,23 @@ declare module 'claude-code' {
27552759
Link: ElementConstructor<LinkProps>;
27562760
Code: ElementConstructor<CodeProps>;
27572761
};
2762+
/**
2763+
* The desktop's table without `Client`: a remote `Client`'s module, presses
2764+
* and posts (ui_client_module, ui_client_press, ui_message) name no surface.
2765+
*
2766+
* They are the desktop's alone today, not a limit of the editor's webview:
2767+
* the table gains `Client` when those asks name a surface.
2768+
*/
2769+
vscode: {
2770+
Box: ElementConstructor<BoxProps>;
2771+
Text: ElementConstructor<TextProps>;
2772+
Button: ElementConstructor<ButtonProps>;
2773+
Input: ElementConstructor<InputProps>;
2774+
Select: ElementConstructor<SelectProps>;
2775+
Svg: ElementConstructor<SvgProps>;
2776+
Link: ElementConstructor<LinkProps>;
2777+
Code: ElementConstructor<CodeProps>;
2778+
};
27582779
};
27592780

27602781
/**
@@ -5963,11 +5984,11 @@ declare module 'claude-code' {
59635984
action?: string;
59645985
/**
59655986
* Drawn without chrome: the hotkey in the accent color, a colon,
5966-
* then the label (`1: Yes`), as a survey's row reads.
5987+
* then the label (`1: Yes`); without a `hotkey`, the label alone.
59675988
*
5968-
* In JSX the label may be the one string child
5969-
* (`<Button hotkey="1" plain onPress={...}>Yes</Button>`); the key
5970-
* defaults to the label.
5989+
* The focus still inverts it. In JSX the label may be the one string
5990+
* child (`<Button hotkey="1" plain onPress={...}>Yes</Button>`); the
5991+
* key defaults to the label.
59715992
*/
59725993
plain?: true;
59735994
/**
@@ -6136,8 +6157,8 @@ declare module 'claude-code' {
61366157
children?: undefined;
61376158
} | {
61386159
/**
6139-
* A vector drawing, the desktop and mobile surfaces' alone: the SVG
6140-
* markup is the element's data, drawn in an isolated box, off the page.
6160+
* A vector drawing, the remote surfaces' alone: the SVG markup is the
6161+
* element's data, drawn in an isolated box, off the page.
61416162
*
61426163
* A leaf: hooks above wrap or replace it whole, nothing reaches inside;
61436164
* a press other plugins should see goes on an enclosing Button. On a
@@ -6624,23 +6645,25 @@ declare module 'claude-code' {
66246645

66256646
/**
66266647
* Where a render event's component is drawn: `terminal` is Ink, which draws
6627-
* the hook's whole tree; `desktop` (Claude Code Desktop) and `mobile` (the
6628-
* Claude mobile app) are remote surfaces that draw the tree themselves.
6648+
* the hook's whole tree; the rest are remote surfaces drawing it themselves.
66296649
*
6630-
* A remote surface asks over the wire (ui_render), draws with the props the
6631-
* hook handed core and draws the tree where it has a slot for it. Each
6632-
* surface's ask is its own evaluation, since a tree may hold an element only
6633-
* some surfaces draw (Svg, Client).
6650+
* `desktop` is Claude Code Desktop, `mobile` the Claude mobile app, `vscode`
6651+
* Claude Code for VS Code. A remote surface asks over the wire (ui_render),
6652+
* draws with the props the hook handed core, and draws the tree where it has
6653+
* a slot for it.
6654+
*
6655+
* Each surface's ask is its own evaluation, since a tree may hold an element
6656+
* only some surfaces draw (Svg, Client).
66346657
*/
6635-
export type RenderSurface = 'terminal' | 'desktop' | 'mobile';
6658+
export type RenderSurface = 'terminal' | 'desktop' | 'mobile' | 'vscode';
66366659

66376660
/**
66386661
* The size of what a surface draws into, in character cells of the
6639-
* surface's monospace metric: on the terminal, the conversation's columns and
6640-
* screen rows; on a remote surface, the pane's width and height divided by the
6641-
* advance and line height of its code font. A pixel-sized companion
6642-
* arrives with the first element that lays out in pixels; until then
6643-
* every element on every surface is cell-based, and so is this.
6662+
* surface's monospace metric, and whether its layout docks a pane.
6663+
*
6664+
* On the terminal, the conversation's columns and screen rows; on a remote
6665+
* surface, the pane's width and height over the advance and line height of
6666+
* its code font. Cell-based until the first element lays out in pixels.
66446667
*/
66456668
export type RenderViewport = {
66466669
/**
@@ -6650,10 +6673,24 @@ declare module 'claude-code' {
66506673
columns: number;
66516674
/**
66526675
* Cells down the whole surface, not the room left for this component.
6676+
*
66536677
* Informational: a change of height alone re-draws nothing and keys no
66546678
* new evaluation, so a hook reads it as of the last width or props change.
66556679
*/
66566680
rows: number;
6681+
/**
6682+
* Whether this surface docks a pane beside the transcript, so a pane a
6683+
* plugin opens unasked is a sidebar, not a takeover; absent is unknown.
6684+
*
6685+
* What `command.run`'s `presentation.isFullscreen` says. The terminal always
6686+
* says: `true` fullscreen, `false` on the main screen, fixed per session; a
6687+
* remote surface once its client reports it with the size, absent before.
6688+
*
6689+
* @remarks The desktop and VS Code report it once they place panes; the
6690+
* mobile app, which has no dock, `false`. A change re-draws the sites.
6691+
* @example if (e.viewport?.isFullscreen === true) void $.ui.open({ id })
6692+
*/
6693+
isFullscreen?: boolean;
66576694
};
66586695

66596696
/**
@@ -7681,13 +7718,12 @@ declare module 'claude-code' {
76817718
};
76827719

76837720
/**
7684-
* The props of `Svg`, the desktop and mobile surfaces' vector leaf: the
7685-
* markup is the element's data, as a string is a Text's, drawn isolated.
7721+
* The props of `Svg`, the remote surfaces' vector leaf: the markup is the
7722+
* element's data, as a string is a Text's, drawn isolated.
76867723
*
76877724
* A leaf: no children. The surface never lets the markup reach the page
7688-
* (the engine bounds it; the desktop draws it as an image, or in a
7689-
* sandboxed frame when `isInteractive`; the mobile app in a sandboxed web
7690-
* view).
7725+
* (the engine bounds it; the desktop and the editor draw it as an image, or
7726+
* in a sandboxed frame when `isInteractive`; the mobile app in a web view).
76917727
*/
76927728
export type SvgProps = {
76937729
/**

0 commit comments

Comments
 (0)