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
| @@ -3,6 +3,7 @@ | ||
| 3 | 3 | export * from './keys' |
| 4 | 4 | export * from './pane-id.js' |
| 5 | 5 | export * from './pane-title.js' |
| 6 | +export * from './plugin-name.js' | |
| 6 | 7 | export * from './texts' |
| 7 | 8 | |
| 8 | 9 | export * as default from '.' |
| @@ -1,0 +1,5 @@ | ||
| 1 | +/** | |
| 2 | + * The plugin's own name, as the engine stamps it on the elements its render | |
| 3 | + * hooks draw (`ui.press`, `ui.focus`). | |
| 4 | + */ | |
| 5 | +export const PLUGIN_NAME = 'diff' |
| @@ -1,0 +1,8 @@ | ||
| 1 | +/** | |
| 2 | + * What the ring landing on a dialog row settles: the file now selected, | |
| 3 | + * and the key of the row to land the ring on once the window re-centres. | |
| 4 | + */ | |
| 5 | +export type DialogFocus = { | |
| 6 | + selectedPath: string | |
| 7 | + landing: string | |
| 8 | +} |
| @@ -1,0 +1,3 @@ | ||
| 1 | +export type * from './dialog-focus.js' | |
| 2 | + | |
| 3 | +export * as default from '.' |
| @@ -2,6 +2,7 @@ | ||
| 2 | 2 | export * from './after-new-session' |
| 3 | 3 | export * from './base-mode-of' |
| 4 | 4 | export * from './bodies' |
| 5 | +export * from './dialog-focus' | |
| 5 | 6 | export * from './dialog-title' |
| 6 | 7 | export * from './dialog-title-of.js' |
| 7 | 8 | export * from './empty-state' |
| @@ -467,13 +467,7 @@ | ||
| 467 | 467 | redraw(engine) |
| 468 | 468 | }, |
| 469 | 469 | scrollList: delta => { |
| 470 | - const place = Views.listScrolledBy(model, delta) | |
| 471 | - const isDocked = model.placement === 'dock' | |
| 472 | - | |
| 473 | - model = isDocked | |
| 474 | - ? { ...model, place } | |
| 475 | - : { ...model, place, selectedPath: null } | |
| 476 | - | |
| 470 | + model = { ...model, place: Views.listScrolledBy(model, delta) } | |
| 477 | 471 | redraw(engine) |
| 478 | 472 | }, |
| 479 | 473 | toggleNoise: () => { |
| @@ -729,6 +723,23 @@ | ||
| 729 | 723 | return result |
| 730 | 724 | }) |
| 731 | 725 | |
| 726 | + on('ui.focus', { plugin: Names.PLUGIN_NAME }, ($, e, next) => { | |
| 727 | + const isListed = | |
| 728 | + model.placement === 'inline' && model.dialogView === 'list' && host | |
| 729 | + | |
| 730 | + const focus = isListed ? Views.dialogFocusOf(model, e.element) : null | |
| 731 | + | |
| 732 | + if (!focus || !host) { | |
| 733 | + return next(e) | |
| 734 | + } | |
| 735 | + | |
| 736 | + model = { ...model, selectedPath: focus.selectedPath } | |
| 737 | + fitDialog(host) | |
| 738 | + host.invalidate() | |
| 739 | + | |
| 740 | + return next({ ...e, element: focus.landing }) | |
| 741 | + }) | |
| 742 | + | |
| 732 | 743 | on('ui.scroll', { requestId: Names.PANE_ID }, ($, e, next) => { |
| 733 | 744 | const isOwnBody = e.origin.kind === 'person' && model.placement === 'dock' |
| 734 | 745 |
| @@ -1,11 +1,10 @@ | ||
| 1 | 1 | import Limits from '../../limits' |
| 2 | 2 | import type PaneState from '../../pane-state' |
| 3 | -import DialogPane from '../dialog-pane' | |
| 4 | 3 | import Plan from './plan' |
| 5 | 4 | |
| 6 | 5 | /** |
| 7 | - * The pane's place with its list window moved by so many files, clamped: | |
| 8 | - * docked, ReplDiffSidebar's scrollSummary; inline, the dialog's window. | |
| 6 | + * The docked pane's place with the list window moved by so many files, | |
| 7 | + * clamped to the list (ReplDiffSidebar's scrollSummary). | |
| 9 | 8 | * |
| 10 | 9 | * @param model the pane's state |
| 11 | 10 | * @param delta the files to move by, negative toward the top |
| @@ -15,19 +14,16 @@ | ||
| 15 | 14 | model: PaneState.PaneModel, |
| 16 | 15 | delta: number, |
| 17 | 16 | ): PaneState.PaneModel['place'] { |
| 18 | - const isDocked = model.placement === 'dock' | |
| 19 | - const paths = DialogPane.dialogEntriesOf(model).map(entry => entry.path) | |
| 17 | + const last = Math.max( | |
| 18 | + 0, | |
| 19 | + Plan.dockPlanOf(model).rows.length - Limits.MAX_SUMMARY_ROWS, | |
| 20 | + ) | |
| 20 | 21 | |
| 21 | - const last = isDocked | |
| 22 | - ? Plan.dockPlanOf(model).rows.length - Limits.MAX_SUMMARY_ROWS | |
| 23 | - : paths.length - Limits.MAX_VISIBLE_FILES | |
| 24 | - | |
| 25 | - const start = isDocked | |
| 26 | - ? Math.min(model.place.listStart, Math.max(0, last)) | |
| 27 | - : DialogPane.dialogWindowOf(model, paths) | |
| 28 | - | |
| 29 | 22 | return { |
| 30 | 23 | ...model.place, |
| 31 | - listStart: Math.max(0, Math.min(Math.max(0, last), start + delta)), | |
| 24 | + listStart: Math.max( | |
| 25 | + 0, | |
| 26 | + Math.min(last, Math.min(model.place.listStart, last) + delta), | |
| 27 | + ), | |
| 32 | 28 | } |
| 33 | 29 | } |
| @@ -1,0 +1,32 @@ | ||
| 1 | +import type PaneState from '../../pane-state' | |
| 2 | +import Sections from '../sections' | |
| 3 | +import { dialogEntriesOf } from './dialog-entries-of' | |
| 4 | +import { dialogWindowOf } from './dialog-window-of' | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * What the focus ring landing on a dialog row makes of the list: that file | |
| 8 | + * selected, the window re-centred, the key of the row drawn where it sits. | |
| 9 | + * | |
| 10 | + * DiffFileList's selection. The engine keeps its ring by position, so the | |
| 11 | + * ring is sent to the row that holds the selected file's place once the | |
| 12 | + * window has moved; null for a key that is none of the listed rows'. | |
| 13 | + * | |
| 14 | + * @param model the pane's state | |
| 15 | + * @param element the key of the element taking the ring | |
| 16 | + * @returns the selected path and the key to land the ring on, or null | |
| 17 | + */ | |
| 18 | +export function dialogFocusOf( | |
| 19 | + model: PaneState.PaneModel, | |
| 20 | + element: string | undefined, | |
| 21 | +): PaneState.DialogFocus | null { | |
| 22 | + const entries = dialogEntriesOf(model) | |
| 23 | + const paths = entries.map(entry => entry.path) | |
| 24 | + const keys = entries.map(entry => Sections.fileKeyOf(entry)) | |
| 25 | + const picked = keys.indexOf(element ?? '') | |
| 26 | + const selectedPath = paths[picked] ?? null | |
| 27 | + const after = dialogWindowOf({ selectedPath }, paths) | |
| 28 | + const landing = keys[dialogWindowOf(model, paths) + picked - after] | |
| 29 | + const isRow = selectedPath !== null && landing !== undefined | |
| 30 | + | |
| 31 | + return isRow ? { selectedPath, landing } : null | |
| 32 | +} |
| @@ -18,9 +18,9 @@ | ||
| 18 | 18 | * The inline pane (DiffDialog): its title, the dim count, then the file |
| 19 | 19 | * list's window, or the picked file's detail alone, and the key hints. |
| 20 | 20 | * |
| 21 | - * Every file lists in the built-in's order, five at a time between `more | |
| 22 | - * files` rows that move the window; the source picker stands in for the | |
| 23 | - * tabs. Arrows walk the rows, Enter views one, Escape backs out or closes. | |
| 21 | + * Every file lists in the built-in's order, five at a time round the | |
| 22 | + * selected row (`❯ `, where the focus ring starts and what its walk moves, | |
| 23 | + * ui.focus); Enter views that file, Escape backs out or closes. | |
| 24 | 24 | * |
| 25 | 25 | * @param kit the elements, the handlers, the width |
| 26 | 26 | * @param model the pane's state |
| @@ -44,6 +44,7 @@ | ||
| 44 | 44 | const isTurn = model.source.kind === 'turn' |
| 45 | 45 | const isPaged = entries.length > Limits.MAX_VISIBLE_FILES |
| 46 | 46 | const picked = entries.find(entry => entry.path === model.selectedPath) |
| 47 | + const selected = picked ?? entries[0] | |
| 47 | 48 | const isDetail = model.dialogView === 'detail' && picked !== undefined |
| 48 | 49 | |
| 49 | 50 | const start = dialogWindowOf( |
| @@ -89,7 +90,7 @@ | ||
| 89 | 90 | added: entry.added, |
| 90 | 91 | removed: entry.removed, |
| 91 | 92 | note: noteOf(entry), |
| 92 | - isSelected: false, | |
| 93 | + isSelected: entry === selected, | |
| 93 | 94 | }, |
| 94 | 95 | () => kit.actions.selectFile(entry.path), |
| 95 | 96 | ) |
| @@ -97,9 +98,8 @@ | ||
| 97 | 98 | function moreOf(way: 'up' | 'down', count: number): RenderElement { |
| 98 | 99 | const arrow = way === 'up' ? '\u2191' : '\u2193' |
| 99 | 100 | const words = Layout.plural(count, 'file').replace(' ', ' more ') |
| 100 | - const edge = way === 'up' ? 'files-up' : 'files-down' | |
| 101 | 101 | |
| 102 | - return Sections.listEdgeButton(kit, edge, ` ${arrow} ${words}`) | |
| 102 | + return Sections.dimNote(kit, ` ${arrow} ${words}`) | |
| 103 | 103 | } |
| 104 | 104 | |
| 105 | 105 | const pagedEdgeOf = (way: 'up' | 'down', count: number) => |
| @@ -3,23 +3,21 @@ | ||
| 3 | 3 | |
| 4 | 4 | /** |
| 5 | 5 | * The first of the MAX_VISIBLE_FILES rows the dialog lists (DiffFileList's |
| 6 | - * window): centred on the file last viewed, else where its edge rows put it. | |
| 6 | + * window): centred on the selected file, the first when none is, clamped. | |
| 7 | 7 | * |
| 8 | - * @param model the pane's state: the pick and the list's start | |
| 8 | + * @param model the pane's state: the selection | |
| 9 | 9 | * @param paths the listed files' paths, in order |
| 10 | 10 | * @returns the first listed row's index |
| 11 | 11 | */ |
| 12 | -export function dialogWindowOf( | |
| 13 | - model: Pick<PaneState.PaneModel, 'selectedPath' | 'place'>, | |
| 12 | +export const dialogWindowOf = ( | |
| 13 | + model: Pick<PaneState.PaneModel, 'selectedPath'>, | |
| 14 | 14 | paths: readonly string[], |
| 15 | -): number { | |
| 16 | - const last = Math.max(0, paths.length - Limits.MAX_VISIBLE_FILES) | |
| 17 | - const picked = paths.indexOf(model.selectedPath ?? '') | |
| 18 | - const isPicked = model.selectedPath !== null && picked >= 0 | |
| 19 | - | |
| 20 | - const wanted = isPicked | |
| 21 | - ? picked - Math.floor(Limits.MAX_VISIBLE_FILES / 2) | |
| 22 | - : model.place.listStart | |
| 23 | - | |
| 24 | - return Math.max(0, Math.min(last, wanted)) | |
| 25 | -} | |
| 15 | +): number => | |
| 16 | + Math.max( | |
| 17 | + 0, | |
| 18 | + Math.min( | |
| 19 | + paths.length - Limits.MAX_VISIBLE_FILES, | |
| 20 | + Math.max(0, paths.indexOf(model.selectedPath ?? '')) - | |
| 21 | + Math.floor(Limits.MAX_VISIBLE_FILES / 2), | |
| 22 | + ), | |
| 23 | + ) |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | export * from './dialog-entries-of' |
| 2 | +export * from './dialog-focus-of.js' | |
| 2 | 3 | export * from './dialog-pane.jsx' |
| 3 | 4 | export * from './dialog-rows-of.js' |
| 4 | 5 | export * from './dialog-window-of' |
| @@ -10,8 +10,7 @@ | ||
| 10 | 10 | selectFile: (path: string) => void |
| 11 | 11 | |
| 12 | 12 | /** |
| 13 | - * Moves the list's window by so many files, clamped to the list; in the | |
| 14 | - * dialog the pick is dropped so the window leaves the file last viewed. | |
| 13 | + * Moves the docked list's window by so many files, clamped to the list. | |
| 15 | 14 | */ |
| 16 | 15 | scrollList: (delta: number) => void |
| 17 | 16 |
| @@ -14,7 +14,7 @@ | ||
| 14 | 14 | |
| 15 | 15 | /** |
| 16 | 16 | * One file of the inline list (DiffFileList FileItem): the pointer before |
| 17 | - * the selected one, the start-truncated name, its counts or note right. | |
| 17 | + * the selected one, where the focus ring starts, the name, counts or note. | |
| 18 | 18 | * |
| 19 | 19 | * @param kit the elements, the handlers, the width |
| 20 | 20 | * @param row the file |
| @@ -42,7 +42,13 @@ | ||
| 42 | 42 | |
| 43 | 43 | return listRow( |
| 44 | 44 | kit, |
| 45 | - { key: row.key, label: `${mark} ${name}`, onPress, isDim: false }, | |
| 45 | + { | |
| 46 | + key: row.key, | |
| 47 | + label: `${mark} ${name}`, | |
| 48 | + onPress, | |
| 49 | + isDim: false, | |
| 50 | + isAutoFocus: row.isSelected, | |
| 51 | + }, | |
| 46 | 52 | tail, |
| 47 | 53 | ) |
| 48 | 54 | } |
| @@ -36,7 +36,7 @@ | ||
| 36 | 36 | |
| 37 | 37 | return listRow( |
| 38 | 38 | kit, |
| 39 | - { key: row.key, label, onPress, isDim: true }, | |
| 39 | + { key: row.key, label, onPress, isDim: true, isAutoFocus: false }, | |
| 40 | 40 | diffStat(kit, row.added, row.removed), |
| 41 | 41 | ) |
| 42 | 42 | } |
| @@ -2,9 +2,10 @@ | ||
| 2 | 2 | |
| 3 | 3 | /** |
| 4 | 4 | * A plain Button's face, press and address: a list row's Button, dim at |
| 5 | - * rest where the built-in draws the row dim. | |
| 5 | + * rest where the built-in's is, the focus ring's start where selected. | |
| 6 | 6 | */ |
| 7 | 7 | export type KeyedPressable = Pressable & { |
| 8 | 8 | key: string |
| 9 | 9 | isDim: boolean |
| 10 | + isAutoFocus: boolean | |
| 10 | 11 | } |
| @@ -3,39 +3,32 @@ | ||
| 3 | 3 | /* @jsxFrag Fragment */ |
| 4 | 4 | import type { RenderElement } from 'claude-code' |
| 5 | 5 | |
| 6 | -import Limits from '../../../limits' | |
| 7 | 6 | import type { Kit } from '../../kit' |
| 8 | 7 | |
| 9 | 8 | /** |
| 10 | - * A list's `more` row as a dim plain Button that moves its window: the | |
| 11 | - * docked list's a file, under the built-in's keys too; the dialog's a page. | |
| 9 | + * The docked list's `more above` or `more below` row as a dim plain Button | |
| 10 | + * that moves its window a file, under the built-in's list keys too. | |
| 12 | 11 | * |
| 13 | - * The built-in dialog moves its window with the selection; a plugin cannot | |
| 14 | - * see the person walk the rows, so there the row itself carries the move. | |
| 15 | - * | |
| 16 | 12 | * @param kit the elements and the handlers |
| 17 | - * @param edge which list and which edge, the Button's key | |
| 13 | + * @param edge which edge, the Button's key | |
| 18 | 14 | * @param label the row's text |
| 19 | 15 | * @returns the Button |
| 20 | 16 | */ |
| 21 | 17 | export function listEdgeButton( |
| 22 | 18 | kit: Kit, |
| 23 | - edge: 'list-up' | 'list-down' | 'files-up' | 'files-down', | |
| 19 | + edge: 'list-up' | 'list-down', | |
| 24 | 20 | label: string, |
| 25 | 21 | ): RenderElement { |
| 26 | 22 | const { Button } = kit.ui |
| 27 | - const isDocked = edge === 'list-up' || edge === 'list-down' | |
| 28 | - const isUp = edge === 'list-up' || edge === 'files-up' | |
| 29 | - const size = isDocked ? 1 : Limits.MAX_VISIBLE_FILES | |
| 30 | - const action = isUp ? 'app:diffFileListUp' : 'app:diffFileListDown' | |
| 23 | + const isUp = edge === 'list-up' | |
| 31 | 24 | |
| 32 | 25 | return ( |
| 33 | 26 | <Button |
| 34 | 27 | key={edge} |
| 35 | 28 | plain |
| 36 | 29 | dimColor |
| 37 | - {...(isDocked ? { action } : {})} | |
| 38 | - onPress={() => kit.actions.scrollList(isUp ? -size : size)} | |
| 30 | + action={isUp ? 'app:diffFileListUp' : 'app:diffFileListDown'} | |
| 31 | + onPress={() => kit.actions.scrollList(isUp ? -1 : 1)} | |
| 39 | 32 | > |
| 40 | 33 | {label} |
| 41 | 34 | </Button> |
| @@ -7,12 +7,12 @@ | ||
| 7 | 7 | import type { KeyedPressable } from '../keyed-pressable' |
| 8 | 8 | |
| 9 | 9 | /** |
| 10 | - * One row of a file list: a plain Button under its key, then whatever the | |
| 11 | - * row shows at its right edge (its counts, a note). | |
| 10 | + * One list row: a plain Button as wide as its label, a spacer, then the | |
| 11 | + * tail at the right edge (the counts, or a note). | |
| 12 | 12 | * |
| 13 | 13 | * @param kit the elements |
| 14 | - * @param row the Button's key, label and press, and whether it rests dim | |
| 15 | - * @param tail the element at the right edge | |
| 14 | + * @param row the Button's key, label, press, rest style and ring start | |
| 15 | + * @param tail what sits at the row's right edge | |
| 16 | 16 | * @returns the row element |
| 17 | 17 | */ |
| 18 | 18 | export function listRow( |
| @@ -24,7 +24,13 @@ | ||
| 24 | 24 | |
| 25 | 25 | return ( |
| 26 | 26 | <Box flexDirection="row"> |
| 27 | - <Button key={row.key} plain dimColor={row.isDim} onPress={row.onPress}> | |
| 27 | + <Button | |
| 28 | + key={row.key} | |
| 29 | + plain | |
| 30 | + dimColor={row.isDim} | |
| 31 | + {...(row.isAutoFocus ? ({ autoFocus: true } as const) : {})} | |
| 32 | + onPress={row.onPress} | |
| 33 | + > | |
| 28 | 34 | {row.label} |
| 29 | 35 | </Button> |
| 30 | 36 | <Box flexGrow={1} /> |