generated from nhcarrigan/template
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.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user