Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ class ClaudeChatProvider {
this._createImageFile(message.imageData, message.imageType);
return;
case 'permissionResponse':
this._handlePermissionResponse(message.id, message.approved, message.alwaysAllow);
this._handlePermissionResponse(message.id, message.approved, message.alwaysAllow, message.scope);
return;
case 'getPermissions':
this._sendPermissions();
Expand Down Expand Up @@ -1500,6 +1500,7 @@ class ClaudeChatProvider {
const toolUseId = request.tool_use_id;

console.log(`Permission request for tool: ${toolName}, requestId: ${requestId}`);
console.log('Permission suggestions:', JSON.stringify(suggestions));

// Check if this tool is pre-approved
const isPreApproved = await this._isToolPreApproved(toolName, input);
Expand Down Expand Up @@ -1532,6 +1533,20 @@ class ClaudeChatProvider {
pattern = this.getCommandPattern(input.command as string);
}

// Compute scope availability from suggestions
let hasProjectScope = false;
let hasUserScope = false;
if (Array.isArray(suggestions)) {
for (const suggestion of suggestions) {
if (suggestion.destination === 'projectSettings' || suggestion.destination === 'localSettings') {
hasProjectScope = true;
}
if (suggestion.destination === 'userSettings') {
hasUserScope = true;
}
}
}

// Send permission request to the UI with pending status
this._sendAndSaveMessage({
type: 'permissionRequest',
Expand All @@ -1543,7 +1558,9 @@ class ClaudeChatProvider {
suggestions: suggestions,
decisionReason: request.decision_reason,
blockedPath: request.blocked_path,
status: 'pending'
status: 'pending',
hasProjectScope: hasProjectScope,
hasUserScope: hasUserScope
}
});
}
Expand All @@ -1561,13 +1578,33 @@ class ClaudeChatProvider {
suggestions?: any[];
toolUseId: string;
},
alwaysAllow?: boolean
alwaysAllow?: boolean,
scope?: string
): void {
if (!this._currentClaudeProcess?.stdin || this._currentClaudeProcess.stdin.destroyed) {
console.error('Cannot send permission response: stdin not available');
return;
}

// Filter suggestions by scope if specified
let filteredSuggestions = pendingRequest.suggestions;
if (alwaysAllow && Array.isArray(pendingRequest.suggestions) && scope) {
let scopeFiltered: any[];
if (scope === 'project') {
scopeFiltered = pendingRequest.suggestions.filter(
(s: any) => s.destination === 'projectSettings' || s.destination === 'localSettings'
);
} else if (scope === 'allProjects') {
scopeFiltered = pendingRequest.suggestions.filter(
(s: any) => s.destination === 'userSettings'
);
} else {
scopeFiltered = pendingRequest.suggestions;
}
// Fall back to all suggestions if filtering yields empty array
filteredSuggestions = scopeFiltered.length > 0 ? scopeFiltered : pendingRequest.suggestions;
}

let response: any;
if (approved) {
response = {
Expand All @@ -1578,8 +1615,8 @@ class ClaudeChatProvider {
response: {
behavior: 'allow',
updatedInput: pendingRequest.input,
// Pass back suggestions if user chose "always allow"
updatedPermissions: alwaysAllow ? pendingRequest.suggestions : undefined,
// Pass back filtered suggestions if user chose "always allow"
updatedPermissions: alwaysAllow ? filteredSuggestions : undefined,
toolUseID: pendingRequest.toolUseId
}
}
Expand Down Expand Up @@ -1615,7 +1652,7 @@ class ClaudeChatProvider {
* Handle permission response from webview UI
* Sends control_response back to Claude CLI via stdin
*/
private _handlePermissionResponse(id: string, approved: boolean, alwaysAllow?: boolean): void {
private _handlePermissionResponse(id: string, approved: boolean, alwaysAllow?: boolean, scope?: string): void {
const pendingRequest = this._pendingPermissionRequests.get(id);
if (!pendingRequest) {
console.error('No pending permission request found for id:', id);
Expand All @@ -1626,7 +1663,7 @@ class ClaudeChatProvider {
this._pendingPermissionRequests.delete(id);

// Send the response to Claude via stdin
this._sendPermissionResponse(id, approved, pendingRequest, alwaysAllow);
this._sendPermissionResponse(id, approved, pendingRequest, alwaysAllow, scope);

// Update the permission request status in UI
this._postMessage({
Expand Down
72 changes: 56 additions & 16 deletions src/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
let thinkingModeEnabled = false;
let lastPendingEditIndex = -1; // Track the last Edit/MultiEdit/Write toolUse without result
let lastPendingEditData = null; // Store diff data for the pending edit { filePath, oldContent, newContent }
let permissionScopes = {}; // Track selected scope per permission request ID

// Open diff using stored data (no file read needed)
function openDiffEditor() {
Expand Down Expand Up @@ -2402,16 +2403,28 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
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}\`;
// Create always allow label - command pattern for Bash, tool name otherwise
let alwaysAllowLabel = toolName;
let alwaysAllowTooltip = '';
if (toolName === 'Bash' && data.pattern) {
const pattern = data.pattern;
// Remove the asterisk for display - show "npm i" instead of "npm i *"
const displayPattern = pattern.replace(' *', '');
const truncatedPattern = displayPattern.length > 30 ? displayPattern.substring(0, 30) + '...' : displayPattern;
alwaysAllowText = \`Always allow <code>\${truncatedPattern}</code>\`;
alwaysAllowTooltip = displayPattern.length > 30 ? \`title="\${displayPattern}"\` : '';
alwaysAllowLabel = '<code>' + truncatedPattern + '</code>';
alwaysAllowTooltip = displayPattern.length > 30 ? 'title="' + displayPattern + '"' : '';
}

// Show scope toggle only when CLI provides suggestions with scope info
permissionScopes[data.id] = 'project';

let scopeHtml = '';
if (data.hasProjectScope && data.hasUserScope) {
scopeHtml = ' for <span class="scope-toggle" onclick="togglePermissionScope(\\'' + data.id + '\\', event)" title="Click to toggle between this project and all projects">this project</span><span class="scope-suffix" id="scopeSuffix-' + data.id + '"> (just you)</span>';
} else if (data.hasProjectScope) {
scopeHtml = ' for this project (just you)';
} else if (data.hasUserScope) {
scopeHtml = ' for all projects';
permissionScopes[data.id] = 'allProjects';
}

// Show different content based on status
Expand All @@ -2437,9 +2450,9 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
<div class="permission-content">
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
<div class="permission-buttons">
<button class="btn deny" onclick="respondToPermission('\${data.id}', false)">Deny</button>
<button class="btn always-allow" onclick="respondToPermission('\${data.id}', true, true)" \${alwaysAllowTooltip}>\${alwaysAllowText}</button>
<button class="btn allow" onclick="respondToPermission('\${data.id}', true)">Allow</button>
<button class="btn deny" onclick="respondToPermission('\${data.id}', false)">No</button>
<button class="btn always-allow" onclick="respondToPermission('\${data.id}', true, true)" \${alwaysAllowTooltip}>Yes, allow \${alwaysAllowLabel}\${scopeHtml}</button>
<button class="btn allow" onclick="respondToPermission('\${data.id}', true)">Yes</button>
</div>
</div>
\`;
Expand Down Expand Up @@ -2531,39 +2544,66 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
}

function respondToPermission(id, approved, alwaysAllow = false) {
const scope = alwaysAllow ? (permissionScopes[id] || 'project') : undefined;

// Send response back to extension
vscode.postMessage({
type: 'permissionResponse',
id: id,
approved: approved,
alwaysAllow: alwaysAllow
alwaysAllow: alwaysAllow,
scope: scope
});

// 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');
let decision = approved ? 'You allowed this' : 'You denied this';

if (alwaysAllow && approved) {
decision = 'You allowed this and set it to always allow';
const scopeLabel = scope === 'allProjects' ? 'all projects' : 'this project';
decision = 'You allowed this for ' + scopeLabel;
}

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);
}

// Clean up scope tracking
delete permissionScopes[id];
}

function togglePermissionScope(permissionId, event) {
event.stopPropagation();
event.preventDefault();

const currentScope = permissionScopes[permissionId] || 'project';
const newScope = currentScope === 'project' ? 'allProjects' : 'project';
permissionScopes[permissionId] = newScope;

const toggleEl = event.target;
const suffixEl = document.getElementById('scopeSuffix-' + permissionId);

if (newScope === 'allProjects') {
toggleEl.textContent = 'all projects';
if (suffixEl) suffixEl.textContent = '';
} else {
toggleEl.textContent = 'this project';
if (suffixEl) suffixEl.textContent = ' (just you)';
}
}

function togglePermissionMenu(permissionId) {
Expand Down
24 changes: 23 additions & 1 deletion src/ui-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,11 @@ const styles = `
font-weight: 500;
min-width: auto;
padding: 6px 14px;
height: 28px;
white-space: normal;
text-align: left;
line-height: 1.3;
height: auto;
min-height: 28px;
}

.permission-buttons .btn.always-allow:hover {
Expand All @@ -287,6 +291,24 @@ const styles = `
vertical-align: baseline;
}

.scope-toggle {
text-decoration: underline;
cursor: pointer;
color: var(--vscode-textLink-foreground);
font-weight: 600;
transition: color 0.2s ease;
}

.scope-toggle:hover {
color: var(--vscode-textLink-activeForeground);
}

.scope-suffix {
font-weight: 400;
opacity: 0.8;
font-size: 11px;
}

.permission-decision {
font-size: 13px;
font-weight: 600;
Expand Down