From ede4fbaf980f45bd65f5fa1796dae9326932de2f Mon Sep 17 00:00:00 2001 From: andrepimenta Date: Tue, 8 Jul 2025 22:01:33 +0100 Subject: [PATCH] Permission UI --- src/extension.ts | 39 +++++++--- src/permissions/mcp-permissions.ts | 48 +++++++----- src/ui-styles.ts | 116 +++++++++++++++++++++++++++++ src/ui.ts | 58 +++++++++++++++ 4 files changed, 232 insertions(+), 29 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 1a6f042..91f05ae 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -96,6 +96,7 @@ class ClaudeChatProvider { private _conversationsPath: string | undefined; private _permissionRequestsPath: string | undefined; private _permissionWatcher: vscode.FileSystemWatcher | undefined; + private _pendingPermissionResolvers: Map void> | undefined; private _currentConversation: Array<{ timestamp: string, messageType: string, data: any }> = []; private _conversationStartTime: string | undefined; private _conversationIndex: Array<{ @@ -270,6 +271,9 @@ class ClaudeChatProvider { case 'createImageFile': this._createImageFile(message.imageData, message.imageType); return; + case 'permissionResponse': + this._handlePermissionResponse(message.id, message.approved); + return; } } @@ -1086,18 +1090,33 @@ class ClaudeChatProvider { private async _showPermissionDialog(request: any): Promise { const toolName = request.tool || 'Unknown Tool'; - const toolInput = JSON.stringify(request.input, null, 2); - const message = `Tool "${toolName}" is requesting permission to execute:\n\n${toolInput}\n\nDo you want to allow this?`; - - const result = await vscode.window.showWarningMessage( - message, - { modal: true }, - 'Allow', - 'Deny' - ); + // Send permission request to the UI + this._postMessage({ + type: 'permissionRequest', + data: { + id: request.id, + tool: toolName, + input: request.input + } + }); - return result === 'Allow'; + // Wait for response from UI + return new Promise((resolve) => { + // Store the resolver so we can call it when we get the response + this._pendingPermissionResolvers = this._pendingPermissionResolvers || new Map(); + this._pendingPermissionResolvers.set(request.id, resolve); + }); + } + + private _handlePermissionResponse(id: string, approved: boolean): void { + if (this._pendingPermissionResolvers && this._pendingPermissionResolvers.has(id)) { + const resolver = this._pendingPermissionResolvers.get(id); + if (resolver) { + resolver(approved); + this._pendingPermissionResolvers.delete(id); + } + } } public getMCPConfigPath(): string | undefined { diff --git a/src/permissions/mcp-permissions.ts b/src/permissions/mcp-permissions.ts index c90580d..e37f262 100644 --- a/src/permissions/mcp-permissions.ts +++ b/src/permissions/mcp-permissions.ts @@ -20,10 +20,10 @@ function generateRequestId(): string { return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } -async function requestPermission(tool_name: string, input: any): Promise { +async function requestPermission(tool_name: string, input: any): Promise<{approved: boolean, reason?: string}> { if (!PERMISSIONS_PATH) { console.error("Permissions path not available"); - return false; + return { approved: false, reason: "Permissions path not configured" }; } const requestId = generateRequestId(); @@ -40,7 +40,7 @@ async function requestPermission(tool_name: string, input: any): Promise setTimeout(resolve, pollInterval)); waitTime += pollInterval; } @@ -65,13 +68,13 @@ async function requestPermission(tool_name: string, input: any): Promise { console.error(`Requesting permission for tool: ${tool_name}`); - - const approved = await requestPermission(tool_name, input); - - const behavior = approved ? "allow" : "deny"; + + const permissionResult = await requestPermission(tool_name, input); + + const behavior = permissionResult.approved ? "allow" : "deny"; console.error(`Permission ${behavior}ed for tool: ${tool_name}`); - + return { content: [ { type: "text", - text: JSON.stringify({ - behavior: behavior, - updatedInput: input, - }), + text: behavior === "allow" ? + JSON.stringify({ + behavior: behavior, + updatedInput: input, + }) + : + JSON.stringify({ + behavior: behavior, + message: permissionResult.reason || "Permission denied", + }) + , }, ], }; diff --git a/src/ui-styles.ts b/src/ui-styles.ts index e137898..e12689e 100644 --- a/src/ui-styles.ts +++ b/src/ui-styles.ts @@ -83,6 +83,122 @@ const styles = ` opacity: 1; } + /* Permission Request */ + .permission-request { + margin: 4px 12px 20px 12px; + background-color: var(--vscode-inputValidation-warningBackground); + border: 1px solid var(--vscode-inputValidation-warningBorder); + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + animation: slideUp 0.3s ease; + } + + .permission-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-weight: 600; + color: var(--vscode-foreground); + } + + .permission-header .icon { + font-size: 16px; + } + + .permission-content { + font-size: 13px; + line-height: 1.4; + color: var(--vscode-descriptionForeground); + } + + .permission-tool { + font-family: var(--vscode-editor-font-family); + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + padding: 8px 10px; + margin: 8px 0; + font-size: 12px; + color: var(--vscode-editor-foreground); + } + + .permission-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; + } + + .permission-buttons .btn { + font-size: 12px; + padding: 6px 12px; + min-width: 70px; + text-align: center; + display: inline-block; + } + + .permission-buttons .btn.allow { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border-color: var(--vscode-button-background); + } + + .permission-buttons .btn.allow:hover { + background-color: var(--vscode-button-hoverBackground); + } + + .permission-buttons .btn.deny { + background-color: transparent; + color: var(--vscode-foreground); + border-color: var(--vscode-panel-border); + } + + .permission-buttons .btn.deny:hover { + background-color: var(--vscode-list-hoverBackground); + border-color: var(--vscode-focusBorder); + } + + .permission-decision { + font-size: 13px; + font-weight: 600; + padding: 8px 12px; + text-align: center; + border-radius: 4px; + margin-top: 8px; + } + + .permission-decision.allowed { + background-color: rgba(0, 122, 204, 0.15); + color: var(--vscode-charts-blue); + border: 1px solid rgba(0, 122, 204, 0.3); + } + + .permission-decision.denied { + background-color: rgba(231, 76, 60, 0.15); + color: #e74c3c; + border: 1px solid rgba(231, 76, 60, 0.3); + } + + .permission-decided { + opacity: 0.7; + pointer-events: none; + } + + .permission-decided .permission-buttons { + display: none; + } + + .permission-decided.allowed { + border-color: var(--vscode-inputValidation-infoBackground); + background-color: rgba(0, 122, 204, 0.1); + } + + .permission-decided.denied { + border-color: var(--vscode-inputValidation-errorBorder); + background-color: var(--vscode-inputValidation-errorBackground); + } + /* WSL Alert */ .wsl-alert { margin: 8px 12px; diff --git a/src/ui.ts b/src/ui.ts index 6cdfe24..de197a4 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1960,9 +1960,67 @@ const html = ` // Display notification about checking the terminal addMessage(message.data, 'system'); break; + case 'permissionRequest': + addPermissionRequestMessage(message.data); + break; } }); + // Permission request functions + function addPermissionRequestMessage(data) { + const messageDiv = document.createElement('div'); + messageDiv.className = 'message permission-request'; + + const toolName = data.tool || 'Unknown Tool'; + + messageDiv.innerHTML = \` +
+ 🔐 + Permission Required +
+
+

Allow \${toolName} to execute the tool call above?

+
+ + +
+
+ \`; + + messagesDiv.appendChild(messageDiv); + messagesDiv.scrollTop = messagesDiv.scrollHeight; + } + + function respondToPermission(id, approved) { + // Send response back to extension + vscode.postMessage({ + type: 'permissionResponse', + id: id, + approved: approved + }); + + // Update the UI to show the decision + const permissionMsg = document.querySelector(\`.permission-request:has([onclick*="\${id}"])\`); + if (permissionMsg) { + const buttons = permissionMsg.querySelector('.permission-buttons'); + const permissionContent = permissionMsg.querySelector('.permission-content'); + const decision = approved ? 'You allowed this' : 'You denied this'; + const emoji = approved ? '✅' : '❌'; + const decisionClass = approved ? 'allowed' : 'denied'; + + // Hide buttons + buttons.style.display = 'none'; + + // Add decision div to permission-content + const decisionDiv = document.createElement('div'); + decisionDiv.className = \`permission-decision \${decisionClass}\`; + decisionDiv.innerHTML = \`\${emoji} \${decision}\`; + permissionContent.appendChild(decisionDiv); + + permissionMsg.classList.add('permission-decided', decisionClass); + } + } + // Session management functions function newSession() { vscode.postMessage({