generated from nhcarrigan/template
fix: resolve message submission and stuck processing bugs (#199)
## Summary - **Fix `isProcessing` tracking**: The `isProcessing` store field was initialised as `false` and never set to `true` in production, making all submission guards no-ops. Now `setProcessing(true)` is called after `send_prompt` succeeds in both `handleSubmit` and `handleQuickAction`, and `setProcessingForConversation(id, false)` is called when the backend emits an idle/success/error state. - **Fix auto-granted tools dropped on permission reconnect** (closes #198): `PermissionModal.svelte` was passing only session-granted tools when reconnecting after a permission approval, silently dropping `config.auto_granted_tools`. Fixed to merge both sets, matching the behaviour of every other `start_claude` call site. - **Add mid-session watchdog**: A watchdog thread now kills the Claude Code process if a user message is sent but no `Result` arrives within 5 minutes. This triggers the existing disconnect/error flow so the user is notified and can reconnect. A generation counter ensures watchdogs from previous sessions exit cleanly when a new session starts. ## Test plan - [ ] Send a message and verify the textarea is disabled and the stop button is visible while Claude is processing - [ ] Verify the textarea re-enables after Claude finishes responding - [ ] Enable a tool in default permissions (e.g. Read), start a session, trigger a permission approval for another tool, approve it — verify the previously auto-granted tool is no longer re-prompted - [ ] Verify all existing tests pass (`./check-all.sh`) ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #199 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #199.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -561,3 +561,99 @@ 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user