/** * errand-wizard.tsx — Multi-step Errand Wizard Ink component. * * Replaces the duplicated `showErrandWizard()` / `showErrandModal()` blessed * modals from tui-quest-picker.ts and tui-browser.ts with a single reusable * React component. * * 4-step wizard: * 1. Description (required, single-line) * 2. Scope (optional, multi-line) * 3. Objectives (optional, multi-line) * 4. Review & confirm * * Props-driven: parent manages visibility, component calls onSubmit/onCancel. */ import React, { useState, useCallback } from "react"; import { Box, Text, useInput } from "ink"; import { Modal } from "./modal"; import { TextInput } from "./text-input"; import type { ErrandSpec } from "../lib/errand-planner"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type ErrandWizardStep = "description" | "scope" | "objectives" | "review"; const STEPS: ErrandWizardStep[] = ["description", "scope", "objectives", "review"]; export interface ErrandWizardProps { /** Called with the completed ErrandSpec when user confirms on the review step. */ onSubmit: (spec: ErrandSpec) => void; /** Called when user cancels (Esc on the first step). */ onCancel: () => void; /** Whether the component is focused. Default: true. */ focus?: boolean; } // --------------------------------------------------------------------------- // Step Labels // --------------------------------------------------------------------------- const STEP_TITLES: Record = { description: "Description", scope: "Scope", objectives: "Objectives", review: "Review", }; const STEP_INSTRUCTIONS: Record = { description: "What needs to be done? (required)", scope: "What areas/files should this focus on? (optional — Enter to skip)", objectives: "Key objectives or acceptance criteria (optional — Enter to skip)", review: "Review your errand details below.", }; // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function ErrandWizard({ onSubmit, onCancel, focus = true, }: ErrandWizardProps): React.ReactElement { const [stepIndex, setStepIndex] = useState(0); const [description, setDescription] = useState(""); const [scope, setScope] = useState(""); const [objectives, setObjectives] = useState(""); const [error, setError] = useState(""); const step = STEPS[stepIndex]; const stepLabel = `Step ${stepIndex + 1}/${STEPS.length}`; const goBack = useCallback(() => { setError(""); if (stepIndex <= 0) { onCancel(); return; } setStepIndex((prev) => prev - 1); }, [stepIndex, onCancel]); const advance = useCallback(() => { setError(""); setStepIndex((prev) => Math.min(prev + 1, STEPS.length - 1)); }, []); const handleDescriptionSubmit = useCallback( (value: string) => { const trimmed = value.trim(); if (!trimmed) { setError("Description cannot be empty"); return; } setDescription(trimmed); advance(); }, [advance], ); const handleScopeSubmit = useCallback( (value: string) => { setScope(value.trim()); advance(); }, [advance], ); const handleObjectivesSubmit = useCallback( (value: string) => { setObjectives(value.trim()); advance(); }, [advance], ); // Escape key handler for text input steps (TextInput ignores Escape) useInput( (_input, key) => { if (key.escape && step !== "review") { goBack(); } }, { isActive: focus && step !== "review" }, ); // Review step keybindings useInput( (_input, key) => { if (key.return) { const spec: ErrandSpec = { description }; if (scope) spec.scope = scope; if (objectives) spec.objectives = objectives; onSubmit(spec); return; } if (key.escape) { goBack(); return; } }, { isActive: focus && step === "review" }, ); // Footer keybind hints const footerHint = step === "description" ? "Ctrl+S: next | Esc: cancel" : step === "review" ? "Enter: launch errand planner | Esc: go back" : "Ctrl+S: next | Esc: go back"; return ( {footerHint}}> {/* Step header */} {stepLabel} — {STEP_TITLES[step]} {STEP_INSTRUCTIONS[step]} {/* Error message */} {error && ( {error} )} {/* Step content */} {step === "description" && ( )} {step === "scope" && ( )} {step === "objectives" && ( )} {step === "review" && ( Description: {description} Scope: {scope || "(none)"} Objectives: {objectives || "(none)"} )} ); }