From dcaf96aad30df9a8a508000af10e7df55ce5b2ff Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 10 Dec 2025 00:58:50 +0100 Subject: [PATCH] feat(cli): implement Claude CLI detection and model selection for features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added a new service to detect the installation status of Claude Code CLI, providing users with installation recommendations and commands. - Integrated CLI detection into the SettingsView to inform users about the CLI status and its benefits for ultrathink tasks. - Enhanced feature creation and editing dialogs to allow users to select from multiple models (Haiku, Sonnet, Opus) and specify thinking levels (None, Low, Medium, High, Ultrathink). - Updated the feature executor to utilize the selected model and thinking configuration during task execution, improving flexibility and performance. This update enhances user experience by providing clearer options for model selection and ensuring optimal performance with the Claude CLI. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4 --- .automaker/feature_list.json | 17 ++ app/electron/main.js | 18 ++ app/electron/preload.js | 3 + app/electron/services/claude-cli-detector.js | 119 ++++++++++++ app/electron/services/feature-executor.js | 161 +++++++++++++++- app/electron/services/feature-loader.js | 6 + app/src/components/ui/dialog.tsx | 10 +- app/src/components/ui/log-viewer.tsx | 3 + .../components/views/agent-output-modal.tsx | 99 ++++++---- app/src/components/views/board-view.tsx | 176 +++++++++++++++++- app/src/components/views/settings-view.tsx | 113 ++++++++++- app/src/hooks/use-window-state.ts | 54 ++++++ app/src/lib/electron.ts | 32 ++-- app/src/lib/log-parser.ts | 28 ++- app/src/store/app-store.ts | 8 + app/src/types/electron.d.ts | 25 +++ 16 files changed, 810 insertions(+), 62 deletions(-) create mode 100644 app/electron/services/claude-cli-detector.js create mode 100644 app/src/hooks/use-window-state.ts diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index ab138177..b8909b49 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -71,5 +71,22 @@ "startedAt": "2025-12-09T22:31:41.946Z", "imagePaths": [], "skipTests": true + }, + { + "id": "feature-1765321570899-oefrfast6", + "category": "Core", + "description": "I would like to have abbility to set correct model for new feautres. so inteas of only using claude opus we could use other models like sonnet or haiku for easier / light one tasks as well to add abbility how much thinking lvl we wanna use on each task as well", + "steps": [ + "User add new feature", + "User Describe it", + "Select the automated testing or manual one", + "If the task is light / easy to implement he use lighter model from anthropi sdk such as sonnet / haiku", + "agent execute task with correct model " + ], + "status": "verified", + "startedAt": "2025-12-09T23:07:37.223Z", + "imagePaths": [], + "skipTests": false, + "summary": "Added model selection (Haiku/Sonnet/Opus) and thinking level (None/Low/Medium/High) controls to feature creation and edit dialogs. Modified: app-store.ts (added AgentModel and ThinkingLevel types), board-view.tsx (UI controls), feature-executor.js (dynamic model/thinking config), feature-loader.js (field persistence). Agent now executes with user-selected model and extended thinking settings." } ] \ No newline at end of file diff --git a/app/electron/main.js b/app/electron/main.js index 75d45d72..968c442b 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -551,3 +551,21 @@ ipcMain.handle("auto-mode:commit-feature", async (_, { projectPath, featureId }) return { success: false, error: error.message }; } }); + +// ============================================================================ +// Claude CLI Detection IPC Handlers +// ============================================================================ + +/** + * Check Claude Code CLI installation status + */ +ipcMain.handle("claude:check-cli", async () => { + try { + const claudeCliDetector = require("./services/claude-cli-detector"); + const info = claudeCliDetector.getInstallationInfo(); + return { success: true, ...info }; + } catch (error) { + console.error("[IPC] claude:check-cli error:", error); + return { success: false, error: error.message }; + } +}); diff --git a/app/electron/preload.js b/app/electron/preload.js index 1b5f49e0..11819dec 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -138,6 +138,9 @@ contextBridge.exposeInMainWorld("electronAPI", { }; }, }, + + // Claude CLI Detection API + checkClaudeCli: () => ipcRenderer.invoke("claude:check-cli"), }); // Also expose a flag to detect if we're in Electron diff --git a/app/electron/services/claude-cli-detector.js b/app/electron/services/claude-cli-detector.js new file mode 100644 index 00000000..31030f0d --- /dev/null +++ b/app/electron/services/claude-cli-detector.js @@ -0,0 +1,119 @@ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +class ClaudeCliDetector { + /** + * Check if Claude Code CLI is installed and accessible + * @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'sdk'|'none' } + */ + static detectClaudeInstallation() { + try { + // Method 1: Check if 'claude' command is in PATH + try { + const claudePath = execSync('which claude', { encoding: 'utf-8' }).trim(); + const version = execSync('claude --version', { encoding: 'utf-8' }).trim(); + return { + installed: true, + path: claudePath, + version: version, + method: 'cli' + }; + } catch (error) { + // CLI not in PATH, check local installation + } + + // Method 2: Check for local installation + const localClaudePath = path.join(os.homedir(), '.claude', 'local', 'claude'); + if (fs.existsSync(localClaudePath)) { + try { + const version = execSync(`${localClaudePath} --version`, { encoding: 'utf-8' }).trim(); + return { + installed: true, + path: localClaudePath, + version: version, + method: 'cli-local' + }; + } catch (error) { + // Local CLI exists but may not be executable + } + } + + // Method 3: Check Windows path + if (process.platform === 'win32') { + try { + const claudePath = execSync('where claude', { encoding: 'utf-8' }).trim(); + const version = execSync('claude --version', { encoding: 'utf-8' }).trim(); + return { + installed: true, + path: claudePath, + version: version, + method: 'cli' + }; + } catch (error) { + // Not found + } + } + + // Method 4: SDK mode (using OAuth token) + if (process.env.CLAUDE_CODE_OAUTH_TOKEN) { + return { + installed: true, + path: null, + version: 'SDK Mode', + method: 'sdk' + }; + } + + return { + installed: false, + path: null, + version: null, + method: 'none' + }; + } catch (error) { + console.error('[ClaudeCliDetector] Error detecting Claude installation:', error); + return { + installed: false, + path: null, + version: null, + method: 'none', + error: error.message + }; + } + } + + /** + * Get installation recommendations + */ + static getInstallationInfo() { + const detection = this.detectClaudeInstallation(); + + if (detection.installed) { + return { + status: 'installed', + method: detection.method, + version: detection.version, + path: detection.path, + recommendation: detection.method === 'cli' + ? 'Using Claude Code CLI - optimal for long-running tasks' + : 'Using SDK mode - works well but CLI may provide better performance' + }; + } + + return { + status: 'not_installed', + recommendation: 'Consider installing Claude Code CLI for better performance with ultrathink', + installCommands: { + macos: 'curl -fsSL claude.ai/install.sh | bash', + windows: 'irm https://claude.ai/install.ps1 | iex', + linux: 'curl -fsSL claude.ai/install.sh | bash', + npm: 'npm install -g @anthropic-ai/claude-code' + } + }; + } +} + +module.exports = ClaudeCliDetector; + diff --git a/app/electron/services/feature-executor.js b/app/electron/services/feature-executor.js index 239c7518..6899d8c5 100644 --- a/app/electron/services/feature-executor.js +++ b/app/electron/services/feature-executor.js @@ -4,10 +4,97 @@ const contextManager = require("./context-manager"); const featureLoader = require("./feature-loader"); const mcpServerFactory = require("./mcp-server-factory"); +// Model name mappings +const MODEL_MAP = { + haiku: "claude-haiku-4-20250514", + sonnet: "claude-sonnet-4-20250514", + opus: "claude-opus-4-5-20251101", +}; + +// Thinking level to budget_tokens mapping +// These values control how much "thinking time" the model gets for extended thinking +const THINKING_BUDGET_MAP = { + none: null, // No extended thinking + low: 4096, // Light thinking + medium: 16384, // Moderate thinking + high: 65536, // Deep thinking + ultrathink: 262144, // Ultra-deep thinking (maximum reasoning) +}; + /** * Feature Executor - Handles feature implementation using Claude Agent SDK */ class FeatureExecutor { + /** + * Get the model string based on feature's model setting + */ + getModelString(feature) { + const modelKey = feature.model || "opus"; // Default to opus + return MODEL_MAP[modelKey] || MODEL_MAP.opus; + } + + /** + * Get thinking configuration based on feature's thinkingLevel + */ + getThinkingConfig(feature) { + const level = feature.thinkingLevel || "none"; + const budgetTokens = THINKING_BUDGET_MAP[level]; + + if (budgetTokens === null) { + return null; // No extended thinking + } + + return { + type: "enabled", + budget_tokens: budgetTokens, + }; + } + + /** + * Prepare for ultrathink execution - validate and warn + */ + prepareForUltrathink(feature, thinkingConfig) { + if (feature.thinkingLevel !== 'ultrathink') { + return { ready: true }; + } + + const warnings = []; + const recommendations = []; + + // Check CLI installation + const claudeCliDetector = require('./claude-cli-detector'); + const cliInfo = claudeCliDetector.getInstallationInfo(); + + if (cliInfo.status === 'not_installed') { + warnings.push('Claude Code CLI not detected - ultrathink may have timeout issues'); + recommendations.push('Install Claude Code CLI for optimal ultrathink performance'); + } + + // Validate budget tokens + if (thinkingConfig && thinkingConfig.budget_tokens > 32000) { + warnings.push(`Ultrathink budget (${thinkingConfig.budget_tokens} tokens) exceeds recommended 32K - may cause long-running requests`); + recommendations.push('Consider using batch processing for budgets above 32K'); + } + + // Cost estimate (rough) + const estimatedCost = (thinkingConfig?.budget_tokens || 0) / 1000 * 0.015; // Rough estimate + if (estimatedCost > 1.0) { + warnings.push(`Estimated cost: ~$${estimatedCost.toFixed(2)} per execution`); + } + + // Time estimate + warnings.push('Ultrathink tasks typically take 45-180 seconds'); + + return { + ready: true, + warnings, + recommendations, + estimatedCost, + estimatedTime: '45-180 seconds', + cliInfo + }; + } + /** * Sleep helper */ @@ -46,9 +133,39 @@ class FeatureExecutor { projectPath ); + // Get model and thinking configuration from feature settings + const modelString = this.getModelString(feature); + const thinkingConfig = this.getThinkingConfig(feature); + + // Prepare for ultrathink if needed + if (feature.thinkingLevel === 'ultrathink') { + const preparation = this.prepareForUltrathink(feature, thinkingConfig); + + console.log(`[FeatureExecutor] Ultrathink preparation:`, preparation); + + // Log warnings + if (preparation.warnings && preparation.warnings.length > 0) { + preparation.warnings.forEach(warning => { + console.warn(`[FeatureExecutor] āš ļø ${warning}`); + }); + } + + // Send preparation info to renderer + sendToRenderer({ + type: 'auto_mode_ultrathink_preparation', + featureId: feature.id, + warnings: preparation.warnings || [], + recommendations: preparation.recommendations || [], + estimatedCost: preparation.estimatedCost, + estimatedTime: preparation.estimatedTime + }); + } + + console.log(`[FeatureExecutor] Using model: ${modelString}, thinking: ${feature.thinkingLevel || 'none'}`); + // Configure options for the SDK query const options = { - model: "claude-opus-4-5-20251101", + model: modelString, systemPrompt: promptBuilder.getCodingPrompt(), maxTurns: 1000, cwd: projectPath, @@ -74,6 +191,11 @@ class FeatureExecutor { abortController: abortController, }; + // Add thinking configuration if enabled + if (thinkingConfig) { + options.thinking = thinkingConfig; + } + // Build the prompt for this specific feature const prompt = promptBuilder.buildFeaturePrompt(feature); @@ -256,8 +378,38 @@ class FeatureExecutor { projectPath ); + // Get model and thinking configuration from feature settings + const modelString = this.getModelString(feature); + const thinkingConfig = this.getThinkingConfig(feature); + + // Prepare for ultrathink if needed + if (feature.thinkingLevel === 'ultrathink') { + const preparation = this.prepareForUltrathink(feature, thinkingConfig); + + console.log(`[FeatureExecutor] Ultrathink preparation:`, preparation); + + // Log warnings + if (preparation.warnings && preparation.warnings.length > 0) { + preparation.warnings.forEach(warning => { + console.warn(`[FeatureExecutor] āš ļø ${warning}`); + }); + } + + // Send preparation info to renderer + sendToRenderer({ + type: 'auto_mode_ultrathink_preparation', + featureId: feature.id, + warnings: preparation.warnings || [], + recommendations: preparation.recommendations || [], + estimatedCost: preparation.estimatedCost, + estimatedTime: preparation.estimatedTime + }); + } + + console.log(`[FeatureExecutor] Resuming with model: ${modelString}, thinking: ${feature.thinkingLevel || 'none'}`); + const options = { - model: "claude-opus-4-5-20251101", + model: modelString, systemPrompt: promptBuilder.getVerificationPrompt(), maxTurns: 1000, cwd: projectPath, @@ -273,6 +425,11 @@ class FeatureExecutor { abortController: abortController, }; + // Add thinking configuration if enabled + if (thinkingConfig) { + options.thinking = thinkingConfig; + } + // Build prompt with previous context const prompt = promptBuilder.buildResumePrompt(feature, previousContext); diff --git a/app/electron/services/feature-loader.js b/app/electron/services/feature-loader.js index e8e1bbbd..31fe84dd 100644 --- a/app/electron/services/feature-loader.js +++ b/app/electron/services/feature-loader.js @@ -84,6 +84,12 @@ class FeatureLoader { if (f.summary !== undefined) { featureData.summary = f.summary; } + if (f.model !== undefined) { + featureData.model = f.model; + } + if (f.thinkingLevel !== undefined) { + featureData.thinkingLevel = f.thinkingLevel; + } return featureData; }); diff --git a/app/src/components/ui/dialog.tsx b/app/src/components/ui/dialog.tsx index d9ccec91..fec3338d 100644 --- a/app/src/components/ui/dialog.tsx +++ b/app/src/components/ui/dialog.tsx @@ -50,9 +50,11 @@ function DialogContent({ className, children, showCloseButton = true, + compact = false, ...props }: React.ComponentProps & { showCloseButton?: boolean + compact?: boolean }) { return ( @@ -60,7 +62,8 @@ function DialogContent({ Close diff --git a/app/src/components/ui/log-viewer.tsx b/app/src/components/ui/log-viewer.tsx index 708f596b..414fd580 100644 --- a/app/src/components/ui/log-viewer.tsx +++ b/app/src/components/ui/log-viewer.tsx @@ -13,6 +13,7 @@ import { Bug, Info, FileOutput, + Brain, } from "lucide-react"; import { cn } from "@/lib/utils"; import { @@ -43,6 +44,8 @@ const getLogIcon = (type: LogEntryType) => { return ; case "warning": return ; + case "thinking": + return ; case "debug": return ; default: diff --git a/app/src/components/views/agent-output-modal.tsx b/app/src/components/views/agent-output-modal.tsx index 14bb1787..701a943b 100644 --- a/app/src/components/views/agent-output-modal.tsx +++ b/app/src/components/views/agent-output-modal.tsx @@ -11,6 +11,7 @@ import { import { Loader2, List, FileText } from "lucide-react"; import { getElectronAPI } from "@/lib/electron"; import { LogViewer } from "@/components/ui/log-viewer"; +import type { AutoModeEvent } from "@/types/electron"; interface AgentOutputModalProps { open: boolean; @@ -113,44 +114,78 @@ export function AgentOutputModal({ if (!api?.autoMode) return; const unsubscribe = api.autoMode.onEvent((event) => { - // Filter events for this specific feature only - if (event.featureId !== featureId) { + // Filter events for this specific feature only (skip events without featureId) + if ("featureId" in event && event.featureId !== featureId) { return; } let newContent = ""; - if (event.type === "auto_mode_progress") { - newContent = event.content || ""; - } else if (event.type === "auto_mode_tool") { - const toolName = event.tool || "Unknown Tool"; - const toolInput = event.input - ? JSON.stringify(event.input, null, 2) - : ""; - newContent = `\nšŸ”§ Tool: ${toolName}\n${ - toolInput ? `Input: ${toolInput}` : "" - }`; - } else if (event.type === "auto_mode_phase") { - const phaseEmoji = - event.phase === "planning" - ? "šŸ“‹" - : event.phase === "action" - ? "⚔" - : "āœ…"; - newContent = `\n${phaseEmoji} ${event.message}\n`; - } else if (event.type === "auto_mode_error") { - newContent = `\nāŒ Error: ${event.error}\n`; - } else if (event.type === "auto_mode_feature_complete") { - const emoji = event.passes ? "āœ…" : "āš ļø"; - newContent = `\n${emoji} Task completed: ${event.message}\n`; + switch (event.type) { + case "auto_mode_progress": + newContent = event.content || ""; + break; + case "auto_mode_tool": + const toolName = event.tool || "Unknown Tool"; + const toolInput = event.input + ? JSON.stringify(event.input, null, 2) + : ""; + newContent = `\nšŸ”§ Tool: ${toolName}\n${ + toolInput ? `Input: ${toolInput}` : "" + }`; + break; + case "auto_mode_phase": + const phaseEmoji = + event.phase === "planning" + ? "šŸ“‹" + : event.phase === "action" + ? "⚔" + : "āœ…"; + newContent = `\n${phaseEmoji} ${event.message}\n`; + break; + case "auto_mode_error": + newContent = `\nāŒ Error: ${event.error}\n`; + break; + case "auto_mode_ultrathink_preparation": + // Format thinking level preparation information + let prepContent = `\n🧠 Ultrathink Preparation\n`; + + if (event.warnings && event.warnings.length > 0) { + prepContent += `\nāš ļø Warnings:\n`; + event.warnings.forEach((warning: string) => { + prepContent += ` • ${warning}\n`; + }); + } + + if (event.recommendations && event.recommendations.length > 0) { + prepContent += `\nšŸ’” Recommendations:\n`; + event.recommendations.forEach((rec: string) => { + prepContent += ` • ${rec}\n`; + }); + } + + if (event.estimatedCost !== undefined) { + prepContent += `\nšŸ’° Estimated Cost: ~$${event.estimatedCost.toFixed(2)} per execution\n`; + } + + if (event.estimatedTime) { + prepContent += `\nā±ļø Estimated Time: ${event.estimatedTime}\n`; + } + + newContent = prepContent; + break; + case "auto_mode_feature_complete": + const emoji = event.passes ? "āœ…" : "āš ļø"; + newContent = `\n${emoji} Task completed: ${event.message}\n`; - // Close the modal when the feature is verified (passes = true) - if (event.passes) { - // Small delay to show the completion message before closing - setTimeout(() => { - onClose(); - }, 1500); - } + // Close the modal when the feature is verified (passes = true) + if (event.passes) { + // Small delay to show the completion message before closing + setTimeout(() => { + onClose(); + }, 1500); + } + break; } if (newContent) { diff --git a/app/src/components/views/board-view.tsx b/app/src/components/views/board-view.tsx index 6d5a4d76..45150843 100644 --- a/app/src/components/views/board-view.tsx +++ b/app/src/components/views/board-view.tsx @@ -16,7 +16,7 @@ import { SortableContext, verticalListSortingStrategy, } from "@dnd-kit/sortable"; -import { useAppStore, Feature, FeatureImage, FeatureImagePath } from "@/store/app-store"; +import { useAppStore, Feature, FeatureImage, FeatureImagePath, AgentModel, ThinkingLevel } from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; import { cn } from "@/lib/utils"; import { @@ -44,7 +44,7 @@ import { KanbanColumn } from "./kanban-column"; import { KanbanCard } from "./kanban-card"; import { AutoModeLog } from "./auto-mode-log"; import { AgentOutputModal } from "./agent-output-modal"; -import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward, FlaskConical, CheckCircle2, MessageSquare, GitCommit } from "lucide-react"; +import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward, FlaskConical, CheckCircle2, MessageSquare, GitCommit, Brain, Zap } from "lucide-react"; import { toast } from "sonner"; import { Slider } from "@/components/ui/slider"; import { Checkbox } from "@/components/ui/checkbox"; @@ -54,6 +54,7 @@ import { ACTION_SHORTCUTS, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; +import { useWindowState } from "@/hooks/use-window-state"; type ColumnId = Feature["status"]; @@ -87,6 +88,8 @@ export function BoardView() { images: [] as FeatureImage[], imagePaths: [] as DescriptionImagePath[], skipTests: false, + model: "opus" as AgentModel, + thinkingLevel: "none" as ThinkingLevel, }); const [isLoading, setIsLoading] = useState(true); const [isMounted, setIsMounted] = useState(false); @@ -118,6 +121,9 @@ export function BoardView() { // Auto mode hook const autoMode = useAutoMode(); + // Window state hook for compact dialog mode + const { isMaximized } = useWindowState(); + // Get in-progress features for keyboard shortcuts (memoized for shortcuts) const inProgressFeaturesForShortcuts = useMemo(() => { return features.filter((f) => { @@ -405,6 +411,8 @@ export function BoardView() { imagePaths: f.imagePaths, skipTests: f.skipTests, summary: f.summary, + model: f.model, + thinkingLevel: f.thinkingLevel, })); await api.writeFile( `${currentProject.path}/.automaker/feature_list.json`, @@ -531,10 +539,12 @@ export function BoardView() { images: newFeature.images, imagePaths: newFeature.imagePaths, skipTests: newFeature.skipTests, + model: newFeature.model, + thinkingLevel: newFeature.thinkingLevel, }); // Persist the category saveCategory(category); - setNewFeature({ category: "", description: "", steps: [""], images: [], imagePaths: [], skipTests: false }); + setNewFeature({ category: "", description: "", steps: [""], images: [], imagePaths: [], skipTests: false, model: "opus", thinkingLevel: "none" }); setShowAddDialog(false); }; @@ -546,6 +556,8 @@ export function BoardView() { description: editingFeature.description, steps: editingFeature.steps, skipTests: editingFeature.skipTests, + model: editingFeature.model, + thinkingLevel: editingFeature.thinkingLevel, }); // Persist the category if it's new if (editingFeature.category) { @@ -1179,6 +1191,7 @@ export function BoardView() { {/* Add Feature Dialog */} { if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && newFeature.description) { @@ -1193,7 +1206,7 @@ export function BoardView() { Create a new feature card for the Kanban board. -
+
-

