feat(cli): implement Claude CLI detection and model selection for features

- 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 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-10 00:58:50 +01:00
parent a2d27b94bc
commit dcaf96aad3
16 changed files with 810 additions and 62 deletions

View File

@@ -50,9 +50,11 @@ function DialogContent({
className,
children,
showCloseButton = true,
compact = false,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
compact?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
@@ -60,7 +62,8 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 max-h-[calc(100vh-4rem)]",
compact ? "max-w-md p-4" : "sm:max-w-lg p-6",
className
)}
{...props}
@@ -69,7 +72,10 @@ function DialogContent({
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
className={cn(
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
compact ? "top-2 right-2" : "top-4 right-4"
)}
>
<XIcon />
<span className="sr-only">Close</span>

View File

@@ -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 <CheckCircle2 className="w-4 h-4" />;
case "warning":
return <AlertTriangle className="w-4 h-4" />;
case "thinking":
return <Brain className="w-4 h-4" />;
case "debug":
return <Bug className="w-4 h-4" />;
default:

View File

@@ -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) {

View File

@@ -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 */}
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent
compact={!isMaximized}
data-testid="add-feature-dialog"
onKeyDown={(e) => {
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.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-4 py-4 overflow-y-auto flex-1 min-h-0">
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<CategoryAutocomplete
@@ -1266,9 +1279,81 @@ export function BoardView() {
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
</div>
</div>
<p className="text-xs text-muted-foreground">
<p className="text-xs text-muted-foreground mb-4">
When enabled, this feature will require manual verification instead of automated TDD.
</p>
{/* Model Selection */}
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Zap className="w-4 h-4 text-muted-foreground" />
Model
</Label>
<div className="flex gap-2">
{(["haiku", "sonnet", "opus"] as AgentModel[]).map((model) => (
<button
key={model}
type="button"
onClick={() => setNewFeature({ ...newFeature, model })}
className={cn(
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors",
newFeature.model === model
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`model-select-${model}`}
>
{model === "haiku" && "Haiku"}
{model === "sonnet" && "Sonnet"}
{model === "opus" && "Opus"}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
Haiku for simple tasks, Sonnet for balanced, Opus for complex tasks.
</p>
</div>
{/* Thinking Level */}
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Brain className="w-4 h-4 text-muted-foreground" />
Thinking Level
</Label>
<div className="flex gap-2 flex-wrap">
{(["none", "low", "medium", "high", "ultrathink"] as ThinkingLevel[]).map((level) => (
<button
key={level}
type="button"
onClick={() => {
setNewFeature({ ...newFeature, thinkingLevel: level });
if (level === "ultrathink") {
toast.warning("Ultrathink Selected", {
description: "Ultrathink uses extensive reasoning (45-180s, ~$0.48/task). Best for complex architecture, migrations, or debugging.",
duration: 5000
});
}
}}
className={cn(
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[80px]",
newFeature.thinkingLevel === level
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`thinking-level-${level}`}
>
{level === "none" && "None"}
{level === "low" && "Low"}
{level === "medium" && "Med"}
{level === "high" && "High"}
{level === "ultrathink" && "Ultra"}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
Higher thinking levels give the model more time to reason through complex problems.
</p>
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => setShowAddDialog(false)}>
@@ -1296,13 +1381,13 @@ export function BoardView() {
open={!!editingFeature}
onOpenChange={() => setEditingFeature(null)}
>
<DialogContent data-testid="edit-feature-dialog">
<DialogContent compact={!isMaximized} data-testid="edit-feature-dialog">
<DialogHeader>
<DialogTitle>Edit Feature</DialogTitle>
<DialogDescription>Modify the feature details.</DialogDescription>
</DialogHeader>
{editingFeature && (
<div className="space-y-4 py-4">
<div className="space-y-4 py-4 overflow-y-auto flex-1 min-h-0">
<div className="space-y-2">
<Label htmlFor="edit-category">Category</Label>
<CategoryAutocomplete
@@ -1377,9 +1462,81 @@ export function BoardView() {
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
</div>
</div>
<p className="text-xs text-muted-foreground">
<p className="text-xs text-muted-foreground mb-4">
When enabled, this feature will require manual verification instead of automated TDD.
</p>
{/* Model Selection */}
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Zap className="w-4 h-4 text-muted-foreground" />
Model
</Label>
<div className="flex gap-2">
{(["haiku", "sonnet", "opus"] as AgentModel[]).map((model) => (
<button
key={model}
type="button"
onClick={() => setEditingFeature({ ...editingFeature, model })}
className={cn(
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors",
(editingFeature.model ?? "opus") === model
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`edit-model-select-${model}`}
>
{model === "haiku" && "Haiku"}
{model === "sonnet" && "Sonnet"}
{model === "opus" && "Opus"}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
Haiku for simple tasks, Sonnet for balanced, Opus for complex tasks.
</p>
</div>
{/* Thinking Level */}
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Brain className="w-4 h-4 text-muted-foreground" />
Thinking Level
</Label>
<div className="flex gap-2 flex-wrap">
{(["none", "low", "medium", "high", "ultrathink"] as ThinkingLevel[]).map((level) => (
<button
key={level}
type="button"
onClick={() => {
setEditingFeature({ ...editingFeature, thinkingLevel: level });
if (level === "ultrathink") {
toast.warning("Ultrathink Selected", {
description: "Ultrathink uses extensive reasoning (45-180s, ~$0.48/task). Best for complex architecture, migrations, or debugging.",
duration: 5000
});
}
}}
className={cn(
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[80px]",
(editingFeature.thinkingLevel ?? "none") === level
? "bg-primary text-primary-foreground border-primary"
: "bg-background hover:bg-accent border-input"
)}
data-testid={`edit-thinking-level-${level}`}
>
{level === "none" && "None"}
{level === "low" && "Low"}
{level === "medium" && "Med"}
{level === "high" && "High"}
{level === "ultrathink" && "Ultra"}
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
Higher thinking levels give the model more time to reason through complex problems.
</p>
</div>
</div>
)}
<DialogFooter>
@@ -1468,6 +1625,7 @@ export function BoardView() {
}
}}>
<DialogContent
compact={!isMaximized}
data-testid="follow-up-dialog"
onKeyDown={(e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && followUpPrompt.trim()) {
@@ -1487,7 +1645,7 @@ export function BoardView() {
)}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-4 py-4 overflow-y-auto flex-1 min-h-0">
<div className="space-y-2">
<Label htmlFor="follow-up-prompt">Instructions</Label>
<DescriptionImageDropZone

View File

@@ -6,7 +6,8 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Settings, Key, Eye, EyeOff, CheckCircle2, AlertCircle, Loader2, Zap, Sun, Moon, Palette, LayoutGrid, Minimize2, Square, Maximize2 } from "lucide-react";
import { Settings, Key, Eye, EyeOff, CheckCircle2, AlertCircle, Loader2, Zap, Sun, Moon, Palette, LayoutGrid, Minimize2, Square, Maximize2, Terminal } from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
export function SettingsView() {
const { apiKeys, setApiKeys, setCurrentView, theme, setTheme, kanbanCardDetailLevel, setKanbanCardDetailLevel } = useAppStore();
@@ -19,12 +20,42 @@ export function SettingsView() {
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(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() {
</div>
</div>
{/* Claude CLI Status Section */}
{claudeCliStatus && (
<div className="rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md overflow-hidden">
<div className="p-6 border-b border-white/10">
<div className="flex items-center gap-2 mb-2">
<Terminal className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-white">Claude Code CLI</h2>
</div>
<p className="text-sm text-zinc-400">
Claude Code CLI provides better performance for long-running tasks, especially with ultrathink.
</p>
</div>
<div className="p-6 space-y-4">
{claudeCliStatus.success && claudeCliStatus.status === 'installed' ? (
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-green-400">Claude Code CLI Installed</p>
<div className="text-xs text-green-400/80 mt-1 space-y-1">
{claudeCliStatus.method && (
<p>Method: <span className="font-mono">{claudeCliStatus.method}</span></p>
)}
{claudeCliStatus.version && (
<p>Version: <span className="font-mono">{claudeCliStatus.version}</span></p>
)}
{claudeCliStatus.path && (
<p className="truncate" title={claudeCliStatus.path}>
Path: <span className="font-mono text-[10px]">{claudeCliStatus.path}</span>
</p>
)}
</div>
</div>
</div>
{claudeCliStatus.recommendation && (
<p className="text-xs text-zinc-400">{claudeCliStatus.recommendation}</p>
)}
</div>
) : (
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-yellow-400">Claude Code CLI Not Detected</p>
<p className="text-xs text-yellow-400/80 mt-1">
{claudeCliStatus.recommendation || 'Consider installing Claude Code CLI for optimal performance with ultrathink.'}
</p>
</div>
</div>
{claudeCliStatus.installCommands && (
<div className="space-y-2">
<p className="text-xs font-medium text-zinc-300">Installation Commands:</p>
<div className="space-y-1">
{claudeCliStatus.installCommands.npm && (
<div className="p-2 rounded bg-zinc-950/50 border border-white/5">
<p className="text-xs text-zinc-400 mb-1">npm:</p>
<code className="text-xs text-zinc-300 font-mono break-all">{claudeCliStatus.installCommands.npm}</code>
</div>
)}
{claudeCliStatus.installCommands.macos && (
<div className="p-2 rounded bg-zinc-950/50 border border-white/5">
<p className="text-xs text-zinc-400 mb-1">macOS/Linux:</p>
<code className="text-xs text-zinc-300 font-mono break-all">{claudeCliStatus.installCommands.macos}</code>
</div>
)}
{claudeCliStatus.installCommands.windows && (
<div className="p-2 rounded bg-zinc-950/50 border border-white/5">
<p className="text-xs text-zinc-400 mb-1">Windows (PowerShell):</p>
<code className="text-xs text-zinc-300 font-mono break-all">{claudeCliStatus.installCommands.windows}</code>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
)}
{/* Appearance Section */}
<div className="rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-md overflow-hidden">
<div className="p-6 border-b border-white/10">