generated from nhcarrigan/template
Compare commits
6 Commits
v1.7.0
..
9c48d5f9ae
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c48d5f9ae | |||
| d605ea15a1 | |||
| 0abf410aa9 | |||
| b8aefef071 | |||
| dd95750a8d | |||
| a79808641b |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hikari-desktop",
|
"name": "hikari-desktop",
|
||||||
"version": "1.7.0",
|
"version": "1.5.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+1
-1
@@ -1636,7 +1636,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hikari-desktop"
|
name = "hikari-desktop"
|
||||||
version = "1.7.0"
|
version = "1.5.1"
|
||||||
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.7.0"
|
version = "1.5.1"
|
||||||
description = "Hikari - Claude Code Visual Assistant"
|
description = "Hikari - Claude Code Visual Assistant"
|
||||||
authors = ["Naomi Carrigan"]
|
authors = ["Naomi Carrigan"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1360,136 +1360,6 @@ pub async fn get_claude_version() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Auth Commands ====================
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ClaudeAuthStatus {
|
|
||||||
pub is_logged_in: bool,
|
|
||||||
pub email: Option<String>,
|
|
||||||
pub org_name: Option<String>,
|
|
||||||
pub api_key_source: Option<String>,
|
|
||||||
pub api_provider: Option<String>,
|
|
||||||
pub subscription_type: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_auth_status() -> Result<ClaudeAuthStatus, String> {
|
|
||||||
tracing::debug!("Getting Claude auth status");
|
|
||||||
|
|
||||||
let output = create_claude_command()
|
|
||||||
.args(["auth", "status"])
|
|
||||||
.output()
|
|
||||||
.map_err(|e| format!("Failed to run claude auth status: {}", e))?;
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
||||||
let raw = if stdout.is_empty() { &stderr } else { &stdout };
|
|
||||||
|
|
||||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(raw) {
|
|
||||||
let is_logged_in = json
|
|
||||||
.get("loggedIn")
|
|
||||||
.and_then(|v| v.as_bool())
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
let email = json
|
|
||||||
.get("email")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(String::from);
|
|
||||||
|
|
||||||
let org_name = json
|
|
||||||
.get("orgName")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(String::from);
|
|
||||||
|
|
||||||
let api_key_source = json
|
|
||||||
.get("apiKeySource")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(String::from);
|
|
||||||
|
|
||||||
let api_provider = json
|
|
||||||
.get("apiProvider")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(String::from);
|
|
||||||
|
|
||||||
let subscription_type = json
|
|
||||||
.get("subscriptionType")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(String::from);
|
|
||||||
|
|
||||||
tracing::info!("Claude auth status: logged_in={}", is_logged_in);
|
|
||||||
Ok(ClaudeAuthStatus {
|
|
||||||
is_logged_in,
|
|
||||||
email,
|
|
||||||
org_name,
|
|
||||||
api_key_source,
|
|
||||||
api_provider,
|
|
||||||
subscription_type,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Non-JSON output: fall back to heuristic
|
|
||||||
let lower = raw.to_lowercase();
|
|
||||||
let is_logged_in = output.status.success()
|
|
||||||
&& !lower.contains("not logged in")
|
|
||||||
&& !lower.contains("not authenticated")
|
|
||||||
&& !lower.contains("no account");
|
|
||||||
tracing::info!("Claude auth status (non-JSON): logged_in={}", is_logged_in);
|
|
||||||
Ok(ClaudeAuthStatus {
|
|
||||||
is_logged_in,
|
|
||||||
email: None,
|
|
||||||
org_name: None,
|
|
||||||
api_key_source: None,
|
|
||||||
api_provider: None,
|
|
||||||
subscription_type: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn auth_login() -> Result<String, String> {
|
|
||||||
tracing::info!("Running claude auth login");
|
|
||||||
|
|
||||||
let output = create_claude_command()
|
|
||||||
.args(["auth", "login"])
|
|
||||||
.output()
|
|
||||||
.map_err(|e| format!("Failed to run claude auth login: {}", e))?;
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
let message = if stdout.is_empty() { "Login successful".to_string() } else { stdout };
|
|
||||||
tracing::info!("Claude auth login succeeded");
|
|
||||||
Ok(message)
|
|
||||||
} else {
|
|
||||||
let error = if stderr.is_empty() { stdout } else { stderr };
|
|
||||||
tracing::error!("Claude auth login failed: {}", error);
|
|
||||||
Err(format!("Login failed: {}", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn auth_logout() -> Result<String, String> {
|
|
||||||
tracing::info!("Running claude auth logout");
|
|
||||||
|
|
||||||
let output = create_claude_command()
|
|
||||||
.args(["auth", "logout"])
|
|
||||||
.output()
|
|
||||||
.map_err(|e| format!("Failed to run claude auth logout: {}", e))?;
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
let message = if stdout.is_empty() { "Logged out successfully".to_string() } else { stdout };
|
|
||||||
tracing::info!("Claude auth logout succeeded");
|
|
||||||
Ok(message)
|
|
||||||
} else {
|
|
||||||
let error = if stderr.is_empty() { stdout } else { stderr };
|
|
||||||
tracing::error!("Claude auth logout failed: {}", error);
|
|
||||||
Err(format!("Logout failed: {}", error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Plugin Management Commands ====================
|
// ==================== Plugin Management Commands ====================
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -25,12 +25,6 @@ pub struct ClaudeStartOptions {
|
|||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub resume_session_id: Option<String>,
|
pub resume_session_id: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub use_worktree: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub disable_1m_context: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -119,12 +113,6 @@ pub struct HikariConfig {
|
|||||||
|
|
||||||
#[serde(default = "default_discord_rpc_enabled")]
|
#[serde(default = "default_discord_rpc_enabled")]
|
||||||
pub discord_rpc_enabled: bool,
|
pub discord_rpc_enabled: bool,
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub use_worktree: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub disable_1m_context: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HikariConfig {
|
impl Default for HikariConfig {
|
||||||
@@ -157,8 +145,6 @@ impl Default for HikariConfig {
|
|||||||
budget_action: BudgetAction::Warn,
|
budget_action: BudgetAction::Warn,
|
||||||
budget_warning_threshold: 0.8,
|
budget_warning_threshold: 0.8,
|
||||||
discord_rpc_enabled: true,
|
discord_rpc_enabled: true,
|
||||||
use_worktree: false,
|
|
||||||
disable_1m_context: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,8 +252,6 @@ mod tests {
|
|||||||
assert_eq!(config.budget_action, BudgetAction::Warn);
|
assert_eq!(config.budget_action, BudgetAction::Warn);
|
||||||
assert!((config.budget_warning_threshold - 0.8).abs() < f32::EPSILON);
|
assert!((config.budget_warning_threshold - 0.8).abs() < f32::EPSILON);
|
||||||
assert!(config.discord_rpc_enabled);
|
assert!(config.discord_rpc_enabled);
|
||||||
assert!(!config.use_worktree);
|
|
||||||
assert!(!config.disable_1m_context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -300,8 +284,6 @@ mod tests {
|
|||||||
budget_action: BudgetAction::Block,
|
budget_action: BudgetAction::Block,
|
||||||
budget_warning_threshold: 0.75,
|
budget_warning_threshold: 0.75,
|
||||||
discord_rpc_enabled: true,
|
discord_rpc_enabled: true,
|
||||||
use_worktree: true,
|
|
||||||
disable_1m_context: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string(&config).unwrap();
|
let json = serde_json::to_string(&config).unwrap();
|
||||||
|
|||||||
@@ -195,9 +195,6 @@ pub fn run() {
|
|||||||
close_application,
|
close_application,
|
||||||
list_memory_files,
|
list_memory_files,
|
||||||
get_claude_version,
|
get_claude_version,
|
||||||
get_auth_status,
|
|
||||||
auth_login,
|
|
||||||
auth_logout,
|
|
||||||
list_plugins,
|
list_plugins,
|
||||||
install_plugin,
|
install_plugin,
|
||||||
uninstall_plugin,
|
uninstall_plugin,
|
||||||
|
|||||||
@@ -63,26 +63,6 @@ pub struct PermissionDenial {
|
|||||||
pub tool_input: serde_json::Value,
|
pub tool_input: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rate limit information from a `rate_limit_event` message.
|
|
||||||
/// All fields are optional to ensure forward-compatibility as the Claude CLI evolves.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct RateLimitInfo {
|
|
||||||
#[serde(default)]
|
|
||||||
pub requests_limit: Option<u64>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub requests_remaining: Option<u64>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub requests_reset: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub tokens_limit: Option<u64>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub tokens_remaining: Option<u64>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub tokens_reset: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub retry_after_ms: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum ClaudeMessage {
|
pub enum ClaudeMessage {
|
||||||
@@ -120,11 +100,6 @@ pub enum ClaudeMessage {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
usage: Option<UsageInfo>,
|
usage: Option<UsageInfo>,
|
||||||
},
|
},
|
||||||
#[serde(rename = "rate_limit_event")]
|
|
||||||
RateLimitEvent {
|
|
||||||
#[serde(default)]
|
|
||||||
rate_limit_info: RateLimitInfo,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -305,8 +280,6 @@ pub struct AgentEndEvent {
|
|||||||
pub duration_ms: Option<u64>,
|
pub duration_ms: Option<u64>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub num_turns: Option<u32>,
|
pub num_turns: Option<u32>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub last_assistant_message: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -473,77 +446,4 @@ mod tests {
|
|||||||
assert!(serialized.contains("\"input_tokens\":100"));
|
assert!(serialized.contains("\"input_tokens\":100"));
|
||||||
assert!(serialized.contains("\"output_tokens\":50"));
|
assert!(serialized.contains("\"output_tokens\":50"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rate_limit_info_default() {
|
|
||||||
let info = RateLimitInfo::default();
|
|
||||||
assert!(info.requests_limit.is_none());
|
|
||||||
assert!(info.requests_remaining.is_none());
|
|
||||||
assert!(info.requests_reset.is_none());
|
|
||||||
assert!(info.tokens_limit.is_none());
|
|
||||||
assert!(info.tokens_remaining.is_none());
|
|
||||||
assert!(info.tokens_reset.is_none());
|
|
||||||
assert!(info.retry_after_ms.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rate_limit_event_deserialization_empty_info() {
|
|
||||||
let json = r#"{"type":"rate_limit_event","rate_limit_info":{}}"#;
|
|
||||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
||||||
assert!(matches!(msg, ClaudeMessage::RateLimitEvent { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rate_limit_event_deserialization_no_info() {
|
|
||||||
// rate_limit_info field is optional via #[serde(default)]
|
|
||||||
let json = r#"{"type":"rate_limit_event"}"#;
|
|
||||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
||||||
assert!(matches!(msg, ClaudeMessage::RateLimitEvent { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rate_limit_event_deserialization_with_data() {
|
|
||||||
let json = r#"{
|
|
||||||
"type": "rate_limit_event",
|
|
||||||
"rate_limit_info": {
|
|
||||||
"requests_limit": 1000,
|
|
||||||
"requests_remaining": 0,
|
|
||||||
"requests_reset": "2024-01-01T00:01:00Z",
|
|
||||||
"tokens_limit": 50000,
|
|
||||||
"tokens_remaining": 0,
|
|
||||||
"tokens_reset": "2024-01-01T00:01:00Z",
|
|
||||||
"retry_after_ms": 60000
|
|
||||||
}
|
|
||||||
}"#;
|
|
||||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
||||||
if let ClaudeMessage::RateLimitEvent { rate_limit_info } = msg {
|
|
||||||
assert_eq!(rate_limit_info.requests_limit, Some(1000));
|
|
||||||
assert_eq!(rate_limit_info.requests_remaining, Some(0));
|
|
||||||
assert_eq!(
|
|
||||||
rate_limit_info.requests_reset,
|
|
||||||
Some("2024-01-01T00:01:00Z".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(rate_limit_info.retry_after_ms, Some(60000));
|
|
||||||
} else {
|
|
||||||
panic!("Expected RateLimitEvent variant");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rate_limit_event_ignores_unknown_fields() {
|
|
||||||
// Ensures forward-compat: unknown fields in rate_limit_info are silently ignored
|
|
||||||
let json = r#"{
|
|
||||||
"type": "rate_limit_event",
|
|
||||||
"rate_limit_info": {
|
|
||||||
"requests_remaining": 0,
|
|
||||||
"some_future_field": "some_value"
|
|
||||||
}
|
|
||||||
}"#;
|
|
||||||
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
||||||
if let ClaudeMessage::RateLimitEvent { rate_limit_info } = msg {
|
|
||||||
assert_eq!(rate_limit_info.requests_remaining, Some(0));
|
|
||||||
} else {
|
|
||||||
panic!("Expected RateLimitEvent variant");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-304
@@ -265,11 +265,6 @@ impl WslBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add worktree flag if requested
|
|
||||||
if options.use_worktree {
|
|
||||||
cmd.arg("--worktree");
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.current_dir(working_dir);
|
cmd.current_dir(working_dir);
|
||||||
|
|
||||||
// Set API key as environment variable if specified
|
// Set API key as environment variable if specified
|
||||||
@@ -279,11 +274,6 @@ impl WslBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable 1M context window if requested
|
|
||||||
if options.disable_1m_context {
|
|
||||||
cmd.env("CLAUDE_CODE_DISABLE_1M_CONTEXT", "1");
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -324,11 +314,6 @@ impl WslBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable 1M context window if requested
|
|
||||||
if options.disable_1m_context {
|
|
||||||
claude_cmd.push_str("CLAUDE_CODE_DISABLE_1M_CONTEXT=1 ");
|
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
||||||
);
|
);
|
||||||
@@ -366,11 +351,6 @@ impl WslBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add worktree flag if requested
|
|
||||||
if options.use_worktree {
|
|
||||||
claude_cmd.push_str(" --worktree");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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]);
|
||||||
|
|
||||||
@@ -765,28 +745,17 @@ fn handle_stderr(
|
|||||||
conversation_id: conversation_id.clone(),
|
conversation_id: conversation_id.clone(),
|
||||||
duration_ms: None,
|
duration_ms: None,
|
||||||
num_turns: None,
|
num_turns: None,
|
||||||
last_assistant_message: stop_data.last_assistant_message,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hook events are informational — emit with distinct types instead of error
|
// Still emit the stderr line as output
|
||||||
let line_type = if line.contains("[WorktreeCreate Hook]")
|
|
||||||
|| line.contains("[WorktreeRemove Hook]")
|
|
||||||
{
|
|
||||||
"worktree"
|
|
||||||
} else if line.contains("[ConfigChange Hook]") {
|
|
||||||
"config-change"
|
|
||||||
} else {
|
|
||||||
"error"
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = app.emit(
|
let _ = app.emit(
|
||||||
"claude:output",
|
"claude:output",
|
||||||
OutputEvent {
|
OutputEvent {
|
||||||
line_type: line_type.to_string(),
|
line_type: "error".to_string(),
|
||||||
content: line,
|
content: line,
|
||||||
tool_name: None,
|
tool_name: None,
|
||||||
conversation_id: conversation_id.clone(),
|
conversation_id: conversation_id.clone(),
|
||||||
@@ -839,78 +808,27 @@ fn parse_subagent_start_hook(line: &str) -> Option<SubagentStartData> {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct SubagentStopData {
|
struct SubagentStopData {
|
||||||
parent_tool_use_id: Option<String>,
|
parent_tool_use_id: Option<String>,
|
||||||
last_assistant_message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts the content of a Rust Debug-formatted `Some("...")` field from a hook line.
|
|
||||||
/// Handles escaped characters (e.g. `\"` → `"`, `\\` → `\`, `\n` → newline).
|
|
||||||
/// Returns `None` if the field is absent or formatted as `None`.
|
|
||||||
fn extract_debug_string_value(line: &str, key: &str) -> Option<String> {
|
|
||||||
let prefix = format!("{}=Some(\"", key);
|
|
||||||
let start_idx = line.find(&prefix)? + prefix.len();
|
|
||||||
let rest = &line[start_idx..];
|
|
||||||
|
|
||||||
let mut result = String::new();
|
|
||||||
let mut chars = rest.chars();
|
|
||||||
loop {
|
|
||||||
match chars.next() {
|
|
||||||
Some('"') => return Some(result),
|
|
||||||
Some('\\') => match chars.next() {
|
|
||||||
Some('n') => result.push('\n'),
|
|
||||||
Some('t') => result.push('\t'),
|
|
||||||
Some('"') => result.push('"'),
|
|
||||||
Some('\\') => result.push('\\'),
|
|
||||||
Some(c) => {
|
|
||||||
result.push('\\');
|
|
||||||
result.push(c);
|
|
||||||
}
|
|
||||||
None => break,
|
|
||||||
},
|
|
||||||
Some(c) => result.push(c),
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_subagent_stop_hook(line: &str) -> Option<SubagentStopData> {
|
fn parse_subagent_stop_hook(line: &str) -> Option<SubagentStopData> {
|
||||||
// Parse: [SubagentStop Hook] ... parent_tool_use_id=Some("toolu_xxx"), last_assistant_message=Some("..."), ...
|
// Parse: [SubagentStop Hook] ... parent_tool_use_id=Some("toolu_xxx"), ...
|
||||||
|
|
||||||
let parent_tool_use_id = extract_debug_string_value(line, "parent_tool_use_id");
|
// Extract parent_tool_use_id if present
|
||||||
let last_assistant_message = extract_debug_string_value(line, "last_assistant_message");
|
let parent_tool_use_id = if line.contains("parent_tool_use_id=Some") {
|
||||||
|
line.split("parent_tool_use_id=Some(\"")
|
||||||
|
.nth(1)?
|
||||||
|
.split('"')
|
||||||
|
.next()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Some(SubagentStopData {
|
Some(SubagentStopData {
|
||||||
parent_tool_use_id,
|
parent_tool_use_id,
|
||||||
last_assistant_message,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract text content from a ToolResult's `content` field.
|
|
||||||
/// The content may be a JSON string or an array of typed content blocks.
|
|
||||||
fn extract_tool_result_text(content: &serde_json::Value) -> Option<String> {
|
|
||||||
match content {
|
|
||||||
serde_json::Value::String(s) if !s.is_empty() => Some(s.clone()),
|
|
||||||
serde_json::Value::Array(blocks) => {
|
|
||||||
let texts: Vec<String> = blocks
|
|
||||||
.iter()
|
|
||||||
.filter_map(|block| {
|
|
||||||
if block.get("type")?.as_str()? == "text" {
|
|
||||||
block.get("text")?.as_str().map(String::from)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if texts.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(texts.join("\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_json_line(
|
fn process_json_line(
|
||||||
line: &str,
|
line: &str,
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
@@ -1164,37 +1082,17 @@ fn process_json_line(
|
|||||||
stats.write().increment_code_blocks();
|
stats.write().increment_code_blocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_prompt_too_long = text.starts_with("Prompt is too long");
|
|
||||||
|
|
||||||
let _ = app.emit(
|
let _ = app.emit(
|
||||||
"claude:output",
|
"claude:output",
|
||||||
OutputEvent {
|
OutputEvent {
|
||||||
line_type: if is_prompt_too_long {
|
line_type: "assistant".to_string(),
|
||||||
"error".to_string()
|
|
||||||
} else {
|
|
||||||
"assistant".to_string()
|
|
||||||
},
|
|
||||||
content: text.clone(),
|
content: text.clone(),
|
||||||
tool_name: None,
|
tool_name: None,
|
||||||
conversation_id: conversation_id.clone(),
|
conversation_id: conversation_id.clone(),
|
||||||
cost: message_cost.clone(),
|
cost: message_cost.clone(), // Include cost with assistant text
|
||||||
parent_tool_use_id: parent_tool_use_id.clone(),
|
parent_tool_use_id: parent_tool_use_id.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if is_prompt_too_long {
|
|
||||||
let _ = app.emit(
|
|
||||||
"claude:output",
|
|
||||||
OutputEvent {
|
|
||||||
line_type: "compact-prompt".to_string(),
|
|
||||||
content: String::new(),
|
|
||||||
tool_name: None,
|
|
||||||
conversation_id: conversation_id.clone(),
|
|
||||||
cost: None,
|
|
||||||
parent_tool_use_id: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ContentBlock::Thinking { thinking } => {
|
ContentBlock::Thinking { thinking } => {
|
||||||
state = CharacterState::Thinking;
|
state = CharacterState::Thinking;
|
||||||
@@ -1212,8 +1110,8 @@ fn process_json_line(
|
|||||||
}
|
}
|
||||||
ContentBlock::ToolResult {
|
ContentBlock::ToolResult {
|
||||||
tool_use_id,
|
tool_use_id,
|
||||||
content,
|
|
||||||
is_error,
|
is_error,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
// Emit agent-end for all tool results
|
// Emit agent-end for all tool results
|
||||||
// The frontend will ignore IDs that don't match known agents
|
// The frontend will ignore IDs that don't match known agents
|
||||||
@@ -1231,7 +1129,6 @@ fn process_json_line(
|
|||||||
conversation_id: conversation_id.clone(),
|
conversation_id: conversation_id.clone(),
|
||||||
duration_ms: None,
|
duration_ms: None,
|
||||||
num_turns: None,
|
num_turns: None,
|
||||||
last_assistant_message: extract_tool_result_text(content),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1624,23 +1521,6 @@ fn process_json_line(
|
|||||||
emit_state_change(app, state, None, conversation_id.clone());
|
emit_state_change(app, state, None, conversation_id.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
ClaudeMessage::RateLimitEvent { rate_limit_info } => {
|
|
||||||
tracing::warn!("Rate limit event received: {:?}", rate_limit_info);
|
|
||||||
|
|
||||||
let content = format_rate_limit_message(rate_limit_info);
|
|
||||||
let _ = app.emit(
|
|
||||||
"claude:output",
|
|
||||||
OutputEvent {
|
|
||||||
line_type: "rate-limit".to_string(),
|
|
||||||
content,
|
|
||||||
tool_name: None,
|
|
||||||
conversation_id: conversation_id.clone(),
|
|
||||||
cost: None,
|
|
||||||
parent_tool_use_id: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ClaudeMessage::User { message } => {
|
ClaudeMessage::User { message } => {
|
||||||
// Increment message count for user messages
|
// Increment message count for user messages
|
||||||
stats.write().increment_messages();
|
stats.write().increment_messages();
|
||||||
@@ -1649,8 +1529,8 @@ fn process_json_line(
|
|||||||
for block in &message.content {
|
for block in &message.content {
|
||||||
if let ContentBlock::ToolResult {
|
if let ContentBlock::ToolResult {
|
||||||
tool_use_id,
|
tool_use_id,
|
||||||
content,
|
|
||||||
is_error,
|
is_error,
|
||||||
|
..
|
||||||
} = block
|
} = block
|
||||||
{
|
{
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
@@ -1667,7 +1547,6 @@ fn process_json_line(
|
|||||||
conversation_id: conversation_id.clone(),
|
conversation_id: conversation_id.clone(),
|
||||||
duration_ms: None,
|
duration_ms: None,
|
||||||
num_turns: None,
|
num_turns: None,
|
||||||
last_assistant_message: extract_tool_result_text(content),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1750,35 +1629,6 @@ fn get_tool_state(tool_name: &str) -> CharacterState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_rate_limit_message(info: &crate::types::RateLimitInfo) -> String {
|
|
||||||
let mut parts = Vec::new();
|
|
||||||
|
|
||||||
if let (Some(remaining), Some(limit)) = (info.requests_remaining, info.requests_limit) {
|
|
||||||
parts.push(format!("requests: {}/{}", remaining, limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (Some(remaining), Some(limit)) = (info.tokens_remaining, info.tokens_limit) {
|
|
||||||
parts.push(format!("tokens: {}/{}", remaining, limit));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(reset) = &info.requests_reset {
|
|
||||||
parts.push(format!("resets at {}", reset));
|
|
||||||
} else if let Some(reset) = &info.tokens_reset {
|
|
||||||
parts.push(format!("resets at {}", reset));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(retry_ms) = info.retry_after_ms {
|
|
||||||
let secs = retry_ms / 1000;
|
|
||||||
parts.push(format!("retry after {}s", secs));
|
|
||||||
}
|
|
||||||
|
|
||||||
if parts.is_empty() {
|
|
||||||
"Rate limit reached".to_string()
|
|
||||||
} else {
|
|
||||||
format!("Rate limit reached — {}", parts.join(", "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_tool_description(name: &str, input: &serde_json::Value) -> String {
|
fn format_tool_description(name: &str, input: &serde_json::Value) -> String {
|
||||||
// Helper function to check if a path is a memory file
|
// Helper function to check if a path is a memory file
|
||||||
fn is_memory_path(path: &str) -> bool {
|
fn is_memory_path(path: &str) -> bool {
|
||||||
@@ -1839,7 +1689,12 @@ fn format_tool_description(name: &str, input: &serde_json::Value) -> String {
|
|||||||
}
|
}
|
||||||
"Bash" => {
|
"Bash" => {
|
||||||
if let Some(cmd) = input.get("command").and_then(|v| v.as_str()) {
|
if let Some(cmd) = input.get("command").and_then(|v| v.as_str()) {
|
||||||
format!("Running: {}", cmd)
|
let truncated = if cmd.len() > 50 {
|
||||||
|
format!("{}...", &cmd[..50])
|
||||||
|
} else {
|
||||||
|
cmd.to_string()
|
||||||
|
};
|
||||||
|
format!("Running: {}", truncated)
|
||||||
} else {
|
} else {
|
||||||
"Running command...".to_string()
|
"Running command...".to_string()
|
||||||
}
|
}
|
||||||
@@ -2000,7 +1855,9 @@ mod tests {
|
|||||||
let long_cmd = "a".repeat(100);
|
let long_cmd = "a".repeat(100);
|
||||||
let input = serde_json::json!({"command": long_cmd});
|
let input = serde_json::json!({"command": long_cmd});
|
||||||
let desc = format_tool_description("Bash", &input);
|
let desc = format_tool_description("Bash", &input);
|
||||||
assert_eq!(desc, format!("Running: {}", long_cmd));
|
assert!(desc.starts_with("Running: "));
|
||||||
|
assert!(desc.ends_with("..."));
|
||||||
|
assert!(desc.len() < 70);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2219,140 +2076,5 @@ mod tests {
|
|||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
let data = result.unwrap();
|
let data = result.unwrap();
|
||||||
assert_eq!(data.parent_tool_use_id, None);
|
assert_eq!(data.parent_tool_use_id, None);
|
||||||
assert_eq!(data.last_assistant_message, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_subagent_stop_hook_with_last_message() {
|
|
||||||
let line = r#"[SubagentStop Hook] stop_hook_active=true, parent_tool_use_id=Some("toolu_01ABC123"), last_assistant_message=Some("Task completed successfully."), session_id=123"#;
|
|
||||||
let result = parse_subagent_stop_hook(line);
|
|
||||||
|
|
||||||
assert!(result.is_some());
|
|
||||||
let data = result.unwrap();
|
|
||||||
assert_eq!(data.parent_tool_use_id, Some("toolu_01ABC123".to_string()));
|
|
||||||
assert_eq!(
|
|
||||||
data.last_assistant_message,
|
|
||||||
Some("Task completed successfully.".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_subagent_stop_hook_with_last_message_none() {
|
|
||||||
let line = r#"[SubagentStop Hook] stop_hook_active=true, parent_tool_use_id=Some("toolu_01ABC123"), last_assistant_message=None, session_id=123"#;
|
|
||||||
let result = parse_subagent_stop_hook(line);
|
|
||||||
|
|
||||||
assert!(result.is_some());
|
|
||||||
let data = result.unwrap();
|
|
||||||
assert_eq!(data.last_assistant_message, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_debug_string_value_simple() {
|
|
||||||
let line = r#"key=Some("hello world")"#;
|
|
||||||
assert_eq!(
|
|
||||||
extract_debug_string_value(line, "key"),
|
|
||||||
Some("hello world".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_debug_string_value_with_escaped_quotes() {
|
|
||||||
let line = r#"key=Some("say \"hi\" there")"#;
|
|
||||||
assert_eq!(
|
|
||||||
extract_debug_string_value(line, "key"),
|
|
||||||
Some(r#"say "hi" there"#.to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_debug_string_value_none_variant() {
|
|
||||||
let line = "key=None";
|
|
||||||
assert_eq!(extract_debug_string_value(line, "key"), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_debug_string_value_missing_key() {
|
|
||||||
let line = "other=Some(\"value\")";
|
|
||||||
assert_eq!(extract_debug_string_value(line, "key"), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_subagent_stop_hook_with_commas_in_message() {
|
|
||||||
let line = r#"[SubagentStop Hook] parent_tool_use_id=Some("toolu_01"), last_assistant_message=Some("Found 3 files, all passing.")"#;
|
|
||||||
let result = parse_subagent_stop_hook(line);
|
|
||||||
|
|
||||||
assert!(result.is_some());
|
|
||||||
let data = result.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
data.last_assistant_message,
|
|
||||||
Some("Found 3 files, all passing.".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract_tool_result_text tests
|
|
||||||
#[test]
|
|
||||||
fn test_extract_tool_result_text_plain_string() {
|
|
||||||
let content = serde_json::json!("Hello from agent");
|
|
||||||
assert_eq!(
|
|
||||||
extract_tool_result_text(&content),
|
|
||||||
Some("Hello from agent".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_tool_result_text_empty_string() {
|
|
||||||
let content = serde_json::json!("");
|
|
||||||
assert_eq!(extract_tool_result_text(&content), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_tool_result_text_array_single_text_block() {
|
|
||||||
let content = serde_json::json!([{"type": "text", "text": "Agent completed the task."}]);
|
|
||||||
assert_eq!(
|
|
||||||
extract_tool_result_text(&content),
|
|
||||||
Some("Agent completed the task.".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_tool_result_text_array_multiple_text_blocks() {
|
|
||||||
let content = serde_json::json!([
|
|
||||||
{"type": "text", "text": "First part."},
|
|
||||||
{"type": "text", "text": "Second part."}
|
|
||||||
]);
|
|
||||||
assert_eq!(
|
|
||||||
extract_tool_result_text(&content),
|
|
||||||
Some("First part.\nSecond part.".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_tool_result_text_array_non_text_block() {
|
|
||||||
let content = serde_json::json!([{"type": "image", "source": {"type": "base64"}}]);
|
|
||||||
assert_eq!(extract_tool_result_text(&content), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_tool_result_text_array_mixed_blocks() {
|
|
||||||
let content = serde_json::json!([
|
|
||||||
{"type": "image", "source": {}},
|
|
||||||
{"type": "text", "text": "Found results."}
|
|
||||||
]);
|
|
||||||
assert_eq!(
|
|
||||||
extract_tool_result_text(&content),
|
|
||||||
Some("Found results.".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_tool_result_text_null() {
|
|
||||||
let content = serde_json::Value::Null;
|
|
||||||
assert_eq!(extract_tool_result_text(&content), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extract_tool_result_text_empty_array() {
|
|
||||||
let content = serde_json::json!([]);
|
|
||||||
assert_eq!(extract_tool_result_text(&content), None);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.7.0",
|
"version": "1.5.1",
|
||||||
"identifier": "com.naomi.hikari-desktop",
|
"identifier": "com.naomi.hikari-desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ async function changeDirectory(path: string): Promise<void> {
|
|||||||
custom_instructions: config.custom_instructions || null,
|
custom_instructions: config.custom_instructions || null,
|
||||||
mcp_servers_json: config.mcp_servers_json || null,
|
mcp_servers_json: config.mcp_servers_json || null,
|
||||||
allowed_tools: allAllowedTools,
|
allowed_tools: allAllowedTools,
|
||||||
use_worktree: config.use_worktree ?? false,
|
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,8 +135,6 @@ async function startNewConversation(): Promise<void> {
|
|||||||
custom_instructions: config.custom_instructions || null,
|
custom_instructions: config.custom_instructions || null,
|
||||||
mcp_servers_json: config.mcp_servers_json || null,
|
mcp_servers_json: config.mcp_servers_json || null,
|
||||||
allowed_tools: allAllowedTools,
|
allowed_tools: allAllowedTools,
|
||||||
use_worktree: config.use_worktree ?? false,
|
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -318,16 +318,6 @@
|
|||||||
<span class="text-[10px] text-red-400">Errored / Killed</span>
|
<span class="text-[10px] text-red-400">Errored / Killed</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Last assistant message snippet -->
|
|
||||||
{#if agent.lastAssistantMessage}
|
|
||||||
<p
|
|
||||||
class="mt-1 text-[10px] text-[var(--text-secondary)] italic truncate"
|
|
||||||
title={agent.lastAssistantMessage}
|
|
||||||
>
|
|
||||||
{agent.lastAssistantMessage}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -2,43 +2,15 @@
|
|||||||
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.50";
|
let version = $state("Loading...");
|
||||||
|
|
||||||
let installedVersion = $state("Loading...");
|
|
||||||
|
|
||||||
function compareVersions(a: string, b: string): number {
|
|
||||||
const aParts = a.split(".").map(Number);
|
|
||||||
const bParts = b.split(".").map(Number);
|
|
||||||
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
||||||
const aVal = aParts[i] ?? 0;
|
|
||||||
const bVal = bParts[i] ?? 0;
|
|
||||||
if (aVal > bVal) return 1;
|
|
||||||
if (aVal < bVal) return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let displayVersion = $derived(installedVersion.split(" (")[0]);
|
|
||||||
|
|
||||||
let supportedBadgeState = $derived.by(() => {
|
|
||||||
if (installedVersion === "Loading..." || installedVersion === "Unknown") {
|
|
||||||
return "neutral";
|
|
||||||
}
|
|
||||||
const semverMatch = /(\d+\.\d+\.\d+)/.exec(installedVersion);
|
|
||||||
if (!semverMatch) return "neutral";
|
|
||||||
const cmp = compareVersions(semverMatch[1], SUPPORTED_CLI_VERSION);
|
|
||||||
if (cmp > 0) return "ahead";
|
|
||||||
if (cmp < 0) return "behind";
|
|
||||||
return "current";
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchVersion() {
|
async function fetchVersion() {
|
||||||
try {
|
try {
|
||||||
const result = await invoke<string>("get_claude_version");
|
const result = await invoke<string>("get_claude_version");
|
||||||
installedVersion = result;
|
version = result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get Claude CLI version:", error);
|
console.error("Failed to get Claude CLI version:", error);
|
||||||
installedVersion = "Unknown";
|
version = "Unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +19,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="cli-versions">
|
<div class="cli-version">
|
||||||
<div class="cli-version">
|
|
||||||
<svg
|
<svg
|
||||||
class="terminal-icon"
|
class="terminal-icon"
|
||||||
width="14"
|
width="14"
|
||||||
@@ -63,44 +34,10 @@
|
|||||||
<polyline points="4 17 10 11 4 5" />
|
<polyline points="4 17 10 11 4 5" />
|
||||||
<line x1="12" y1="19" x2="20" y2="19" />
|
<line x1="12" y1="19" x2="20" y2="19" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="version-text">CLI {displayVersion}</span>
|
<span class="version-text">CLI {version}</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cli-version supported {supportedBadgeState}" title="Highest audited CLI version">
|
|
||||||
<svg
|
|
||||||
class="terminal-icon"
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
||||||
</svg>
|
|
||||||
<span class="version-text">Supported {SUPPORTED_CLI_VERSION}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if supportedBadgeState === "ahead"}
|
|
||||||
<span class="version-warning ahead"
|
|
||||||
>Your version is newer, some features may not be supported</span
|
|
||||||
>
|
|
||||||
{:else if supportedBadgeState === "behind"}
|
|
||||||
<span class="version-warning behind"
|
|
||||||
>Your version is out of date, please update to ensure compatibility</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.cli-versions {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cli-version {
|
.cli-version {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -120,21 +57,6 @@
|
|||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-version.supported.current {
|
|
||||||
border-color: var(--success-color, #4caf50);
|
|
||||||
color: var(--success-color, #4caf50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cli-version.supported.ahead {
|
|
||||||
border-color: var(--warning-color, #ff9800);
|
|
||||||
color: var(--warning-color, #ff9800);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cli-version.supported.behind {
|
|
||||||
border-color: var(--error-color, #f44336);
|
|
||||||
color: var(--error-color, #f44336);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-icon {
|
.terminal-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
@@ -143,18 +65,4 @@
|
|||||||
.version-text {
|
.version-text {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-warning {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-style: italic;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.version-warning.ahead {
|
|
||||||
color: var(--warning-color, #ff9800);
|
|
||||||
}
|
|
||||||
|
|
||||||
.version-warning.behind {
|
|
||||||
color: var(--error-color, #f44336);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
} from "$lib/stores/config";
|
} from "$lib/stores/config";
|
||||||
import { claudeStore } from "$lib/stores/claude";
|
import { claudeStore } from "$lib/stores/claude";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import CostSummary from "./CostSummary.svelte";
|
import CostSummary from "./CostSummary.svelte";
|
||||||
|
|
||||||
let config: HikariConfig = $state({
|
let config: HikariConfig = $state({
|
||||||
@@ -53,26 +52,10 @@
|
|||||||
budget_warning_threshold: 0.8,
|
budget_warning_threshold: 0.8,
|
||||||
discord_rpc_enabled: true,
|
discord_rpc_enabled: true,
|
||||||
show_thinking_blocks: true,
|
show_thinking_blocks: true,
|
||||||
use_worktree: false,
|
|
||||||
disable_1m_context: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let showCustomThemeEditor = $state(false);
|
let showCustomThemeEditor = $state(false);
|
||||||
|
|
||||||
interface AuthStatus {
|
|
||||||
is_logged_in: boolean;
|
|
||||||
email: string | null;
|
|
||||||
org_name: string | null;
|
|
||||||
api_key_source: string | null;
|
|
||||||
api_provider: string | null;
|
|
||||||
subscription_type: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let authStatus: AuthStatus | null = $state(null);
|
|
||||||
let authLoading = $state(false);
|
|
||||||
let authActionLoading = $state(false);
|
|
||||||
let authError: string | null = $state(null);
|
|
||||||
|
|
||||||
let isOpen = $state(false);
|
let isOpen = $state(false);
|
||||||
let isSaving = $state(false);
|
let isSaving = $state(false);
|
||||||
let saveError: string | null = $state(null);
|
let saveError: string | null = $state(null);
|
||||||
@@ -86,9 +69,6 @@
|
|||||||
|
|
||||||
configStore.isSidebarOpen.subscribe((open) => {
|
configStore.isSidebarOpen.subscribe((open) => {
|
||||||
isOpen = open;
|
isOpen = open;
|
||||||
if (open && authStatus === null) {
|
|
||||||
void refreshAuthStatus();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
configStore.saveError.subscribe((error) => {
|
configStore.saveError.subscribe((error) => {
|
||||||
@@ -131,44 +111,6 @@
|
|||||||
"Task",
|
"Task",
|
||||||
];
|
];
|
||||||
|
|
||||||
async function refreshAuthStatus() {
|
|
||||||
authLoading = true;
|
|
||||||
authError = null;
|
|
||||||
try {
|
|
||||||
authStatus = await invoke<AuthStatus>("get_auth_status");
|
|
||||||
} catch (e) {
|
|
||||||
authError = String(e);
|
|
||||||
} finally {
|
|
||||||
authLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAuthLogin() {
|
|
||||||
authActionLoading = true;
|
|
||||||
authError = null;
|
|
||||||
try {
|
|
||||||
await invoke<string>("auth_login");
|
|
||||||
await refreshAuthStatus();
|
|
||||||
} catch (e) {
|
|
||||||
authError = String(e);
|
|
||||||
} finally {
|
|
||||||
authActionLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAuthLogout() {
|
|
||||||
authActionLoading = true;
|
|
||||||
authError = null;
|
|
||||||
try {
|
|
||||||
await invoke<string>("auth_logout");
|
|
||||||
await refreshAuthStatus();
|
|
||||||
} catch (e) {
|
|
||||||
authError = String(e);
|
|
||||||
} finally {
|
|
||||||
authActionLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
isSaving = true;
|
isSaving = true;
|
||||||
saveError = null;
|
saveError = null;
|
||||||
@@ -286,101 +228,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Account Section -->
|
|
||||||
<section class="mb-6">
|
|
||||||
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
|
|
||||||
Account
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{#if authLoading}
|
|
||||||
<div class="text-sm text-[var(--text-secondary)] py-2">Checking auth status...</div>
|
|
||||||
{:else if authStatus}
|
|
||||||
<div class="flex items-center gap-2 mb-3">
|
|
||||||
<span
|
|
||||||
class="inline-block w-2.5 h-2.5 rounded-full flex-shrink-0 {authStatus.is_logged_in
|
|
||||||
? 'bg-green-500'
|
|
||||||
: 'bg-red-500'}"
|
|
||||||
></span>
|
|
||||||
<span class="text-sm font-medium text-[var(--text-primary)]">
|
|
||||||
{authStatus.is_logged_in ? "Logged in" : "Not logged in"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{#if authStatus.email || authStatus.org_name || authStatus.api_key_source || config.api_key}
|
|
||||||
<dl class="text-xs space-y-1 mb-3">
|
|
||||||
{#if authStatus.email}
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<dt class="text-[var(--text-tertiary)] w-20 flex-shrink-0">Email</dt>
|
|
||||||
<dd class="text-[var(--text-primary)] break-all">{authStatus.email}</dd>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if authStatus.org_name}
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<dt class="text-[var(--text-tertiary)] w-20 flex-shrink-0">Org</dt>
|
|
||||||
<dd class="text-[var(--text-primary)]">{authStatus.org_name}</dd>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if authStatus.api_key_source}
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<dt class="text-[var(--text-tertiary)] w-20 flex-shrink-0">API key</dt>
|
|
||||||
<dd class="text-[var(--text-primary)]">{authStatus.api_key_source}</dd>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if authStatus.subscription_type}
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<dt class="text-[var(--text-tertiary)] w-20 flex-shrink-0">Plan</dt>
|
|
||||||
<dd class="text-[var(--text-primary)]">{authStatus.subscription_type}</dd>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<dt class="text-[var(--text-tertiary)] w-20 flex-shrink-0">Override</dt>
|
|
||||||
<dd class="text-[var(--text-primary)]">
|
|
||||||
{#if config.api_key}
|
|
||||||
{config.streamer_mode ? "Custom key set đź”’" : "Custom key set"}
|
|
||||||
{:else}
|
|
||||||
None
|
|
||||||
{/if}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<div class="text-sm text-[var(--text-secondary)] py-2">Auth status unavailable</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if authError}
|
|
||||||
<div class="mb-3 p-2 bg-red-500/20 border border-red-500/50 rounded text-red-400 text-xs">
|
|
||||||
{authError}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button
|
|
||||||
onclick={refreshAuthStatus}
|
|
||||||
disabled={authLoading || authActionLoading}
|
|
||||||
class="px-3 py-1.5 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-secondary)] hover:border-[var(--accent-primary)] hover:text-[var(--text-primary)] transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
{#if authStatus && !authStatus.is_logged_in}
|
|
||||||
<button
|
|
||||||
onclick={handleAuthLogin}
|
|
||||||
disabled={authActionLoading}
|
|
||||||
class="btn-trans-gradient px-3 py-1.5 text-sm rounded-lg disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{authActionLoading ? "Logging in..." : "Login"}
|
|
||||||
</button>
|
|
||||||
{:else if authStatus && authStatus.is_logged_in}
|
|
||||||
<button
|
|
||||||
onclick={handleAuthLogout}
|
|
||||||
disabled={authActionLoading}
|
|
||||||
class="px-3 py-1.5 text-sm bg-red-500/20 border border-red-500/50 rounded-lg text-red-400 hover:bg-red-500/30 transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{authActionLoading ? "Logging out..." : "Logout"}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Agent Settings Section -->
|
<!-- Agent Settings 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">
|
||||||
@@ -475,37 +322,6 @@
|
|||||||
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"
|
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>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Worktree Isolation -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="flex items-center gap-3 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={config.use_worktree}
|
|
||||||
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)]">Worktree isolation</span>
|
|
||||||
</label>
|
|
||||||
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
||||||
Launch sessions with <code class="font-mono">--worktree</code> for isolated git worktree environments
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Disable 1M Context Window -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="flex items-center gap-3 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={config.disable_1m_context}
|
|
||||||
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 1M context window</span>
|
|
||||||
</label>
|
|
||||||
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
|
||||||
Sets <code class="font-mono">CLAUDE_CODE_DISABLE_1M_CONTEXT=1</code> to opt out of the extended
|
|
||||||
context window
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Greeting Section -->
|
<!-- Greeting Section -->
|
||||||
|
|||||||
@@ -362,8 +362,6 @@ User: ${formattedMessage}`;
|
|||||||
custom_instructions: config.custom_instructions || null,
|
custom_instructions: config.custom_instructions || null,
|
||||||
mcp_servers_json: config.mcp_servers_json || null,
|
mcp_servers_json: config.mcp_servers_json || null,
|
||||||
allowed_tools: allAllowedTools,
|
allowed_tools: allAllowedTools,
|
||||||
use_worktree: config.use_worktree ?? false,
|
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -87,8 +87,6 @@
|
|||||||
custom_instructions: config.custom_instructions || null,
|
custom_instructions: config.custom_instructions || null,
|
||||||
mcp_servers_json: config.mcp_servers_json || null,
|
mcp_servers_json: config.mcp_servers_json || null,
|
||||||
allowed_tools: newGrantedTools,
|
allowed_tools: newGrantedTools,
|
||||||
use_worktree: config.use_worktree ?? false,
|
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -101,8 +101,6 @@
|
|||||||
budget_warning_threshold: 0.8,
|
budget_warning_threshold: 0.8,
|
||||||
discord_rpc_enabled: true,
|
discord_rpc_enabled: true,
|
||||||
show_thinking_blocks: true,
|
show_thinking_blocks: true,
|
||||||
use_worktree: false,
|
|
||||||
disable_1m_context: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let streamerModeActive = $state(false);
|
let streamerModeActive = $state(false);
|
||||||
@@ -180,8 +178,6 @@
|
|||||||
custom_instructions: currentConfig.custom_instructions || null,
|
custom_instructions: currentConfig.custom_instructions || null,
|
||||||
mcp_servers_json: currentConfig.mcp_servers_json || null,
|
mcp_servers_json: currentConfig.mcp_servers_json || null,
|
||||||
allowed_tools: allAllowedTools,
|
allowed_tools: allAllowedTools,
|
||||||
use_worktree: currentConfig.use_worktree ?? false,
|
|
||||||
disable_1m_context: currentConfig.disable_1m_context ?? false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,8 +289,6 @@
|
|||||||
custom_instructions: currentConfig.custom_instructions || null,
|
custom_instructions: currentConfig.custom_instructions || null,
|
||||||
mcp_servers_json: currentConfig.mcp_servers_json || null,
|
mcp_servers_json: currentConfig.mcp_servers_json || null,
|
||||||
allowed_tools: allAllowedTools,
|
allowed_tools: allAllowedTools,
|
||||||
use_worktree: currentConfig.use_worktree ?? false,
|
|
||||||
disable_1m_context: currentConfig.disable_1m_context ?? false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { claudeStore, type TerminalLine } from "$lib/stores/claude";
|
import { claudeStore, type TerminalLine } from "$lib/stores/claude";
|
||||||
import { afterUpdate, tick, onMount, onDestroy } from "svelte";
|
import { afterUpdate, tick, onMount, onDestroy } from "svelte";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
|
|
||||||
import ConversationTabs from "./ConversationTabs.svelte";
|
import ConversationTabs from "./ConversationTabs.svelte";
|
||||||
import Markdown from "./Markdown.svelte";
|
import Markdown from "./Markdown.svelte";
|
||||||
import HighlightedText from "./HighlightedText.svelte";
|
import HighlightedText from "./HighlightedText.svelte";
|
||||||
@@ -94,14 +92,6 @@
|
|||||||
return "terminal-error";
|
return "terminal-error";
|
||||||
case "thinking":
|
case "thinking":
|
||||||
return "terminal-thinking";
|
return "terminal-thinking";
|
||||||
case "rate-limit":
|
|
||||||
return "terminal-rate-limit";
|
|
||||||
case "compact-prompt":
|
|
||||||
return "terminal-compact-prompt";
|
|
||||||
case "worktree":
|
|
||||||
return "terminal-worktree";
|
|
||||||
case "config-change":
|
|
||||||
return "terminal-config-change";
|
|
||||||
default:
|
default:
|
||||||
return "terminal-default";
|
return "terminal-default";
|
||||||
}
|
}
|
||||||
@@ -119,12 +109,6 @@
|
|||||||
return "[tool]";
|
return "[tool]";
|
||||||
case "error":
|
case "error":
|
||||||
return "[error]";
|
return "[error]";
|
||||||
case "rate-limit":
|
|
||||||
return "[rate-limit]";
|
|
||||||
case "worktree":
|
|
||||||
return "[worktree]";
|
|
||||||
case "config-change":
|
|
||||||
return "[config]";
|
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -203,27 +187,6 @@
|
|||||||
copiedMessageId = null;
|
copiedMessageId = null;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCompact() {
|
|
||||||
if (!currentConversationId) return;
|
|
||||||
await invoke("send_prompt", { conversationId: currentConversationId, message: "/compact" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collapsible tool lines
|
|
||||||
const TOOL_COLLAPSE_THRESHOLD = 60;
|
|
||||||
let expandedToolLines: Record<string, boolean> = {};
|
|
||||||
|
|
||||||
function isToolContentLong(content: string): boolean {
|
|
||||||
return content.length > TOOL_COLLAPSE_THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
function truncateToolContent(content: string): string {
|
|
||||||
return content.slice(0, TOOL_COLLAPSE_THRESHOLD) + "…";
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleToolLine(id: string) {
|
|
||||||
expandedToolLines = { ...expandedToolLines, [id]: !expandedToolLines[id] };
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -299,11 +262,7 @@
|
|||||||
{#if line.toolName}
|
{#if line.toolName}
|
||||||
<span class="terminal-tool-name mr-2">[{line.toolName}]</span>
|
<span class="terminal-tool-name mr-2">[{line.toolName}]</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if line.type === "compact-prompt"}
|
{#if line.type === "assistant" || line.type === "user"}
|
||||||
<button class="compact-action-btn" onclick={handleCompact}>
|
|
||||||
⚡ Compact Conversation
|
|
||||||
</button>
|
|
||||||
{:else if line.type === "assistant" || line.type === "user"}
|
|
||||||
<div class="message-content-wrapper">
|
<div class="message-content-wrapper">
|
||||||
<Markdown
|
<Markdown
|
||||||
content={maskPaths(line.content, hidePaths)}
|
content={maskPaths(line.content, hidePaths)}
|
||||||
@@ -330,22 +289,6 @@
|
|||||||
<span class="copy-text">{copiedMessageId === line.id ? "Copied!" : "Copy"}</span>
|
<span class="copy-text">{copiedMessageId === line.id ? "Copied!" : "Copy"}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else if line.type === "tool" && isToolContentLong(maskPaths(line.content, hidePaths))}
|
|
||||||
<span class="tool-collapsible">
|
|
||||||
<HighlightedText
|
|
||||||
content={expandedToolLines[line.id]
|
|
||||||
? maskPaths(line.content, hidePaths)
|
|
||||||
: truncateToolContent(maskPaths(line.content, hidePaths))}
|
|
||||||
searchQuery={currentSearchQuery}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="tool-toggle-btn"
|
|
||||||
onclick={() => toggleToolLine(line.id)}
|
|
||||||
title={expandedToolLines[line.id] ? "Collapse" : "Expand to see full content"}
|
|
||||||
>
|
|
||||||
{expandedToolLines[line.id] ? "â–˛" : "â–Ľ"}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
{:else}
|
{:else}
|
||||||
<HighlightedText
|
<HighlightedText
|
||||||
content={maskPaths(line.content, hidePaths)}
|
content={maskPaths(line.content, hidePaths)}
|
||||||
@@ -386,42 +329,6 @@
|
|||||||
color: var(--terminal-error, #f87171);
|
color: var(--terminal-error, #f87171);
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-rate-limit {
|
|
||||||
color: var(--terminal-rate-limit, #fb923c);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-compact-prompt {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-worktree {
|
|
||||||
color: var(--terminal-worktree, #34d399);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-config-change {
|
|
||||||
color: var(--terminal-config-change, #a78bfa);
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-action-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4em;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--terminal-error, #f87171);
|
|
||||||
color: var(--terminal-error, #f87171);
|
|
||||||
padding: 0.3em 0.8em;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
font-family: inherit;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-action-btn:hover {
|
|
||||||
background: color-mix(in srgb, var(--terminal-error, #f87171) 15%, transparent);
|
|
||||||
color: var(--terminal-error, #f87171);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-default {
|
.terminal-default {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
@@ -501,28 +408,4 @@
|
|||||||
.terminal-line {
|
.terminal-line {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-collapsible {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-toggle-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-tertiary, #6b7280);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.7em;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1;
|
|
||||||
opacity: 0.7;
|
|
||||||
transition: opacity 0.15s ease;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-toggle-btn:hover {
|
|
||||||
opacity: 1;
|
|
||||||
color: var(--terminal-tool, #c084fc);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,264 +0,0 @@
|
|||||||
/**
|
|
||||||
* Terminal Component Tests
|
|
||||||
*
|
|
||||||
* Tests the pure helper functions extracted from the Terminal component:
|
|
||||||
* - getLineClass: maps line types to CSS class names
|
|
||||||
* - getLinePrefix: maps line types to display prefixes
|
|
||||||
* - formatTime: formats a Date as "HH:MM AM/PM"
|
|
||||||
* - isToolContentLong: checks if tool content exceeds collapse threshold
|
|
||||||
* - truncateToolContent: truncates long tool content with ellipsis
|
|
||||||
*
|
|
||||||
* Manual testing checklist:
|
|
||||||
* - [ ] rate-limit lines appear in amber
|
|
||||||
* - [ ] error lines appear in red
|
|
||||||
* - [ ] tool lines appear in purple
|
|
||||||
* - [ ] system lines appear in grey italic
|
|
||||||
* - [ ] user lines appear in cyan
|
|
||||||
* - [ ] assistant lines appear in primary text colour
|
|
||||||
* - [ ] long tool content is collapsed by default with a toggle button
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from "vitest";
|
|
||||||
|
|
||||||
// Mirror functions from Terminal.svelte for isolated testing
|
|
||||||
|
|
||||||
function getLineClass(type: string): string {
|
|
||||||
switch (type) {
|
|
||||||
case "user":
|
|
||||||
return "terminal-user";
|
|
||||||
case "assistant":
|
|
||||||
return "terminal-assistant";
|
|
||||||
case "system":
|
|
||||||
return "terminal-system italic";
|
|
||||||
case "tool":
|
|
||||||
return "terminal-tool";
|
|
||||||
case "error":
|
|
||||||
return "terminal-error";
|
|
||||||
case "thinking":
|
|
||||||
return "terminal-thinking";
|
|
||||||
case "rate-limit":
|
|
||||||
return "terminal-rate-limit";
|
|
||||||
case "compact-prompt":
|
|
||||||
return "terminal-compact-prompt";
|
|
||||||
case "worktree":
|
|
||||||
return "terminal-worktree";
|
|
||||||
case "config-change":
|
|
||||||
return "terminal-config-change";
|
|
||||||
default:
|
|
||||||
return "terminal-default";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLinePrefix(type: string): string {
|
|
||||||
switch (type) {
|
|
||||||
case "user":
|
|
||||||
return ">";
|
|
||||||
case "assistant":
|
|
||||||
return "";
|
|
||||||
case "system":
|
|
||||||
return "[system]";
|
|
||||||
case "tool":
|
|
||||||
return "[tool]";
|
|
||||||
case "error":
|
|
||||||
return "[error]";
|
|
||||||
case "rate-limit":
|
|
||||||
return "[rate-limit]";
|
|
||||||
case "worktree":
|
|
||||||
return "[worktree]";
|
|
||||||
case "config-change":
|
|
||||||
return "[config]";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(date: Date): string {
|
|
||||||
return date.toLocaleTimeString("en-US", {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const TOOL_COLLAPSE_THRESHOLD = 60;
|
|
||||||
|
|
||||||
function isToolContentLong(content: string): boolean {
|
|
||||||
return content.length > TOOL_COLLAPSE_THRESHOLD;
|
|
||||||
}
|
|
||||||
|
|
||||||
function truncateToolContent(content: string): string {
|
|
||||||
return content.slice(0, TOOL_COLLAPSE_THRESHOLD) + "…";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
describe("getLineClass", () => {
|
|
||||||
it("returns terminal-user for user lines", () => {
|
|
||||||
expect(getLineClass("user")).toBe("terminal-user");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns terminal-assistant for assistant lines", () => {
|
|
||||||
expect(getLineClass("assistant")).toBe("terminal-assistant");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns terminal-system italic for system lines", () => {
|
|
||||||
expect(getLineClass("system")).toBe("terminal-system italic");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns terminal-tool for tool lines", () => {
|
|
||||||
expect(getLineClass("tool")).toBe("terminal-tool");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns terminal-error for error lines", () => {
|
|
||||||
expect(getLineClass("error")).toBe("terminal-error");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns terminal-thinking for thinking lines", () => {
|
|
||||||
expect(getLineClass("thinking")).toBe("terminal-thinking");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns terminal-rate-limit for rate-limit lines", () => {
|
|
||||||
expect(getLineClass("rate-limit")).toBe("terminal-rate-limit");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns terminal-compact-prompt for compact-prompt lines", () => {
|
|
||||||
expect(getLineClass("compact-prompt")).toBe("terminal-compact-prompt");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns terminal-worktree for worktree lines", () => {
|
|
||||||
expect(getLineClass("worktree")).toBe("terminal-worktree");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns terminal-config-change for config-change lines", () => {
|
|
||||||
expect(getLineClass("config-change")).toBe("terminal-config-change");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns terminal-default for unknown line types", () => {
|
|
||||||
expect(getLineClass("unknown")).toBe("terminal-default");
|
|
||||||
expect(getLineClass("")).toBe("terminal-default");
|
|
||||||
expect(getLineClass("random-future-type")).toBe("terminal-default");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getLinePrefix", () => {
|
|
||||||
it("returns > for user lines", () => {
|
|
||||||
expect(getLinePrefix("user")).toBe(">");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns empty string for assistant lines", () => {
|
|
||||||
expect(getLinePrefix("assistant")).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns [system] for system lines", () => {
|
|
||||||
expect(getLinePrefix("system")).toBe("[system]");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns [tool] for tool lines", () => {
|
|
||||||
expect(getLinePrefix("tool")).toBe("[tool]");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns [error] for error lines", () => {
|
|
||||||
expect(getLinePrefix("error")).toBe("[error]");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns [rate-limit] for rate-limit lines", () => {
|
|
||||||
expect(getLinePrefix("rate-limit")).toBe("[rate-limit]");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns empty string for compact-prompt lines (button renders instead)", () => {
|
|
||||||
expect(getLinePrefix("compact-prompt")).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns [worktree] for worktree lines", () => {
|
|
||||||
expect(getLinePrefix("worktree")).toBe("[worktree]");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns [config] for config-change lines", () => {
|
|
||||||
expect(getLinePrefix("config-change")).toBe("[config]");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns empty string for thinking lines (no prefix)", () => {
|
|
||||||
expect(getLinePrefix("thinking")).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns empty string for unknown line types", () => {
|
|
||||||
expect(getLinePrefix("unknown")).toBe("");
|
|
||||||
expect(getLinePrefix("")).toBe("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("formatTime", () => {
|
|
||||||
it("formats time in 12-hour format with AM/PM", () => {
|
|
||||||
const date = new Date(2026, 1, 7, 14, 35);
|
|
||||||
const formatted = formatTime(date);
|
|
||||||
expect(formatted).toMatch(/\d{2}:\d{2}\s?(AM|PM)/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("formats afternoon times correctly", () => {
|
|
||||||
const date = new Date(2026, 1, 7, 14, 35);
|
|
||||||
const formatted = formatTime(date);
|
|
||||||
expect(formatted).toContain("02:35");
|
|
||||||
expect(formatted.toUpperCase()).toContain("PM");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("formats morning times correctly", () => {
|
|
||||||
const date = new Date(2026, 1, 7, 9, 5);
|
|
||||||
const formatted = formatTime(date);
|
|
||||||
expect(formatted).toContain("09:05");
|
|
||||||
expect(formatted.toUpperCase()).toContain("AM");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("formats midnight correctly", () => {
|
|
||||||
const date = new Date(2026, 1, 7, 0, 0);
|
|
||||||
const formatted = formatTime(date);
|
|
||||||
expect(formatted).toContain("12:00");
|
|
||||||
expect(formatted.toUpperCase()).toContain("AM");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("formats noon correctly", () => {
|
|
||||||
const date = new Date(2026, 1, 7, 12, 0);
|
|
||||||
const formatted = formatTime(date);
|
|
||||||
expect(formatted).toContain("12:00");
|
|
||||||
expect(formatted.toUpperCase()).toContain("PM");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isToolContentLong", () => {
|
|
||||||
it("returns false for content at or below the threshold", () => {
|
|
||||||
const exactThreshold = "x".repeat(TOOL_COLLAPSE_THRESHOLD);
|
|
||||||
expect(isToolContentLong(exactThreshold)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for content exceeding the threshold", () => {
|
|
||||||
const overThreshold = "x".repeat(TOOL_COLLAPSE_THRESHOLD + 1);
|
|
||||||
expect(isToolContentLong(overThreshold)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false for short content", () => {
|
|
||||||
expect(isToolContentLong("short")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false for empty content", () => {
|
|
||||||
expect(isToolContentLong("")).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("truncateToolContent", () => {
|
|
||||||
it("truncates content to the threshold length with an ellipsis", () => {
|
|
||||||
const long = "x".repeat(100);
|
|
||||||
const result = truncateToolContent(long);
|
|
||||||
expect(result).toBe("x".repeat(TOOL_COLLAPSE_THRESHOLD) + "…");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps content shorter than threshold unchanged (plus ellipsis)", () => {
|
|
||||||
const short = "hello";
|
|
||||||
const result = truncateToolContent(short);
|
|
||||||
expect(result).toBe("hello…");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses the unicode ellipsis character (not three dots)", () => {
|
|
||||||
const long = "x".repeat(100);
|
|
||||||
const result = truncateToolContent(long);
|
|
||||||
expect(result.endsWith("…")).toBe(true);
|
|
||||||
expect(result.endsWith("...")).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -106,8 +106,6 @@
|
|||||||
custom_instructions: config.custom_instructions || null,
|
custom_instructions: config.custom_instructions || null,
|
||||||
mcp_servers_json: config.mcp_servers_json || null,
|
mcp_servers_json: config.mcp_servers_json || null,
|
||||||
allowed_tools: grantedToolsList,
|
allowed_tools: grantedToolsList,
|
||||||
use_worktree: config.use_worktree ?? false,
|
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -177,32 +177,6 @@ describe("agents store", () => {
|
|||||||
const agents = get(getAgentsForConversation(conversationId));
|
const agents = get(getAgentsForConversation(conversationId));
|
||||||
expect(agents[0].status).toBe("running"); // Status unchanged
|
expect(agents[0].status).toBe("running"); // Status unchanged
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stores lastAssistantMessage when provided", () => {
|
|
||||||
const agent = createMockAgent({ status: "running" });
|
|
||||||
agentStore.addAgent(conversationId, agent);
|
|
||||||
|
|
||||||
agentStore.endAgent(
|
|
||||||
conversationId,
|
|
||||||
agent.toolUseId,
|
|
||||||
Date.now(),
|
|
||||||
false,
|
|
||||||
"Task completed successfully."
|
|
||||||
);
|
|
||||||
|
|
||||||
const agents = get(getAgentsForConversation(conversationId));
|
|
||||||
expect(agents[0].lastAssistantMessage).toBe("Task completed successfully.");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("leaves lastAssistantMessage undefined when not provided", () => {
|
|
||||||
const agent = createMockAgent({ status: "running" });
|
|
||||||
agentStore.addAgent(conversationId, agent);
|
|
||||||
|
|
||||||
agentStore.endAgent(conversationId, agent.toolUseId, Date.now(), false);
|
|
||||||
|
|
||||||
const agents = get(getAgentsForConversation(conversationId));
|
|
||||||
expect(agents[0].lastAssistantMessage).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("markAllErrored", () => {
|
describe("markAllErrored", () => {
|
||||||
|
|||||||
@@ -45,13 +45,7 @@ function createAgentStore() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
endAgent(
|
endAgent(conversationId: string, toolUseId: string, endedAt: number, isError: boolean) {
|
||||||
conversationId: string,
|
|
||||||
toolUseId: string,
|
|
||||||
endedAt: number,
|
|
||||||
isError: boolean,
|
|
||||||
lastAssistantMessage?: string
|
|
||||||
) {
|
|
||||||
agentsByConversation.update((state) => {
|
agentsByConversation.update((state) => {
|
||||||
const agents = state[conversationId];
|
const agents = state[conversationId];
|
||||||
if (!agents) return state;
|
if (!agents) return state;
|
||||||
@@ -68,7 +62,6 @@ function createAgentStore() {
|
|||||||
endedAt,
|
endedAt,
|
||||||
status: isError ? "errored" : "completed",
|
status: isError ? "errored" : "completed",
|
||||||
durationMs,
|
durationMs,
|
||||||
lastAssistantMessage,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -194,8 +194,6 @@ describe("config store", () => {
|
|||||||
budget_warning_threshold: 0.8,
|
budget_warning_threshold: 0.8,
|
||||||
discord_rpc_enabled: true,
|
discord_rpc_enabled: true,
|
||||||
show_thinking_blocks: true,
|
show_thinking_blocks: true,
|
||||||
use_worktree: false,
|
|
||||||
disable_1m_context: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(config.model).toBe("claude-sonnet-4");
|
expect(config.model).toBe("claude-sonnet-4");
|
||||||
@@ -242,8 +240,6 @@ describe("config store", () => {
|
|||||||
budget_warning_threshold: 0.8,
|
budget_warning_threshold: 0.8,
|
||||||
discord_rpc_enabled: true,
|
discord_rpc_enabled: true,
|
||||||
show_thinking_blocks: true,
|
show_thinking_blocks: true,
|
||||||
use_worktree: false,
|
|
||||||
disable_1m_context: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(config.model).toBeNull();
|
expect(config.model).toBeNull();
|
||||||
@@ -789,8 +785,6 @@ describe("config store", () => {
|
|||||||
budget_warning_threshold: 0.9,
|
budget_warning_threshold: 0.9,
|
||||||
discord_rpc_enabled: false,
|
discord_rpc_enabled: false,
|
||||||
show_thinking_blocks: true,
|
show_thinking_blocks: true,
|
||||||
use_worktree: false,
|
|
||||||
disable_1m_context: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockInvokeImpl = vi.mocked(invoke);
|
const mockInvokeImpl = vi.mocked(invoke);
|
||||||
|
|||||||
@@ -47,10 +47,6 @@ export interface HikariConfig {
|
|||||||
discord_rpc_enabled: boolean;
|
discord_rpc_enabled: boolean;
|
||||||
// Thinking blocks settings
|
// Thinking blocks settings
|
||||||
show_thinking_blocks: boolean;
|
show_thinking_blocks: boolean;
|
||||||
// Worktree isolation
|
|
||||||
use_worktree: boolean;
|
|
||||||
// Disable 1M context window
|
|
||||||
disable_1m_context: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig: HikariConfig = {
|
const defaultConfig: HikariConfig = {
|
||||||
@@ -91,8 +87,6 @@ const defaultConfig: HikariConfig = {
|
|||||||
budget_warning_threshold: 0.8,
|
budget_warning_threshold: 0.8,
|
||||||
discord_rpc_enabled: true,
|
discord_rpc_enabled: true,
|
||||||
show_thinking_blocks: true,
|
show_thinking_blocks: true,
|
||||||
use_worktree: false,
|
|
||||||
disable_1m_context: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function createConfigStore() {
|
function createConfigStore() {
|
||||||
|
|||||||
+8
-79
@@ -29,7 +29,6 @@ interface StateChangePayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const connectedConversations = new Set<string>();
|
const connectedConversations = new Set<string>();
|
||||||
const greetingPendingConversations = new Set<string>();
|
|
||||||
let unlisteners: Array<() => void> = [];
|
let unlisteners: Array<() => void> = [];
|
||||||
let skipNextGreeting = false;
|
let skipNextGreeting = false;
|
||||||
|
|
||||||
@@ -56,17 +55,17 @@ function generateGreetingPrompt(): string {
|
|||||||
return `[System: A new session has started. It's currently ${timeOfDay}. Please greet the user warmly and briefly. Keep it short - just 1-2 sentences.]`;
|
return `[System: A new session has started. It's currently ${timeOfDay}. Please greet the user warmly and briefly. Keep it short - just 1-2 sentences.]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendGreeting(conversationId: string): Promise<boolean> {
|
async function sendGreeting(conversationId: string) {
|
||||||
// Check if we should skip this greeting
|
// Check if we should skip this greeting
|
||||||
if (skipNextGreeting) {
|
if (skipNextGreeting) {
|
||||||
skipNextGreeting = false; // Reset the flag
|
skipNextGreeting = false; // Reset the flag
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = configStore.getConfig();
|
const config = configStore.getConfig();
|
||||||
|
|
||||||
if (!config.greeting_enabled) {
|
if (!config.greeting_enabled) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const greetingPrompt = config.greeting_custom_prompt?.trim() || generateGreetingPrompt();
|
const greetingPrompt = config.greeting_custom_prompt?.trim() || generateGreetingPrompt();
|
||||||
@@ -82,12 +81,10 @@ async function sendGreeting(conversationId: string): Promise<boolean> {
|
|||||||
conversationId,
|
conversationId,
|
||||||
message: greetingPrompt,
|
message: greetingPrompt,
|
||||||
});
|
});
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send greeting:", error);
|
console.error("Failed to send greeting:", error);
|
||||||
claudeStore.addLineToConversation(conversationId, "error", `Failed to send greeting: ${error}`);
|
claudeStore.addLineToConversation(conversationId, "error", `Failed to send greeting: ${error}`);
|
||||||
characterState.setTemporaryState("error", 3000);
|
characterState.setTemporaryState("error", 3000);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +118,6 @@ interface WorkingDirectoryPayload {
|
|||||||
|
|
||||||
export async function cleanupConversationTracking(conversationId: string) {
|
export async function cleanupConversationTracking(conversationId: string) {
|
||||||
connectedConversations.delete(conversationId);
|
connectedConversations.delete(conversationId);
|
||||||
greetingPendingConversations.delete(conversationId);
|
|
||||||
|
|
||||||
// Clean up any temp files associated with this conversation
|
// Clean up any temp files associated with this conversation
|
||||||
try {
|
try {
|
||||||
@@ -177,24 +173,7 @@ export async function initializeTauriListeners() {
|
|||||||
if (!connectedConversations.has(targetConversationId)) {
|
if (!connectedConversations.has(targetConversationId)) {
|
||||||
connectedConversations.add(targetConversationId);
|
connectedConversations.add(targetConversationId);
|
||||||
resetSessionStats(); // Reset session stats on new connection
|
resetSessionStats(); // Reset session stats on new connection
|
||||||
|
await sendGreeting(targetConversationId);
|
||||||
// Immediately hold the tab at yellow while we wait for the greeting response.
|
|
||||||
// This avoids a brief green flash before the greeting is even sent.
|
|
||||||
greetingPendingConversations.add(targetConversationId);
|
|
||||||
claudeStore.setConnectionStatusForConversation(
|
|
||||||
targetConversationId,
|
|
||||||
"connecting" as ConnectionStatus
|
|
||||||
);
|
|
||||||
|
|
||||||
const greetingSent = await sendGreeting(targetConversationId);
|
|
||||||
if (!greetingSent) {
|
|
||||||
// Greeting was disabled or failed — flip straight to connected.
|
|
||||||
greetingPendingConversations.delete(targetConversationId);
|
|
||||||
claudeStore.setConnectionStatusForConversation(
|
|
||||||
targetConversationId,
|
|
||||||
"connected" as ConnectionStatus
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (status === "disconnected") {
|
} else if (status === "disconnected") {
|
||||||
@@ -212,7 +191,6 @@ export async function initializeTauriListeners() {
|
|||||||
// Only remove from connected set if we're not about to reconnect
|
// Only remove from connected set if we're not about to reconnect
|
||||||
if (!skipNextGreeting && targetConversationId) {
|
if (!skipNextGreeting && targetConversationId) {
|
||||||
connectedConversations.delete(targetConversationId);
|
connectedConversations.delete(targetConversationId);
|
||||||
greetingPendingConversations.delete(targetConversationId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't add system message if we're about to reconnect
|
// Don't add system message if we're about to reconnect
|
||||||
@@ -227,14 +205,6 @@ export async function initializeTauriListeners() {
|
|||||||
todos.clear();
|
todos.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the tab's connection status on real disconnects
|
|
||||||
if (!skipNextGreeting && targetConversationId) {
|
|
||||||
claudeStore.setConnectionStatusForConversation(
|
|
||||||
targetConversationId,
|
|
||||||
"disconnected" as ConnectionStatus
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update character state for this conversation
|
// Update character state for this conversation
|
||||||
if (targetConversationId) {
|
if (targetConversationId) {
|
||||||
claudeStore.setCharacterStateForConversation(targetConversationId, "idle");
|
claudeStore.setCharacterStateForConversation(targetConversationId, "idle");
|
||||||
@@ -244,7 +214,6 @@ export async function initializeTauriListeners() {
|
|||||||
|
|
||||||
if (targetConversationId) {
|
if (targetConversationId) {
|
||||||
connectedConversations.delete(targetConversationId);
|
connectedConversations.delete(targetConversationId);
|
||||||
greetingPendingConversations.delete(targetConversationId);
|
|
||||||
claudeStore.addLineToConversation(targetConversationId, "error", "Connection error");
|
claudeStore.addLineToConversation(targetConversationId, "error", "Connection error");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,34 +275,11 @@ export async function initializeTauriListeners() {
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Flip to connected when first assistant message arrives after greeting
|
|
||||||
if (
|
|
||||||
conversation_id &&
|
|
||||||
line_type === "assistant" &&
|
|
||||||
greetingPendingConversations.has(conversation_id)
|
|
||||||
) {
|
|
||||||
greetingPendingConversations.delete(conversation_id);
|
|
||||||
claudeStore.setConnectionStatusForConversation(
|
|
||||||
conversation_id,
|
|
||||||
"connected" as ConnectionStatus
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always store the output to the correct conversation
|
// Always store the output to the correct conversation
|
||||||
if (conversation_id) {
|
if (conversation_id) {
|
||||||
claudeStore.addLineToConversation(
|
claudeStore.addLineToConversation(
|
||||||
conversation_id,
|
conversation_id,
|
||||||
line_type as
|
line_type as "user" | "assistant" | "system" | "tool" | "error" | "thinking",
|
||||||
| "user"
|
|
||||||
| "assistant"
|
|
||||||
| "system"
|
|
||||||
| "tool"
|
|
||||||
| "error"
|
|
||||||
| "thinking"
|
|
||||||
| "rate-limit"
|
|
||||||
| "compact-prompt"
|
|
||||||
| "worktree"
|
|
||||||
| "config-change",
|
|
||||||
content,
|
content,
|
||||||
tool_name || undefined,
|
tool_name || undefined,
|
||||||
costData,
|
costData,
|
||||||
@@ -342,17 +288,7 @@ export async function initializeTauriListeners() {
|
|||||||
} else {
|
} else {
|
||||||
// Fallback to active conversation if no conversation_id provided
|
// Fallback to active conversation if no conversation_id provided
|
||||||
claudeStore.addLine(
|
claudeStore.addLine(
|
||||||
line_type as
|
line_type as "user" | "assistant" | "system" | "tool" | "error" | "thinking",
|
||||||
| "user"
|
|
||||||
| "assistant"
|
|
||||||
| "system"
|
|
||||||
| "tool"
|
|
||||||
| "error"
|
|
||||||
| "thinking"
|
|
||||||
| "rate-limit"
|
|
||||||
| "compact-prompt"
|
|
||||||
| "worktree"
|
|
||||||
| "config-change",
|
|
||||||
content,
|
content,
|
||||||
tool_name || undefined,
|
tool_name || undefined,
|
||||||
costData,
|
costData,
|
||||||
@@ -474,17 +410,10 @@ export async function initializeTauriListeners() {
|
|||||||
unlisteners.push(agentUpdateUnlisten);
|
unlisteners.push(agentUpdateUnlisten);
|
||||||
|
|
||||||
const agentEndUnlisten = await listen<AgentEndPayload>("claude:agent-end", (event) => {
|
const agentEndUnlisten = await listen<AgentEndPayload>("claude:agent-end", (event) => {
|
||||||
const { tool_use_id, ended_at, is_error, conversation_id, last_assistant_message } =
|
const { tool_use_id, ended_at, is_error, conversation_id } = event.payload;
|
||||||
event.payload;
|
|
||||||
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
||||||
if (targetConversationId) {
|
if (targetConversationId) {
|
||||||
agentStore.endAgent(
|
agentStore.endAgent(targetConversationId, tool_use_id, ended_at, is_error);
|
||||||
targetConversationId,
|
|
||||||
tool_use_id,
|
|
||||||
ended_at,
|
|
||||||
is_error,
|
|
||||||
last_assistant_message
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
unlisteners.push(agentEndUnlisten);
|
unlisteners.push(agentEndUnlisten);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export interface AgentInfo {
|
|||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
characterName: string;
|
characterName: string;
|
||||||
characterAvatar: string;
|
characterAvatar: string;
|
||||||
lastAssistantMessage?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentStartPayload {
|
export interface AgentStartPayload {
|
||||||
@@ -32,5 +31,4 @@ export interface AgentEndPayload {
|
|||||||
conversation_id?: string;
|
conversation_id?: string;
|
||||||
duration_ms?: number;
|
duration_ms?: number;
|
||||||
num_turns?: number;
|
num_turns?: number;
|
||||||
last_assistant_message?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
export interface TerminalLine {
|
export interface TerminalLine {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "user" | "assistant" | "system" | "tool" | "error" | "thinking";
|
||||||
| "user"
|
|
||||||
| "assistant"
|
|
||||||
| "system"
|
|
||||||
| "tool"
|
|
||||||
| "error"
|
|
||||||
| "thinking"
|
|
||||||
| "rate-limit"
|
|
||||||
| "compact-prompt"
|
|
||||||
| "worktree"
|
|
||||||
| "config-change";
|
|
||||||
content: string;
|
content: string;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
toolName?: string;
|
toolName?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user