feat: add AskUserQuestion tool support (#60)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 53s
CI / Lint & Test (push) Successful in 14m12s
CI / Build Linux (push) Successful in 16m41s
CI / Build Windows (cross-compile) (push) Successful in 27m0s

## 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:
2026-01-23 14:11:18 -08:00
parent 94991796be
commit 06810537a9
11 changed files with 497 additions and 13 deletions
+24 -1
View File
@@ -6,7 +6,11 @@ import { characterState } from "$lib/stores/character";
import { configStore } from "$lib/stores/config";
import { initStatsListener, resetSessionStats } from "$lib/stores/stats";
import { initAchievementsListener } from "$lib/stores/achievements";
import type { ConnectionStatus, PermissionPromptEvent } from "$lib/types/messages";
import type {
ConnectionStatus,
PermissionPromptEvent,
UserQuestionEvent,
} from "$lib/types/messages";
import type { CharacterState } from "$lib/types/states";
import {
initializeNotificationRules,
@@ -317,6 +321,25 @@ export async function initializeTauriListeners() {
}
});
unlisteners.push(permissionUnlisten);
const questionUnlisten = await listen<UserQuestionEvent>("claude:question", (event) => {
const questionEvent = event.payload;
// Store question request for the specific conversation
if (questionEvent.conversation_id) {
claudeStore.requestQuestionForConversation(questionEvent.conversation_id, questionEvent);
claudeStore.addLineToConversation(
questionEvent.conversation_id,
"system",
`Question: ${questionEvent.question}`
);
} else {
// Fallback to active conversation if no conversation_id
claudeStore.requestQuestion(questionEvent);
claudeStore.addLine("system", `Question: ${questionEvent.question}`);
}
});
unlisteners.push(questionUnlisten);
}
export function cleanupTauriListeners() {