Update chat input: hide attachments bar, move/restyle context window hint (#296390)

* Simplify chat input: hide attachments bar, move context usage widget

- Hide attachments bar by default, only show with manual attachments
- Remove Add Files button entirely
- Stop rendering implicit context pill (data still collected/sent)
- Move context usage pie widget from top-right to bottom toolbar
- Make input editor 2 lines tall by default (non-compact mode)

* fix: chat input toolbar icons use icon-foreground instead of foreground

- Move Add Context action to ChatInput menu alongside tools button
- Fix theme-2026 styles.css override that stomped toolbar codicon color
  with --vscode-foreground; now uses --vscode-icon-foreground
- Add matching icon-foreground override in chat.css for specificity safety
- Switch context usage indicator from pie chart to stroke-based ring
- Account for context usage widget width in toolbar layout calculation
- Set explicit ordering for toolbar sections (input, context usage, execute)

* fix: remove unused isAttachmentAlreadyAttached method and dead imports

* fix: derive input min height from editor options instead of hardcoded values

* fix: remove dead ChatInputAttachmentToolbar menu registration

* fix: show attach context in execute toolbar for quick chat, not input toolbar
This commit is contained in:
David Dossett
2026-02-20 10:23:22 -08:00
committed by GitHub
parent 9a3a7aaa13
commit 4eb8f169e5
6 changed files with 107 additions and 233 deletions

View File

@@ -492,10 +492,10 @@
color: var(--vscode-icon-foreground) !important;
}
/* Chat input toolbar icons should use proper foreground color, not the muted icon.foreground */
/* Chat input toolbar icons should follow icon foreground token */
.monaco-workbench .interactive-session .chat-input-toolbars .monaco-action-bar .action-item .codicon,
.monaco-workbench .interactive-session .chat-input-toolbars .action-label .codicon {
color: var(--vscode-foreground) !important;
color: var(--vscode-icon-foreground) !important;
}
/* Buttons */

View File

@@ -465,15 +465,16 @@ export class AttachContextAction extends Action2 {
super({
id: 'workbench.action.chat.attachContext',
title: localize2('workbench.action.chat.attachContext.label.2', "Add Context..."),
icon: Codicon.attach,
icon: Codicon.add,
category: CHAT_CATEGORY,
keybinding: {
when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat)),
primary: KeyMod.CtrlCmd | KeyCode.Slash,
weight: KeybindingWeight.EditorContrib
},
menu: {
menu: [{
when: ContextKeyExpr.and(
ChatContextKeys.inQuickChat.negate(),
ContextKeyExpr.or(
ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat),
ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditorInline), CTX_INLINE_CHAT_V2_ENABLED)
@@ -483,10 +484,21 @@ export class AttachContextAction extends Action2 {
ChatContextKeys.agentSupportsAttachments
)
),
id: MenuId.ChatInputAttachmentToolbar,
id: MenuId.ChatInput,
group: 'navigation',
order: 3
},
order: 101
}, {
when: ContextKeyExpr.and(
ChatContextKeys.inQuickChat,
ContextKeyExpr.or(
ChatContextKeys.lockedToCodingAgent.negate(),
ChatContextKeys.agentSupportsAttachments
)
),
id: MenuId.ChatExecute,
group: 'navigation',
order: -1
}],
});
}

View File

