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 _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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
})
|
||||||
|
,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
116
src/ui-styles.ts
116
src/ui-styles.ts
@@ -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;
|
||||||
|
|||||||
58
src/ui.ts
58
src/ui.ts
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user