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
| @@ -13,12 +13,15 @@ | ||
| 13 | 13 | through Buttons that declare the engine's own actions. The pane refreshes |
| 14 | 14 | as Claude edits and runs shell commands, and while it is open it polls |
| 15 | 15 | the repository's HEAD so a commit or checkout made elsewhere shows too. |
| 16 | -The first successful edit of a session opens the pane by itself where the | |
| 16 | +The main loop's first successful edit of a session opens the pane by | |
| 17 | +itself, as the built-in panel opens on its first checkpoint: where the | |
| 17 | 18 | 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. | |
| 19 | +drawing's `viewport` says), the terminal is wide enough (144 columns when | |
| 20 | +the person never chose, 110 when they kept it open before; a person who | |
| 21 | +closed it is left alone) and file checkpointing is on; a subagent's edit | |
| 22 | +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. | |
| 22 | 25 | |
| 23 | 26 | Under the fullscreen layout a terminal under 110 columns gets the |
| 24 | 27 | built-in's line asking for a wider one and nothing opens. Without that |
| @@ -68,16 +71,17 @@ | ||
| 68 | 71 | | `ui.scroll` of the pane | Docked, moves the hunks under the pinned header and list (three rows a wheel tick, a page a page key), or the list when the wheel is over it, and keeps the engine's window still. | |
| 69 | 72 | | `ui.focus` in the pane | In the dialog's list, selects the file the ring lands on, re-centres the five rows on it, and lands the ring where that row now sits. | |
| 70 | 73 | | `command.run` of `clear`, `resume` | Closes the pane and forgets the session's state, the pinned repository with it. | |
| 71 | -| `tool.call` of `Edit`, `Write`, `NotebookEdit` | After an edit that landed (not refused, not failed), refreshes an open pane; the session's first such edit opens it, pinning the repository then if the terminal has the room. | | |
| 74 | +| `tool.call` of `Edit`, `Write`, `NotebookEdit` | After an edit that landed (not refused, not failed), refreshes an open pane; the main loop's first such edit opens it, pinning the repository then if the terminal has the room and checkpointing is on. | | |
| 72 | 75 | | `tool.call` of `Bash`, `PowerShell` | After a command that was not refused, failed and interrupted ones too, refreshes an open pane. | |
| 73 | 76 | | `prompt.submit` | Adds the armed file's hunks to the prompt's context and disarms. | |
| 74 | 77 | |
| 75 | 78 | ## What it calls on `$` |
| 76 | 79 | |
| 77 | -`clock.after`, `clock.every`, `clock.now`, `command.register`, `fs.list`, | |
| 78 | -`fs.read`, `fs.stat`, `process.run` (git, read-only), `session.messages`, | |
| 79 | -`store.get`, `store.set`, `telemetry.log`, `telemetry.mark`, `ui.close`, | |
| 80 | -`ui.invalidate`, `ui.log`, `ui.open`, `ui.resolve`, `ui.status`. | |
| 80 | +`clock.after`, `clock.every`, `clock.now`, `command.register`, `env.get` | |
| 81 | +(`CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING`), `fs.list`, `fs.read`, `fs.stat`, | |
| 82 | +`process.run` (git, read-only), `session.id`, `session.messages`, | |
| 83 | +`settings.read`, `store.get`, `store.set`, `telemetry.log`, `telemetry.mark`, | |
| 84 | +`ui.close`, `ui.invalidate`, `ui.log`, `ui.open`, `ui.resolve`, `ui.status`. | |
| 81 | 85 | |
| 82 | 86 | `$.telemetry` is the telemetry plugin's noun; where it is absent the rows |
| 83 | 87 | are dropped and nothing else changes. |
| @@ -65,6 +65,12 @@ | ||
| 65 | 65 | storeSet: (key: string, value: unknown) => Promise<void> |
| 66 | 66 | |
| 67 | 67 | /** |
| 68 | + * Whether the session checkpoints edits (`$.settings.read`, `$.env.get`): | |
| 69 | + * the built-in panel opens on an edit only while it does. | |
| 70 | + */ | |
| 71 | + isCheckpointing: () => Promise<boolean> | |
| 72 | + | |
| 73 | + /** | |
| 68 | 74 | * `$.session.messages`. |
| 69 | 75 | */ |
| 70 | 76 | messages: () => Promise<SessionMessage[]> |
| @@ -8,6 +8,7 @@ | ||
| 8 | 8 | export * from './entry-kinds-of' |
| 9 | 9 | export * from './git' |
| 10 | 10 | export * from './host' |
| 11 | +export * from './is-checkpointing' | |
| 11 | 12 | export * from './is-on-pane-surface' |
| 12 | 13 | export * from './is-record' |
| 13 | 14 | export * from './kept-of' |
| @@ -1,0 +1,3 @@ | ||
| 1 | +export * from './is-checkpointing.js' | |
| 2 | + | |
| 3 | +export * as default from '.' |
| @@ -1,0 +1,20 @@ | ||
| 1 | +import type { Settings } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Whether the session checkpoints Claude's edits, read as the built-in | |
| 5 | + * reads it: the setting on unless set false, the variable unset or falsy. | |
| 6 | + * | |
| 7 | + * The built-in panel opens on an edit only through a checkpoint, so with | |
| 8 | + * checkpointing off its first-edit open never happens; `/diff` still opens. | |
| 9 | + * | |
| 10 | + * @param settings the merged settings (`$.settings.read()`) | |
| 11 | + * @param disabling `CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING` as `$.env.get` | |
| 12 | + * answers it | |
| 13 | + * @returns false when either turns checkpointing off | |
| 14 | + */ | |
| 15 | +export const isCheckpointing = ( | |
| 16 | + settings: Settings, | |
| 17 | + disabling: string | undefined, | |
| 18 | +): boolean => | |
| 19 | + settings.fileCheckpointingEnabled !== false && | |
| 20 | + !['1', 'true', 'yes', 'on'].includes((disabling ?? '').trim().toLowerCase()) |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import type { |
| 2 | + Args, | |
| 2 | 3 | On, |
| 3 | 4 | PaneOpenArgs, |
| 4 | 5 | ResultOf, |
| @@ -13,7 +14,9 @@ | ||
| 13 | 14 | import { entryKindsOf } from './entry-kinds-of' |
| 14 | 15 | import type Git from './git' |
| 15 | 16 | import type { Host } from './host' |
| 17 | +import { isCheckpointing } from './is-checkpointing' | |
| 16 | 18 | import { isOnPaneSurface } from './is-on-pane-surface' |
| 19 | +import { isRecord } from './is-record' | |
| 17 | 20 | import Limits from './limits' |
| 18 | 21 | import { mapLimited } from './map-limited' |
| 19 | 22 | import { messageOf } from './message-of' |
| @@ -30,8 +33,8 @@ | ||
| 30 | 33 | * pane's drawing and refresh, its opening on Claude's first edit, the ask. |
| 31 | 34 | * |
| 32 | 35 | * Git runs when the built-in's would: `session.start` binds the host and |
| 33 | - * registers `/diff`; `/diff` or the first edit with room pins the backend | |
| 34 | - * where the session started, until `/clear`; an open pane alone fetches. | |
| 36 | + * 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. | |
| 35 | 38 | * |
| 36 | 39 | * @param on the engine's registrar |
| 37 | 40 | */ |
| @@ -394,7 +397,7 @@ | ||
| 394 | 397 | async function openPane( |
| 395 | 398 | engine: Host, |
| 396 | 399 | trigger: (typeof Record.SHOWN_TRIGGERS)[number], |
| 397 | - ): Promise<void> { | |
| 400 | + ): Promise<boolean> { | |
| 398 | 401 | const isDialog = model.isFullscreen === false |
| 399 | 402 | |
| 400 | 403 | model = { |
| @@ -406,12 +409,20 @@ | ||
| 406 | 409 | |
| 407 | 410 | dialogRows = isDialog ? Views.dialogRowsOf(model) : null |
| 408 | 411 | |
| 409 | - await engine.openPane( | |
| 412 | + const opened = await engine.openPane( | |
| 410 | 413 | isDialog |
| 411 | 414 | ? { ...dialogPane(), focus: true } |
| 412 | 415 | : { id: Names.PANE_ID, title: Names.PANE_TITLE, holdToasts: true }, |
| 413 | 416 | ) |
| 414 | 417 | |
| 418 | + const isWaiting = isRecord(opened) && opened.isPlaced === false | |
| 419 | + | |
| 420 | + if (isWaiting) { | |
| 421 | + await engine.closePane({ id: Names.PANE_ID }).catch(() => undefined) | |
| 422 | + | |
| 423 | + return false | |
| 424 | + } | |
| 425 | + | |
| 415 | 426 | isPaneOpen = true |
| 416 | 427 | |
| 417 | 428 | const sessionId = await engine.sessionId().catch(() => null) |
| @@ -422,6 +433,8 @@ | ||
| 422 | 433 | } |
| 423 | 434 | |
| 424 | 435 | void refresh(engine) |
| 436 | + | |
| 437 | + return true | |
| 425 | 438 | } |
| 426 | 439 | |
| 427 | 440 | async function closePane(engine: Host): Promise<void> { |
| @@ -460,6 +473,12 @@ | ||
| 460 | 473 | return |
| 461 | 474 | } |
| 462 | 475 | |
| 476 | + const isCheckpointed = await engine.isCheckpointing().catch(() => true) | |
| 477 | + | |
| 478 | + if (!isCheckpointed || isTaken()) { | |
| 479 | + return | |
| 480 | + } | |
| 481 | + | |
| 463 | 482 | await pinBackend(engine) |
| 464 | 483 | |
| 465 | 484 | if (!backend || isTaken()) { |
| @@ -467,7 +486,7 @@ | ||
| 467 | 486 | } |
| 468 | 487 | |
| 469 | 488 | hasAutoOpened = true |
| 470 | - await openPane(engine, 'auto_open') | |
| 489 | + hasAutoOpened = await openPane(engine, 'auto_open') | |
| 471 | 490 | } |
| 472 | 491 | |
| 473 | 492 | function disarm(engine: Host) { |
| @@ -602,6 +621,11 @@ | ||
| 602 | 621 | readFile: path => $.fs.read(path), |
| 603 | 622 | storeGet: key => $.store.get(key), |
| 604 | 623 | storeSet: (key, value) => $.store.set(key, value), |
| 624 | + isCheckpointing: async () => | |
| 625 | + isCheckpointing( | |
| 626 | + await $.settings.read(), | |
| 627 | + await $.env.get('CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING'), | |
| 628 | + ), | |
| 605 | 629 | messages: () => $.session.messages(), |
| 606 | 630 | invalidate: () => $.ui.invalidate('ui.render'), |
| 607 | 631 | status: text => $.ui.status(text), |
| @@ -699,8 +723,15 @@ | ||
| 699 | 723 | } |
| 700 | 724 | |
| 701 | 725 | const isOpening = toggle === 'open' |
| 702 | - await (isOpening ? openPane(host, 'manual') : closePane(host)) | |
| 703 | 726 | |
| 727 | + const isDone = isOpening | |
| 728 | + ? await openPane(host, 'manual') | |
| 729 | + : await closePane(host).then(() => true) | |
| 730 | + | |
| 731 | + if (!isDone) { | |
| 732 | + return { text: Names.RESIZE_TERMINAL_TEXT } | |
| 733 | + } | |
| 734 | + | |
| 704 | 735 | if (!isFullscreen) { |
| 705 | 736 | return isOpening ? {} : { text: Names.DIALOG_DISMISSED_TEXT } |
| 706 | 737 | } |
| @@ -818,10 +849,10 @@ | ||
| 818 | 849 | |
| 819 | 850 | function afterTool( |
| 820 | 851 | engine: Host, |
| 821 | - tool: string, | |
| 852 | + e: Args<'tool.call'>, | |
| 822 | 853 | result: ResultOf['tool.call'] | undefined, |
| 823 | 854 | ) { |
| 824 | - const isEdit = Tools.EDITING_TOOLS.some(name => name === tool) | |
| 855 | + const isEdit = Tools.EDITING_TOOLS.some(name => name === e.tool) | |
| 825 | 856 | |
| 826 | 857 | const hasEdited = |
| 827 | 858 | isEdit && |
| @@ -837,7 +868,9 @@ | ||
| 837 | 868 | scheduleRefresh(engine) |
| 838 | 869 | } |
| 839 | 870 | |
| 840 | - if (hasEdited) { | |
| 871 | + const isMainLoopEdit = hasEdited && e.agentId === undefined | |
| 872 | + | |
| 873 | + if (isMainLoopEdit) { | |
| 841 | 874 | void openOnFirstEdit(engine).catch(() => undefined) |
| 842 | 875 | } |
| 843 | 876 | } |
| @@ -854,7 +887,7 @@ | ||
| 854 | 887 | return result |
| 855 | 888 | } finally { |
| 856 | 889 | if (host) { |
| 857 | - afterTool(host, e.tool, result) | |
| 890 | + afterTool(host, e, result) | |
| 858 | 891 | } |
| 859 | 892 | } |
| 860 | 893 | }, |
| @@ -1,0 +1,28 @@ | ||
| 1 | +import type { Settings } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * What the world beneath a repository session holds and answers besides | |
| 5 | + * git: the store, the settings, the environment, whether a pane is seated. | |
| 6 | + */ | |
| 7 | +export type Beneath = { | |
| 8 | + /** | |
| 9 | + * What the plugin's store holds at the start; nothing when not given. | |
| 10 | + */ | |
| 11 | + stored?: Readonly<Record<string, unknown>> | |
| 12 | + | |
| 13 | + /** | |
| 14 | + * What `$.settings.read` answers; empty when not given. | |
| 15 | + */ | |
| 16 | + settings?: Settings | |
| 17 | + | |
| 18 | + /** | |
| 19 | + * The variables `$.env.get` answers from; none set when not given. | |
| 20 | + */ | |
| 21 | + env?: Readonly<Record<string, string>> | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * Whether an open made now is left waiting undrawn (LEFT_WAITING), as an | |
| 25 | + * engine leaves an unasked open on a narrow terminal; placed when not given. | |
| 26 | + */ | |
| 27 | + isLeftWaiting?: () => boolean | |
| 28 | +} |
| @@ -1,0 +1,4 @@ | ||
| 1 | +export type * from './beneath.js' | |
| 2 | +export * from './left-waiting.js' | |
| 3 | + | |
| 4 | +export * as default from '.' |
| @@ -1,0 +1,17 @@ | ||
| 1 | +import type { ResultOf } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * What an engine answers an open it leaves waiting undrawn: the plugin | |
| 5 | + * opened unasked on a terminal narrower than an unrequested pane is given. | |
| 6 | + * | |
| 7 | + * Typed through `never` so it compiles against declarations that predate the | |
| 8 | + * answer, where `ui.open` resolves nothing. | |
| 9 | + */ | |
| 10 | +export const LEFT_WAITING: ResultOf['ui.open'] = { | |
| 11 | + value: { | |
| 12 | + isPlaced: false, | |
| 13 | + reason: | |
| 14 | + 'unasked below 144 columns (120 now): placed when the person opens ' + | |
| 15 | + 'it, or when the terminal is widened to 144 columns', | |
| 16 | + } as never, | |
| 17 | +} |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | import type { Args, On } from 'claude-code' |
| 2 | 2 | import { mock } from 'claude-code/testing' |
| 3 | 3 | |
| 4 | +import Beneath from './beneath' | |
| 4 | 5 | import { gitIn } from './git-in.js' |
| 5 | 6 | import { HINT_DRAWN } from './hint-drawn.js' |
| 6 | 7 | import { keeping } from './keeping.js' |
| @@ -11,24 +12,25 @@ | ||
| 11 | 12 | * A session in a repository git answers for from a script (REPOSITORY, in |
| 12 | 13 | * /work, when none is given), keeping what the plugin does there. |
| 13 | 14 | * |
| 14 | - * Kept: each git run, ring move, pane opened or closed, status line. The clock | |
| 15 | - * starts at 0 and the engine draws the hint. A test that rewrites the | |
| 16 | - * script between calls changes what git answers next. | |
| 15 | + * Kept: each git run, ring move, pane opened, left waiting or closed, status | |
| 16 | + * line. The clock starts at 0 and the engine draws the hint; the rest of the | |
| 17 | + * world is the test's (Beneath). Rewriting the script changes git's answers. | |
| 17 | 18 | * |
| 18 | 19 | * @param on the test's `on` |
| 19 | 20 | * @param script git's output for each invocation whose line holds the key |
| 20 | - * @param stored what the plugin's store holds at the start | |
| 21 | - * @returns the runs, the ring's moves, the panes opened and closed, the | |
| 22 | - * statuses, the clock | |
| 21 | + * @param beneath the store, settings, environment, whether an open is seated | |
| 22 | + * @returns the runs, the ring's moves, the panes opened, left waiting and | |
| 23 | + * closed, the statuses, the clock | |
| 23 | 24 | */ |
| 24 | 25 | export function inRepository( |
| 25 | 26 | on: On, |
| 26 | 27 | script: Readonly<Record<string, string>> = REPOSITORY, |
| 27 | - stored: Readonly<Record<string, unknown>> = {}, | |
| 28 | + beneath: Beneath.Beneath = {}, | |
| 28 | 29 | ) { |
| 29 | 30 | const runs: Args<'process.run'>[] = [] |
| 30 | 31 | const focused: Args<'ui.focus'>[] = [] |
| 31 | 32 | const statuses: (string | undefined)[] = [] |
| 33 | + const waiting: Args<'ui.open'>[] = [] | |
| 32 | 34 | const opened = keeping<Args<'ui.open'>>() |
| 33 | 35 | const closed = keeping<Args<'ui.close'>>() |
| 34 | 36 | const clock = startsSession(on) |
| @@ -51,17 +53,31 @@ | ||
| 51 | 53 | return { value: undefined } |
| 52 | 54 | }) |
| 53 | 55 | |
| 54 | - on('ui.open', opened.hook) | |
| 56 | + on('ui.open', (engine, e) => { | |
| 57 | + const isWaiting = beneath.isLeftWaiting?.() === true | |
| 58 | + | |
| 59 | + if (!isWaiting) { | |
| 60 | + return opened.hook(engine, e) | |
| 61 | + } | |
| 62 | + | |
| 63 | + waiting.push(e) | |
| 64 | + | |
| 65 | + return Beneath.LEFT_WAITING | |
| 66 | + }) | |
| 67 | + | |
| 55 | 68 | on('ui.close', closed.hook) |
| 56 | 69 | on('ui.invalidate', () => ({ value: undefined })) |
| 57 | 70 | on('ui.render', { component: 'PromptHint' }, () => HINT_DRAWN) |
| 58 | 71 | on('session.messages', () => ({ value: [] })) |
| 59 | - mock.store(on, stored) | |
| 72 | + on('settings.read', () => ({ value: beneath.settings ?? {} })) | |
| 73 | + mock.store(on, beneath.stored ?? {}) | |
| 74 | + mock.env(on, beneath.env ?? {}) | |
| 60 | 75 | |
| 61 | 76 | return { |
| 62 | 77 | runs, |
| 63 | 78 | focused, |
| 64 | 79 | opened: opened.kept, |
| 80 | + waiting, | |
| 65 | 81 | closed: closed.kept, |
| 66 | 82 | statuses, |
| 67 | 83 | clock, |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | export * from './answers-of.js' |
| 2 | 2 | export * from './ask' |
| 3 | 3 | export * from './backend-of' |
| 4 | +export * from './beneath' | |
| 4 | 5 | export * from './big-lines.js' |
| 5 | 6 | export * from './branch-base-of' |
| 6 | 7 | export * from './builtin-holds.js' |
| @@ -448,6 +448,180 @@ | ||
| 448 | 448 | ).toEqual(['diff']) |
| 449 | 449 | }) |
| 450 | 450 | |
| 451 | + test("only the main loop's edit opens the pane", async ($, on) => { | |
| 452 | + const world = Fixtures.inRepository(on) | |
| 453 | + | |
| 454 | + const edit = (agentId?: string) => | |
| 455 | + $.tool.call({ | |
| 456 | + tool: 'Write', | |
| 457 | + file_path: '/work/app.ts', | |
| 458 | + content: '2', | |
| 459 | + ...(agentId !== undefined && { agentId }), | |
| 460 | + }) | |
| 461 | + | |
| 462 | + on('tool.call', () => ({ result: 'written' })) | |
| 463 | + | |
| 464 | + await $.session.start(Fixtures.SESSION) | |
| 465 | + await $.ui.render(Fixtures.HINT) | |
| 466 | + await edit('a-explorer') | |
| 467 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 468 | + | |
| 469 | + expect(world.opened, 'the built-in checkpoints no subagent edit').toEqual( | |
| 470 | + [], | |
| 471 | + ) | |
| 472 | + | |
| 473 | + expect(world.runs, 'so nothing was asked of the repository').toEqual([]) | |
| 474 | + | |
| 475 | + await edit() | |
| 476 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 477 | + | |
| 478 | + expect( | |
| 479 | + world.opened.map(pane => pane.id), | |
| 480 | + "the main loop's", | |
| 481 | + ).toEqual(['diff']) | |
| 482 | + }) | |
| 483 | + | |
| 484 | + test('checkpointing off in settings: edits open nothing', async ($, on) => { | |
| 485 | + const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { | |
| 486 | + settings: { fileCheckpointingEnabled: false }, | |
| 487 | + }) | |
| 488 | + | |
| 489 | + on('tool.call', () => ({ result: 'edited' })) | |
| 490 | + | |
| 491 | + await $.session.start(Fixtures.SESSION) | |
| 492 | + await $.ui.render(Fixtures.HINT) | |
| 493 | + | |
| 494 | + await $.tool.call({ | |
| 495 | + tool: 'Edit', | |
| 496 | + file_path: '/work/app.ts', | |
| 497 | + old_string: '1', | |
| 498 | + new_string: '2', | |
| 499 | + }) | |
| 500 | + | |
| 501 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 502 | + | |
| 503 | + expect(world.opened, 'the built-in opens on a checkpoint alone').toEqual([]) | |
| 504 | + | |
| 505 | + expect(world.runs, 'so nothing was asked of the repository').toEqual([]) | |
| 506 | + | |
| 507 | + expect(await $.command.run(Fixtures.DIFF)).toEqual({ | |
| 508 | + text: 'Diff panel shown', | |
| 509 | + }) | |
| 510 | + }) | |
| 511 | + | |
| 512 | + test('checkpointing off by variable: edits open nothing', async ($, on) => { | |
| 513 | + const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { | |
| 514 | + env: { CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING: 'true' }, | |
| 515 | + }) | |
| 516 | + | |
| 517 | + on('tool.call', () => ({ result: 'edited' })) | |
| 518 | + | |
| 519 | + await $.session.start(Fixtures.SESSION) | |
| 520 | + await $.ui.render(Fixtures.HINT) | |
| 521 | + | |
| 522 | + await $.tool.call({ | |
| 523 | + tool: 'Edit', | |
| 524 | + file_path: '/work/app.ts', | |
| 525 | + old_string: '1', | |
| 526 | + new_string: '2', | |
| 527 | + }) | |
| 528 | + | |
| 529 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 530 | + | |
| 531 | + expect(world.opened).toEqual([]) | |
| 532 | + }) | |
| 533 | + | |
| 534 | + test('an open left waiting is withdrawn; /diff opens it', async ($, on) => { | |
| 535 | + let isNarrow = true | |
| 536 | + | |
| 537 | + const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { | |
| 538 | + stored: { [Names.STORE_OPEN_KEY]: true }, | |
| 539 | + isLeftWaiting: () => isNarrow, | |
| 540 | + }) | |
| 541 | + | |
| 542 | + const edit = () => | |
| 543 | + $.tool.call({ | |
| 544 | + tool: 'Edit', | |
| 545 | + file_path: '/work/app.ts', | |
| 546 | + old_string: '1', | |
| 547 | + new_string: '2', | |
| 548 | + }) | |
| 549 | + | |
| 550 | + on('tool.call', () => ({ result: 'edited' })) | |
| 551 | + | |
| 552 | + await $.session.start(Fixtures.SESSION) | |
| 553 | + await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)) | |
| 554 | + await edit() | |
| 555 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 556 | + | |
| 557 | + expect( | |
| 558 | + world.waiting.map(pane => pane.id), | |
| 559 | + 'the edit opened', | |
| 560 | + ).toEqual(['diff']) | |
| 561 | + | |
| 562 | + expect( | |
| 563 | + world.closed.map(pane => pane.id), | |
| 564 | + 'and withdrew the pane the engine left waiting, so no resize seats it', | |
| 565 | + ).toEqual(['diff']) | |
| 566 | + | |
| 567 | + expect( | |
| 568 | + 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']) | |
| 571 | + | |
| 572 | + isNarrow = false | |
| 573 | + | |
| 574 | + expect( | |
| 575 | + await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)), | |
| 576 | + '/diff opens it, where a pane believed open would have been hidden', | |
| 577 | + ).toEqual({ text: 'Diff panel shown' }) | |
| 578 | + | |
| 579 | + await $.command.run(Fixtures.diffAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)) | |
| 580 | + await edit() | |
| 581 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 582 | + | |
| 583 | + expect( | |
| 584 | + world.opened.map(pane => pane.id), | |
| 585 | + 'closed by hand, no edit opens it again', | |
| 586 | + ).toEqual(['diff']) | |
| 587 | + }) | |
| 588 | + | |
| 589 | + test('an unplaced open is tried again at the next edit', async ($, on) => { | |
| 590 | + let isNarrow = true | |
| 591 | + | |
| 592 | + const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { | |
| 593 | + stored: { [Names.STORE_OPEN_KEY]: true }, | |
| 594 | + isLeftWaiting: () => isNarrow, | |
| 595 | + }) | |
| 596 | + | |
| 597 | + const edit = () => | |
| 598 | + $.tool.call({ | |
| 599 | + tool: 'Edit', | |
| 600 | + file_path: '/work/app.ts', | |
| 601 | + old_string: '1', | |
| 602 | + new_string: '2', | |
| 603 | + }) | |
| 604 | + | |
| 605 | + on('tool.call', () => ({ result: 'edited' })) | |
| 606 | + | |
| 607 | + await $.session.start(Fixtures.SESSION) | |
| 608 | + await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)) | |
| 609 | + await edit() | |
| 610 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 611 | + | |
| 612 | + expect(world.opened, 'the first try waited and was withdrawn').toEqual([]) | |
| 613 | + | |
| 614 | + isNarrow = false | |
| 615 | + | |
| 616 | + await edit() | |
| 617 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 618 | + | |
| 619 | + expect( | |
| 620 | + world.opened.map(pane => pane.id), | |
| 621 | + 'the next edit opened it, as the built-in reads the width again then', | |
| 622 | + ).toEqual(['diff']) | |
| 623 | + }) | |
| 624 | + | |
| 451 | 625 | test('/clear closes the pane it finds open', async ($, on) => { |
| 452 | 626 | const world = Fixtures.inRepository(on) |
| 453 | 627 | |
| @@ -528,7 +702,7 @@ | ||
| 528 | 702 | |
| 529 | 703 | test('a worktree opens on the base it kept', async ($, on) => { |
| 530 | 704 | const world = Fixtures.inRepository(on, Fixtures.oneSecret(), { |
| 531 | - 'base:/main/wt': 'uncommitted', | |
| 705 | + stored: { 'base:/main/wt': 'uncommitted' }, | |
| 532 | 706 | }) |
| 533 | 707 | |
| 534 | 708 | await $.session.start(Fixtures.WORKTREE_SESSION) |