diff --git a/packages/cli/src/acp-integration/session/Session.ts b/packages/cli/src/acp-integration/session/Session.ts index 602b8ff9a..daad3df32 100644 --- a/packages/cli/src/acp-integration/session/Session.ts +++ b/packages/cli/src/acp-integration/session/Session.ts @@ -512,13 +512,27 @@ export class Session implements SessionContext { } const confirmationDetails = - this.config.getApprovalMode() !== ApprovalMode.YOLO - ? await invocation.shouldConfirmExecute(abortSignal) - : false; + await invocation.shouldConfirmExecute(abortSignal); + + // In YOLO mode, auto-approve everything except ask_user_question + // (the user must always have a chance to respond to questions) + const isAskUserQuestionTool = + confirmationDetails && confirmationDetails.type === 'ask_user_question'; + const effectiveConfirmationDetails = + this.config.getApprovalMode() === ApprovalMode.YOLO && + !isAskUserQuestionTool + ? false + : confirmationDetails; // Check for plan mode enforcement - block non-read-only tools + // but allow ask_user_question so users can answer clarification questions const isPlanMode = this.config.getApprovalMode() === ApprovalMode.PLAN; - if (isPlanMode && !isExitPlanModeTool && confirmationDetails) { + if ( + isPlanMode && + !isExitPlanModeTool && + !isAskUserQuestionTool && + effectiveConfirmationDetails + ) { // In plan mode, block any tool that requires confirmation (write operations) return errorResponse( new Error( @@ -528,25 +542,25 @@ export class Session implements SessionContext { ); } - if (confirmationDetails) { + if (effectiveConfirmationDetails) { const content: acp.ToolCallContent[] = []; - if (confirmationDetails.type === 'edit') { + if (effectiveConfirmationDetails.type === 'edit') { content.push({ type: 'diff', - path: confirmationDetails.fileName, - oldText: confirmationDetails.originalContent, - newText: confirmationDetails.newContent, + path: effectiveConfirmationDetails.fileName, + oldText: effectiveConfirmationDetails.originalContent, + newText: effectiveConfirmationDetails.newContent, }); } // Add plan content for exit_plan_mode - if (confirmationDetails.type === 'plan') { + if (effectiveConfirmationDetails.type === 'plan') { content.push({ type: 'content', content: { type: 'text', - text: confirmationDetails.plan, + text: effectiveConfirmationDetails.plan, }, }); } @@ -556,7 +570,7 @@ export class Session implements SessionContext { const params: acp.RequestPermissionRequest = { sessionId: this.sessionId, - options: toPermissionOptions(confirmationDetails), + options: toPermissionOptions(effectiveConfirmationDetails), toolCall: { toolCallId: callId, status: 'pending', @@ -576,7 +590,7 @@ export class Session implements SessionContext { .nativeEnum(ToolConfirmationOutcome) .parse(output.outcome.optionId); - await confirmationDetails.onConfirm(outcome, { + await effectiveConfirmationDetails.onConfirm(outcome, { answers: output.answers, }); diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 73983c812..193549245 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -103,7 +103,9 @@ export const Composer = () => { )} {/* Exclusive area: only one component visible at a time */} + {/* Hide footer when a confirmation dialog (e.g. ask_user_question) is active */} {!showSuggestions && + uiState.streamingState !== StreamingState.WaitingForConfirmation && (showShortcuts ? ( ) : ( diff --git a/packages/cli/src/ui/components/messages/AskUserQuestionDialog.test.tsx b/packages/cli/src/ui/components/messages/AskUserQuestionDialog.test.tsx index a8884d805..9ea69bcbd 100644 --- a/packages/cli/src/ui/components/messages/AskUserQuestionDialog.test.tsx +++ b/packages/cli/src/ui/components/messages/AskUserQuestionDialog.test.tsx @@ -402,8 +402,6 @@ describe('', () => { stdin.write('Orange'); await wait(); - console.log(lastFrame()); - expect(lastFrame()).toContain('Orange'); unmount(); }); diff --git a/packages/cli/src/ui/components/messages/AskUserQuestionDialog.tsx b/packages/cli/src/ui/components/messages/AskUserQuestionDialog.tsx index 635e8aaea..56a4eb61a 100644 --- a/packages/cli/src/ui/components/messages/AskUserQuestionDialog.tsx +++ b/packages/cli/src/ui/components/messages/AskUserQuestionDialog.tsx @@ -127,7 +127,7 @@ export const AskUserQuestionDialog: React.FC = ({ } else { if (currentQuestionIndex < totalTabs - 1) { setTimeout(() => { - setCurrentQuestionIndex(currentQuestionIndex + 1); + setCurrentQuestionIndex((prev) => Math.min(prev + 1, totalTabs - 1)); setSelectedIndex(0); }, 150); } @@ -166,7 +166,7 @@ export const AskUserQuestionDialog: React.FC = ({ // Auto-advance to next tab if (currentQuestionIndex < totalTabs - 1) { setTimeout(() => { - setCurrentQuestionIndex(currentQuestionIndex + 1); + setCurrentQuestionIndex((prev) => Math.min(prev + 1, totalTabs - 1)); setSelectedIndex(0); }, 150); } @@ -314,7 +314,9 @@ export const AskUserQuestionDialog: React.FC = ({ // Auto-advance to next tab after selection if (currentQuestionIndex < totalTabs - 1) { setTimeout(() => { - setCurrentQuestionIndex(currentQuestionIndex + 1); + setCurrentQuestionIndex((prev) => + Math.min(prev + 1, totalTabs - 1), + ); setSelectedIndex(0); }, 150); } @@ -352,7 +354,7 @@ export const AskUserQuestionDialog: React.FC = ({ ); })} - + ▸ {t('Submit')} @@ -368,7 +370,7 @@ export const AskUserQuestionDialog: React.FC = ({ {q.header}:{' '} {answer ? ( - {answer} + {answer} ) : ( {t('(not answered)')} )} @@ -386,7 +388,9 @@ export const AskUserQuestionDialog: React.FC = ({ {selectedIndex === 0 ? '❯ ' : ' '}1. {t('Submit answers')} @@ -394,7 +398,9 @@ export const AskUserQuestionDialog: React.FC = ({ {selectedIndex === 1 ? '❯ ' : ' '}2. {t('Cancel')} @@ -424,7 +430,7 @@ export const AskUserQuestionDialog: React.FC = ({ = ({ {!hasMultipleQuestions && ( - + {currentQuestion!.header} @@ -468,11 +474,15 @@ export const AskUserQuestionDialog: React.FC = ({ !isMultiSelect && selectedOptions[currentQuestionIndex] === opt.label; const isHighlighted = isSelected || isAnswered || isMultiChecked; + // Calculate prefix width for description alignment: + // 2 (cursor) + checkbox (4 if multi) + number + ". " (2) + const prefixWidth = + 2 + (isMultiSelect ? 4 : 0) + String(index + 1).length + 2; return ( {isSelected ? '❯ ' : ' '} @@ -482,7 +492,7 @@ export const AskUserQuestionDialog: React.FC = ({ {opt.description && ( - + {opt.description} )} @@ -495,7 +505,7 @@ export const AskUserQuestionDialog: React.FC = ({ {isCustomInputSelected ? ( // Inline TextInput replaces the option text - + ❯{' '} {isMultiSelect ? customInputChecked[currentQuestionIndex] @@ -534,7 +544,7 @@ export const AskUserQuestionDialog: React.FC = ({ color={ isCustomInputAnswer || customInputChecked[currentQuestionIndex] - ? theme.text.link + ? theme.text.accent : theme.text.primary } bold={ @@ -569,7 +579,7 @@ export const AskUserQuestionDialog: React.FC = ({ Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received (for example, to make edits). Instead, you should: 1. Answer the user's query comprehensively -2. When you're done researching, present your plan ${planOnly ? 'directly' : `by calling the ${ToolNames.EXIT_PLAN_MODE} tool, which will prompt the user to confirm the plan`}. Do NOT make any file changes or run any tools that modify the system state in any way until the user has confirmed the plan. Use AskUserQuestion if you need to clarify approaches. +2. When you're done researching, present your plan ${planOnly ? 'directly' : `by calling the ${ToolNames.EXIT_PLAN_MODE} tool, which will prompt the user to confirm the plan`}. Do NOT make any file changes or run any tools that modify the system state in any way until the user has confirmed the plan. Use ${ToolNames.ASK_USER_QUESTION} if you need to clarify approaches. `; } diff --git a/packages/core/src/tools/askUserQuestion.ts b/packages/core/src/tools/askUserQuestion.ts index 2b92f24e8..e1c6af26e 100644 --- a/packages/core/src/tools/askUserQuestion.ts +++ b/packages/core/src/tools/askUserQuestion.ts @@ -37,7 +37,6 @@ export interface Question { export interface AskUserQuestionParams { questions: Question[]; - answers?: Record; metadata?: { source?: string; }; @@ -117,16 +116,6 @@ const askUserQuestionToolSchemaData: FunctionDeclaration = { additionalProperties: false, }, }, - answers: { - description: 'User answers collected by the permission component', - type: 'object', - propertyNames: { - type: 'string', - }, - additionalProperties: { - type: 'string', - }, - }, metadata: { description: 'Optional metadata for tracking and analytics purposes. Not displayed to user.', diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index 929bb0b16..bf8f3f918 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -51,7 +51,7 @@ export class AcpConnection { onAskUserQuestion: (data: AskUserQuestionRequest) => Promise<{ optionId: string; answers?: Record; - }> = () => Promise.resolve({ optionId: 'proceed_once' }); + }> = () => Promise.resolve({ optionId: 'cancel' }); // Called after successful initialize() with the initialize result onInitialized: (init: unknown) => void = () => {}; diff --git a/packages/vscode-ide-companion/src/services/acpMessageHandler.ts b/packages/vscode-ide-companion/src/services/acpMessageHandler.ts index d43cc8f61..1e4ad153a 100644 --- a/packages/vscode-ide-companion/src/services/acpMessageHandler.ts +++ b/packages/vscode-ide-companion/src/services/acpMessageHandler.ts @@ -224,9 +224,9 @@ export class AcpMessageHandler { answers?: Record; }> { try { - // Check if this is an ask_user_question request - const isInteract = - params.toolCall?.toolCallId?.includes('ask_user_question'); + // Check if this is an ask_user_question request by inspecting rawInput + // (toolCallId is model-generated and unreliable for detection) + const isInteract = Array.isArray(params.toolCall?.rawInput?.questions); if (isInteract) { // Handle ask_user_question separately @@ -286,7 +286,8 @@ export class AcpMessageHandler { }, }; } - } catch (_error) { + } catch (error) { + console.error('[ACP] handlePermissionRequest failed:', error); return { outcome: { outcome: 'rejected', diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 166d2f8de..62a02af4e 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -53,7 +53,17 @@ export class WebViewProvider { this.agentManager = new QwenAgentManager(); this.conversationStore = new ConversationStore(context); this.panelManager = new PanelManager(extensionUri, () => { - // Panel dispose callback + // Panel dispose callback — unblock any pending ACP Promises + if (this.pendingPermissionResolve) { + this.pendingPermissionResolve('cancel'); + this.pendingPermissionResolve = null; + this.pendingPermissionRequest = null; + } + if (this.pendingAskUserQuestionResolve) { + this.pendingAskUserQuestionResolve({ optionId: 'cancel' }); + this.pendingAskUserQuestionResolve = null; + this.pendingAskUserQuestionRequest = null; + } this.disposables.forEach((d) => d.dispose()); }); this.messageHandler = new MessageHandler( @@ -1422,6 +1432,17 @@ export class WebViewProvider { * Dispose the WebView provider and clean up resources */ dispose(): void { + // Unblock any pending ACP Promises before tearing down + if (this.pendingPermissionResolve) { + this.pendingPermissionResolve('cancel'); + this.pendingPermissionResolve = null; + this.pendingPermissionRequest = null; + } + if (this.pendingAskUserQuestionResolve) { + this.pendingAskUserQuestionResolve({ optionId: 'cancel' }); + this.pendingAskUserQuestionResolve = null; + this.pendingAskUserQuestionRequest = null; + } this.panelManager.dispose(); this.agentManager.disconnect(); this.disposables.forEach((d) => d.dispose()); diff --git a/packages/webui/src/components/messages/AskUserQuestionDialog.tsx b/packages/webui/src/components/messages/AskUserQuestionDialog.tsx index 36da95a3c..d30926d99 100644 --- a/packages/webui/src/components/messages/AskUserQuestionDialog.tsx +++ b/packages/webui/src/components/messages/AskUserQuestionDialog.tsx @@ -175,6 +175,7 @@ export const AskUserQuestionDialog: FC = ({ const updated = { ...answerState, selectedOption: option.label, + customInput: undefined, }; setAnswers({ ...answers, [currentQuestionIndex]: updated });