import type { InputRenderable, TextareaRenderable } from "@opentui/core"; import { useKeyboard, useTerminalDimensions } from "@opentui/react"; import { useCallback, useMemo, useRef, useState } from "react"; import { formatPriorityList } from "@/lib/priority-list"; import { PriorityText } from "@/lib/tui/sessions/priority-text"; import { TUI_COLORS } from "@/lib/tui/shared/colors"; import type { DefaultReview, Priority } from "@/lib/types"; import { VALID_PRIORITIES as PRIORITIES } from "@/lib/types/domain"; export type ReviewModeSelection = "uncommitted" | "base" | "commit"; type ReviewModeInputMode = Exclude; type ReviewModeStep = "picker" | "branch-picker" | "commit-picker" | "options"; type ReviewExecutionMode = "review-only" | "auto-all" | "auto-priority"; type OptionsFocusTarget = | "iterations" | "force-max-iterations" | "execution-review-only" | "execution-auto-all" | "execution-auto-priority" | "custom-instructions"; const OPTIONS_FOCUS_ORDER: OptionsFocusTarget[] = [ "iterations", "force-max-iterations", "execution-review-only", "execution-auto-all", "execution-auto-priority", ]; function executionFocusToMode(focus: OptionsFocusTarget): ReviewExecutionMode | null { if (focus === "execution-review-only") return "review-only"; if (focus === "execution-auto-all") return "auto-all"; if (focus === "execution-auto-priority") return "auto-priority"; return null; } function executionModeToFocus(mode: ReviewExecutionMode): OptionsFocusTarget { if (mode === "review-only") return "execution-review-only"; if (mode === "auto-all") return "execution-auto-all"; return "execution-auto-priority"; } interface ReviewModeOption { label: string; description: string; mode: ReviewModeSelection; } interface ReviewExecutionOption { label: string; description: string; mode: ReviewExecutionMode; } const DEFAULT_MAX_ITERATIONS = 5; const MIN_MAX_ITERATIONS = 1; const MAX_MAX_ITERATIONS = 999; const CUSTOM_INSTRUCTIONS_PLACEHOLDER = "Focus on security boundaries, migrations, and error handling..."; interface ReviewModeOverlayProps { defaultReview?: DefaultReview; defaultMaxIterations?: number; projectPath: string; onClose: () => void; onSubmit: (args: string[]) => void; } const REVIEW_MODE_OPTIONS: ReviewModeOption[] = [ { label: "Review uncommitted changes", description: "Review the current working tree changes.", mode: "uncommitted", }, { label: "Review against base branch", description: "Compare the current branch against a base branch or ref.", mode: "base", }, { label: "Review a commit", description: "Review a specific commit SHA or ref.", mode: "commit", }, ]; const REVIEW_EXECUTION_OPTIONS: ReviewExecutionOption[] = [ { label: "Review only", description: "Persist findings for later selection and remediation.", mode: "review-only", }, { label: "Auto-fix all", description: "Run remediation immediately for every persisted finding.", mode: "auto-all", }, { label: "Auto-fix priorities", description: "Run remediation immediately for a CSV priority filter.", mode: "auto-priority", }, ]; const LIST_PICKER_PADDING = 1; const LIST_PICKER_VERTICAL_OVERHEAD = LIST_PICKER_PADDING * 2 + 6; const MAX_LIST_PICKER_SELECT_HEIGHT = 10; const OPTIONS_WIDE_MIN_WIDTH = 96; const OPTIONS_WIDE_MIN_HEIGHT = 28; const OPTIONS_WIDE_OVERLAY_WIDTH = 118; const OPTIONS_COMPACT_OVERLAY_WIDTH = 90; interface ReviewModeMeta { title: string; emptyError: string; singleLineError?: string; runFlag: "--base" | "--commit"; } const REVIEW_MODE_META: Record = { base: { title: "Against Base Branch", emptyError: "Base branch is required.", singleLineError: "Base branch must be a single line.", runFlag: "--base", }, commit: { title: "Target Commit", emptyError: "Target commit is required.", singleLineError: "Target commit must be a single line.", runFlag: "--commit", }, }; function getInitialReviewMode(defaultReview?: DefaultReview): ReviewModeSelection { if (defaultReview?.type === "base") { return "base"; } return "uncommitted"; } function sortSelectedPriorities(selectedPriorities: Priority[]): Priority[] { return PRIORITIES.filter((priority) => selectedPriorities.includes(priority)); } function clampPriorityCursorIndex(index: number): number { return Math.min(PRIORITIES.length - 1, Math.max(0, index)); } function isUpNavigationKey(keyName: string): boolean { return keyName === "up" || keyName === "k"; } function isDownNavigationKey(keyName: string): boolean { return keyName === "down" || keyName === "j"; } function isLeftNavigationKey(keyName: string): boolean { return keyName === "left" || keyName === "h"; } function isRightNavigationKey(keyName: string): boolean { return keyName === "right" || keyName === "l"; } function getReviewTargetSummary(pendingArgs: string[] | null): string { if (!pendingArgs || pendingArgs.length === 0) { return "Unknown"; } const [flag, value] = pendingArgs; if (flag === "--uncommitted") { return "Uncommitted"; } if (flag === "--base") { return `Base: ${value ?? "?"}`; } if (flag === "--commit") { return `Commit: ${value ?? "?"}`; } return "Unknown"; } function getExecutionSummary( executionMode: ReviewExecutionMode, selectedPriorityList: string | null ): string { if (executionMode === "review-only") { return "Review only"; } if (executionMode === "auto-all") { return "Auto-fix all"; } return selectedPriorityList ? `Auto-fix priorities · ${selectedPriorityList}` : "Auto-fix priorities"; } const PREVIEW_CUSTOM_INSTRUCTIONS_TOKEN = ""; function buildReviewCommandPreview(options: { pendingArgs: string[] | null; maxIterationsDraft: string; forceMaxIterations: boolean; executionMode: ReviewExecutionMode; selectedPriorityList: string | null; customInstructionsDraft: string; }): string { const parts = ["rr", "run"]; if (options.pendingArgs) { parts.push(...options.pendingArgs); } if (options.customInstructionsDraft.trim().length > 0) { parts.push(PREVIEW_CUSTOM_INSTRUCTIONS_TOKEN); } const maxIterations = options.maxIterationsDraft.trim().length > 0 ? options.maxIterationsDraft.trim() : ""; parts.push("--max", maxIterations); if (options.forceMaxIterations) { parts.push("--force"); } if (options.executionMode !== "review-only") { parts.push("--auto"); } if (options.executionMode === "auto-priority") { parts.push("--priority", options.selectedPriorityList ?? ""); } return parts.join(" "); } function renderPrioritySelectionRow(priority: Priority, isSelected: boolean) { return ( <> {isSelected ? "◈" : "◇"}{" "} ); } export function buildReviewRunArgs(mode: ReviewModeSelection, value?: string): string[] { if (mode === "uncommitted") { return ["--uncommitted"]; } const metadata = REVIEW_MODE_META[mode]; const rawValue = value ?? ""; const trimmedValue = rawValue.trim(); if (trimmedValue.length === 0) { throw new Error(metadata.emptyError); } if (metadata.singleLineError && /[\r\n]/.test(trimmedValue)) { throw new Error(metadata.singleLineError); } return [metadata.runFlag, trimmedValue]; } interface GitBranchData { currentBranch: string | null; branches: string[]; } interface GitCommit { shortSha: string; subject: string; } function getGitBranches(projectPath: string): GitBranchData { try { const currentResult = Bun.spawnSync(["git", "branch", "--show-current"], { cwd: projectPath, stdout: "pipe", stderr: "pipe", }); const currentBranch = currentResult.exitCode === 0 ? currentResult.stdout.toString().trim() : ""; const branchesResult = Bun.spawnSync(["git", "branch", "--format=%(refname:short)"], { cwd: projectPath, stdout: "pipe", stderr: "pipe", }); if (branchesResult.exitCode !== 0) { return { currentBranch: currentBranch || null, branches: [], }; } return { currentBranch: currentBranch || null, branches: branchesResult.stdout .toString() .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0 && line !== currentBranch) .sort((a, b) => a.localeCompare(b)), }; } catch { return { currentBranch: null, branches: [], }; } } function getGitCommits(projectPath: string): GitCommit[] { try { const commitsResult = Bun.spawnSync( ["git", "log", "--no-color", "--pretty=format:%h%x09%s", "HEAD"], { cwd: projectPath, stdout: "pipe", stderr: "pipe", } ); if (commitsResult.exitCode !== 0) { return []; } return commitsResult.stdout .toString() .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0) .flatMap((line) => { const separatorIndex = line.indexOf("\t"); if (separatorIndex <= 0) { return []; } const shortSha = line.slice(0, separatorIndex).trim(); const subject = line.slice(separatorIndex + 1).trim(); if (shortSha.length === 0) { return []; } return [ { shortSha, subject, }, ]; }); } catch { return []; } } function clampMaxIterations(value: number): number { if (!Number.isFinite(value)) { return DEFAULT_MAX_ITERATIONS; } return Math.min(MAX_MAX_ITERATIONS, Math.max(MIN_MAX_ITERATIONS, Math.trunc(value))); } export function ReviewModeOverlay({ defaultReview, defaultMaxIterations, projectPath, onClose, onSubmit, }: ReviewModeOverlayProps) { const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions(); const initialMaxIterations = clampMaxIterations(defaultMaxIterations ?? DEFAULT_MAX_ITERATIONS); const [selectedMode, setSelectedMode] = useState( getInitialReviewMode(defaultReview) ); const [step, setStep] = useState("picker"); const [error, setError] = useState(null); const [pendingArgs, setPendingArgs] = useState(null); const [previousStep, setPreviousStep] = useState("picker"); const [maxIterationsDraft, setMaxIterationsDraft] = useState( String(initialMaxIterations) ); const [forceMaxIterations, setForceMaxIterations] = useState(false); const [executionMode, setExecutionMode] = useState("review-only"); const [selectedPriorities, setSelectedPriorities] = useState([]); const [priorityCursorIndex, setPriorityCursorIndex] = useState(0); const [customInstructionsDraft, setCustomInstructionsDraft] = useState(""); const [showCustomInstructions, setShowCustomInstructions] = useState(false); const [optionsFocus, setOptionsFocus] = useState("iterations"); const lastNonCustomFocusRef = useRef("iterations"); const customInstructionsRef = useRef(null); const maxIterationsInputRef = useRef(null); const attachMaxIterationsInput = useCallback((input: InputRenderable | null) => { maxIterationsInputRef.current = input; if (!input) { return; } const handleFocus = () => { input.selectAll(); }; input.on("focused", handleFocus); return () => { input.off("focused", handleFocus); }; }, []); const branchPickerData = useMemo(() => { const branchData = getGitBranches(projectPath); const description = branchData.currentBranch ? `Current: ${branchData.currentBranch}` : "Current repo branch"; return { ...branchData, options: branchData.branches.map((name) => ({ name, description, value: name, })), }; }, [projectPath]); const branchOptions = branchPickerData.options; const commitOptions = useMemo( () => getGitCommits(projectPath).map((commit) => ({ name: commit.subject || commit.shortSha, description: commit.shortSha, value: commit.shortSha, })), [projectPath] ); const pickerSelectHeight = Math.max( 1, Math.min(MAX_LIST_PICKER_SELECT_HEIGHT, terminalHeight - LIST_PICKER_VERTICAL_OVERHEAD) ); const pickerOverlayHeight = pickerSelectHeight + LIST_PICKER_VERTICAL_OVERHEAD; const orderedSelectedPriorities = useMemo( () => sortSelectedPriorities(selectedPriorities), [selectedPriorities] ); const selectedPriorityList = orderedSelectedPriorities.length > 0 ? formatPriorityList(orderedSelectedPriorities) : null; const commandPreview = buildReviewCommandPreview({ pendingArgs, maxIterationsDraft, forceMaxIterations, executionMode, selectedPriorityList, customInstructionsDraft, }); const isWideOptionsLayout = step === "options" && terminalWidth >= OPTIONS_WIDE_MIN_WIDTH && terminalHeight >= OPTIONS_WIDE_MIN_HEIGHT; const targetSummary = getReviewTargetSummary(pendingArgs); const executionSummary = getExecutionSummary(executionMode, selectedPriorityList); const customInstructionsStatus = customInstructionsDraft.trim().length > 0 ? "Set" : "Not set"; const isCustomInstructionsFocused = showCustomInstructions && optionsFocus === "custom-instructions"; const optionsStatusColor = error ? TUI_COLORS.status.error : TUI_COLORS.text.muted; const optionsOverlayWidth = isWideOptionsLayout ? Math.min(OPTIONS_WIDE_OVERLAY_WIDTH, Math.max(96, terminalWidth - 4)) : Math.min(OPTIONS_COMPACT_OVERLAY_WIDTH, Math.max(78, terminalWidth - 4)); const configurationPaneWidth = isWideOptionsLayout ? Math.min(50, Math.max(46, Math.floor((optionsOverlayWidth - 8) * 0.45))) : undefined; const configurationContentWidth = Math.max( 28, (configurationPaneWidth ?? optionsOverlayWidth) - 8 ); const previewPaneWidth = isWideOptionsLayout ? Math.max(48, optionsOverlayWidth - (configurationPaneWidth ?? 0) - 8) : undefined; const textareaWidth = Math.max( 30, Math.min(68, configurationContentWidth - (showCustomInstructions ? 2 : 0)) ); function movePriorityCursor(direction: 1 | -1) { setPriorityCursorIndex((current) => clampPriorityCursorIndex(current + direction)); setError(null); } function advancePriorityCursor() { setPriorityCursorIndex((current) => (current + 1) % PRIORITIES.length); setError(null); } function toggleSelectedPriority() { const priority = PRIORITIES[priorityCursorIndex] ?? PRIORITIES[0]; if (!priority) { return; } setSelectedPriorities((current) => sortSelectedPriorities( current.includes(priority) ? current.filter((value) => value !== priority) : [...current, priority] ) ); setError(null); } function toggleForceMaxIterations() { setForceMaxIterations((current) => !current); setError(null); } useKeyboard((key) => { if (step === "options") { if (isCustomInstructionsFocused) { if (key.name === "escape") { hideCustomInstructions(); } return; } if (key.name === "c") { openCustomInstructions(); return; } if (key.name === "escape") { setError(null); setPendingArgs(null); setStep(previousStep); return; } if (isUpNavigationKey(key.name) || isDownNavigationKey(key.name)) { const direction = isUpNavigationKey(key.name) ? -1 : 1; const currentIndex = OPTIONS_FOCUS_ORDER.indexOf(optionsFocus); const nextIndex = Math.min( OPTIONS_FOCUS_ORDER.length - 1, Math.max(0, currentIndex + direction) ); const nextFocus = OPTIONS_FOCUS_ORDER[nextIndex]; if (nextFocus && nextFocus !== optionsFocus) { setOptionsFocus(nextFocus); const mode = executionFocusToMode(nextFocus); if (mode) { setExecutionMode(mode); } setError(null); } return; } if (optionsFocus === "force-max-iterations" && key.name === "space") { toggleForceMaxIterations(); return; } if (optionsFocus === "execution-auto-priority") { if (isLeftNavigationKey(key.name)) { movePriorityCursor(-1); return; } if (isRightNavigationKey(key.name)) { movePriorityCursor(1); return; } if (key.name === "space") { toggleSelectedPriority(); return; } if (key.name === "tab") { advancePriorityCursor(); return; } } if (key.name === "enter" || key.name === "return") { submitWithOptions(); } return; } if (step === "branch-picker" || step === "commit-picker") { if (key.name === "escape") { setError(null); setStep("picker"); return; } return; } if (step !== "picker") { return; } if (key.name === "escape" || key.name === "q") { onClose(); return; } if (key.name === "up" || key.name === "k") { const currentIndex = REVIEW_MODE_OPTIONS.findIndex((option) => option.mode === selectedMode); const nextMode = REVIEW_MODE_OPTIONS[Math.max(0, currentIndex - 1)]?.mode; if (nextMode) { setSelectedMode(nextMode); setError(null); } return; } if (key.name === "down" || key.name === "j") { const currentIndex = REVIEW_MODE_OPTIONS.findIndex((option) => option.mode === selectedMode); const nextMode = REVIEW_MODE_OPTIONS[Math.min(REVIEW_MODE_OPTIONS.length - 1, currentIndex + 1)]?.mode; if (nextMode) { setSelectedMode(nextMode); setError(null); } return; } if (key.name === "enter" || key.name === "return") { if (selectedMode === "uncommitted") { submitSelectedMode(selectedMode); return; } if (selectedMode === "base") { openBranchPicker(); return; } openCommitPicker(); } }); function syncCustomInstructionsDraft(): string { const nextValue = customInstructionsRef.current?.plainText ?? customInstructionsDraft; setCustomInstructionsDraft(nextValue); return nextValue; } function submitSelectedMode(mode: ReviewModeSelection, value?: string) { try { setError(null); const args = buildReviewRunArgs(mode, value); goToOptions(args); } catch (submitError) { setError(submitError instanceof Error ? submitError.message : String(submitError)); } } function goToOptions(args: string[]) { setPendingArgs(args); setPreviousStep(step); setMaxIterationsDraft(String(initialMaxIterations)); setForceMaxIterations(false); setExecutionMode("review-only"); setSelectedPriorities([]); setPriorityCursorIndex(0); setShowCustomInstructions(false); setOptionsFocus("iterations"); lastNonCustomFocusRef.current = "iterations"; setError(null); setStep("options"); } function submitWithOptions() { const raw = maxIterationsDraft; const parsed = parseInt(raw, 10); if (!Number.isInteger(parsed) || parsed < MIN_MAX_ITERATIONS) { setError(`Max iterations must be an integer greater than or equal to ${MIN_MAX_ITERATIONS}.`); return; } if (parsed > MAX_MAX_ITERATIONS) { setError(`Max iterations must be ${MAX_MAX_ITERATIONS} or fewer.`); return; } if (!pendingArgs) { setError("Review mode is missing."); return; } let priorityList: string | undefined; if (executionMode === "auto-priority") { const currentSelectedPriorityList = formatPriorityList( sortSelectedPriorities(selectedPriorities) ); if (currentSelectedPriorityList.length === 0) { setError("Select at least one priority for auto-fix priorities."); return; } priorityList = currentSelectedPriorityList; } const customInstructions = syncCustomInstructionsDraft(); const nextArgs = customInstructions.trim().length > 0 ? [...pendingArgs, customInstructions, "--max", String(parsed)] : [...pendingArgs, "--max", String(parsed)]; if (forceMaxIterations) { nextArgs.push("--force"); } if (executionMode !== "review-only") { nextArgs.push("--auto"); } if (priorityList) { nextArgs.push("--priority", priorityList); } setError(null); onSubmit(nextArgs); } function openCustomInstructions() { if (optionsFocus !== "custom-instructions") { lastNonCustomFocusRef.current = optionsFocus; } setShowCustomInstructions(true); setOptionsFocus("custom-instructions"); setError(null); } function hideCustomInstructions() { syncCustomInstructionsDraft(); setShowCustomInstructions(false); setOptionsFocus(lastNonCustomFocusRef.current); setError(null); } function openBranchPicker() { setError(null); setStep("branch-picker"); } function openCommitPicker() { setError(null); setStep("commit-picker"); } function handleMaxIterationsInput(next: string) { if (next === "" || /^\d+$/.test(next)) { setMaxIterationsDraft(next); setError(null); return; } const input = maxIterationsInputRef.current; if (input) { input.value = maxIterationsDraft; } } function renderPicker() { return ( Choose the review mode. {REVIEW_MODE_OPTIONS.map((option) => { const isSelected = option.mode === selectedMode; return ( {isSelected ? "▶" : " "} {" "} {option.label} {option.description} ); })} ); } function renderBranchPicker() { if (branchOptions.length === 0) { return ( No alternate branches available. {branchPickerData.currentBranch ? `Current repo branch: ${branchPickerData.currentBranch}` : "Current repo branch could not be determined."} {error && {error}} [Esc] Back ); } return ( Select a base branch to compare against. { if (!option) { return; } submitSelectedMode("commit", option.value as string); }} /> {error && {error}} [Enter] Select [Esc] Back ); } function renderExecutionModeOptions() { return ( {REVIEW_EXECUTION_OPTIONS.map((option) => { const isSelected = option.mode === executionMode; const isFocused = optionsFocus === executionModeToFocus(option.mode); return ( {isFocused ? "▶ " : " "} {isSelected ? "◉" : "◎"} {" "} {option.label} ); })} ); } function renderPreviewField( label: string, value: string, color: string = TUI_COLORS.text.secondary ) { return ( {label}: {" "} {value} ); } function renderConfigurationPane() { const isForceFocused = optionsFocus === "force-max-iterations"; return ( Iterations Force Max Iterations {isForceFocused ? "▶ " : " "} {forceMaxIterations ? "◉" : "◎"} {" "} {forceMaxIterations ? "Enabled" : "Disabled"} Execution {renderExecutionModeOptions()} {optionsFocus === "execution-auto-priority" && ( {PRIORITIES.map((priority, index) => { const isSelected = selectedPriorities.includes(priority); const isHighlighted = priorityCursorIndex === index; return ( {isHighlighted ? "▶ " : " "} {renderPrioritySelectionRow(priority, isSelected)} ); })} )} Custom instructions [C] {showCustomInstructions && (