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
| @@ -1,4 +1,10 @@ | ||
| 1 | -import type { On, ResultOf, SessionMessage, Timer } from 'claude-code' | |
| 1 | +import type { | |
| 2 | + On, | |
| 3 | + PaneOpenArgs, | |
| 4 | + ResultOf, | |
| 5 | + SessionMessage, | |
| 6 | + Timer, | |
| 7 | +} from 'claude-code' | |
| 2 | 8 | |
| 3 | 9 | import Ask from './ask' |
| 4 | 10 | import Backend from './backend' |
| @@ -35,6 +41,7 @@ | ||
| 35 | 41 | let probing: Promise<boolean> | null = null |
| 36 | 42 | let sessionStartMs = 0 |
| 37 | 43 | let isPaneOpen = false |
| 44 | + let dialogRows: number | null = null | |
| 38 | 45 | let hasAutoOpened = false |
| 39 | 46 | let columns: number | null = null |
| 40 | 47 | let shownSessionId: string | null = null |
| @@ -141,7 +148,31 @@ | ||
| 141 | 148 | return true |
| 142 | 149 | } |
| 143 | 150 | |
| 151 | + function dialogPane(): PaneOpenArgs { | |
| 152 | + return { | |
| 153 | + id: Names.PANE_ID, | |
| 154 | + title: Names.PANE_TITLE, | |
| 155 | + holdToasts: true, | |
| 156 | + closeOnEscape: true, | |
| 157 | + rows: Views.dialogRowsOf(model), | |
| 158 | + } | |
| 159 | + } | |
| 160 | + | |
| 161 | + function fitDialog(engine: Host) { | |
| 162 | + const rows = Views.dialogRowsOf(model) | |
| 163 | + | |
| 164 | + const isStale = | |
| 165 | + isPaneOpen && model.isFullscreen === false && rows !== dialogRows | |
| 166 | + | |
| 167 | + if (isStale) { | |
| 168 | + dialogRows = rows | |
| 169 | + void engine.openPane(dialogPane()).catch(() => undefined) | |
| 170 | + } | |
| 171 | + } | |
| 172 | + | |
| 144 | 173 | function redraw(engine: Host) { |
| 174 | + fitDialog(engine) | |
| 175 | + | |
| 145 | 176 | if (timers.has('redraw')) { |
| 146 | 177 | return |
| 147 | 178 | } |
| @@ -352,13 +383,14 @@ | ||
| 352 | 383 | place: { ...model.place, top: 0, listStart: 0 }, |
| 353 | 384 | } |
| 354 | 385 | |
| 355 | - await engine.openPane({ | |
| 356 | - id: Names.PANE_ID, | |
| 357 | - title: Names.PANE_TITLE, | |
| 358 | - holdToasts: true, | |
| 359 | - ...(isDialog ? ({ focus: true, closeOnEscape: true } as const) : {}), | |
| 360 | - }) | |
| 386 | + dialogRows = isDialog ? Views.dialogRowsOf(model) : null | |
| 361 | 387 | |
| 388 | + await engine.openPane( | |
| 389 | + isDialog | |
| 390 | + ? { ...dialogPane(), focus: true } | |
| 391 | + : { id: Names.PANE_ID, title: Names.PANE_TITLE, holdToasts: true }, | |
| 392 | + ) | |
| 393 | + | |
| 362 | 394 | isPaneOpen = true |
| 363 | 395 | |
| 364 | 396 | const sessionId = await engine.sessionId().catch(() => null) |
| @@ -666,14 +698,10 @@ | ||
| 666 | 698 | model = { ...model, dialogView: 'list' } |
| 667 | 699 | redraw(host) |
| 668 | 700 | |
| 701 | + dialogRows = Views.dialogRowsOf(model) | |
| 702 | + | |
| 669 | 703 | void host |
| 670 | - .openPane({ | |
| 671 | - id: Names.PANE_ID, | |
| 672 | - title: Names.PANE_TITLE, | |
| 673 | - focus: true, | |
| 674 | - closeOnEscape: true, | |
| 675 | - holdToasts: true, | |
| 676 | - }) | |
| 704 | + .openPane({ ...dialogPane(), focus: true }) | |
| 677 | 705 | .catch(() => undefined) |
| 678 | 706 | |
| 679 | 707 | return { deny: 'back to the file list' } |
| @@ -708,7 +736,15 @@ | ||
| 708 | 736 | return next(e) |
| 709 | 737 | } |
| 710 | 738 | |
| 711 | - model = { ...model, place: Views.bodyScrolledBy(model, e) } | |
| 739 | + const isOverList = Views.isWheelOverList(model, e) | |
| 740 | + | |
| 741 | + model = { | |
| 742 | + ...model, | |
| 743 | + place: isOverList | |
| 744 | + ? Views.listScrolledBy(model, e.by) | |
| 745 | + : Views.bodyScrolledBy(model, e), | |
| 746 | + } | |
| 747 | + | |
| 712 | 748 | host.invalidate() |
| 713 | 749 | |
| 714 | 750 | return {} |
| @@ -5,8 +5,8 @@ | ||
| 5 | 5 | import Plan from './plan' |
| 6 | 6 | |
| 7 | 7 | /** |
| 8 | - * The docked pane's place after one of the person's scrolls: a wheel tick | |
| 9 | - * or arrow three rows, a page key the window's rows, Home and End the ends. | |
| 8 | + * The docked pane's place after one of the person's scrolls of the body: | |
| 9 | + * a wheel tick or arrow three rows, a page key the window, Home/End an end. | |
| 10 | 10 | * |
| 11 | 11 | * Told apart by the rows the move asks for against the pane's body and |
| 12 | 12 | * tree, as the engine sizes each; clamped to the body's extent. |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | export * from './body-scrolled-by.js' |
| 2 | 2 | export * from './draw-window.jsx' |
| 3 | +export * from './is-wheel-over-list.js' | |
| 3 | 4 | export * from './list-block-of.jsx' |
| 4 | 5 | export * from './list-scrolled-by.js' |
| 5 | 6 | export * from './place-at-file.js' |
| @@ -1,0 +1,28 @@ | ||
| 1 | +import type { UiScrollInput } from 'claude-code' | |
| 2 | + | |
| 3 | +import Limits from '../../limits' | |
| 4 | +import type PaneState from '../../pane-state' | |
| 5 | +import Plan from './plan' | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * Whether a scroll is a wheel tick over the docked list block while the | |
| 9 | + * list overflows its window: it moves the list then, as ReplDiffSidebar's. | |
| 10 | + * | |
| 11 | + * @param model the pane's state | |
| 12 | + * @param scroll the move: the cell the wheel was over, absent for keys | |
| 13 | + * @returns whether the list takes it | |
| 14 | + */ | |
| 15 | +export function isWheelOverList( | |
| 16 | + model: PaneState.PaneModel, | |
| 17 | + scroll: Pick<UiScrollInput, 'pointer'>, | |
| 18 | +): boolean { | |
| 19 | + const plan = Plan.dockPlanOf(model) | |
| 20 | + const { listTop, listEnd } = Plan.bodyLayoutOf(model, plan) | |
| 21 | + const row = scroll.pointer?.row ?? -1 | |
| 22 | + | |
| 23 | + return ( | |
| 24 | + row >= listTop && | |
| 25 | + row < listEnd && | |
| 26 | + plan.rows.length > Limits.MAX_SUMMARY_ROWS | |
| 27 | + ) | |
| 28 | +} |
| @@ -5,7 +5,7 @@ | ||
| 5 | 5 | |
| 6 | 6 | /** |
| 7 | 7 | * The docked pane's scrolling body laid out under its pinned rows: the |
| 8 | - * segments, their rows, the window's rows, where each file's block starts. | |
| 8 | + * segments and their rows, the window's rows, the files' and list's rows. | |
| 9 | 9 | * |
| 10 | 10 | * The pinned rows are counted as sidebarPane draws them (header, base or |
| 11 | 11 | * turn line, todo bar, list block); read from the box last drawn (`place`), |
| @@ -54,11 +54,15 @@ | ||
| 54 | 54 | extent += Segments.segmentRowsOf(segment) |
| 55 | 55 | } |
| 56 | 56 | |
| 57 | + const listTop = Limits.PANE_TOP_PAD_ROWS + headRows + 1 | |
| 58 | + | |
| 57 | 59 | return { |
| 58 | 60 | segments, |
| 59 | 61 | extent, |
| 60 | 62 | visibleRows, |
| 61 | 63 | maxTop: Math.max(0, extent - visibleRows), |
| 62 | 64 | tops, |
| 65 | + listTop, | |
| 66 | + listEnd: hasList ? listTop + listRows - 2 : listTop, | |
| 63 | 67 | } |
| 64 | 68 | } |
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | import type { Segment } from './segment' |
| 2 | 2 | |
| 3 | 3 | /** |
| 4 | - * The docked pane's scrolling body laid out: its segments, their rows in | |
| 5 | - * all, the window's rows, its last top, the row each file's block starts. | |
| 4 | + * The docked pane's scrolling body laid out: its segments and their rows, | |
| 5 | + * the window, each file's first row, the list block's rows (end exclusive). | |
| 6 | 6 | */ |
| 7 | 7 | export type BodyLayout = { |
| 8 | 8 | segments: readonly Segment[] |
| @@ -10,4 +10,6 @@ | ||
| 10 | 10 | visibleRows: number |
| 11 | 11 | maxTop: number |
| 12 | 12 | tops: ReadonlyMap<string, number> |
| 13 | + listTop: number | |
| 14 | + listEnd: number | |
| 13 | 15 | } |
| @@ -1,0 +1,46 @@ | ||
| 1 | +import Limits from '../../limits' | |
| 2 | +import PaneState from '../../pane-state' | |
| 3 | +import { dialogEntriesOf } from './dialog-entries-of' | |
| 4 | +import { dialogWindowOf } from './dialog-window-of' | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * The rows the dialog's content takes as dialogPane draws it, for the | |
| 8 | + * `rows` an inline open asks for: title, blank, count, picker, body, hints. | |
| 9 | + * | |
| 10 | + * The body is the message line, the list's window with its `more files` | |
| 11 | + * rows, or the file's name, rule and hunk lines (their wraps not counted: | |
| 12 | + * the layout caps a long file anyway and its arrows scroll it). | |
| 13 | + * | |
| 14 | + * @param model the pane's state | |
| 15 | + * @returns the rows wanted | |
| 16 | + */ | |
| 17 | +export function dialogRowsOf(model: PaneState.PaneModel): number { | |
| 18 | + const entries = dialogEntriesOf(model) | |
| 19 | + | |
| 20 | + const hasCount = | |
| 21 | + PaneState.pickedTurnOf(model) !== undefined || model.data !== null | |
| 22 | + | |
| 23 | + const isPaged = entries.length > Limits.MAX_VISIBLE_FILES | |
| 24 | + const picked = entries.find(entry => entry.path === model.selectedPath) | |
| 25 | + const isDetail = model.dialogView === 'detail' && picked !== undefined | |
| 26 | + | |
| 27 | + const start = dialogWindowOf( | |
| 28 | + model, | |
| 29 | + entries.map(entry => entry.path), | |
| 30 | + ) | |
| 31 | + | |
| 32 | + const lines = (picked?.body?.hunks ?? []).reduce( | |
| 33 | + (sum, hunk) => sum + 1 + hunk.lines.length, | |
| 34 | + 0, | |
| 35 | + ) | |
| 36 | + | |
| 37 | + const listed = Math.min(entries.length - start, Limits.MAX_VISIBLE_FILES) | |
| 38 | + | |
| 39 | + const isEmpty = entries.length === 0 | |
| 40 | + const listRows = isEmpty ? 1 : listed + (isPaged ? 2 : 0) | |
| 41 | + const bodyRows = isDetail ? 2 + Math.max(1, lines) : listRows | |
| 42 | + | |
| 43 | + return ( | |
| 44 | + 2 + Number(hasCount) + Number(model.turns.length > 0) + 2 + bodyRows + 1 | |
| 45 | + ) | |
| 46 | +} |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | export * from './dialog-entries-of' |
| 2 | 2 | export * from './dialog-pane.jsx' |
| 3 | +export * from './dialog-rows-of.js' | |
| 3 | 4 | export * from './dialog-window-of' |
| 4 | 5 | export * from './list-body-of' |
| 5 | 6 | export * from './message-pane-of' |