Skip to content

Commit e6337ef

Browse files
committed
feat: add Claude Haiku model support and fix WSL binary path validation
- Add Haiku as third model option in model selector, agent execution, and queue - Update model display names to simplified 'Claude Sonnet/Opus/Haiku' format - Fix WSL binary path validation by skipping checks for WSL environment (Linux paths like /home/user/.nvm/.../claude don't exist from Windows)
1 parent 21ce60d commit e6337ef

10 files changed

Lines changed: 121 additions & 45 deletions

File tree

src-tauri/src/commands/agents.rs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1598,21 +1598,35 @@ pub async fn get_claude_binary_path(db: State<'_, AgentDb>) -> Result<Option<Str
15981598
pub async fn set_claude_binary_path(db: State<'_, AgentDb>, path: String) -> Result<(), String> {
15991599
let conn = db.0.lock().map_err(|e| e.to_string())?;
16001600

1601-
// Validate that the path exists and is executable
1602-
let path_buf = std::path::PathBuf::from(&path);
1603-
if !path_buf.exists() {
1604-
return Err(format!("File does not exist: {}", path));
1605-
}
1601+
// Check if shell environment is WSL - skip validation for WSL paths
1602+
// since Linux paths won't exist on the Windows filesystem
1603+
let is_wsl_environment = conn
1604+
.query_row(
1605+
"SELECT value FROM app_settings WHERE key = 'shell_environment'",
1606+
[],
1607+
|row| row.get::<_, String>(0),
1608+
)
1609+
.map(|env| env == "wsl")
1610+
.unwrap_or(false);
1611+
1612+
// Only validate path exists if not using WSL environment
1613+
// WSL paths like /home/user/.nvm/.../claude won't exist from Windows perspective
1614+
if !is_wsl_environment {
1615+
let path_buf = std::path::PathBuf::from(&path);
1616+
if !path_buf.exists() {
1617+
return Err(format!("File does not exist: {}", path));
1618+
}
16061619

1607-
// Check if it's executable (on Unix systems)
1608-
#[cfg(unix)]
1609-
{
1610-
use std::os::unix::fs::PermissionsExt;
1611-
let metadata = std::fs::metadata(&path_buf)
1612-
.map_err(|e| format!("Failed to read file metadata: {}", e))?;
1613-
let permissions = metadata.permissions();
1614-
if permissions.mode() & 0o111 == 0 {
1615-
return Err(format!("File is not executable: {}", path));
1620+
// Check if it's executable (on Unix systems)
1621+
#[cfg(unix)]
1622+
{
1623+
use std::os::unix::fs::PermissionsExt;
1624+
let metadata = std::fs::metadata(&path_buf)
1625+
.map_err(|e| format!("Failed to read file metadata: {}", e))?;
1626+
let permissions = metadata.permissions();
1627+
if permissions.mode() & 0o111 == 0 {
1628+
return Err(format!("File is not executable: {}", path));
1629+
}
16161630
}
16171631
}
16181632

src/components/AgentExecution.tsx

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
469469
const handleCopyAsMarkdown = async () => {
470470
let markdown = `# Agent Execution: ${agent.name}\n\n`;
471471
markdown += `**Task:** ${task}\n`;
472-
markdown += `**Model:** ${model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}\n`;
472+
markdown += `**Model:** ${model === 'opus' ? 'Claude Opus' : model === 'haiku' ? 'Claude Haiku' : 'Claude Sonnet'}\n`;
473473
markdown += `**Date:** ${new Date().toISOString()}\n\n`;
474474
markdown += `---\n\n`;
475475

@@ -553,7 +553,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
553553
<div>
554554
<h1 className="text-heading-1">{agent.name}</h1>
555555
<p className="mt-1 text-body-small text-muted-foreground">
556-
{isRunning ? 'Running' : messages.length > 0 ? 'Complete' : 'Ready'}{model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}
556+
{isRunning ? 'Running' : messages.length > 0 ? 'Complete' : 'Ready'}{model === 'opus' ? 'Claude Opus' : model === 'haiku' ? 'Claude Haiku' : 'Claude Sonnet'}
557557
</p>
558558
</div>
559559
</div>
@@ -616,10 +616,10 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
616616
<div className="w-2 h-2 rounded-full bg-primary" />
617617
)}
618618
</div>
619-
<div className="text-left">
620-
<div className="text-body-small font-medium">Claude 4 Sonnet</div>
621-
<div className="text-caption text-muted-foreground">Faster, efficient</div>
622-
</div>
619+
<div className="text-left">
620+
<div className="text-body-small font-medium">Claude Sonnet</div>
621+
<div className="text-caption text-muted-foreground">Balanced performance</div>
622+
</div>
623623
</div>
624624
</motion.button>
625625

@@ -646,9 +646,39 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
646646
<div className="w-2 h-2 rounded-full bg-primary" />
647647
)}
648648
</div>
649+
<div className="text-left">
650+
<div className="text-body-small font-medium">Claude Opus</div>
651+
<div className="text-caption text-muted-foreground">Most capable</div>
652+
</div>
653+
</div>
654+
</motion.button>
655+
656+
<motion.button
657+
type="button"
658+
onClick={() => !isRunning && setModel("haiku")}
659+
whileTap={{ scale: 0.97 }}
660+
transition={{ duration: 0.15 }}
661+
className={cn(
662+
"flex-1 px-4 py-3 rounded-md border transition-all",
663+
model === "haiku"
664+
? "border-primary bg-primary/10 text-primary"
665+
: "border-border hover:border-primary/50 hover:bg-accent",
666+
isRunning && "opacity-50 cursor-not-allowed"
667+
)}
668+
disabled={isRunning}
669+
>
670+
<div className="flex items-center gap-3">
671+
<div className={cn(
672+
"w-4 h-4 rounded-full border-2 flex items-center justify-center",
673+
model === "haiku" ? "border-primary" : "border-muted-foreground"
674+
)}>
675+
{model === "haiku" && (
676+
<div className="w-2 h-2 rounded-full bg-primary" />
677+
)}
678+
</div>
649679
<div className="text-left">
650-
<div className="text-body-small font-medium">Claude 4 Opus</div>
651-
<div className="text-caption text-muted-foreground">More capable</div>
680+
<div className="text-body-small font-medium">Claude Haiku</div>
681+
<div className="text-caption text-muted-foreground">Fast & efficient</div>
652682
</div>
653683
</div>
654684
</motion.button>

src/components/AgentRunOutputViewer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ export function AgentRunOutputViewer({
330330
if (!run) return;
331331
let markdown = `# Agent Execution: ${run.agent_name}\n\n`;
332332
markdown += `**Task:** ${run.task}\n`;
333-
markdown += `**Model:** ${run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}\n`;
333+
markdown += `**Model:** ${run.model === 'opus' ? 'Claude Opus' : run.model === 'haiku' ? 'Claude Haiku' : 'Claude Sonnet'}\n`;
334334
markdown += `**Date:** ${formatISOTimestamp(run.created_at)}\n`;
335335
if (run.metrics?.duration_ms) markdown += `**Duration:** ${(run.metrics.duration_ms / 1000).toFixed(2)}s\n`;
336336
if (run.metrics?.total_tokens) markdown += `**Total Tokens:** ${run.metrics.total_tokens}\n`;
@@ -566,7 +566,7 @@ export function AgentRunOutputViewer({
566566
</p>
567567
<div className="flex items-center gap-3 text-xs text-muted-foreground mt-2">
568568
<Badge variant="outline" className="text-xs">
569-
{run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}
569+
{run.model === 'opus' ? 'Claude Opus' : run.model === 'haiku' ? 'Claude Haiku' : 'Claude Sonnet'}
570570
</Badge>
571571
<div className="flex items-center gap-1">
572572
<Clock className="h-3 w-3" />

src/components/AgentRunView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ export const AgentRunView: React.FC<AgentRunViewProps> = ({
327327
<h3 className="text-sm font-medium">Task:</h3>
328328
<p className="text-sm text-muted-foreground flex-1">{run.task}</p>
329329
<Badge variant="outline" className="text-xs">
330-
{run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}
330+
{run.model === 'opus' ? 'Claude Opus' : run.model === 'haiku' ? 'Claude Haiku' : 'Claude Sonnet'}
331331
</Badge>
332332
</div>
333333

src/components/AgentsModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ export const AgentsModal: React.FC<AgentsModalProps> = ({ open, onOpenChange })
378378
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
379379
<span>Started: {formatISOTimestamp(run.created_at)}</span>
380380
<Badge variant="outline" className="text-xs">
381-
{run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}
381+
{run.model === 'opus' ? 'Claude Opus' : run.model === 'haiku' ? 'Claude Haiku' : 'Claude Sonnet'}
382382
</Badge>
383383
</div>
384384
</div>

src/components/ClaudeCodeSession.refactored.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
5151
const [showSlashCommandsSettings, setShowSlashCommandsSettings] = useState(false);
5252
const [forkCheckpointId, setForkCheckpointId] = useState<string | null>(null);
5353
const [forkSessionName, setForkSessionName] = useState("");
54-
const [queuedPrompts, setQueuedPrompts] = useState<Array<{ id: string; prompt: string; model: "sonnet" | "opus" }>>([]);
54+
const [queuedPrompts, setQueuedPrompts] = useState<Array<{ id: string; prompt: string; model: "sonnet" | "opus" | "haiku" }>>([]);
5555
const [showPreview, setShowPreview] = useState(false);
5656
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
5757
const [isPreviewMaximized, setIsPreviewMaximized] = useState(false);
@@ -106,7 +106,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
106106
};
107107

108108
// Handle sending prompts
109-
const handleSendPrompt = useCallback(async (prompt: string, model: "sonnet" | "opus") => {
109+
const handleSendPrompt = useCallback(async (prompt: string, model: "sonnet" | "opus" | "haiku") => {
110110
console.log('[TRACE] handleSendPrompt called:');
111111
console.log('[TRACE] prompt length:', prompt.length);
112112
console.log('[TRACE] model:', model);

src/components/ClaudeCodeSession.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
124124
const [forkSessionName, setForkSessionName] = useState("");
125125

126126
// Queued prompts state
127-
const [queuedPrompts, setQueuedPrompts] = useState<Array<{ id: string; prompt: string; model: "sonnet" | "opus" }>>([]);
127+
const [queuedPrompts, setQueuedPrompts] = useState<Array<{ id: string; prompt: string; model: "sonnet" | "opus" | "haiku" }>>([]);
128128

129129
// New state for preview feature
130130
const [showPreview, setShowPreview] = useState(false);
@@ -140,7 +140,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
140140
const unlistenRefs = useRef<UnlistenFn[]>([]);
141141
const hasActiveSessionRef = useRef(false);
142142
const floatingPromptRef = useRef<FloatingPromptInputRef>(null);
143-
const queuedPromptsRef = useRef<Array<{ id: string; prompt: string; model: "sonnet" | "opus" }>>([]);
143+
const queuedPromptsRef = useRef<Array<{ id: string; prompt: string; model: "sonnet" | "opus" | "haiku" }>>([]);
144144
const isMountedRef = useRef(true);
145145
const isListeningRef = useRef(false);
146146
const sessionStartTime = useRef<number>(Date.now());
@@ -486,7 +486,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
486486

487487
// Project path selection handled by parent tab controls
488488

489-
const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => {
489+
const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus" | "haiku") => {
490490
console.log('[ClaudeCodeSession] handleSendPrompt called with:', { prompt, model, projectPath, claudeSessionId, effectiveSession });
491491

492492
if (!projectPath) {

src/components/CreateAgent.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,8 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
257257
model === "sonnet" ? "text-primary" : "text-muted-foreground"
258258
)} />
259259
<div className="text-left">
260-
<div className="text-body-small font-medium">Claude 4 Sonnet</div>
261-
<div className="text-caption text-muted-foreground">Faster, efficient for most tasks</div>
260+
<div className="text-body-small font-medium">Claude Sonnet</div>
261+
<div className="text-caption text-muted-foreground">Balanced performance</div>
262262
</div>
263263
</div>
264264
</motion.button>
@@ -281,8 +281,32 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
281281
model === "opus" ? "text-primary" : "text-muted-foreground"
282282
)} />
283283
<div className="text-left">
284-
<div className="text-body-small font-medium">Claude 4 Opus</div>
285-
<div className="text-caption text-muted-foreground">More capable, better for complex tasks</div>
284+
<div className="text-body-small font-medium">Claude Opus</div>
285+
<div className="text-caption text-muted-foreground">Most capable, complex tasks</div>
286+
</div>
287+
</div>
288+
</motion.button>
289+
290+
<motion.button
291+
type="button"
292+
onClick={() => setModel("haiku")}
293+
whileTap={{ scale: 0.97 }}
294+
transition={{ duration: 0.15 }}
295+
className={cn(
296+
"flex-1 px-4 py-3 rounded-md border transition-all",
297+
model === "haiku"
298+
? "border-primary bg-primary/10 text-primary"
299+
: "border-border hover:border-primary/50 hover:bg-accent"
300+
)}
301+
>
302+
<div className="flex items-center gap-3">
303+
<Zap className={cn(
304+
"h-4 w-4",
305+
model === "haiku" ? "text-primary" : "text-muted-foreground"
306+
)} />
307+
<div className="text-left">
308+
<div className="text-body-small font-medium">Claude Haiku</div>
309+
<div className="text-caption text-muted-foreground">Fast & efficient</div>
286310
</div>
287311
</div>
288312
</motion.button>

src/components/FloatingPromptInput.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ interface FloatingPromptInputProps {
4141
/**
4242
* Callback when prompt is sent
4343
*/
44-
onSend: (prompt: string, model: "sonnet" | "opus") => void;
44+
onSend: (prompt: string, model: "sonnet" | "opus" | "haiku") => void;
4545
/**
4646
* Whether the input is loading
4747
*/
@@ -53,7 +53,7 @@ interface FloatingPromptInputProps {
5353
/**
5454
* Default model to select
5555
*/
56-
defaultModel?: "sonnet" | "opus";
56+
defaultModel?: "sonnet" | "opus" | "haiku";
5757
/**
5858
* Project path for file picker
5959
*/
@@ -173,7 +173,7 @@ const ThinkingModeIndicator: React.FC<{ level: number; color?: string }> = ({ le
173173
};
174174

175175
type Model = {
176-
id: "sonnet" | "opus";
176+
id: "sonnet" | "opus" | "haiku";
177177
name: string;
178178
description: string;
179179
icon: React.ReactNode;
@@ -184,19 +184,27 @@ type Model = {
184184
const MODELS: Model[] = [
185185
{
186186
id: "sonnet",
187-
name: "Claude 4 Sonnet",
188-
description: "Faster, efficient for most tasks",
187+
name: "Claude Sonnet",
188+
description: "Best balance of speed and capability",
189189
icon: <Zap className="h-3.5 w-3.5" />,
190190
shortName: "S",
191191
color: "text-primary"
192192
},
193193
{
194194
id: "opus",
195-
name: "Claude 4 Opus",
196-
description: "More capable, better for complex tasks",
195+
name: "Claude Opus",
196+
description: "Most capable, best for complex tasks",
197197
icon: <Zap className="h-3.5 w-3.5" />,
198198
shortName: "O",
199199
color: "text-primary"
200+
},
201+
{
202+
id: "haiku",
203+
name: "Claude Haiku",
204+
description: "Fastest, best for simple tasks",
205+
icon: <Zap className="h-3.5 w-3.5" />,
206+
shortName: "H",
207+
color: "text-primary"
200208
}
201209
];
202210

@@ -225,7 +233,7 @@ const FloatingPromptInputInner = (
225233
ref: React.Ref<FloatingPromptInputRef>,
226234
) => {
227235
const [prompt, setPrompt] = useState("");
228-
const [selectedModel, setSelectedModel] = useState<"sonnet" | "opus">(defaultModel);
236+
const [selectedModel, setSelectedModel] = useState<"sonnet" | "opus" | "haiku">(defaultModel);
229237
const [selectedThinkingMode, setSelectedThinkingMode] = useState<ThinkingMode>("auto");
230238
const [isExpanded, setIsExpanded] = useState(false);
231239
const [modelPickerOpen, setModelPickerOpen] = useState(false);

src/components/claude-code-session/PromptQueue.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { cn } from '@/lib/utils';
88
interface QueuedPrompt {
99
id: string;
1010
prompt: string;
11-
model: "sonnet" | "opus";
11+
model: "sonnet" | "opus" | "haiku";
1212
}
1313

1414
interface PromptQueueProps {
@@ -62,7 +62,7 @@ export const PromptQueue: React.FC<PromptQueueProps> = React.memo(({
6262
<div className="flex-1 min-w-0">
6363
<p className="text-sm truncate">{queuedPrompt.prompt}</p>
6464
<span className="text-xs text-muted-foreground">
65-
{queuedPrompt.model === "opus" ? "Opus" : "Sonnet"}
65+
{queuedPrompt.model === "opus" ? "Opus" : queuedPrompt.model === "haiku" ? "Haiku" : "Sonnet"}
6666
</span>
6767
</div>
6868

0 commit comments

Comments
 (0)