generated from nhcarrigan/template
fix: properly track isProcessing state to block duplicate message submission
The isProcessing field existed on conversations but was never set to true in production code, making all submission guards effectively no-ops. - Add setProcessingForConversation to conversations store and claude.ts - Set isProcessing=true after send_prompt succeeds in handleSubmit and handleQuickAction - Set isProcessing=false when claude:state emits idle, success, or error - Add tests for the new setProcessingForConversation logic
This commit is contained in:
@@ -339,6 +339,7 @@ User: ${formattedMessage}`;
|
|||||||
conversationId,
|
conversationId,
|
||||||
message: messageToSend,
|
message: messageToSend,
|
||||||
});
|
});
|
||||||
|
claudeStore.setProcessing(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send prompt:", error);
|
console.error("Failed to send prompt:", error);
|
||||||
claudeStore.addLine("error", `Failed to send: ${error}`);
|
claudeStore.addLine("error", `Failed to send: ${error}`);
|
||||||
@@ -793,6 +794,7 @@ User: ${formattedMessage}`;
|
|||||||
conversationId,
|
conversationId,
|
||||||
message: prompt,
|
message: prompt,
|
||||||
});
|
});
|
||||||
|
claudeStore.setProcessing(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send quick action:", error);
|
console.error("Failed to send quick action:", error);
|
||||||
claudeStore.addLine("error", `Failed to send: ${error}`);
|
claudeStore.addLine("error", `Failed to send: ${error}`);
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const claudeStore = {
|
|||||||
setWorkingDirectory: conversationsStore.setWorkingDirectory,
|
setWorkingDirectory: conversationsStore.setWorkingDirectory,
|
||||||
setWorkingDirectoryForConversation: conversationsStore.setWorkingDirectoryForConversation,
|
setWorkingDirectoryForConversation: conversationsStore.setWorkingDirectoryForConversation,
|
||||||
setProcessing: conversationsStore.setProcessing,
|
setProcessing: conversationsStore.setProcessing,
|
||||||
|
setProcessingForConversation: conversationsStore.setProcessingForConversation,
|
||||||
addLine: conversationsStore.addLine,
|
addLine: conversationsStore.addLine,
|
||||||
addLineToConversation: conversationsStore.addLineToConversation,
|
addLineToConversation: conversationsStore.addLineToConversation,
|
||||||
updateLine: conversationsStore.updateLine,
|
updateLine: conversationsStore.updateLine,
|
||||||
|
|||||||
@@ -561,3 +561,97 @@ describe("draft text persistence", () => {
|
|||||||
expect(conversation.draftText).toBe("");
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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: (
|
addLine: (
|
||||||
type: TerminalLine["type"],
|
type: TerminalLine["type"],
|
||||||
content: string,
|
content: string,
|
||||||
|
|||||||
@@ -333,13 +333,21 @@ export async function initializeTauriListeners() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Always update the conversation's state
|
// Always update the conversation's state
|
||||||
|
const isTerminalState =
|
||||||
|
mappedState === "idle" || mappedState === "success" || mappedState === "error";
|
||||||
if (conversation_id) {
|
if (conversation_id) {
|
||||||
claudeStore.setCharacterStateForConversation(conversation_id, mappedState);
|
claudeStore.setCharacterStateForConversation(conversation_id, mappedState);
|
||||||
|
if (isTerminalState) {
|
||||||
|
claudeStore.setProcessingForConversation(conversation_id, false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to active conversation if no conversation_id
|
// Fallback to active conversation if no conversation_id
|
||||||
const activeConversationId = get(claudeStore.activeConversationId);
|
const activeConversationId = get(claudeStore.activeConversationId);
|
||||||
if (activeConversationId) {
|
if (activeConversationId) {
|
||||||
claudeStore.setCharacterStateForConversation(activeConversationId, mappedState);
|
claudeStore.setCharacterStateForConversation(activeConversationId, mappedState);
|
||||||
|
if (isTerminalState) {
|
||||||
|
claudeStore.setProcessingForConversation(activeConversationId, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user