fix: restore permission modal reactivity by replacing array mutation with new array creation
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m6s
CI / Lint & Test (pull_request) Successful in 16m21s
CI / Build Linux (pull_request) Successful in 20m11s
CI / Build Windows (cross-compile) (pull_request) Successful in 30m24s

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.
This commit is contained in:
2026-02-26 22:39:15 -08:00
committed by Naomi Carrigan
parent feba03155c
commit aa40d09b29
3 changed files with 53 additions and 7 deletions
+48 -2
View File
@@ -111,6 +111,9 @@ pub struct WslBridge {
conversation_id: Option<String>,
/// Set to true once the `system:init` message arrives, false at the start of every new session.
received_init: Arc<AtomicBool>,
/// 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<AtomicBool>,
}
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<RwLock<UsageStats>>,
conversation_id: Option<String>,
received_init: Arc<AtomicBool>,
intentional_stop: Arc<AtomicBool>,
) {
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);
}
+3 -3
View File
@@ -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<string>();
let grantedToolsList: string[] = $state([]);
let workingDirectory = $state("");
let grantedToolsList: string[] = [];
let workingDirectory = "";
conversationsStore.pendingPermissions.subscribe((perms) => {
permissions = perms;
+2 -2
View File
@@ -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;