generated from nhcarrigan/template
feat: add ability to configure the agent (also theme switcher) #3
+2
-1
@@ -26,7 +26,8 @@
|
|||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2",
|
"@tauri-apps/plugin-dialog": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.4"
|
"@tauri-apps/plugin-shell": "^2.3.4",
|
||||||
|
"@tauri-apps/plugin-store": "^2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
|
|||||||
Generated
+10
@@ -20,6 +20,9 @@ importers:
|
|||||||
'@tauri-apps/plugin-shell':
|
'@tauri-apps/plugin-shell':
|
||||||
specifier: ^2.3.4
|
specifier: ^2.3.4
|
||||||
version: 2.3.4
|
version: 2.3.4
|
||||||
|
'@tauri-apps/plugin-store':
|
||||||
|
specifier: ^2
|
||||||
|
version: 2.4.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.39.2
|
specifier: ^9.39.2
|
||||||
@@ -729,6 +732,9 @@ packages:
|
|||||||
'@tauri-apps/plugin-shell@2.3.4':
|
'@tauri-apps/plugin-shell@2.3.4':
|
||||||
resolution: {integrity: sha512-ktsRWf8wHLD17aZEyqE8c5x98eNAuTizR1FSX475zQ4TxaiJnhwksLygQz+AGwckJL5bfEP13nWrlTNQJUpKpA==}
|
resolution: {integrity: sha512-ktsRWf8wHLD17aZEyqE8c5x98eNAuTizR1FSX475zQ4TxaiJnhwksLygQz+AGwckJL5bfEP13nWrlTNQJUpKpA==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-store@2.4.2':
|
||||||
|
resolution: {integrity: sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A==}
|
||||||
|
|
||||||
'@testing-library/dom@10.4.1':
|
'@testing-library/dom@10.4.1':
|
||||||
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2265,6 +2271,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.9.1
|
'@tauri-apps/api': 2.9.1
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-store@2.4.2':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.9.1
|
||||||
|
|
||||||
'@testing-library/dom@10.4.1':
|
'@testing-library/dom@10.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.28.6
|
'@babel/code-frame': 7.28.6
|
||||||
|
|||||||
Generated
+18
@@ -1403,6 +1403,8 @@ dependencies = [
|
|||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
|
"tauri-plugin-store",
|
||||||
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
@@ -3761,6 +3763,22 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-store"
|
||||||
|
version = "2.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ca1a8ff83c269b115e98726ffc13f9e548a10161544a92ad121d6d0a96e16ea"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.9.2"
|
version = "2.9.2"
|
||||||
|
|||||||
@@ -22,4 +22,6 @@ serde_json = "1"
|
|||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
tauri-plugin-store = "2.4.2"
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
use tauri::{AppHandle, State};
|
use tauri::{AppHandle, State};
|
||||||
|
use tauri_plugin_store::StoreExt;
|
||||||
|
|
||||||
|
use crate::config::{ClaudeStartOptions, HikariConfig};
|
||||||
use crate::wsl_bridge::SharedBridge;
|
use crate::wsl_bridge::SharedBridge;
|
||||||
|
|
||||||
|
const CONFIG_STORE_KEY: &str = "config";
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn start_claude(
|
pub async fn start_claude(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
bridge: State<'_, SharedBridge>,
|
bridge: State<'_, SharedBridge>,
|
||||||
working_dir: String,
|
options: ClaudeStartOptions,
|
||||||
allowed_tools: Option<Vec<String>>,
|
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut bridge = bridge.lock();
|
let mut bridge = bridge.lock();
|
||||||
bridge.start(app, &working_dir, allowed_tools.unwrap_or_default())
|
bridge.start(app, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -42,3 +45,30 @@ pub async fn get_working_directory(bridge: State<'_, SharedBridge>) -> Result<St
|
|||||||
pub async fn select_wsl_directory() -> Result<String, String> {
|
pub async fn select_wsl_directory() -> Result<String, String> {
|
||||||
Ok("/home".to_string())
|
Ok("/home".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_config(app: AppHandle) -> Result<HikariConfig, String> {
|
||||||
|
let store = app
|
||||||
|
.store("hikari-config.json")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
match store.get(CONFIG_STORE_KEY) {
|
||||||
|
Some(value) => {
|
||||||
|
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
None => Ok(HikariConfig::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_config(app: AppHandle, config: HikariConfig) -> Result<(), String> {
|
||||||
|
let store = app
|
||||||
|
.store("hikari-config.json")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let value = serde_json::to_value(&config).map_err(|e| e.to_string())?;
|
||||||
|
store.set(CONFIG_STORE_KEY, value);
|
||||||
|
store.save().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct ClaudeStartOptions {
|
||||||
|
#[serde(default)]
|
||||||
|
pub working_dir: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub model: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub api_key: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub custom_instructions: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub mcp_servers_json: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub allowed_tools: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct HikariConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub model: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub api_key: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub custom_instructions: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub mcp_servers_json: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub auto_granted_tools: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub theme: Theme,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Theme {
|
||||||
|
#[default]
|
||||||
|
Dark,
|
||||||
|
Light,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_config() {
|
||||||
|
let config = HikariConfig::default();
|
||||||
|
assert!(config.model.is_none());
|
||||||
|
assert!(config.api_key.is_none());
|
||||||
|
assert!(config.custom_instructions.is_none());
|
||||||
|
assert!(config.mcp_servers_json.is_none());
|
||||||
|
assert!(config.auto_granted_tools.is_empty());
|
||||||
|
assert_eq!(config.theme, Theme::Dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_serialization() {
|
||||||
|
let config = HikariConfig {
|
||||||
|
model: Some("claude-sonnet-4-20250514".to_string()),
|
||||||
|
api_key: None,
|
||||||
|
custom_instructions: Some("Be helpful".to_string()),
|
||||||
|
mcp_servers_json: None,
|
||||||
|
auto_granted_tools: vec!["Read".to_string(), "Glob".to_string()],
|
||||||
|
theme: Theme::Light,
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&config).unwrap();
|
||||||
|
let deserialized: HikariConfig = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(deserialized.model, config.model);
|
||||||
|
assert_eq!(deserialized.custom_instructions, config.custom_instructions);
|
||||||
|
assert_eq!(deserialized.auto_granted_tools, config.auto_granted_tools);
|
||||||
|
assert_eq!(deserialized.theme, Theme::Light);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_theme_serialization() {
|
||||||
|
let dark = Theme::Dark;
|
||||||
|
let light = Theme::Light;
|
||||||
|
|
||||||
|
assert_eq!(serde_json::to_string(&dark).unwrap(), "\"dark\"");
|
||||||
|
assert_eq!(serde_json::to_string(&light).unwrap(), "\"light\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod commands;
|
mod commands;
|
||||||
|
mod config;
|
||||||
mod types;
|
mod types;
|
||||||
mod wsl_bridge;
|
mod wsl_bridge;
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
.manage(bridge)
|
.manage(bridge)
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
start_claude,
|
start_claude,
|
||||||
@@ -21,6 +23,8 @@ pub fn run() {
|
|||||||
is_claude_running,
|
is_claude_running,
|
||||||
get_working_directory,
|
get_working_directory,
|
||||||
select_wsl_directory,
|
select_wsl_directory,
|
||||||
|
get_config,
|
||||||
|
save_config,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ use std::process::{Child, ChildStdin, Command, Stdio};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
|
use crate::config::ClaudeStartOptions;
|
||||||
use crate::types::{CharacterState, ClaudeMessage, ConnectionStatus, ContentBlock, StateChangeEvent, OutputEvent, PermissionPromptEvent};
|
use crate::types::{CharacterState, ClaudeMessage, ConnectionStatus, ContentBlock, StateChangeEvent, OutputEvent, PermissionPromptEvent};
|
||||||
|
|
||||||
const SEARCH_TOOLS: [&str; 5] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"];
|
const SEARCH_TOOLS: [&str; 5] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"];
|
||||||
@@ -69,6 +71,7 @@ pub struct WslBridge {
|
|||||||
stdin: Option<ChildStdin>,
|
stdin: Option<ChildStdin>,
|
||||||
working_directory: String,
|
working_directory: String,
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
|
mcp_config_file: Option<NamedTempFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WslBridge {
|
impl WslBridge {
|
||||||
@@ -78,22 +81,50 @@ impl WslBridge {
|
|||||||
stdin: None,
|
stdin: None,
|
||||||
working_directory: String::new(),
|
working_directory: String::new(),
|
||||||
session_id: None,
|
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() {
|
if self.process.is_some() {
|
||||||
return Err("Process already running".to_string());
|
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);
|
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
|
// Detect if we're running inside WSL or on Windows
|
||||||
let is_wsl = detect_wsl();
|
let is_wsl = detect_wsl();
|
||||||
eprintln!("[DEBUG] is_wsl: {}", is_wsl);
|
eprintln!("[DEBUG] is_wsl: {}", is_wsl);
|
||||||
eprintln!("[DEBUG] allowed_tools: {:?}", allowed_tools);
|
eprintln!("[DEBUG] options: {:?}", options);
|
||||||
|
|
||||||
let mut command = if is_wsl {
|
let mut command = if is_wsl {
|
||||||
// Running inside WSL - call claude directly
|
// Running inside WSL - call claude directly
|
||||||
@@ -111,12 +142,39 @@ impl WslBridge {
|
|||||||
"--verbose",
|
"--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
|
// Add allowed tools if any
|
||||||
for tool in &allowed_tools {
|
for tool in &options.allowed_tools {
|
||||||
cmd.args(["--allowedTools", tool]);
|
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);
|
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
|
cmd
|
||||||
} else {
|
} else {
|
||||||
// Running on Windows - use wsl with bash login shell to ensure PATH is loaded
|
// 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
|
// Build the claude command with all arguments
|
||||||
let mut claude_cmd = format!(
|
let mut claude_cmd = format!(
|
||||||
"cd '{}' && claude --output-format stream-json --input-format stream-json --verbose",
|
"cd '{}' && ",
|
||||||
working_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() {
|
||||||
|
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
|
// Add allowed tools if any
|
||||||
for tool in &allowed_tools {
|
for tool in &options.allowed_tools {
|
||||||
claude_cmd.push_str(&format!(" --allowedTools '{}'", tool));
|
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)
|
// Use bash -lc to load login profile (ensures PATH includes claude)
|
||||||
cmd.args(["-e", "bash", "-lc", &claude_cmd]);
|
cmd.args(["-e", "bash", "-lc", &claude_cmd]);
|
||||||
|
|
||||||
@@ -212,6 +300,7 @@ impl WslBridge {
|
|||||||
}
|
}
|
||||||
self.stdin = None;
|
self.stdin = None;
|
||||||
self.session_id = None;
|
self.session_id = None;
|
||||||
|
self.mcp_config_file = None; // Temp file is automatically deleted when dropped
|
||||||
emit_connection_status(app, ConnectionStatus::Disconnected);
|
emit_connection_status(app, ConnectionStatus::Disconnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -1,9 +1,11 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
:root,
|
||||||
|
[data-theme="dark"] {
|
||||||
--bg-primary: #1a1a2e;
|
--bg-primary: #1a1a2e;
|
||||||
--bg-secondary: #16213e;
|
--bg-secondary: #16213e;
|
||||||
--bg-terminal: #0f0f1a;
|
--bg-terminal: #0f0f1a;
|
||||||
|
--bg-hover: #2a2a4a;
|
||||||
--accent-primary: #e94560;
|
--accent-primary: #e94560;
|
||||||
--accent-secondary: #ff6b9d;
|
--accent-secondary: #ff6b9d;
|
||||||
--text-primary: #ffffff;
|
--text-primary: #ffffff;
|
||||||
@@ -11,6 +13,18 @@
|
|||||||
--border-color: #2a2a4a;
|
--border-color: #2a2a4a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] {
|
||||||
|
--bg-primary: #f8f9fa;
|
||||||
|
--bg-secondary: #ffffff;
|
||||||
|
--bg-terminal: #f1f3f4;
|
||||||
|
--bg-hover: #e8e8e8;
|
||||||
|
--accent-primary: #e94560;
|
||||||
|
--accent-secondary: #ff6b9d;
|
||||||
|
--text-primary: #1a1a2e;
|
||||||
|
--text-secondary: #5a5a7a;
|
||||||
|
--border-color: #d0d0e0;
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { configStore, type HikariConfig, type Theme } from "$lib/stores/config";
|
||||||
|
import { claudeStore } from "$lib/stores/claude";
|
||||||
|
|
||||||
|
let config: HikariConfig = $state({
|
||||||
|
model: null,
|
||||||
|
api_key: null,
|
||||||
|
custom_instructions: null,
|
||||||
|
mcp_servers_json: null,
|
||||||
|
auto_granted_tools: [],
|
||||||
|
theme: "dark",
|
||||||
|
});
|
||||||
|
|
||||||
|
let isOpen = $state(false);
|
||||||
|
let isSaving = $state(false);
|
||||||
|
let saveError: string | null = $state(null);
|
||||||
|
let newToolName = $state("");
|
||||||
|
let showApiKey = $state(false);
|
||||||
|
let grantedTools: string[] = $state([]);
|
||||||
|
|
||||||
|
configStore.config.subscribe((c) => {
|
||||||
|
config = { ...c };
|
||||||
|
});
|
||||||
|
|
||||||
|
configStore.isSidebarOpen.subscribe((open) => {
|
||||||
|
isOpen = open;
|
||||||
|
});
|
||||||
|
|
||||||
|
configStore.saveError.subscribe((error) => {
|
||||||
|
saveError = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
claudeStore.grantedTools.subscribe((tools) => {
|
||||||
|
grantedTools = Array.from(tools);
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableModels = [
|
||||||
|
{ value: "", label: "Default (from ~/.claude)" },
|
||||||
|
{ value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4" },
|
||||||
|
{ value: "claude-opus-4-20250514", label: "Claude Opus 4" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const commonTools = [
|
||||||
|
"Read",
|
||||||
|
"Write",
|
||||||
|
"Edit",
|
||||||
|
"Bash",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"WebFetch",
|
||||||
|
"WebSearch",
|
||||||
|
"Task",
|
||||||
|
];
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
isSaving = true;
|
||||||
|
saveError = null;
|
||||||
|
try {
|
||||||
|
await configStore.saveConfig(config);
|
||||||
|
} catch {
|
||||||
|
// Error is handled by the store
|
||||||
|
} finally {
|
||||||
|
isSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleThemeChange(theme: Theme) {
|
||||||
|
config.theme = theme;
|
||||||
|
await configStore.setTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTool(tool: string) {
|
||||||
|
if (config.auto_granted_tools.includes(tool)) {
|
||||||
|
config.auto_granted_tools = config.auto_granted_tools.filter((t) => t !== tool);
|
||||||
|
} else {
|
||||||
|
config.auto_granted_tools = [...config.auto_granted_tools, tool];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCustomTool() {
|
||||||
|
if (newToolName.trim() && !config.auto_granted_tools.includes(newToolName.trim())) {
|
||||||
|
config.auto_granted_tools = [...config.auto_granted_tools, newToolName.trim()];
|
||||||
|
newToolName = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTool(tool: string) {
|
||||||
|
config.auto_granted_tools = config.auto_granted_tools.filter((t) => t !== tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importFromSession() {
|
||||||
|
config.auto_granted_tools = [...new Set([...config.auto_granted_tools, ...grantedTools])];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Backdrop -->
|
||||||
|
{#if isOpen}
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-black/50 z-40 transition-opacity"
|
||||||
|
onclick={configStore.closeSidebar}
|
||||||
|
onkeydown={(e) => e.key === "Escape" && configStore.closeSidebar()}
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label="Close sidebar"
|
||||||
|
></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside
|
||||||
|
class="fixed right-0 top-0 h-full w-96 bg-[var(--bg-secondary)] border-l border-[var(--border-color)] z-50 transform transition-transform duration-300 ease-in-out overflow-y-auto {isOpen
|
||||||
|
? 'translate-x-0'
|
||||||
|
: 'translate-x-full'}"
|
||||||
|
>
|
||||||
|
<div class="p-4">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h2 class="text-lg font-semibold text-[var(--text-primary)]">Settings</h2>
|
||||||
|
<button
|
||||||
|
onclick={configStore.closeSidebar}
|
||||||
|
class="p-1 text-gray-400 hover:text-white transition-colors"
|
||||||
|
aria-label="Close settings"
|
||||||
|
>
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if saveError}
|
||||||
|
<div class="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400 text-sm">
|
||||||
|
{saveError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Agent Settings Section -->
|
||||||
|
<section class="mb-6">
|
||||||
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
||||||
|
Agent Settings
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Model Selection -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="model" class="block text-sm text-gray-400 mb-1">Model</label>
|
||||||
|
<select
|
||||||
|
id="model"
|
||||||
|
bind:value={config.model}
|
||||||
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
||||||
|
>
|
||||||
|
{#each availableModels as model (model.value)}
|
||||||
|
<option value={model.value}>{model.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Key -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="api-key" class="block text-sm text-gray-400 mb-1">
|
||||||
|
API Key <span class="text-gray-600">(optional override)</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
id="api-key"
|
||||||
|
type={showApiKey ? "text" : "password"}
|
||||||
|
bind:value={config.api_key}
|
||||||
|
placeholder="Falls back to ~/.claude settings"
|
||||||
|
class="w-full px-3 py-2 pr-10 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => (showApiKey = !showApiKey)}
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300"
|
||||||
|
aria-label={showApiKey ? "Hide API key" : "Show API key"}
|
||||||
|
>
|
||||||
|
{#if showApiKey}
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Instructions -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="instructions" class="block text-sm text-gray-400 mb-1"
|
||||||
|
>Custom Instructions</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="instructions"
|
||||||
|
bind:value={config.custom_instructions}
|
||||||
|
rows="4"
|
||||||
|
placeholder="Additional instructions for the agent..."
|
||||||
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)] resize-none"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- MCP Servers Section -->
|
||||||
|
<section class="mb-6">
|
||||||
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
||||||
|
MCP Servers
|
||||||
|
</h3>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="mcp-config" class="block text-sm text-gray-400 mb-1">
|
||||||
|
Server Configuration <span class="text-gray-600">(JSON)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="mcp-config"
|
||||||
|
bind:value={config.mcp_servers_json}
|
||||||
|
rows="6"
|
||||||
|
placeholder={`{\n "servers": {\n "example": {\n "command": "npx",\n "args": ["-y", "@example/mcp-server"]\n }\n }\n}`}
|
||||||
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] font-mono text-sm focus:outline-none focus:border-[var(--accent-primary)] resize-none"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Auto-Granted Tools Section -->
|
||||||
|
<section class="mb-6">
|
||||||
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
||||||
|
Auto-Granted Tools
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-gray-500 mb-3">
|
||||||
|
These tools will be pre-approved for every session (no permission prompts).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Common tools checkboxes -->
|
||||||
|
<div class="grid grid-cols-2 gap-2 mb-3">
|
||||||
|
{#each commonTools as tool (tool)}
|
||||||
|
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.auto_granted_tools.includes(tool)}
|
||||||
|
onchange={() => toggleTool(tool)}
|
||||||
|
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
||||||
|
/>
|
||||||
|
{tool}
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Currently granted tools (with import) -->
|
||||||
|
{#if grantedTools.length > 0}
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-xs text-gray-500">Session-granted tools:</span>
|
||||||
|
<button
|
||||||
|
onclick={importFromSession}
|
||||||
|
class="text-xs text-[var(--accent-primary)] hover:text-[var(--accent-secondary)] transition-colors"
|
||||||
|
>
|
||||||
|
Import all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
{#each grantedTools as tool (tool)}
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 text-xs bg-[var(--accent-primary)]/20 text-[var(--accent-primary)] rounded"
|
||||||
|
>
|
||||||
|
{tool}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Custom tools list -->
|
||||||
|
{#if config.auto_granted_tools.filter((t) => !commonTools.includes(t)).length > 0}
|
||||||
|
<div class="mb-3">
|
||||||
|
<span class="text-xs text-gray-500 block mb-2">Custom tools:</span>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
{#each config.auto_granted_tools.filter((t) => !commonTools.includes(t)) as tool (tool)}
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{tool}
|
||||||
|
<button
|
||||||
|
onclick={() => removeTool(tool)}
|
||||||
|
class="text-gray-500 hover:text-red-400"
|
||||||
|
aria-label="Remove {tool}"
|
||||||
|
>
|
||||||
|
Ć
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Add custom tool -->
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newToolName}
|
||||||
|
placeholder="Add custom tool..."
|
||||||
|
onkeydown={(e) => e.key === "Enter" && addCustomTool()}
|
||||||
|
class="flex-1 px-3 py-1.5 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onclick={addCustomTool}
|
||||||
|
disabled={!newToolName.trim()}
|
||||||
|
class="px-3 py-1.5 text-sm bg-[var(--accent-primary)] hover:bg-[var(--accent-secondary)] text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Appearance Section -->
|
||||||
|
<section class="mb-6">
|
||||||
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
||||||
|
Appearance
|
||||||
|
</h3>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
onclick={() => handleThemeChange("dark")}
|
||||||
|
class="flex-1 px-4 py-2 rounded-lg border transition-colors {config.theme === 'dark'
|
||||||
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
||||||
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-gray-400 hover:border-[var(--accent-primary)]'}"
|
||||||
|
>
|
||||||
|
Dark
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => handleThemeChange("light")}
|
||||||
|
class="flex-1 px-4 py-2 rounded-lg border transition-colors {config.theme === 'light'
|
||||||
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
||||||
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-gray-400 hover:border-[var(--accent-primary)]'}"
|
||||||
|
>
|
||||||
|
Light
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Save Button -->
|
||||||
|
<div class="sticky bottom-0 pt-4 pb-2 bg-[var(--bg-secondary)]">
|
||||||
|
<button
|
||||||
|
onclick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
class="w-full py-3 bg-[var(--accent-primary)] hover:bg-[var(--accent-secondary)] text-white font-medium rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSaving ? "Saving..." : "Save Settings"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
import { claudeStore } from "$lib/stores/claude";
|
import { claudeStore } from "$lib/stores/claude";
|
||||||
|
import { configStore, type HikariConfig } from "$lib/stores/config";
|
||||||
import type { ConnectionStatus } from "$lib/types/messages";
|
import type { ConnectionStatus } from "$lib/types/messages";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
@@ -15,6 +16,14 @@
|
|||||||
let isConnecting = $state(false);
|
let isConnecting = $state(false);
|
||||||
let grantedToolsList: string[] = $state([]);
|
let grantedToolsList: string[] = $state([]);
|
||||||
let appVersion = $state("");
|
let appVersion = $state("");
|
||||||
|
let currentConfig: HikariConfig = $state({
|
||||||
|
model: null,
|
||||||
|
api_key: null,
|
||||||
|
custom_instructions: null,
|
||||||
|
mcp_servers_json: null,
|
||||||
|
auto_granted_tools: [],
|
||||||
|
theme: "dark",
|
||||||
|
});
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
appVersion = await getVersion();
|
appVersion = await getVersion();
|
||||||
@@ -33,6 +42,10 @@
|
|||||||
grantedToolsList = Array.from(tools);
|
grantedToolsList = Array.from(tools);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
configStore.config.subscribe((config) => {
|
||||||
|
currentConfig = config;
|
||||||
|
});
|
||||||
|
|
||||||
async function handleBrowse() {
|
async function handleBrowse() {
|
||||||
try {
|
try {
|
||||||
const selected = await open({
|
const selected = await open({
|
||||||
@@ -54,11 +67,21 @@
|
|||||||
|
|
||||||
const targetDir = selectedDirectory || "/home/naomi";
|
const targetDir = selectedDirectory || "/home/naomi";
|
||||||
|
|
||||||
|
// Combine session-granted tools with config auto-granted tools
|
||||||
|
const allAllowedTools = [
|
||||||
|
...new Set([...grantedToolsList, ...currentConfig.auto_granted_tools]),
|
||||||
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Pass granted tools to Claude so they're pre-approved
|
|
||||||
await invoke("start_claude", {
|
await invoke("start_claude", {
|
||||||
workingDir: targetDir,
|
options: {
|
||||||
allowedTools: grantedToolsList.length > 0 ? grantedToolsList : null,
|
working_dir: targetDir,
|
||||||
|
model: currentConfig.model || null,
|
||||||
|
api_key: currentConfig.api_key || null,
|
||||||
|
custom_instructions: currentConfig.custom_instructions || null,
|
||||||
|
mcp_servers_json: currentConfig.mcp_servers_json || null,
|
||||||
|
allowed_tools: allAllowedTools,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start Claude:", error);
|
console.error("Failed to start Claude:", error);
|
||||||
@@ -140,6 +163,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onclick={configStore.openSidebar}
|
||||||
|
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
|
||||||
|
title="Settings"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => openUrl(DISCORD_URL)}
|
onclick={() => openUrl(DISCORD_URL)}
|
||||||
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
|
class="p-1 text-gray-500 hover:text-[var(--accent-primary)] transition-colors"
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { writable, derived } from "svelte/store";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
export type Theme = "dark" | "light";
|
||||||
|
|
||||||
|
export interface HikariConfig {
|
||||||
|
model: string | null;
|
||||||
|
api_key: string | null;
|
||||||
|
custom_instructions: string | null;
|
||||||
|
mcp_servers_json: string | null;
|
||||||
|
auto_granted_tools: string[];
|
||||||
|
theme: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultConfig: HikariConfig = {
|
||||||
|
model: null,
|
||||||
|
api_key: null,
|
||||||
|
custom_instructions: null,
|
||||||
|
mcp_servers_json: null,
|
||||||
|
auto_granted_tools: [],
|
||||||
|
theme: "dark",
|
||||||
|
};
|
||||||
|
|
||||||
|
function createConfigStore() {
|
||||||
|
const config = writable<HikariConfig>(defaultConfig);
|
||||||
|
const isLoading = writable<boolean>(true);
|
||||||
|
const isSidebarOpen = writable<boolean>(false);
|
||||||
|
const saveError = writable<string | null>(null);
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
isLoading.set(true);
|
||||||
|
try {
|
||||||
|
const savedConfig = await invoke<HikariConfig>("get_config");
|
||||||
|
config.set(savedConfig);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load config:", error);
|
||||||
|
config.set(defaultConfig);
|
||||||
|
} finally {
|
||||||
|
isLoading.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig(newConfig: HikariConfig) {
|
||||||
|
saveError.set(null);
|
||||||
|
try {
|
||||||
|
await invoke("save_config", { config: newConfig });
|
||||||
|
config.set(newConfig);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
saveError.set(errorMessage);
|
||||||
|
console.error("Failed to save config:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateConfig(updates: Partial<HikariConfig>) {
|
||||||
|
let currentConfig: HikariConfig = defaultConfig;
|
||||||
|
config.subscribe((c) => (currentConfig = c))();
|
||||||
|
const newConfig = { ...currentConfig, ...updates };
|
||||||
|
await saveConfig(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: { subscribe: config.subscribe },
|
||||||
|
isLoading: { subscribe: isLoading.subscribe },
|
||||||
|
isSidebarOpen: { subscribe: isSidebarOpen.subscribe },
|
||||||
|
saveError: { subscribe: saveError.subscribe },
|
||||||
|
|
||||||
|
loadConfig,
|
||||||
|
saveConfig,
|
||||||
|
updateConfig,
|
||||||
|
|
||||||
|
openSidebar: () => isSidebarOpen.set(true),
|
||||||
|
closeSidebar: () => isSidebarOpen.set(false),
|
||||||
|
toggleSidebar: () => isSidebarOpen.update((open) => !open),
|
||||||
|
|
||||||
|
setTheme: async (theme: Theme) => {
|
||||||
|
await updateConfig({ theme });
|
||||||
|
applyTheme(theme);
|
||||||
|
},
|
||||||
|
|
||||||
|
addAutoGrantedTool: async (tool: string) => {
|
||||||
|
let currentConfig: HikariConfig = defaultConfig;
|
||||||
|
config.subscribe((c) => (currentConfig = c))();
|
||||||
|
if (!currentConfig.auto_granted_tools.includes(tool)) {
|
||||||
|
const newTools = [...currentConfig.auto_granted_tools, tool];
|
||||||
|
await updateConfig({ auto_granted_tools: newTools });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAutoGrantedTool: async (tool: string) => {
|
||||||
|
let currentConfig: HikariConfig = defaultConfig;
|
||||||
|
config.subscribe((c) => (currentConfig = c))();
|
||||||
|
const newTools = currentConfig.auto_granted_tools.filter((t) => t !== tool);
|
||||||
|
await updateConfig({ auto_granted_tools: newTools });
|
||||||
|
},
|
||||||
|
|
||||||
|
getConfig: (): HikariConfig => {
|
||||||
|
let currentConfig: HikariConfig = defaultConfig;
|
||||||
|
config.subscribe((c) => (currentConfig = c))();
|
||||||
|
return currentConfig;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTheme(theme: Theme) {
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configStore = createConfigStore();
|
||||||
|
|
||||||
|
export const isDarkTheme = derived(configStore.config, ($config) => $config.theme === "dark");
|
||||||
@@ -1,14 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { initializeTauriListeners } from "$lib/tauri";
|
import { initializeTauriListeners } from "$lib/tauri";
|
||||||
|
import { configStore, applyTheme } from "$lib/stores/config";
|
||||||
import Terminal from "$lib/components/Terminal.svelte";
|
import Terminal from "$lib/components/Terminal.svelte";
|
||||||
import InputBar from "$lib/components/InputBar.svelte";
|
import InputBar from "$lib/components/InputBar.svelte";
|
||||||
import StatusBar from "$lib/components/StatusBar.svelte";
|
import StatusBar from "$lib/components/StatusBar.svelte";
|
||||||
import AnimeGirl from "$lib/components/AnimeGirl.svelte";
|
import AnimeGirl from "$lib/components/AnimeGirl.svelte";
|
||||||
import PermissionModal from "$lib/components/PermissionModal.svelte";
|
import PermissionModal from "$lib/components/PermissionModal.svelte";
|
||||||
|
import ConfigSidebar from "$lib/components/ConfigSidebar.svelte";
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await initializeTauriListeners();
|
await initializeTauriListeners();
|
||||||
|
await configStore.loadConfig();
|
||||||
|
|
||||||
|
// Apply saved theme on startup
|
||||||
|
const config = configStore.getConfig();
|
||||||
|
applyTheme(config.theme);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -31,6 +38,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<PermissionModal />
|
<PermissionModal />
|
||||||
|
<ConfigSidebar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Reference in New Issue
Block a user