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>,
|
conversation_id: Option<String>,
|
||||||
/// Set to true once the `system:init` message arrives, false at the start of every new session.
|
/// Set to true once the `system:init` message arrives, false at the start of every new session.
|
||||||
received_init: Arc<AtomicBool>,
|
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 {
|
impl WslBridge {
|
||||||
@@ -124,6 +127,7 @@ impl WslBridge {
|
|||||||
stats: Arc::new(RwLock::new(UsageStats::new())),
|
stats: Arc::new(RwLock::new(UsageStats::new())),
|
||||||
conversation_id: None,
|
conversation_id: None,
|
||||||
received_init: Arc::new(AtomicBool::new(false)),
|
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())),
|
stats: Arc::new(RwLock::new(UsageStats::new())),
|
||||||
conversation_id: Some(conversation_id),
|
conversation_id: Some(conversation_id),
|
||||||
received_init: Arc::new(AtomicBool::new(false)),
|
received_init: Arc::new(AtomicBool::new(false)),
|
||||||
|
intentional_stop: Arc::new(AtomicBool::new(false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,8 +411,9 @@ impl WslBridge {
|
|||||||
self.stdin = stdin;
|
self.stdin = stdin;
|
||||||
*self.process.lock() = Some(child);
|
*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.received_init.store(false, Ordering::SeqCst);
|
||||||
|
self.intentional_stop.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
// Note: We no longer reset stats here - stats persist across reconnects
|
// Note: We no longer reset stats here - stats persist across reconnects
|
||||||
// Stats are only reset when explicitly disconnecting via stop()
|
// Stats are only reset when explicitly disconnecting via stop()
|
||||||
@@ -425,8 +431,16 @@ impl WslBridge {
|
|||||||
let stats_clone = self.stats.clone();
|
let stats_clone = self.stats.clone();
|
||||||
let conv_id = self.conversation_id.clone();
|
let conv_id = self.conversation_id.clone();
|
||||||
let received_init_clone = self.received_init.clone();
|
let received_init_clone = self.received_init.clone();
|
||||||
|
let intentional_stop_clone = self.intentional_stop.clone();
|
||||||
thread::spawn(move || {
|
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
|
// See: https://github.com/anthropics/claude-code/issues/3455
|
||||||
// Extract the process first so the MutexGuard is dropped before we mutably
|
// Extract the process first so the MutexGuard is dropped before we mutably
|
||||||
// borrow `self` again via estimate_interrupted_request_cost.
|
// 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();
|
let maybe_process = self.process.lock().take();
|
||||||
if let Some(mut process) = maybe_process {
|
if let Some(mut process) = maybe_process {
|
||||||
// Estimate cost for interrupted request before killing
|
// Estimate cost for interrupted request before killing
|
||||||
@@ -674,6 +693,9 @@ impl WslBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&mut self, app: &AppHandle) {
|
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() {
|
if let Some(mut process) = self.process.lock().take() {
|
||||||
let _ = process.kill();
|
let _ = process.kill();
|
||||||
let _ = process.wait();
|
let _ = process.wait();
|
||||||
@@ -729,6 +751,7 @@ fn handle_stdout(
|
|||||||
stats: Arc<RwLock<UsageStats>>,
|
stats: Arc<RwLock<UsageStats>>,
|
||||||
conversation_id: Option<String>,
|
conversation_id: Option<String>,
|
||||||
received_init: Arc<AtomicBool>,
|
received_init: Arc<AtomicBool>,
|
||||||
|
intentional_stop: Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
let reader = BufReader::new(stdout);
|
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.
|
// If stdout closed before system:init arrived the process exited without initialising.
|
||||||
// Emit an error line so the user understands why the connection failed.
|
// Emit an error line so the user understands why the connection failed.
|
||||||
if !received_init.load(Ordering::SeqCst) {
|
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);
|
emit_connection_status(&app, ConnectionStatus::Disconnected, conversation_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
import { conversationsStore } from "$lib/stores/conversations";
|
import { conversationsStore } from "$lib/stores/conversations";
|
||||||
import { configStore } from "$lib/stores/config";
|
import { configStore } from "$lib/stores/config";
|
||||||
|
|
||||||
let permissions: PermissionRequest[] = $state([]);
|
let permissions: PermissionRequest[] = [];
|
||||||
let selectedPermissions = new SvelteSet<string>();
|
let selectedPermissions = new SvelteSet<string>();
|
||||||
let grantedToolsList: string[] = $state([]);
|
let grantedToolsList: string[] = [];
|
||||||
let workingDirectory = $state("");
|
let workingDirectory = "";
|
||||||
|
|
||||||
conversationsStore.pendingPermissions.subscribe((perms) => {
|
conversationsStore.pendingPermissions.subscribe((perms) => {
|
||||||
permissions = perms;
|
permissions = perms;
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ function createConversationsStore() {
|
|||||||
conversations.update((convs) => {
|
conversations.update((convs) => {
|
||||||
const conv = convs.get(activeId);
|
const conv = convs.get(activeId);
|
||||||
if (conv) {
|
if (conv) {
|
||||||
conv.pendingPermissions.push(request);
|
conv.pendingPermissions = [...conv.pendingPermissions, request];
|
||||||
conv.lastActivityAt = new Date();
|
conv.lastActivityAt = new Date();
|
||||||
}
|
}
|
||||||
return convs;
|
return convs;
|
||||||
@@ -227,7 +227,7 @@ function createConversationsStore() {
|
|||||||
conversations.update((convs) => {
|
conversations.update((convs) => {
|
||||||
const conv = convs.get(conversationId);
|
const conv = convs.get(conversationId);
|
||||||
if (conv) {
|
if (conv) {
|
||||||
conv.pendingPermissions.push(request);
|
conv.pendingPermissions = [...conv.pendingPermissions, request];
|
||||||
conv.lastActivityAt = new Date();
|
conv.lastActivityAt = new Date();
|
||||||
}
|
}
|
||||||
return convs;
|
return convs;
|
||||||
|
|||||||
Reference in New Issue
Block a user