generated from nhcarrigan/template
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14e00a756b |
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hikari-desktop",
|
"name": "hikari-desktop",
|
||||||
"version": "1.14.0",
|
"version": "1.13.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
"prettier-plugin-svelte": "3.5.0",
|
"prettier-plugin-svelte": "3.5.0",
|
||||||
"svelte": "5.53.5",
|
"svelte": "5.53.5",
|
||||||
"svelte-check": "4.4.3",
|
"svelte-check": "4.4.3",
|
||||||
"tailwindcss": "4.2.1",
|
"tailwindcss": "4.2.2",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.56.1",
|
"typescript-eslint": "8.56.1",
|
||||||
"vite": "6.4.1",
|
"vite": "6.4.1",
|
||||||
|
|||||||
Generated
+7
-2
@@ -181,8 +181,8 @@ importers:
|
|||||||
specifier: 4.4.3
|
specifier: 4.4.3
|
||||||
version: 4.4.3(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3)
|
version: 4.4.3(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: 4.2.1
|
specifier: 4.2.2
|
||||||
version: 4.2.1
|
version: 4.2.2
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 5.9.3
|
specifier: 5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -1944,6 +1944,9 @@ packages:
|
|||||||
tailwindcss@4.2.1:
|
tailwindcss@4.2.1:
|
||||||
resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==}
|
resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==}
|
||||||
|
|
||||||
|
tailwindcss@4.2.2:
|
||||||
|
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
|
||||||
|
|
||||||
tapable@2.3.0:
|
tapable@2.3.0:
|
||||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3925,6 +3928,8 @@ snapshots:
|
|||||||
|
|
||||||
tailwindcss@4.2.1: {}
|
tailwindcss@4.2.1: {}
|
||||||
|
|
||||||
|
tailwindcss@4.2.2: {}
|
||||||
|
|
||||||
tapable@2.3.0: {}
|
tapable@2.3.0: {}
|
||||||
|
|
||||||
tinybench@2.9.0: {}
|
tinybench@2.9.0: {}
|
||||||
|
|||||||
Generated
+1
-1
@@ -1648,7 +1648,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hikari-desktop"
|
name = "hikari-desktop"
|
||||||
version = "1.14.0"
|
version = "1.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hikari-desktop"
|
name = "hikari-desktop"
|
||||||
version = "1.14.0"
|
version = "1.13.0"
|
||||||
description = "Hikari - Claude Code Visual Assistant"
|
description = "Hikari - Claude Code Visual Assistant"
|
||||||
authors = ["Naomi Carrigan"]
|
authors = ["Naomi Carrigan"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -2618,103 +2618,6 @@ pub async fn open_binary_file(app: AppHandle, path: String) -> Result<(), String
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read `~/.claude/CLAUDE.md` via WSL (for Windows).
|
|
||||||
/// Returns an empty string if the file does not exist.
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
async fn get_global_claude_md_via_wsl() -> Result<String, String> {
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
let output = Command::new("wsl")
|
|
||||||
.hide_window()
|
|
||||||
.args(["-e", "bash", "-l", "-c", "cat ~/.claude/CLAUDE.md 2>/dev/null || true"])
|
|
||||||
.output()
|
|
||||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
|
||||||
|
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write content to `~/.claude/CLAUDE.md` via WSL (for Windows).
|
|
||||||
/// Creates the file (and `~/.claude/` directory) if they do not exist.
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
async fn save_global_claude_md_via_wsl(content: String) -> Result<(), String> {
|
|
||||||
use std::io::Write;
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
|
|
||||||
let mut child = Command::new("wsl")
|
|
||||||
.hide_window()
|
|
||||||
.args([
|
|
||||||
"-e",
|
|
||||||
"bash",
|
|
||||||
"-l",
|
|
||||||
"-c",
|
|
||||||
"mkdir -p ~/.claude && cat > ~/.claude/CLAUDE.md",
|
|
||||||
])
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
.map_err(|e| format!("Failed to execute WSL command: {}", e))?;
|
|
||||||
|
|
||||||
if let Some(stdin) = child.stdin.as_mut() {
|
|
||||||
stdin
|
|
||||||
.write_all(content.as_bytes())
|
|
||||||
.map_err(|e| format!("Failed to write content to WSL stdin: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = child
|
|
||||||
.wait()
|
|
||||||
.map_err(|e| format!("Failed to wait for WSL command: {}", e))?;
|
|
||||||
|
|
||||||
if !status.success() {
|
|
||||||
return Err("Failed to save CLAUDE.md via WSL".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the contents of `~/.claude/CLAUDE.md`.
|
|
||||||
/// Returns an empty string if the file does not exist.
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_global_claude_md() -> Result<String, String> {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
return get_global_claude_md_via_wsl().await;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
let path = dirs::home_dir()
|
|
||||||
.ok_or_else(|| "Could not determine home directory".to_string())?
|
|
||||||
.join(".claude")
|
|
||||||
.join("CLAUDE.md");
|
|
||||||
|
|
||||||
if !path.exists() {
|
|
||||||
return Ok(String::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fs::read_to_string(&path).map_err(|e| format!("Failed to read CLAUDE.md: {}", e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write content to `~/.claude/CLAUDE.md`.
|
|
||||||
/// Creates the file (and `~/.claude/` directory) if they do not exist.
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn save_global_claude_md(content: String) -> Result<(), String> {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
return save_global_claude_md_via_wsl(content).await;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
let claude_dir = dirs::home_dir()
|
|
||||||
.ok_or_else(|| "Could not determine home directory".to_string())?
|
|
||||||
.join(".claude");
|
|
||||||
|
|
||||||
if !claude_dir.exists() {
|
|
||||||
std::fs::create_dir_all(&claude_dir)
|
|
||||||
.map_err(|e| format!("Failed to create ~/.claude directory: {}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = claude_dir.join("CLAUDE.md");
|
|
||||||
std::fs::write(&path, content).map_err(|e| format!("Failed to write CLAUDE.md: {}", e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -3464,22 +3367,4 @@ gitea: gitea-mcp -t stdio (STDIO) - âś“ Connected"#;
|
|||||||
let (_, args) = build_wslpath_command(path);
|
let (_, args) = build_wslpath_command(path);
|
||||||
assert_eq!(args[2], path);
|
assert_eq!(args[2], path);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_global_claude_md_path_construction() {
|
|
||||||
// Verify that home_dir() resolves successfully on the test platform
|
|
||||||
let home = dirs::home_dir();
|
|
||||||
assert!(home.is_some(), "home_dir() should be available in test environment");
|
|
||||||
let expected = home.unwrap().join(".claude").join("CLAUDE.md");
|
|
||||||
assert!(expected.to_string_lossy().contains(".claude"));
|
|
||||||
assert!(expected.to_string_lossy().ends_with("CLAUDE.md"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_save_global_claude_md_dir_path_construction() {
|
|
||||||
let home = dirs::home_dir();
|
|
||||||
assert!(home.is_some());
|
|
||||||
let dir = home.unwrap().join(".claude");
|
|
||||||
assert!(dir.to_string_lossy().contains(".claude"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,22 +54,6 @@ pub struct ClaudeStartOptions {
|
|||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub session_name: Option<String>,
|
pub session_name: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub disable_skill_shell_execution: bool,
|
|
||||||
|
|
||||||
/// Pass `--bare` flag to suppress UI chrome, useful for scripted headless `-p` calls (v2.1.81+).
|
|
||||||
#[serde(default)]
|
|
||||||
pub bare_mode: bool,
|
|
||||||
|
|
||||||
/// Controls `showClearContextOnPlanAccept` in `--settings` (v2.1.81+).
|
|
||||||
/// Defaults to true (matching CLI default). Set to false to suppress the dialog.
|
|
||||||
#[serde(default = "default_show_clear_context")]
|
|
||||||
pub show_clear_context_on_plan_accept: bool,
|
|
||||||
|
|
||||||
/// Sets `ANTHROPIC_CUSTOM_MODEL_OPTION` env var for custom model providers (v2.1.81+).
|
|
||||||
#[serde(default)]
|
|
||||||
pub custom_model_option: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -219,23 +203,6 @@ pub struct HikariConfig {
|
|||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub model_overrides: Option<HashMap<String, String>>,
|
pub model_overrides: Option<HashMap<String, String>>,
|
||||||
|
|
||||||
/// Prevents skill scripts from executing shell commands (Claude Code v2.1.91+).
|
|
||||||
/// Passes `"disableSkillShellExecution": true` via the `--settings` flag.
|
|
||||||
#[serde(default)]
|
|
||||||
pub disable_skill_shell_execution: bool,
|
|
||||||
|
|
||||||
/// Pass `--bare` flag to suppress UI chrome, useful for scripted headless `-p` calls (v2.1.81+).
|
|
||||||
#[serde(default)]
|
|
||||||
pub bare_mode: bool,
|
|
||||||
|
|
||||||
/// Controls `showClearContextOnPlanAccept` in `--settings` (v2.1.81+).
|
|
||||||
#[serde(default = "default_show_clear_context")]
|
|
||||||
pub show_clear_context_on_plan_accept: bool,
|
|
||||||
|
|
||||||
/// Sets `ANTHROPIC_CUSTOM_MODEL_OPTION` env var for custom model providers (v2.1.81+).
|
|
||||||
#[serde(default)]
|
|
||||||
pub custom_model_option: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HikariConfig {
|
impl Default for HikariConfig {
|
||||||
@@ -287,10 +254,6 @@ impl Default for HikariConfig {
|
|||||||
enable_claudeai_mcp_servers: true,
|
enable_claudeai_mcp_servers: true,
|
||||||
auto_memory_directory: None,
|
auto_memory_directory: None,
|
||||||
model_overrides: None,
|
model_overrides: None,
|
||||||
disable_skill_shell_execution: false,
|
|
||||||
bare_mode: false,
|
|
||||||
show_clear_context_on_plan_accept: true,
|
|
||||||
custom_model_option: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,10 +306,6 @@ fn default_enable_claudeai_mcp_servers() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_show_clear_context() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum BudgetAction {
|
pub enum BudgetAction {
|
||||||
@@ -446,10 +405,6 @@ mod tests {
|
|||||||
assert!(config.enable_claudeai_mcp_servers);
|
assert!(config.enable_claudeai_mcp_servers);
|
||||||
assert!(config.auto_memory_directory.is_none());
|
assert!(config.auto_memory_directory.is_none());
|
||||||
assert!(config.model_overrides.is_none());
|
assert!(config.model_overrides.is_none());
|
||||||
assert!(!config.disable_skill_shell_execution);
|
|
||||||
assert!(!config.bare_mode);
|
|
||||||
assert!(config.show_clear_context_on_plan_accept);
|
|
||||||
assert!(config.custom_model_option.is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -504,10 +459,6 @@ mod tests {
|
|||||||
"claude-opus-4-6".to_string(),
|
"claude-opus-4-6".to_string(),
|
||||||
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-6-v1".to_string(),
|
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-6-v1".to_string(),
|
||||||
)])),
|
)])),
|
||||||
disable_skill_shell_execution: true,
|
|
||||||
bare_mode: false,
|
|
||||||
show_clear_context_on_plan_accept: true,
|
|
||||||
custom_model_option: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string(&config).unwrap();
|
let json = serde_json::to_string(&config).unwrap();
|
||||||
|
|||||||
@@ -224,8 +224,6 @@ pub fn run() {
|
|||||||
delete_all_drafts,
|
delete_all_drafts,
|
||||||
scan_project,
|
scan_project,
|
||||||
open_binary_file,
|
open_binary_file,
|
||||||
get_global_claude_md,
|
|
||||||
save_global_claude_md,
|
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -95,9 +95,6 @@ pub enum ClaudeMessage {
|
|||||||
cwd: Option<String>,
|
cwd: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
tools: Option<Vec<String>>,
|
tools: Option<Vec<String>>,
|
||||||
/// Output style hint from Claude Code (v2.1.81+). Informational only.
|
|
||||||
#[serde(default)]
|
|
||||||
output_style: Option<String>,
|
|
||||||
},
|
},
|
||||||
#[serde(rename = "assistant")]
|
#[serde(rename = "assistant")]
|
||||||
Assistant {
|
Assistant {
|
||||||
@@ -122,15 +119,6 @@ pub enum ClaudeMessage {
|
|||||||
permission_denials: Option<Vec<PermissionDenial>>,
|
permission_denials: Option<Vec<PermissionDenial>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
usage: Option<UsageInfo>,
|
usage: Option<UsageInfo>,
|
||||||
/// Fast mode state from Claude Code v2.1.81+. Values: "default" | "enabled" | "disabled".
|
|
||||||
#[serde(default)]
|
|
||||||
fast_mode_state: Option<String>,
|
|
||||||
/// Per-model usage breakdown from Claude Code v2.1.81+.
|
|
||||||
#[serde(default)]
|
|
||||||
model_usage: Option<serde_json::Value>,
|
|
||||||
/// Authoritative total cost in USD reported by Claude Code v2.1.81+.
|
|
||||||
#[serde(default)]
|
|
||||||
total_cost_usd: Option<f64>,
|
|
||||||
},
|
},
|
||||||
#[serde(rename = "rate_limit_event")]
|
#[serde(rename = "rate_limit_event")]
|
||||||
RateLimitEvent {
|
RateLimitEvent {
|
||||||
@@ -330,42 +318,6 @@ pub struct PostCompactEvent {
|
|||||||
pub conversation_id: Option<String>,
|
pub conversation_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CwdChangedEvent {
|
|
||||||
pub cwd: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub conversation_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct FileChangedEvent {
|
|
||||||
pub file: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub conversation_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct TaskCreatedEvent {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub task_id: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub description: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub parent_tool_use_id: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub conversation_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct PermissionDeniedEvent {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub tool_name: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub reason: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub conversation_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AgentStartEvent {
|
pub struct AgentStartEvent {
|
||||||
pub tool_use_id: String,
|
pub tool_use_id: String,
|
||||||
@@ -789,200 +741,4 @@ mod tests {
|
|||||||
assert!(serialized.contains("\"session_id\":\"sess-xyz\""));
|
assert!(serialized.contains("\"session_id\":\"sess-xyz\""));
|
||||||
assert!(!serialized.contains("conversation_id"));
|
assert!(!serialized.contains("conversation_id"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cwd_changed_event_serialization() {
|
|
||||||
let event = CwdChangedEvent {
|
|
||||||
cwd: "/home/naomi/code/my-project".to_string(),
|
|
||||||
conversation_id: Some("conv-abc".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(serialized.contains("\"cwd\":\"/home/naomi/code/my-project\""));
|
|
||||||
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cwd_changed_event_omits_none_fields() {
|
|
||||||
let event = CwdChangedEvent {
|
|
||||||
cwd: "/tmp/workspace".to_string(),
|
|
||||||
conversation_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(serialized.contains("\"cwd\":\"/tmp/workspace\""));
|
|
||||||
assert!(!serialized.contains("conversation_id"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_file_changed_event_serialization() {
|
|
||||||
let event = FileChangedEvent {
|
|
||||||
file: "/home/naomi/code/my-project/src/main.rs".to_string(),
|
|
||||||
conversation_id: Some("conv-abc".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(serialized.contains("\"file\":\"/home/naomi/code/my-project/src/main.rs\""));
|
|
||||||
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_file_changed_event_omits_none_fields() {
|
|
||||||
let event = FileChangedEvent {
|
|
||||||
file: "/tmp/test.txt".to_string(),
|
|
||||||
conversation_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(serialized.contains("\"file\":\"/tmp/test.txt\""));
|
|
||||||
assert!(!serialized.contains("conversation_id"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_task_created_event_serialization() {
|
|
||||||
let event = TaskCreatedEvent {
|
|
||||||
task_id: Some("task-abc123".to_string()),
|
|
||||||
description: Some("Explore the codebase".to_string()),
|
|
||||||
parent_tool_use_id: Some("toolu_xyz".to_string()),
|
|
||||||
conversation_id: Some("conv-abc".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(serialized.contains("\"task_id\":\"task-abc123\""));
|
|
||||||
assert!(serialized.contains("\"description\":\"Explore the codebase\""));
|
|
||||||
assert!(serialized.contains("\"parent_tool_use_id\":\"toolu_xyz\""));
|
|
||||||
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_task_created_event_omits_none_fields() {
|
|
||||||
let event = TaskCreatedEvent {
|
|
||||||
task_id: None,
|
|
||||||
description: None,
|
|
||||||
parent_tool_use_id: None,
|
|
||||||
conversation_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&event).unwrap();
|
|
||||||
assert_eq!(serialized, "{}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_task_created_event_partial_fields() {
|
|
||||||
let event = TaskCreatedEvent {
|
|
||||||
task_id: Some("task-001".to_string()),
|
|
||||||
description: None,
|
|
||||||
parent_tool_use_id: None,
|
|
||||||
conversation_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(serialized.contains("\"task_id\":\"task-001\""));
|
|
||||||
assert!(!serialized.contains("description"));
|
|
||||||
assert!(!serialized.contains("parent_tool_use_id"));
|
|
||||||
assert!(!serialized.contains("conversation_id"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_permission_denied_event_serialization() {
|
|
||||||
let event = PermissionDeniedEvent {
|
|
||||||
tool_name: Some("Bash".to_string()),
|
|
||||||
reason: Some("Tool not in allow list".to_string()),
|
|
||||||
conversation_id: Some("conv-abc".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(serialized.contains("\"tool_name\":\"Bash\""));
|
|
||||||
assert!(serialized.contains("\"reason\":\"Tool not in allow list\""));
|
|
||||||
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_permission_denied_event_omits_none_fields() {
|
|
||||||
let event = PermissionDeniedEvent {
|
|
||||||
tool_name: None,
|
|
||||||
reason: None,
|
|
||||||
conversation_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&event).unwrap();
|
|
||||||
assert_eq!(serialized, "{}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_permission_denied_event_partial_fields() {
|
|
||||||
let event = PermissionDeniedEvent {
|
|
||||||
tool_name: Some("Edit".to_string()),
|
|
||||||
reason: None,
|
|
||||||
conversation_id: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(serialized.contains("\"tool_name\":\"Edit\""));
|
|
||||||
assert!(!serialized.contains("reason"));
|
|
||||||
assert!(!serialized.contains("conversation_id"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_system_init_with_output_style() {
|
|
||||||
let json = r#"{"type":"system","subtype":"init","session_id":"sess-1","output_style":"auto"}"#;
|
|
||||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
||||||
if let ClaudeMessage::System { output_style, .. } = msg {
|
|
||||||
assert_eq!(output_style, Some("auto".to_string()));
|
|
||||||
} else {
|
|
||||||
panic!("Expected System variant");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_system_init_without_output_style() {
|
|
||||||
let json = r#"{"type":"system","subtype":"init","session_id":"sess-1"}"#;
|
|
||||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
||||||
if let ClaudeMessage::System { output_style, .. } = msg {
|
|
||||||
assert!(output_style.is_none());
|
|
||||||
} else {
|
|
||||||
panic!("Expected System variant");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_result_message_with_fast_mode_state() {
|
|
||||||
let json = r#"{"type":"result","subtype":"success","fast_mode_state":"enabled"}"#;
|
|
||||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
||||||
if let ClaudeMessage::Result { fast_mode_state, .. } = msg {
|
|
||||||
assert_eq!(fast_mode_state, Some("enabled".to_string()));
|
|
||||||
} else {
|
|
||||||
panic!("Expected Result variant");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_result_message_with_total_cost_usd() {
|
|
||||||
let json = r#"{"type":"result","subtype":"success","total_cost_usd":0.05}"#;
|
|
||||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
||||||
if let ClaudeMessage::Result { total_cost_usd, .. } = msg {
|
|
||||||
assert!((total_cost_usd.unwrap() - 0.05).abs() < f64::EPSILON);
|
|
||||||
} else {
|
|
||||||
panic!("Expected Result variant");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_result_message_without_new_fields() {
|
|
||||||
let json = r#"{"type":"result","subtype":"success"}"#;
|
|
||||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
||||||
if let ClaudeMessage::Result {
|
|
||||||
fast_mode_state,
|
|
||||||
model_usage,
|
|
||||||
total_cost_usd,
|
|
||||||
..
|
|
||||||
} = msg
|
|
||||||
{
|
|
||||||
assert!(fast_mode_state.is_none());
|
|
||||||
assert!(model_usage.is_none());
|
|
||||||
assert!(total_cost_usd.is_none());
|
|
||||||
} else {
|
|
||||||
panic!("Expected Result variant");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-430
@@ -15,11 +15,10 @@ use crate::process_ext::HideWindow;
|
|||||||
use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats};
|
use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent,
|
AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent,
|
||||||
ConnectionStatus, ContentBlock, CwdChangedEvent, ElicitationEvent, ElicitationResultEvent,
|
ConnectionStatus, ContentBlock, ElicitationEvent, ElicitationResultEvent, MessageCost,
|
||||||
FileChangedEvent, MessageCost, OutputEvent, PermissionDeniedEvent, PermissionPromptEvent,
|
OutputEvent, PermissionPromptEvent, PermissionPromptEventItem, QuestionOption, SessionEvent,
|
||||||
PermissionPromptEventItem, PostCompactEvent, QuestionOption, SessionEvent, StateChangeEvent,
|
PostCompactEvent, StateChangeEvent, StopFailureEvent, TodoItem, TodoUpdateEvent,
|
||||||
StopFailureEvent, TaskCreatedEvent, TodoItem, TodoUpdateEvent, UserQuestionEvent,
|
UserQuestionEvent, WorkingDirectoryEvent, WorktreeEvent, WorktreeInfo,
|
||||||
WorkingDirectoryEvent, WorktreeEvent, WorktreeInfo,
|
|
||||||
};
|
};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
@@ -37,10 +36,7 @@ struct PendingToolUse {
|
|||||||
tool_input: serde_json::Value,
|
tool_input: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Monitor" added in Claude Code v2.1.98 — it streams events and is observational in nature,
|
const SEARCH_TOOLS: [&str; 5] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"];
|
||||||
// so it maps to the Searching character state. It may appear as name "Monitor" in tool_use
|
|
||||||
// blocks (confirmed by CLI source inspection; it is a local_bash with kind="monitor" internally).
|
|
||||||
const SEARCH_TOOLS: [&str; 6] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch", "Monitor"];
|
|
||||||
const CODING_TOOLS: [&str; 3] = ["Edit", "Write", "NotebookEdit"];
|
const CODING_TOOLS: [&str; 3] = ["Edit", "Write", "NotebookEdit"];
|
||||||
|
|
||||||
fn detect_wsl() -> bool {
|
fn detect_wsl() -> bool {
|
||||||
@@ -303,11 +299,6 @@ impl WslBridge {
|
|||||||
cmd.arg("--worktree");
|
cmd.arg("--worktree");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add bare flag if requested (v2.1.81+)
|
|
||||||
if options.bare_mode {
|
|
||||||
cmd.arg("--bare");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass combined settings via --settings flag if any settings are specified
|
// Pass combined settings via --settings flag if any settings are specified
|
||||||
{
|
{
|
||||||
let has_memory_dir = options
|
let has_memory_dir = options
|
||||||
@@ -320,13 +311,8 @@ impl WslBridge {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|m| !m.is_empty())
|
.map(|m| !m.is_empty())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let suppress_clear_context = !options.show_clear_context_on_plan_accept;
|
|
||||||
|
|
||||||
if has_memory_dir
|
if has_memory_dir || has_overrides {
|
||||||
|| has_overrides
|
|
||||||
|| options.disable_skill_shell_execution
|
|
||||||
|| suppress_clear_context
|
|
||||||
{
|
|
||||||
let mut settings = serde_json::Map::new();
|
let mut settings = serde_json::Map::new();
|
||||||
if let Some(ref dir) = options.auto_memory_directory {
|
if let Some(ref dir) = options.auto_memory_directory {
|
||||||
if !dir.is_empty() {
|
if !dir.is_empty() {
|
||||||
@@ -343,18 +329,6 @@ impl WslBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.disable_skill_shell_execution {
|
|
||||||
settings.insert(
|
|
||||||
"disableSkillShellExecution".to_string(),
|
|
||||||
serde_json::Value::Bool(true),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if suppress_clear_context {
|
|
||||||
settings.insert(
|
|
||||||
"showClearContextOnPlanAccept".to_string(),
|
|
||||||
serde_json::Value::Bool(false),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Ok(settings_json) = serde_json::to_string(&settings) {
|
if let Ok(settings_json) = serde_json::to_string(&settings) {
|
||||||
cmd.args(["--settings", &settings_json]);
|
cmd.args(["--settings", &settings_json]);
|
||||||
}
|
}
|
||||||
@@ -395,13 +369,6 @@ impl WslBridge {
|
|||||||
cmd.env("ENABLE_CLAUDEAI_MCP_SERVERS", "false");
|
cmd.env("ENABLE_CLAUDEAI_MCP_SERVERS", "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set custom model option if specified (v2.1.81+)
|
|
||||||
if let Some(ref custom_opt) = options.custom_model_option {
|
|
||||||
if !custom_opt.is_empty() {
|
|
||||||
cmd.env("ANTHROPIC_CUSTOM_MODEL_OPTION", custom_opt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -469,14 +436,6 @@ impl WslBridge {
|
|||||||
claude_cmd.push_str("ENABLE_CLAUDEAI_MCP_SERVERS=false ");
|
claude_cmd.push_str("ENABLE_CLAUDEAI_MCP_SERVERS=false ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set custom model option if specified (v2.1.81+)
|
|
||||||
if let Some(ref custom_opt) = options.custom_model_option {
|
|
||||||
if !custom_opt.is_empty() {
|
|
||||||
let escaped = custom_opt.replace('\'', "'\\''");
|
|
||||||
claude_cmd.push_str(&format!("ANTHROPIC_CUSTOM_MODEL_OPTION='{}' ", escaped));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
claude_cmd.push_str(
|
claude_cmd.push_str(
|
||||||
"claude --output-format stream-json --input-format stream-json --verbose",
|
"claude --output-format stream-json --input-format stream-json --verbose",
|
||||||
);
|
);
|
||||||
@@ -527,11 +486,6 @@ impl WslBridge {
|
|||||||
claude_cmd.push_str(" --worktree");
|
claude_cmd.push_str(" --worktree");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add bare flag if requested (v2.1.81+)
|
|
||||||
if options.bare_mode {
|
|
||||||
claude_cmd.push_str(" --bare");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass combined settings via --settings flag if any settings are specified
|
// Pass combined settings via --settings flag if any settings are specified
|
||||||
{
|
{
|
||||||
let has_memory_dir = options
|
let has_memory_dir = options
|
||||||
@@ -544,13 +498,8 @@ impl WslBridge {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|m| !m.is_empty())
|
.map(|m| !m.is_empty())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let suppress_clear_context = !options.show_clear_context_on_plan_accept;
|
|
||||||
|
|
||||||
if has_memory_dir
|
if has_memory_dir || has_overrides {
|
||||||
|| has_overrides
|
|
||||||
|| options.disable_skill_shell_execution
|
|
||||||
|| suppress_clear_context
|
|
||||||
{
|
|
||||||
let mut settings = serde_json::Map::new();
|
let mut settings = serde_json::Map::new();
|
||||||
if let Some(ref dir) = options.auto_memory_directory {
|
if let Some(ref dir) = options.auto_memory_directory {
|
||||||
if !dir.is_empty() {
|
if !dir.is_empty() {
|
||||||
@@ -567,18 +516,6 @@ impl WslBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.disable_skill_shell_execution {
|
|
||||||
settings.insert(
|
|
||||||
"disableSkillShellExecution".to_string(),
|
|
||||||
serde_json::Value::Bool(true),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if suppress_clear_context {
|
|
||||||
settings.insert(
|
|
||||||
"showClearContextOnPlanAccept".to_string(),
|
|
||||||
serde_json::Value::Bool(false),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Ok(settings_json) = serde_json::to_string(&settings) {
|
if let Ok(settings_json) = serde_json::to_string(&settings) {
|
||||||
let escaped = settings_json.replace('\'', "'\\''");
|
let escaped = settings_json.replace('\'', "'\\''");
|
||||||
claude_cmd.push_str(&format!(" --settings '{}'", escaped));
|
claude_cmd.push_str(&format!(" --settings '{}'", escaped));
|
||||||
@@ -1141,10 +1078,6 @@ fn handle_stderr(
|
|||||||
let is_elicitation_result = line.contains("[ElicitationResult Hook]");
|
let is_elicitation_result = line.contains("[ElicitationResult Hook]");
|
||||||
let is_stop_failure = line.contains("[StopFailure Hook]");
|
let is_stop_failure = line.contains("[StopFailure Hook]");
|
||||||
let is_post_compact = line.contains("[PostCompact Hook]");
|
let is_post_compact = line.contains("[PostCompact Hook]");
|
||||||
let is_cwd_changed = line.contains("[CwdChanged Hook]");
|
|
||||||
let is_file_changed = line.contains("[FileChanged Hook]");
|
|
||||||
let is_task_created = line.contains("[TaskCreated Hook]");
|
|
||||||
let is_permission_denied = line.contains("[PermissionDenied Hook]");
|
|
||||||
|
|
||||||
let line_type = if is_worktree_create || is_worktree_remove {
|
let line_type = if is_worktree_create || is_worktree_remove {
|
||||||
"worktree"
|
"worktree"
|
||||||
@@ -1156,14 +1089,6 @@ fn handle_stderr(
|
|||||||
"error"
|
"error"
|
||||||
} else if is_post_compact {
|
} else if is_post_compact {
|
||||||
"compact-prompt"
|
"compact-prompt"
|
||||||
} else if is_cwd_changed {
|
|
||||||
"cwd-changed"
|
|
||||||
} else if is_file_changed {
|
|
||||||
"file-changed"
|
|
||||||
} else if is_task_created {
|
|
||||||
"task-created"
|
|
||||||
} else if is_permission_denied {
|
|
||||||
"permission-denied"
|
|
||||||
} else {
|
} else {
|
||||||
"error"
|
"error"
|
||||||
};
|
};
|
||||||
@@ -1302,124 +1227,6 @@ fn handle_stderr(
|
|||||||
parent_tool_use_id: None,
|
parent_tool_use_id: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if is_cwd_changed {
|
|
||||||
let data = parse_cwd_changed_hook(&line);
|
|
||||||
let friendly_content =
|
|
||||||
format!("Working directory changed to: {}", data.cwd);
|
|
||||||
|
|
||||||
let _ = app.emit(
|
|
||||||
"claude:cwd-changed",
|
|
||||||
CwdChangedEvent {
|
|
||||||
cwd: data.cwd,
|
|
||||||
conversation_id: conversation_id.clone(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = app.emit(
|
|
||||||
"claude:output",
|
|
||||||
OutputEvent {
|
|
||||||
line_type: "cwd-changed".to_string(),
|
|
||||||
content: friendly_content,
|
|
||||||
tool_name: None,
|
|
||||||
conversation_id: conversation_id.clone(),
|
|
||||||
cost: None,
|
|
||||||
parent_tool_use_id: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if is_file_changed {
|
|
||||||
let data = parse_file_changed_hook(&line);
|
|
||||||
let friendly_content = if data.file.is_empty() {
|
|
||||||
"File changed".to_string()
|
|
||||||
} else {
|
|
||||||
format!("File changed: {}", data.file)
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = app.emit(
|
|
||||||
"claude:file-changed",
|
|
||||||
FileChangedEvent {
|
|
||||||
file: data.file,
|
|
||||||
conversation_id: conversation_id.clone(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = app.emit(
|
|
||||||
"claude:output",
|
|
||||||
OutputEvent {
|
|
||||||
line_type: "file-changed".to_string(),
|
|
||||||
content: friendly_content,
|
|
||||||
tool_name: None,
|
|
||||||
conversation_id: conversation_id.clone(),
|
|
||||||
cost: None,
|
|
||||||
parent_tool_use_id: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if is_task_created {
|
|
||||||
let data = parse_task_created_hook(&line);
|
|
||||||
let friendly_content = match data.description.as_deref() {
|
|
||||||
Some(desc) if !desc.is_empty() => format!("Task created: {}", desc),
|
|
||||||
_ => "Task created".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = app.emit(
|
|
||||||
"claude:task-created",
|
|
||||||
TaskCreatedEvent {
|
|
||||||
task_id: data.task_id,
|
|
||||||
description: data.description,
|
|
||||||
parent_tool_use_id: data.parent_tool_use_id,
|
|
||||||
conversation_id: conversation_id.clone(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = app.emit(
|
|
||||||
"claude:output",
|
|
||||||
OutputEvent {
|
|
||||||
line_type: "task-created".to_string(),
|
|
||||||
content: friendly_content,
|
|
||||||
tool_name: None,
|
|
||||||
conversation_id: conversation_id.clone(),
|
|
||||||
cost: None,
|
|
||||||
parent_tool_use_id: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if is_permission_denied {
|
|
||||||
let data = parse_permission_denied_hook(&line);
|
|
||||||
let friendly_content = match (data.tool_name.as_deref(), data.reason.as_deref()) {
|
|
||||||
(Some(tool), Some(reason)) => {
|
|
||||||
format!("Permission denied for {}: {}", tool, reason)
|
|
||||||
}
|
|
||||||
(Some(tool), None) => format!("Permission denied for {}", tool),
|
|
||||||
_ => "Permission denied".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = app.emit(
|
|
||||||
"claude:state-change",
|
|
||||||
StateChangeEvent {
|
|
||||||
state: CharacterState::Permission,
|
|
||||||
tool_name: data.tool_name.clone(),
|
|
||||||
conversation_id: conversation_id.clone(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = app.emit(
|
|
||||||
"claude:permission-denied",
|
|
||||||
PermissionDeniedEvent {
|
|
||||||
tool_name: data.tool_name,
|
|
||||||
reason: data.reason,
|
|
||||||
conversation_id: conversation_id.clone(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = app.emit(
|
|
||||||
"claude:output",
|
|
||||||
OutputEvent {
|
|
||||||
line_type: "permission-denied".to_string(),
|
|
||||||
content: friendly_content,
|
|
||||||
tool_name: None,
|
|
||||||
conversation_id: conversation_id.clone(),
|
|
||||||
cost: None,
|
|
||||||
parent_tool_use_id: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
let _ = app.emit(
|
let _ = app.emit(
|
||||||
"claude:output",
|
"claude:output",
|
||||||
@@ -1633,85 +1440,6 @@ fn parse_post_compact_hook(line: &str) -> PostCompactData {
|
|||||||
PostCompactData { session_id }
|
PostCompactData { session_id }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct CwdChangedData {
|
|
||||||
cwd: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cwd_changed_hook(line: &str) -> CwdChangedData {
|
|
||||||
let cwd = extract_quoted_value(line, "cwd")
|
|
||||||
.or_else(|| extract_quoted_value(line, "path"))
|
|
||||||
.or_else(|| {
|
|
||||||
line.split("[CwdChanged Hook]")
|
|
||||||
.nth(1)
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
CwdChangedData { cwd }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct FileChangedData {
|
|
||||||
file: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_file_changed_hook(line: &str) -> FileChangedData {
|
|
||||||
let file = extract_quoted_value(line, "file")
|
|
||||||
.or_else(|| extract_quoted_value(line, "path"))
|
|
||||||
.or_else(|| {
|
|
||||||
line.split("[FileChanged Hook]")
|
|
||||||
.nth(1)
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
FileChangedData { file }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct TaskCreatedData {
|
|
||||||
task_id: Option<String>,
|
|
||||||
description: Option<String>,
|
|
||||||
parent_tool_use_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_task_created_hook(line: &str) -> TaskCreatedData {
|
|
||||||
let task_id = extract_debug_string_value(line, "task_id")
|
|
||||||
.or_else(|| extract_quoted_value(line, "task_id"));
|
|
||||||
|
|
||||||
let description = extract_quoted_value(line, "description")
|
|
||||||
.or_else(|| extract_quoted_value(line, "prompt"));
|
|
||||||
|
|
||||||
let parent_tool_use_id = if line.contains("parent_tool_use_id=Some") {
|
|
||||||
line.split("parent_tool_use_id=Some(\"")
|
|
||||||
.nth(1)
|
|
||||||
.and_then(|s| s.split('"').next())
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
TaskCreatedData { task_id, description, parent_tool_use_id }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct PermissionDeniedData {
|
|
||||||
tool_name: Option<String>,
|
|
||||||
reason: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_permission_denied_hook(line: &str) -> PermissionDeniedData {
|
|
||||||
let tool_name = extract_quoted_value(line, "tool_name")
|
|
||||||
.or_else(|| extract_quoted_value(line, "tool"))
|
|
||||||
.or_else(|| extract_debug_string_value(line, "tool_name"));
|
|
||||||
|
|
||||||
let reason = extract_quoted_value(line, "reason")
|
|
||||||
.or_else(|| extract_quoted_value(line, "message"));
|
|
||||||
|
|
||||||
PermissionDeniedData { tool_name, reason }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts a double-quoted string value from a `key="value"` pair in a hook line.
|
/// Extracts a double-quoted string value from a `key="value"` pair in a hook line.
|
||||||
/// Handles escape sequences within the quoted value.
|
/// Handles escape sequences within the quoted value.
|
||||||
fn extract_quoted_value(line: &str, key: &str) -> Option<String> {
|
fn extract_quoted_value(line: &str, key: &str) -> Option<String> {
|
||||||
@@ -2169,9 +1897,6 @@ fn process_json_line(
|
|||||||
usage,
|
usage,
|
||||||
duration_ms,
|
duration_ms,
|
||||||
num_turns,
|
num_turns,
|
||||||
fast_mode_state,
|
|
||||||
model_usage,
|
|
||||||
total_cost_usd,
|
|
||||||
} => {
|
} => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Received Result message: subtype={}, has_denials={}, denial_count={:?}",
|
"Received Result message: subtype={}, has_denials={}, denial_count={:?}",
|
||||||
@@ -2256,25 +1981,6 @@ fn process_json_line(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log fast mode state if present (v2.1.81+)
|
|
||||||
if let Some(ref state) = fast_mode_state {
|
|
||||||
tracing::debug!("Fast mode state: {}", state);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log per-model usage if available (v2.1.81+)
|
|
||||||
if let Some(ref model_usage_val) = model_usage {
|
|
||||||
if let Some(map) = model_usage_val.as_object() {
|
|
||||||
for (model_name, _usage_val) in map {
|
|
||||||
tracing::debug!("Per-model usage logged for: {}", model_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log authoritative cost from Claude Code if available (v2.1.81+)
|
|
||||||
if let Some(auth_cost) = total_cost_usd {
|
|
||||||
tracing::debug!("Authoritative total cost from Claude Code: ${:.6}", auth_cost);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear tracking fields since request completed successfully
|
// Clear tracking fields since request completed successfully
|
||||||
{
|
{
|
||||||
let mut stats_guard = stats.write();
|
let mut stats_guard = stats.write();
|
||||||
@@ -2840,11 +2546,6 @@ mod tests {
|
|||||||
get_tool_state("WebFetch"),
|
get_tool_state("WebFetch"),
|
||||||
CharacterState::Searching
|
CharacterState::Searching
|
||||||
));
|
));
|
||||||
// Monitor tool added in v2.1.98 — observational/streaming, maps to Searching
|
|
||||||
assert!(matches!(
|
|
||||||
get_tool_state("Monitor"),
|
|
||||||
CharacterState::Searching
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3887,130 +3588,6 @@ mod tests {
|
|||||||
assert_eq!(data.session_id, None);
|
assert_eq!(data.session_id, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_cwd_changed_hook_with_cwd_key() {
|
|
||||||
let line = r#"[CwdChanged Hook] cwd="/home/naomi/code/my-project""#;
|
|
||||||
let data = parse_cwd_changed_hook(line);
|
|
||||||
assert_eq!(data.cwd, "/home/naomi/code/my-project");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_cwd_changed_hook_with_path_key() {
|
|
||||||
let line = r#"[CwdChanged Hook] path="/tmp/workspace""#;
|
|
||||||
let data = parse_cwd_changed_hook(line);
|
|
||||||
assert_eq!(data.cwd, "/tmp/workspace");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_cwd_changed_hook_bare_path_fallback() {
|
|
||||||
let line = "[CwdChanged Hook] /home/naomi/code/project";
|
|
||||||
let data = parse_cwd_changed_hook(line);
|
|
||||||
assert_eq!(data.cwd, "/home/naomi/code/project");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_cwd_changed_hook_empty_line() {
|
|
||||||
let line = "[CwdChanged Hook]";
|
|
||||||
let data = parse_cwd_changed_hook(line);
|
|
||||||
assert_eq!(data.cwd, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_file_changed_hook_with_file_key() {
|
|
||||||
let line = r#"[FileChanged Hook] file="/home/naomi/code/project/src/main.rs""#;
|
|
||||||
let data = parse_file_changed_hook(line);
|
|
||||||
assert_eq!(data.file, "/home/naomi/code/project/src/main.rs");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_file_changed_hook_with_path_key() {
|
|
||||||
let line = r#"[FileChanged Hook] path="/tmp/test.txt""#;
|
|
||||||
let data = parse_file_changed_hook(line);
|
|
||||||
assert_eq!(data.file, "/tmp/test.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_file_changed_hook_bare_path_fallback() {
|
|
||||||
let line = "[FileChanged Hook] /home/naomi/code/project/README.md";
|
|
||||||
let data = parse_file_changed_hook(line);
|
|
||||||
assert_eq!(data.file, "/home/naomi/code/project/README.md");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_file_changed_hook_empty_line() {
|
|
||||||
let line = "[FileChanged Hook]";
|
|
||||||
let data = parse_file_changed_hook(line);
|
|
||||||
assert_eq!(data.file, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_task_created_hook_with_all_fields() {
|
|
||||||
let line = r#"[TaskCreated Hook] task_id=Some("task-abc123"), description="Explore the codebase", parent_tool_use_id=Some("toolu_xyz")"#;
|
|
||||||
let data = parse_task_created_hook(line);
|
|
||||||
assert_eq!(data.task_id, Some("task-abc123".to_string()));
|
|
||||||
assert_eq!(data.description, Some("Explore the codebase".to_string()));
|
|
||||||
assert_eq!(data.parent_tool_use_id, Some("toolu_xyz".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_task_created_hook_with_description_only() {
|
|
||||||
let line = r#"[TaskCreated Hook] description="Search for relevant files""#;
|
|
||||||
let data = parse_task_created_hook(line);
|
|
||||||
assert_eq!(data.description, Some("Search for relevant files".to_string()));
|
|
||||||
assert_eq!(data.task_id, None);
|
|
||||||
assert_eq!(data.parent_tool_use_id, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_task_created_hook_no_parent() {
|
|
||||||
let line = r#"[TaskCreated Hook] task_id=Some("task-001"), description="Run tests""#;
|
|
||||||
let data = parse_task_created_hook(line);
|
|
||||||
assert_eq!(data.task_id, Some("task-001".to_string()));
|
|
||||||
assert_eq!(data.description, Some("Run tests".to_string()));
|
|
||||||
assert_eq!(data.parent_tool_use_id, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_task_created_hook_empty_line() {
|
|
||||||
let line = "[TaskCreated Hook]";
|
|
||||||
let data = parse_task_created_hook(line);
|
|
||||||
assert_eq!(data.task_id, None);
|
|
||||||
assert_eq!(data.description, None);
|
|
||||||
assert_eq!(data.parent_tool_use_id, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_permission_denied_hook_with_all_fields() {
|
|
||||||
let line = r#"[PermissionDenied Hook] tool_name="Bash", reason="Tool not in allow list""#;
|
|
||||||
let data = parse_permission_denied_hook(line);
|
|
||||||
assert_eq!(data.tool_name, Some("Bash".to_string()));
|
|
||||||
assert_eq!(data.reason, Some("Tool not in allow list".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_permission_denied_hook_tool_only() {
|
|
||||||
let line = r#"[PermissionDenied Hook] tool_name="Edit""#;
|
|
||||||
let data = parse_permission_denied_hook(line);
|
|
||||||
assert_eq!(data.tool_name, Some("Edit".to_string()));
|
|
||||||
assert_eq!(data.reason, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_permission_denied_hook_tool_key_alias() {
|
|
||||||
let line = r#"[PermissionDenied Hook] tool="Write", reason="Workspace not trusted""#;
|
|
||||||
let data = parse_permission_denied_hook(line);
|
|
||||||
assert_eq!(data.tool_name, Some("Write".to_string()));
|
|
||||||
assert_eq!(data.reason, Some("Workspace not trusted".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_permission_denied_hook_empty_line() {
|
|
||||||
let line = "[PermissionDenied Hook]";
|
|
||||||
let data = parse_permission_denied_hook(line);
|
|
||||||
assert_eq!(data.tool_name, None);
|
|
||||||
assert_eq!(data.reason, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_stop_failure_message_no_fields() {
|
fn test_build_stop_failure_message_no_fields() {
|
||||||
let data = StopFailureData {
|
let data = StopFailureData {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "hikari-desktop",
|
"productName": "hikari-desktop",
|
||||||
"version": "1.14.0",
|
"version": "1.13.0",
|
||||||
"identifier": "com.naomi.hikari-desktop",
|
"identifier": "com.naomi.hikari-desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
@@ -67,16 +67,11 @@ async function changeDirectory(path: string): Promise<void> {
|
|||||||
use_worktree: config.use_worktree ?? false,
|
use_worktree: config.use_worktree ?? false,
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
disable_1m_context: config.disable_1m_context ?? false,
|
||||||
max_output_tokens: config.max_output_tokens ?? null,
|
max_output_tokens: config.max_output_tokens ?? null,
|
||||||
disable_cron: config.disable_cron ?? false,
|
|
||||||
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
|
|
||||||
include_git_instructions: config.include_git_instructions ?? true,
|
include_git_instructions: config.include_git_instructions ?? true,
|
||||||
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
||||||
auto_memory_directory: config.auto_memory_directory || null,
|
auto_memory_directory: config.auto_memory_directory || null,
|
||||||
model_overrides: config.model_overrides || null,
|
model_overrides: config.model_overrides || null,
|
||||||
session_name: null,
|
session_name: null,
|
||||||
bare_mode: config.bare_mode ?? false,
|
|
||||||
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
|
|
||||||
custom_model_option: config.custom_model_option || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,16 +149,11 @@ async function startNewConversation(): Promise<void> {
|
|||||||
use_worktree: config.use_worktree ?? false,
|
use_worktree: config.use_worktree ?? false,
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
disable_1m_context: config.disable_1m_context ?? false,
|
||||||
max_output_tokens: config.max_output_tokens ?? null,
|
max_output_tokens: config.max_output_tokens ?? null,
|
||||||
disable_cron: config.disable_cron ?? false,
|
|
||||||
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
|
|
||||||
include_git_instructions: config.include_git_instructions ?? true,
|
include_git_instructions: config.include_git_instructions ?? true,
|
||||||
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
||||||
auto_memory_directory: config.auto_memory_directory || null,
|
auto_memory_directory: config.auto_memory_directory || null,
|
||||||
model_overrides: config.model_overrides || null,
|
model_overrides: config.model_overrides || null,
|
||||||
session_name: null,
|
session_name: null,
|
||||||
bare_mode: config.bare_mode ?? false,
|
|
||||||
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
|
|
||||||
custom_model_option: config.custom_model_option || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
const SUPPORTED_CLI_VERSION = "2.1.104";
|
const SUPPORTED_CLI_VERSION = "2.1.80";
|
||||||
|
|
||||||
let installedVersion = $state("Loading...");
|
let installedVersion = $state("Loading...");
|
||||||
let latestNpmVersion = $state<string | null>(null);
|
let latestNpmVersion = $state<string | null>(null);
|
||||||
|
|||||||
@@ -63,10 +63,6 @@
|
|||||||
enable_claudeai_mcp_servers: true,
|
enable_claudeai_mcp_servers: true,
|
||||||
auto_memory_directory: null,
|
auto_memory_directory: null,
|
||||||
model_overrides: null,
|
model_overrides: null,
|
||||||
disable_skill_shell_execution: false,
|
|
||||||
bare_mode: false,
|
|
||||||
show_clear_context_on_plan_accept: true,
|
|
||||||
custom_model_option: null,
|
|
||||||
max_output_tokens: null,
|
max_output_tokens: null,
|
||||||
trusted_workspaces: [],
|
trusted_workspaces: [],
|
||||||
background_image_path: null,
|
background_image_path: null,
|
||||||
@@ -89,9 +85,6 @@
|
|||||||
let customUiFontStatus: string | null = $state(null);
|
let customUiFontStatus: string | null = $state(null);
|
||||||
let modelOverridesJson = $state("");
|
let modelOverridesJson = $state("");
|
||||||
let modelOverridesError: string | null = $state(null);
|
let modelOverridesError: string | null = $state(null);
|
||||||
let globalClaudeMd = $state("");
|
|
||||||
let globalClaudeMdSaving = $state(false);
|
|
||||||
let globalClaudeMdSaveStatus: string | null = $state(null);
|
|
||||||
|
|
||||||
interface AuthStatus {
|
interface AuthStatus {
|
||||||
is_logged_in: boolean;
|
is_logged_in: boolean;
|
||||||
@@ -129,9 +122,6 @@
|
|||||||
if (open && authStatus === null) {
|
if (open && authStatus === null) {
|
||||||
void refreshAuthStatus();
|
void refreshAuthStatus();
|
||||||
}
|
}
|
||||||
if (open) {
|
|
||||||
void loadGlobalClaudeMd();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
configStore.saveError.subscribe((error) => {
|
configStore.saveError.subscribe((error) => {
|
||||||
@@ -207,30 +197,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadGlobalClaudeMd() {
|
|
||||||
try {
|
|
||||||
globalClaudeMd = await invoke<string>("get_global_claude_md");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load global CLAUDE.md:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveGlobalClaudeMd() {
|
|
||||||
globalClaudeMdSaving = true;
|
|
||||||
globalClaudeMdSaveStatus = null;
|
|
||||||
try {
|
|
||||||
await invoke("save_global_claude_md", { content: globalClaudeMd });
|
|
||||||
globalClaudeMdSaveStatus = "Saved!";
|
|
||||||
setTimeout(() => {
|
|
||||||
globalClaudeMdSaveStatus = null;
|
|
||||||
}, 2000);
|
|
||||||
} catch (error) {
|
|
||||||
globalClaudeMdSaveStatus = `Error: ${error}`;
|
|
||||||
} finally {
|
|
||||||
globalClaudeMdSaving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
isSaving = true;
|
isSaving = true;
|
||||||
saveError = null;
|
saveError = null;
|
||||||
@@ -619,22 +585,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Disable Skill Shell Execution -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="flex items-center gap-3 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={config.disable_skill_shell_execution}
|
|
||||||
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-[var(--text-primary)]">Disable skill shell execution</span>
|
|
||||||
</label>
|
|
||||||
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
||||||
Passes <code class="font-mono">disableSkillShellExecution: true</code> to prevent skill scripts
|
|
||||||
from executing shell commands (requires Claude Code v2.1.91+)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Include Git Instructions -->
|
<!-- Include Git Instructions -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="flex items-center gap-3 cursor-pointer">
|
<label class="flex items-center gap-3 cursor-pointer">
|
||||||
@@ -712,59 +662,6 @@
|
|||||||
defaults.
|
defaults.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bare Mode -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="flex items-center gap-3 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={config.bare_mode}
|
|
||||||
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-[var(--text-primary)]">Bare mode</span>
|
|
||||||
</label>
|
|
||||||
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
||||||
Passes <code class="font-mono">--bare</code> to suppress UI chrome — useful for scripted
|
|
||||||
headless <code class="font-mono">-p</code> calls (requires Claude Code v2.1.81+)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Show Clear Context on Plan Accept -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="flex items-center gap-3 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={config.show_clear_context_on_plan_accept}
|
|
||||||
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-[var(--text-primary)]"
|
|
||||||
>Show clear context prompt on plan accept</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
||||||
When enabled, prompts to clear context when accepting a plan. Passes
|
|
||||||
<code class="font-mono">showClearContextOnPlanAccept: false</code> in
|
|
||||||
<code class="font-mono">--settings</code> when disabled (requires Claude Code v2.1.81+)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Custom Model Option -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="custom-model-option" class="block text-sm text-[var(--text-primary)] mb-1">
|
|
||||||
Custom model option <span class="text-[var(--text-tertiary)]">(optional)</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="custom-model-option"
|
|
||||||
type="text"
|
|
||||||
placeholder="Leave blank to use default"
|
|
||||||
bind:value={config.custom_model_option}
|
|
||||||
class="w-full px-3 py-2 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-[var(--text-tertiary)] mt-1">
|
|
||||||
Sets <code class="font-mono">ANTHROPIC_CUSTOM_MODEL_OPTION</code> environment variable for custom
|
|
||||||
model providers (requires Claude Code v2.1.81+)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Greeting Section -->
|
<!-- Greeting Section -->
|
||||||
@@ -805,47 +702,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Global Instructions Section -->
|
|
||||||
<section class="mb-6">
|
|
||||||
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
||||||
Global Instructions
|
|
||||||
</h3>
|
|
||||||
<div class="mb-2">
|
|
||||||
<label for="global-claude-md" class="block text-sm text-[var(--text-secondary)] mb-1">
|
|
||||||
~/.claude/CLAUDE.md
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="global-claude-md"
|
|
||||||
bind:value={globalClaudeMd}
|
|
||||||
rows="12"
|
|
||||||
placeholder="Add persistent instructions for all Claude Code sessions..."
|
|
||||||
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-y"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onclick={saveGlobalClaudeMd}
|
|
||||||
disabled={globalClaudeMdSaving}
|
|
||||||
class="px-3 py-1.5 text-sm bg-[var(--accent-primary)] text-white rounded hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{globalClaudeMdSaving ? "Saving..." : "Save"}
|
|
||||||
</button>
|
|
||||||
{#if globalClaudeMdSaveStatus}
|
|
||||||
<span
|
|
||||||
class="text-xs {globalClaudeMdSaveStatus.startsWith('Error')
|
|
||||||
? 'text-red-500'
|
|
||||||
: 'text-green-500'}"
|
|
||||||
>
|
|
||||||
{globalClaudeMdSaveStatus}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-[var(--text-tertiary)] mt-2">
|
|
||||||
Persistent instructions applied to all Claude Code sessions. Changes take effect on the next
|
|
||||||
session start.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- MCP Servers Section -->
|
<!-- MCP Servers Section -->
|
||||||
<section class="mb-6">
|
<section class="mb-6">
|
||||||
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
||||||
|
|||||||
@@ -68,16 +68,11 @@
|
|||||||
allowed_tools: grantedToolsList,
|
allowed_tools: grantedToolsList,
|
||||||
use_worktree: config.use_worktree ?? false,
|
use_worktree: config.use_worktree ?? false,
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
disable_1m_context: config.disable_1m_context ?? false,
|
||||||
disable_cron: config.disable_cron ?? false,
|
|
||||||
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
|
|
||||||
include_git_instructions: config.include_git_instructions ?? true,
|
include_git_instructions: config.include_git_instructions ?? true,
|
||||||
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
||||||
auto_memory_directory: config.auto_memory_directory || null,
|
auto_memory_directory: config.auto_memory_directory || null,
|
||||||
model_overrides: config.model_overrides || null,
|
model_overrides: config.model_overrides || null,
|
||||||
session_name: null,
|
session_name: null,
|
||||||
bare_mode: config.bare_mode ?? false,
|
|
||||||
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
|
|
||||||
custom_model_option: config.custom_model_option || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -402,16 +402,11 @@ User: ${formattedMessage}`;
|
|||||||
allowed_tools: allAllowedTools,
|
allowed_tools: allAllowedTools,
|
||||||
use_worktree: config.use_worktree ?? false,
|
use_worktree: config.use_worktree ?? false,
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
disable_1m_context: config.disable_1m_context ?? false,
|
||||||
disable_cron: config.disable_cron ?? false,
|
|
||||||
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
|
|
||||||
include_git_instructions: config.include_git_instructions ?? true,
|
include_git_instructions: config.include_git_instructions ?? true,
|
||||||
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
||||||
auto_memory_directory: config.auto_memory_directory || null,
|
auto_memory_directory: config.auto_memory_directory || null,
|
||||||
model_overrides: config.model_overrides || null,
|
model_overrides: config.model_overrides || null,
|
||||||
session_name: null,
|
session_name: null,
|
||||||
bare_mode: config.bare_mode ?? false,
|
|
||||||
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
|
|
||||||
custom_model_option: config.custom_model_option || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -89,16 +89,11 @@
|
|||||||
allowed_tools: [...new Set([...newGrantedTools, ...config.auto_granted_tools])],
|
allowed_tools: [...new Set([...newGrantedTools, ...config.auto_granted_tools])],
|
||||||
use_worktree: config.use_worktree ?? false,
|
use_worktree: config.use_worktree ?? false,
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
disable_1m_context: config.disable_1m_context ?? false,
|
||||||
disable_cron: config.disable_cron ?? false,
|
|
||||||
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
|
|
||||||
include_git_instructions: config.include_git_instructions ?? true,
|
include_git_instructions: config.include_git_instructions ?? true,
|
||||||
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
||||||
auto_memory_directory: config.auto_memory_directory || null,
|
auto_memory_directory: config.auto_memory_directory || null,
|
||||||
model_overrides: config.model_overrides || null,
|
model_overrides: config.model_overrides || null,
|
||||||
session_name: null,
|
session_name: null,
|
||||||
bare_mode: config.bare_mode ?? false,
|
|
||||||
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
|
|
||||||
custom_model_option: config.custom_model_option || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -93,10 +93,6 @@
|
|||||||
enable_claudeai_mcp_servers: true,
|
enable_claudeai_mcp_servers: true,
|
||||||
auto_memory_directory: null,
|
auto_memory_directory: null,
|
||||||
model_overrides: null,
|
model_overrides: null,
|
||||||
disable_skill_shell_execution: false,
|
|
||||||
bare_mode: false,
|
|
||||||
show_clear_context_on_plan_accept: true,
|
|
||||||
custom_model_option: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let streamerModeActive = $state(false);
|
let streamerModeActive = $state(false);
|
||||||
@@ -178,17 +174,11 @@
|
|||||||
use_worktree: currentConfig.use_worktree ?? false,
|
use_worktree: currentConfig.use_worktree ?? false,
|
||||||
disable_1m_context: currentConfig.disable_1m_context ?? false,
|
disable_1m_context: currentConfig.disable_1m_context ?? false,
|
||||||
max_output_tokens: currentConfig.max_output_tokens ?? null,
|
max_output_tokens: currentConfig.max_output_tokens ?? null,
|
||||||
disable_cron: currentConfig.disable_cron ?? false,
|
|
||||||
disable_skill_shell_execution: currentConfig.disable_skill_shell_execution ?? false,
|
|
||||||
include_git_instructions: currentConfig.include_git_instructions ?? true,
|
include_git_instructions: currentConfig.include_git_instructions ?? true,
|
||||||
enable_claudeai_mcp_servers: currentConfig.enable_claudeai_mcp_servers ?? true,
|
enable_claudeai_mcp_servers: currentConfig.enable_claudeai_mcp_servers ?? true,
|
||||||
auto_memory_directory: currentConfig.auto_memory_directory || null,
|
auto_memory_directory: currentConfig.auto_memory_directory || null,
|
||||||
model_overrides: currentConfig.model_overrides || null,
|
model_overrides: currentConfig.model_overrides || null,
|
||||||
session_name: activeConversationForName?.name || null,
|
session_name: activeConversationForName?.name || null,
|
||||||
bare_mode: currentConfig.bare_mode ?? false,
|
|
||||||
show_clear_context_on_plan_accept:
|
|
||||||
currentConfig.show_clear_context_on_plan_accept ?? true,
|
|
||||||
custom_model_option: currentConfig.custom_model_option || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -346,17 +336,11 @@
|
|||||||
use_worktree: currentConfig.use_worktree ?? false,
|
use_worktree: currentConfig.use_worktree ?? false,
|
||||||
disable_1m_context: currentConfig.disable_1m_context ?? false,
|
disable_1m_context: currentConfig.disable_1m_context ?? false,
|
||||||
max_output_tokens: currentConfig.max_output_tokens ?? null,
|
max_output_tokens: currentConfig.max_output_tokens ?? null,
|
||||||
disable_cron: currentConfig.disable_cron ?? false,
|
|
||||||
disable_skill_shell_execution: currentConfig.disable_skill_shell_execution ?? false,
|
|
||||||
include_git_instructions: currentConfig.include_git_instructions ?? true,
|
include_git_instructions: currentConfig.include_git_instructions ?? true,
|
||||||
enable_claudeai_mcp_servers: currentConfig.enable_claudeai_mcp_servers ?? true,
|
enable_claudeai_mcp_servers: currentConfig.enable_claudeai_mcp_servers ?? true,
|
||||||
auto_memory_directory: currentConfig.auto_memory_directory || null,
|
auto_memory_directory: currentConfig.auto_memory_directory || null,
|
||||||
model_overrides: currentConfig.model_overrides || null,
|
model_overrides: currentConfig.model_overrides || null,
|
||||||
session_name: null,
|
session_name: null,
|
||||||
bare_mode: currentConfig.bare_mode ?? false,
|
|
||||||
show_clear_context_on_plan_accept:
|
|
||||||
currentConfig.show_clear_context_on_plan_accept ?? true,
|
|
||||||
custom_model_option: currentConfig.custom_model_option || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -218,16 +218,11 @@
|
|||||||
use_worktree: cfg.use_worktree ?? false,
|
use_worktree: cfg.use_worktree ?? false,
|
||||||
disable_1m_context: cfg.disable_1m_context ?? false,
|
disable_1m_context: cfg.disable_1m_context ?? false,
|
||||||
max_output_tokens: cfg.max_output_tokens ?? null,
|
max_output_tokens: cfg.max_output_tokens ?? null,
|
||||||
disable_cron: cfg.disable_cron ?? false,
|
|
||||||
disable_skill_shell_execution: cfg.disable_skill_shell_execution ?? false,
|
|
||||||
include_git_instructions: cfg.include_git_instructions ?? true,
|
include_git_instructions: cfg.include_git_instructions ?? true,
|
||||||
enable_claudeai_mcp_servers: cfg.enable_claudeai_mcp_servers ?? true,
|
enable_claudeai_mcp_servers: cfg.enable_claudeai_mcp_servers ?? true,
|
||||||
auto_memory_directory: cfg.auto_memory_directory || null,
|
auto_memory_directory: cfg.auto_memory_directory || null,
|
||||||
model_overrides: cfg.model_overrides || null,
|
model_overrides: cfg.model_overrides || null,
|
||||||
session_name: null,
|
session_name: null,
|
||||||
bare_mode: cfg.bare_mode ?? false,
|
|
||||||
show_clear_context_on_plan_accept: cfg.show_clear_context_on_plan_accept ?? true,
|
|
||||||
custom_model_option: cfg.custom_model_option || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -108,16 +108,11 @@
|
|||||||
allowed_tools: grantedToolsList,
|
allowed_tools: grantedToolsList,
|
||||||
use_worktree: config.use_worktree ?? false,
|
use_worktree: config.use_worktree ?? false,
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
disable_1m_context: config.disable_1m_context ?? false,
|
||||||
disable_cron: config.disable_cron ?? false,
|
|
||||||
disable_skill_shell_execution: config.disable_skill_shell_execution ?? false,
|
|
||||||
include_git_instructions: config.include_git_instructions ?? true,
|
include_git_instructions: config.include_git_instructions ?? true,
|
||||||
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
enable_claudeai_mcp_servers: config.enable_claudeai_mcp_servers ?? true,
|
||||||
auto_memory_directory: config.auto_memory_directory || null,
|
auto_memory_directory: config.auto_memory_directory || null,
|
||||||
model_overrides: config.model_overrides || null,
|
model_overrides: config.model_overrides || null,
|
||||||
session_name: null,
|
session_name: null,
|
||||||
bare_mode: config.bare_mode ?? false,
|
|
||||||
show_clear_context_on_plan_accept: config.show_clear_context_on_plan_accept ?? true,
|
|
||||||
custom_model_option: config.custom_model_option || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -225,10 +225,6 @@ describe("config store", () => {
|
|||||||
enable_claudeai_mcp_servers: true,
|
enable_claudeai_mcp_servers: true,
|
||||||
auto_memory_directory: null,
|
auto_memory_directory: null,
|
||||||
model_overrides: null,
|
model_overrides: null,
|
||||||
disable_skill_shell_execution: false,
|
|
||||||
bare_mode: false,
|
|
||||||
show_clear_context_on_plan_accept: true,
|
|
||||||
custom_model_option: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(config.model).toBe("claude-sonnet-4");
|
expect(config.model).toBe("claude-sonnet-4");
|
||||||
@@ -293,10 +289,6 @@ describe("config store", () => {
|
|||||||
enable_claudeai_mcp_servers: true,
|
enable_claudeai_mcp_servers: true,
|
||||||
auto_memory_directory: null,
|
auto_memory_directory: null,
|
||||||
model_overrides: null,
|
model_overrides: null,
|
||||||
disable_skill_shell_execution: false,
|
|
||||||
bare_mode: false,
|
|
||||||
show_clear_context_on_plan_accept: true,
|
|
||||||
custom_model_option: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(config.model).toBeNull();
|
expect(config.model).toBeNull();
|
||||||
@@ -916,10 +908,6 @@ describe("config store", () => {
|
|||||||
enable_claudeai_mcp_servers: true,
|
enable_claudeai_mcp_servers: true,
|
||||||
auto_memory_directory: null,
|
auto_memory_directory: null,
|
||||||
model_overrides: null,
|
model_overrides: null,
|
||||||
disable_skill_shell_execution: false,
|
|
||||||
bare_mode: false,
|
|
||||||
show_clear_context_on_plan_accept: true,
|
|
||||||
custom_model_option: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockInvokeImpl = vi.mocked(invoke);
|
const mockInvokeImpl = vi.mocked(invoke);
|
||||||
|
|||||||
@@ -91,14 +91,6 @@ export interface HikariConfig {
|
|||||||
auto_memory_directory: string | null;
|
auto_memory_directory: string | null;
|
||||||
// Model overrides for provider-specific model IDs (AWS Bedrock, Google Vertex, etc.)
|
// Model overrides for provider-specific model IDs (AWS Bedrock, Google Vertex, etc.)
|
||||||
model_overrides: Record<string, string> | null;
|
model_overrides: Record<string, string> | null;
|
||||||
// Prevents skill scripts from executing shell commands (Claude Code v2.1.91+)
|
|
||||||
disable_skill_shell_execution: boolean;
|
|
||||||
// Bare mode — suppress UI chrome for scripted headless -p calls (v2.1.81+)
|
|
||||||
bare_mode: boolean;
|
|
||||||
// Show clear context dialog when accepting a plan (v2.1.81+)
|
|
||||||
show_clear_context_on_plan_accept: boolean;
|
|
||||||
// Custom model option env var (v2.1.81+)
|
|
||||||
custom_model_option: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig: HikariConfig = {
|
const defaultConfig: HikariConfig = {
|
||||||
@@ -157,10 +149,6 @@ const defaultConfig: HikariConfig = {
|
|||||||
enable_claudeai_mcp_servers: true,
|
enable_claudeai_mcp_servers: true,
|
||||||
auto_memory_directory: null,
|
auto_memory_directory: null,
|
||||||
model_overrides: null,
|
model_overrides: null,
|
||||||
disable_skill_shell_execution: false,
|
|
||||||
bare_mode: false,
|
|
||||||
show_clear_context_on_plan_accept: true,
|
|
||||||
custom_model_option: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function createConfigStore() {
|
function createConfigStore() {
|
||||||
|
|||||||
Reference in New Issue
Block a user