Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 195 additions & 2 deletions src-tauri/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,12 +761,205 @@ pub async fn write_file_async(path: String, content: Option<Vec<u8>>, source_pat
#[tauri::command]
pub fn get_file_metadata(path: String) -> Result<serde_json::Value, String> {
use std::fs;

let metadata = fs::metadata(&path).map_err(|e| e.to_string())?;

Ok(serde_json::json!({
"size": metadata.len(),
"isFile": metadata.is_file(),
"isDirectory": metadata.is_dir()
}))
}

/// Check if Claude Code CLI is installed and authenticated
#[tauri::command]
pub fn check_claude_code_available() -> Result<serde_json::Value, String> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: When attempting this locally without claude code installed, it seems like I was able to select the model and try running it, and I didn't get an error.

$ which claude
claude not found
Screenshot 2025-12-15 at 7 25 59 PM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-12-16 at 09 07 38

Fixed this - now you cant select the claude code option if you dont have claude code.

use std::process::Command;

// Check if claude CLI exists
let version_output = Command::new("claude")
.arg("--version")
.output();

match version_output {
Ok(output) if output.status.success() => {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();

// Check if credentials file exists
let home = std::env::var("HOME").unwrap_or_default();
let credentials_path = format!("{}/.claude/.credentials.json", home);
let has_credentials = std::path::Path::new(&credentials_path).exists();

Ok(serde_json::json!({
"available": true,
"version": version,
"authenticated": has_credentials
}))
}
_ => {
Ok(serde_json::json!({
"available": false,
"version": null,
"authenticated": false
}))
}
}
}

/// Stream a response from Claude Code CLI
/// This spawns the claude CLI process and emits events as responses stream in
#[tauri::command]
pub async fn stream_claude_code_response(
app_handle: AppHandle,
request_id: String,
prompt: String,
system_prompt: Option<String>,
model: Option<String>,
disable_project_context: Option<bool>,
) -> Result<(), String> {
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};

// Build the command
let mut cmd = Command::new("claude");
cmd.arg("-p") // Print mode (non-interactive)
.arg("--output-format")
.arg("stream-json")
.arg("--verbose")
.arg("--permission-mode")
.arg("bypassPermissions"); // Auto-approve for chat use

// If we want to disable project context, run without tools and from home directory
// This makes Claude act as a general assistant without codebase awareness
let disable_context = disable_project_context.unwrap_or(true);
if disable_context {
// Disable all tools to prevent reading project files
cmd.arg("--tools").arg("");
// Only load user settings, not project-specific ones
cmd.arg("--setting-sources").arg("user");
// Run from home directory to avoid picking up any project context
if let Ok(home) = std::env::var("HOME") {
cmd.current_dir(home);
}
}

// Add model if specified
if let Some(m) = model {
cmd.arg("--model").arg(m);
}

// Add system prompt - use append to keep Claude Code's base capabilities
// but add our general assistant instructions
if let Some(sp) = system_prompt {
if !sp.is_empty() {
cmd.arg("--append-system-prompt").arg(sp);
}
} else if disable_context {
// Default prompt for general assistant mode
cmd.arg("--append-system-prompt")
.arg("You are a helpful general assistant. Answer questions directly and helpfully. Do not reference any specific project or codebase context.");
}

// Add the prompt as the final argument
cmd.arg(&prompt);

// Set up stdio
cmd.stdout(Stdio::piped())
.stderr(Stdio::piped());

// Spawn the process
let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn claude CLI: {}", e))?;

let stdout = child.stdout.take().ok_or("Failed to capture stdout")?;
let stderr = child.stderr.take().ok_or("Failed to capture stderr")?;

let request_id_clone = request_id.clone();
let app_handle_clone = app_handle.clone();

// Spawn a thread to read stdout and emit events
std::thread::spawn(move || {
let reader = BufReader::new(stdout);

for line in reader.lines() {
match line {
Ok(json_line) => {
if !json_line.is_empty() {
// Emit each JSON line as an event
let _ = app_handle_clone.emit(
&format!("claude-code-stream-{}", request_id_clone),
serde_json::json!({
"type": "data",
"data": json_line
})
);
}
}
Err(e) => {
let _ = app_handle_clone.emit(
&format!("claude-code-stream-{}", request_id_clone),
serde_json::json!({
"type": "error",
"error": format!("Failed to read line: {}", e)
})
);
break;
}
}
}
});

// Spawn a thread to read stderr
let request_id_clone2 = request_id.clone();
let app_handle_clone2 = app_handle.clone();

std::thread::spawn(move || {
let reader = BufReader::new(stderr);
let mut stderr_content = String::new();

for line in reader.lines() {
if let Ok(l) = line {
stderr_content.push_str(&l);
stderr_content.push('\n');
}
}

if !stderr_content.is_empty() {
let _ = app_handle_clone2.emit(
&format!("claude-code-stream-{}", request_id_clone2),
serde_json::json!({
"type": "stderr",
"data": stderr_content
})
);
}
});

// Wait for the process to complete in a separate task
let request_id_clone3 = request_id.clone();
let app_handle_clone3 = app_handle.clone();

tauri::async_runtime::spawn_blocking(move || {
Comment thread
bcongdon marked this conversation as resolved.
Outdated
match child.wait() {
Ok(status) => {
let _ = app_handle_clone3.emit(
&format!("claude-code-stream-{}", request_id_clone3),
serde_json::json!({
"type": "done",
"exitCode": status.code()
})
);
}
Err(e) => {
let _ = app_handle_clone3.emit(
&format!("claude-code-stream-{}", request_id_clone3),
serde_json::json!({
"type": "error",
"error": format!("Process error: {}", e)
})
);
}
}
});

Ok(())
}
2 changes: 2 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@ pub fn run() {
command::get_instance_name,
command::write_file_async,
command::get_file_metadata,
command::check_claude_code_available,
command::stream_claude_code_response,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
15 changes: 15 additions & 0 deletions src-tauri/src/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2458,5 +2458,20 @@ You have full access to bash commands on the user''''s computer. If you write a
('selected_model_configs_compare', '["openrouter::anthropic/claude-opus-4.5"]');
"#,
},
Migration {
version: 132,
description: "add claude code provider model for using claude code subscription",
kind: MigrationKind::Up,
sql: r#"
-- Add Claude (via Claude Code) model
-- This uses the local Claude Code CLI and subscription instead of an API key
INSERT OR REPLACE INTO models (id, display_name, is_enabled, supported_attachment_types) VALUES
('claude-code::default', 'Claude (via Claude Code)', 1, '["text", "webpage"]');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how you plan on using this, but do you think it's worth having the different models (Sonnet, Opus) exposed here?


-- Add Claude (via Claude Code) model config
INSERT OR REPLACE INTO model_configs (author, id, model_id, display_name, system_prompt, is_default, new_until) VALUES
('system', 'claude-code::default', 'claude-code::default', 'Claude (via Claude Code)', '', 0, '2026-01-15 00:00:00');
"#,
},
];
}
Loading