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

chore: Update mods/Browse files
GitHub Actions (publish-mods) authored 10 days ago parent fa3ffa4 · commit addd7a69690b
12 changed files+248 -25compared with fa3ffa4
Mmods/diff/README.md
Amods/diff/hooks/names/texts/awaits-surface-text.ts
Mmods/diff/hooks/names/texts/index.ts
Amods/diff/hooks/open-outcome/index.ts
Amods/diff/hooks/open-outcome/open-outcome-of.ts
Amods/diff/hooks/open-outcome/open-outcome.ts
Mmods/diff/hooks/register.ts
Mmods/diff/tests/fixtures/beneath/beneath.ts
Mmods/diff/tests/fixtures/beneath/index.ts
Amods/diff/tests/fixtures/beneath/left-unseen.ts
Mmods/diff/tests/fixtures/in-repository.ts
Mmods/diff/tests/register.test.ts
@@ -23,9 +23,13 @@
2323 itself. A docked pane fetches before it opens, as the built-in panel
2424 primes its data, so it never lands on `Loading diff…`; an open the engine
2525 leaves waiting undrawn is withdrawn, so no later resize seats it, and the
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.
26+next edit asks again. An open answered `no-surface` (nothing is attached
27+that draws it yet, as before a host's page attaches) is kept instead:
28+`/diff` says so, nothing polls or refreshes while no one sees the pane, and
29+its first drawing reads the repository again and counts it shown. A session
30+resumed or continued whose transcript already holds such an edit opens the
31+pane on the same terms as soon as the width is known, as the built-in opens
32+on the history it restores.
2933
3034 Under the fullscreen layout a terminal under 110 columns gets the
3135 built-in's line asking for a wider one and nothing opens. Without that
@@ -1,0 +1,6 @@
1+/**
2+ * `/diff`'s answer where nothing is attached that draws the pane yet: it is
3+ * kept open, and shows as soon as something is.
4+ */
5+export const AWAITS_SURFACE_TEXT =
6+ 'Nothing here can draw the diff panel yet; it opens as soon as something can'
@@ -1,3 +1,4 @@
1+export * from './awaits-surface-text.js'
12 export * from './builtin-holds-pattern.js'
23 export * from './git-unanswered-text.js'
34 export * from './more-below-text.js'
@@ -1,0 +1,4 @@
1+export type * from './open-outcome.js'
2+export * from './open-outcome-of.js'
3+
4+export * as default from '.'
@@ -1,0 +1,25 @@
1+import { isRecord } from '../is-record'
2+import type { OpenOutcome } from './open-outcome.js'
3+
4+/**
5+ * Reads what `$.ui.open` resolved: an unplaced pane is withdrawn, unless the
6+ * reason starts `no-surface`, where the engine keeps it for a surface to come.
7+ *
8+ * An engine that resolves nothing, or never gives that reason, reads as
9+ * before: placed, or withdrawn.
10+ *
11+ * @param opened what the open resolved
12+ * @returns what the plugin does with the pane
13+ */
14+export function openOutcomeOf(opened: unknown): OpenOutcome {
15+ const isUnplaced = isRecord(opened) && opened.isPlaced === false
16+
17+ if (!isUnplaced) {
18+ return 'placed'
19+ }
20+
21+ const isUnseen =
22+ typeof opened.reason === 'string' && opened.reason.startsWith('no-surface')
23+
24+ return isUnseen ? 'awaited' : 'withdrawn'
25+}
@@ -1,0 +1,5 @@
1+/**
2+ * What became of an open: the pane is drawn (`placed`), kept until something
3+ * attaches that draws it (`awaited`), or taken back (`withdrawn`).
4+ */
5+export type OpenOutcome = 'placed' | 'awaited' | 'withdrawn'
@@ -21,6 +21,7 @@
2121 import { messageOf } from './message-of'
2222 import { mtimeOf } from './mtime-of'
2323 import Names from './names'
24+import OpenOutcome from './open-outcome'
2425 import PaneState from './pane-state'
2526 import PaneToggle from './pane-toggle'
2627 import Record from './record'
@@ -44,6 +45,7 @@
4445 let probing: Promise<boolean> | null = null
4546 let sessionStartMs = 0
4647 let isPaneOpen = false
48+ let awaited: (typeof Record.SHOWN_TRIGGERS)[number] | null = null
4749 let dialogRows: number | null = null
4850 let hasAutoOpened = false
4951 let hasRestoredEdits = false
@@ -69,6 +71,7 @@
6971 const loggedBaseKinds = new Set<'ok' | 'sad'>()
7072
7173 const currentOf = (engine: Host): Host => host ?? engine
74+ const isPaneSeen = (): boolean => isPaneOpen && awaited === null
7275
7376 const backendHostOf = (engine: Host): Backend.BackendHost => ({
7477 run: (argv, init) =>
@@ -302,7 +305,7 @@
302305 timers.set(
303306 'poll',
304307 engine.every(Limits.HEAD_POLL_MS, () => {
305- if (!isPaneOpen) {
308+ if (!isPaneSeen()) {
306309 return
307310 }
308311
@@ -414,10 +417,20 @@
414417 }
415418 }
416419
420+ async function showKept(
421+ engine: Host,
422+ trigger: (typeof Record.SHOWN_TRIGGERS)[number],
423+ ): Promise<void> {
424+ await markShown(engine, trigger)
425+ await pinBackend(engine)
426+ keepBaseline()
427+ void refresh(engine)
428+ }
429+
417430 async function openPane(
418431 engine: Host,
419432 trigger: (typeof Record.SHOWN_TRIGGERS)[number],
420- ): Promise<boolean> {
433+ ): Promise<OpenOutcome.OpenOutcome> {
421434 const isDialog = model.isFullscreen === false
422435
423436 model = {
@@ -441,18 +454,22 @@
441454 : { id: Names.PANE_ID, title: Names.PANE_TITLE },
442455 )
443456
444- const isWaiting = isRecord(opened) && opened.isPlaced === false
457+ const outcome = OpenOutcome.openOutcomeOf(opened)
445458
446- if (isWaiting) {
459+ if (outcome === 'withdrawn') {
447460 await engine.closePane({ id: Names.PANE_ID }).catch(() => undefined)
448461
449- return false
462+ return outcome
450463 }
451464
452465 isPaneOpen = true
453466 keepBaseline()
454467
455- await markShown(engine, trigger)
468+ if (outcome === 'awaited') {
469+ awaited = trigger
470+ } else {
471+ await markShown(engine, trigger)
472+ }
456473
457474 const isStale = isDialog || landed !== landedBefore
458475
@@ -460,12 +477,13 @@
460477 void refresh(engine)
461478 }
462479
463- return true
480+ return outcome
464481 }
465482
466483 async function closePane(engine: Host): Promise<void> {
467484 await engine.closePane({ id: Names.PANE_ID })
468485 isPaneOpen = false
486+ awaited = null
469487 }
470488
471489 function markTabSwitch(engine: Host, tab: (typeof Record.TABS)[number]) {
@@ -512,7 +530,7 @@
512530 }
513531
514532 hasAutoOpened = true
515- hasAutoOpened = await openPane(engine, 'auto_open')
533+ hasAutoOpened = (await openPane(engine, 'auto_open')) !== 'withdrawn'
516534 }
517535
518536 async function openOnRestore(engine: Host): Promise<void> {
@@ -727,6 +745,13 @@
727745
728746 columns = e.viewport?.columns ?? columns
729747
748+ if (awaited !== null) {
749+ const trigger = awaited
750+
751+ awaited = null
752+ void showKept(host, trigger).catch(() => undefined)
753+ }
754+
730755 /**
731756 * A seat that changed since the last drawing lists other rows, whose
732757 * bodies are read once.
@@ -796,14 +821,18 @@
796821
797822 const isOpening = toggle === 'open'
798823
799- const isDone = isOpening
824+ const outcome = isOpening
800825 ? await openPane(host, 'manual')
801- : await closePane(host).then(() => true)
826+ : await closePane(host).then(() => 'placed' as const)
802827
803- if (!isDone) {
828+ if (outcome === 'withdrawn') {
804829 return { text: Names.RESIZE_TERMINAL_TEXT }
805830 }
806831
832+ if (outcome === 'awaited') {
833+ return { text: Names.AWAITS_SURFACE_TEXT }
834+ }
835+
807836 if (!isFullscreen) {
808837 return isOpening ? {} : { text: Names.DIALOG_DISMISSED_TEXT }
809838 }
@@ -841,6 +870,7 @@
841870
842871 if (isClosed) {
843872 isPaneOpen = false
873+ awaited = null
844874 }
845875
846876 const isDocking = model.isFullscreen !== false
@@ -904,7 +934,7 @@
904934 }
905935
906936 const isResume = e.command === 'resume'
907- const isKeptOpen = isPaneOpen && !isResume
937+ const isKeptOpen = isPaneSeen() && !isResume
908938
909939 if (isPaneOpen && isResume) {
910940 await closePane(host).catch(() => undefined)
@@ -924,10 +954,7 @@
924954 (isResume ? sessionStartMs : await host.now())
925955
926956 if (isKeptOpen) {
927- await markShown(host, 'manual')
928- await pinBackend(host)
929- keepBaseline()
930- void refresh(host)
957+ await showKept(host, 'manual')
931958 }
932959
933960 if (isResume) {
@@ -956,7 +983,7 @@
956983 landed += 1
957984 }
958985
959- if (hasLanded && isPaneOpen) {
986+ if (hasLanded && isPaneSeen()) {
960987 scheduleRefresh(engine)
961988 }
962989
@@ -36,6 +36,12 @@
3636 isLeftWaiting?: () => boolean
3737
3838 /**
39+ * Whether an open made now is kept with nothing attached to draw it
40+ * (LEFT_UNSEEN), as before a host's page attaches; placed when not given.
41+ */
42+ isLeftUnseen?: () => boolean
43+
44+ /**
3945 * Why a hunks read asked now is refused, as the host refuses a git that
4046 * outran its timeout; answered from the script when null or not given.
4147 */
@@ -1,4 +1,5 @@
11 export type * from './beneath.js'
2+export * from './left-unseen.js'
23 export * from './left-waiting.js'
34 export * from './slow-diff-ms.js'
45
@@ -1,0 +1,16 @@
1+import type { ResultOf } from 'claude-code'
2+
3+/**
4+ * What an engine answers an open it keeps where nothing is attached to draw
5+ * it yet: a host whose page has not attached, or one that never draws.
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_UNSEEN: ResultOf['ui.open'] = {
11+ value: {
12+ isPlaced: false,
13+ reason:
14+ 'no-surface: no surface is attached ($.session.surfaces() is empty)',
15+ } as never,
16+}
@@ -12,16 +12,18 @@
1212 * A session in a repository git answers for from a script (REPOSITORY, in
1313 * /work, when none is given), keeping what the plugin does there.
1414 *
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.
15+ * Kept: each git run, ring move, pane opened, left waiting, left unseen or
16+ * closed, status line. The clock starts at 0 and the engine draws the hint.
1817 *
18+ * The rest of the world is the test's (Beneath). Rewriting the script changes
19+ * git's answers.
20+ *
1921 * @param on the test's `on`
2022 * @param script git's output for each invocation whose line holds the key
2123 * @param beneath the store, settings, environment, transcript, whether an
2224 * open is seated, whether a hunks read is refused
23- * @returns the runs, the ring's moves, the panes opened, left waiting and
24- * closed, the statuses, the clock
25+ * @returns the runs, the ring's moves, the panes opened, left waiting, left
26+ * unseen and closed, the statuses, the clock
2527 */
2628 export function inRepository(
2729 on: On,
@@ -32,6 +34,7 @@
3234 const focused: Args<'ui.focus'>[] = []
3335 const statuses: (string | undefined)[] = []
3436 const waiting: Args<'ui.open'>[] = []
37+ const unseen: Args<'ui.open'>[] = []
3538 const opened = keeping<Args<'ui.open'>>()
3639 const closed = keeping<Args<'ui.close'>>()
3740 const clock = startsSession(on)
@@ -59,7 +62,14 @@
5962
6063 on('ui.open', (engine, e) => {
6164 const isWaiting = beneath.isLeftWaiting?.() === true
65+ const isUnseen = beneath.isLeftUnseen?.() === true
6266
67+ if (isUnseen) {
68+ unseen.push(e)
69+
70+ return Beneath.LEFT_UNSEEN
71+ }
72+
6373 if (!isWaiting) {
6474 return opened.hook(engine, e)
6575 }
@@ -82,6 +92,7 @@
8292 focused,
8393 opened: opened.kept,
8494 waiting,
95+ unseen,
8596 closed: closed.kept,
8697 statuses,
8798 clock,
@@ -4,6 +4,7 @@
44
55 import Limits from '../hooks/limits'
66 import Names from '../hooks/names'
7+import OpenOutcome from '../hooks/open-outcome'
78 import Record from '../hooks/record'
89 import Fixtures from './fixtures'
910
@@ -701,6 +702,101 @@
701702 ).toEqual(['diff'])
702703 })
703704
705+ test('an open nothing draws yet is kept until drawn', async ($, on) => {
706+ const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
707+ isLeftUnseen: () => true,
708+ })
709+
710+ const logged = Fixtures.keeping<Args<'telemetry.log'>>()
711+
712+ on('telemetry.log', logged.hook)
713+ on('session.id', () => ({ value: 'first' }))
714+
715+ await $.session.start(Fixtures.SESSION)
716+
717+ expect(
718+ await $.command.run(Fixtures.DIALOG_DIFF),
719+ 'said as it is, with no word of a terminal to resize',
720+ ).toEqual({ text: Names.AWAITS_SURFACE_TEXT })
721+
722+ await world.clock.advance(Limits.HEAD_POLL_MS + Fixtures.SETTLE_MS)
723+
724+ expect(
725+ world.unseen.map(pane => pane.id),
726+ '/diff opened, then fitted the rows to what it read',
727+ ).toEqual(['diff', 'diff'])
728+
729+ expect(world.closed, 'and kept what it asked for').toEqual([])
730+
731+ expect(
732+ world.runs
733+ .map(run => Fixtures.gitWordOf(run.argv))
734+ .filter(word => word === Fixtures.POLL_WORD),
735+ 'HEAD was read once at the open, as ever; no poll for a pane no one sees',
736+ ).toEqual([Fixtures.POLL_WORD])
737+
738+ expect(logged.kept, 'nor is it counted shown').toEqual([])
739+
740+ const read = world.runs.length
741+
742+ expect(
743+ Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE)),
744+ 'the first drawing has what the open read',
745+ ).toContain('app.ts')
746+
747+ await world.clock.advance(Fixtures.SETTLE_MS)
748+
749+ expect(
750+ world.runs.length,
751+ 'and reads again, for what changed while no one saw',
752+ ).toBeGreaterThan(read)
753+
754+ expect(
755+ logged.kept.map(shown => shown.props?.trigger),
756+ 'and counted it, as the person asked for it',
757+ ).toEqual([{ value: 'manual', of: [...Record.SHOWN_TRIGGERS] }])
758+
759+ expect(
760+ await $.command.run(Fixtures.DIALOG_DIFF),
761+ 'believed open all along, so /diff closes it',
762+ ).toEqual({ text: 'Diff dialog dismissed' })
763+ })
764+
765+ test('while it waits unseen an edit reads nothing', async ($, on) => {
766+ const world = Fixtures.inRepository(on, Fixtures.REPOSITORY, {
767+ stored: { [Names.STORE_OPEN_KEY]: true },
768+ isLeftUnseen: () => true,
769+ })
770+
771+ const edit = () =>
772+ $.tool.call({
773+ tool: 'Edit',
774+ file_path: '/work/app.ts',
775+ old_string: '1',
776+ new_string: '2',
777+ })
778+
779+ on('tool.call', () => ({ result: 'edited' }))
780+
781+ await $.session.start(Fixtures.SESSION)
782+ await $.ui.render(Fixtures.HINT)
783+ await edit()
784+ await world.clock.advance(Fixtures.SETTLE_MS)
785+
786+ const read = world.runs.length
787+
788+ await edit()
789+ await world.clock.advance(Limits.HEAD_POLL_MS + Fixtures.SETTLE_MS)
790+
791+ expect(
792+ world.unseen.map(pane => pane.id),
793+ 'the first edit opened, and the second found it open',
794+ ).toEqual(['diff'])
795+
796+ expect(world.closed, 'kept for whatever attaches').toEqual([])
797+ expect(world.runs.length, 'git is left alone meanwhile').toBe(read)
798+ })
799+
704800 test('a docked pane opens once its first fetch settled', async ($, on) => {
705801 const opened: string[] = []
706802 const clock = Fixtures.startsSession(on)
@@ -1157,4 +1253,25 @@
11571253 expect(openedNarrow).toEqual([])
11581254 expect(world.opened.map(pane => pane.id)).toEqual([Names.PANE_ID])
11591255 })
1256+
1257+ describe('open-outcome-of', () => {
1258+ test('placed: a placed pane, and an engine that resolves nothing', () => {
1259+ expect(OpenOutcome.openOutcomeOf({ isPlaced: true })).toBe('placed')
1260+ expect(OpenOutcome.openOutcomeOf(undefined)).toBe('placed')
1261+ })
1262+
1263+ test('left waiting it is withdrawn, with a reason or none', () => {
1264+ expect(OpenOutcome.openOutcomeOf(Fixtures.LEFT_WAITING.value)).toBe(
1265+ 'withdrawn',
1266+ )
1267+
1268+ expect(OpenOutcome.openOutcomeOf({ isPlaced: false })).toBe('withdrawn')
1269+ })
1270+
1271+ test('with nothing attached to draw it yet it is awaited', () => {
1272+ expect(OpenOutcome.openOutcomeOf(Fixtures.LEFT_UNSEEN.value)).toBe(
1273+ 'awaited',
1274+ )
1275+ })
1276+ })
11601277 })