From 25069363c1a7466c4d07878df86795bb29bcf991 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 29 Apr 2026 16:10:26 -0700 Subject: [PATCH 1/7] update --- .../vscode-node/workspaceIndexingStatus.ts | 10 +- .../browser/chatStatus/chatStatusDashboard.ts | 353 +++++++++++------- .../browser/chatStatus/media/chatStatus.css | 66 ++++ 3 files changed, 297 insertions(+), 132 deletions(-) diff --git a/extensions/copilot/src/extension/workspaceChunkSearch/vscode-node/workspaceIndexingStatus.ts b/extensions/copilot/src/extension/workspaceChunkSearch/vscode-node/workspaceIndexingStatus.ts index cdd01f1f16dfc..c6e379593be5d 100644 --- a/extensions/copilot/src/extension/workspaceChunkSearch/vscode-node/workspaceIndexingStatus.ts +++ b/extensions/copilot/src/extension/workspaceChunkSearch/vscode-node/workspaceIndexingStatus.ts @@ -52,6 +52,7 @@ export class MockWorkspaceIndexStateReporter extends Disposable implements Works interface ChatStatusItemState { readonly primary: { readonly message: string; + readonly icon?: string; readonly busy?: boolean; }; readonly details?: { @@ -154,6 +155,7 @@ export class ChatStatusWorkspaceIndexingStatus extends Disposable { return this._writeStatusItem({ primary: { message: t('{0} repos with indexes', readyRepos.length), + icon: '$(warning)', }, details: { message: t(`[Try re-authenticating for {0} additional repos](${commandUri(reauthenticateCommandId, [inaccessibleRepo])} "${t('Try signing in again to use the codebase index')}")`, errorRepos.length), @@ -164,6 +166,7 @@ export class ChatStatusWorkspaceIndexingStatus extends Disposable { return this._writeStatusItem({ primary: { message: t('Index unavailable'), + icon: '$(error)', }, details: { message: t(`[Try re-authenticating](${commandUri(reauthenticateCommandId, [inaccessibleRepo])} "${t('Try signing in again to use the codebase index')}")`), @@ -180,6 +183,7 @@ export class ChatStatusWorkspaceIndexingStatus extends Disposable { message: state.remoteIndexState.repos.every(repo => repo.status === CodeSearchRepoStatus.NotYetIndexed) ? t('Index not yet built') : t('Index not yet built for a repo in the workspace'), + icon: '$(warning)', }, details: { message: `[${t`Build index`}](command:${buildRemoteIndexCommandId} "${t('Build Codebase Index')}")`, @@ -202,7 +206,8 @@ export class ChatStatusWorkspaceIndexingStatus extends Disposable { ) { return this._writeStatusItem({ primary: { - message: t('Index ready') + message: t('Index ready'), + icon: '$(check)', }, }); } @@ -212,6 +217,7 @@ export class ChatStatusWorkspaceIndexingStatus extends Disposable { return this._writeStatusItem({ primary: { message: t('Out of date'), + icon: '$(warning)', }, details: { message: `[${t`Update index`}](command:${buildRemoteIndexCommandId} "${t('Update Codebase Index')}")`, @@ -231,6 +237,7 @@ export class ChatStatusWorkspaceIndexingStatus extends Disposable { this._writeStatusItem({ primary: { message: t('Codebase index not available'), + icon: '$(circle-slash)', }, details: undefined }); @@ -252,6 +259,7 @@ export class ChatStatusWorkspaceIndexingStatus extends Disposable { }; this._statusItem.description = coalesce([ + values.primary.icon, values.primary.message, values.primary.busy ? spinnerCodicon : undefined, ]).join(' '); diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index 680fcbdb04eed..0c8a65d23b95e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -8,7 +8,7 @@ import { Gesture, EventType as TouchEventType } from '../../../../../base/browse import { ActionBar } from '../../../../../base/browser/ui/actionbar/actionbar.js'; import { renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; -import { Checkbox } from '../../../../../base/browser/ui/toggle/toggle.js'; +import { Checkbox, TriStateCheckbox } from '../../../../../base/browser/ui/toggle/toggle.js'; import { IAction, toAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../../base/common/actions.js'; import { CancellationToken, cancelOnDispose } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; @@ -24,9 +24,7 @@ import { IInlineCompletionsService } from '../../../../../editor/browser/service import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; -import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import * as languages from '../../../../../editor/common/languages.js'; import { localize } from '../../../../../nls.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -87,6 +85,7 @@ export interface IChatStatusDashboardOptions { export class ChatStatusDashboard extends DomWidget { private static readonly QUICK_SETTINGS_COLLAPSED_KEY = 'chatStatusDashboard.quickSettingsCollapsed'; + private static readonly CONTRIBUTED_COLLAPSED_KEY_PREFIX = 'chatStatusDashboard.contributedCollapsed.'; readonly element = $('div.chat-status-bar-entry-tooltip'); @@ -109,7 +108,6 @@ export class ChatStatusDashboard extends DomWidget { @IInlineCompletionsService private readonly inlineCompletionsService: IInlineCompletionsService, @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IQuickInputService private readonly quickInputService: IQuickInputService, @IStorageService private readonly storageService: IStorageService, ) { super(); @@ -198,10 +196,15 @@ export class ChatStatusDashboard extends DomWidget { includedContainer.appendChild($('div.description', undefined, localize('premiumIncluded', "Included with your organization's plan."))); } - // Quick Settings — collapsible region + // Next Edit Suggestions — collapsible region if (hasQuickSettingsContent) { const hasContentAbove = hasUsageSection || hasVisibleUsageContent || hasPremiumUnlimited; - this.renderQuickSettings(contributedEntries, hasContentAbove); + this.renderNextEditSuggestionsSection(hasContentAbove); + } + + // Contributed sections (e.g. Codebase Semantic Index) — each gets its own collapsible + if (contributedEntries.length > 0) { + this.renderContributedSections(contributedEntries); } // New to Chat / Signed out @@ -274,12 +277,27 @@ export class ChatStatusDashboard extends DomWidget { } } - private renderQuickSettings(contributedEntries: ChatStatusEntry[], hasContentAbove: boolean): void { + private renderNextEditSuggestionsSection(hasContentAbove: boolean): void { const nonCollapsible = !!this.options?.disableQuickSettingsCollapsible; const collapsed = !nonCollapsible && this.storageService.getBoolean(ChatStatusDashboard.QUICK_SETTINGS_COLLAPSED_KEY, StorageScope.PROFILE, true); + // Compute status based on effective enablement for the active editor's language + const activeLanguageId = this.editorService.activeTextEditorLanguageId; + const getStatusText = () => { + if (!this.canUseChat()) { + return localize('nesDisabled', "Disabled"); + } + const enabled = activeLanguageId + ? isCompletionsEnabled(this.configurationService, activeLanguageId) + : isCompletionsEnabled(this.configurationService); + return enabled + ? localize('nesEnabled', "Enabled") + : localize('nesDisabled', "Disabled"); + }; + let disclosureHeader: HTMLElement | undefined; let chevron: HTMLElement | undefined; + let statusEl: HTMLElement | undefined; if (!nonCollapsible) { disclosureHeader = this.element.appendChild($('button.collapsible-header')); if (!hasContentAbove) { @@ -290,7 +308,9 @@ export class ChatStatusDashboard extends DomWidget { chevron = disclosureHeader.appendChild($('span.collapsible-chevron')); chevron.classList.add(...ThemeIcon.asClassNameArray(collapsed ? Codicon.chevronRight : Codicon.chevronDown)); - disclosureHeader.appendChild($('span.collapsible-label', undefined, localize('quickSettingsTab', "Quick Settings"))); + disclosureHeader.appendChild($('span.collapsible-label', undefined, localize('inlineSuggestionsTab', "Inline Suggestions"))); + + statusEl = disclosureHeader.appendChild($('span.collapsible-status', undefined, getStatusText())); } const collapsibleContent = this.element.appendChild($('div.collapsible-content')); @@ -311,26 +331,74 @@ export class ChatStatusDashboard extends DomWidget { this._store.add(addDisposableListener(disclosureHeader, EventType.CLICK, () => toggle())); } + // Update status text when completions setting changes + if (statusEl) { + this._store.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(defaultChat.completionsEnablementSetting)) { + statusEl!.textContent = getStatusText(); + } + })); + } + this.renderInlineSuggestionsContent(collapsibleInner); + } - // Contributions + private renderContributedSections(contributedEntries: ChatStatusEntry[]): void { for (const item of contributedEntries) { - collapsibleInner.appendChild($('hr')); + const storageKey = ChatStatusDashboard.CONTRIBUTED_COLLAPSED_KEY_PREFIX + item.id; + const collapsed = this.storageService.getBoolean(storageKey, StorageScope.PROFILE, true); - const itemDisposables = this._store.add(new MutableDisposable()); + const headerLabel = typeof item.label === 'string' ? item.label : item.label.label; + const headerLink = typeof item.label === 'string' ? undefined : item.label.link; - let rendered = this.renderContributedChatStatusItem(item); - itemDisposables.value = rendered.disposables; - collapsibleInner.appendChild(rendered.element); + const disclosureHeader = this.element.appendChild($('button.collapsible-header')); + disclosureHeader.setAttribute('aria-expanded', String(!collapsed)); - this._store.add(this.chatStatusItemService.onDidChange(e => { - if (e.entry.id === item.id) { - const previousElement = rendered.element; + const chevron = disclosureHeader.appendChild($('span.collapsible-chevron')); + chevron.classList.add(...ThemeIcon.asClassNameArray(collapsed ? Codicon.chevronRight : Codicon.chevronDown)); + + disclosureHeader.appendChild($('span.collapsible-label', undefined, headerLabel)); + + const statusEl = disclosureHeader.appendChild($('span.collapsible-status')); + this.renderTextPlus(statusEl, item.description, this._store); + + const collapsibleContent = this.element.appendChild($('div.collapsible-content')); + const collapsibleInner = collapsibleContent.appendChild($('div.collapsible-inner')); + if (collapsed) { + collapsibleContent.classList.add('collapsed'); + } - rendered = this.renderContributedChatStatusItem(e.entry); - itemDisposables.value = rendered.disposables; + const toggle = () => { + const isCollapsed = collapsibleContent.classList.toggle('collapsed'); + disclosureHeader.setAttribute('aria-expanded', String(!isCollapsed)); + chevron.className = 'collapsible-chevron'; + chevron.classList.add(...ThemeIcon.asClassNameArray(isCollapsed ? Codicon.chevronRight : Codicon.chevronDown)); + this.storageService.store(storageKey, isCollapsed, StorageScope.PROFILE, StorageTarget.USER); + }; - previousElement.replaceWith(rendered.element); + this._store.add(addDisposableListener(disclosureHeader, EventType.CLICK, () => toggle())); + + // Description with Learn More + if (headerLink) { + const descriptionEl = collapsibleInner.appendChild($('div.section-description')); + this.renderTextPlus(descriptionEl, localize('indexDescription', "Indexes your codebase for more relevant AI results.") + ' ' + `[${localize('learnMore', "Learn More")}](${headerLink})`, this._store); + } + + // Detail content (action links like "Build index", etc.) + if (item.detail) { + const detailEl = collapsibleInner.appendChild($('div.section-detail')); + this.renderTextPlus(detailEl, item.detail, this._store); + } + + // Listen for updates to re-render status and detail + const itemDisposables = this._store.add(new MutableDisposable()); + this._store.add(this.chatStatusItemService.onDidChange(e => { + if (e.entry.id === item.id) { + // Update status in header + statusEl.textContent = ''; + const statusDisposables = new DisposableStore(); + itemDisposables.value = statusDisposables; + this.renderTextPlus(statusEl, e.entry.description, statusDisposables); } })); } @@ -412,17 +480,21 @@ export class ChatStatusDashboard extends DomWidget { modelContainer.appendChild($('span.model-text', undefined, localize('modelLabel', "Model"))); - const actionBar = modelContainer.appendChild($('div.model-action-bar')); - const toolbar = this._store.add(new ActionBar(actionBar, { hoverDelegate: nativeHoverDelegate })); - toolbar.push([toAction({ - id: 'workbench.action.selectInlineCompletionsModel', - label: currentModel.name, - tooltip: localize('selectModel', "Select Model"), - class: ThemeIcon.asClassName(Codicon.gear), - run: async () => { - await this.showModelPicker(provider); + const select = modelContainer.appendChild($('select.inline-select')) as HTMLSelectElement; + for (const model of modelInfo.models) { + const option = document.createElement('option'); + option.value = model.id; + option.textContent = model.name; + if (model.id === modelInfo.currentModelId) { + option.selected = true; + } + select.appendChild(option); + } + this._store.add(addDisposableListener(select, 'change', async () => { + if (select.value !== modelInfo.currentModelId && provider.setModelId) { + await provider.setModelId(select.value); } - })], { icon: false, label: true }); + })); } } } @@ -438,16 +510,21 @@ export class ChatStatusDashboard extends DomWidget { optionContainer.appendChild($('span.suggest-option-text', undefined, option.label)); - const actionBar = optionContainer.appendChild($('div.suggest-option-action-bar')); - const toolbar = this._store.add(new ActionBar(actionBar, { hoverDelegate: nativeHoverDelegate })); - toolbar.push([toAction({ - id: `workbench.action.selectProviderOption.${option.id}`, - label: currentValue.label, - tooltip: localize('selectOption', "Select {0}", option.label), - run: async () => { - await this.showProviderOptionPicker(provider, option); + const select = optionContainer.appendChild($('select.inline-select')) as HTMLSelectElement; + for (const value of option.values) { + const optEl = document.createElement('option'); + optEl.value = value.id; + optEl.textContent = value.label; + if (value.id === option.currentValueId) { + optEl.selected = true; } - })], { icon: false, label: true }); + select.appendChild(optEl); + } + this._store.add(addDisposableListener(select, 'change', async () => { + if (select.value !== option.currentValueId && provider.setProviderOption) { + await provider.setProviderOption(option.id, select.value); + } + })); } } } @@ -489,36 +566,6 @@ export class ChatStatusDashboard extends DomWidget { return header; } - private renderContributedChatStatusItem(item: ChatStatusEntry): { element: HTMLElement; disposables: DisposableStore } { - const disposables = new DisposableStore(); - - const itemElement = $('div.contribution'); - - const headerLabel = typeof item.label === 'string' ? item.label : item.label.label; - const headerLink = typeof item.label === 'string' ? undefined : item.label.link; - this.renderHeader(itemElement, disposables, headerLabel, headerLink ? toAction({ - id: 'workbench.action.openChatStatusItemLink', - label: localize('learnMore', "Learn More"), - tooltip: localize('learnMore', "Learn More"), - class: ThemeIcon.asClassName(Codicon.linkExternal), - run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(headerLink))), - }) : undefined); - - const itemBody = itemElement.appendChild($('div.body')); - - const description = itemBody.appendChild($('span.description')); - this.renderTextPlus(description, item.description, disposables); - - if (item.detail) { - const separator = itemBody.appendChild($('span.separator')); - separator.textContent = '\u2014'; - const detail = itemBody.appendChild($('span.detail-item')); - this.renderTextPlus(detail, item.detail, disposables); - } - - return { element: itemElement, disposables }; - } - private renderTextPlus(target: HTMLElement, text: string, store: DisposableStore): void { for (const node of parseLinkedText(text).nodes) { if (typeof node === 'string') { @@ -658,11 +705,20 @@ export class ChatStatusDashboard extends DomWidget { // --- Inline Suggestions { const globalSetting = append(settings, $('div.setting')); - this.createInlineSuggestionsSetting(globalSetting, localize('settings.codeCompletions.allFiles', "All files"), '*'); + this.createInlineSuggestionsSetting(globalSetting, localize('settings.codeCompletions.allFiles', "Ghost text suggestions"), '*'); + + const overriddenHint = globalSetting.appendChild($('span.setting-overridden')); + const updateOverriddenHint = () => { + const obj = this.configurationService.getValue>(defaultChat.completionsEnablementSetting); + const hasOverride = modeId && isObject(obj) && typeof obj[modeId] !== 'undefined'; + overriddenHint.textContent = hasOverride ? localize('settings.overridden', "(overridden)") : ''; + }; + updateOverriddenHint(); if (modeId) { const languageSetting = append(settings, $('div.setting')); - this.createInlineSuggestionsSetting(languageSetting, localize('settings.codeCompletions.language', "{0}", this.languageService.getLanguageName(modeId) ?? modeId), modeId); + const languageName = this.languageService.getLanguageName(modeId) ?? modeId; + this.createTriStateLanguageSetting(languageSetting, localize('settings.codeCompletions.language', "Ghost text suggestions for {0}", languageName), modeId, updateOverriddenHint); } } @@ -714,6 +770,101 @@ export class ChatStatusDashboard extends DomWidget { this.createSetting(container, [defaultChat.completionsEnablementSetting], label, this.getCompletionsSettingAccessor(modeId)); } + private createTriStateLanguageSetting(container: HTMLElement, label: string, modeId: string, onStateChange: () => void): void { + const settingId = defaultChat.completionsEnablementSetting; + + const getState = (): boolean | 'mixed' => { + const obj = this.configurationService.getValue>(settingId); + if (!isObject(obj) || typeof obj[modeId] === 'undefined') { + return 'mixed'; // no override — inherits from * + } + return Boolean(obj[modeId]); + }; + + const checkbox = this._store.add(new TriStateCheckbox(label, getState(), { ...defaultCheckboxStyles })); + container.appendChild(checkbox.domNode); + + const settingLabel = append(container, $('span.setting-label', undefined, label)); + this._store.add(Gesture.addTarget(settingLabel)); + + const cycleState = () => { + const current = checkbox.checked; + // Cycle: true → false → mixed → true + if (current === true) { + checkbox.checked = false; + } else if (current === false) { + checkbox.checked = 'mixed'; + } else { + checkbox.checked = true; + } + }; + + const writeState = (state: boolean | 'mixed') => { + let result = this.configurationService.getValue>(settingId); + if (!isObject(result)) { + result = Object.create(null); + } + + if (state === 'mixed') { + // Remove the language key to inherit from * + const { [modeId]: _, ...rest } = result; + this.telemetryService.publicLog2('chatStatus.settingChanged', { + settingIdentifier: settingId, + settingMode: modeId, + settingEnablement: 'enabled' // inheriting + }); + this.configurationService.updateValue(settingId, rest); + } else { + this.telemetryService.publicLog2('chatStatus.settingChanged', { + settingIdentifier: settingId, + settingMode: modeId, + settingEnablement: state ? 'enabled' : 'disabled' + }); + this.configurationService.updateValue(settingId, { ...result, [modeId]: state }); + } + onStateChange(); + }; + + // Track previous state so onChange can apply tri-state cycling + let previousState = getState(); + + const cycleAndWrite = () => { + cycleState(); + previousState = checkbox.checked; + writeState(checkbox.checked); + }; + + [EventType.CLICK, TouchEventType.Tap].forEach(eventType => { + this._store.add(addDisposableListener(settingLabel, eventType, e => { + if (checkbox?.enabled) { + EventHelper.stop(e, true); + cycleAndWrite(); + checkbox.focus(); + } + })); + }); + + this._store.add(checkbox.onChange(() => { + // The internal Toggle only cycles true↔false; revert and apply our tri-state cycle + checkbox.checked = previousState; // undo internal toggle + cycleAndWrite(); + })); + + this._store.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(settingId)) { + checkbox.checked = getState(); + previousState = checkbox.checked; + onStateChange(); + } + })); + + if (!this.canUseChat()) { + container.classList.add('disabled'); + checkbox.disable(); + checkbox.checked = false; + } + } + private getCompletionsSettingAccessor(modeId = '*'): ISettingsAccessor { const settingId = defaultChat.completionsEnablementSetting; @@ -853,64 +1004,4 @@ export class ChatStatusDashboard extends DomWidget { updateIntervalTimer(); })); } - - private async showQuickPick( - items: IQuickPickItem[], - placeHolder: string, - apply: (selectedId: string) => Promise, - ): Promise { - const selected = await this.quickInputService.pick(items, { - placeHolder, - canPickMany: false - }); - - if (selected?.id) { - await apply(selected.id); - } - - this.hoverService.hideHover(true); - } - - private async showModelPicker(provider: languages.InlineCompletionsProvider): Promise { - if (!provider.modelInfo || !provider.setModelId) { - return; - } - - const modelInfo = provider.modelInfo; - await this.showQuickPick( - modelInfo.models.map(model => ({ - id: model.id, - label: model.name, - description: model.id === modelInfo.currentModelId ? localize('currentModel.description', "Currently selected") : undefined, - picked: model.id === modelInfo.currentModelId - })), - localize('selectModelFor', "Select a model for {0}", provider.displayName || 'inline completions'), - async (id) => { - if (id !== modelInfo.currentModelId) { - await provider.setModelId!(id); - } - }, - ); - } - - private async showProviderOptionPicker(provider: languages.InlineCompletionsProvider, option: languages.IInlineCompletionProviderOption): Promise { - if (!provider.setProviderOption) { - return; - } - - await this.showQuickPick( - option.values.map(value => ({ - id: value.id, - label: value.label, - description: value.id === option.currentValueId ? localize('currentOption.description', "Currently selected") : undefined, - picked: value.id === option.currentValueId, - })), - localize('selectProviderOptionFor', "Select {0}", option.label), - async (id) => { - if (id !== option.currentValueId) { - await provider.setProviderOption!(option.id, id); - } - }, - ); - } } diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css index e4b2b58edca4b..94a23e14cb173 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css @@ -55,6 +55,35 @@ align-items: center; } +.chat-status-bar-entry-tooltip .collapsible-status { + margin-left: auto; + font-size: 12px; + font-weight: 400; + color: var(--vscode-descriptionForeground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: flex; + align-items: center; + gap: 4px; +} + +.chat-status-bar-entry-tooltip .section-description { + font-size: 12px; + line-height: 16px; + color: var(--vscode-descriptionForeground); +} + +.chat-status-bar-entry-tooltip .section-description .monaco-link { + white-space: nowrap; +} + +.chat-status-bar-entry-tooltip .section-detail { + font-size: 12px; + line-height: 16px; + color: var(--vscode-descriptionForeground); +} + .chat-status-bar-entry-tooltip .collapsible-content { display: grid; grid-template-rows: 1fr; @@ -262,6 +291,12 @@ color: var(--vscode-disabledForeground); } +.chat-status-bar-entry-tooltip .settings .setting .setting-overridden { + font-style: italic; + color: var(--vscode-descriptionForeground); + margin-left: 4px; +} + /* Model Selection */ .chat-status-bar-entry-tooltip .model-selection { @@ -283,6 +318,37 @@ color: var(--vscode-descriptionForeground); } +/* Inline Select (native dropdown) */ + +.chat-status-bar-entry-tooltip select.inline-select { + margin-left: auto; + background: transparent; + color: var(--vscode-descriptionForeground); + border: none; + font-size: inherit; + font-family: inherit; + cursor: pointer; + padding: 0 2px; + outline: none; + text-align: right; + -webkit-appearance: none; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath fill='%23888' d='M0 2l4 4 4-4z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right center; + padding-right: 14px; +} + +.chat-status-bar-entry-tooltip select.inline-select:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.chat-status-bar-entry-tooltip select.inline-select option { + background: var(--vscode-editorWidget-background); + color: var(--vscode-foreground); +} + /* Provider Options */ .chat-status-bar-entry-tooltip .suggest-option-selection { From eeff6da5e7a883770c8c98747a3266ba8d11f700 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 29 Apr 2026 16:26:20 -0700 Subject: [PATCH 2/7] PR --- .../browser/chatStatus/chatStatusDashboard.ts | 60 ++++++++++++++----- .../browser/chatStatus/media/chatStatus.css | 21 ++++++- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index 0c8a65d23b95e..808fd0e1d92fa 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -131,8 +131,7 @@ export class ChatStatusDashboard extends DomWidget { !this.options?.disableInlineSuggestionsSettings || !this.options?.disableModelSelection || !this.options?.disableProviderOptions || - !this.options?.disableCompletionsSnooze || - contributedEntries.length > 0; + !this.options?.disableCompletionsSnooze; // Title header with plan name, CTA buttons, and manage action let headerAdditionalSpendButton: Button | undefined; @@ -359,8 +358,9 @@ export class ChatStatusDashboard extends DomWidget { disclosureHeader.appendChild($('span.collapsible-label', undefined, headerLabel)); + // Use renderLabelWithIcons for header status (plain text + icons only, no links inside button) const statusEl = disclosureHeader.appendChild($('span.collapsible-status')); - this.renderTextPlus(statusEl, item.description, this._store); + statusEl.append(...renderLabelWithIcons(item.description)); const collapsibleContent = this.element.appendChild($('div.collapsible-content')); const collapsibleInner = collapsibleContent.appendChild($('div.collapsible-inner')); @@ -378,27 +378,54 @@ export class ChatStatusDashboard extends DomWidget { this._store.add(addDisposableListener(disclosureHeader, EventType.CLICK, () => toggle())); - // Description with Learn More + // Use a single disposable store for all contributed section content + const sectionDisposables = this._store.add(new MutableDisposable()); + const sectionStore = new DisposableStore(); + sectionDisposables.value = sectionStore; + + // Description with Learn More (use contributed data, not hardcoded text) + let descriptionEl: HTMLElement | undefined; if (headerLink) { - const descriptionEl = collapsibleInner.appendChild($('div.section-description')); - this.renderTextPlus(descriptionEl, localize('indexDescription', "Indexes your codebase for more relevant AI results.") + ' ' + `[${localize('learnMore', "Learn More")}](${headerLink})`, this._store); + descriptionEl = collapsibleInner.appendChild($('div.section-description')); + this.renderTextPlus(descriptionEl, `[${localize('learnMore', "Learn More")}](${headerLink})`, sectionStore); } // Detail content (action links like "Build index", etc.) + let detailEl: HTMLElement | undefined; if (item.detail) { - const detailEl = collapsibleInner.appendChild($('div.section-detail')); - this.renderTextPlus(detailEl, item.detail, this._store); + detailEl = collapsibleInner.appendChild($('div.section-detail')); + this.renderTextPlus(detailEl, item.detail, sectionStore); } // Listen for updates to re-render status and detail - const itemDisposables = this._store.add(new MutableDisposable()); this._store.add(this.chatStatusItemService.onDidChange(e => { if (e.entry.id === item.id) { - // Update status in header + // Update status in header (plain text + icons only) statusEl.textContent = ''; - const statusDisposables = new DisposableStore(); - itemDisposables.value = statusDisposables; - this.renderTextPlus(statusEl, e.entry.description, statusDisposables); + statusEl.append(...renderLabelWithIcons(e.entry.description)); + + // Re-render detail content + const newStore = new DisposableStore(); + sectionDisposables.value = newStore; + + if (detailEl) { + detailEl.textContent = ''; + if (e.entry.detail) { + this.renderTextPlus(detailEl, e.entry.detail, newStore); + } + } else if (e.entry.detail) { + detailEl = collapsibleInner.appendChild($('div.section-detail')); + this.renderTextPlus(detailEl, e.entry.detail, newStore); + } + + // Re-render Learn More link if needed + if (descriptionEl) { + const updatedLink = typeof e.entry.label === 'string' ? undefined : e.entry.label.link; + descriptionEl.textContent = ''; + if (updatedLink) { + this.renderTextPlus(descriptionEl, `[${localize('learnMore', "Learn More")}](${updatedLink})`, newStore); + } + } } })); } @@ -481,6 +508,8 @@ export class ChatStatusDashboard extends DomWidget { modelContainer.appendChild($('span.model-text', undefined, localize('modelLabel', "Model"))); const select = modelContainer.appendChild($('select.inline-select')) as HTMLSelectElement; + select.ariaLabel = localize('selectModel', "Select Model"); + modelContainer.appendChild($('span.inline-select-chevron')); for (const model of modelInfo.models) { const option = document.createElement('option'); option.value = model.id; @@ -511,6 +540,8 @@ export class ChatStatusDashboard extends DomWidget { optionContainer.appendChild($('span.suggest-option-text', undefined, option.label)); const select = optionContainer.appendChild($('select.inline-select')) as HTMLSelectElement; + select.ariaLabel = localize('selectOption', "Select {0}", option.label); + optionContainer.appendChild($('span.inline-select-chevron')); for (const value of option.values) { const optEl = document.createElement('option'); optEl.value = value.id; @@ -808,10 +839,11 @@ export class ChatStatusDashboard extends DomWidget { if (state === 'mixed') { // Remove the language key to inherit from * const { [modeId]: _, ...rest } = result; + const inheritedEnablement = typeof rest['*'] === 'boolean' ? (rest['*'] ? 'enabled' : 'disabled') : 'enabled'; this.telemetryService.publicLog2('chatStatus.settingChanged', { settingIdentifier: settingId, settingMode: modeId, - settingEnablement: 'enabled' // inheriting + settingEnablement: inheritedEnablement }); this.configurationService.updateValue(settingId, rest); } else { diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css index 94a23e14cb173..59c4e064a3e39 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css @@ -333,12 +333,27 @@ text-align: right; -webkit-appearance: none; appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath fill='%23888' d='M0 2l4 4 4-4z'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right center; padding-right: 14px; } +.chat-status-bar-entry-tooltip .model-selection, +.chat-status-bar-entry-tooltip .suggest-option-selection { + position: relative; +} + +.chat-status-bar-entry-tooltip select.inline-select + .inline-select-chevron { + position: absolute; + right: 2px; + top: 50%; + transform: translateY(-50%); + width: 8px; + height: 8px; + background-color: var(--vscode-descriptionForeground); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath d='M0 2l4 4 4-4z'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath d='M0 2l4 4 4-4z'/%3E%3C/svg%3E"); + pointer-events: none; +} + .chat-status-bar-entry-tooltip select.inline-select:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; From eca149a30c5235c874f89fbc1f16aa96800dc8f0 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 29 Apr 2026 16:59:42 -0700 Subject: [PATCH 3/7] fix --- .../vscode-node/workspaceIndexingStatus.ts | 1 + src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostChatStatus.ts | 4 ++-- .../browser/chatStatus/chatStatusDashboard.ts | 16 ++++++++++++++-- .../browser/chatStatus/chatStatusItemService.ts | 2 +- .../vscode.proposed.chatStatusItem.d.ts | 2 +- 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/extensions/copilot/src/extension/workspaceChunkSearch/vscode-node/workspaceIndexingStatus.ts b/extensions/copilot/src/extension/workspaceChunkSearch/vscode-node/workspaceIndexingStatus.ts index c6e379593be5d..c3e35962c25d3 100644 --- a/extensions/copilot/src/extension/workspaceChunkSearch/vscode-node/workspaceIndexingStatus.ts +++ b/extensions/copilot/src/extension/workspaceChunkSearch/vscode-node/workspaceIndexingStatus.ts @@ -256,6 +256,7 @@ export class ChatStatusWorkspaceIndexingStatus extends Disposable { this._statusItem.title = { label: statusTitle, link: codebaseSemanticSearchDocsLink, + helpText: t`Indexes your codebase for more relevant AI results.`, }; this._statusItem.description = coalesce([ diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2139682d8d423..dd1c25a9e2a56 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3642,7 +3642,7 @@ export interface MainThreadTestingShape { export type ChatStatusItemDto = { id: string; - title: string | { label: string; link: string }; + title: string | { label: string; link: string; helpText?: string }; description: string; detail: string | undefined; }; diff --git a/src/vs/workbench/api/common/extHostChatStatus.ts b/src/vs/workbench/api/common/extHostChatStatus.ts index 47a8553731384..bcd5b0f23f8f1 100644 --- a/src/vs/workbench/api/common/extHostChatStatus.ts +++ b/src/vs/workbench/api/common/extHostChatStatus.ts @@ -49,10 +49,10 @@ export class ExtHostChatStatus { const item = Object.freeze({ id: id, - get title(): string | { label: string; link: string } { + get title(): string | { label: string; link: string; helpText?: string } { return state.title; }, - set title(value: string | { label: string; link: string }) { + set title(value: string | { label: string; link: string; helpText?: string }) { state.title = value; syncState(); }, diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index 808fd0e1d92fa..5bcad2ddca1b2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -316,11 +316,13 @@ export class ChatStatusDashboard extends DomWidget { const collapsibleInner = collapsibleContent.appendChild($('div.collapsible-inner')); if (collapsed) { collapsibleContent.classList.add('collapsed'); + collapsibleInner.inert = true; } if (disclosureHeader && chevron) { const toggle = () => { const isCollapsed = collapsibleContent.classList.toggle('collapsed'); + collapsibleInner.inert = isCollapsed; disclosureHeader!.setAttribute('aria-expanded', String(!isCollapsed)); chevron!.className = 'collapsible-chevron'; chevron!.classList.add(...ThemeIcon.asClassNameArray(isCollapsed ? Codicon.chevronRight : Codicon.chevronDown)); @@ -349,6 +351,7 @@ export class ChatStatusDashboard extends DomWidget { const headerLabel = typeof item.label === 'string' ? item.label : item.label.label; const headerLink = typeof item.label === 'string' ? undefined : item.label.link; + const linkDescription = typeof item.label === 'string' ? undefined : item.label.helpText; const disclosureHeader = this.element.appendChild($('button.collapsible-header')); disclosureHeader.setAttribute('aria-expanded', String(!collapsed)); @@ -366,10 +369,12 @@ export class ChatStatusDashboard extends DomWidget { const collapsibleInner = collapsibleContent.appendChild($('div.collapsible-inner')); if (collapsed) { collapsibleContent.classList.add('collapsed'); + collapsibleInner.inert = true; } const toggle = () => { const isCollapsed = collapsibleContent.classList.toggle('collapsed'); + collapsibleInner.inert = isCollapsed; disclosureHeader.setAttribute('aria-expanded', String(!isCollapsed)); chevron.className = 'collapsible-chevron'; chevron.classList.add(...ThemeIcon.asClassNameArray(isCollapsed ? Codicon.chevronRight : Codicon.chevronDown)); @@ -387,7 +392,10 @@ export class ChatStatusDashboard extends DomWidget { let descriptionEl: HTMLElement | undefined; if (headerLink) { descriptionEl = collapsibleInner.appendChild($('div.section-description')); - this.renderTextPlus(descriptionEl, `[${localize('learnMore', "Learn More")}](${headerLink})`, sectionStore); + const descText = linkDescription + ? `${linkDescription} [${localize('learnMore', "Learn More")}](${headerLink})` + : `[${localize('learnMore', "Learn More")}](${headerLink})`; + this.renderTextPlus(descriptionEl, descText, sectionStore); } // Detail content (action links like "Build index", etc.) @@ -421,9 +429,13 @@ export class ChatStatusDashboard extends DomWidget { // Re-render Learn More link if needed if (descriptionEl) { const updatedLink = typeof e.entry.label === 'string' ? undefined : e.entry.label.link; + const updatedLinkDesc = typeof e.entry.label === 'string' ? undefined : e.entry.label.helpText; descriptionEl.textContent = ''; if (updatedLink) { - this.renderTextPlus(descriptionEl, `[${localize('learnMore', "Learn More")}](${updatedLink})`, newStore); + const descText = updatedLinkDesc + ? `${updatedLinkDesc} [${localize('learnMore', "Learn More")}](${updatedLink})` + : `[${localize('learnMore', "Learn More")}](${updatedLink})`; + this.renderTextPlus(descriptionEl, descText, newStore); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusItemService.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusItemService.ts index b87135ccce61e..bc340b9c69d9a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusItemService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusItemService.ts @@ -27,7 +27,7 @@ export interface IChatStatusItemChangeEvent { export type ChatStatusEntry = { id: string; - label: string | { label: string; link: string }; + label: string | { label: string; link: string; helpText?: string }; description: string; detail: string | undefined; }; diff --git a/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts b/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts index b03afe16ca16d..ccbbd8773f31b 100644 --- a/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts +++ b/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts @@ -14,7 +14,7 @@ declare module 'vscode' { /** * The main name of the entry, like 'Indexing Status' */ - title: string | { label: string; link: string }; + title: string | { label: string; link: string; helpText?: string }; /** * Optional additional description of the entry. From 6e24c116029894f92bcbef7d3523eca84c9113d8 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 29 Apr 2026 17:24:23 -0700 Subject: [PATCH 4/7] clean --- .../browser/chatStatus/chatStatusDashboard.ts | 53 ++++++++----------- .../browser/chatStatus/media/chatStatus.css | 53 +++---------------- 2 files changed, 30 insertions(+), 76 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index 5bcad2ddca1b2..abcb3d4941ff8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -8,6 +8,7 @@ import { Gesture, EventType as TouchEventType } from '../../../../../base/browse import { ActionBar } from '../../../../../base/browser/ui/actionbar/actionbar.js'; import { renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { SelectBox } from '../../../../../base/browser/ui/selectBox/selectBox.js'; import { Checkbox, TriStateCheckbox } from '../../../../../base/browser/ui/toggle/toggle.js'; import { IAction, toAction, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../../base/common/actions.js'; import { CancellationToken, cancelOnDispose } from '../../../../../base/common/cancellation.js'; @@ -33,11 +34,12 @@ import { IMarkdownRendererService } from '../../../../../platform/markdown/brows import { Link } from '../../../../../platform/opener/browser/link.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; +import { defaultButtonStyles, defaultCheckboxStyles, defaultSelectBoxStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { DomWidget } from '../../../../../platform/domWidget/browser/domWidget.js'; import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js'; import { IChatEntitlementService, ChatEntitlementService, ChatEntitlement, IQuotaSnapshot, getChatPlanName } from '../../../../services/chat/common/chatEntitlementService.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IContextViewService } from '../../../../../platform/contextview/browser/contextView.js'; import { isNewUser } from './chatStatus.js'; import { IChatStatusItemService, ChatStatusEntry } from './chatStatusItemService.js'; import product from '../../../../../platform/product/common/product.js'; @@ -108,6 +110,7 @@ export class ChatStatusDashboard extends DomWidget { @IInlineCompletionsService private readonly inlineCompletionsService: IInlineCompletionsService, @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IContextViewService private readonly contextViewService: IContextViewService, @IStorageService private readonly storageService: IStorageService, ) { super(); @@ -519,21 +522,15 @@ export class ChatStatusDashboard extends DomWidget { modelContainer.appendChild($('span.model-text', undefined, localize('modelLabel', "Model"))); - const select = modelContainer.appendChild($('select.inline-select')) as HTMLSelectElement; - select.ariaLabel = localize('selectModel', "Select Model"); - modelContainer.appendChild($('span.inline-select-chevron')); - for (const model of modelInfo.models) { - const option = document.createElement('option'); - option.value = model.id; - option.textContent = model.name; - if (model.id === modelInfo.currentModelId) { - option.selected = true; - } - select.appendChild(option); - } - this._store.add(addDisposableListener(select, 'change', async () => { - if (select.value !== modelInfo.currentModelId && provider.setModelId) { - await provider.setModelId(select.value); + const selectOptions = modelInfo.models.map(m => ({ text: m.name })); + const selectedIndex = modelInfo.models.findIndex(m => m.id === modelInfo.currentModelId); + const selectBox = this._store.add(new SelectBox(selectOptions, Math.max(0, selectedIndex), this.contextViewService, defaultSelectBoxStyles, { ariaLabel: localize('selectModel', "Select Model") })); + const selectContainer = modelContainer.appendChild($('div.model-select-container')); + selectBox.render(selectContainer); + this._store.add(selectBox.onDidSelect(async e => { + const selectedModel = modelInfo.models[e.index]; + if (selectedModel && selectedModel.id !== modelInfo.currentModelId && provider.setModelId) { + await provider.setModelId(selectedModel.id); } })); } @@ -551,21 +548,15 @@ export class ChatStatusDashboard extends DomWidget { optionContainer.appendChild($('span.suggest-option-text', undefined, option.label)); - const select = optionContainer.appendChild($('select.inline-select')) as HTMLSelectElement; - select.ariaLabel = localize('selectOption', "Select {0}", option.label); - optionContainer.appendChild($('span.inline-select-chevron')); - for (const value of option.values) { - const optEl = document.createElement('option'); - optEl.value = value.id; - optEl.textContent = value.label; - if (value.id === option.currentValueId) { - optEl.selected = true; - } - select.appendChild(optEl); - } - this._store.add(addDisposableListener(select, 'change', async () => { - if (select.value !== option.currentValueId && provider.setProviderOption) { - await provider.setProviderOption(option.id, select.value); + const selectOptions = option.values.map(v => ({ text: v.label })); + const selectedIndex = option.values.findIndex(v => v.id === option.currentValueId); + const selectBox = this._store.add(new SelectBox(selectOptions, Math.max(0, selectedIndex), this.contextViewService, defaultSelectBoxStyles, { ariaLabel: localize('selectOption', "Select {0}", option.label) })); + const selectContainer = optionContainer.appendChild($('div.suggest-option-select-container')); + selectBox.render(selectContainer); + this._store.add(selectBox.onDidSelect(async e => { + const selectedValue = option.values[e.index]; + if (selectedValue && selectedValue.id !== option.currentValueId && provider.setProviderOption) { + await provider.setProviderOption(option.id, selectedValue.id); } })); } diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css index 59c4e064a3e39..669a9c2700383 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css @@ -307,7 +307,7 @@ } .chat-status-bar-entry-tooltip .model-selection .model-text { - flex: 1; + flex-shrink: 0; } .chat-status-bar-entry-tooltip .model-selection .model-action-bar { @@ -318,50 +318,13 @@ color: var(--vscode-descriptionForeground); } -/* Inline Select (native dropdown) */ - -.chat-status-bar-entry-tooltip select.inline-select { - margin-left: auto; - background: transparent; - color: var(--vscode-descriptionForeground); - border: none; - font-size: inherit; - font-family: inherit; - cursor: pointer; - padding: 0 2px; - outline: none; - text-align: right; - -webkit-appearance: none; - appearance: none; - padding-right: 14px; -} - -.chat-status-bar-entry-tooltip .model-selection, -.chat-status-bar-entry-tooltip .suggest-option-selection { - position: relative; -} - -.chat-status-bar-entry-tooltip select.inline-select + .inline-select-chevron { - position: absolute; - right: 2px; - top: 50%; - transform: translateY(-50%); - width: 8px; - height: 8px; - background-color: var(--vscode-descriptionForeground); - -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath d='M0 2l4 4 4-4z'/%3E%3C/svg%3E"); - mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath d='M0 2l4 4 4-4z'/%3E%3C/svg%3E"); - pointer-events: none; -} - -.chat-status-bar-entry-tooltip select.inline-select:focus-visible { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; -} +/* SelectBox containers */ -.chat-status-bar-entry-tooltip select.inline-select option { - background: var(--vscode-editorWidget-background); - color: var(--vscode-foreground); +.chat-status-bar-entry-tooltip .model-select-container .monaco-select-box, +.chat-status-bar-entry-tooltip .suggest-option-select-container .monaco-select-box { + padding: 2px 23px 2px 8px; + height: 22px; + font-size: 12px; } /* Provider Options */ @@ -374,7 +337,7 @@ } .chat-status-bar-entry-tooltip .suggest-option-selection .suggest-option-text { - flex: 1; + flex-shrink: 0; } .chat-status-bar-entry-tooltip .suggest-option-selection .suggest-option-action-bar { From 261bfb2e79e3a4bb723da5ab84f1f7cdd29e78bd Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 29 Apr 2026 17:47:08 -0700 Subject: [PATCH 5/7] overflow fixes --- .../browser/chatStatus/chatStatusDashboard.ts | 3 +++ .../browser/chatStatus/media/chatStatus.css | 20 ++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index abcb3d4941ff8..44da443e82170 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -21,6 +21,7 @@ import { language } from '../../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { isObject } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; +import { stripIcons } from '../../../../../base/common/iconLabels.js'; import { IInlineCompletionsService } from '../../../../../editor/browser/services/inlineCompletionsService.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js'; @@ -367,6 +368,7 @@ export class ChatStatusDashboard extends DomWidget { // Use renderLabelWithIcons for header status (plain text + icons only, no links inside button) const statusEl = disclosureHeader.appendChild($('span.collapsible-status')); statusEl.append(...renderLabelWithIcons(item.description)); + statusEl.title = stripIcons(item.description).trim(); const collapsibleContent = this.element.appendChild($('div.collapsible-content')); const collapsibleInner = collapsibleContent.appendChild($('div.collapsible-inner')); @@ -414,6 +416,7 @@ export class ChatStatusDashboard extends DomWidget { // Update status in header (plain text + icons only) statusEl.textContent = ''; statusEl.append(...renderLabelWithIcons(e.entry.description)); + statusEl.title = stripIcons(e.entry.description).trim(); // Re-render detail content const newStore = new DisposableStore(); diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css index 669a9c2700383..901af5e8efcf5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/media/chatStatus.css @@ -55,6 +55,11 @@ align-items: center; } +.chat-status-bar-entry-tooltip .collapsible-label { + white-space: nowrap; + flex-shrink: 0; +} + .chat-status-bar-entry-tooltip .collapsible-status { margin-left: auto; font-size: 12px; @@ -63,9 +68,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - display: flex; - align-items: center; - gap: 4px; + min-width: 0; } .chat-status-bar-entry-tooltip .section-description { @@ -307,7 +310,7 @@ } .chat-status-bar-entry-tooltip .model-selection .model-text { - flex-shrink: 0; + flex: 1; } .chat-status-bar-entry-tooltip .model-selection .model-action-bar { @@ -320,8 +323,15 @@ /* SelectBox containers */ +.chat-status-bar-entry-tooltip .model-select-container, +.chat-status-bar-entry-tooltip .suggest-option-select-container { + margin-left: auto; + flex-shrink: 0; +} + .chat-status-bar-entry-tooltip .model-select-container .monaco-select-box, .chat-status-bar-entry-tooltip .suggest-option-select-container .monaco-select-box { + width: auto; padding: 2px 23px 2px 8px; height: 22px; font-size: 12px; @@ -337,7 +347,7 @@ } .chat-status-bar-entry-tooltip .suggest-option-selection .suggest-option-text { - flex-shrink: 0; + flex: 1; } .chat-status-bar-entry-tooltip .suggest-option-selection .suggest-option-action-bar { From 20dbb10819cae5d72edb42703e1b9e52bec2980a Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 29 Apr 2026 17:51:45 -0700 Subject: [PATCH 6/7] clean --- .../chat/browser/chatStatus/chatStatusDashboard.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index 44da443e82170..ce5abee5c54b0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -202,7 +202,7 @@ export class ChatStatusDashboard extends DomWidget { // Next Edit Suggestions — collapsible region if (hasQuickSettingsContent) { const hasContentAbove = hasUsageSection || hasVisibleUsageContent || hasPremiumUnlimited; - this.renderNextEditSuggestionsSection(hasContentAbove); + this.renderInlineSuggestionsSection(hasContentAbove); } // Contributed sections (e.g. Codebase Semantic Index) — each gets its own collapsible @@ -280,7 +280,7 @@ export class ChatStatusDashboard extends DomWidget { } } - private renderNextEditSuggestionsSection(hasContentAbove: boolean): void { + private renderInlineSuggestionsSection(hasContentAbove: boolean): void { const nonCollapsible = !!this.options?.disableQuickSettingsCollapsible; const collapsed = !nonCollapsible && this.storageService.getBoolean(ChatStatusDashboard.QUICK_SETTINGS_COLLAPSED_KEY, StorageScope.PROFILE, true); @@ -288,14 +288,14 @@ export class ChatStatusDashboard extends DomWidget { const activeLanguageId = this.editorService.activeTextEditorLanguageId; const getStatusText = () => { if (!this.canUseChat()) { - return localize('nesDisabled', "Disabled"); + return localize('inlineSuggestionsDisabled', "Disabled"); } const enabled = activeLanguageId ? isCompletionsEnabled(this.configurationService, activeLanguageId) : isCompletionsEnabled(this.configurationService); return enabled - ? localize('nesEnabled', "Enabled") - : localize('nesDisabled', "Disabled"); + ? localize('inlineSuggestionsEnabled', "Enabled") + : localize('inlineSuggestionsDisabled', "Disabled"); }; let disclosureHeader: HTMLElement | undefined; From 464825972fd1e3c06646b876901a8c26475aa695 Mon Sep 17 00:00:00 2001 From: Paul Wang Date: Wed, 29 Apr 2026 18:12:27 -0700 Subject: [PATCH 7/7] PR fixes --- .../browser/chatStatus/chatStatusDashboard.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index ce5abee5c54b0..a12c97bf4a179 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -423,9 +423,12 @@ export class ChatStatusDashboard extends DomWidget { sectionDisposables.value = newStore; if (detailEl) { - detailEl.textContent = ''; if (e.entry.detail) { + detailEl.textContent = ''; this.renderTextPlus(detailEl, e.entry.detail, newStore); + } else { + detailEl.remove(); + detailEl = undefined; } } else if (e.entry.detail) { detailEl = collapsibleInner.appendChild($('div.section-detail')); @@ -433,16 +436,25 @@ export class ChatStatusDashboard extends DomWidget { } // Re-render Learn More link if needed + const updatedLink = typeof e.entry.label === 'string' ? undefined : e.entry.label.link; + const updatedLinkDesc = typeof e.entry.label === 'string' ? undefined : e.entry.label.helpText; if (descriptionEl) { - const updatedLink = typeof e.entry.label === 'string' ? undefined : e.entry.label.link; - const updatedLinkDesc = typeof e.entry.label === 'string' ? undefined : e.entry.label.helpText; - descriptionEl.textContent = ''; if (updatedLink) { + descriptionEl.textContent = ''; const descText = updatedLinkDesc ? `${updatedLinkDesc} [${localize('learnMore', "Learn More")}](${updatedLink})` : `[${localize('learnMore', "Learn More")}](${updatedLink})`; this.renderTextPlus(descriptionEl, descText, newStore); + } else { + descriptionEl.remove(); + descriptionEl = undefined; } + } else if (updatedLink) { + descriptionEl = collapsibleInner.insertBefore($('div.section-description'), detailEl ?? null); + const descText = updatedLinkDesc + ? `${updatedLinkDesc} [${localize('learnMore', "Learn More")}](${updatedLink})` + : `[${localize('learnMore', "Learn More")}](${updatedLink})`; + this.renderTextPlus(descriptionEl, descText, newStore); } } }));