@@ -13,7 +13,6 @@ import * as aria from '../../../../../../base/browser/ui/aria/aria.js';
import { ButtonWithIcon } from '../../../../../../base/browser/ui/button/button.js';
import { createInstantHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js';
import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js';
import { IAction } from '../../../../../../base/common/actions.js';
import { equals as arraysEqual } from '../../../../../../base/common/arrays.js';
import { DeferredPromise, RunOnceScheduler } from '../../../../../../base/common/async.js';
@@ -32,15 +31,13 @@ import { autorun, derived, derivedOpts, IObservable, ISettableObservable, observ
import { isMacintosh } from '../../../../../../base/common/platform.js';
import { isEqual } from '../../../../../../base/common/resources.js';
import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js';
import { assertType } from '../../../../../../base/common/types.js';
import { URI } from '../../../../../../base/common/uri.js';
import { IEditorConstructionOptions } from '../../../../../../editor/browser/config/editorConfiguration.js';
import { EditorExtensionsRegistry } from '../../../../../../editor/browser/editorExtensions.js';
import { CodeEditorWidget } from '../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
import { EditorLayoutInfo, EditorOptions, IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js';
import { EditorLayoutInfo, EditorOption, EditorOptions, IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js';
import { IDimension } from '../../../../../../editor/common/core/2d/dimension.js';
import { IPosition } from '../../../../../../editor/common/core/position.js';
import { IRange, Range } from '../../../../../../editor/common/core/range.js';
import { isLocation } from '../../../../../../editor/common/languages.js';
import { ITextModel } from '../../../../../../editor/common/model.js';
import { IModelService } from '../../../../../../editor/common/services/model.js';
@@ -83,7 +80,7 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd
import { InlineChatConfigKeys } from '../../../../inlineChat/common/inlineChat.js';
import { IChatViewTitleActionContext } from '../../../common/actions/chatActions.js';
import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js';
import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js';
import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry } from '../../../common/attachments/chatVariableEntries.js';
import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js';
import { IChatFollowup, IChatQuestionCarousel, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js';
import { agentOptionId, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, isIChatSessionFileChange2, localChatSessionType } from '../../../common/chatSessionsService.js';
@@ -104,7 +101,6 @@ import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js';
import { IChatAttachmentWidgetRegistry } from '../../attachments/chatAttachmentWidgetRegistry.js';
import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../../attachments/chatAttachmentWidgets.js';
import { ChatImplicitContexts } from '../../attachments/chatImplicitContext.js';
import { ImplicitContextAttachmentWidget } from '../../attachments/implicitContextAttachment.js';
import { IChatWidget, IChatWidgetViewModelChangeEvent, ISessionTypePickerDelegate, isIChatResourceViewContext, isIChatViewViewContext, IWorkspacePickerDelegate } from '../../chat.js';
import { ChatEditingShowChangesAction, ViewAllSessionChangesAction, ViewPreviousEditsAction } from '../../chatEditing/chatEditingActions.js';
import { resizeImage } from '../../chatImageUtils.js';
@@ -133,6 +129,7 @@ import { EnhancedModelPickerActionItem } from './modelPickerActionItem2.js';
const $ = dom.$;
const INPUT_EDITOR_MAX_HEIGHT = 250;
const INPUT_EDITOR_MIN_VISIBLE_LINES = 2;
const CachedLanguageModelsKey = 'chat.cachedLanguageModels.v2';
export interface IChatInputStyles {
@@ -235,8 +232,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
private _onDidClickOverlay = this._register(new Emitter<void>());
readonly onDidClickOverlay: Event<void> = this._onDidClickOverlay.event;
private readonly _implicitContextWidget: MutableDisposable<ImplicitContextAttachmentWidget> = this._register(new MutableDisposable<ImplicitContextAttachmentWidget>());
private readonly _attachmentModel: ChatAttachmentModel;
private _widget?: IChatWidget;
public get attachmentModel(): ChatAttachmentModel {
@@ -332,8 +327,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
private executeToolbar!: MenuWorkbenchToolBar;
private inputActionsToolbar!: MenuWorkbenchToolBar;
private addFilesToolbar: MenuWorkbenchToolBar | undefined;
private addFilesButton: AddFilesButton | undefined;
get inputEditor() {
return this._inputEditor;
@@ -1944,13 +1938,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
dom.h('.chat-getting-started-tip-container@chatGettingStartedTipContainer'),
dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [
dom.h('.chat-input-container@inputContainer', [
dom.h('.chat-context-usage-container@contextUsageWidgetContainer'),
dom.h('.chat-editor-container@editorContainer'),
dom.h('.chat-input-toolbars@inputToolbars'),
dom.h('.chat-input-toolbars@inputToolbars', [
dom.h('.chat-context-usage-container@contextUsageWidgetContainer'),
]),
]),
]),
dom.h('.chat-attachments-container@attachmentsContainer', [
dom.h('.chat-attachment-toolbar@attachmentToolbar'),
dom.h('.chat-attached-context@attachedContextContainer'),
]),
dom.h('.interactive-input-followups@followupsContainer'),
@@ -1966,13 +1960,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
dom.h('.chat-getting-started-tip-container@chatGettingStartedTipContainer'),
dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [
dom.h('.chat-input-container@inputContainer', [
dom.h('.chat-context-usage-container@contextUsageWidgetContainer'),
dom.h('.chat-attachments-container@attachmentsContainer', [
dom.h('.chat-attachment-toolbar@attachmentToolbar'),
dom.h('.chat-attached-context@attachedContextContainer'),
]),
dom.h('.chat-editor-container@editorContainer'),
dom.h('.chat-input-toolbars@inputToolbars'),
dom.h('.chat-input-toolbars@inputToolbars', [
dom.h('.chat-context-usage-container@contextUsageWidgetContainer'),
]),
]),
]),
]);
@@ -1995,7 +1989,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.attachmentsContainer = elements.attachmentsContainer;
this.attachedContextContainer = elements.attachedContextContainer;
const toolbarsContainer = elements.inputToolbars;
const attachmentToolbarContainer = elements.attachmentToolbar;
this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer;
this.chatInputTodoListWidgetContainer = elements.chatInputTodoListWidgetContainer;
this.chatGettingStartedTipContainer = elements.chatGettingStartedTipContainer;
@@ -2004,7 +1997,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.chatInputWidgetsContainer = elements.chatInputWidgetsContainer;
this.contextUsageWidgetContainer = elements.contextUsageWidgetContainer;
// Context usage widget
// Context usage widget — will be positioned in the toolbar after toolbars are created
this.contextUsageWidget = this._register(this.instantiationService.createInstance(ChatContextUsageWidget));
this.contextUsageWidgetContainer.appendChild(this.contextUsageWidget.domNode);
@@ -2337,23 +2330,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.renderAttachedContext();
}));
this.addFilesToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, attachmentToolbarContainer, MenuId.ChatInputAttachmentToolbar, {
telemetrySource: this.options.menus.telemetrySource,
label: true,
menuOptions: { shouldForwardArgs: true, renderShortTitle: true },
hiddenItemStrategy: HiddenItemStrategy.NoHide,
hoverDelegate,
actionViewItemProvider: (action, options) => {
if (action.id === 'workbench.action.chat.attachContext') {
const viewItem = this.instantiationService.createInstance(AddFilesButton, this._attachmentModel, action, options);
viewItem.setShowLabel(this._attachmentModel.size === 0 && !this._implicitContextWidget.value?.hasRenderedContexts);
this.addFilesButton = viewItem;
return this.addFilesButton;
}
return undefined;
}
}));
this.addFilesToolbar.context = { widget, placeholder: localize('chatAttachFiles', 'Search for files and context to add to your request') };
this.renderAttachedContext();
const inputResizeObserver = this._register(new dom.DisposableResizeObserver(() => {
@@ -2400,15 +2376,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
}));
const attachments = [...this.attachmentModel.attachments.entries()];
const hasAttachments = Boolean(attachments.length) || Boolean(this.implicitContext?.hasValue);
dom.setVisibility(Boolean(this.options.renderInputToolbarBelowInput || hasAttachments || (this.addFilesToolbar && !this.addFilesToolbar.isEmpty())), this.attachmentsContainer);
const hasAttachments = Boolean(attachments.length);
dom.setVisibility(Boolean(this.options.renderInputToolbarBelowInput || hasAttachments), this.attachmentsContainer);
dom.setVisibility(hasAttachments, this.attachedContextContainer);
if (!attachments.length) {
this._indexOfLastAttachedContextDeletedWithKeyboard = -1;
this._indexOfLastOpenedContext = -1;
}
const isSuggestedEnabled = this.configurationService.getValue<boolean>('chat.implicitContext.suggestedContext');
for (const [index, attachment] of attachments) {
const resource = URI.isUri(attachment.value) ? attachment.value : isLocation(attachment.value) ? attachment.value.uri : undefined;
@@ -2465,54 +2440,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
}));
}
if (isSuggestedEnabled && this.implicitContext?.hasValue) {
this._implicitContextWidget.value = this.instantiationService.createInstance(ImplicitContextAttachmentWidget, () => this._widget, (targetUri: URI | undefined, targetRange: IRange | undefined, targetHandle: number | undefined) => this.isAttachmentAlreadyAttached(targetUri, targetRange, targetHandle, attachments.map(([, a]) => a)), this.implicitContext, this._contextResourceLabels, this._attachmentModel, container);
} else {
this._implicitContextWidget.clear();
}
this.addFilesButton?.setShowLabel(this._attachmentModel.size === 0 && !this._implicitContextWidget.value?.hasRenderedContexts);
this._indexOfLastOpenedContext = -1;
}
private isAttachmentAlreadyAttached(targetUri: URI | undefined, targetRange: IRange | undefined, targetHandle: number | undefined, attachments: IChatRequestVariableEntry[]): boolean {
return attachments.some((attachment) => {
let uri: URI | undefined;
let range: IRange | undefined;
let handle: number | undefined;
if (URI.isUri(attachment.value)) {
uri = attachment.value;
} else if (isLocation(attachment.value)) {
uri = attachment.value.uri;
range = attachment.value.range;
} else if (isStringVariableEntry(attachment)) {
uri = attachment.uri;
handle = attachment.handle;
}
if ((handle !== undefined && targetHandle === undefined) || (handle === undefined && targetHandle !== undefined)) {
return false;
}
if (handle !== undefined && targetHandle !== undefined && handle !== targetHandle) {
return false;
}
if (!uri || !isEqual(uri, targetUri)) {
return false;
}
// check if the exact range is already attached
if (targetRange) {
return range && Range.equalsRange(range, targetRange);
}
return true;
});
}
private handleAttachmentDeletion(e: KeyboardEvent | unknown, index: number, attachment: IChatRequestVariableEntry) {
// Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click)
if (dom.isKeyboardEvent(e)) {
@@ -2555,20 +2485,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
return;
}
// eslint-disable-next-line no-restricted-syntax
const toolbar = this.addFilesToolbar?.getElement().querySelector('.action-label');
if (!toolbar) {
return;
}
// eslint-disable-next-line no-restricted-syntax
const attachments = Array.from(this.attachedContextContainer.querySelectorAll('.chat-attached-context-attachment'));
if (!attachments.length) {
return;
}
attachments.unshift(toolbar);
const activeElement = dom.getWindow(this.attachmentsContainer).document.activeElement;
const currentIndex = attachments.findIndex(attachment => attachment === activeElement);
let newIndex = currentIndex;
@@ -3029,7 +2951,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
const initialEditorScrollWidth = this._inputEditor.getScrollWidth();
const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.inputPartHorizontalPaddingInside - data.toolbarsWidth - data.sideToolbarWidth;
const inputEditorHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight);
const lineHeight = this._inputEditor.getOption(EditorOption.lineHeight);
const { top, bottom } = this._inputEditor.getOption(EditorOption.padding);
const inputEditorMinHeight = this.options.renderStyle === 'compact' ? 0 : (lineHeight * INPUT_EDITOR_MIN_VISIBLE_LINES) + top + bottom;
const inputEditorHeight = Math.max(inputEditorMinHeight, Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight));
const newDimension = { width: newEditorWidth, height: inputEditorHeight };
if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) {
// This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler
@@ -3063,7 +2988,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
const inputToolbarWidth = this.cachedInputToolbarWidth = this.inputActionsToolbar.getItemsWidth();
const executeToolbarPadding = (this.executeToolbar.getItemsLength() - 1) * 4;
const inputToolbarPadding = this.inputActionsToolbar.getItemsLength() ? (this.inputActionsToolbar.getItemsLength() - 1) * 4 : 0;
return executeToolbarWidth + executeToolbarPadding + (this.options.renderInputToolbarBelowInput ? 0 : inputToolbarWidth + inputToolbarPadding);
const contextUsageWidth = dom.getTotalWidth(this.contextUsageWidgetContainer);
return executeToolbarWidth + executeToolbarPadding + contextUsageWidth + (this.options.renderInputToolbarBelowInput ? 0 : inputToolbarWidth + inputToolbarPadding);
};
return {
@@ -3152,37 +3078,3 @@ class ChatSessionPickersContainerActionItem extends ActionViewItem {
super.dispose();
}
}
class AddFilesButton extends ActionViewItem {
private showLabel: boolean | undefined;
constructor(context: unknown, action: IAction, options: IActionViewItemOptions) {
super(context, action, {
...options,
icon: false,
label: true,
keybindingNotRenderedWithLabel: true,
});
}
public setShowLabel(show: boolean): void {
this.showLabel = show;
this.updateLabel();
}
override render(container: HTMLElement): void {
container.classList.add('chat-attachment-button');
super.render(container);
this.updateLabel();
}
protected override updateLabel(): void {
if (!this.label) {
return;
}
assertType(this.label);
this.label.classList.toggle('has-label', this.showLabel);
const message = this.showLabel ? `$(attach) ${this.action.label}` : `$(attach)`;
dom.reset(this.label, ...renderLabelWithIcons(message));
}
}

