Permission UI

This commit is contained in:
andrepimenta
2025-07-08 22:01:33 +01:00
parent 06eb335f7b
commit ede4fbaf98
4 changed files with 232 additions and 29 deletions

View File

@@ -96,6 +96,7 @@ class ClaudeChatProvider {
private _conversationsPath: string | undefined; private _conversationsPath: string | undefined;
private _permissionRequestsPath: string | undefined; private _permissionRequestsPath: string | undefined;
private _permissionWatcher: vscode.FileSystemWatcher | undefined; private _permissionWatcher: vscode.FileSystemWatcher | undefined;
private _pendingPermissionResolvers: Map<string, (approved: boolean) => void> | undefined;
private _currentConversation: Array<{ timestamp: string, messageType: string, data: any }> = []; private _currentConversation: Array<{ timestamp: string, messageType: string, data: any }> = [];
private _conversationStartTime: string | undefined; private _conversationStartTime: string | undefined;
private _conversationIndex: Array<{ private _conversationIndex: Array<{
@@ -270,6 +271,9 @@ class ClaudeChatProvider {
case 'createImageFile': case 'createImageFile':
this._createImageFile(message.imageData, message.imageType); this._createImageFile(message.imageData, message.imageType);
return; return;
case 'permissionResponse':
this._handlePermissionResponse(message.id, message.approved);
return;
} }
} }
@@ -1086,18 +1090,33 @@ class ClaudeChatProvider {
private async _showPermissionDialog(request: any): Promise<boolean> { private async _showPermissionDialog(request: any): Promise<boolean> {
const toolName = request.tool || 'Unknown Tool'; 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?`; // Send permission request to the UI
this._postMessage({
const result = await vscode.window.showWarningMessage( type: 'permissionRequest',
message, data: {
{ modal: true }, id: request.id,
'Allow', tool: toolName,
'Deny' 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 { public getMCPConfigPath(): string | undefined {

View File

@@ -20,10 +20,10 @@ function generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
} }
async function requestPermission(tool_name: string, input: any): Promise<boolean> { async function requestPermission(tool_name: string, input: any): Promise<{approved: boolean, reason?: string}> {
if (!PERMISSIONS_PATH) { if (!PERMISSIONS_PATH) {
console.error("Permissions path not available"); console.error("Permissions path not available");
return false; return { approved: false, reason: "Permissions path not configured" };
} }
const requestId = generateRequestId(); const requestId = generateRequestId();
@@ -40,7 +40,7 @@ async function requestPermission(tool_name: string, input: any): Promise<boolean
try { try {
fs.writeFileSync(requestFile, JSON.stringify(request, null, 2)); fs.writeFileSync(requestFile, JSON.stringify(request, null, 2));
// Poll for response file // Poll for response file
const maxWaitTime = 30000; // 30 seconds timeout const maxWaitTime = 30000; // 30 seconds timeout
const pollInterval = 100; // Check every 100ms const pollInterval = 100; // Check every 100ms
@@ -50,13 +50,16 @@ async function requestPermission(tool_name: string, input: any): Promise<boolean
if (fs.existsSync(responseFile)) { if (fs.existsSync(responseFile)) {
const responseContent = fs.readFileSync(responseFile, 'utf8'); const responseContent = fs.readFileSync(responseFile, 'utf8');
const response = JSON.parse(responseContent); const response = JSON.parse(responseContent);
// Clean up response file // Clean up response file
fs.unlinkSync(responseFile); fs.unlinkSync(responseFile);
return response.approved; return {
approved: response.approved,
reason: response.approved ? undefined : "User rejected the request"
};
} }
await new Promise(resolve => setTimeout(resolve, pollInterval)); await new Promise(resolve => setTimeout(resolve, pollInterval));
waitTime += pollInterval; waitTime += pollInterval;
} }
@@ -65,13 +68,13 @@ async function requestPermission(tool_name: string, input: any): Promise<boolean
if (fs.existsSync(requestFile)) { if (fs.existsSync(requestFile)) {
fs.unlinkSync(requestFile); fs.unlinkSync(requestFile);
} }
console.error(`Permission request ${requestId} timed out`); console.error(`Permission request ${requestId} timed out`);
return false; return { approved: false, reason: "Permission request timed out" };
} catch (error) { } catch (error) {
console.error(`Error requesting permission: ${error}`); console.error(`Error requesting permission: ${error}`);
return false; return { approved: false, reason: `Error processing permission request: ${error}` };
} }
} }
@@ -85,20 +88,27 @@ server.tool(
}, },
async ({ tool_name, input }) => { async ({ tool_name, input }) => {
console.error(`Requesting permission for tool: ${tool_name}`); console.error(`Requesting permission for tool: ${tool_name}`);
const approved = await requestPermission(tool_name, input); const permissionResult = await requestPermission(tool_name, input);
const behavior = approved ? "allow" : "deny"; const behavior = permissionResult.approved ? "allow" : "deny";
console.error(`Permission ${behavior}ed for tool: ${tool_name}`); console.error(`Permission ${behavior}ed for tool: ${tool_name}`);
return { return {
content: [ content: [
{ {
type: "text", type: "text",
text: JSON.stringify({ text: behavior === "allow" ?
behavior: behavior, JSON.stringify({
updatedInput: input, behavior: behavior,
}), updatedInput: input,
})
:
JSON.stringify({
behavior: behavior,
message: permissionResult.reason || "Permission denied",
})
,
}, },
], ],
}; };

View File

@@ -83,6 +83,122 @@ const styles = `
opacity: 1; 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 */
.wsl-alert { .wsl-alert {
margin: 8px 12px; margin: 8px 12px;

View File

@@ -1960,9 +1960,67 @@ const html = `<!DOCTYPE html>
// Display notification about checking the terminal // Display notification about checking the terminal
addMessage(message.data, 'system'); addMessage(message.data, 'system');
break; 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 = \`
<div class="permission-header">
<span class="icon">🔐</span>
<span>Permission Required</span>
</div>
<div class="permission-content">
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
<div class="permission-buttons">
<button class="btn allow" onclick="respondToPermission('\${data.id}', true)">Allow</button>
<button class="btn deny" onclick="respondToPermission('\${data.id}', false)">Deny</button>
</div>
</div>
\`;
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 // Session management functions
function newSession() { function newSession() {
vscode.postMessage({ vscode.postMessage({