generated from nhcarrigan/template
feat: add AskUserQuestion tool support (#60)
## Summary Implements support for Claude's `AskUserQuestion` tool, allowing Claude to ask the user questions with multiple choice options during a conversation. ## Changes - Add `UserQuestionEvent` and `QuestionOption` types (Rust and TypeScript) - Detect `AskUserQuestion` in permission denials and emit `claude:question` event - Create `UserQuestionModal` component with option selection and custom answer input - Use stop/reconnect approach (same as `PermissionModal`) since Claude API doesn't accept tool_result for permission-denied tools - Add `pendingQuestion` to conversation store and `hasQuestionPending` derived store ## Technical Notes We discovered that Claude Code's permission denial system doesn't allow sending tool results back directly - the API rejects them with "unexpected tool_use_id found in tool_result blocks". The solution was to use the same stop/reconnect pattern that permissions use: stop the session, reconnect with context, and include the user's answer in the context restoration message. ## Test Plan - [x] Build compiles without errors (Rust + TypeScript) - [x] Question modal appears when Claude uses `AskUserQuestion` - [x] Can select options and submit answer - [x] Answer is properly restored to Claude after reconnect Closes #51 --- ✨ This PR was created with help from Hikari~ 🌸 Co-authored-by: Hikari <hikari@nhcarrigan.com> Reviewed-on: #60
This commit was merged in pull request #60.
This commit is contained in:
@@ -21,6 +21,7 @@ export const claudeStore = {
|
||||
currentWorkingDirectory: conversationsStore.currentWorkingDirectory,
|
||||
terminalLines: conversationsStore.terminalLines,
|
||||
pendingPermission: conversationsStore.pendingPermission,
|
||||
pendingQuestion: conversationsStore.pendingQuestion,
|
||||
isProcessing: conversationsStore.isProcessing,
|
||||
grantedTools: conversationsStore.grantedTools,
|
||||
pendingRetryMessage: conversationsStore.pendingRetryMessage,
|
||||
@@ -49,6 +50,10 @@ export const claudeStore = {
|
||||
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,
|
||||
@@ -89,6 +94,11 @@ export const hasPermissionPending = derived(
|
||||
($conversation) => $conversation?.pendingPermission !== null
|
||||
);
|
||||
|
||||
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],
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { writable, derived, get } from "svelte/store";
|
||||
import type { TerminalLine, ConnectionStatus, PermissionRequest } from "$lib/types/messages";
|
||||
import type {
|
||||
TerminalLine,
|
||||
ConnectionStatus,
|
||||
PermissionRequest,
|
||||
UserQuestionEvent,
|
||||
} from "$lib/types/messages";
|
||||
import type { CharacterState } from "$lib/types/states";
|
||||
import { cleanupConversationTracking } from "$lib/tauri";
|
||||
import { characterState } from "$lib/stores/character";
|
||||
@@ -15,6 +20,7 @@ export interface Conversation {
|
||||
isProcessing: boolean;
|
||||
grantedTools: Set<string>;
|
||||
pendingPermission: PermissionRequest | null;
|
||||
pendingQuestion: UserQuestionEvent | null;
|
||||
createdAt: Date;
|
||||
lastActivityAt: Date;
|
||||
}
|
||||
@@ -48,6 +54,7 @@ function createConversationsStore() {
|
||||
isProcessing: false,
|
||||
grantedTools: new Set(),
|
||||
pendingPermission: null,
|
||||
pendingQuestion: null,
|
||||
createdAt: new Date(),
|
||||
lastActivityAt: new Date(),
|
||||
};
|
||||
@@ -98,6 +105,7 @@ function createConversationsStore() {
|
||||
activeConversation,
|
||||
($conv) => $conv?.pendingPermission || null
|
||||
);
|
||||
const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null);
|
||||
|
||||
return {
|
||||
// Expose derived stores for compatibility
|
||||
@@ -106,6 +114,7 @@ function createConversationsStore() {
|
||||
currentWorkingDirectory: { subscribe: currentWorkingDirectory.subscribe },
|
||||
terminalLines: { subscribe: terminalLines.subscribe },
|
||||
pendingPermission: { subscribe: pendingPermission.subscribe },
|
||||
pendingQuestion: { subscribe: pendingQuestion.subscribe },
|
||||
isProcessing: { subscribe: isProcessing.subscribe },
|
||||
grantedTools: { subscribe: grantedTools.subscribe },
|
||||
pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe },
|
||||
@@ -199,6 +208,52 @@ function createConversationsStore() {
|
||||
return convs;
|
||||
});
|
||||
},
|
||||
requestQuestion: (question: UserQuestionEvent) => {
|
||||
const activeId = get(activeConversationId);
|
||||
if (!activeId) return;
|
||||
|
||||
conversations.update((convs) => {
|
||||
const conv = convs.get(activeId);
|
||||
if (conv) {
|
||||
conv.pendingQuestion = question;
|
||||
conv.lastActivityAt = new Date();
|
||||
}
|
||||
return convs;
|
||||
});
|
||||
},
|
||||
clearQuestion: () => {
|
||||
const activeId = get(activeConversationId);
|
||||
if (!activeId) return;
|
||||
|
||||
conversations.update((convs) => {
|
||||
const conv = convs.get(activeId);
|
||||
if (conv) {
|
||||
conv.pendingQuestion = null;
|
||||
conv.lastActivityAt = new Date();
|
||||
}
|
||||
return convs;
|
||||
});
|
||||
},
|
||||
requestQuestionForConversation: (conversationId: string, question: UserQuestionEvent) => {
|
||||
conversations.update((convs) => {
|
||||
const conv = convs.get(conversationId);
|
||||
if (conv) {
|
||||
conv.pendingQuestion = question;
|
||||
conv.lastActivityAt = new Date();
|
||||
}
|
||||
return convs;
|
||||
});
|
||||
},
|
||||
clearQuestionForConversation: (conversationId: string) => {
|
||||
conversations.update((convs) => {
|
||||
const conv = convs.get(conversationId);
|
||||
if (conv) {
|
||||
conv.pendingQuestion = null;
|
||||
conv.lastActivityAt = new Date();
|
||||
}
|
||||
return convs;
|
||||
});
|
||||
},
|
||||
setPendingRetryMessage: (message: string | null) => pendingRetryMessage.set(message),
|
||||
|
||||
// Conversation management
|
||||
|
||||
Reference in New Issue
Block a user