View File

@@ -804,17 +804,16 @@ have to be updated for changes to the rules above, or to support more deeply nes
position: relative;
}
/* Context usage widget container - positioned at top right of chat input */
.interactive-session .chat-input-container .chat-context-usage-container {
position: absolute;
top: 4px;
right: 6px;
z-index: 1;
/* Context usage widget container - positioned in the bottom toolbar */
.interactive-session .chat-input-toolbars .chat-context-usage-container {
display: flex;
align-items: center;
flex-shrink: 0;
order: 1;
}
/* Hide context usage widget in compact mode (quick chat) */
.interactive-session .interactive-input-part.compact .chat-context-usage-container {
display: none;
.interactive-session .chat-input-toolbars > .chat-execute-toolbar {
order: 2;
}
.interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container,
@@ -1007,7 +1006,6 @@ have to be updated for changes to the rules above, or to support more deeply nes
flex-direction: row;
gap: 4px;
margin-top: 6px;
margin-right: 20px;
flex-wrap: wrap;
cursor: default;
}
@@ -1302,6 +1300,8 @@ have to be updated for changes to the rules above, or to support more deeply nes
.interactive-session .chat-input-toolbars {
display: flex;
align-items: center;
gap: 6px;
}
.interactive-session .chat-input-toolbars :not(.responsive.chat-input-toolbar) .actions-container:first-child {
@@ -1321,9 +1321,15 @@ have to be updated for changes to the rules above, or to support more deeply nes
}
.interactive-session .chat-input-toolbars > .chat-input-toolbar {
order: 0;
overflow: hidden;
min-width: 0px;
width: 100%;
color: var(--vscode-icon-foreground);
.monaco-action-bar .action-item .codicon {
color: var(--vscode-icon-foreground);
}
.chat-input-picker-item {
min-width: 0px;
@@ -1382,7 +1388,7 @@ have to be updated for changes to the rules above, or to support more deeply nes
padding: 3px 0px 3px 6px;
display: flex;
align-items: center;
color: var(--vscode-foreground);
color: var(--vscode-icon-foreground);
}
.monaco-workbench .interactive-session .chat-input-toolbar .chat-input-picker-item .action-label .codicon-chevron-down,
@@ -1401,6 +1407,16 @@ have to be updated for changes to the rules above, or to support more deeply nes
max-width: 100%;
}
.monaco-workbench .interactive-session .chat-input-toolbars .monaco-action-bar .action-item .codicon,
.monaco-workbench .interactive-session .chat-input-toolbars .action-label .codicon {
color: var(--vscode-icon-foreground) !important;
}
.monaco-workbench .interactive-session .chat-input-toolbars .monaco-action-bar .action-item.disabled .codicon,
.monaco-workbench .interactive-session .chat-input-toolbars .action-label.disabled .codicon {
color: var(--vscode-disabledForeground) !important;
}
.action-widget .monaco-list-row.chat-model-picker-unavailable .description a,
.action-widget .monaco-list-row.chat-model-picker-unavailable .description a:visited {
color: var(--vscode-textLink-foreground);
@@ -1593,7 +1609,6 @@ have to be updated for changes to the rules above, or to support more deeply nes
padding: 8px 0 0 0
}
.action-item.chat-attachment-button .action-label,
.interactive-session .chat-attached-context .chat-attached-context-attachment {
display: flex;
overflow: hidden;
@@ -1606,40 +1621,11 @@ have to be updated for changes to the rules above, or to support more deeply nes
width: fit-content;
}
.action-item.chat-attachment-button .action-label {
padding: 0 4px;
}
.interactive-session .interactive-list .chat-attached-context .chat-attached-context-attachment {
font-family: var(--vscode-chat-font-family, inherit);
font-size: var(--vscode-chat-font-size-body-xs);
}
.action-item.chat-attachment-button > .action-label > .codicon {
font-size: 14px;
height: auto;
}
.interactive-session .action-item.chat-attachment-button .action-label:not(.has-label) {
display: flex;
align-items: center;
justify-content: center;
padding: 2px 4px 2px 4px;
height: fit-content;
gap: 0;
border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent));
border-radius: 4px;
box-sizing: border-box;
.codicon {
width: 14px;
}
}
.action-item.chat-attachment-button .codicon {
font-size: 14px;
}
.action-item.chat-mcp {
min-width: 22px !important;
@@ -1742,11 +1728,6 @@ have to be updated for changes to the rules above, or to support more deeply nes
gap: 4px;
}
.interactive-session .chat-attachment-toolbar .actions-container {
gap: 4px;
flex-wrap: wrap;
}
.interactive-session .interactive-input-part.compact .chat-attached-context {
padding-bottom: 0px;
display: flex;

View File

@@ -27,71 +27,55 @@ import { KeyCode } from '../../../../../../base/common/keyCodes.js';
const $ = dom.$;
/**
* A reusable circular progress indicator that displays a pie chart.
* The pie fills clockwise from the top based on the percentage value.
* A reusable circular progress indicator that displays a ring.
* The ring fills clockwise from the top based on the percentage value.
*/
export class CircularProgressIndicator {
readonly domNode: SVGSVGElement;
private readonly progressPie: SVGPathElement;
private readonly progressCircle: SVGCircleElement;
private readonly circumference: number;
private static readonly CENTER_X = 18;
private static readonly CENTER_Y = 18;
private static readonly RADIUS = 16;
private static readonly RADIUS = 14;
constructor() {
const r = CircularProgressIndicator.RADIUS;
this.circumference = 2 * Math.PI * r;
this.domNode = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.domNode.setAttribute('viewBox', '0 0 36 36');
this.domNode.classList.add('circular-progress');
// Background circle (outline only)
// Background circle
const bgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
bgCircle.setAttribute('cx', String(CircularProgressIndicator.CENTER_X));
bgCircle.setAttribute('cy', String(CircularProgressIndicator.CENTER_Y));
bgCircle.setAttribute('r', String(CircularProgressIndicator.RADIUS));
bgCircle.setAttribute('r', String(r));
bgCircle.classList.add('progress-bg');
this.domNode.appendChild(bgCircle);
// Progress pie (filled arc)
this.progressPie = document.createElementNS('http://www.w3.org/2000/svg', 'path');
this.progressPie.classList.add('progress-pie');
this.domNode.appendChild(this.progressPie);
// Progress arc (stroke-based ring)
this.progressCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
this.progressCircle.setAttribute('cx', String(CircularProgressIndicator.CENTER_X));
this.progressCircle.setAttribute('cy', String(CircularProgressIndicator.CENTER_Y));
this.progressCircle.setAttribute('r', String(r));
this.progressCircle.classList.add('progress-arc');
this.progressCircle.setAttribute('stroke-dasharray', String(this.circumference));
this.progressCircle.setAttribute('stroke-dashoffset', String(this.circumference));
this.domNode.appendChild(this.progressCircle);
}
/**
* Updates the pie chart to display the given percentage (0-100).
* @param percentage The percentage of the pie to fill (clamped to 0-100)
* Updates the ring to display the given percentage (0-100).
* @param percentage The percentage of the ring to fill (clamped to 0-100)
*/
setProgress(percentage: number): void {
const cx = CircularProgressIndicator.CENTER_X;
const cy = CircularProgressIndicator.CENTER_Y;
const r = CircularProgressIndicator.RADIUS;
if (percentage >= 100) {
// Full circle - use a circle element's path equivalent
this.progressPie.setAttribute('d', `M ${cx} ${cy - r} A ${r} ${r} 0 1 1 ${cx - 0.001} ${cy - r} Z`);
} else if (percentage <= 0) {
// Empty - no path
this.progressPie.setAttribute('d', '');
} else {
// Calculate the arc endpoint
const angle = (percentage / 100) * 360;
const radians = (angle - 90) * (Math.PI / 180); // Start from top (-90 degrees)
const x = cx + r * Math.cos(radians);
const y = cy + r * Math.sin(radians);
const largeArcFlag = angle > 180 ? 1 : 0;
// Create pie slice path: move to center, line to top, arc to endpoint, close
const d = [
`M ${cx} ${cy}`, // Move to center
`L ${cx} ${cy - r}`, // Line to top
`A ${r} ${r} 0 ${largeArcFlag} 1 ${x} ${y}`, // Arc to endpoint
'Z' // Close path back to center
].join(' ');
this.progressPie.setAttribute('d', d);
}
const clamped = Math.max(0, Math.min(100, percentage));
const offset = this.circumference - (clamped / 100) * this.circumference;
this.progressCircle.setAttribute('stroke-dashoffset', String(offset));
}
}

View File

@@ -8,6 +8,7 @@
align-items: center;
justify-content: center;
height: 22px;
width: 22px;
flex-shrink: 0;
cursor: pointer;
padding: 3px;
@@ -19,8 +20,8 @@
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
width: 14px;
height: 14px;
flex-shrink: 0;
}
@@ -46,22 +47,26 @@
}
.chat-context-usage-widget .progress-bg {
fill: transparent;
stroke: var(--vscode-icon-foreground);
stroke-width: 2;
opacity: 0.4;
fill: none;
stroke: var(--vscode-disabledForeground);
stroke-width: 4;
opacity: 0.5;
}
.chat-context-usage-widget .progress-pie {
fill: var(--vscode-icon-foreground);
opacity: 0.8;
.chat-context-usage-widget .progress-arc {
fill: none;
stroke: var(--vscode-descriptionForeground);
stroke-width: 4;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: center;
pointer-events: none;
}
.chat-context-usage-widget.warning .progress-pie {
fill: var(--vscode-editorWarning-foreground);
.chat-context-usage-widget.warning .progress-arc {
stroke: var(--vscode-editorWarning-foreground);
}
.chat-context-usage-widget.error .progress-pie {
fill: var(--vscode-editorError-foreground);
.chat-context-usage-widget.error .progress-arc {
stroke: var(--vscode-editorError-foreground);
}