/** * quest-picker.tsx — Ink QuestPickerView component. * * Replaces the neo-blessed QuestPicker class with a declarative React * component. The parent manages data loading, quest operations, and * selected index; this component handles rendering and keybind dispatch. * * Layout: * +-----------------------------------------------------------+ * | WOMBO-COMBO Quest Picker | 4 quests | * +---------------------------+-------------------------------+ * | > All Tasks (42) | Quest: auth-overhaul | * | auth-overhaul ACTV | Status: active | * | search-api DRFT | Priority: high | * | perf-optim PAUS | Tasks: 8 (3 done, 62%) | * | ui-redesign PLAN | Goal: Replace basic auth... | * +---------------------------+-------------------------------+ * | Enter:select C:create Q:quit | * +-----------------------------------------------------------+ * * Keybinds are dispatched via callback props — the parent decides * what each action does (load data, navigate, etc.). */ import React from "react"; import { Box, Text, useInput } from "ink"; import type { Quest } from "../lib/quest"; import type { UsageTotals } from "../lib/token-usage"; import { formatTokenCount } from "./usage-overlay"; import { QUEST_STATUS_COLORS, QUEST_STATUS_ABBREV, TASK_PRIORITY_COLORS, progressBar, } from "./tui-constants"; import { useTerminalSize } from "./use-terminal-size"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface QuestSummary { quest: Quest; totalTasks: number; doneTasks: number; completionPct: number; } export interface QuestPickerViewProps { /** Quest summaries to display. */ quests: QuestSummary[]; /** Total number of tasks across all quests. */ totalTaskCount: number; /** Currently selected index (0 = "All Tasks", 1+ = quest). */ selectedIndex: number; /** Called when Enter/Space is pressed on the selected item. */ onSelect: (questId: string | null) => void; /** Called when the user presses Q or Ctrl+C. */ onQuit: () => void; /** Called when up/down navigation changes the selection. */ onSelectionChange: (index: number) => void; /** Per-quest token usage data. */ questUsage?: Map; /** Overall usage totals (for All Tasks detail). */ overallUsage?: UsageTotals | null; /** Whether dev-mode key hints are shown. */ devMode?: boolean; /** Called when 'c' is pressed (create quest). */ onCreate?: () => void; /** Called when 'a' is pressed (activate/pause quest). */ onToggleActive?: () => void; /** Called when 'p' is pressed (plan quest). */ onPlan?: () => void; /** Called when 'g' is pressed (genesis). */ onGenesis?: () => void; /** Called when 'e' is pressed (errand). */ onErrand?: () => void; /** Called when 'w' is pressed (wishlist). */ onWishlist?: () => void; /** Called when 'o' is pressed (onboarding). */ onOnboarding?: () => void; /** Called when 'd' is pressed (delete quest). */ onDelete?: () => void; /** Called when 'f' is pressed (seed fake tasks, devMode only). */ onSeedFake?: () => void; } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- function Header({ questCount, activeCount, totalTaskCount, }: { questCount: number; activeCount: number; totalTaskCount: number; }): React.ReactElement { return ( wombo-combo Quest Picker | {questCount} quest{questCount !== 1 ? "s" : ""} {activeCount > 0 && ( <> | {activeCount} active )} | {totalTaskCount} total tasks Select a quest to filter tasks, or choose "All Tasks" for the full list ); } function QuestListItem({ summary, isSelected, }: { summary: QuestSummary; isSelected: boolean; }): React.ReactElement { const { quest, totalTasks, doneTasks, completionPct } = summary; const isEmpty = totalTasks === 0; // Status badge const sColor = isEmpty ? "gray" : (QUEST_STATUS_COLORS[quest.status] ?? "white"); const sAbbr = QUEST_STATUS_ABBREV[quest.status] ?? quest.status.slice(0, 4).toUpperCase(); // Priority dot const pColor = isEmpty ? "gray" : (TASK_PRIORITY_COLORS[quest.priority] ?? "white"); // Task info const taskInfo = isEmpty ? "(needs planning)" : `${doneTasks}/${totalTasks}`; // Build a compact single-line string to avoid wrapping issues const prefix = isSelected ? "▸ " : " "; const dot = "●"; return ( {prefix} {dot} {sAbbr} {quest.title} {taskInfo} ); } function AllTasksItem({ totalTaskCount, isSelected, }: { totalTaskCount: number; isSelected: boolean; }): React.ReactElement { return ( {isSelected ? "▸ " : " "} All Tasks ({totalTaskCount}) ); } function AllTasksDetail({ totalTaskCount, overallUsage, }: { totalTaskCount: number; overallUsage?: UsageTotals | null; }): React.ReactElement { return ( All Tasks Browse all tasks across all quests and unassigned tasks. Total: {totalTaskCount} tasks {overallUsage && ( Overall Token Usage: Input: {formatTokenCount(overallUsage.input_tokens)} Output: {formatTokenCount(overallUsage.output_tokens)} {overallUsage.cache_read > 0 && ( Cache read: {formatTokenCount(overallUsage.cache_read)} )} {overallUsage.reasoning_tokens > 0 && ( Reasoning: {formatTokenCount(overallUsage.reasoning_tokens)} )} Total: {formatTokenCount(overallUsage.total_tokens)} {overallUsage.total_cost > 0 && ( Cost: {formatTokenCount(overallUsage.total_cost)} )} Steps: {overallUsage.record_count} )} Press Enter to open the full task browser. ); } function QuestDetail({ summary, usage, }: { summary: QuestSummary; usage?: UsageTotals; }): React.ReactElement { const { quest, totalTasks, doneTasks, completionPct } = summary; const sColor = QUEST_STATUS_COLORS[quest.status] ?? "white"; const pColor = TASK_PRIORITY_COLORS[quest.priority] ?? "white"; return ( {/* Title */} {quest.title} {/* Status & Priority */} Status: {quest.status} Priority: {quest.priority} Difficulty: {quest.difficulty} HITL: {quest.hitlMode} {/* Progress */} Progress: {totalTasks > 0 ? ( <> Tasks: {doneTasks}/{totalTasks} done ({completionPct}%) [{progressBar(completionPct, 100, 20)}] ) : ( No tasks assigned )} {/* Goal */} {quest.goal && ( <> Goal: {quest.goal} )} {/* Branch */} Branch: {quest.branch} Base: {quest.baseBranch} {/* Constraints */} {quest.constraints.add.length > 0 && ( <> Added Constraints: {quest.constraints.add.map((c, i) => ( + {c} ))} )} {quest.constraints.ban.length > 0 && ( <> Banned: {quest.constraints.ban.map((b, i) => ( - {b} ))} )} {/* Dependencies */} {quest.depends_on.length > 0 && ( <> Depends on: {quest.depends_on.map((d, i) => ( → {d} ))} )} {/* Notes */} {quest.notes.length > 0 && ( <> Notes: {quest.notes.map((n, i) => ( {n} ))} )} {/* Timeline */} Timeline: Created: {quest.created_at.slice(0, 10)} {quest.started_at && Started: {quest.started_at.slice(0, 10)}} {quest.ended_at && Ended: {quest.ended_at.slice(0, 10)}} {/* Token Usage */} {usage && ( <> Token Usage: Input: {formatTokenCount(usage.input_tokens)} Output: {formatTokenCount(usage.output_tokens)} {usage.cache_read > 0 && ( Cache read: {formatTokenCount(usage.cache_read)} )} {usage.reasoning_tokens > 0 && ( Reasoning: {formatTokenCount(usage.reasoning_tokens)} )} Total: {formatTokenCount(usage.total_tokens)} {usage.total_cost > 0 && ( Cost: {formatTokenCount(usage.total_cost)} )} Steps: {usage.record_count} )} ); } function StatusBar({ devMode, }: { devMode?: boolean; }): React.ReactElement { return ( Keys: Enter select C create E errand P plan G genesis A activate/pause D delete W wishlist O onboarding {devMode && ( <> F seed-fake )} Q quit ); } // --------------------------------------------------------------------------- // Main Component // --------------------------------------------------------------------------- /** * QuestPickerView — a declarative quest picker component. * * Pure view: all data is passed in via props, all actions dispatched * via callbacks. The parent is responsible for loading quests, * computing summaries, and handling navigation. */ export function QuestPickerView(props: QuestPickerViewProps): React.ReactElement { const { quests, totalTaskCount, selectedIndex, onSelect, onQuit, onSelectionChange, questUsage, overallUsage, devMode, onCreate, onToggleActive, onPlan, onGenesis, onErrand, onWishlist, onOnboarding, onDelete, onSeedFake, } = props; // Total item count: "All Tasks" + quests const itemCount = 1 + quests.length; // Keyboard handling useInput((input, key) => { // Quit if (input === "q") { onQuit(); return; } // Navigate if ((key.downArrow || input === "j") && itemCount > 0) { const next = Math.min(selectedIndex + 1, itemCount - 1); onSelectionChange(next); return; } if ((key.upArrow || input === "k") && itemCount > 0) { const prev = Math.max(selectedIndex - 1, 0); onSelectionChange(prev); return; } // Select if (key.return) { if (selectedIndex === 0) { onSelect(null); // All Tasks } else { const quest = quests[selectedIndex - 1]; if (quest) onSelect(quest.quest.id); } return; } // Action keys if (input === "c") { onCreate?.(); return; } if (input === "a") { onToggleActive?.(); return; } if (input === "p") { onPlan?.(); return; } if (input === "g") { onGenesis?.(); return; } if (input === "e") { onErrand?.(); return; } if (input === "w") { onWishlist?.(); return; } if (input === "o") { onOnboarding?.(); return; } if (input === "d") { onDelete?.(); return; } if (input === "f" && devMode) { onSeedFake?.(); return; } }); // Compute derived display data const questCount = quests.length; const activeCount = quests.filter((s) => s.quest.status === "active").length; // Determine what to show in the detail pane const selectedQuest = selectedIndex > 0 && selectedIndex <= quests.length ? quests[selectedIndex - 1] : null; const selectedQuestUsage = selectedQuest && questUsage ? questUsage.get(selectedQuest.quest.id) : undefined; // Fill the entire terminal height for fullscreen rendering const { rows } = useTerminalSize(); return ( {/* Header */}
{/* Main body: list + detail */} {/* Quest list (left pane) */} {quests.map((summary, i) => ( ))} {quests.length === 0 && ( No quests found )} {/* Detail pane (right pane) */} {selectedIndex === 0 ? ( ) : selectedQuest ? ( ) : ( No item selected )} {/* Status bar */} ); }