fix: correct terminal sandbox icon in thinking dropdown (#304327)

When sandbox is enabled, the tool-level icon was set to terminalSecure (lock)
at registration time, which leaked into every rendering path during streaming
before we knew if a specific command was actually sandboxed. This caused
unsandboxed commands (requestUnsandboxedExecution=true) to show the lock icon.

Fix:
- Set toolData.icon to always be Codicon.terminal (no lock). The per-command
  isSandboxWrapped flag in toolSpecificData is the authoritative source.
- In the existing autorun in trackToolMetadata, update the icon element when
  the tool transitions out of streaming and toolSpecificData becomes available.
- Store icon elements in toolIconsByCallId map for direct access.

Fixes #303505
This commit is contained in:
Alexandru Dima
2026-03-24 11:27:57 +01:00
committed by GitHub
parent 0f66161e36
commit 5d82d8ab1e
2 changed files with 18 additions and 2 deletions

View File

@@ -7,7 +7,7 @@ import { $, clearNode, getWindow, hide, scheduleAtNextAnimationFrame } from '../
import { alert } from '../../../../../../base/browser/ui/aria/aria.js';
import { DomScrollableElement } from '../../../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js';
import { IChatMarkdownContent, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js';
import { IChatMarkdownContent, IChatTerminalToolInvocationData, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js';
import { IChatContentPartRenderContext, IChatContentPart } from './chatContentParts.js';
import { IChatRendererContent } from '../../../common/model/chatViewModel.js';
import { ChatConfiguration, ThinkingDisplayMode } from '../../../common/constants.js';
@@ -228,6 +228,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
private workingSpinnerLabel: HTMLElement | undefined;
private availableMessagesByCategory = new Map<WorkingMessageCategory, string[]>();
private readonly toolWrappersByCallId = new Map<string, HTMLElement>();
private readonly toolIconsByCallId = new Map<string, HTMLElement>();
private readonly toolLabelsByCallId = new Map<string, string>();
private readonly toolDisposables = this._register(new DisposableMap<string, DisposableStore>());
private readonly ownedToolParts = new Map<string, IDisposable>();
@@ -1224,6 +1225,7 @@ ${this.hookCount > 0 ? `EXAMPLES WITH BLOCKED CONTENT (from hooks):
const wrapper = this.toolWrappersByCallId.get(toolCallId);
if (wrapper) {
this.toolWrappersByCallId.delete(toolCallId);
this.toolIconsByCallId.delete(toolCallId);
}
this.appendedItemCount = Math.max(0, this.appendedItemCount - 1);
@@ -1342,6 +1344,7 @@ ${this.hookCount > 0 ? `EXAMPLES WITH BLOCKED CONTENT (from hooks):
if (wrapper) {
wrapper.remove();
this.toolWrappersByCallId.delete(toolCallId);
this.toolIconsByCallId.delete(toolCallId);
}
// make sure to remove any lazy item as well
@@ -1470,6 +1473,18 @@ ${this.hookCount > 0 ? `EXAMPLES WITH BLOCKED CONTENT (from hooks):
// queue item to be removed if it was streaming and presentation is hidden
if (isStreaming && currentState.type !== IChatToolInvocation.StateKind.Streaming) {
isStreaming = false;
// Update terminal tool icon based on sandbox wrapping state
const termData = toolInvocationOrMarkdown.toolSpecificData as IChatTerminalToolInvocationData | undefined;
if (termData?.kind === 'terminal') {
const iconEl = this.toolIconsByCallId.get(toolCallId);
if (iconEl) {
const newIcon = termData.commandLine?.isSandboxWrapped ? Codicon.terminalSecure : Codicon.terminal;
iconEl.className = 'chat-thinking-icon';
iconEl.classList.add(...ThemeIcon.asClassNameArray(newIcon));
}
}
if (toolInvocationOrMarkdown.presentation === 'hidden') {
this.pendingRemovals.push({ toolCallId: toolInvocationOrMarkdown.toolCallId, toolLabel: currentToolLabel });
this.schedulePendingRemovalsFlush();
@@ -1628,6 +1643,7 @@ ${this.hookCount > 0 ? `EXAMPLES WITH BLOCKED CONTENT (from hooks):
const isToolInvocation = toolInvocationOrMarkdown && (toolInvocationOrMarkdown.kind === 'toolInvocation' || toolInvocationOrMarkdown.kind === 'toolInvocationSerialized');
if (isToolInvocation && toolInvocationOrMarkdown.toolCallId) {
this.toolWrappersByCallId.set(toolInvocationOrMarkdown.toolCallId, itemWrapper);
this.toolIconsByCallId.set(toolInvocationOrMarkdown.toolCallId, iconElement);
}
this.appendToWrapper(itemWrapper);

View File

@@ -274,7 +274,7 @@ export async function createRunInTerminalToolData(
modelDescription,
userDescription: localize('runInTerminalTool.userDescription', 'Run commands in the terminal'),
source: ToolDataSource.Internal,
icon: isSandboxEnabled ? Codicon.terminalSecure : Codicon.terminal,
icon: Codicon.terminal,
inputSchema: {
type: 'object',
properties: {