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 _permissionRequestsPath: string | undefined;
private _permissionWatcher: vscode.FileSystemWatcher | undefined;
private _pendingPermissionResolvers: Map<string, (approved: boolean) => 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<boolean> {
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({
type: 'permissionRequest',
data: {
id: request.id,
tool: toolName,
input: request.input
}
});
const result = await vscode.window.showWarningMessage(
message,
{ modal: true },
'Allow',
'Deny'
);
// 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);
});
}
return result === 'Allow';
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 {

View File

@@ -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<boolean> {
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();
@@ -54,7 +54,10 @@ async function requestPermission(tool_name: string, input: any): Promise<boolean
// Clean up response file
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));
@@ -67,11 +70,11 @@ async function requestPermission(tool_name: string, input: any): Promise<boolean
}
console.error(`Permission request ${requestId} timed out`);
return false;
return { approved: false, reason: "Permission request timed out" };
} catch (error) {
console.error(`Error requesting permission: ${error}`);
return false;
return { approved: false, reason: `Error processing permission request: ${error}` };
}
}
@@ -86,19 +89,26 @@ server.tool(
async ({ tool_name, input }) => {
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}`);
return {
content: [
{
type: "text",
text: JSON.stringify({
text: behavior === "allow" ?
JSON.stringify({
behavior: behavior,
updatedInput: input,
}),
})
:
JSON.stringify({
behavior: behavior,
message: permissionResult.reason || "Permission denied",
})
,
},
],
};

View File

@@ -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;

View File

@@ -1960,9 +1960,67 @@ const html = `<!DOCTYPE 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 = \`
<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
function newSession() {
vscode.postMessage({