Files
hikari-desktop/src/lib/stores/claude.ts
T
hikari 89a0bdd8f1
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
fix: assorted bug fixes for lists, sounds, interrupts, and permissions (#173)
## 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>
2026-02-26 23:34:51 -08:00

135 lines
5.5 KiB
TypeScript

import { derived } from "svelte/store";
import { conversationsStore } from "./conversations";
import type { TerminalLine } from "$lib/types/messages";
import { characterState } from "$lib/stores/character";
import {
setShouldRestoreHistory,
setSavedHistory,
getShouldRestoreHistory,
getSavedHistory,
clearHistoryRestore,
} from "./historyRestore";
// Re-export TerminalLine type for backwards compatibility
export type { TerminalLine };
// Re-export from conversations store for backwards compatibility
export const claudeStore = {
// Existing subscriptions
connectionStatus: conversationsStore.connectionStatus,
sessionId: conversationsStore.sessionId,
currentWorkingDirectory: conversationsStore.currentWorkingDirectory,
terminalLines: conversationsStore.terminalLines,
pendingPermission: conversationsStore.pendingPermission,
pendingQuestion: conversationsStore.pendingQuestion,
isProcessing: conversationsStore.isProcessing,
grantedTools: conversationsStore.grantedTools,
pendingRetryMessage: conversationsStore.pendingRetryMessage,
attachments: conversationsStore.attachments,
// New conversation-aware subscriptions
conversations: conversationsStore.conversations,
activeConversationId: conversationsStore.activeConversationId,
activeConversation: conversationsStore.activeConversation,
// Methods
setConnectionStatus: conversationsStore.setConnectionStatus,
setConnectionStatusForConversation: conversationsStore.setConnectionStatusForConversation,
setCharacterStateForConversation: conversationsStore.setCharacterStateForConversation,
setSessionId: conversationsStore.setSessionId,
setSessionIdForConversation: conversationsStore.setSessionIdForConversation,
setWorkingDirectory: conversationsStore.setWorkingDirectory,
setWorkingDirectoryForConversation: conversationsStore.setWorkingDirectoryForConversation,
setProcessing: conversationsStore.setProcessing,
addLine: conversationsStore.addLine,
addLineToConversation: conversationsStore.addLineToConversation,
updateLine: conversationsStore.updateLine,
appendToLine: conversationsStore.appendToLine,
clearTerminal: conversationsStore.clearTerminal,
getConversationHistory: conversationsStore.getConversationHistory,
requestPermission: conversationsStore.requestPermission,
clearPermission: conversationsStore.clearPermission,
requestPermissionForConversation: conversationsStore.requestPermissionForConversation,
clearPermissionForConversation: conversationsStore.clearPermissionForConversation,
requestQuestion: conversationsStore.requestQuestion,
clearQuestion: conversationsStore.clearQuestion,
requestQuestionForConversation: conversationsStore.requestQuestionForConversation,
clearQuestionForConversation: conversationsStore.clearQuestionForConversation,
grantTool: conversationsStore.grantTool,
revokeAllTools: conversationsStore.revokeAllTools,
isToolGranted: conversationsStore.isToolGranted,
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
createConversation: conversationsStore.createConversation,
deleteConversation: conversationsStore.deleteConversation,
switchConversation: conversationsStore.switchConversation,
renameConversation: conversationsStore.renameConversation,
saveScrollPosition: conversationsStore.saveScrollPosition,
getScrollPosition: conversationsStore.getScrollPosition,
// Attachment management
addAttachment: conversationsStore.addAttachment,
removeAttachment: conversationsStore.removeAttachment,
clearAttachments: conversationsStore.clearAttachments,
getAttachments: conversationsStore.getAttachments,
getGrantedTools: (): string[] => {
let tools: string[] = [];
conversationsStore.grantedTools.subscribe((t) => (tools = Array.from(t)))();
return tools;
},
// History restoration methods from main branch
setShouldRestoreHistory: setShouldRestoreHistory,
setSavedConversationHistory: setSavedHistory,
getShouldRestoreHistory: getShouldRestoreHistory,
getSavedConversationHistory: getSavedHistory,
reset: () => {
// Reset only the active conversation
conversationsStore.clearTerminal();
conversationsStore.setSessionId(null);
conversationsStore.setWorkingDirectory("");
conversationsStore.setProcessing(false);
conversationsStore.revokeAllTools();
conversationsStore.clearAttachments();
// Also clear history restoration
clearHistoryRestore();
},
};
export const hasPermissionPending = derived(
claudeStore.activeConversation,
($conversation) =>
$conversation?.pendingPermissions !== null &&
$conversation?.pendingPermissions !== undefined &&
$conversation.pendingPermissions.length > 0
);
export const hasQuestionPending = derived(
claudeStore.activeConversation,
($conversation) => $conversation?.pendingQuestion !== null
);
// Derived store to check if Claude is currently processing (can be interrupted)
export const isClaudeProcessing = derived(
[claudeStore.connectionStatus, characterState],
([$connectionStatus, $characterState]) => {
// Must be connected and in one of the processing states
return (
$connectionStatus === "connected" &&
["thinking", "typing", "searching", "coding", "mcp"].includes($characterState)
);
}
);