generated from nhcarrigan/template
feat: tabs seem to be working now with multi connections
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::config::ClaudeStartOptions;
|
||||
use crate::stats::UsageStats;
|
||||
use crate::wsl_bridge::WslBridge;
|
||||
|
||||
pub struct BridgeManager {
|
||||
bridges: HashMap<String, WslBridge>,
|
||||
app_handle: Option<AppHandle>,
|
||||
}
|
||||
|
||||
impl BridgeManager {
|
||||
pub fn new() -> Self {
|
||||
BridgeManager {
|
||||
bridges: HashMap::new(),
|
||||
app_handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_app_handle(&mut self, app: AppHandle) {
|
||||
self.app_handle = Some(app);
|
||||
}
|
||||
|
||||
pub fn start_claude(
|
||||
&mut self,
|
||||
conversation_id: &str,
|
||||
options: ClaudeStartOptions,
|
||||
) -> Result<(), String> {
|
||||
// Check if a bridge already exists for this conversation
|
||||
if self.bridges.get(conversation_id).map(|b| b.is_running()).unwrap_or(false) {
|
||||
return Err("Claude is already running for this conversation".to_string());
|
||||
}
|
||||
|
||||
let app = self.app_handle.as_ref()
|
||||
.ok_or_else(|| "App handle not set".to_string())?
|
||||
.clone();
|
||||
|
||||
// Create a new bridge for this conversation
|
||||
let mut bridge = WslBridge::new_with_conversation_id(conversation_id.to_string());
|
||||
|
||||
// Start the Claude process
|
||||
bridge.start(app, options)?;
|
||||
|
||||
// Store the bridge
|
||||
self.bridges.insert(conversation_id.to_string(), bridge);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop_claude(&mut self, conversation_id: &str) -> Result<(), String> {
|
||||
if let Some(bridge) = self.bridges.get_mut(conversation_id) {
|
||||
let app = self.app_handle.as_ref()
|
||||
.ok_or_else(|| "App handle not set".to_string())?;
|
||||
bridge.stop(app);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("No Claude instance found for this conversation".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interrupt_claude(&mut self, conversation_id: &str) -> Result<(), String> {
|
||||
if let Some(bridge) = self.bridges.get_mut(conversation_id) {
|
||||
let app = self.app_handle.as_ref()
|
||||
.ok_or_else(|| "App handle not set".to_string())?;
|
||||
bridge.interrupt(app)
|
||||
} else {
|
||||
Err("No Claude instance found for this conversation".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_prompt(&mut self, conversation_id: &str, message: String) -> Result<(), String> {
|
||||
if let Some(bridge) = self.bridges.get_mut(conversation_id) {
|
||||
bridge.send_message(&message)
|
||||
} else {
|
||||
Err("No Claude instance found for this conversation".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_claude_running(&self, conversation_id: &str) -> bool {
|
||||
self.bridges.get(conversation_id)
|
||||
.map(|b| b.is_running())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn get_working_directory(&self, conversation_id: &str) -> Result<String, String> {
|
||||
self.bridges.get(conversation_id)
|
||||
.map(|b| b.get_working_directory().to_string())
|
||||
.ok_or_else(|| "No Claude instance found for this conversation".to_string())
|
||||
}
|
||||
|
||||
pub fn get_usage_stats(&self, conversation_id: &str) -> Result<UsageStats, String> {
|
||||
self.bridges.get(conversation_id)
|
||||
.map(|b| b.get_stats())
|
||||
.ok_or_else(|| "No Claude instance found for this conversation".to_string())
|
||||
}
|
||||
|
||||
pub fn cleanup_stopped_bridges(&mut self) {
|
||||
// Remove bridges that are no longer running
|
||||
self.bridges.retain(|_, bridge| bridge.is_running());
|
||||
}
|
||||
|
||||
pub fn stop_all(&mut self) {
|
||||
if let Some(app) = &self.app_handle {
|
||||
for (_, bridge) in self.bridges.iter_mut() {
|
||||
bridge.stop(app);
|
||||
}
|
||||
}
|
||||
self.bridges.clear();
|
||||
}
|
||||
|
||||
pub fn get_active_conversations(&self) -> Vec<String> {
|
||||
self.bridges.keys()
|
||||
.filter(|id| self.bridges.get(*id).map(|b| b.is_running()).unwrap_or(false))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BridgeManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub type SharedBridgeManager = Arc<Mutex<BridgeManager>>;
|
||||
|
||||
pub fn create_shared_bridge_manager() -> SharedBridgeManager {
|
||||
Arc::new(Mutex::new(BridgeManager::new()))
|
||||
}
|
||||
+42
-24
@@ -3,50 +3,65 @@ use tauri_plugin_store::StoreExt;
|
||||
|
||||
use crate::config::{ClaudeStartOptions, HikariConfig};
|
||||
use crate::stats::UsageStats;
|
||||
use crate::wsl_bridge::SharedBridge;
|
||||
use crate::bridge_manager::SharedBridgeManager;
|
||||
use crate::achievements::{load_achievements, get_achievement_info, AchievementUnlockedEvent};
|
||||
|
||||
const CONFIG_STORE_KEY: &str = "config";
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_claude(
|
||||
app: AppHandle,
|
||||
bridge: State<'_, SharedBridge>,
|
||||
bridge_manager: State<'_, SharedBridgeManager>,
|
||||
conversation_id: String,
|
||||
options: ClaudeStartOptions,
|
||||
) -> Result<(), String> {
|
||||
let mut bridge = bridge.lock();
|
||||
bridge.start(app, options)
|
||||
let mut manager = bridge_manager.lock();
|
||||
manager.start_claude(&conversation_id, options)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_claude(app: AppHandle, bridge: State<'_, SharedBridge>) -> Result<(), String> {
|
||||
let mut bridge = bridge.lock();
|
||||
bridge.stop(&app);
|
||||
Ok(())
|
||||
pub async fn stop_claude(
|
||||
bridge_manager: State<'_, SharedBridgeManager>,
|
||||
conversation_id: String,
|
||||
) -> Result<(), String> {
|
||||
let mut manager = bridge_manager.lock();
|
||||
manager.stop_claude(&conversation_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn interrupt_claude(app: AppHandle, bridge: State<'_, SharedBridge>) -> Result<(), String> {
|
||||
let mut bridge = bridge.lock();
|
||||
bridge.interrupt(&app)
|
||||
pub async fn interrupt_claude(
|
||||
bridge_manager: State<'_, SharedBridgeManager>,
|
||||
conversation_id: String,
|
||||
) -> Result<(), String> {
|
||||
let mut manager = bridge_manager.lock();
|
||||
manager.interrupt_claude(&conversation_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_prompt(bridge: State<'_, SharedBridge>, message: String) -> Result<(), String> {
|
||||
let mut bridge = bridge.lock();
|
||||
bridge.send_message(&message)
|
||||
pub async fn send_prompt(
|
||||
bridge_manager: State<'_, SharedBridgeManager>,
|
||||
conversation_id: String,
|
||||
message: String,
|
||||
) -> Result<(), String> {
|
||||
let mut manager = bridge_manager.lock();
|
||||
manager.send_prompt(&conversation_id, message)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn is_claude_running(bridge: State<'_, SharedBridge>) -> Result<bool, String> {
|
||||
let bridge = bridge.lock();
|
||||
Ok(bridge.is_running())
|
||||
pub async fn is_claude_running(
|
||||
bridge_manager: State<'_, SharedBridgeManager>,
|
||||
conversation_id: String,
|
||||
) -> Result<bool, String> {
|
||||
let manager = bridge_manager.lock();
|
||||
Ok(manager.is_claude_running(&conversation_id))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_working_directory(bridge: State<'_, SharedBridge>) -> Result<String, String> {
|
||||
let bridge = bridge.lock();
|
||||
Ok(bridge.get_working_directory().to_string())
|
||||
pub async fn get_working_directory(
|
||||
bridge_manager: State<'_, SharedBridgeManager>,
|
||||
conversation_id: String,
|
||||
) -> Result<String, String> {
|
||||
let manager = bridge_manager.lock();
|
||||
manager.get_working_directory(&conversation_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -82,9 +97,12 @@ pub async fn save_config(app: AppHandle, config: HikariConfig) -> Result<(), Str
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_usage_stats(bridge: State<'_, SharedBridge>) -> Result<UsageStats, String> {
|
||||
let bridge = bridge.lock();
|
||||
Ok(bridge.get_stats())
|
||||
pub async fn get_usage_stats(
|
||||
bridge_manager: State<'_, SharedBridgeManager>,
|
||||
conversation_id: String,
|
||||
) -> Result<UsageStats, String> {
|
||||
let manager = bridge_manager.lock();
|
||||
manager.get_usage_stats(&conversation_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod achievements;
|
||||
mod bridge_manager;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod notifications;
|
||||
@@ -11,7 +12,7 @@ mod windows_toast;
|
||||
|
||||
use commands::*;
|
||||
use notifications::*;
|
||||
use wsl_bridge::create_shared_bridge;
|
||||
use bridge_manager::create_shared_bridge_manager;
|
||||
use commands::load_saved_achievements;
|
||||
use wsl_notifications::*;
|
||||
use vbs_notification::*;
|
||||
@@ -19,7 +20,7 @@ use windows_toast::*;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let bridge = create_shared_bridge();
|
||||
let bridge_manager = create_shared_bridge_manager();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
@@ -28,7 +29,12 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.manage(bridge)
|
||||
.manage(bridge_manager.clone())
|
||||
.setup(move |app| {
|
||||
// Initialize the app handle in the bridge manager
|
||||
bridge_manager.lock().set_app_handle(app.handle().clone());
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
start_claude,
|
||||
stop_claude,
|
||||
|
||||
@@ -172,6 +172,8 @@ pub struct DeltaContent {
|
||||
pub struct StateChangeEvent {
|
||||
pub state: CharacterState,
|
||||
pub tool_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conversation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -179,6 +181,8 @@ pub struct OutputEvent {
|
||||
pub line_type: String,
|
||||
pub content: String,
|
||||
pub tool_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conversation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -187,6 +191,29 @@ pub struct PermissionPromptEvent {
|
||||
pub tool_name: String,
|
||||
pub tool_input: serde_json::Value,
|
||||
pub description: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conversation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConnectionEvent {
|
||||
pub status: ConnectionStatus,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conversation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionEvent {
|
||||
pub session_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conversation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkingDirectoryEvent {
|
||||
pub directory: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conversation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+52
-30
@@ -1,4 +1,3 @@
|
||||
use parking_lot::Mutex;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
@@ -12,7 +11,7 @@ use std::os::windows::process::CommandExt;
|
||||
use crate::config::ClaudeStartOptions;
|
||||
use crate::stats::{UsageStats, StatsUpdateEvent};
|
||||
use parking_lot::RwLock;
|
||||
use crate::types::{CharacterState, ClaudeMessage, ConnectionStatus, ContentBlock, StateChangeEvent, OutputEvent, PermissionPromptEvent};
|
||||
use crate::types::{CharacterState, ClaudeMessage, ConnectionStatus, ContentBlock, StateChangeEvent, OutputEvent, PermissionPromptEvent, ConnectionEvent, SessionEvent, WorkingDirectoryEvent};
|
||||
use crate::achievements::{get_achievement_info, AchievementUnlockedEvent};
|
||||
|
||||
const SEARCH_TOOLS: [&str; 5] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"];
|
||||
@@ -76,6 +75,7 @@ pub struct WslBridge {
|
||||
session_id: Option<String>,
|
||||
mcp_config_file: Option<NamedTempFile>,
|
||||
stats: Arc<RwLock<UsageStats>>,
|
||||
conversation_id: Option<String>,
|
||||
}
|
||||
|
||||
impl WslBridge {
|
||||
@@ -87,6 +87,19 @@ impl WslBridge {
|
||||
session_id: None,
|
||||
mcp_config_file: None,
|
||||
stats: Arc::new(RwLock::new(UsageStats::new())),
|
||||
conversation_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_conversation_id(conversation_id: String) -> Self {
|
||||
WslBridge {
|
||||
process: None,
|
||||
stdin: None,
|
||||
working_directory: String::new(),
|
||||
session_id: None,
|
||||
mcp_config_file: None,
|
||||
stats: Arc::new(RwLock::new(UsageStats::new())),
|
||||
conversation_id: Some(conversation_id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +133,7 @@ impl WslBridge {
|
||||
let working_dir = &options.working_dir;
|
||||
self.working_directory = working_dir.clone();
|
||||
|
||||
emit_connection_status(&app, ConnectionStatus::Connecting);
|
||||
emit_connection_status(&app, ConnectionStatus::Connecting, self.conversation_id.clone());
|
||||
|
||||
// Create temp file for MCP config if provided
|
||||
let mcp_config_path = if let Some(ref mcp_json) = options.mcp_servers_json {
|
||||
@@ -290,19 +303,21 @@ impl WslBridge {
|
||||
if let Some(stdout) = stdout {
|
||||
let app_clone = app.clone();
|
||||
let stats_clone = self.stats.clone();
|
||||
let conv_id = self.conversation_id.clone();
|
||||
thread::spawn(move || {
|
||||
handle_stdout(stdout, app_clone, stats_clone);
|
||||
handle_stdout(stdout, app_clone, stats_clone, conv_id);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(stderr) = stderr {
|
||||
let app_clone = app.clone();
|
||||
let conv_id = self.conversation_id.clone();
|
||||
thread::spawn(move || {
|
||||
handle_stderr(stderr, app_clone);
|
||||
handle_stderr(stderr, app_clone, conv_id);
|
||||
});
|
||||
}
|
||||
|
||||
emit_connection_status(&app, ConnectionStatus::Connected);
|
||||
emit_connection_status(&app, ConnectionStatus::Connected, self.conversation_id.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -348,7 +363,7 @@ impl WslBridge {
|
||||
// The user will see what session was interrupted
|
||||
|
||||
// Emit disconnected status
|
||||
emit_connection_status(app, ConnectionStatus::Disconnected);
|
||||
emit_connection_status(app, ConnectionStatus::Disconnected, self.conversation_id.clone());
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -364,7 +379,7 @@ impl WslBridge {
|
||||
self.stdin = None;
|
||||
self.session_id = None;
|
||||
self.mcp_config_file = None; // Temp file is automatically deleted when dropped
|
||||
emit_connection_status(app, ConnectionStatus::Disconnected);
|
||||
emit_connection_status(app, ConnectionStatus::Disconnected, self.conversation_id.clone());
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
@@ -396,13 +411,13 @@ impl Default for WslBridge {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_stdout(stdout: std::process::ChildStdout, app: AppHandle, stats: Arc<RwLock<UsageStats>>) {
|
||||
fn handle_stdout(stdout: std::process::ChildStdout, app: AppHandle, stats: Arc<RwLock<UsageStats>>, conversation_id: Option<String>) {
|
||||
let reader = BufReader::new(stdout);
|
||||
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(line) if !line.is_empty() => {
|
||||
if let Err(e) = process_json_line(&line, &app, &stats) {
|
||||
if let Err(e) = process_json_line(&line, &app, &stats, &conversation_id) {
|
||||
eprintln!("Error processing line: {}", e);
|
||||
}
|
||||
}
|
||||
@@ -414,10 +429,10 @@ fn handle_stdout(stdout: std::process::ChildStdout, app: AppHandle, stats: Arc<R
|
||||
}
|
||||
}
|
||||
|
||||
emit_connection_status(&app, ConnectionStatus::Disconnected);
|
||||
emit_connection_status(&app, ConnectionStatus::Disconnected, conversation_id);
|
||||
}
|
||||
|
||||
fn handle_stderr(stderr: std::process::ChildStderr, app: AppHandle) {
|
||||
fn handle_stderr(stderr: std::process::ChildStderr, app: AppHandle, conversation_id: Option<String>) {
|
||||
let reader = BufReader::new(stderr);
|
||||
|
||||
for line in reader.lines() {
|
||||
@@ -427,6 +442,7 @@ fn handle_stderr(stderr: std::process::ChildStderr, app: AppHandle) {
|
||||
line_type: "error".to_string(),
|
||||
content: line,
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
});
|
||||
}
|
||||
Err(_) => break,
|
||||
@@ -435,7 +451,7 @@ fn handle_stderr(stderr: std::process::ChildStderr, app: AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
fn process_json_line(line: &str, app: &AppHandle, stats: &Arc<RwLock<UsageStats>>) -> Result<(), String> {
|
||||
fn process_json_line(line: &str, app: &AppHandle, stats: &Arc<RwLock<UsageStats>>, conversation_id: &Option<String>) -> Result<(), String> {
|
||||
let message: ClaudeMessage = serde_json::from_str(line)
|
||||
.map_err(|e| format!("Failed to parse JSON: {} - Line: {}", e, line))?;
|
||||
|
||||
@@ -443,12 +459,18 @@ fn process_json_line(line: &str, app: &AppHandle, stats: &Arc<RwLock<UsageStats>
|
||||
ClaudeMessage::System { subtype, session_id, cwd, .. } => {
|
||||
if subtype == "init" {
|
||||
if let Some(id) = session_id {
|
||||
let _ = app.emit("claude:session", id.clone());
|
||||
let _ = app.emit("claude:session", SessionEvent {
|
||||
session_id: id.clone(),
|
||||
conversation_id: conversation_id.clone(),
|
||||
});
|
||||
}
|
||||
if let Some(dir) = cwd {
|
||||
let _ = app.emit("claude:cwd", dir.clone());
|
||||
let _ = app.emit("claude:cwd", WorkingDirectoryEvent {
|
||||
directory: dir.clone(),
|
||||
conversation_id: conversation_id.clone(),
|
||||
});
|
||||
}
|
||||
emit_state_change(app, CharacterState::Idle, None);
|
||||
emit_state_change(app, CharacterState::Idle, None, conversation_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,6 +524,7 @@ fn process_json_line(line: &str, app: &AppHandle, stats: &Arc<RwLock<UsageStats>
|
||||
line_type: "tool".to_string(),
|
||||
content: desc,
|
||||
tool_name: Some(name.clone()),
|
||||
conversation_id: conversation_id.clone(),
|
||||
});
|
||||
}
|
||||
ContentBlock::Text { text } => {
|
||||
@@ -515,6 +538,7 @@ fn process_json_line(line: &str, app: &AppHandle, stats: &Arc<RwLock<UsageStats>
|
||||
line_type: "assistant".to_string(),
|
||||
content: text.clone(),
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
});
|
||||
}
|
||||
ContentBlock::Thinking { thinking } => {
|
||||
@@ -523,13 +547,14 @@ fn process_json_line(line: &str, app: &AppHandle, stats: &Arc<RwLock<UsageStats>
|
||||
line_type: "system".to_string(),
|
||||
content: format!("[Thinking] {}", thinking),
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
emit_state_change(app, state, tool_name);
|
||||
emit_state_change(app, state, tool_name, conversation_id.clone());
|
||||
}
|
||||
|
||||
ClaudeMessage::StreamEvent { event } => {
|
||||
@@ -547,7 +572,7 @@ fn process_json_line(line: &str, app: &AppHandle, stats: &Arc<RwLock<UsageStats>
|
||||
}
|
||||
_ => CharacterState::Typing,
|
||||
};
|
||||
emit_state_change(app, state, block.name.clone());
|
||||
emit_state_change(app, state, block.name.clone(), conversation_id.clone());
|
||||
}
|
||||
} else if event.event_type == "content_block_delta" {
|
||||
if let Some(delta) = &event.delta {
|
||||
@@ -614,6 +639,7 @@ fn process_json_line(line: &str, app: &AppHandle, stats: &Arc<RwLock<UsageStats>
|
||||
line_type: "error".to_string(),
|
||||
content: text.clone(),
|
||||
tool_name: None,
|
||||
conversation_id: conversation_id.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -627,17 +653,18 @@ fn process_json_line(line: &str, app: &AppHandle, stats: &Arc<RwLock<UsageStats>
|
||||
tool_name: denial.tool_name.clone(),
|
||||
tool_input: denial.tool_input.clone(),
|
||||
description,
|
||||
conversation_id: conversation_id.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// Show permission state if there were denials
|
||||
if !denials.is_empty() {
|
||||
emit_state_change(app, CharacterState::Permission, None);
|
||||
emit_state_change(app, CharacterState::Permission, None, conversation_id.clone());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
emit_state_change(app, state, None);
|
||||
emit_state_change(app, state, None, conversation_id.clone());
|
||||
}
|
||||
|
||||
ClaudeMessage::User { message } => {
|
||||
@@ -694,7 +721,7 @@ fn process_json_line(line: &str, app: &AppHandle, stats: &Arc<RwLock<UsageStats>
|
||||
});
|
||||
}
|
||||
|
||||
emit_state_change(app, CharacterState::Thinking, None);
|
||||
emit_state_change(app, CharacterState::Thinking, None, conversation_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,19 +788,14 @@ fn format_tool_description(name: &str, input: &serde_json::Value) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_state_change(app: &AppHandle, state: CharacterState, tool_name: Option<String>) {
|
||||
let _ = app.emit("claude:state", StateChangeEvent { state, tool_name });
|
||||
fn emit_state_change(app: &AppHandle, state: CharacterState, tool_name: Option<String>, conversation_id: Option<String>) {
|
||||
let _ = app.emit("claude:state", StateChangeEvent { state, tool_name, conversation_id });
|
||||
}
|
||||
|
||||
fn emit_connection_status(app: &AppHandle, status: ConnectionStatus) {
|
||||
let _ = app.emit("claude:connection", status);
|
||||
fn emit_connection_status(app: &AppHandle, status: ConnectionStatus, conversation_id: Option<String>) {
|
||||
let _ = app.emit("claude:connection", ConnectionEvent { status, conversation_id });
|
||||
}
|
||||
|
||||
pub type SharedBridge = Arc<Mutex<WslBridge>>;
|
||||
|
||||
pub fn create_shared_bridge() -> SharedBridge {
|
||||
Arc::new(Mutex::new(WslBridge::new()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -11,8 +11,19 @@
|
||||
// Track which conversation actually has the Claude connection
|
||||
let connectedConversationId: string | null = null;
|
||||
|
||||
// Track last seen message count for each conversation
|
||||
let lastSeenMessageCount: Map<string, number> = new Map();
|
||||
|
||||
claudeStore.conversations.subscribe((convs) => {
|
||||
conversations = convs;
|
||||
|
||||
// Update the last seen count for the active conversation
|
||||
if (activeConversationId) {
|
||||
const activeConv = convs.get(activeConversationId);
|
||||
if (activeConv) {
|
||||
lastSeenMessageCount.set(activeConversationId, activeConv.terminalLines.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
claudeStore.activeConversationId.subscribe((id) => {
|
||||
@@ -43,6 +54,14 @@
|
||||
saveTabName();
|
||||
}
|
||||
await claudeStore.switchConversation(id);
|
||||
|
||||
// Mark messages as seen when switching to this tab
|
||||
const conv = conversations.get(id);
|
||||
if (conv) {
|
||||
lastSeenMessageCount.set(id, conv.terminalLines.length);
|
||||
// Trigger reactivity
|
||||
lastSeenMessageCount = lastSeenMessageCount;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteTab(id: string, event: MouseEvent) {
|
||||
@@ -79,6 +98,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function hasUnreadMessages(id: string, conversation: Conversation): boolean {
|
||||
if (id === activeConversationId) return false; // Active tab never has unread
|
||||
const lastSeen = lastSeenMessageCount.get(id) || 0;
|
||||
return conversation.terminalLines.length > lastSeen;
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
saveTabName();
|
||||
@@ -171,6 +196,12 @@
|
||||
(active)
|
||||
</span>
|
||||
{/if}
|
||||
{#if hasUnreadMessages(id, conversation)}
|
||||
<div
|
||||
class="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-blue-500 animate-pulse"
|
||||
title="New messages"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { get } from "svelte/store";
|
||||
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
||||
import { characterState } from "$lib/stores/character";
|
||||
import { handleNewUserMessage } from "$lib/notifications/rules";
|
||||
@@ -72,7 +73,14 @@ User: ${formattedMessage}`;
|
||||
characterState.setState("thinking");
|
||||
|
||||
try {
|
||||
await invoke("send_prompt", { message: messageToSend });
|
||||
const conversationId = get(claudeStore.activeConversationId);
|
||||
if (!conversationId) {
|
||||
throw new Error("No active conversation");
|
||||
}
|
||||
await invoke("send_prompt", {
|
||||
conversationId,
|
||||
message: messageToSend
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to send prompt:", error);
|
||||
claudeStore.addLine("error", `Failed to send: ${error}`);
|
||||
@@ -96,7 +104,11 @@ User: ${formattedMessage}`;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke("interrupt_claude");
|
||||
const conversationId = get(claudeStore.activeConversationId);
|
||||
if (!conversationId) {
|
||||
throw new Error("No active conversation");
|
||||
}
|
||||
await invoke("interrupt_claude", { conversationId });
|
||||
claudeStore.addLine("system", "Process interrupted - reconnecting...");
|
||||
characterState.setState("idle");
|
||||
|
||||
@@ -106,14 +118,20 @@ User: ${formattedMessage}`;
|
||||
// Auto-reconnect after a brief delay
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const conversationId = get(claudeStore.activeConversationId);
|
||||
if (!conversationId) {
|
||||
throw new Error("No active conversation");
|
||||
}
|
||||
|
||||
// Get current working directory before reconnecting
|
||||
const workingDir = await invoke<string>("get_working_directory");
|
||||
const workingDir = await invoke<string>("get_working_directory", { conversationId });
|
||||
|
||||
// Set the flag to skip greeting on next connection
|
||||
setSkipNextGreeting(true);
|
||||
|
||||
// Reconnect to Claude
|
||||
await invoke("start_claude", {
|
||||
conversationId,
|
||||
options: {
|
||||
working_dir: workingDir,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { get } from "svelte/store";
|
||||
import { claudeStore, hasPermissionPending } from "$lib/stores/claude";
|
||||
import { characterState } from "$lib/stores/character";
|
||||
import type { PermissionRequest } from "$lib/types/messages";
|
||||
@@ -45,12 +46,18 @@
|
||||
|
||||
// Stop current session and reconnect with new permissions
|
||||
try {
|
||||
await invoke("stop_claude");
|
||||
const conversationId = get(claudeStore.activeConversationId);
|
||||
if (!conversationId) {
|
||||
throw new Error("No active conversation");
|
||||
}
|
||||
|
||||
await invoke("stop_claude", { conversationId });
|
||||
|
||||
// Small delay to ensure clean shutdown
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
await invoke("start_claude", {
|
||||
conversationId,
|
||||
options: {
|
||||
working_dir: workingDirectory || "/home/naomi",
|
||||
allowed_tools: newGrantedTools,
|
||||
@@ -72,7 +79,10 @@ ${JSON.stringify(toolInput, null, 2)}
|
||||
|
||||
Please continue where we left off and retry that action now that you have permission.`;
|
||||
|
||||
await invoke("send_prompt", { message: contextMessage });
|
||||
await invoke("send_prompt", {
|
||||
conversationId,
|
||||
message: contextMessage
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to reconnect:", error);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { get } from "svelte/store";
|
||||
import { claudeStore } from "$lib/stores/claude";
|
||||
import { configStore, type HikariConfig } from "$lib/stores/config";
|
||||
import type { ConnectionStatus } from "$lib/types/messages";
|
||||
@@ -87,7 +88,12 @@
|
||||
];
|
||||
|
||||
try {
|
||||
const conversationId = get(claudeStore.activeConversationId);
|
||||
if (!conversationId) {
|
||||
throw new Error("No active conversation");
|
||||
}
|
||||
await invoke("start_claude", {
|
||||
conversationId,
|
||||
options: {
|
||||
working_dir: targetDir,
|
||||
model: currentConfig.model || null,
|
||||
@@ -105,7 +111,11 @@
|
||||
|
||||
async function handleDisconnect() {
|
||||
try {
|
||||
await invoke("stop_claude");
|
||||
const conversationId = get(claudeStore.activeConversationId);
|
||||
if (!conversationId) {
|
||||
throw new Error("No active conversation");
|
||||
}
|
||||
await invoke("stop_claude", { conversationId });
|
||||
} catch (error) {
|
||||
console.error("Failed to stop Claude:", error);
|
||||
}
|
||||
|
||||
@@ -32,10 +32,14 @@ export const claudeStore = {
|
||||
|
||||
// Methods
|
||||
setConnectionStatus: conversationsStore.setConnectionStatus,
|
||||
setConnectionStatusForConversation: conversationsStore.setConnectionStatusForConversation,
|
||||
setSessionId: conversationsStore.setSessionId,
|
||||
setSessionIdForConversation: conversationsStore.setSessionIdForConversation,
|
||||
setWorkingDirectory: conversationsStore.setWorkingDirectory,
|
||||
setWorkingDirectoryForConversation: conversationsStore.setWorkingDirectoryForConversation,
|
||||
setProcessing: conversationsStore.setProcessing,
|
||||
addLine: conversationsStore.addLine,
|
||||
addLineToConversation: conversationsStore.addLineToConversation,
|
||||
updateLine: conversationsStore.updateLine,
|
||||
appendToLine: conversationsStore.appendToLine,
|
||||
clearTerminal: conversationsStore.clearTerminal,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { writable, derived, get } from "svelte/store";
|
||||
import type { TerminalLine, ConnectionStatus, PermissionRequest } from "$lib/types/messages";
|
||||
import { cleanupConversationTracking } from "$lib/tauri";
|
||||
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
@@ -110,6 +111,17 @@ function createConversationsStore() {
|
||||
return convs;
|
||||
});
|
||||
},
|
||||
|
||||
setConnectionStatusForConversation: (conversationId: string, status: ConnectionStatus) => {
|
||||
conversations.update((convs) => {
|
||||
const conv = convs.get(conversationId);
|
||||
if (conv) {
|
||||
conv.connectionStatus = status;
|
||||
conv.lastActivityAt = new Date();
|
||||
}
|
||||
return convs;
|
||||
});
|
||||
},
|
||||
requestPermission: (request: PermissionRequest) => pendingPermission.set(request),
|
||||
clearPermission: () => pendingPermission.set(null),
|
||||
setPendingRetryMessage: (message: string | null) => pendingRetryMessage.set(message),
|
||||
@@ -136,6 +148,9 @@ function createConversationsStore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clean up tracking for this conversation
|
||||
cleanupConversationTracking(id);
|
||||
|
||||
conversations.update((c) => {
|
||||
c.delete(id);
|
||||
return c;
|
||||
@@ -220,6 +235,17 @@ function createConversationsStore() {
|
||||
});
|
||||
},
|
||||
|
||||
setSessionIdForConversation: (conversationId: string, id: string | null) => {
|
||||
conversations.update((convs) => {
|
||||
const conv = convs.get(conversationId);
|
||||
if (conv) {
|
||||
conv.sessionId = id;
|
||||
conv.lastActivityAt = new Date();
|
||||
}
|
||||
return convs;
|
||||
});
|
||||
},
|
||||
|
||||
setWorkingDirectory: (dir: string) => {
|
||||
const activeId = get(activeConversationId);
|
||||
if (!activeId) return;
|
||||
@@ -234,6 +260,17 @@ function createConversationsStore() {
|
||||
});
|
||||
},
|
||||
|
||||
setWorkingDirectoryForConversation: (conversationId: string, dir: string) => {
|
||||
conversations.update((convs) => {
|
||||
const conv = convs.get(conversationId);
|
||||
if (conv) {
|
||||
conv.workingDirectory = dir;
|
||||
conv.lastActivityAt = new Date();
|
||||
}
|
||||
return convs;
|
||||
});
|
||||
},
|
||||
|
||||
setProcessing: (processing: boolean) => {
|
||||
const activeId = get(activeConversationId);
|
||||
if (!activeId) return;
|
||||
@@ -273,6 +310,29 @@ function createConversationsStore() {
|
||||
return line.id;
|
||||
},
|
||||
|
||||
addLineToConversation: (conversationId: string, type: TerminalLine["type"], content: string, toolName?: string) => {
|
||||
ensureInitialized();
|
||||
|
||||
const line: TerminalLine = {
|
||||
id: generateLineId(),
|
||||
type,
|
||||
content,
|
||||
timestamp: new Date(),
|
||||
toolName,
|
||||
};
|
||||
|
||||
conversations.update((convs) => {
|
||||
const conv = convs.get(conversationId);
|
||||
if (conv) {
|
||||
conv.terminalLines.push(line);
|
||||
conv.lastActivityAt = new Date();
|
||||
}
|
||||
return convs;
|
||||
});
|
||||
|
||||
return line.id;
|
||||
},
|
||||
|
||||
updateLine: (id: string, content: string) => {
|
||||
const activeId = get(activeConversationId);
|
||||
if (!activeId) return;
|
||||
|
||||
+143
-40
@@ -1,5 +1,6 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { get } from "svelte/store";
|
||||
import { claudeStore } from "$lib/stores/claude";
|
||||
import { characterState } from "$lib/stores/character";
|
||||
import { configStore } from "$lib/stores/config";
|
||||
@@ -17,9 +18,10 @@ import {
|
||||
interface StateChangePayload {
|
||||
state: CharacterState;
|
||||
tool_name: string | null;
|
||||
conversation_id?: string;
|
||||
}
|
||||
|
||||
let hasConnectedThisSession = false;
|
||||
let connectedConversations = new Set<string>();
|
||||
let unlisteners: Array<() => void> = [];
|
||||
let skipNextGreeting = false;
|
||||
|
||||
@@ -46,7 +48,7 @@ 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.]`;
|
||||
}
|
||||
|
||||
async function sendGreeting() {
|
||||
async function sendGreeting(conversationId: string) {
|
||||
// Check if we should skip this greeting
|
||||
if (skipNextGreeting) {
|
||||
skipNextGreeting = false; // Reset the flag
|
||||
@@ -68,10 +70,13 @@ async function sendGreeting() {
|
||||
handleNewUserMessage();
|
||||
|
||||
try {
|
||||
await invoke("send_prompt", { message: greetingPrompt });
|
||||
await invoke("send_prompt", {
|
||||
conversationId,
|
||||
message: greetingPrompt
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to send greeting:", error);
|
||||
claudeStore.addLine("error", `Failed to send greeting: ${error}`);
|
||||
claudeStore.addLineToConversation(conversationId, "error", `Failed to send greeting: ${error}`);
|
||||
characterState.setTemporaryState("error", 3000);
|
||||
}
|
||||
}
|
||||
@@ -80,6 +85,26 @@ interface OutputPayload {
|
||||
line_type: string;
|
||||
content: string;
|
||||
tool_name: string | null;
|
||||
conversation_id?: string;
|
||||
}
|
||||
|
||||
interface ConnectionPayload {
|
||||
status: ConnectionStatus;
|
||||
conversation_id?: string;
|
||||
}
|
||||
|
||||
interface SessionPayload {
|
||||
session_id: string;
|
||||
conversation_id?: string;
|
||||
}
|
||||
|
||||
interface WorkingDirectoryPayload {
|
||||
directory: string;
|
||||
conversation_id?: string;
|
||||
}
|
||||
|
||||
export function cleanupConversationTracking(conversationId: string) {
|
||||
connectedConversations.delete(conversationId);
|
||||
}
|
||||
|
||||
export async function initializeTauriListeners() {
|
||||
@@ -95,43 +120,72 @@ export async function initializeTauriListeners() {
|
||||
// Initialize achievements listener
|
||||
await initAchievementsListener();
|
||||
|
||||
const connectionUnlisten = await listen<string>("claude:connection", async (event) => {
|
||||
const status = event.payload as ConnectionStatus;
|
||||
claudeStore.setConnectionStatus(status);
|
||||
const connectionUnlisten = await listen<ConnectionPayload>("claude:connection", async (event) => {
|
||||
const { status, conversation_id } = event.payload;
|
||||
|
||||
// Update connection status for the specific conversation
|
||||
if (conversation_id) {
|
||||
claudeStore.setConnectionStatusForConversation(conversation_id, status);
|
||||
} else {
|
||||
// Fallback to active conversation if no conversation_id
|
||||
claudeStore.setConnectionStatus(status);
|
||||
}
|
||||
|
||||
// Handle notification for connection status
|
||||
handleConnectionStatusChange(status);
|
||||
|
||||
if (status === "connected") {
|
||||
claudeStore.addLine("system", "Connected to Claude Code");
|
||||
characterState.setState("idle");
|
||||
if (!hasConnectedThisSession) {
|
||||
hasConnectedThisSession = true;
|
||||
resetSessionStats(); // Reset session stats on new connection
|
||||
await sendGreeting();
|
||||
// Get the actual conversation ID (fallback to active if not provided)
|
||||
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
||||
|
||||
if (targetConversationId) {
|
||||
// Add system message to the correct conversation
|
||||
claudeStore.addLineToConversation(targetConversationId, "system", "Connected to Claude Code");
|
||||
|
||||
characterState.setState("idle");
|
||||
|
||||
// Check if this specific conversation has connected before
|
||||
if (!connectedConversations.has(targetConversationId)) {
|
||||
connectedConversations.add(targetConversationId);
|
||||
resetSessionStats(); // Reset session stats on new connection
|
||||
await sendGreeting(targetConversationId);
|
||||
}
|
||||
}
|
||||
} else if (status === "disconnected") {
|
||||
// Only reset session flag if we're not about to reconnect
|
||||
if (!skipNextGreeting) {
|
||||
hasConnectedThisSession = false;
|
||||
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
||||
|
||||
// Only remove from connected set if we're not about to reconnect
|
||||
if (!skipNextGreeting && targetConversationId) {
|
||||
connectedConversations.delete(targetConversationId);
|
||||
}
|
||||
|
||||
// Don't add system message if we're about to reconnect
|
||||
if (!skipNextGreeting) {
|
||||
claudeStore.addLine("system", "Disconnected from Claude Code");
|
||||
if (!skipNextGreeting && targetConversationId) {
|
||||
claudeStore.addLineToConversation(targetConversationId, "system", "Disconnected from Claude Code");
|
||||
}
|
||||
|
||||
characterState.setState("idle");
|
||||
} else if (status === "error") {
|
||||
hasConnectedThisSession = false;
|
||||
claudeStore.addLine("error", "Connection error");
|
||||
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
||||
|
||||
if (targetConversationId) {
|
||||
connectedConversations.delete(targetConversationId);
|
||||
claudeStore.addLineToConversation(targetConversationId, "error", "Connection error");
|
||||
}
|
||||
|
||||
characterState.setTemporaryState("error", 3000);
|
||||
}
|
||||
});
|
||||
unlisteners.push(connectionUnlisten);
|
||||
|
||||
const stateUnlisten = await listen<StateChangePayload>("claude:state", (event) => {
|
||||
const { state } = event.payload;
|
||||
const { state, conversation_id } = event.payload;
|
||||
|
||||
// Only process events for the active conversation
|
||||
const activeConversationId = get(claudeStore.activeConversationId);
|
||||
if (conversation_id && activeConversationId && conversation_id !== activeConversationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stateMap: Record<string, CharacterState> = {
|
||||
idle: "idle",
|
||||
@@ -156,12 +210,33 @@ export async function initializeTauriListeners() {
|
||||
unlisteners.push(stateUnlisten);
|
||||
|
||||
const outputUnlisten = await listen<OutputPayload>("claude:output", (event) => {
|
||||
const { line_type, content, tool_name } = event.payload;
|
||||
claudeStore.addLine(
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error",
|
||||
content,
|
||||
tool_name || undefined
|
||||
);
|
||||
const { line_type, content, tool_name, conversation_id } = event.payload;
|
||||
|
||||
// Debug log
|
||||
const activeId = get(claudeStore.activeConversationId);
|
||||
console.log("claude:output event", {
|
||||
conversation_id,
|
||||
activeConversationId: activeId,
|
||||
line_type,
|
||||
content: content.substring(0, 50) + "...",
|
||||
});
|
||||
|
||||
// Always store the output to the correct conversation
|
||||
if (conversation_id) {
|
||||
claudeStore.addLineToConversation(
|
||||
conversation_id,
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error",
|
||||
content,
|
||||
tool_name || undefined
|
||||
);
|
||||
} else {
|
||||
// Fallback to active conversation if no conversation_id provided
|
||||
claudeStore.addLine(
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error",
|
||||
content,
|
||||
tool_name || undefined
|
||||
);
|
||||
}
|
||||
});
|
||||
unlisteners.push(outputUnlisten);
|
||||
|
||||
@@ -170,26 +245,54 @@ export async function initializeTauriListeners() {
|
||||
});
|
||||
unlisteners.push(streamUnlisten);
|
||||
|
||||
const sessionUnlisten = await listen<string>("claude:session", (event) => {
|
||||
claudeStore.setSessionId(event.payload);
|
||||
claudeStore.addLine("system", `Session: ${event.payload.substring(0, 8)}...`);
|
||||
const sessionUnlisten = await listen<SessionPayload>("claude:session", (event) => {
|
||||
const { session_id, conversation_id } = event.payload;
|
||||
|
||||
// Store session ID for the correct conversation
|
||||
if (conversation_id) {
|
||||
claudeStore.setSessionIdForConversation(conversation_id, session_id);
|
||||
claudeStore.addLineToConversation(conversation_id, "system", `Session: ${session_id.substring(0, 8)}...`);
|
||||
} else {
|
||||
// Fallback to active conversation if no conversation_id
|
||||
claudeStore.setSessionId(session_id);
|
||||
claudeStore.addLine("system", `Session: ${session_id.substring(0, 8)}...`);
|
||||
}
|
||||
});
|
||||
unlisteners.push(sessionUnlisten);
|
||||
|
||||
const cwdUnlisten = await listen<string>("claude:cwd", (event) => {
|
||||
claudeStore.setWorkingDirectory(event.payload);
|
||||
const cwdUnlisten = await listen<WorkingDirectoryPayload>("claude:cwd", (event) => {
|
||||
const { directory, conversation_id } = event.payload;
|
||||
|
||||
// Store working directory for the correct conversation
|
||||
if (conversation_id) {
|
||||
claudeStore.setWorkingDirectoryForConversation(conversation_id, directory);
|
||||
} else {
|
||||
// Fallback to active conversation if no conversation_id
|
||||
claudeStore.setWorkingDirectory(directory);
|
||||
}
|
||||
});
|
||||
unlisteners.push(cwdUnlisten);
|
||||
|
||||
const permissionUnlisten = await listen<PermissionPromptEvent>("claude:permission", (event) => {
|
||||
const { id, tool_name, tool_input, description } = event.payload;
|
||||
claudeStore.requestPermission({
|
||||
id,
|
||||
tool: tool_name,
|
||||
description,
|
||||
input: tool_input,
|
||||
});
|
||||
claudeStore.addLine("system", `Permission requested for: ${tool_name}`);
|
||||
const { id, tool_name, tool_input, description, conversation_id } = event.payload;
|
||||
|
||||
// Only process permission requests for the active conversation
|
||||
const activeConversationId = get(claudeStore.activeConversationId);
|
||||
if (conversation_id === activeConversationId) {
|
||||
claudeStore.requestPermission({
|
||||
id,
|
||||
tool: tool_name,
|
||||
description,
|
||||
input: tool_input,
|
||||
});
|
||||
}
|
||||
|
||||
// Always store the permission message to the correct conversation
|
||||
if (conversation_id) {
|
||||
claudeStore.addLineToConversation(conversation_id, "system", `Permission requested for: ${tool_name}`);
|
||||
} else if (conversation_id === activeConversationId) {
|
||||
claudeStore.addLine("system", `Permission requested for: ${tool_name}`);
|
||||
}
|
||||
});
|
||||
unlisteners.push(permissionUnlisten);
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ export interface PermissionPromptEvent {
|
||||
tool_name: string;
|
||||
tool_input: Record<string, unknown>;
|
||||
description: string;
|
||||
conversation_id?: string;
|
||||
}
|
||||
|
||||
export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { initializeTauriListeners, cleanupTauriListeners } from "$lib/tauri";
|
||||
import { configStore, applyTheme } from "$lib/stores/config";
|
||||
import { conversationsStore } from "$lib/stores/conversations";
|
||||
import "$lib/notifications/testNotifications";
|
||||
import Terminal from "$lib/components/Terminal.svelte";
|
||||
import InputBar from "$lib/components/InputBar.svelte";
|
||||
@@ -18,6 +19,10 @@
|
||||
onMount(async () => {
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
|
||||
// Initialize conversations store first to ensure activeConversationId is set
|
||||
conversationsStore.initialize();
|
||||
|
||||
await initializeTauriListeners();
|
||||
await configStore.loadConfig();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user