From 3ec983188a1d330b2d8c04368976e158e855cb20 Mon Sep 17 00:00:00 2001 From: andrepimenta Date: Tue, 8 Jul 2025 23:57:38 +0100 Subject: [PATCH] Settings UI for permissions --- src/extension.ts | 86 ++++++++++++++++++++++++++++++++++ src/ui-styles.ts | 119 +++++++++++++++++++++++++++++++++++++++++++++++ src/ui.ts | 77 ++++++++++++++++++++++++++++++ 3 files changed, 282 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index 7d8d3ce..2be1ea1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -274,6 +274,12 @@ class ClaudeChatProvider { case 'permissionResponse': this._handlePermissionResponse(message.id, message.approved, message.alwaysAllow); return; + case 'getPermissions': + this._sendPermissions(); + return; + case 'removePermission': + this._removePermission(message.toolName, message.command); + return; } } @@ -1290,6 +1296,86 @@ class ClaudeChatProvider { return command; } + private async _sendPermissions(): Promise { + try { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { + this._postMessage({ + type: 'permissionsData', + data: { alwaysAllow: {} } + }); + return; + } + + const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json')); + let permissions: any = { alwaysAllow: {} }; + + try { + const content = await vscode.workspace.fs.readFile(permissionsUri); + permissions = JSON.parse(new TextDecoder().decode(content)); + } catch { + // File doesn't exist or can't be read, use default permissions + } + + this._postMessage({ + type: 'permissionsData', + data: permissions + }); + } catch (error) { + console.error('Error sending permissions:', error); + this._postMessage({ + type: 'permissionsData', + data: { alwaysAllow: {} } + }); + } + } + + private async _removePermission(toolName: string, command: string | null): Promise { + try { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) return; + + const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json')); + let permissions: any = { alwaysAllow: {} }; + + try { + const content = await vscode.workspace.fs.readFile(permissionsUri); + permissions = JSON.parse(new TextDecoder().decode(content)); + } catch { + // File doesn't exist or can't be read, nothing to remove + return; + } + + // Remove the permission + if (command === null) { + // Remove entire tool permission + delete permissions.alwaysAllow[toolName]; + } else { + // Remove specific command from tool permissions + if (Array.isArray(permissions.alwaysAllow[toolName])) { + permissions.alwaysAllow[toolName] = permissions.alwaysAllow[toolName].filter( + (cmd: string) => cmd !== command + ); + // If no commands left, remove the tool entirely + if (permissions.alwaysAllow[toolName].length === 0) { + delete permissions.alwaysAllow[toolName]; + } + } + } + + // Save updated permissions + const permissionsContent = new TextEncoder().encode(JSON.stringify(permissions, null, 2)); + await vscode.workspace.fs.writeFile(permissionsUri, permissionsContent); + + // Send updated permissions to UI + this._sendPermissions(); + + console.log(`Removed permission for ${toolName}${command ? ` command: ${command}` : ''}`); + } catch (error) { + console.error('Error removing permission:', error); + } + } + public getMCPConfigPath(): string | undefined { const storagePath = this._context.storageUri?.fsPath; if (!storagePath) {return undefined;} diff --git a/src/ui-styles.ts b/src/ui-styles.ts index 8baf16e..0339012 100644 --- a/src/ui-styles.ts +++ b/src/ui-styles.ts @@ -241,6 +241,125 @@ const styles = ` background-color: var(--vscode-inputValidation-errorBackground); } + /* Permissions Management */ + .permissions-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + background-color: var(--vscode-input-background); + margin-top: 8px; + } + + .permission-item { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 6px; + padding-right: 6px; + border-bottom: 1px solid var(--vscode-panel-border); + transition: background-color 0.2s ease; + min-height: 32px; + } + + .permission-item:hover { + background-color: var(--vscode-list-hoverBackground); + } + + .permission-item:last-child { + border-bottom: none; + } + + .permission-info { + display: flex; + align-items: center; + gap: 8px; + flex-grow: 1; + min-width: 0; + } + + .permission-tool { + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + padding: 3px 6px; + border-radius: 3px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + flex-shrink: 0; + height: 18px; + display: inline-flex; + align-items: center; + line-height: 1; + } + + .permission-command { + font-size: 12px; + color: var(--vscode-foreground); + flex-grow: 1; + } + + .permission-command code { + background-color: var(--vscode-textCodeBlock-background); + padding: 3px 6px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family); + color: var(--vscode-textLink-foreground); + font-size: 11px; + height: 18px; + display: inline-flex; + align-items: center; + line-height: 1; + } + + .permission-desc { + color: var(--vscode-descriptionForeground); + font-size: 11px; + font-style: italic; + flex-grow: 1; + height: 18px; + display: inline-flex; + align-items: center; + line-height: 1; + } + + .permission-remove-btn { + background-color: transparent; + color: var(--vscode-descriptionForeground); + border: none; + padding: 4px 8px; + border-radius: 3px; + cursor: pointer; + font-size: 10px; + transition: all 0.2s ease; + font-weight: 500; + flex-shrink: 0; + opacity: 0.7; + } + + .permission-remove-btn:hover { + background-color: rgba(231, 76, 60, 0.1); + color: var(--vscode-errorForeground); + opacity: 1; + } + + .permissions-empty { + padding: 16px; + text-align: center; + color: var(--vscode-descriptionForeground); + font-style: italic; + font-size: 13px; + } + + .permissions-empty::before { + content: "🔒"; + display: block; + font-size: 16px; + margin-bottom: 8px; + opacity: 0.5; + } + /* WSL Alert */ .wsl-alert { margin: 8px 12px; diff --git a/src/ui.ts b/src/ui.ts index b217972..aed512c 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -248,6 +248,20 @@ const html = ` +

Permissions

+
+

+ Manage commands and tools that are automatically allowed without asking for permission. +

+
+
+
+
+ Loading permissions... +
+
+
+

MCP Configuration (coming soon)

@@ -2434,6 +2448,10 @@ const html = ` vscode.postMessage({ type: 'getSettings' }); + // Request current permissions + vscode.postMessage({ + type: 'getPermissions' + }); settingsModal.style.display = 'flex'; } else { hideSettingsModal(); @@ -2467,6 +2485,60 @@ const html = ` }); } + // Permissions management functions + function renderPermissions(permissions) { + const permissionsList = document.getElementById('permissionsList'); + + if (!permissions || !permissions.alwaysAllow || Object.keys(permissions.alwaysAllow).length === 0) { + permissionsList.innerHTML = \` +

+ No always-allow permissions set +
+ \`; + return; + } + + let html = ''; + + for (const [toolName, permission] of Object.entries(permissions.alwaysAllow)) { + if (permission === true) { + // Tool is always allowed + html += \` +
+
+ \${toolName} + All +
+ +
+ \`; + } else if (Array.isArray(permission)) { + // Tool has specific commands/patterns + for (const command of permission) { + const displayCommand = command.replace(' *', ''); // Remove asterisk for display + html += \` +
+
+ \${toolName} + \${displayCommand} +
+ +
+ \`; + } + } + } + + permissionsList.innerHTML = html; + } + + function removePermission(toolName, command) { + vscode.postMessage({ + type: 'removePermission', + toolName: toolName, + command: command + }); + } // Close settings modal when clicking outside document.getElementById('settingsModal').addEventListener('click', (e) => { @@ -2537,6 +2609,11 @@ const html = ` }, 1000); } } + + if (message.type === 'permissionsData') { + // Update permissions UI + renderPermissions(message.data); + } });