Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class QuestionPartRenderer
}}
disabled={isDisabled}
key={index}
title={option.description || ''}
>
{option.text}
</button>
Expand Down
4 changes: 2 additions & 2 deletions packages/ai-chat/src/common/chat-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ export type QuestionResponseHandler = (
export interface QuestionResponseContent extends ChatResponseContent {
kind: 'question';
question: string;
options: { text: string, value?: string }[];
options: { text: string, value?: string, description?: string }[];
selectedOption?: { text: string, value?: string };
handler?: QuestionResponseHandler;
request?: MutableChatRequestModel;
Expand Down Expand Up @@ -2460,7 +2460,7 @@ export class QuestionResponseContentImpl implements QuestionResponseContent {

constructor(
public question: string,
public options: { text: string, value?: string }[],
public options: { text: string, value?: string, description?: string }[],
public request: MutableChatRequestModel | undefined,
public handler: QuestionResponseHandler | undefined,
selectedOption?: { text: string; value?: string }
Expand Down
113 changes: 111 additions & 2 deletions packages/ai-claude-code/src/browser/claude-code-chat-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { EditorManager } from '@theia/editor/lib/browser';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import {
AskUserQuestionInput,
ContentBlock,
EditInput,
MultiEditInput,
Expand All @@ -59,6 +60,8 @@ export const CLAUDE_APPROVAL_TOOL_INPUTS_KEY = 'claudeApprovalToolInputs';
export const CLAUDE_MODEL_NAME_KEY = 'claudeModelName';
export const CLAUDE_COST_KEY = 'claudeCost';
export const CLAUDE_SESSION_APPROVED_TOOLS_KEY = 'claudeSessionApprovedTools';
export const CLAUDE_ASK_USER_QUESTION_IDS_KEY = 'claudeAskUserQuestionIds';
export const CLAUDE_PENDING_ASK_USER_QUESTIONS_KEY = 'claudePendingAskUserQuestions';

const APPROVAL_OPTIONS = [
{ text: nls.localizeByDefault('Allow'), value: 'allow' },
Expand Down Expand Up @@ -354,6 +357,11 @@ export class ClaudeCodeChatAgent implements ChatAgent {
approvalRequest: ToolApprovalRequestMessage,
request: MutableChatRequestModel
): void {
if (approvalRequest.toolName === 'AskUserQuestion') {
this.handleAskUserQuestion(approvalRequest, request);
return;
}

if (this.isToolApprovedForSession(request, approvalRequest.toolName)) {
const response: ToolApprovalResponseMessage = {
type: 'tool-approval-response',
Expand Down Expand Up @@ -421,12 +429,102 @@ export class ClaudeCodeChatAgent implements ChatAgent {

this.claudeCode.sendApprovalResponse(response);

// Only stop waiting for input if there are no more pending approvals
if (pendingApprovals.size === 0) {
// Only stop waiting for input if there are no more pending approvals or questions
if (pendingApprovals.size === 0 && this.getPendingAskUserQuestions(request).size === 0) {
request.response.stopWaitingForInput();
}
}

protected handleAskUserQuestion(
approvalRequest: ToolApprovalRequestMessage,
request: MutableChatRequestModel
): void {
const toolInput = approvalRequest.toolInput;
if (!AskUserQuestionInput.is(toolInput)) {
const response: ToolApprovalResponseMessage = {
type: 'tool-approval-response',
requestId: approvalRequest.requestId,
approved: false,
message: 'Invalid AskUserQuestion input format'
};
this.claudeCode.sendApprovalResponse(response);
return;
}

const questions = toolInput.questions;
const answers: Record<string, string> = {};
let answeredCount = 0;
const totalQuestions = questions.length;

this.getPendingAskUserQuestions(request).add(approvalRequest.requestId);

for (const questionItem of questions) {
const options = questionItem.options.map(opt => ({
text: opt.label,
value: opt.label,
description: opt.description
}));

const questionText = questionItem.header
? `**${questionItem.header}:** ${questionItem.question}`
: questionItem.question;

const questionContent = new QuestionResponseContentImpl(
questionText,
options,
request,
(selectedOption) => {
answers[questionItem.question] = selectedOption.value ?? selectedOption.text;
answeredCount++;

if (answeredCount === totalQuestions) {
this.getPendingAskUserQuestions(request).delete(approvalRequest.requestId);

const updatedInput: AskUserQuestionInput = {
...toolInput,
answers
};

const response: ToolApprovalResponseMessage = {
type: 'tool-approval-response',
requestId: approvalRequest.requestId,
approved: true,
updatedInput
};

this.claudeCode.sendApprovalResponse(response);

if (this.getPendingApprovals(request).size === 0 && this.getPendingAskUserQuestions(request).size === 0) {
request.response.stopWaitingForInput();
}
}
}
);

request.response.response.addContent(questionContent);
}

request.response.waitForInput();
}

protected getPendingAskUserQuestions(request: MutableChatRequestModel): Set<string> {
let ids = request.getDataByKey(CLAUDE_PENDING_ASK_USER_QUESTIONS_KEY) as Set<string> | undefined;
if (!ids) {
ids = new Set<string>();
request.addData(CLAUDE_PENDING_ASK_USER_QUESTIONS_KEY, ids);
}
return ids;
}

protected getAskUserQuestionToolUseIds(request: MutableChatRequestModel): Set<string> {
let ids = request.getDataByKey(CLAUDE_ASK_USER_QUESTION_IDS_KEY) as Set<string> | undefined;
if (!ids) {
ids = new Set<string>();
request.addData(CLAUDE_ASK_USER_QUESTION_IDS_KEY, ids);
}
return ids;
}

protected getEditToolUses(request: MutableChatRequestModel): Map<string, ToolUseBlock> | undefined {
return request.getDataByKey(CLAUDE_EDIT_TOOL_USES_KEY);
}
Expand Down Expand Up @@ -577,6 +675,12 @@ export class ClaudeCodeChatAgent implements ChatAgent {
break;
case 'tool_use':
case 'server_tool_use':
// Suppress AskUserQuestion - already shown as interactive questions via the approval flow
if (block.name === 'AskUserQuestion') {
this.getAskUserQuestionToolUseIds(request).add(block.id);
break;
}

if (block.name === 'Task' && TaskInput.is(block.input)) {
request.response.response.addContent(new MarkdownChatResponseContentImpl(`\n\n### Task: ${block.input.description}\n\n${block.input.prompt}`));
}
Expand All @@ -594,6 +698,11 @@ export class ClaudeCodeChatAgent implements ChatAgent {
request.response.response.addContent(new ClaudeCodeToolCallChatResponseContent(block.id, block.name, JSON.stringify(block.input)));
break;
case 'tool_result':
// Suppress AskUserQuestion tool results
if (this.getAskUserQuestionToolUseIds(request).has(block.tool_use_id)) {
break;
}

if (this.getEditToolUses(request)?.has(block.tool_use_id)) {
const toolUse = this.getEditToolUses(request)?.get(block.tool_use_id);
if (toolUse) {
Expand Down
32 changes: 32 additions & 0 deletions packages/ai-claude-code/src/common/claude-code-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,35 @@ export namespace WriteInput {
typeof (input as WriteInput).content === 'string';
}
}

export interface AskUserQuestionOption {
label: string;
description: string;
}

export interface AskUserQuestionItem {
question: string;
header: string;
options: AskUserQuestionOption[];
multiSelect: boolean;
}

export interface AskUserQuestionInput {
questions: AskUserQuestionItem[];
answers?: Record<string, string>;
}

export namespace AskUserQuestionInput {
export function is(input: unknown): input is AskUserQuestionInput {
// eslint-disable-next-line no-null/no-null
return typeof input === 'object' && input !== null &&
'questions' in input &&
Array.isArray((input as AskUserQuestionInput).questions) &&
(input as AskUserQuestionInput).questions.every(q =>
// eslint-disable-next-line no-null/no-null
typeof q === 'object' && q !== null &&
typeof q.question === 'string' &&
Array.isArray(q.options)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class ClaudeCodeServiceImpl implements ClaudeCodeService {
private pendingApprovals = new Map<string, (result: ToolApprovalResult) => void>();

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

setClient(client: ClaudeCodeClient): void {
this.client = client;
Expand Down
Loading