From aa40d09b293b21e74c0d9f72a6ae7792611385f2 Mon Sep 17 00:00:00 2001 From: Hikari Date: Thu, 26 Feb 2026 22:39:15 -0800 Subject: [PATCH] fix: restore permission modal reactivity by replacing array mutation with new array creation Previously, pendingPermissions was mutated in-place via .push(), causing Svelte's reactivity chain to potentially receive the same array reference and skip re-rendering the PermissionModal. Switching to spread syntax guarantees a new reference on every update. Also removed $state() from PermissionModal's local variables to match the Svelte 4 reactive pattern used by other working components (Terminal), avoiding rune-mode signal equality short-circuits. --- src-tauri/src/wsl_bridge.rs | 50 ++++++++++++++++++++++- src/lib/components/PermissionModal.svelte | 6 +-- src/lib/stores/conversations.ts | 4 +- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index 8f3a4dd..5df9852 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -111,6 +111,9 @@ pub struct WslBridge { conversation_id: Option, /// Set to true once the `system:init` message arrives, false at the start of every new session. received_init: Arc, + /// Set to true by stop()/interrupt() before killing the process so handle_stdout knows + /// the disconnect was intentional and should not emit a second Disconnected event. + intentional_stop: Arc, } impl WslBridge { @@ -124,6 +127,7 @@ impl WslBridge { stats: Arc::new(RwLock::new(UsageStats::new())), conversation_id: None, received_init: Arc::new(AtomicBool::new(false)), + intentional_stop: Arc::new(AtomicBool::new(false)), } } @@ -137,6 +141,7 @@ impl WslBridge { stats: Arc::new(RwLock::new(UsageStats::new())), conversation_id: Some(conversation_id), received_init: Arc::new(AtomicBool::new(false)), + intentional_stop: Arc::new(AtomicBool::new(false)), } } @@ -406,8 +411,9 @@ impl WslBridge { self.stdin = stdin; *self.process.lock() = Some(child); - // Reset the init flag so the watchdog and stdout handler start fresh. + // Reset flags so the watchdog and stdout handler start fresh. self.received_init.store(false, Ordering::SeqCst); + self.intentional_stop.store(false, Ordering::SeqCst); // Note: We no longer reset stats here - stats persist across reconnects // Stats are only reset when explicitly disconnecting via stop() @@ -425,8 +431,16 @@ impl WslBridge { let stats_clone = self.stats.clone(); let conv_id = self.conversation_id.clone(); let received_init_clone = self.received_init.clone(); + let intentional_stop_clone = self.intentional_stop.clone(); thread::spawn(move || { - handle_stdout(stdout, app_clone, stats_clone, conv_id, received_init_clone); + handle_stdout( + stdout, + app_clone, + stats_clone, + conv_id, + received_init_clone, + intentional_stop_clone, + ); }); } @@ -543,6 +557,11 @@ impl WslBridge { // See: https://github.com/anthropics/claude-code/issues/3455 // Extract the process first so the MutexGuard is dropped before we mutably // borrow `self` again via estimate_interrupted_request_cost. + + // Signal handle_stdout that this is an intentional stop so it doesn't emit + // a second Disconnected event after stdout closes due to the kill. + self.intentional_stop.store(true, Ordering::SeqCst); + let maybe_process = self.process.lock().take(); if let Some(mut process) = maybe_process { // Estimate cost for interrupted request before killing @@ -674,6 +693,9 @@ impl WslBridge { } pub fn stop(&mut self, app: &AppHandle) { + // Signal handle_stdout that this is an intentional stop so it doesn't emit + // a second Disconnected event after stdout closes due to the kill. + self.intentional_stop.store(true, Ordering::SeqCst); if let Some(mut process) = self.process.lock().take() { let _ = process.kill(); let _ = process.wait(); @@ -729,6 +751,7 @@ fn handle_stdout( stats: Arc>, conversation_id: Option, received_init: Arc, + intentional_stop: Arc, ) { let reader = BufReader::new(stdout); @@ -749,6 +772,12 @@ fn handle_stdout( } } + // If this was an intentional stop (stop()/interrupt() was called), the caller already + // emitted a Disconnected event. Skip all post-loop emissions to prevent duplicates. + if intentional_stop.load(Ordering::SeqCst) { + return; + } + // If stdout closed before system:init arrived the process exited without initialising. // Emit an error line so the user understands why the connection failed. if !received_init.load(Ordering::SeqCst) { @@ -765,6 +794,23 @@ fn handle_stdout( ); } + // If Claude exited while a prompt was in-flight, the user's message was never processed. + // Emit a specific error so they know to resend their prompt. + let had_pending_request = stats.read().current_request_input.is_some(); + if had_pending_request { + let _ = app.emit( + "claude:output", + OutputEvent { + line_type: "error".to_string(), + content: "Claude Code exited before finishing your request — your last prompt was not processed. Please reconnect and try again.".to_string(), + tool_name: None, + conversation_id: conversation_id.clone(), + cost: None, + parent_tool_use_id: None, + }, + ); + } + emit_connection_status(&app, ConnectionStatus::Disconnected, conversation_id); } diff --git a/src/lib/components/PermissionModal.svelte b/src/lib/components/PermissionModal.svelte index 5943ff5..061e4b9 100644 --- a/src/lib/components/PermissionModal.svelte +++ b/src/lib/components/PermissionModal.svelte @@ -9,10 +9,10 @@ import { conversationsStore } from "$lib/stores/conversations"; import { configStore } from "$lib/stores/config"; - let permissions: PermissionRequest[] = $state([]); + let permissions: PermissionRequest[] = []; let selectedPermissions = new SvelteSet(); - let grantedToolsList: string[] = $state([]); - let workingDirectory = $state(""); + let grantedToolsList: string[] = []; + let workingDirectory = ""; conversationsStore.pendingPermissions.subscribe((perms) => { permissions = perms; diff --git a/src/lib/stores/conversations.ts b/src/lib/stores/conversations.ts index 68ec630..00e2644 100644 --- a/src/lib/stores/conversations.ts +++ b/src/lib/stores/conversations.ts @@ -204,7 +204,7 @@ function createConversationsStore() { conversations.update((convs) => { const conv = convs.get(activeId); if (conv) { - conv.pendingPermissions.push(request); + conv.pendingPermissions = [...conv.pendingPermissions, request]; conv.lastActivityAt = new Date(); } return convs; @@ -227,7 +227,7 @@ function createConversationsStore() { conversations.update((convs) => { const conv = convs.get(conversationId); if (conv) { - conv.pendingPermissions.push(request); + conv.pendingPermissions = [...conv.pendingPermissions, request]; conv.lastActivityAt = new Date(); } return convs;