diff --git a/cli-ts/src/components/FocusChain.tsx b/cli-ts/src/components/FocusChain.tsx new file mode 100644 index 0000000000..1f676142e6 --- /dev/null +++ b/cli-ts/src/components/FocusChain.tsx @@ -0,0 +1,185 @@ +/** + * Focus Chain / To-Do List component for CLI + * Displays a progress-tracked checklist of tasks + */ + +import { isCompletedFocusChainItem, isFocusChainItem, parseFocusChainItem } from "@shared/focus-chain-utils" +import { Box, Text } from "ink" +import React, { useMemo } from "react" + +interface TodoInfo { + currentTodo: { text: string; completed: boolean; index: number } | null + currentIndex: number + completedCount: number + totalCount: number + progressPercentage: number +} + +interface TodoItem { + text: string + checked: boolean +} + +interface FocusChainProps { + focusChainChecklist?: string | null + expanded?: boolean +} + +/** + * Parse the focus chain checklist text into TodoInfo + */ +function parseCurrentTodoInfo(text: string): TodoInfo | null { + if (!text) { + return null + } + + let completedCount = 0 + let totalCount = 0 + let firstIncompleteIndex = -1 + let firstIncompleteText: string | null = null + + const lines = text.split("\n") + for (const rawLine of lines) { + const line = rawLine.trim() + if (isFocusChainItem(line)) { + const isCompleted = isCompletedFocusChainItem(line) + + if (isCompleted) { + completedCount++ + } else if (firstIncompleteIndex === -1) { + firstIncompleteIndex = totalCount + // Extract text after "- [ ] " + firstIncompleteText = line.substring(5).trim() + } + + totalCount++ + } + } + + if (totalCount === 0) { + return null + } + + const currentTodo = firstIncompleteText ? { text: firstIncompleteText, completed: false, index: firstIncompleteIndex } : null + + return { + currentTodo, + currentIndex: firstIncompleteIndex >= 0 ? firstIncompleteIndex + 1 : totalCount, + completedCount, + totalCount, + progressPercentage: (completedCount / totalCount) * 100, + } +} + +/** + * Parse all todo items from the checklist + */ +function parseTodoItems(text: string): TodoItem[] { + const items: TodoItem[] = [] + const lines = text.split("\n") + + for (const rawLine of lines) { + const line = rawLine.trim() + const parsed = parseFocusChainItem(line) + if (parsed) { + items.push(parsed) + } + } + + return items +} + +/** + * Render progress bar + */ +const ProgressBar: React.FC<{ percentage: number; width?: number }> = ({ percentage, width = 20 }) => { + const filled = Math.round((percentage / 100) * width) + const empty = width - filled + const bar = "█".repeat(filled) + "░".repeat(empty) + + return ( + + {bar} + {Math.round(percentage)}% + + ) +} + +/** + * Header view showing current task and progress + */ +const Header: React.FC<{ + todoInfo: TodoInfo +}> = ({ todoInfo }) => { + const { currentTodo, currentIndex, totalCount, completedCount } = todoInfo + const isCompleted = completedCount === totalCount + + const displayText = isCompleted ? "All tasks completed!" : currentTodo?.text || "To-Do list" + const truncatedText = displayText.length > 50 ? displayText.substring(0, 47) + "..." : displayText + + return ( + + + [{currentIndex}/{totalCount}] + + {truncatedText} + + ) +} + +/** + * Expanded view showing all todo items + */ +const ExpandedList: React.FC<{ + items: TodoItem[] + isCompleted: boolean +}> = ({ items, isCompleted }) => { + return ( + + {items.map((item, index) => ( + + {item.checked ? "✓" : "○"} + + {item.text} + + + ))} + {isCompleted && ( + + + New steps will be generated if you continue the task + + + )} + + ) +} + +/** + * Main FocusChain component for CLI + * Shows a progress summary of the current to-do list + * Use expanded={true} to show all items (e.g., in verbose mode) + */ +export const FocusChain: React.FC = ({ focusChainChecklist, expanded = false }) => { + const todoInfo = useMemo( + () => (focusChainChecklist ? parseCurrentTodoInfo(focusChainChecklist) : null), + [focusChainChecklist], + ) + + const todoItems = useMemo(() => (focusChainChecklist ? parseTodoItems(focusChainChecklist) : []), [focusChainChecklist]) + + // No content to display + if (!todoInfo) { + return null + } + + const isCompleted = todoInfo.completedCount === todoInfo.totalCount + + return ( + +
+ + {expanded && } + + ) +} diff --git a/cli-ts/src/components/MessageRow.tsx b/cli-ts/src/components/MessageRow.tsx index 94446fa8ce..9c1aa74adb 100644 --- a/cli-ts/src/components/MessageRow.tsx +++ b/cli-ts/src/components/MessageRow.tsx @@ -206,11 +206,11 @@ const SayMessageContent: React.FC<{ message: ClineMessage; verbose?: boolean }> return {text} case "reasoning": - return verbose ? ( + return ( Thinking: {text} - ) : null + ) case "error": return ( diff --git a/cli-ts/src/components/TaskView.tsx b/cli-ts/src/components/TaskView.tsx index 0bcdda55e2..42e80c4dbe 100644 --- a/cli-ts/src/components/TaskView.tsx +++ b/cli-ts/src/components/TaskView.tsx @@ -8,6 +8,7 @@ import React, { useEffect } from "react" import { useTaskContext, useTaskState } from "../context/TaskContext" import { useCompletionSignals, useIsSpinnerActive } from "../hooks/useStateSubscriber" import { AskPrompt } from "./AskPrompt" +import { FocusChain } from "./FocusChain" import { MessageList } from "./MessageList" import { LoadingSpinner } from "./Spinner" @@ -74,6 +75,13 @@ export const TaskView: React.FC = ({ taskId, verbose = false, onC )} + {/* Focus Chain / To-Do List */} + {state.currentFocusChainChecklist && ( + + + + )} + {/* Messages list */} diff --git a/cli-ts/src/console.ts b/cli-ts/src/console.ts index 5d3f1e763e..2ca3c7025f 100644 --- a/cli-ts/src/console.ts +++ b/cli-ts/src/console.ts @@ -10,6 +10,7 @@ export const originalConsoleLog = console.log.bind(console) export const originalConsoleError = console.error.bind(console) export const originalConsoleWarn = console.warn.bind(console) export const originalConsoleInfo = console.info.bind(console) +export const originalConsoleDebug = console.debug.bind(console) // Check for verbose flag early (before commander parses) const isVerbose = process.argv.includes("-v") || process.argv.includes("--verbose") @@ -18,6 +19,8 @@ const isVerbose = process.argv.includes("-v") || process.argv.includes("--verbos if (!isVerbose) { console.log = () => {} console.warn = () => {} + console.error = () => {} + console.debug = () => {} } /** @@ -28,4 +31,5 @@ export function restoreConsole() { console.error = originalConsoleError console.warn = originalConsoleWarn console.info = originalConsoleInfo + console.debug = originalConsoleDebug } diff --git a/cli-ts/src/index.ts b/cli-ts/src/index.ts index 435687a142..821234842f 100644 --- a/cli-ts/src/index.ts +++ b/cli-ts/src/index.ts @@ -171,12 +171,14 @@ function waitForCondition(check: () => boolean, timeoutMs: number, intervalMs: n async function runTask( prompt: string, options: { - switch?: string + act?: boolean + plan?: boolean model?: string verbose?: boolean cwd?: string config?: string thinking?: boolean + yolo?: boolean images?: string[] }, existingContext?: CliContext, @@ -195,8 +197,10 @@ async function runTask( // Use clean prompt (with image refs removed) const taskPrompt = cleanPrompt || prompt - if (options.switch) { - StateManager.get().setGlobalState("mode", options.switch === "plan" ? "plan" : "act") + if (options.plan) { + StateManager.get().setGlobalState("mode", "plan") + } else if (options.act) { + StateManager.get().setGlobalState("mode", "act") } if (options.model) { const selectedMode = (StateManager.get().getGlobalSettingsKey("mode") || "act") as "act" | "plan" @@ -222,6 +226,11 @@ async function runTask( const thinkingKey = currentMode === "act" ? "actModeThinkingBudgetTokens" : "planModeThinkingBudgetTokens" StateManager.get().setGlobalState(thinkingKey, thinkingBudget) + // Set yolo mode based on --yolo flag + if (options.yolo) { + StateManager.get().setGlobalState("yoloModeToggled", true) + } + printInfo(`Starting Cline task...`) printInfo(`Working directory: ${ctx.workspacePath}`) if (imageDataUrls.length > 0) { @@ -393,13 +402,15 @@ program .alias("t") .description("Run a new task") .argument("", "The task prompt") - .option("-s, --switch ", "Switch mode: act, plan") + .option("-a, --act", "Run in act mode") + .option("-p, --plan", "Run in plan mode") + .option("-y, --yolo", "Enable yolo mode (auto-approve actions)") .option("-m, --model ", "Model to use for the task") .option("-i, --images ", "Image file paths to include with the task") - .option("-v, --verbose", "Show verbose output including reasoning") + .option("-v, --verbose", "Show verbose output") .option("-c, --cwd ", "Working directory for the task") .option("--config ", "Path to Cline configuration directory") - .option("--thinking", "Enable extended thinking (1024 token budget)") + .option("-t, --thinking", "Enable extended thinking (1024 token budget)") .action((prompt, options) => runTask(prompt, options)) program