Skip to content

Commit 8d99d5f

Browse files
authored
feat(ai-claude-code): Support for the AskUserQuestion functionality (#16981)
1 parent 78112b2 commit 8d99d5f

File tree

5 files changed

+148
-5
lines changed

5 files changed

+148
-5
lines changed

packages/ai-chat-ui/src/browser/chat-response-renderer/question-part-renderer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export class QuestionPartRenderer
5050
}}
5151
disabled={isDisabled}
5252
key={index}
53+
title={option.description}
5354
>
5455
{option.text}
5556
</button>

packages/ai-chat/src/common/chat-model.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,7 @@ export type QuestionResponseHandler = (
751751
export interface QuestionResponseContent extends ChatResponseContent {
752752
kind: 'question';
753753
question: string;
754-
options: { text: string, value?: string }[];
754+
options: { text: string, value?: string, description?: string }[];
755755
selectedOption?: { text: string, value?: string };
756756
handler?: QuestionResponseHandler;
757757
request?: MutableChatRequestModel;
@@ -2460,7 +2460,7 @@ export class QuestionResponseContentImpl implements QuestionResponseContent {
24602460

24612461
constructor(
24622462
public question: string,
2463-
public options: { text: string, value?: string }[],
2463+
public options: { text: string, value?: string, description?: string }[],
24642464
public request: MutableChatRequestModel | undefined,
24652465
public handler: QuestionResponseHandler | undefined,
24662466
selectedOption?: { text: string; value?: string }

packages/ai-claude-code/src/browser/claude-code-chat-agent.ts

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { EditorManager } from '@theia/editor/lib/browser';
3333
import { FileService } from '@theia/filesystem/lib/browser/file-service';
3434
import { WorkspaceService } from '@theia/workspace/lib/browser';
3535
import {
36+
AskUserQuestionInput,
3637
ContentBlock,
3738
EditInput,
3839
MultiEditInput,
@@ -59,6 +60,8 @@ export const CLAUDE_APPROVAL_TOOL_INPUTS_KEY = 'claudeApprovalToolInputs';
5960
export const CLAUDE_MODEL_NAME_KEY = 'claudeModelName';
6061
export const CLAUDE_COST_KEY = 'claudeCost';
6162
export const CLAUDE_SESSION_APPROVED_TOOLS_KEY = 'claudeSessionApprovedTools';
63+
export const CLAUDE_ASK_USER_QUESTION_IDS_KEY = 'claudeAskUserQuestionIds';
64+
export const CLAUDE_PENDING_ASK_USER_QUESTIONS_KEY = 'claudePendingAskUserQuestions';
6265

6366
const APPROVAL_OPTIONS = [
6467
{ text: nls.localizeByDefault('Allow'), value: 'allow' },
@@ -354,6 +357,11 @@ export class ClaudeCodeChatAgent implements ChatAgent {
354357
approvalRequest: ToolApprovalRequestMessage,
355358
request: MutableChatRequestModel
356359
): void {
360+
if (approvalRequest.toolName === 'AskUserQuestion') {
361+
this.handleAskUserQuestion(approvalRequest, request);
362+
return;
363+
}
364+
357365
if (this.isToolApprovedForSession(request, approvalRequest.toolName)) {
358366
const response: ToolApprovalResponseMessage = {
359367
type: 'tool-approval-response',
@@ -421,12 +429,103 @@ export class ClaudeCodeChatAgent implements ChatAgent {
421429

422430
this.claudeCode.sendApprovalResponse(response);
423431

424-
// Only stop waiting for input if there are no more pending approvals
425-
if (pendingApprovals.size === 0) {
432+
// Only stop waiting for input if there are no more pending approvals or questions
433+
if (pendingApprovals.size === 0 && this.getPendingAskUserQuestions(request).size === 0) {
426434
request.response.stopWaitingForInput();
427435
}
428436
}
429437

438+
protected handleAskUserQuestion(
439+
approvalRequest: ToolApprovalRequestMessage,
440+
request: MutableChatRequestModel
441+
): void {
442+
const toolInput = approvalRequest.toolInput;
443+
if (!AskUserQuestionInput.is(toolInput)) {
444+
const response: ToolApprovalResponseMessage = {
445+
type: 'tool-approval-response',
446+
requestId: approvalRequest.requestId,
447+
approved: false,
448+
message: 'Invalid AskUserQuestion input format'
449+
};
450+
this.claudeCode.sendApprovalResponse(response);
451+
return;
452+
}
453+
454+
const questions = toolInput.questions;
455+
const answers: Record<string, string> = {};
456+
let answeredCount = 0;
457+
const totalQuestions = questions.length;
458+
459+
this.getPendingAskUserQuestions(request).add(approvalRequest.requestId);
460+
461+
for (const questionItem of questions) {
462+
const options = questionItem.options.map(opt => ({
463+
text: opt.label,
464+
value: opt.label,
465+
description: opt.description
466+
}));
467+
468+
const questionText = questionItem.header
469+
? `**${questionItem.header}:** ${questionItem.question}`
470+
: questionItem.question;
471+
472+
const questionContent = new QuestionResponseContentImpl(
473+
questionText,
474+
options,
475+
request,
476+
selectedOption => {
477+
// Key by question text to match the Claude Code SDK's expected answers format
478+
answers[questionItem.question] = selectedOption.value ?? selectedOption.text;
479+
answeredCount++;
480+
481+
if (answeredCount === totalQuestions) {
482+
this.getPendingAskUserQuestions(request).delete(approvalRequest.requestId);
483+
484+
const updatedInput: AskUserQuestionInput = {
485+
...toolInput,
486+
answers
487+
};
488+
489+
const response: ToolApprovalResponseMessage = {
490+
type: 'tool-approval-response',
491+
requestId: approvalRequest.requestId,
492+
approved: true,
493+
updatedInput
494+
};
495+
496+
this.claudeCode.sendApprovalResponse(response);
497+
498+
if (this.getPendingApprovals(request).size === 0 && this.getPendingAskUserQuestions(request).size === 0) {
499+
request.response.stopWaitingForInput();
500+
}
501+
}
502+
}
503+
);
504+
505+
request.response.response.addContent(questionContent);
506+
}
507+
508+
request.response.waitForInput();
509+
}
510+
511+
protected getPendingAskUserQuestions(request: MutableChatRequestModel): Set<string> {
512+
let ids = request.getDataByKey(CLAUDE_PENDING_ASK_USER_QUESTIONS_KEY) as Set<string> | undefined;
513+
if (!ids) {
514+
ids = new Set<string>();
515+
request.addData(CLAUDE_PENDING_ASK_USER_QUESTIONS_KEY, ids);
516+
}
517+
return ids;
518+
}
519+
520+
protected getAskUserQuestionToolUseIds(request: MutableChatRequestModel): Set<string> {
521+
let ids = request.getDataByKey(CLAUDE_ASK_USER_QUESTION_IDS_KEY) as Set<string> | undefined;
522+
if (!ids) {
523+
ids = new Set<string>();
524+
request.addData(CLAUDE_ASK_USER_QUESTION_IDS_KEY, ids);
525+
}
526+
return ids;
527+
}
528+
430529
protected getEditToolUses(request: MutableChatRequestModel): Map<string, ToolUseBlock> | undefined {
431530
return request.getDataByKey(CLAUDE_EDIT_TOOL_USES_KEY);
432531
}
@@ -577,6 +676,12 @@ export class ClaudeCodeChatAgent implements ChatAgent {
577676
break;
578677
case 'tool_use':
579678
case 'server_tool_use':
679+
// Suppress AskUserQuestion - already shown as interactive questions via the approval flow
680+
if (block.name === 'AskUserQuestion') {
681+
this.getAskUserQuestionToolUseIds(request).add(block.id);
682+
break;
683+
}
684+
580685
if (block.name === 'Task' && TaskInput.is(block.input)) {
581686
request.response.response.addContent(new MarkdownChatResponseContentImpl(`\n\n### Task: ${block.input.description}\n\n${block.input.prompt}`));
582687
}
@@ -594,6 +699,11 @@ export class ClaudeCodeChatAgent implements ChatAgent {
594699
request.response.response.addContent(new ClaudeCodeToolCallChatResponseContent(block.id, block.name, JSON.stringify(block.input)));
595700
break;
596701
case 'tool_result':
702+
// Suppress AskUserQuestion tool results
703+
if (this.getAskUserQuestionToolUseIds(request).has(block.tool_use_id)) {
704+
break;
705+
}
706+
597707
if (this.getEditToolUses(request)?.has(block.tool_use_id)) {
598708
const toolUse = this.getEditToolUses(request)?.get(block.tool_use_id);
599709
if (toolUse) {

packages/ai-claude-code/src/common/claude-code-service.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,35 @@ export namespace WriteInput {
310310
typeof (input as WriteInput).content === 'string';
311311
}
312312
}
313+
314+
export interface AskUserQuestionOption {
315+
label: string;
316+
description?: string;
317+
}
318+
319+
export interface AskUserQuestionItem {
320+
question: string;
321+
header: string;
322+
options: AskUserQuestionOption[];
323+
multiSelect: boolean;
324+
}
325+
326+
export interface AskUserQuestionInput {
327+
questions: AskUserQuestionItem[];
328+
answers?: Record<string, string>;
329+
}
330+
331+
export namespace AskUserQuestionInput {
332+
export function is(input: unknown): input is AskUserQuestionInput {
333+
// eslint-disable-next-line no-null/no-null
334+
return typeof input === 'object' && input !== null &&
335+
'questions' in input &&
336+
Array.isArray((input as AskUserQuestionInput).questions) &&
337+
(input as AskUserQuestionInput).questions.every(q =>
338+
// eslint-disable-next-line no-null/no-null
339+
typeof q === 'object' && q !== null &&
340+
typeof q.question === 'string' &&
341+
Array.isArray(q.options)
342+
);
343+
}
344+
}

packages/ai-claude-code/src/node/claude-code-service-impl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class ClaudeCodeServiceImpl implements ClaudeCodeService {
4545
private pendingApprovals = new Map<string, (result: ToolApprovalResult) => void>();
4646

4747
// Tools that don't require approval - they are safe and non-intrusive
48-
protected readonly autoApprovedTools = new Set(['AskUserQuestion']);
48+
protected readonly autoApprovedTools = new Set<string>();
4949

5050
setClient(client: ClaudeCodeClient): void {
5151
this.client = client;

0 commit comments

Comments
 (0)