From 79a1dc859a8f2f8cdbadbb624a8bde9b68b0fc12 Mon Sep 17 00:00:00 2001 From: Frangus90 <49726680+Frangus90@users.noreply.github.com> Date: Fri, 2 Jan 2026 01:58:10 +0100 Subject: [PATCH 1/9] Implement AskUserQuestion feature with UI and state management --- .claude/settings.local.json | 6 +- package-lock.json | 8 +- src/extension.ts | 160 ++++++++++++++++++++- src/script.ts | 272 ++++++++++++++++++++++++++++++++++++ src/ui-styles.ts | 196 ++++++++++++++++++++++++++ 5 files changed, 637 insertions(+), 5 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ba69db5..d9fba08 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,9 +5,11 @@ "Bash(grep:*)", "Bash(sed:*)", "Bash(rg:*)", - "Bash(npx tsc:*)" + "Bash(npx tsc:*)", + "Bash(npx typescript:*)", + "Bash(npm install:*)" ], "deny": [] }, "enableAllProjectMcpServers": false -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 82a87fc..a9fa5c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-chat", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-chat", - "version": "1.0.0", + "version": "1.1.0", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@types/mocha": "^10.0.10", @@ -1020,6 +1020,7 @@ "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", @@ -1519,6 +1520,7 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2652,6 +2654,7 @@ "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -6146,6 +6149,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/extension.ts b/src/extension.ts index 6d62328..96a57c4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -134,6 +134,17 @@ class ClaudeChatProvider { suggestions?: any[]; toolUseId: string; }> = new Map(); + // Pending AskUserQuestion requests from stdio control_request messages + private _pendingQuestionRequests: Map; + }>; + toolUseId: string; + }> = new Map(); private _currentConversation: Array<{ timestamp: string, messageType: string, data: any }> = []; private _conversationStartTime: string | undefined; private _conversationIndex: Array<{ @@ -153,6 +164,7 @@ class ClaudeChatProvider { private _selectedModel: string = 'default'; // Default model private _isProcessing: boolean | undefined; private _draftMessage: string = ''; + private _planModeEnabled: boolean = false; // Track plan mode state from webview constructor( private readonly _extensionUri: vscode.Uri, @@ -386,6 +398,16 @@ class ClaudeChatProvider { case 'saveInputText': this._saveInputText(message.text); return; + case 'planModeChanged': + this._planModeEnabled = message.enabled; + return; + case 'questionResponse': + this._handleQuestionResponse(message.id, message.answers); + return; + case 'triggerTestQuestion': + // Debug: trigger test AskUserQuestion UI + this._postMessage({ type: 'testAskUserQuestion' }); + return; } } @@ -1501,6 +1523,26 @@ class ClaudeChatProvider { console.log(`Permission request for tool: ${toolName}, requestId: ${requestId}`); + // Handle AskUserQuestion tool specially - show question UI instead of permission dialog + if (toolName === 'AskUserQuestion') { + this._handleAskUserQuestion(requestId, input.questions || [], toolUseId); + return; + } + + // Auto-deny EnterPlanMode if Plan First toggle is OFF + // This prevents Claude from entering plan mode on its own when user hasn't enabled it + if (toolName === 'EnterPlanMode' && !this._planModeEnabled) { + console.log('Auto-denying EnterPlanMode because Plan First toggle is OFF'); + this._sendPermissionResponse(requestId, false, { + requestId, + toolName, + input, + suggestions, + toolUseId + }, false); + return; + } + // Check if this tool is pre-approved const isPreApproved = await this._isToolPreApproved(toolName, input); @@ -1644,9 +1686,113 @@ class ClaudeChatProvider { } /** - * Cancel all pending permission requests (called when process ends) + * Handle AskUserQuestion tool requests from Claude CLI + * Shows interactive question UI to user and sends response back + */ + private _handleAskUserQuestion( + requestId: string, + questions: Array<{ + question: string; + header: string; + multiSelect: boolean; + options: Array<{ label: string; description: string }>; + }>, + toolUseId: string + ): void { + console.log(`AskUserQuestion request: ${requestId}, ${questions.length} questions`); + + // Store the pending request + this._pendingQuestionRequests.set(requestId, { + requestId, + questions, + toolUseId + }); + + // Send to webview for display + this._sendAndSaveMessage({ + type: 'askUserQuestion', + data: { + id: requestId, + questions: questions, + status: 'pending' + } + }); + } + + /** + * Handle user's answer to AskUserQuestion from webview + */ + private _handleQuestionResponse(id: string, answers: Record): void { + const pendingRequest = this._pendingQuestionRequests.get(id); + if (!pendingRequest) { + console.error('No pending question request found for id:', id); + return; + } + + // Remove from pending + this._pendingQuestionRequests.delete(id); + + // Send response to Claude CLI via stdin + this._sendQuestionResponse(id, answers, pendingRequest); + + // Update UI to show answered state + this._postMessage({ + type: 'updateQuestionStatus', + data: { + id: id, + status: 'answered', + answers: answers + } + }); + } + + /** + * Send AskUserQuestion response back to Claude CLI via stdin + */ + private _sendQuestionResponse( + requestId: string, + answers: Record, + pendingRequest: { + requestId: string; + questions: Array<{ + question: string; + header: string; + multiSelect: boolean; + options: Array<{ label: string; description: string }>; + }>; + toolUseId: string; + } + ): void { + if (!this._currentClaudeProcess?.stdin || this._currentClaudeProcess.stdin.destroyed) { + console.error('Cannot send question response: stdin not available'); + return; + } + + const response = { + type: 'control_response', + response: { + subtype: 'success', + request_id: requestId, + response: { + behavior: 'allow', + updatedInput: { + answers: answers + }, + toolUseID: pendingRequest.toolUseId + } + } + }; + + const responseJson = JSON.stringify(response) + '\n'; + console.log('Sending question response:', responseJson); + this._currentClaudeProcess.stdin.write(responseJson); + } + + /** + * Cancel all pending permission and question requests (called when process ends) */ private _cancelPendingPermissionRequests(): void { + // Cancel permission requests for (const [id, _request] of this._pendingPermissionRequests) { this._postMessage({ type: 'updatePermissionStatus', @@ -1657,6 +1803,18 @@ class ClaudeChatProvider { }); } this._pendingPermissionRequests.clear(); + + // Cancel question requests + for (const [id, _request] of this._pendingQuestionRequests) { + this._postMessage({ + type: 'updateQuestionStatus', + data: { + id: id, + status: 'cancelled' + } + }); + } + this._pendingQuestionRequests.clear(); } /** diff --git a/src/script.ts b/src/script.ts index 7029415..dd5270c 100644 --- a/src/script.ts +++ b/src/script.ts @@ -858,6 +858,11 @@ const getScript = (isTelemetryEnabled: boolean) => `