feat: add ability to configure the agent (also theme switcher) (#3)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 57s
CI / Lint & Test (push) Successful in 13m59s
CI / Build Linux (push) Successful in 16m25s
CI / Build Windows (cross-compile) (push) Successful in 26m30s

### Explanation

_No response_

### Issue

_No response_

### Attestations

- [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [ ] I have pinned the dependencies to a specific patch version.

### Style

- [ ] I have run the linter and resolved any errors.
- [ ] My pull request uses an appropriate title, matching the conventional commit standards.
- [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [ ] All new and existing tests pass locally with my changes.
- [ ] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

_No response_

Reviewed-on: #3
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #3.
This commit is contained in:
2026-01-16 11:56:17 -08:00
committed by Naomi Carrigan
parent c241544743
commit 2220c26c5e
13 changed files with 811 additions and 14 deletions
+95 -6
View File
@@ -4,10 +4,12 @@ use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::Arc;
use std::thread;
use tauri::{AppHandle, Emitter};
use tempfile::NamedTempFile;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use crate::config::ClaudeStartOptions;
use crate::types::{CharacterState, ClaudeMessage, ConnectionStatus, ContentBlock, StateChangeEvent, OutputEvent, PermissionPromptEvent};
const SEARCH_TOOLS: [&str; 5] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"];
@@ -69,6 +71,7 @@ pub struct WslBridge {
stdin: Option<ChildStdin>,
working_directory: String,
session_id: Option<String>,
mcp_config_file: Option<NamedTempFile>,
}
impl WslBridge {
@@ -78,22 +81,50 @@ impl WslBridge {
stdin: None,
working_directory: String::new(),
session_id: None,
mcp_config_file: None,
}
}
pub fn start(&mut self, app: AppHandle, working_dir: &str, allowed_tools: Vec<String>) -> Result<(), String> {
pub fn start(&mut self, app: AppHandle, options: ClaudeStartOptions) -> Result<(), String> {
if self.process.is_some() {
return Err("Process already running".to_string());
}
self.working_directory = working_dir.to_string();
let working_dir = &options.working_dir;
self.working_directory = working_dir.clone();
emit_connection_status(&app, ConnectionStatus::Connecting);
// Create temp file for MCP config if provided
let mcp_config_path = if let Some(ref mcp_json) = options.mcp_servers_json {
if !mcp_json.trim().is_empty() {
// Validate JSON before writing
serde_json::from_str::<serde_json::Value>(mcp_json)
.map_err(|e| format!("Invalid MCP servers JSON: {}", e))?;
let mut temp_file = NamedTempFile::new()
.map_err(|e| format!("Failed to create temp file for MCP config: {}", e))?;
temp_file
.write_all(mcp_json.as_bytes())
.map_err(|e| format!("Failed to write MCP config: {}", e))?;
temp_file
.flush()
.map_err(|e| format!("Failed to flush MCP config: {}", e))?;
let path = temp_file.path().to_string_lossy().to_string();
self.mcp_config_file = Some(temp_file);
Some(path)
} else {
None
}
} else {
None
};
// Detect if we're running inside WSL or on Windows
let is_wsl = detect_wsl();
eprintln!("[DEBUG] is_wsl: {}", is_wsl);
eprintln!("[DEBUG] allowed_tools: {:?}", allowed_tools);
eprintln!("[DEBUG] options: {:?}", options);
let mut command = if is_wsl {
// Running inside WSL - call claude directly
@@ -111,12 +142,39 @@ impl WslBridge {
"--verbose",
]);
// Add model if specified
if let Some(ref model) = options.model {
if !model.is_empty() {
cmd.args(["--model", model]);
}
}
// Add allowed tools if any
for tool in &allowed_tools {
for tool in &options.allowed_tools {
cmd.args(["--allowedTools", tool]);
}
// Add custom instructions as system prompt if specified
if let Some(ref instructions) = options.custom_instructions {
if !instructions.is_empty() {
cmd.args(["--system-prompt", instructions]);
}
}
// Add MCP config if provided
if let Some(ref mcp_path) = mcp_config_path {
cmd.args(["--mcp-config", mcp_path]);
}
cmd.current_dir(working_dir);
// Set API key as environment variable if specified
if let Some(ref api_key) = options.api_key {
if !api_key.is_empty() {
cmd.env("ANTHROPIC_API_KEY", api_key);
}
}
cmd
} else {
// Running on Windows - use wsl with bash login shell to ensure PATH is loaded
@@ -125,15 +183,45 @@ impl WslBridge {
// Build the claude command with all arguments
let mut claude_cmd = format!(
"cd '{}' && claude --output-format stream-json --input-format stream-json --verbose",
"cd '{}' && ",
working_dir
);
// Set API key as environment variable if specified
if let Some(ref api_key) = options.api_key {
if !api_key.is_empty() {
claude_cmd.push_str(&format!("ANTHROPIC_API_KEY='{}' ", api_key));
}
}
claude_cmd.push_str("claude --output-format stream-json --input-format stream-json --verbose");
// Add model if specified
if let Some(ref model) = options.model {
if !model.is_empty() {
claude_cmd.push_str(&format!(" --model '{}'", model));
}
}
// Add allowed tools if any
for tool in &allowed_tools {
for tool in &options.allowed_tools {
claude_cmd.push_str(&format!(" --allowedTools '{}'", tool));
}
// Add custom instructions as system prompt if specified
if let Some(ref instructions) = options.custom_instructions {
if !instructions.is_empty() {
// Escape single quotes in instructions
let escaped = instructions.replace('\'', "'\\''");
claude_cmd.push_str(&format!(" --system-prompt '{}'", escaped));
}
}
// Add MCP config if provided
if let Some(ref mcp_path) = mcp_config_path {
claude_cmd.push_str(&format!(" --mcp-config '{}'", mcp_path));
}
// Use bash -lc to load login profile (ensures PATH includes claude)
cmd.args(["-e", "bash", "-lc", &claude_cmd]);
@@ -212,6 +300,7 @@ impl WslBridge {
}
self.stdin = None;
self.session_id = None;
self.mcp_config_file = None; // Temp file is automatically deleted when dropped
emit_connection_status(app, ConnectionStatus::Disconnected);
}