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
| Mmods/diff/README.md |
| Mmods/diff/tests/fixtures/in-repository.ts |
| Mmods/diff/tests/fixtures/index.ts |
| Amods/diff/tests/fixtures/ring-onto.ts |
| Mmods/diff/tests/views.test.ts |
| @@ -22,9 +22,10 @@ | ||
| 22 | 22 | layout (`CLAUDE_CODE_NO_FLICKER=0`, which `/diff` learns from the command's |
| 23 | 23 | `presentation`) the pane opens inline at any width, focused and as tall |
| 24 | 24 | as its content (the open's `rows`), in the built-in dialog's shape: the |
| 25 | -title, the count, five file rows at a time between `more files` rows that | |
| 26 | -page them, the key hints; the arrows walk the rows, Enter shows that | |
| 27 | -file's hunks alone, | |
| 25 | +title, the count, five file rows at a time round the selected one (`❯`, | |
| 26 | +where the focus ring starts; the plugin follows the ring's walk through | |
| 27 | +`ui.focus` and re-centres the rows as the built-in does), the key hints; | |
| 28 | +Enter shows that file's hunks alone, | |
| 28 | 29 | Escape backs out to the list and then closes, leaving `Diff dialog |
| 29 | 30 | dismissed`; toasts are held while it is up. A file's ask button arms that |
| 30 | 31 | file: its hunks ride the next prompt as context, once. |
| @@ -52,6 +53,7 @@ | ||
| 52 | 53 | | `command.run` of `diff` | Opens or closes the pane (focused and closing on Escape without the fullscreen layout), says which, and remembers the choice. | |
| 53 | 54 | | `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. | |
| 54 | 55 | | `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. | |
| 56 | +| `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. | | |
| 55 | 57 | | `command.run` of `clear`, `resume` | Closes the pane and forgets the session's state. | |
| 56 | 58 | | `tool.call` of `Edit`, `Write`, `NotebookEdit` | After the edit, refreshes an open pane; the session's first successful edit opens it. | |
| 57 | 59 | | `tool.call` of `Bash`, `PowerShell` | After the command, refreshes an open pane. | |
| @@ -22,6 +22,7 @@ | ||
| 22 | 22 | answers: Readonly<Record<string, string>> = REPOSITORY, |
| 23 | 23 | ) { |
| 24 | 24 | const runs: Args<'process.run'>[] = [] |
| 25 | + const focused: Args<'ui.focus'>[] = [] | |
| 25 | 26 | const opened = keeping<Args<'ui.open'>>() |
| 26 | 27 | const closed = keeping<Args<'ui.close'>>() |
| 27 | 28 | const clock = startsSession(on) |
| @@ -32,6 +33,12 @@ | ||
| 32 | 33 | return { value: gitIn(e.argv, answers) } |
| 33 | 34 | }) |
| 34 | 35 | |
| 36 | + on('ui.focus', (_engine, e) => { | |
| 37 | + focused.push(e) | |
| 38 | + | |
| 39 | + return {} | |
| 40 | + }) | |
| 41 | + | |
| 35 | 42 | on('ui.open', opened.hook) |
| 36 | 43 | on('ui.close', closed.hook) |
| 37 | 44 | on('ui.invalidate', () => ({ value: undefined })) |
| @@ -39,5 +46,5 @@ | ||
| 39 | 46 | on('session.messages', () => ({ value: [] })) |
| 40 | 47 | mock.store(on) |
| 41 | 48 | |
| 42 | - return { runs, opened: opened.kept, closed: closed.kept, clock } | |
| 49 | + return { runs, focused, opened: opened.kept, closed: closed.kept, clock } | |
| 43 | 50 | } |
| @@ -19,6 +19,7 @@ | ||
| 19 | 19 | export * from './renamed.js' |
| 20 | 20 | export * from './repository.js' |
| 21 | 21 | export * from './repository-of.js' |
| 22 | +export * from './ring-onto.js' | |
| 22 | 23 | export * from './session.js' |
| 23 | 24 | export * from './settle-ms.js' |
| 24 | 25 | export * from './starts-session.js' |
| @@ -1,0 +1,18 @@ | ||
| 1 | +import type { UiFocusInput } from 'claude-code' | |
| 2 | + | |
| 3 | +import { PANE } from './pane.js' | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * The person walking the pane's focus ring onto one of the plugin's | |
| 7 | + * elements, as the engine raises it before the ring moves. | |
| 8 | + * | |
| 9 | + * @param key the element's key | |
| 10 | + * @returns the `ui.focus` input | |
| 11 | + */ | |
| 12 | +export const ringOnto = (key: string): UiFocusInput => ({ | |
| 13 | + component: 'Pane', | |
| 14 | + requestId: PANE.requestId, | |
| 15 | + plugin: 'diff', | |
| 16 | + element: key, | |
| 17 | + origin: { kind: 'person' }, | |
| 18 | +}) |
| @@ -140,7 +140,7 @@ | ||
| 140 | 140 | |
| 141 | 141 | expect(drawn).toContain('Uncommitted changes (git diff HEAD)') |
| 142 | 142 | expect(drawn).toContain('2 files changed +3 -1') |
| 143 | - expect(drawn).toContain(' app.ts') | |
| 143 | + expect(drawn).toContain('\u276f app.ts') | |
| 144 | 144 | expect(drawn).toContain(' lib.ts') |
| 145 | 145 | expect(drawn).not.toContain('+const a = 2') |
| 146 | 146 | |
| @@ -171,7 +171,7 @@ | ||
| 171 | 171 | ) |
| 172 | 172 | }) |
| 173 | 173 | |
| 174 | - test('off fullscreen, forty files list five at a time', async ($, on) => { | |
| 174 | + test('off fullscreen, the window follows the walk', async ($, on) => { | |
| 175 | 175 | const world = Fixtures.inRepository(on, Fixtures.MANY_FILES) |
| 176 | 176 | |
| 177 | 177 | await $.session.start(Fixtures.SESSION) |
| @@ -180,19 +180,24 @@ | ||
| 180 | 180 | |
| 181 | 181 | const drawn = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE)) |
| 182 | 182 | |
| 183 | + expect(drawn).toContain('\u276f file0.ts') | |
| 183 | 184 | expect(drawn).toContain(' file4.ts') |
| 184 | 185 | expect(drawn).not.toContain('file5.ts') |
| 185 | 186 | expect(drawn).toContain(' \u2193 5 more files') |
| 187 | + expect(await $.ui.focus(Fixtures.ringOnto('file:file3.ts'))).toEqual({}) | |
| 186 | 188 | |
| 187 | - expect(await $.ui.press({ plugin: 'diff', key: 'files-down' })).toEqual({ | |
| 188 | - element: 'files-down', | |
| 189 | - }) | |
| 189 | + expect( | |
| 190 | + world.focused.map(focus => focus.element), | |
| 191 | + 'its row once centred', | |
| 192 | + ).toEqual(['file:file2.ts']) | |
| 190 | 193 | |
| 191 | - const paged = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE)) | |
| 194 | + const walked = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE)) | |
| 192 | 195 | |
| 193 | - expect(paged).toContain(' \u2191 5 more files') | |
| 194 | - expect(paged).toContain(' file9.ts') | |
| 195 | - expect(paged).not.toContain('file4.ts') | |
| 196 | + expect(walked).toContain(' \u2191 1 more file') | |
| 197 | + expect(walked).toContain('\u276f file3.ts') | |
| 198 | + expect(walked).toContain(' file5.ts') | |
| 199 | + expect(walked).toContain(' \u2193 4 more files') | |
| 200 | + expect(walked).not.toContain('file0.ts') | |
| 196 | 201 | }) |
| 197 | 202 | |
| 198 | 203 | test('off fullscreen, the dialog draws at any width', async ($, on) => { |