mirror of
https://github.com/andrepimenta/claude-code-chat.git
synced 2025-12-11 23:59:51 +00:00
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:
@@ -12,3 +12,5 @@ vsc-extension-quickstart.md
|
||||
backup
|
||||
.claude
|
||||
claude-code-chat-permissions-mcp/**
|
||||
node_modules
|
||||
mcp-permissions.js
|
||||
514
src/extension.ts
514
src/extension.ts
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user