Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
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
27,309 changes: 12,443 additions & 14,866 deletions pnpm-lock.yaml

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.

Looks like this has regressed to lockfileVersion: '6.0'. Can you update your pnpm so we continue using version 9.0 (e.g. at main)

Large diffs are not rendered by default.

205 changes: 203 additions & 2 deletions src-tauri/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,12 +761,213 @@ 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()
}))
}

#[tauri::command]
pub async fn check_claude_code_available() -> Result<serde_json::Value, String> {
use std::process::Command;

let result = tauri::async_runtime::spawn_blocking(|| {
let which_output = Command::new("which")
.arg("claude")
.output();

let cli_exists = match which_output {
Ok(output) => output.status.success(),
Err(_) => false,
};

if !cli_exists {
return serde_json::json!({
"available": false,
"version": null,
"authenticated": false
});
}

let version_output = Command::new("claude")
.arg("--version")
.output();

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

serde_json::json!({
"available": true,
"version": version,
"authenticated": true
})
}).await.map_err(|e| format!("Failed to check Claude Code availability: {}", e))?;

Ok(result)
}

/// 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