From 001252c584d21cc189a0af57af3507c0f8b252d0 Mon Sep 17 00:00:00 2001 From: "gfargo-horizon-agent[bot]" <294710345+gfargo-horizon-agent[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 17:55:29 +0000 Subject: [PATCH] refactor(workstation): extract rebase-plan input handlers into surfaces/rebase/input.ts Consolidates the four `activeView === 'rebase' && state.rebasePlan` branches scattered across getLogInkInputEvents (Esc discard-guard, J/K/p/s/f/d/e/r/Enter keymap, up/k cursor, down/j cursor) into a single handleRebaseInput() call site, mirroring the surfaces/bisect pattern already shipped for the bisect surface. --- src/workstation/runtime/inkInput.ts | 64 ++-------------- src/workstation/surfaces/rebase/input.ts | 95 ++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 58 deletions(-) create mode 100644 src/workstation/surfaces/rebase/input.ts diff --git a/src/workstation/runtime/inkInput.ts b/src/workstation/runtime/inkInput.ts index f4a78776..64ce105f 100644 --- a/src/workstation/runtime/inkInput.ts +++ b/src/workstation/runtime/inkInput.ts @@ -27,6 +27,7 @@ import { } from './inkWorkflows' import { sidebarTabHasSelectableItems } from '../chrome/sidebarSelection' import { handleBisectInput } from '../surfaces/bisect/input' +import { handleRebaseInput } from '../surfaces/rebase/input' export type LogInkInputKey = { backspace?: boolean @@ -2079,14 +2080,11 @@ export function getLogInkInputEvents( ] } - // #1446 — rebase-plan discard guard. A fully retagged/reordered - // rebase plan is expensive to recreate; Esc-ing away should confirm - // before silently dropping it, matching the compose-draft pattern. - // The confirm is only raised when Esc WOULD pop away from the rebase - // view (viewStack > 1) — otherwise there's nowhere to go and Esc - // is a no-op anyway. - if (key.escape && state.activeView === 'rebase' && state.rebasePlan && state.viewStack.length > 1) { - return [action({ type: 'setPendingConfirmation', value: 'discard-rebase-plan' })] + // #1359 / #1446 — in-TUI interactive rebase surface, extracted to + // `surfaces/rebase/input.ts` (#1625 second surface). + const rebaseEvents = handleRebaseInput(state, inputValue, key, context) + if (rebaseEvents) { + return rebaseEvents } if (key.escape && state.viewStack.length > 1) { @@ -2459,48 +2457,6 @@ export function getLogInkInputEvents( return [action({ type: 'setPendingKey', value: 'g' })] } - // ── In-TUI interactive rebase surface (#1359) ─────────────────────── - // The plan claims its keys while the view is active: j/k cursor, J/K - // reorder, p/s/f/d/e retag, r reword (prompt), Enter executes (behind - // a y-confirm), Esc pops (which clears the plan). Placed before every - // other single-letter handler so the rebase letters can't leak into - // sort/fixup/diff-toggle semantics. - if (state.activeView === 'rebase' && state.rebasePlan) { - if (inputValue === 'J') { - return [action({ type: 'moveRebaseRow', delta: 1 })] - } - if (inputValue === 'K') { - return [action({ type: 'moveRebaseRow', delta: -1 })] - } - if (inputValue === 'p') { - return [action({ type: 'setRebaseAction', action: 'pick' })] - } - if (inputValue === 's') { - return [action({ type: 'setRebaseAction', action: 'squash' })] - } - if (inputValue === 'f') { - return [action({ type: 'setRebaseAction', action: 'fixup' })] - } - if (inputValue === 'd') { - return [action({ type: 'setRebaseAction', action: 'drop' })] - } - if (inputValue === 'e') { - return [action({ type: 'setRebaseAction', action: 'edit' })] - } - if (inputValue === 'r') { - const row = state.rebasePlan.rows[state.rebasePlan.selectedIndex] - return [action({ - type: 'openInputPrompt', - kind: 'rebase-reword', - label: `New message for ${row?.shortSha ?? 'commit'}`, - initial: row?.newMessage ?? row?.subject ?? '', - })] - } - if (key.return) { - return [action({ type: 'setPendingConfirmation', value: 'execute-rebase-plan' })] - } - } - // `d` on the diff view toggles between unified and side-by-side split // rendering (#785). Scoped to the diff view so the letter stays free // for other surfaces. The chord branch above already claimed `gd`, @@ -2874,10 +2830,6 @@ export function getLogInkInputEvents( })] } - if (state.activeView === 'rebase' && state.rebasePlan) { - return [action({ type: 'moveRebaseCursor', delta: -1 })] - } - // Worktree (staging) diff: ↑/↓ scroll lines — consistent with the // commit / stash diffs (#1185). `[`/`]` jump between hunks (the // staging unit), and the current hunk is derived from the scroll @@ -3070,10 +3022,6 @@ export function getLogInkInputEvents( })] } - if (state.activeView === 'rebase' && state.rebasePlan) { - return [action({ type: 'moveRebaseCursor', delta: 1 })] - } - // Worktree (staging) diff: ↓ scrolls lines (see the ↑ handler) — // `[`/`]` jump hunks (#1185). if (isWorktreeDiffTarget(state) && context.worktreeDiffLineCount) { diff --git a/src/workstation/surfaces/rebase/input.ts b/src/workstation/surfaces/rebase/input.ts new file mode 100644 index 00000000..39364202 --- /dev/null +++ b/src/workstation/surfaces/rebase/input.ts @@ -0,0 +1,95 @@ +import type { LogInkAction, LogInkState } from '../../runtime/inkViewModel' +import type { + LogInkInputContext, + LogInkInputEvent, + LogInkInputKey, +} from '../../runtime/inkInput' + +/** + * In-TUI interactive rebase surface (#1359), extracted out of + * `inkInput.ts`'s monolithic router (#1625 second surface, following + * `surfaces/bisect/input.ts`). Scoped entirely to `state.activeView === + * 'rebase' && state.rebasePlan`, with no dependency on any other view's + * state, which is what makes it safe to lift verbatim. + * + * Returns `null` when no rebase-local binding matches, so the caller + * (`getLogInkInputEvents`) falls through to the rest of the router. + */ +export function handleRebaseInput( + state: LogInkState, + inputValue: string, + key: LogInkInputKey, + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- signature kept symmetric with handleBisectInput; no rebase binding reads context + context: LogInkInputContext +): LogInkInputEvent[] | null { + if (state.activeView !== 'rebase' || !state.rebasePlan) { + return null + } + + // #1446 — rebase-plan discard guard. A fully retagged/reordered + // rebase plan is expensive to recreate; Esc-ing away should confirm + // before silently dropping it, matching the compose-draft pattern. + // The confirm is only raised when Esc WOULD pop away from the rebase + // view (viewStack > 1) — otherwise there's nowhere to go and Esc + // is a no-op anyway. + if (key.escape && state.viewStack.length > 1) { + return [action({ type: 'setPendingConfirmation', value: 'discard-rebase-plan' })] + } + + // ── In-TUI interactive rebase surface (#1359) ─────────────────────── + // The plan claims its keys while the view is active: j/k cursor, J/K + // reorder, p/s/f/d/e retag, r reword (prompt), Enter executes (behind + // a y-confirm), Esc pops (which clears the plan). Placed before every + // other single-letter handler so the rebase letters can't leak into + // sort/fixup/diff-toggle semantics. + if (inputValue === 'J') { + return [action({ type: 'moveRebaseRow', delta: 1 })] + } + if (inputValue === 'K') { + return [action({ type: 'moveRebaseRow', delta: -1 })] + } + if (inputValue === 'p') { + return [action({ type: 'setRebaseAction', action: 'pick' })] + } + if (inputValue === 's') { + return [action({ type: 'setRebaseAction', action: 'squash' })] + } + if (inputValue === 'f') { + return [action({ type: 'setRebaseAction', action: 'fixup' })] + } + if (inputValue === 'd') { + return [action({ type: 'setRebaseAction', action: 'drop' })] + } + if (inputValue === 'e') { + return [action({ type: 'setRebaseAction', action: 'edit' })] + } + if (inputValue === 'r') { + const row = state.rebasePlan.rows[state.rebasePlan.selectedIndex] + return [action({ + type: 'openInputPrompt', + kind: 'rebase-reword', + label: `New message for ${row?.shortSha ?? 'commit'}`, + initial: row?.newMessage ?? row?.subject ?? '', + })] + } + if (key.return) { + return [action({ type: 'setPendingConfirmation', value: 'execute-rebase-plan' })] + } + + if (key.upArrow || inputValue === 'k') { + return [action({ type: 'moveRebaseCursor', delta: -1 })] + } + + if (key.downArrow || inputValue === 'j') { + return [action({ type: 'moveRebaseCursor', delta: 1 })] + } + + return null +} + +function action(actionValue: LogInkAction): LogInkInputEvent { + return { + type: 'action', + action: actionValue, + } +}