+

When enabled, this feature will require manual verification instead of automated TDD.

+ + {/* Model Selection */} +
+ +
+ {(["haiku", "sonnet", "opus"] as AgentModel[]).map((model) => ( + + ))} +
+

+ Haiku for simple tasks, Sonnet for balanced, Opus for complex tasks. +

+
+ + {/* Thinking Level */} +
+ +
+ {(["none", "low", "medium", "high", "ultrathink"] as ThinkingLevel[]).map((level) => ( + + ))} +
+

+ Higher thinking levels give the model more time to reason through complex problems. +

+
+ ))} + +

+ Haiku for simple tasks, Sonnet for balanced, Opus for complex tasks. +

+ + + {/* Thinking Level */} +
+ +
+ {(["none", "low", "medium", "high", "ultrathink"] as ThinkingLevel[]).map((level) => ( + + ))} +
+

+ Higher thinking levels give the model more time to reason through complex problems. +

+
)} @@ -1468,6 +1625,7 @@ export function BoardView() { } }}> { if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && followUpPrompt.trim()) { @@ -1487,7 +1645,7 @@ export function BoardView() { )} -
+
(null); const [testingGeminiConnection, setTestingGeminiConnection] = useState(false); const [geminiTestResult, setGeminiTestResult] = useState<{ success: boolean; message: string } | null>(null); + const [claudeCliStatus, setClaudeCliStatus] = useState<{ + success: boolean; + status?: string; + method?: string; + version?: string; + path?: string; + recommendation?: string; + installCommands?: { + macos?: string; + windows?: string; + linux?: string; + npm?: string; + }; + error?: string; + } | null>(null); useEffect(() => { setAnthropicKey(apiKeys.anthropic); setGoogleKey(apiKeys.google); }, [apiKeys]); + useEffect(() => { + const checkCliStatus = async () => { + const api = getElectronAPI(); + if (api?.checkClaudeCli) { + try { + const status = await api.checkClaudeCli(); + setClaudeCliStatus(status); + } catch (error) { + console.error("Failed to check Claude CLI status:", error); + } + } + }; + checkCliStatus(); + }, []); + const handleTestConnection = async () => { setTestingConnection(true); setTestResult(null); @@ -309,6 +340,86 @@ export function SettingsView() {
+ {/* Claude CLI Status Section */} + {claudeCliStatus && ( +
+
+
+ +

Claude Code CLI

+
+

+ Claude Code CLI provides better performance for long-running tasks, especially with ultrathink. +

+
+
+ {claudeCliStatus.success && claudeCliStatus.status === 'installed' ? ( +
+
+ +
+

Claude Code CLI Installed

+
+ {claudeCliStatus.method && ( +

Method: {claudeCliStatus.method}

+ )} + {claudeCliStatus.version && ( +

Version: {claudeCliStatus.version}

+ )} + {claudeCliStatus.path && ( +

+ Path: {claudeCliStatus.path} +

+ )} +
+
+
+ {claudeCliStatus.recommendation && ( +

{claudeCliStatus.recommendation}

+ )} +
+ ) : ( +
+
+ +
+

Claude Code CLI Not Detected

+

+ {claudeCliStatus.recommendation || 'Consider installing Claude Code CLI for optimal performance with ultrathink.'} +

+
+
+ {claudeCliStatus.installCommands && ( +
+

Installation Commands:

+
+ {claudeCliStatus.installCommands.npm && ( +
+

npm:

+ {claudeCliStatus.installCommands.npm} +
+ )} + {claudeCliStatus.installCommands.macos && ( +
+

macOS/Linux:

+ {claudeCliStatus.installCommands.macos} +
+ )} + {claudeCliStatus.installCommands.windows && ( +
+

Windows (PowerShell):

+ {claudeCliStatus.installCommands.windows} +
+ )} +
+
+ )} +
+ )} +
+
+ )} + {/* Appearance Section */}
diff --git a/app/src/hooks/use-window-state.ts b/app/src/hooks/use-window-state.ts new file mode 100644 index 00000000..8a4bc332 --- /dev/null +++ b/app/src/hooks/use-window-state.ts @@ -0,0 +1,54 @@ +import { useState, useEffect } from "react"; + +export interface WindowState { + isMaximized: boolean; + windowWidth: number; + windowHeight: number; +} + +/** + * Hook to track window state (dimensions and maximized status) + * For Electron apps, considers window maximized if width > 1400px + * Also listens for window resize events to update state + */ +export function useWindowState(): WindowState { + const [windowState, setWindowState] = useState(() => { + if (typeof window === "undefined") { + return { isMaximized: false, windowWidth: 0, windowHeight: 0 }; + } + const width = window.innerWidth; + const height = window.innerHeight; + return { + isMaximized: width > 1400, + windowWidth: width, + windowHeight: height, + }; + }); + + useEffect(() => { + if (typeof window === "undefined") return; + + const updateWindowState = () => { + const width = window.innerWidth; + const height = window.innerHeight; + setWindowState({ + isMaximized: width > 1400, + windowWidth: width, + windowHeight: height, + }); + }; + + // Set initial state + updateWindowState(); + + // Listen for resize events + window.addEventListener("resize", updateWindowState); + + return () => { + window.removeEventListener("resize", updateWindowState); + }; + }, []); + + return windowState; +} + diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index 91bb6854..8a3a13a0 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -41,21 +41,8 @@ export interface StatResult { error?: string; } -// Auto Mode types -export type AutoModePhase = "planning" | "action" | "verification"; - -export interface AutoModeEvent { - type: "auto_mode_feature_start" | "auto_mode_progress" | "auto_mode_tool" | "auto_mode_feature_complete" | "auto_mode_error" | "auto_mode_complete" | "auto_mode_phase"; - featureId?: string; - feature?: object; - content?: string; - tool?: string; - input?: unknown; - passes?: boolean; - message?: string; - error?: string; - phase?: AutoModePhase; -} +// Auto Mode types - Import from electron.d.ts to avoid duplication +import type { AutoModeEvent } from "@/types/electron"; export interface AutoModeAPI { start: (projectPath: string) => Promise<{ success: boolean; error?: string }>; @@ -92,6 +79,21 @@ export interface ElectronAPI { getPath: (name: string) => Promise; saveImageToTemp?: (data: string, filename: string, mimeType: string) => Promise; autoMode?: AutoModeAPI; + checkClaudeCli?: () => Promise<{ + success: boolean; + status?: string; + method?: string; + version?: string; + path?: string; + recommendation?: string; + installCommands?: { + macos?: string; + windows?: string; + linux?: string; + npm?: string; + }; + error?: string; + }>; } declare global { diff --git a/app/src/lib/log-parser.ts b/app/src/lib/log-parser.ts index 76a6ce15..04ab85ee 100644 --- a/app/src/lib/log-parser.ts +++ b/app/src/lib/log-parser.ts @@ -12,7 +12,8 @@ export type LogEntryType = | "success" | "info" | "debug" - | "warning"; + | "warning" + | "thinking"; export interface LogEntry { id: string; @@ -75,6 +76,18 @@ function detectEntryType(content: string): LogEntryType { return "warning"; } + // Thinking/Preparation info + if ( + trimmed.toLowerCase().includes("ultrathink") || + trimmed.toLowerCase().includes("thinking level") || + trimmed.toLowerCase().includes("estimated cost") || + trimmed.toLowerCase().includes("estimated time") || + trimmed.toLowerCase().includes("budget tokens") || + trimmed.match(/thinking.*preparation/i) + ) { + return "thinking"; + } + // Debug info (JSON, stack traces, etc.) if ( trimmed.startsWith("{") || @@ -130,6 +143,8 @@ function generateTitle(type: LogEntryType, content: string): string { return "Success"; case "warning": return "Warning"; + case "thinking": + return "Thinking Level"; case "debug": return "Debug Info"; case "prompt": @@ -180,6 +195,9 @@ export function parseLogOutput(rawOutput: string): LogEntry[] { trimmedLine.startsWith("āœ…") || trimmedLine.startsWith("āŒ") || trimmedLine.startsWith("āš ļø") || + trimmedLine.startsWith("🧠") || + trimmedLine.toLowerCase().includes("ultrathink preparation") || + trimmedLine.toLowerCase().includes("thinking level") || (trimmedLine.startsWith("Input:") && currentEntry?.type === "tool_call"); if (isNewEntry) { @@ -321,6 +339,14 @@ export function getLogTypeColors(type: LogEntryType): { icon: "text-orange-400", badge: "bg-orange-500/20 text-orange-300", }; + case "thinking": + return { + bg: "bg-indigo-500/10", + border: "border-l-indigo-500", + text: "text-indigo-300", + icon: "text-indigo-400", + badge: "bg-indigo-500/20 text-indigo-300", + }; case "debug": return { bg: "bg-purple-500/10", diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index e58eda5a..02aa6559 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -52,6 +52,12 @@ export interface FeatureImagePath { mimeType: string; } +// Available models for feature execution +export type AgentModel = "opus" | "sonnet" | "haiku"; + +// Thinking level (budget_tokens) options +export type ThinkingLevel = "none" | "low" | "medium" | "high" | "ultrathink"; + export interface Feature { id: string; category: string; @@ -63,6 +69,8 @@ export interface Feature { startedAt?: string; // ISO timestamp for when the card moved to in_progress skipTests?: boolean; // When true, skip TDD approach and require manual verification summary?: string; // Summary of what was done/modified by the agent + model?: AgentModel; // Model to use for this feature (defaults to opus) + thinkingLevel?: ThinkingLevel; // Thinking level for extended thinking (defaults to none) } export interface AppState { diff --git a/app/src/types/electron.d.ts b/app/src/types/electron.d.ts index 2a44841a..137accdb 100644 --- a/app/src/types/electron.d.ts +++ b/app/src/types/electron.d.ts @@ -195,6 +195,14 @@ export type AutoModeEvent = featureId: string; phase: "planning" | "action" | "verification"; message: string; + } + | { + type: "auto_mode_ultrathink_preparation"; + featureId: string; + warnings: string[]; + recommendations: string[]; + estimatedCost?: number; + estimatedTime?: string; }; export interface AutoModeAPI { @@ -315,6 +323,23 @@ export interface ElectronAPI { // Auto Mode APIs autoMode: AutoModeAPI; + + // Claude CLI Detection API + checkClaudeCli: () => Promise<{ + success: boolean; + status?: string; + method?: string; + version?: string; + path?: string; + recommendation?: string; + installCommands?: { + macos?: string; + windows?: string; + linux?: string; + npm?: string; + }; + error?: string; + }>; } declare global {