fix: assorted bug fixes for lists, sounds, interrupts, and permissions (#173)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 59s
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled
CI / Lint & Test (push) Has been cancelled

## Summary

- **Markdown lists**: Explicitly set `list-style-type: disc` / `decimal` in the Markdown renderer — Tauri's WebView strips browser defaults, leaving bullets and numbers invisible.
- **Notification sounds**: Moved all per-task sounds (success, error, permission, task-start) from a global `characterState` subscription into the per-conversation `claude:state` event handler, so background tabs receive their sounds correctly and tab-switching never replays a sound that already fired. Closes #172
- **Draft text**: Persists `inputValue` per conversation tab so a half-typed prompt survives switching to another tab and back.
- **Interrupt messages**: Replaced vague "Process interrupted" / "Disconnected" strings with source-specific descriptions (keyboard shortcut, stop button, unexpected crash) so it's clear what actually happened.
- **Silent prompt loss**: When Claude Code exits whilst a prompt is in-flight, emits a visible error line telling the user their last prompt was not processed and to reconnect and retry.
- **Double disconnect**: Added an `intentional_stop` flag to `WslBridge` so that `stop()` / `interrupt()` — which kill the process themselves — suppress the duplicate "Disconnected unexpectedly" message that `handle_stdout`'s EOF path was also emitting.
- **Permission modal**: Fixed two cooperating reactivity bugs — `pendingPermissions` was mutated in-place (`.push()`), causing Svelte's derived-store chain to receive the same array reference and skip re-rendering; `PermissionModal.svelte` also used `$state()` (runes mode) where plain `let` is required for correct store-subscription reactivity.

## Test plan

- [ ] Unordered and ordered lists render with visible bullets and numbers in the chat terminal
- [ ] Completion sound plays once when a background tab finishes; switching back to that tab does not replay it
- [ ] Sounds for error, permission request, and task-start also play for background tabs and do not replay on tab switch
- [ ] Typing a prompt, switching tabs, and switching back restores the draft text
- [ ] Pressing Ctrl+C shows "keyboard shortcut (Ctrl+C)"; clicking the stop button shows "via stop button"
- [ ] If Claude exits mid-request, an error message appears prompting the user to resend
- [ ] Clicking stop or pressing Ctrl+C produces exactly one disconnect message (not two)
- [ ] When a tool requires permission, the permission modal appears and the user can approve or dismiss it

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #173
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #173.
This commit is contained in:
2026-02-26 23:34:51 -08:00
committed by Naomi Carrigan
parent 2e3f203508
commit 89a0bdd8f1
11 changed files with 261 additions and 88 deletions
+48 -2
View File
@@ -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);
} }
+1 -1
View File
@@ -135,7 +135,7 @@
setSkipNextGreeting(true); setSkipNextGreeting(true);
await invoke("interrupt_claude", { conversationId }); await invoke("interrupt_claude", { conversationId });
claudeStore.addLine("system", "Interrupted"); claudeStore.addLine("system", "Process interrupted via stop button");
characterState.setState("idle"); characterState.setState("idle");
} catch (error) { } catch (error) {
console.error("Failed to interrupt:", error); console.error("Failed to interrupt:", error);
+18 -1
View File
@@ -164,6 +164,17 @@
attachments = storedAttachments; attachments = storedAttachments;
}); });
// Per-tab draft persistence — restore the draft text whenever the active
// conversation changes, and save it back on every keystroke.
claudeStore.activeConversationId.subscribe((conversationId) => {
if (conversationId) {
const conv = get(claudeStore.conversations).get(conversationId);
inputValue = conv?.draftText ?? "";
} else {
inputValue = "";
}
});
function handleInputChange() { function handleInputChange() {
// If input is empty, allow history navigation again // If input is empty, allow history navigation again
// Otherwise, mark that user has manually typed // Otherwise, mark that user has manually typed
@@ -176,6 +187,12 @@
historyIndex = -1; historyIndex = -1;
tempInput = ""; tempInput = "";
// Save the current draft so it persists if the user switches tabs.
const activeId = get(claudeStore.activeConversationId);
if (activeId) {
claudeStore.setDraftText(activeId, inputValue);
}
if (isSlashCommand(inputValue)) { if (isSlashCommand(inputValue)) {
matchingCommands = getMatchingCommands(inputValue); matchingCommands = getMatchingCommands(inputValue);
showCommandMenu = matchingCommands.length > 0; showCommandMenu = matchingCommands.length > 0;
@@ -326,7 +343,7 @@ User: ${formattedMessage}`;
throw new Error("No active conversation"); throw new Error("No active conversation");
} }
await invoke("interrupt_claude", { conversationId }); await invoke("interrupt_claude", { conversationId });
claudeStore.addLine("system", "Process interrupted - reconnecting..."); claudeStore.addLine("system", "Process interrupted via stop button — reconnecting...");
characterState.setState("idle"); characterState.setState("idle");
// Show connecting status while we reconnect // Show connecting status while we reconnect
+7 -1
View File
@@ -281,10 +281,16 @@
font-family: "JetBrains Mono", "Fira Code", monospace; font-family: "JetBrains Mono", "Fira Code", monospace;
} }
.markdown-content :global(ul), .markdown-content :global(ul) {
margin: 0.5em 0;
padding-left: 1.5em;
list-style-type: disc;
}
.markdown-content :global(ol) { .markdown-content :global(ol) {
margin: 0.5em 0; margin: 0.5em 0;
padding-left: 1.5em; padding-left: 1.5em;
list-style-type: decimal;
} }
.markdown-content :global(li) { .markdown-content :global(li) {
+3 -3
View File
@@ -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;
+6 -74
View File
@@ -1,52 +1,8 @@
import { characterState } from "$lib/stores/character";
import { notificationManager } from "./notificationManager"; import { notificationManager } from "./notificationManager";
import type { CharacterState } from "$lib/types/states";
import type { ConnectionStatus } from "$lib/types/messages"; import type { ConnectionStatus } from "$lib/types/messages";
// Track previous states to detect transitions // Track previous connection status to detect transitions
let previousCharacterState: CharacterState | null = null;
let previousConnectionStatus: ConnectionStatus | null = null; let previousConnectionStatus: ConnectionStatus | null = null;
let taskStartTime: number | null = null;
let hasNotifiedTaskStart = false;
export function handleCharacterStateChange(newState: CharacterState): void {
// Detect state transitions
if (previousCharacterState === newState) return;
// Task completion: any state -> success
if (newState === "success" && previousCharacterState !== null) {
const taskDuration = taskStartTime ? Date.now() - taskStartTime : 0;
// Only notify for tasks that took more than 2 seconds
if (taskDuration > 2000) {
notificationManager.notifySuccess();
}
taskStartTime = null;
}
// Error occurred
if (newState === "error" && previousCharacterState !== "error") {
notificationManager.notifyError();
}
// Permission needed
if (newState === "permission") {
notificationManager.notifyPermission();
}
// Starting long tasks - only notify once per response
if (
(newState === "coding" || newState === "searching") &&
previousCharacterState !== "coding" &&
previousCharacterState !== "searching" &&
!hasNotifiedTaskStart
) {
taskStartTime = Date.now();
hasNotifiedTaskStart = true;
notificationManager.notifyTaskStart();
}
previousCharacterState = newState;
}
export function handleConnectionStatusChange(newStatus: ConnectionStatus): void { export function handleConnectionStatusChange(newStatus: ConnectionStatus): void {
// Only notify on successful connection after being disconnected // Only notify on successful connection after being disconnected
@@ -67,37 +23,13 @@ export function handleToolExecution(_toolName: string): void {
// But we could add specific rules here if needed // But we could add specific rules here if needed
} }
// Reset notification state for a new response // No-op: sound tracking is now per-conversation in tauri.ts
export function handleNewUserMessage(): void { export function handleNewUserMessage(): void {}
hasNotifiedTaskStart = false;
}
// Store unsubscribe functions // No-op: all per-conversation sounds are driven by tauri.ts event listeners
let unsubscribeCharacterState: (() => void) | null = null; export function initializeNotificationRules(): void {}
// Initialize listeners // Cleanup — reset connection tracking on teardown
export function initializeNotificationRules(): void {
// Clean up any existing subscriptions first
cleanupNotificationRules();
// Subscribe to character state changes
unsubscribeCharacterState = characterState.subscribe((state) => {
handleCharacterStateChange(state);
});
// We'll connect to connection status in the next step
}
// Cleanup function to prevent duplicate listeners
export function cleanupNotificationRules(): void { export function cleanupNotificationRules(): void {
if (unsubscribeCharacterState) {
unsubscribeCharacterState();
unsubscribeCharacterState = null;
}
// Reset state tracking
previousCharacterState = null;
previousConnectionStatus = null; previousConnectionStatus = null;
taskStartTime = null;
hasNotifiedTaskStart = false;
} }
+9
View File
@@ -60,6 +60,15 @@ export const claudeStore = {
isToolGranted: conversationsStore.isToolGranted, isToolGranted: conversationsStore.isToolGranted,
setPendingRetryMessage: conversationsStore.setPendingRetryMessage, setPendingRetryMessage: conversationsStore.setPendingRetryMessage,
// Sound tracking
resetSoundState: conversationsStore.resetSoundState,
setTaskStartTime: conversationsStore.setTaskStartTime,
markSuccessSoundFired: conversationsStore.markSuccessSoundFired,
markTaskStartSoundFired: conversationsStore.markTaskStartSoundFired,
// Draft text (per-tab input persistence)
setDraftText: conversationsStore.setDraftText,
// Conversation management // Conversation management
createConversation: conversationsStore.createConversation, createConversation: conversationsStore.createConversation,
deleteConversation: conversationsStore.deleteConversation, deleteConversation: conversationsStore.deleteConversation,
+38
View File
@@ -523,3 +523,41 @@ describe("pending retry message", () => {
expect(pendingRetryMessage).toBeNull(); expect(pendingRetryMessage).toBeNull();
}); });
}); });
describe("draft text persistence", () => {
it("initialises draft text as empty string", () => {
const conversation = { draftText: "" };
expect(conversation.draftText).toBe("");
});
it("stores draft text per conversation", () => {
const conversations = new Map([
["conv-1", { draftText: "Hello world" }],
["conv-2", { draftText: "" }],
]);
expect(conversations.get("conv-1")?.draftText).toBe("Hello world");
expect(conversations.get("conv-2")?.draftText).toBe("");
});
it("updates draft text independently per conversation", () => {
const conversations = new Map([
["conv-1", { draftText: "Draft A" }],
["conv-2", { draftText: "Draft B" }],
]);
const convA = conversations.get("conv-1");
if (convA) convA.draftText = "Updated A";
expect(conversations.get("conv-1")?.draftText).toBe("Updated A");
expect(conversations.get("conv-2")?.draftText).toBe("Draft B");
});
it("clears draft text after submission", () => {
const conversation = { draftText: "My prompt" };
conversation.draftText = "";
expect(conversation.draftText).toBe("");
});
});
+71 -4
View File
@@ -37,6 +37,10 @@ export interface Conversation {
attachments: Attachment[]; attachments: Attachment[];
summary: ConversationSummary | null; summary: ConversationSummary | null;
startedAt: Date; startedAt: Date;
taskStartTime: number | null;
successSoundFired: boolean;
taskStartSoundFired: boolean;
draftText: string;
} }
function createConversationsStore() { function createConversationsStore() {
@@ -75,6 +79,10 @@ function createConversationsStore() {
attachments: [], attachments: [],
summary: null, summary: null,
startedAt: new Date(), startedAt: new Date(),
taskStartTime: null,
successSoundFired: false,
taskStartSoundFired: false,
draftText: "",
}; };
} }
@@ -196,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;
@@ -219,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;
@@ -364,9 +372,15 @@ function createConversationsStore() {
if (currentId !== id) { if (currentId !== id) {
activeConversationId.set(id); activeConversationId.set(id);
// Update the global character state to match the conversation's state // Update the global character state to match the conversation's state.
// Map success/error → idle since those are transient states that have
// already been displayed — restoring them would re-trigger sound rules.
if (targetConv) { if (targetConv) {
characterState.setState(targetConv.characterState); const stateToRestore =
targetConv.characterState === "success" || targetConv.characterState === "error"
? "idle"
: targetConv.characterState;
characterState.setState(stateToRestore);
} }
} }
}, },
@@ -816,6 +830,59 @@ function createConversationsStore() {
}); });
}, },
// Sound tracking methods
resetSoundState: (conversationId: string) => {
conversations.update((convs) => {
const conv = convs.get(conversationId);
if (conv) {
conv.taskStartTime = null;
conv.successSoundFired = false;
conv.taskStartSoundFired = false;
}
return convs;
});
},
setTaskStartTime: (conversationId: string, time: number | null) => {
conversations.update((convs) => {
const conv = convs.get(conversationId);
if (conv) {
conv.taskStartTime = time;
}
return convs;
});
},
markSuccessSoundFired: (conversationId: string) => {
conversations.update((convs) => {
const conv = convs.get(conversationId);
if (conv) {
conv.successSoundFired = true;
}
return convs;
});
},
markTaskStartSoundFired: (conversationId: string) => {
conversations.update((convs) => {
const conv = convs.get(conversationId);
if (conv) {
conv.taskStartSoundFired = true;
}
return convs;
});
},
setDraftText: (conversationId: string, text: string) => {
conversations.update((convs) => {
const conv = convs.get(conversationId);
if (conv) {
conv.draftText = text;
}
return convs;
});
},
// Add initialization helper // Add initialization helper
initialize: () => { initialize: () => {
ensureInitialized(); ensureInitialized();
+59 -1
View File
@@ -21,6 +21,7 @@ import {
handleConnectionStatusChange, handleConnectionStatusChange,
handleNewUserMessage, handleNewUserMessage,
} from "$lib/notifications/rules"; } from "$lib/notifications/rules";
import { notificationManager } from "$lib/notifications/notificationManager";
interface StateChangePayload { interface StateChangePayload {
state: CharacterState; state: CharacterState;
@@ -220,7 +221,7 @@ export async function initializeTauriListeners() {
claudeStore.addLineToConversation( claudeStore.addLineToConversation(
targetConversationId, targetConversationId,
"system", "system",
"Disconnected from Claude Code" "Disconnected from Claude Code unexpectedly — the process may have crashed or been stopped by the system"
); );
// Clear todos on real disconnect (not on reconnects for permissions) // Clear todos on real disconnect (not on reconnects for permissions)
@@ -270,6 +271,63 @@ export async function initializeTauriListeners() {
const mappedState = stateMap[state.toLowerCase()] || "idle"; const mappedState = stateMap[state.toLowerCase()] || "idle";
// Per-conversation sound tracking — fires for any tab (active or background).
// All sounds are driven from state-change events rather than a global store
// subscription, so background tabs receive their sounds correctly and
// switching tabs never replays a sound that has already fired.
const resolvedConversationId = conversation_id || get(claudeStore.activeConversationId) || null;
if (resolvedConversationId) {
const conv = get(claudeStore.conversations).get(resolvedConversationId);
if (conv) {
const previousState = conv.characterState;
// New response starting — clear all per-task sound flags.
if (mappedState === "thinking") {
claudeStore.resetSoundState(resolvedConversationId);
}
// Record when a long-running phase begins (used for the 2-second
// minimum duration check before playing the completion sound).
if (
(mappedState === "coding" || mappedState === "searching") &&
previousState !== "coding" &&
previousState !== "searching"
) {
claudeStore.setTaskStartTime(resolvedConversationId, Date.now());
}
// Task-start sound — fires once when work enters a long-running phase.
if (
(mappedState === "coding" || mappedState === "searching") &&
previousState !== "coding" &&
previousState !== "searching" &&
!conv.taskStartSoundFired
) {
notificationManager.notifyTaskStart();
claudeStore.markTaskStartSoundFired(resolvedConversationId);
}
// Error sound — fires each time a new error state is entered.
if (mappedState === "error" && previousState !== "error") {
notificationManager.notifyError();
}
// Permission sound — fires each time a permission request arrives.
if (mappedState === "permission") {
notificationManager.notifyPermission();
}
// Completion sound — fires once per task after sufficient duration.
if (mappedState === "success" && !conv.successSoundFired) {
const duration = conv.taskStartTime ? Date.now() - conv.taskStartTime : 0;
if (duration > 2000) {
notificationManager.notifySuccess();
}
claudeStore.markSuccessSoundFired(resolvedConversationId);
}
}
}
// Always update the conversation's state // Always update the conversation's state
if (conversation_id) { if (conversation_id) {
claudeStore.setCharacterStateForConversation(conversation_id, mappedState); claudeStore.setCharacterStateForConversation(conversation_id, mappedState);
+1 -1
View File
@@ -337,7 +337,7 @@
setSkipNextGreeting(true); setSkipNextGreeting(true);
await invoke("interrupt_claude", { conversationId }); await invoke("interrupt_claude", { conversationId });
claudeStore.addLine("system", "Process interrupted"); claudeStore.addLine("system", "Process interrupted by keyboard shortcut (Ctrl+C)");
} catch (error) { } catch (error) {
console.error("Failed to interrupt:", error); console.error("Failed to interrupt:", error);
} }