mirror of
https://github.com/andrepimenta/claude-code-chat.git
synced 2025-12-13 05:39:46 +00:00
Permission UI
This commit is contained in:
@@ -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?`;
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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();
|
||||
@@ -40,7 +40,7 @@ async function requestPermission(tool_name: string, input: any): Promise<boolean
|
||||
|
||||
try {
|
||||
fs.writeFileSync(requestFile, JSON.stringify(request, null, 2));
|
||||
|
||||
|
||||
// Poll for response file
|
||||
const maxWaitTime = 30000; // 30 seconds timeout
|
||||
const pollInterval = 100; // Check every 100ms
|
||||
@@ -50,13 +50,16 @@ async function requestPermission(tool_name: string, input: any): Promise<boolean
|
||||
if (fs.existsSync(responseFile)) {
|
||||
const responseContent = fs.readFileSync(responseFile, 'utf8');
|
||||
const response = JSON.parse(responseContent);
|
||||
|
||||
|
||||
// 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));
|
||||
waitTime += pollInterval;
|
||||
}
|
||||
@@ -65,13 +68,13 @@ async function requestPermission(tool_name: string, input: any): Promise<boolean
|
||||
if (fs.existsSync(requestFile)) {
|
||||
fs.unlinkSync(requestFile);
|
||||
}
|
||||
|
||||
|
||||
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}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,20 +88,27 @@ server.tool(
|
||||
},
|
||||
async ({ tool_name, input }) => {
|
||||
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",
|
||||
})
|
||||
,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
116
src/ui-styles.ts
116
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;
|
||||
|
||||
58
src/ui.ts
58
src/ui.ts
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user