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
| @@ -23,7 +23,9 @@ | ||
| 23 | 23 | itself. A docked pane fetches before it opens, as the built-in panel |
| 24 | 24 | primes its data, so it never lands on `Loading diff…`; an open the engine |
| 25 | 25 | leaves waiting undrawn is withdrawn, so no later resize seats it, and the |
| 26 | -next edit asks again. | |
| 26 | +next edit asks again. A session resumed or continued whose transcript | |
| 27 | +already holds such an edit opens the pane on the same terms as soon as the | |
| 28 | +width is known, as the built-in opens on the history it restores. | |
| 27 | 29 | |
| 28 | 30 | Under the fullscreen layout a terminal under 110 columns gets the |
| 29 | 31 | built-in's line asking for a wider one and nothing opens. Without that |
| @@ -39,7 +41,9 @@ | ||
| 39 | 41 | file: its hunks ride the next prompt as context, once. |
| 40 | 42 | |
| 41 | 43 | The pane compares the working tree against HEAD, split at the session's |
| 42 | -start (the default), against HEAD plainly, or against the merge-base with | |
| 44 | +start (the default; the start the engine gives in `$.session.usage()`, so a | |
| 45 | +resumed session keeps its first and `/clear` begins anew), against HEAD | |
| 46 | +plainly, or against the merge-base with | |
| 43 | 47 | the default branch; the base line under the header names a base other than |
| 44 | 48 | the session's, and the choice is kept per repository in the plugin's store. |
| 45 | 49 | A picker shows one earlier turn's edits instead of the working tree, read |
| @@ -65,7 +69,7 @@ | ||
| 65 | 69 | |
| 66 | 70 | | event | what the hook does | |
| 67 | 71 | | --- | --- | |
| 68 | -| `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. | | |
| 72 | +| `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; off its dispatch, reads the transcript, and for a resumed session whose turns edited opens the pane as the first edit would. | | |
| 69 | 73 | | `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. | |
| 70 | 74 | | `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. | |
| 71 | 75 | | `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. | |
| @@ -111,6 +111,14 @@ | ||
| 111 | 111 | sessionId: () => Promise<string> |
| 112 | 112 | |
| 113 | 113 | /** |
| 114 | + * When the session began, `$.session.usage`'s `startedAt`: where the line | |
| 115 | + * between this session's edits and earlier ones falls, a resumed session's | |
| 116 | + * first start, moved by `/clear`. Not a number under an engine that | |
| 117 | + * predates it, where the plugin's own start stands in. | |
| 118 | + */ | |
| 119 | + startedAt: () => Promise<unknown> | |
| 120 | + | |
| 121 | + /** | |
| 114 | 122 | * `$.telemetry.mark`; rejects where the telemetry built-in is absent. |
| 115 | 123 | */ |
| 116 | 124 | mark: EngineInterface['telemetry']['mark'] |
| @@ -26,6 +26,7 @@ | ||
| 26 | 26 | import PaneToggle from './pane-toggle' |
| 27 | 27 | import Record from './record' |
| 28 | 28 | import Tools from './tools' |
| 29 | +import Turns from './turns' | |
| 29 | 30 | import Views from './views' |
| 30 | 31 | |
| 31 | 32 | /** |
| @@ -33,8 +34,11 @@ | ||
| 33 | 34 | * pane's drawing and refresh, its opening on Claude's first edit, the ask. |
| 34 | 35 | * |
| 35 | 36 | * Git runs when the built-in's would: `session.start` binds the host and |
| 36 | - * registers `/diff`; `/diff` or the main loop's first checkpointed edit with | |
| 37 | - * room pins the backend, until `/clear`; a docked pane fetches, then opens. | |
| 37 | + * registers `/diff`, and off its dispatch reads the transcript, so a resumed | |
| 38 | + * session whose turns already edited opens as its first edit would; `/diff` | |
| 39 | + * or the main loop's first checkpointed edit with room pins the backend, | |
| 40 | + * until `/clear`, which reads afresh under a pane it leaves open; a docked | |
| 41 | + * pane fetches, then opens. | |
| 38 | 42 | * |
| 39 | 43 | * @param on the engine's registrar |
| 40 | 44 | */ |
| @@ -46,6 +50,7 @@ | ||
| 46 | 50 | let isPaneOpen = false |
| 47 | 51 | let dialogRows: number | null = null |
| 48 | 52 | let hasAutoOpened = false |
| 53 | + let hasRestoredEdits = false | |
| 49 | 54 | let columns: number | null = null |
| 50 | 55 | let shownSessionId: string | null = null |
| 51 | 56 | let armed: Ask.ArmedAsk | null = null |
| @@ -500,6 +505,16 @@ | ||
| 500 | 505 | hasAutoOpened = await openPane(engine, 'auto_open') |
| 501 | 506 | } |
| 502 | 507 | |
| 508 | + async function openOnRestore(engine: Host): Promise<void> { | |
| 509 | + const messages = await engine.messages().catch((): SessionMessage[] => []) | |
| 510 | + | |
| 511 | + hasRestoredEdits = Turns.turnDiffsOf(messages).length > 0 | |
| 512 | + | |
| 513 | + if (hasRestoredEdits) { | |
| 514 | + await openOnFirstEdit(engine) | |
| 515 | + } | |
| 516 | + } | |
| 517 | + | |
| 503 | 518 | function disarm(engine: Host) { |
| 504 | 519 | armed = null |
| 505 | 520 | model = { ...model, armedPath: null } |
| @@ -604,8 +619,14 @@ | ||
| 604 | 619 | redraw(engine) |
| 605 | 620 | } |
| 606 | 621 | |
| 622 | + async function startedAtOf(engine: Host): Promise<number | null> { | |
| 623 | + const startedAt: unknown = await engine.startedAt().catch(() => undefined) | |
| 624 | + | |
| 625 | + return typeof startedAt === 'number' ? startedAt : null | |
| 626 | + } | |
| 627 | + | |
| 607 | 628 | async function bind(engine: Host, cwd: string): Promise<void> { |
| 608 | - sessionStartMs = await engine.now() | |
| 629 | + sessionStartMs = (await startedAtOf(engine)) ?? (await engine.now()) | |
| 609 | 630 | pin.cwd = cwd |
| 610 | 631 | |
| 611 | 632 | try { |
| @@ -645,12 +666,22 @@ | ||
| 645 | 666 | closePane: pane => $.ui.close(pane), |
| 646 | 667 | registerCommand: spec => $.command.register(spec), |
| 647 | 668 | sessionId: () => $.session.id(), |
| 669 | + startedAt: () => | |
| 670 | + $.session | |
| 671 | + .usage() | |
| 672 | + .then((usage: unknown) => | |
| 673 | + isRecord(usage) ? usage.startedAt : undefined, | |
| 674 | + ), | |
| 648 | 675 | mark: entry => $.telemetry.mark(entry), |
| 649 | 676 | log: entry => $.telemetry.log(entry), |
| 650 | 677 | }, |
| 651 | 678 | e.cwd, |
| 652 | 679 | ) |
| 653 | 680 | |
| 681 | + if (host) { | |
| 682 | + void openOnRestore(host).catch(() => undefined) | |
| 683 | + } | |
| 684 | + | |
| 654 | 685 | return next(e) |
| 655 | 686 | }) |
| 656 | 687 | |
| @@ -659,12 +690,18 @@ | ||
| 659 | 690 | const viewport: { columns?: number; isFullscreen?: boolean } | undefined = |
| 660 | 691 | e.viewport |
| 661 | 692 | |
| 693 | + const isFirstMeasure = columns === null && viewport?.columns !== undefined | |
| 694 | + | |
| 662 | 695 | columns = viewport?.columns ?? columns |
| 663 | 696 | |
| 664 | 697 | model = { |
| 665 | 698 | ...model, |
| 666 | 699 | isFullscreen: viewport?.isFullscreen ?? model.isFullscreen, |
| 667 | 700 | } |
| 701 | + | |
| 702 | + if (isFirstMeasure && hasRestoredEdits && host) { | |
| 703 | + void openOnFirstEdit(host).catch(() => undefined) | |
| 704 | + } | |
| 668 | 705 | } |
| 669 | 706 | |
| 670 | 707 | return next(e) |
| @@ -841,20 +878,39 @@ | ||
| 841 | 878 | on('command.run', { command: ['clear', 'resume'] }, async ($, e, next) => { |
| 842 | 879 | const result = await next(e) |
| 843 | 880 | |
| 844 | - if (host) { | |
| 845 | - if (isPaneOpen) { | |
| 846 | - await closePane(host).catch(() => undefined) | |
| 847 | - } | |
| 881 | + if (!host) { | |
| 882 | + return result | |
| 883 | + } | |
| 848 | 884 | |
| 849 | - unpin() | |
| 850 | - hasAutoOpened = false | |
| 851 | - bodyStamp = null | |
| 852 | - bodyBase = null | |
| 853 | - bodyLoads.clear() | |
| 854 | - disarm(host) | |
| 855 | - model = PaneState.afterNewSession(model) | |
| 885 | + const isResume = e.command === 'resume' | |
| 886 | + const isKeptOpen = isPaneOpen && !isResume | |
| 887 | + | |
| 888 | + if (isPaneOpen && isResume) { | |
| 889 | + await closePane(host).catch(() => undefined) | |
| 856 | 890 | } |
| 857 | 891 | |
| 892 | + unpin() | |
| 893 | + hasAutoOpened = false | |
| 894 | + hasRestoredEdits = false | |
| 895 | + bodyStamp = null | |
| 896 | + bodyBase = null | |
| 897 | + bodyLoads.clear() | |
| 898 | + disarm(host) | |
| 899 | + model = PaneState.afterNewSession(model) | |
| 900 | + | |
| 901 | + sessionStartMs = | |
| 902 | + (await startedAtOf(host)) ?? | |
| 903 | + (isResume ? sessionStartMs : await host.now()) | |
| 904 | + | |
| 905 | + if (isKeptOpen) { | |
| 906 | + await pinBackend(host) | |
| 907 | + void refresh(host) | |
| 908 | + } | |
| 909 | + | |
| 910 | + if (isResume) { | |
| 911 | + void openOnRestore(host).catch(() => undefined) | |
| 912 | + } | |
| 913 | + | |
| 858 | 914 | return result |
| 859 | 915 | }) |
| 860 | 916 |
| @@ -1,8 +1,9 @@ | ||
| 1 | -import type { Settings } from 'claude-code' | |
| 1 | +import type { SessionMessage, Settings } from 'claude-code' | |
| 2 | 2 | |
| 3 | 3 | /** |
| 4 | 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. | |
| 5 | + * git: the store, the settings, the environment, the transcript, whether a | |
| 6 | + * pane is seated. | |
| 6 | 7 | */ |
| 7 | 8 | export type Beneath = { |
| 8 | 9 | /** |
| @@ -21,6 +22,12 @@ | ||
| 21 | 22 | env?: Readonly<Record<string, string>> |
| 22 | 23 | |
| 23 | 24 | /** |
| 25 | + * What `$.session.messages` answers each time it is read, as a resumed | |
| 26 | + * session's transcript already holds turns; empty when not given. | |
| 27 | + */ | |
| 28 | + messages?: () => readonly SessionMessage[] | |
| 29 | + | |
| 30 | + /** | |
| 24 | 31 | * Whether an open made now is left waiting undrawn (LEFT_WAITING), as an |
| 25 | 32 | * engine leaves an unasked open on a narrow terminal; placed when not given. |
| 26 | 33 | */ |
| @@ -1,0 +1,14 @@ | ||
| 1 | +import type { SessionMessage } from 'claude-code' | |
| 2 | + | |
| 3 | +import { editPatchOf } from './edit-patch-of.js' | |
| 4 | +import TurnDiffsOf from './turn-diffs-of' | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * A transcript a resumed session brings back whose one turn edited a file: | |
| 8 | + * the prompt, the Edit that landed, its result. | |
| 9 | + */ | |
| 10 | +export const EDITED_TRANSCRIPT: readonly SessionMessage[] = [ | |
| 11 | + TurnDiffsOf.promptOf('change a to b'), | |
| 12 | + TurnDiffsOf.editsOf(editPatchOf(['-a', '+b'])), | |
| 13 | + TurnDiffsOf.toolResults(), | |
| 14 | +] |
| @@ -18,7 +18,8 @@ | ||
| 18 | 18 | * |
| 19 | 19 | * @param on the test's `on` |
| 20 | 20 | * @param script git's output for each invocation whose line holds the key |
| 21 | - * @param beneath the store, settings, environment, whether an open is seated | |
| 21 | + * @param beneath the store, settings, environment, transcript, whether an | |
| 22 | + * open is seated | |
| 22 | 23 | * @returns the runs, the ring's moves, the panes opened, left waiting and |
| 23 | 24 | * closed, the statuses, the clock |
| 24 | 25 | */ |
| @@ -68,7 +69,7 @@ | ||
| 68 | 69 | on('ui.close', closed.hook) |
| 69 | 70 | on('ui.invalidate', () => ({ value: undefined })) |
| 70 | 71 | on('ui.render', { component: 'PromptHint' }, () => HINT_DRAWN) |
| 71 | - on('session.messages', () => ({ value: [] })) | |
| 72 | + on('session.messages', () => ({ value: [...(beneath.messages?.() ?? [])] })) | |
| 72 | 73 | on('settings.read', () => ({ value: beneath.settings ?? {} })) |
| 73 | 74 | mock.store(on, beneath.stored ?? {}) |
| 74 | 75 | mock.env(on, beneath.env ?? {}) |
| @@ -13,6 +13,7 @@ | ||
| 13 | 13 | export * from './diff.js' |
| 14 | 14 | export * from './diff-at.js' |
| 15 | 15 | export * from './edit-patch-of.js' |
| 16 | +export * from './edited-transcript.js' | |
| 16 | 17 | export * from './fetch-diff' |
| 17 | 18 | export * from './git-hung.js' |
| 18 | 19 | export * from './git-in.js' |
| @@ -42,6 +43,7 @@ | ||
| 42 | 43 | export * from './renamed.js' |
| 43 | 44 | export * from './repository.js' |
| 44 | 45 | export * from './repository-of' |
| 46 | +export * from './resume.js' | |
| 45 | 47 | export * from './ring-onto.js' |
| 46 | 48 | export * from './scripted' |
| 47 | 49 | export * from './session.js' |
| @@ -54,6 +56,7 @@ | ||
| 54 | 56 | export * from './two-files.js' |
| 55 | 57 | export * from './typed-prompt-of.js' |
| 56 | 58 | export * from './unsaid-hint.js' |
| 59 | +export * from './usage-at.js' | |
| 57 | 60 | export * from './vs-main.js' |
| 58 | 61 | export * from './wheel-over-list.js' |
| 59 | 62 | export * from './wheel-tick.js' |
| @@ -1,0 +1,12 @@ | ||
| 1 | +import type { CommandRunInput } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * The command that brings an earlier conversation back, as the person types | |
| 5 | + * it under the fullscreen layout on a 160-column terminal. | |
| 6 | + */ | |
| 7 | +export const RESUME: CommandRunInput = { | |
| 8 | + command: 'resume', | |
| 9 | + args: '', | |
| 10 | + origin: { kind: 'composer' }, | |
| 11 | + presentation: { isFullscreen: true, columns: 160 }, | |
| 12 | +} |
| @@ -1,0 +1,14 @@ | ||
| 1 | +/** | |
| 2 | + * What `$.session.usage()` answers for a session that began at the given | |
| 3 | + * moment: an empty window, no rate limits, nothing spent. | |
| 4 | + * | |
| 5 | + * Left untyped, so it compiles against declarations that predate `startedAt`. | |
| 6 | + * | |
| 7 | + * @param startedAt when the session began, by the test's clock | |
| 8 | + * @returns the usage | |
| 9 | + */ | |
| 10 | +export const usageAt = (startedAt: number) => ({ | |
| 11 | + startedAt, | |
| 12 | + context: { window: 200_000 }, | |
| 13 | + rateLimits: [], | |
| 14 | +}) |
| @@ -94,4 +94,42 @@ | ||
| 94 | 94 | expect(drawn).toContain('moved.ts') |
| 95 | 95 | expect(drawn).toContain('+1 file edited before this session (show)') |
| 96 | 96 | }) |
| 97 | + | |
| 98 | + test("the session began when the engine says: a resumed one's first start, again at /clear", async ($, on) => { | |
| 99 | + let startedAt = 0 | |
| 100 | + | |
| 101 | + const clock = Fixtures.startsSession(on, Fixtures.SETTLE_MS) | |
| 102 | + | |
| 103 | + on('process.run', ($, e) => ({ | |
| 104 | + value: Fixtures.gitIn(e.argv, Fixtures.MOVED_IN), | |
| 105 | + })) | |
| 106 | + | |
| 107 | + on('ui.open', () => ({ value: undefined })) | |
| 108 | + on('ui.invalidate', () => ({ value: undefined })) | |
| 109 | + on('session.messages', () => ({ value: [] })) | |
| 110 | + on('session.usage', () => ({ value: Fixtures.usageAt(startedAt) })) | |
| 111 | + on('command.run', { command: 'clear' }, () => ({})) | |
| 112 | + Fixtures.oldFiles(on) | |
| 113 | + mock.store(on) | |
| 114 | + | |
| 115 | + await $.session.start(Fixtures.SESSION) | |
| 116 | + await $.command.run(Fixtures.DIFF) | |
| 117 | + await clock.advance(Fixtures.SETTLE_MS) | |
| 118 | + | |
| 119 | + expect( | |
| 120 | + Fixtures.textOf(await $.ui.render(Fixtures.PANE)), | |
| 121 | + 'resumed from before the old files were written: both are the ' + | |
| 122 | + "session's, as the built-in restores its start", | |
| 123 | + ).toContain('2 files changed +2 -2') | |
| 124 | + | |
| 125 | + startedAt = clock.now() | |
| 126 | + | |
| 127 | + await $.command.run(Fixtures.CLEAR) | |
| 128 | + await clock.advance(Fixtures.SETTLE_MS) | |
| 129 | + | |
| 130 | + expect( | |
| 131 | + Fixtures.textOf(await $.ui.render(Fixtures.PANE)), | |
| 132 | + '/clear started the session over, past the dirty file', | |
| 133 | + ).toContain('+1 file edited before this session (show)') | |
| 134 | + }) | |
| 97 | 135 | }) |
| @@ -1,4 +1,4 @@ | ||
| 1 | -import type { Args, ResultOf } from 'claude-code' | |
| 1 | +import type { Args, ResultOf, SessionMessage } from 'claude-code' | |
| 2 | 2 | import { describe, expect, mock, test, tier } from 'claude-code/testing' |
| 3 | 3 | |
| 4 | 4 | import Limits from '../hooks/limits' |
| @@ -681,18 +681,139 @@ | ||
| 681 | 681 | expect(drawn).not.toContain('Loading diff') |
| 682 | 682 | }) |
| 683 | 683 | |
| 684 | - test('/clear closes the pane it finds open', async ($, on) => { | |
| 684 | + test('/clear leaves the pane it finds open up and reads the repository afresh', async ($, on) => { | |
| 685 | 685 | const world = Fixtures.inRepository(on) |
| 686 | 686 | |
| 687 | 687 | on('command.run', { command: 'clear' }, () => ({})) |
| 688 | 688 | |
| 689 | 689 | await $.session.start(Fixtures.SESSION) |
| 690 | 690 | await $.command.run(Fixtures.DIFF) |
| 691 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 692 | + | |
| 693 | + const read = world.runs.length | |
| 694 | + | |
| 691 | 695 | await $.command.run(Fixtures.CLEAR) |
| 696 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 692 | 697 | |
| 693 | - expect(world.closed.map(pane => pane.id)).toEqual(['diff']) | |
| 698 | + expect(world.closed, 'as the built-in panel stays across /clear').toEqual( | |
| 699 | + [], | |
| 700 | + ) | |
| 701 | + | |
| 702 | + expect( | |
| 703 | + world.runs.slice(read).map(run => Fixtures.gitWordOf(run.argv))[0], | |
| 704 | + 'the repository found again for the conversation that starts over', | |
| 705 | + ).toBe('rev-parse --show-toplevel') | |
| 706 | + | |
| 707 | + expect( | |
| 708 | + Fixtures.textOf(await $.ui.render(Fixtures.PANE)), | |
| 709 | + 'drawn from the fresh read', | |
| 710 | + ).toContain('1 file changed') | |
| 694 | 711 | }) |
| 695 | 712 | |
| 713 | + test('a resumed session whose turns edited opens the pane before any new edit', async ($, on) => { | |
| 714 | + const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { | |
| 715 | + messages: () => Fixtures.EDITED_TRANSCRIPT, | |
| 716 | + }) | |
| 717 | + | |
| 718 | + await $.session.start(Fixtures.SESSION) | |
| 719 | + | |
| 720 | + expect(world.runs, 'the start itself still runs no git').toEqual([]) | |
| 721 | + | |
| 722 | + await $.ui.render(Fixtures.HINT) | |
| 723 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 724 | + | |
| 725 | + expect( | |
| 726 | + world.opened.map(pane => pane.id), | |
| 727 | + 'open once the width is known, as the built-in opens on the restore', | |
| 728 | + ).toEqual(['diff']) | |
| 729 | + }) | |
| 730 | + | |
| 731 | + test('a resumed session opens nothing where its first edit would not', async ($, on) => { | |
| 732 | + const narrow = Fixtures.inRepository(on, Fixtures.REPOSITORY, { | |
| 733 | + messages: () => Fixtures.EDITED_TRANSCRIPT, | |
| 734 | + }) | |
| 735 | + | |
| 736 | + await $.session.start(Fixtures.SESSION) | |
| 737 | + await $.ui.render(Fixtures.hintAt(Limits.AUTO_OPEN_MIN_COLUMNS - 1)) | |
| 738 | + await narrow.clock.advance(Fixtures.SETTLE_MS) | |
| 739 | + | |
| 740 | + expect(narrow.opened, 'under the unasked floor, never kept open').toEqual( | |
| 741 | + [], | |
| 742 | + ) | |
| 743 | + | |
| 744 | + expect(narrow.runs, 'and no git for it').toEqual([]) | |
| 745 | + }) | |
| 746 | + | |
| 747 | + test('a resumed session the person kept the pane open in opens it from the lower floor', async ($, on) => { | |
| 748 | + const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { | |
| 749 | + messages: () => Fixtures.EDITED_TRANSCRIPT, | |
| 750 | + stored: { [Names.STORE_OPEN_KEY]: true }, | |
| 751 | + }) | |
| 752 | + | |
| 753 | + await $.session.start(Fixtures.SESSION) | |
| 754 | + await $.ui.render(Fixtures.hintAt(Limits.OPEN_MIN_COLUMNS)) | |
| 755 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 756 | + | |
| 757 | + expect(world.opened.map(pane => pane.id)).toEqual(['diff']) | |
| 758 | + }) | |
| 759 | + | |
| 760 | + test('a resumed session with no edits opens nothing', async ($, on) => { | |
| 761 | + const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { | |
| 762 | + messages: () => [Fixtures.promptOf('just talk')], | |
| 763 | + }) | |
| 764 | + | |
| 765 | + await $.session.start(Fixtures.SESSION) | |
| 766 | + await $.ui.render(Fixtures.HINT) | |
| 767 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 768 | + | |
| 769 | + expect(world.opened).toEqual([]) | |
| 770 | + expect(world.runs).toEqual([]) | |
| 771 | + }) | |
| 772 | + | |
| 773 | + test('a resumed session whose pane the person closed opens nothing', async ($, on) => { | |
| 774 | + const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { | |
| 775 | + messages: () => Fixtures.EDITED_TRANSCRIPT, | |
| 776 | + stored: { [Names.STORE_OPEN_KEY]: false }, | |
| 777 | + }) | |
| 778 | + | |
| 779 | + await $.session.start(Fixtures.SESSION) | |
| 780 | + await $.ui.render(Fixtures.HINT) | |
| 781 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 782 | + | |
| 783 | + expect(world.opened).toEqual([]) | |
| 784 | + expect(world.runs).toEqual([]) | |
| 785 | + }) | |
| 786 | + | |
| 787 | + test('/resume closes the pane, then opens it for the turns it brought back', async ($, on) => { | |
| 788 | + let transcript: readonly SessionMessage[] = [] | |
| 789 | + | |
| 790 | + const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, { | |
| 791 | + messages: () => transcript, | |
| 792 | + }) | |
| 793 | + | |
| 794 | + on('command.run', { command: 'resume' }, () => { | |
| 795 | + transcript = Fixtures.EDITED_TRANSCRIPT | |
| 796 | + | |
| 797 | + return {} | |
| 798 | + }) | |
| 799 | + | |
| 800 | + await $.session.start(Fixtures.SESSION) | |
| 801 | + await $.ui.render(Fixtures.HINT) | |
| 802 | + await $.command.run(Fixtures.DIFF) | |
| 803 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 804 | + await $.command.run(Fixtures.RESUME) | |
| 805 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 806 | + | |
| 807 | + expect(world.closed.map(pane => pane.id), 'closed at /resume').toEqual([ | |
| 808 | + 'diff', | |
| 809 | + ]) | |
| 810 | + | |
| 811 | + expect( | |
| 812 | + world.opened.map(pane => pane.id), | |
| 813 | + '/diff opened it, /resume opened it again for the restored turns', | |
| 814 | + ).toEqual(['diff', 'diff']) | |
| 815 | + }) | |
| 816 | + | |
| 696 | 817 | test('ask attaches the hunks on screen and calls no tool', async ($, on) => { |
| 697 | 818 | const world = Fixtures.inRepository(on, Fixtures.oneSecret()) |
| 698 | 819 | const called: Args<'tool.call'>[] = [] |