Migrate permission system from MCP file-based to stdio-based

Replace MCP permission server with stdio-based permission flow that
communicates directly with Claude CLI via stdin/stdout. This simplifies
the architecture and fixes permission expiration issues.

Key changes:
- Use --permission-prompt-tool stdio and --input-format stream-json
- Handle control_request messages for permission prompts
- Send control_response via stdin to approve/deny
- Check local permissions for auto-approval of pre-approved tools
- Only expire pending permissions when VS Code restarts, not panel close

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
andrepimenta
2025-12-02 18:07:45 +00:00
parent 0764bf8202
commit a156881a08
4 changed files with 479 additions and 206 deletions

View File

@@ -12,3 +12,5 @@ vsc-extension-quickstart.md
backup
.claude
claude-code-chat-permissions-mcp/**
node_modules
mcp-permissions.js

View File

@@ -124,9 +124,14 @@ class ClaudeChatProvider {
private _backupRepoPath: string | undefined;
private _commits: Array<{ id: string, sha: string, message: string, timestamp: string }> = [];
private _conversationsPath: string | undefined;
private _permissionRequestsPath: string | undefined;
private _permissionWatcher: vscode.FileSystemWatcher | undefined;
private _pendingPermissionResolvers: Map<string, (approved: boolean) => void> | undefined;
// Pending permission requests from stdio control_request messages
private _pendingPermissionRequests: Map<string, {
requestId: string;
toolName: string;
input: Record<string, unknown>;
suggestions?: any[];
toolUseId: string;
}> = new Map();
private _currentConversation: Array<{ timestamp: string, messageType: string, data: any }> = [];
private _conversationStartTime: string | undefined;
private _conversationIndex: Array<{
@@ -496,9 +501,12 @@ class ClaudeChatProvider {
});
// Build command arguments with session management
// Use stream-json for both input and output to enable bidirectional communication
// This is required for stdio-based permission prompts
const args = [
'-p',
'--output-format', 'stream-json', '--verbose'
'--output-format', 'stream-json',
'--input-format', 'stream-json',
'--verbose'
];
// Get configuration
@@ -506,16 +514,17 @@ class ClaudeChatProvider {
const yoloMode = config.get<boolean>('permissions.yoloMode', false);
if (yoloMode) {
// Yolo mode: skip all permissions regardless of MCP config
// Yolo mode: skip all permissions
args.push('--dangerously-skip-permissions');
} else {
// Add MCP configuration for permissions
// Use stdio-based permission prompts (no MCP server needed)
args.push('--permission-prompt-tool', 'stdio');
}
// Add MCP config if user has custom servers configured
const mcpConfigPath = this.getMCPConfigPath();
if (mcpConfigPath) {
args.push('--mcp-config', this.convertToWSLPath(mcpConfigPath));
args.push('--allowedTools', 'mcp__claude-code-chat-permissions__approval_prompt');
args.push('--permission-prompt-tool', 'mcp__claude-code-chat-permissions__approval_prompt');
}
}
// Add model selection if not using default
@@ -585,10 +594,19 @@ class ClaudeChatProvider {
// Store process reference for potential termination
this._currentClaudeProcess = claudeProcess;
// Send the message to Claude's stdin (with mode prefixes if enabled)
// Send the message to Claude's stdin as JSON (stream-json input format)
// Don't end stdin yet - we need to keep it open for permission responses
if (claudeProcess.stdin) {
claudeProcess.stdin.write(actualMessage + '\n');
claudeProcess.stdin.end();
const userMessage = {
type: 'user',
session_id: this._currentSessionId || '',
message: {
role: 'user',
content: [{ type: 'text', text: actualMessage }]
},
parent_tool_use_id: null
};
claudeProcess.stdin.write(JSON.stringify(userMessage) + '\n');
}
let rawOutput = '';
@@ -606,6 +624,22 @@ class ClaudeChatProvider {
if (line.trim()) {
try {
const jsonData = JSON.parse(line.trim());
// Handle control_request messages (permission requests via stdio)
if (jsonData.type === 'control_request') {
this._handleControlRequest(jsonData, claudeProcess).catch(err => {
console.error('Error handling control request:', err);
});
continue;
}
// Handle result message - end stdin when done
if (jsonData.type === 'result') {
if (claudeProcess.stdin && !claudeProcess.stdin.destroyed) {
claudeProcess.stdin.end();
}
}
this._processJsonStreamData(jsonData);
} catch (error) {
console.log('Failed to parse JSON line:', line, error);
@@ -632,6 +666,9 @@ class ClaudeChatProvider {
// Clear process reference
this._currentClaudeProcess = undefined;
// Cancel any pending permission requests (process is gone)
this._cancelPendingPermissionRequests();
// Clear loading indicator and set processing to false
this._postMessage({
type: 'clearLoading'
@@ -665,6 +702,9 @@ class ClaudeChatProvider {
// Clear process reference
this._currentClaudeProcess = undefined;
// Cancel any pending permission requests (process is gone)
this._cancelPendingPermissionRequests();
this._postMessage({
type: 'clearLoading'
});
@@ -1270,10 +1310,9 @@ class ClaudeChatProvider {
console.log(`Created MCP config directory at: ${mcpConfigDir}`);
}
// Create or update mcp-servers.json with permissions server, preserving existing servers
// Create or update mcp-servers.json, preserving user's custom servers
// Note: Permissions are now handled via stdio, not MCP
const mcpConfigPath = path.join(mcpConfigDir, 'mcp-servers.json');
const mcpPermissionsPath = this.convertToWSLPath(path.join(this._extensionUri.fsPath, 'mcp-permissions.js'));
const permissionRequestsPath = this.convertToWSLPath(path.join(storagePath, 'permission-requests'));
// Load existing config or create new one
let mcpConfig: any = { mcpServers: {} };
@@ -1292,14 +1331,11 @@ class ClaudeChatProvider {
mcpConfig.mcpServers = {};
}
// Add or update the permissions server entry
mcpConfig.mcpServers['claude-code-chat-permissions'] = {
command: 'node',
args: [mcpPermissionsPath],
env: {
CLAUDE_PERMISSIONS_PATH: permissionRequestsPath
// Remove old permissions server if it exists (migrating from file-based to stdio)
if (mcpConfig.mcpServers['claude-code-chat-permissions']) {
delete mcpConfig.mcpServers['claude-code-chat-permissions'];
console.log('Removed legacy permissions MCP server (now using stdio)');
}
};
const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2));
await vscode.workspace.fs.writeFile(mcpConfigUri, configContent);
@@ -1310,180 +1346,293 @@ class ClaudeChatProvider {
}
}
private async _initializePermissions(): Promise<void> {
/**
* Check if a tool is pre-approved in local permissions
*/
private async _isToolPreApproved(toolName: string, input: Record<string, unknown>): Promise<boolean> {
try {
if (this._permissionWatcher) {
this._permissionWatcher.dispose();
this._permissionWatcher = undefined;
}
const storagePath = this._context.storageUri?.fsPath;
if (!storagePath) { return; }
if (!storagePath) return false;
// Create permission requests directory
this._permissionRequestsPath = path.join(path.join(storagePath, 'permission-requests'));
try {
await vscode.workspace.fs.stat(vscode.Uri.file(this._permissionRequestsPath));
} catch {
await vscode.workspace.fs.createDirectory(vscode.Uri.file(this._permissionRequestsPath));
console.log(`Created permission requests directory at: ${this._permissionRequestsPath}`);
}
console.log("DIRECTORY-----", this._permissionRequestsPath)
// Set up file watcher for *.request files
this._permissionWatcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(this._permissionRequestsPath, '*.request')
);
this._permissionWatcher.onDidCreate(async (uri) => {
// Only handle file scheme URIs, ignore vscode-userdata scheme
if (uri.scheme === 'file') {
await this._handlePermissionRequest(uri);
}
});
this._disposables.push(this._permissionWatcher);
} catch (error: any) {
console.error('Failed to initialize permissions:', error.message);
}
}
private async _handlePermissionRequest(requestUri: vscode.Uri): Promise<void> {
try {
// Read the request file
const content = await vscode.workspace.fs.readFile(requestUri);
const request = JSON.parse(new TextDecoder().decode(content));
// Show permission dialog
const approved = await this._showPermissionDialog(request);
// Write response file
const responseFile = requestUri.fsPath.replace('.request', '.response');
const response = {
id: request.id,
approved: approved,
timestamp: new Date().toISOString()
};
const responseContent = new TextEncoder().encode(JSON.stringify(response));
await vscode.workspace.fs.writeFile(vscode.Uri.file(responseFile), responseContent);
// Clean up request file
await vscode.workspace.fs.delete(requestUri);
} catch (error: any) {
console.error('Failed to handle permission request:', error.message);
}
}
private async _showPermissionDialog(request: any): Promise<boolean> {
const toolName = request.tool || 'Unknown Tool';
// Generate pattern for Bash commands
let pattern = undefined;
if (toolName === 'Bash' && request.input?.command) {
pattern = this.getCommandPattern(request.input.command);
}
// Send permission request to the UI
this._sendAndSaveMessage({
type: 'permissionRequest',
data: {
id: request.id,
tool: toolName,
input: request.input,
pattern: pattern
}
});
// 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, alwaysAllow?: boolean): void {
if (this._pendingPermissionResolvers && this._pendingPermissionResolvers.has(id)) {
const resolver = this._pendingPermissionResolvers.get(id);
if (resolver) {
resolver(approved);
this._pendingPermissionResolvers.delete(id);
// Handle always allow setting
if (alwaysAllow && approved) {
void this._saveAlwaysAllowPermission(id);
}
}
}
}
private async _saveAlwaysAllowPermission(requestId: string): Promise<void> {
try {
// Read the original request to get tool name and input
const storagePath = this._context.storageUri?.fsPath;
if (!storagePath) return;
const requestFileUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', `${requestId}.request`));
let requestContent: Uint8Array;
try {
requestContent = await vscode.workspace.fs.readFile(requestFileUri);
} catch {
return; // Request file doesn't exist
}
const request = JSON.parse(new TextDecoder().decode(requestContent));
// Load existing workspace permissions
const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json'));
const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permissions', '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 yet, use default permissions
return false; // No permissions file
}
// Add the new permission
const toolName = request.tool;
if (toolName === 'Bash' && request.input?.command) {
// For Bash, store the command pattern
const toolPermission = permissions.alwaysAllow?.[toolName];
if (toolPermission === true) {
// Tool is fully approved (all commands/inputs)
return true;
}
if (Array.isArray(toolPermission) && toolName === 'Bash' && input.command) {
// Check if the command matches any approved pattern
const command = (input.command as string).trim();
for (const pattern of toolPermission) {
if (this._matchesPattern(command, pattern)) {
return true;
}
}
}
return false;
} catch (error) {
console.error('Error checking pre-approved permissions:', error);
return false;
}
}
/**
* Check if a command matches a permission pattern (supports * wildcard)
*/
private _matchesPattern(command: string, pattern: string): boolean {
if (pattern === command) return true;
// Handle wildcard patterns like "npm install *"
if (pattern.endsWith(' *')) {
const prefix = pattern.slice(0, -1); // Remove the *
return command.startsWith(prefix);
}
// Handle exact match patterns
return false;
}
/**
* Handle control_request messages from Claude CLI via stdio
* This is the new permission flow that replaces the MCP file-based approach
*/
private async _handleControlRequest(controlRequest: any, claudeProcess: cp.ChildProcess): Promise<void> {
const request = controlRequest.request;
const requestId = controlRequest.request_id;
// Only handle can_use_tool requests (permission requests)
if (request?.subtype !== 'can_use_tool') {
console.log('Ignoring non-permission control request:', request?.subtype);
return;
}
const toolName = request.tool_name || 'Unknown Tool';
const input = request.input || {};
const suggestions = request.permission_suggestions;
const toolUseId = request.tool_use_id;
console.log(`Permission request for tool: ${toolName}, requestId: ${requestId}`);
// Check if this tool is pre-approved
const isPreApproved = await this._isToolPreApproved(toolName, input);
if (isPreApproved) {
console.log(`Tool ${toolName} is pre-approved, auto-allowing`);
// Auto-approve without showing UI
this._sendPermissionResponse(requestId, true, {
requestId,
toolName,
input,
suggestions,
toolUseId
}, false);
return;
}
// Store the request data so we can respond later
this._pendingPermissionRequests.set(requestId, {
requestId,
toolName,
input,
suggestions,
toolUseId
});
// Generate pattern for Bash commands (for display purposes)
let pattern: string | undefined;
if (toolName === 'Bash' && input.command) {
pattern = this.getCommandPattern(input.command as string);
}
// Send permission request to the UI with pending status
this._sendAndSaveMessage({
type: 'permissionRequest',
data: {
id: requestId,
tool: toolName,
input: input,
pattern: pattern,
suggestions: suggestions,
decisionReason: request.decision_reason,
blockedPath: request.blocked_path,
status: 'pending'
}
});
}
/**
* Send permission response back to Claude CLI via stdin
*/
private _sendPermissionResponse(
requestId: string,
approved: boolean,
pendingRequest: {
requestId: string;
toolName: string;
input: Record<string, unknown>;
suggestions?: any[];
toolUseId: string;
},
alwaysAllow?: boolean
): void {
if (!this._currentClaudeProcess?.stdin || this._currentClaudeProcess.stdin.destroyed) {
console.error('Cannot send permission response: stdin not available');
return;
}
let response: any;
if (approved) {
response = {
type: 'control_response',
response: {
subtype: 'success',
request_id: requestId,
response: {
behavior: 'allow',
updatedInput: pendingRequest.input,
// Pass back suggestions if user chose "always allow"
updatedPermissions: alwaysAllow ? pendingRequest.suggestions : undefined,
toolUseID: pendingRequest.toolUseId
}
}
};
} else {
response = {
type: 'control_response',
response: {
subtype: 'success',
request_id: requestId,
response: {
behavior: 'deny',
message: 'User denied permission',
interrupt: true,
toolUseID: pendingRequest.toolUseId
}
}
};
}
const responseJson = JSON.stringify(response) + '\n';
console.log('Sending permission response:', responseJson);
console.log('Always allow:', alwaysAllow, 'Suggestions included:', !!pendingRequest.suggestions);
this._currentClaudeProcess.stdin.write(responseJson);
}
private async _initializePermissions(): Promise<void> {
// No longer needed - permissions are handled via stdio
// This method is kept for compatibility but does nothing
}
/**
* Handle permission response from webview UI
* Sends control_response back to Claude CLI via stdin
*/
private _handlePermissionResponse(id: string, approved: boolean, alwaysAllow?: boolean): void {
const pendingRequest = this._pendingPermissionRequests.get(id);
if (!pendingRequest) {
console.error('No pending permission request found for id:', id);
return;
}
// Remove from pending requests
this._pendingPermissionRequests.delete(id);
// Send the response to Claude via stdin
this._sendPermissionResponse(id, approved, pendingRequest, alwaysAllow);
// Update the permission request status in UI
this._postMessage({
type: 'updatePermissionStatus',
data: {
id: id,
status: approved ? 'approved' : 'denied'
}
});
// Also save to local permissions.json for UI display purposes
if (alwaysAllow && approved) {
void this._saveLocalPermission(pendingRequest.toolName, pendingRequest.input);
}
}
/**
* Cancel all pending permission requests (called when process ends)
*/
private _cancelPendingPermissionRequests(): void {
for (const [id, _request] of this._pendingPermissionRequests) {
this._postMessage({
type: 'updatePermissionStatus',
data: {
id: id,
status: 'cancelled'
}
});
}
this._pendingPermissionRequests.clear();
}
/**
* Save permission to local storage for UI display in settings
* Note: The actual "always allow" is handled by Claude via updatedPermissions
*/
private async _saveLocalPermission(toolName: string, input: Record<string, unknown>): Promise<void> {
try {
const storagePath = this._context.storageUri?.fsPath;
if (!storagePath) return;
// Ensure permissions directory exists
const permissionsDir = path.join(storagePath, 'permissions');
try {
await vscode.workspace.fs.stat(vscode.Uri.file(permissionsDir));
} catch {
await vscode.workspace.fs.createDirectory(vscode.Uri.file(permissionsDir));
}
// Load existing permissions
const permissionsUri = vscode.Uri.file(path.join(permissionsDir, '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 yet
}
// Add the permission
if (toolName === 'Bash' && input.command) {
if (!permissions.alwaysAllow[toolName]) {
permissions.alwaysAllow[toolName] = [];
}
if (Array.isArray(permissions.alwaysAllow[toolName])) {
const command = request.input.command.trim();
const pattern = this.getCommandPattern(command);
const pattern = this.getCommandPattern(input.command as string);
if (!permissions.alwaysAllow[toolName].includes(pattern)) {
permissions.alwaysAllow[toolName].push(pattern);
}
}
} else {
// For other tools, allow all instances
permissions.alwaysAllow[toolName] = true;
}
// Ensure permissions directory exists
const permissionsDir = vscode.Uri.file(path.dirname(permissionsUri.fsPath));
try {
await vscode.workspace.fs.stat(permissionsDir);
} catch {
await vscode.workspace.fs.createDirectory(permissionsDir);
}
// Save the permissions
// Save permissions
const permissionsContent = new TextEncoder().encode(JSON.stringify(permissions, null, 2));
await vscode.workspace.fs.writeFile(permissionsUri, permissionsContent);
console.log(`Saved always-allow permission for ${toolName}`);
console.log(`Saved local permission for ${toolName}`);
} catch (error) {
console.error('Error saving always-allow permission:', error);
console.error('Error saving local permission:', error);
}
}
@@ -1592,7 +1741,7 @@ class ClaudeChatProvider {
return;
}
const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json'));
const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permissions', 'permissions.json'));
let permissions: any = { alwaysAllow: {} };
try {
@@ -1620,7 +1769,7 @@ class ClaudeChatProvider {
const storagePath = this._context.storageUri?.fsPath;
if (!storagePath) return;
const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json'));
const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permissions', 'permissions.json'));
let permissions: any = { alwaysAllow: {} };
try {
@@ -1666,7 +1815,7 @@ class ClaudeChatProvider {
const storagePath = this._context.storageUri?.fsPath;
if (!storagePath) return;
const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json'));
const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permissions', 'permissions.json'));
let permissions: any = { alwaysAllow: {} };
try {
@@ -2293,10 +2442,18 @@ class ClaudeChatProvider {
}
// For tool messages, include the message index so Open Diff buttons work
const messageData = (message.messageType === 'toolUse' || message.messageType === 'toolResult')
let messageData = (message.messageType === 'toolUse' || message.messageType === 'toolResult')
? { ...message.data, messageIndex: i }
: message.data;
// For permission requests loaded from history, mark pending ones as expired
// ONLY if there's no active Claude process (i.e., VS Code was restarted)
if (message.messageType === 'permissionRequest' &&
message.data?.status === 'pending' &&
!this._currentClaudeProcess) {
messageData = { ...message.data, status: 'expired' };
}
this._postMessage({
type: message.messageType,
data: messageData
@@ -2328,6 +2485,15 @@ class ClaudeChatProvider {
data: { isProcessing: this._isProcessing, requestStartTime }
});
}
// Mark any pending permission requests as expired ONLY if there's no active Claude process
// (i.e., VS Code was restarted, not just the panel was closed/reopened)
if (!this._currentClaudeProcess) {
this._postMessage({
type: 'expirePendingPermissions'
});
}
// Send ready message after conversation is loaded
this._sendReadyMessage();
}, 50);

View File

@@ -2265,6 +2265,12 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
case 'permissionRequest':
addPermissionRequestMessage(message.data);
break;
case 'updatePermissionStatus':
updatePermissionStatus(message.data.id, message.data.status);
break;
case 'expirePendingPermissions':
expireAllPendingPermissions();
break;
case 'mcpServers':
displayMCPServers(message.data);
break;
@@ -2289,8 +2295,11 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
const messageDiv = document.createElement('div');
messageDiv.className = 'message permission-request';
messageDiv.id = \`permission-\${data.id}\`;
messageDiv.dataset.status = data.status || 'pending';
const toolName = data.tool || 'Unknown Tool';
const status = data.status || 'pending';
// Create always allow button text with command styling for Bash
let alwaysAllowText = \`Always allow \${toolName}\`;
@@ -2304,7 +2313,10 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
alwaysAllowTooltip = displayPattern.length > 30 ? \`title="\${displayPattern}"\` : '';
}
messageDiv.innerHTML = \`
// Show different content based on status
let contentHtml = '';
if (status === 'pending') {
contentHtml = \`
<div class="permission-header">
<span class="icon">🔐</span>
<span>Permission Required</span>
@@ -2330,11 +2342,93 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
</div>
</div>
\`;
} else if (status === 'approved') {
contentHtml = \`
<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-decision allowed">✅ You allowed this</div>
</div>
\`;
messageDiv.classList.add('permission-decided', 'allowed');
} else if (status === 'denied') {
contentHtml = \`
<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-decision denied">❌ You denied this</div>
</div>
\`;
messageDiv.classList.add('permission-decided', 'denied');
} else if (status === 'cancelled' || status === 'expired') {
contentHtml = \`
<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-decision expired">⏱️ This request expired</div>
</div>
\`;
messageDiv.classList.add('permission-decided', 'expired');
}
messageDiv.innerHTML = contentHtml;
messagesDiv.appendChild(messageDiv);
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
}
function updatePermissionStatus(id, status) {
const permissionMsg = document.getElementById(\`permission-\${id}\`);
if (!permissionMsg) return;
permissionMsg.dataset.status = status;
const permissionContent = permissionMsg.querySelector('.permission-content');
const buttons = permissionMsg.querySelector('.permission-buttons');
const menuDiv = permissionMsg.querySelector('.permission-menu');
// Hide buttons and menu if present
if (buttons) buttons.style.display = 'none';
if (menuDiv) menuDiv.style.display = 'none';
// Remove existing decision div if any
const existingDecision = permissionContent.querySelector('.permission-decision');
if (existingDecision) existingDecision.remove();
// Add new decision div
const decisionDiv = document.createElement('div');
if (status === 'approved') {
decisionDiv.className = 'permission-decision allowed';
decisionDiv.innerHTML = '✅ You allowed this';
permissionMsg.classList.add('permission-decided', 'allowed');
} else if (status === 'denied') {
decisionDiv.className = 'permission-decision denied';
decisionDiv.innerHTML = '❌ You denied this';
permissionMsg.classList.add('permission-decided', 'denied');
} else if (status === 'cancelled' || status === 'expired') {
decisionDiv.className = 'permission-decision expired';
decisionDiv.innerHTML = '⏱️ This request expired';
permissionMsg.classList.add('permission-decided', 'expired');
}
permissionContent.appendChild(decisionDiv);
}
function expireAllPendingPermissions() {
document.querySelectorAll('.permission-request').forEach(permissionMsg => {
if (permissionMsg.dataset.status === 'pending') {
const id = permissionMsg.id.replace('permission-', '');
updatePermissionStatus(id, 'expired');
}
});
}
function respondToPermission(id, approved, alwaysAllow = false) {
// Send response back to extension
vscode.postMessage({

View File

@@ -302,6 +302,12 @@ const styles = `
border: 1px solid rgba(231, 76, 60, 0.3);
}
.permission-decision.expired {
background-color: rgba(128, 128, 128, 0.15);
color: var(--vscode-descriptionForeground);
border: 1px solid rgba(128, 128, 128, 0.3);
}
.permission-decided {
opacity: 0.7;
pointer-events: none;
@@ -321,6 +327,11 @@ const styles = `
background-color: var(--vscode-inputValidation-errorBackground);
}
.permission-decided.expired {
border-color: var(--vscode-panel-border);
background-color: rgba(128, 128, 128, 0.05);
}
/* Permissions Management */
.permissions-list {
max-height: 300px;