diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index 96a494b..b682753 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -339,6 +339,7 @@ User: ${formattedMessage}`; conversationId, message: messageToSend, }); + claudeStore.setProcessing(true); } catch (error) { console.error("Failed to send prompt:", error); claudeStore.addLine("error", `Failed to send: ${error}`); @@ -793,6 +794,7 @@ User: ${formattedMessage}`; conversationId, message: prompt, }); + claudeStore.setProcessing(true); } catch (error) { console.error("Failed to send quick action:", error); claudeStore.addLine("error", `Failed to send: ${error}`); diff --git a/src/lib/stores/claude.ts b/src/lib/stores/claude.ts index 8220beb..b163f73 100644 --- a/src/lib/stores/claude.ts +++ b/src/lib/stores/claude.ts @@ -41,6 +41,7 @@ export const claudeStore = { setWorkingDirectory: conversationsStore.setWorkingDirectory, setWorkingDirectoryForConversation: conversationsStore.setWorkingDirectoryForConversation, setProcessing: conversationsStore.setProcessing, + setProcessingForConversation: conversationsStore.setProcessingForConversation, addLine: conversationsStore.addLine, addLineToConversation: conversationsStore.addLineToConversation, updateLine: conversationsStore.updateLine, diff --git a/src/lib/stores/conversations.test.ts b/src/lib/stores/conversations.test.ts index c587109..c5cc6c7 100644 --- a/src/lib/stores/conversations.test.ts +++ b/src/lib/stores/conversations.test.ts @@ -561,3 +561,97 @@ describe("draft text persistence", () => { expect(conversation.draftText).toBe(""); }); }); + +describe("isProcessing state management", () => { + it("starts as false by default", () => { + const conversation = { id: "conv-1", isProcessing: false }; + expect(conversation.isProcessing).toBe(false); + }); + + it("setProcessingForConversation sets processing true for the target conversation", () => { + const conversations = new Map([ + ["conv-1", { isProcessing: false, lastActivityAt: new Date(0) }], + ["conv-2", { isProcessing: false, lastActivityAt: new Date(0) }], + ]); + + const setProcessingForConversation = (conversationId: string, processing: boolean) => { + const conv = conversations.get(conversationId); + if (conv) { + conv.isProcessing = processing; + conv.lastActivityAt = new Date(); + } + }; + + setProcessingForConversation("conv-1", true); + + expect(conversations.get("conv-1")?.isProcessing).toBe(true); + expect(conversations.get("conv-2")?.isProcessing).toBe(false); + }); + + it("setProcessingForConversation resets processing to false", () => { + const conversations = new Map([ + ["conv-1", { isProcessing: true, lastActivityAt: new Date(0) }], + ]); + + const setProcessingForConversation = (conversationId: string, processing: boolean) => { + const conv = conversations.get(conversationId); + if (conv) { + conv.isProcessing = processing; + conv.lastActivityAt = new Date(); + } + }; + + setProcessingForConversation("conv-1", false); + + expect(conversations.get("conv-1")?.isProcessing).toBe(false); + }); + + it("setProcessingForConversation does nothing for unknown conversation", () => { + const conversations = new Map([["conv-1", { isProcessing: false, lastActivityAt: new Date(0) }]]); + + const setProcessingForConversation = (conversationId: string, processing: boolean) => { + const conv = conversations.get(conversationId); + if (conv) { + conv.isProcessing = processing; + conv.lastActivityAt = new Date(); + } + }; + + setProcessingForConversation("unknown", true); + + expect(conversations.get("conv-1")?.isProcessing).toBe(false); + }); + + it("isProcessing is cleared when idle state arrives", () => { + const conversation = { isProcessing: true, characterState: "thinking" }; + + const terminalStates = ["idle", "success", "error"]; + const handleStateChange = (state: string) => { + conversation.characterState = state; + if (terminalStates.includes(state)) { + conversation.isProcessing = false; + } + }; + + handleStateChange("idle"); + + expect(conversation.isProcessing).toBe(false); + }); + + it("isProcessing stays true during non-terminal states", () => { + const conversation = { isProcessing: true, characterState: "thinking" }; + + const terminalStates = ["idle", "success", "error"]; + const handleStateChange = (state: string) => { + conversation.characterState = state; + if (terminalStates.includes(state)) { + conversation.isProcessing = false; + } + }; + + for (const state of ["thinking", "typing", "coding", "searching", "mcp"]) { + handleStateChange(state); + expect(conversation.isProcessing).toBe(true); + } + }); +}); diff --git a/src/lib/stores/conversations.ts b/src/lib/stores/conversations.ts index a3968b4..666b4d1 100644 --- a/src/lib/stores/conversations.ts +++ b/src/lib/stores/conversations.ts @@ -560,6 +560,17 @@ function createConversationsStore() { }); }, + setProcessingForConversation: (conversationId: string, processing: boolean) => { + conversations.update((convs) => { + const conv = convs.get(conversationId); + if (conv) { + conv.isProcessing = processing; + conv.lastActivityAt = new Date(); + } + return convs; + }); + }, + addLine: ( type: TerminalLine["type"], content: string, diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 98374c1..910002c 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -333,13 +333,21 @@ export async function initializeTauriListeners() { } // Always update the conversation's state + const isTerminalState = + mappedState === "idle" || mappedState === "success" || mappedState === "error"; if (conversation_id) { claudeStore.setCharacterStateForConversation(conversation_id, mappedState); + if (isTerminalState) { + claudeStore.setProcessingForConversation(conversation_id, false); + } } else { // Fallback to active conversation if no conversation_id const activeConversationId = get(claudeStore.activeConversationId); if (activeConversationId) { claudeStore.setCharacterStateForConversation(activeConversationId, mappedState); + if (isTerminalState) { + claudeStore.setProcessingForConversation(activeConversationId, false); + } } }