diff --git a/.github/instructions/design-tokens.instructions.md b/.github/instructions/design-tokens.instructions.md index b286b7785cf2d..577e2c57a0abd 100644 --- a/.github/instructions/design-tokens.instructions.md +++ b/.github/instructions/design-tokens.instructions.md @@ -168,8 +168,8 @@ to its `*Compact` variant (e.g. `Codicon.close` → `Codicon.closeCompact`) so t icon is visually optimized for the small size. CSS `font-size` alone only scales the icon — it does not change to the compact glyph. Only swap the glyph when no CSS selector targets the original glyph class (e.g. `.codicon-close`), otherwise -update that selector too. Some icons (settings/sliders, agent, vm, info, lock, -plus) have no compact variant — keep the regular glyph at the compact size. +update that selector too. Some icons (agent, vm, info, lock) have no compact +variant — keep the regular glyph at the compact size. ## Stroke — border width diff --git a/src/vs/sessions/browser/parts/media/sessionsPart.css b/src/vs/sessions/browser/parts/media/sessionsPart.css index 3d70af4794c5c..1cce8687f69cb 100644 --- a/src/vs/sessions/browser/parts/media/sessionsPart.css +++ b/src/vs/sessions/browser/parts/media/sessionsPart.css @@ -116,14 +116,6 @@ max-width: 150px; } -.monaco-workbench .part.sessionspart .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:has(> .action-label.codicon-arrow-up-compact) { - border-radius: var(--vscode-cornerRadius-small); -} - -.monaco-workbench .part.sessionspart .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item > .action-label.codicon-arrow-up-compact { - border-radius: var(--vscode-cornerRadius-small); -} - /* Nudge icon position to ensure it looks optically aligned within the button */ .monaco-workbench .part.sessionspart .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item > .action-label.codicon-arrow-up-compact::before { display: inline-block; diff --git a/src/vs/sessions/contrib/chat/browser/media/chatInput.css b/src/vs/sessions/contrib/chat/browser/media/chatInput.css index 81d228168a95d..6aac710bbe929 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatInput.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatInput.css @@ -184,15 +184,18 @@ position: relative; } -/* Match the send button's control tier (22×22, control radius) and the compact +/* Match the send button's control tier (22×22, fully rounded) and the compact glyph size used by the pickers, so the voice controls, the dictation mic and - Send all read as one row. */ + Send all read as one row. On their own these lose the segmented pill's + enclosure, so each carries the pill's border as a ring of its own — see the + matching rule in chat.css for the editor composer. */ .sessions-chat-voice-toolbar .monaco-action-bar .action-item > .action-label { box-sizing: border-box; width: 22px; height: 22px; justify-content: center; - border-radius: var(--vscode-cornerRadius-small); + border: var(--vscode-strokeThickness) solid var(--vscode-input-border, var(--vscode-editorWidget-border)); + border-radius: var(--vscode-cornerRadius-circle); font-size: var(--vscode-codiconFontSize-compact); } @@ -311,7 +314,8 @@ gap: 2px; } -/* Send button - wraps a Button widget */ +/* Send button - wraps a Button widget. Circular, matching the send button in the + editor chat composer (chat.css) and the Agents window's active chat. */ .sessions-chat-send-button { display: flex; align-items: center; @@ -320,7 +324,7 @@ position: relative; width: 22px; height: 22px; - border-radius: var(--vscode-cornerRadius-small); + border-radius: var(--vscode-cornerRadius-circle); } .sessions-chat-send-button .monaco-button { @@ -332,7 +336,7 @@ min-width: 22px; padding: 0; font-size: var(--vscode-codiconFontSize-compact); - border-radius: var(--vscode-cornerRadius-small); + border-radius: var(--vscode-cornerRadius-circle); color: var(--vscode-icon-foreground); background: transparent; border: none; @@ -449,10 +453,12 @@ } .monaco-workbench .sessions-chat-attach-button .codicon[class*='codicon-'] { - font-size: 14px; + font-size: var(--vscode-codiconFontSize-compact); } -/* Dictation (speech-to-text) mic button */ +/* Dictation (speech-to-text) mic button. Same ringed circle as the voice + controls beside it, so the standalone mic reads as a single-cell version of + the segmented pill rather than a bare glyph. */ .sessions-chat-stt-button { display: flex; align-items: center; @@ -461,11 +467,12 @@ width: 22px; height: 22px; flex-shrink: 0; - border-radius: var(--vscode-cornerRadius-small); + box-sizing: border-box; + border: var(--vscode-strokeThickness) solid var(--vscode-input-border, var(--vscode-editorWidget-border)); + border-radius: var(--vscode-cornerRadius-circle); cursor: pointer; color: var(--vscode-icon-foreground); background: transparent; - border: none; outline: none; } diff --git a/src/vs/sessions/contrib/chat/browser/newChatInput.ts b/src/vs/sessions/contrib/chat/browser/newChatInput.ts index a51acc401d3c7..8908de43c48de 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatInput.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatInput.ts @@ -771,7 +771,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation position: { hoverPosition: HoverPosition.BELOW }, appearance: { showPointer: true } })); - dom.append(attachButton, renderIcon(Codicon.add)); + dom.append(attachButton, renderIcon(Codicon.addCompact)); this._register(dom.addDisposableListener(attachButton, dom.EventType.CLICK, () => { this._contextAttachments.showPicker(this.options.getContextFolderUri()); })); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 00c445ab3f3b9..5c0d324b88f82 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -471,7 +471,7 @@ export class AttachContextAction extends Action2 { super({ id: 'workbench.action.chat.attachContext', title: localize2('workbench.action.chat.attachContext.label.2', "Add Context..."), - icon: Codicon.add, + icon: Codicon.addCompact, category: CHAT_CATEGORY, keybinding: { when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat)), diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts index 4a96c99af1d98..dbb267b6dc977 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts @@ -122,7 +122,7 @@ export class ConfigureToolsAction extends Action2 { super({ id: ConfigureToolsAction.ID, title: localize('label', "Configure Tools..."), - icon: Codicon.settings, + icon: Codicon.settingsCompact, f1: false, category: CHAT_CATEGORY, precondition: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index 6c6edcf0278ab..8ddfd55044af4 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -2683,11 +2683,11 @@ pane is first mounted. View switches inside the modal are not animated. */ * Hide chips from the embedded chat input's primary toolbar that don't fit * the automation dialog's reduced surface: * - "Configure Tools" (`workbench.action.chat.configureTools`, - * `.codicon-settings`) — tool selection is not relevant for a scheduled - * prompt. - * - "Add Context" (`workbench.action.chat.attachContext`, `.codicon-add`, - * the `+`) — the dialog doesn't support attachments; the prompt is the - * only payload sent at run time. + * `.codicon-settings-compact`) — tool selection is not relevant for a + * scheduled prompt. + * - "Add Context" (`workbench.action.chat.attachContext`, + * `.codicon-add-compact`, the `+`) — the dialog doesn't support + * attachments; the prompt is the only payload sent at run time. * - "List MCP Servers" (`workbench.mcp.listServer`, `.codicon-server`) — * MCP server management belongs in the live chat surface, not a one-off * prompt definition. @@ -2705,8 +2705,8 @@ pane is first mounted. View switches inside the modal are not animated. */ * will migrate to `newChatInput.ts`, which builds its own toolbars and never * renders these `MenuId.ChatInput` chips at all. */ -.automation-form-prompt-host .chat-input-toolbar .action-label.codicon-settings, -.automation-form-prompt-host .chat-input-toolbar .action-label.codicon-add, +.automation-form-prompt-host .chat-input-toolbar .action-label.codicon-settings-compact, +.automation-form-prompt-host .chat-input-toolbar .action-label.codicon-add-compact, .automation-form-prompt-host .chat-input-toolbar .action-item.chat-mcp { display: none; } diff --git a/src/vs/workbench/contrib/chat/browser/speechToText/media/dictationMicGlow.css b/src/vs/workbench/contrib/chat/browser/speechToText/media/dictationMicGlow.css index 18d2176377e89..414221c4f096e 100644 --- a/src/vs/workbench/contrib/chat/browser/speechToText/media/dictationMicGlow.css +++ b/src/vs/workbench/contrib/chat/browser/speechToText/media/dictationMicGlow.css @@ -55,6 +55,14 @@ --dictation-mic-glow-radius: var(--vscode-cornerRadius-circle); } +/* Standalone dictation buttons are discs, so the glow has to be one too — + otherwise the light spills past the button's own edge. Covers the editor + chat toolbar and the Agents window composer's mic button. */ +.chat-execute-toolbar .monaco-action-bar .action-item.dictation-mic-active, +.sessions-chat-stt-button.dictation-mic-active { + --dictation-mic-glow-radius: var(--vscode-cornerRadius-circle); +} + /* High-contrast themes replace the soft glow with a solid outline, which must stay fully opaque to keep its contrast at low levels. */ .hc-black .dictation-mic-active::after, diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index f14393b8cee14..27498125870bb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1293,7 +1293,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item.chat-submit-button { position: relative; - border-radius: var(--vscode-cornerRadius-small); + border-radius: var(--vscode-cornerRadius-circle); } .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item.chat-submit-button > .action-label { @@ -1302,9 +1302,9 @@ have to be updated for changes to the rules above, or to support more deeply nes height: 22px; justify-content: center; font-size: var(--vscode-codiconFontSize-compact); - /* Keep the control corner radius in both states — the enabled rule below only + /* Keep the fully-rounded shape in both states — the enabled rule below only adds the filled background, it shouldn't be what defines the shape. */ - border-radius: var(--vscode-cornerRadius-small); + border-radius: var(--vscode-cornerRadius-circle); transition: background-color 250ms ease, color 250ms ease; } @@ -1323,7 +1323,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item.chat-submit-button:not(.disabled):has(> .action-label:focus-visible) { outline: 1px solid var(--vscode-focusBorder); outline-offset: 1px; - border-radius: var(--vscode-cornerRadius-small); + border-radius: var(--vscode-cornerRadius-circle); } .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item.chat-submit-button > .action-label:focus, @@ -2101,15 +2101,16 @@ have to be updated for changes to the rules above, or to support more deeply nes } -/* Add context button icon sizing */ -.interactive-session .chat-input-toolbar .action-item:has(.codicon-add) .action-label { - display: flex; - align-items: center; +/* Icon-only chips in the primary input toolbar (add context, configure tools, + MCP servers) all sit on the compact tier, so the row reads as one dense strip + of chrome instead of a 16px glyph towering over the 12px send / mic buttons + across the toolbar. The base action-bar rule gives these labels a fixed 16px + box with no inline centering, so a 12px glyph would otherwise sit flush left + inside it — center it explicitly. Scoped to labels that are *themselves* + codicons, so the text chips (mode / model pickers) keep their own type size. */ +.monaco-workbench .interactive-session .chat-input-toolbar .monaco-action-bar .action-item > .action-label.codicon[class*='codicon-'] { justify-content: center; -} - -.interactive-session .chat-input-toolbar .action-item:has(.codicon-add) .codicon-add { - font-size: 14px; + font-size: var(--vscode-codiconFontSize-compact); } .monaco-workbench .interactive-session .chat-input-toolbar .chat-input-picker-item .action-label .codicon-chevron-down, @@ -4680,11 +4681,18 @@ have to be updated for changes to the rules above, or to support more deeply nes /* Standalone dictation / Voice Mode buttons (shown when the segmented voice pill isn't active). Give them the same control tier as the send button they sit - beside — a 22×22 surface with the control corner radius — and the compact 12px - glyph used by the agent / model pickers, so the mic group reads as one - consistent row in every state: dictation idle / recording / preparing / - connecting, and Voice Mode idle / connecting / listening / speaking / - disconnect. */ + beside — a 22×22 surface with the compact 12px glyph used by the agent / + model pickers — so the mic group reads as one consistent row in every state: + dictation idle / recording / preparing / connecting, and Voice Mode idle / + connecting / listening / speaking / disconnect. + + On their own they lose the segmented pill's enclosure, so each one carries a + circular outline of its own and reads as a single-cell version of it rather + than a bare glyph. The ring uses the pill's exact border declaration so the + two enclosures are indistinguishable when the UI swaps between them — + `input.border` is `null` outside high contrast and resolves to a soft + 20%-alpha `editorWidget.border`, and in high contrast it resolves to + `contrastBorder` so the ring stays fully opaque there. */ .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item > .action-label.codicon-mic, .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item > .action-label.codicon-mic-filled, .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item > .action-label.codicon-mic-download-compact, @@ -4695,7 +4703,8 @@ have to be updated for changes to the rules above, or to support more deeply nes width: 22px; height: 22px; justify-content: center; - border-radius: var(--vscode-cornerRadius-small); + border: var(--vscode-strokeThickness) solid var(--vscode-input-border, var(--vscode-editorWidget-border)); + border-radius: var(--vscode-cornerRadius-circle); font-size: var(--vscode-codiconFontSize-compact); } @@ -4712,11 +4721,11 @@ have to be updated for changes to the rules above, or to support more deeply nes /* Voice mode: blue when listening. The glow keeps the button's resting shape — only its color and the pulse change between states, so the control never - morphs from a rounded square into a circle mid-session. */ + morphs from a circle into a rounded square mid-session. */ .chat-input-container.voice-active.voice-listening .chat-input-toolbars .action-label.codicon-voice-mode-compact { --chat-voice-icon-glow-color: rgba(88, 166, 255, 0.4); color: var(--vscode-charts-blue, #58a6ff) !important; - border-radius: var(--vscode-cornerRadius-small); + border-radius: var(--vscode-cornerRadius-circle); } .monaco-workbench.monaco-enable-motion .chat-input-container.voice-active.voice-listening .chat-input-toolbars .action-label.codicon-voice-mode-compact { @@ -4727,15 +4736,19 @@ have to be updated for changes to the rules above, or to support more deeply nes .chat-input-container.voice-active.voice-speaking .chat-input-toolbars .action-label.codicon-voice-mode-compact { --chat-voice-icon-glow-color: rgba(163, 113, 247, 0.4); color: var(--vscode-charts-purple, #a371f7) !important; - border-radius: var(--vscode-cornerRadius-small); + border-radius: var(--vscode-cornerRadius-circle); } .monaco-workbench.monaco-enable-motion .chat-input-container.voice-active.voice-speaking .chat-input-toolbars .action-label.codicon-voice-mode-compact { animation: chat-voice-icon-glow 1.4s ease-in-out infinite; } -/* Voice mode: green disconnect button */ -.chat-input-container .chat-input-toolbars .codicon-debug-disconnect-compact { +/* Voice mode: green disconnect button. This has to out-specify the blanket + `icon-foreground !important` rule that colors every toolbar codicon, so it + carries the full toolbar path rather than a short `.chat-input-container` + prefix — with equal `!important` the higher specificity wins, and the short + form silently lost the cascade and rendered grey. */ +.interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item > .action-label.codicon-debug-disconnect-compact { color: var(--vscode-charts-green, #3fb950) !important; } diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatInput.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatInput.fixture.ts index 5d72b3a71ccdd..b63f48901bf77 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatInput.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatInput.fixture.ts @@ -103,4 +103,15 @@ export default defineThemedFixtureGroup({ path: 'chat/input/' }, { todos: sampleTodos, }) }), + // Standalone dictation / Voice Mode controls, shown when the segmented voice + // pill isn't active. Each state changes part of the border / color / glow + // cascade, so they are covered individually. + VoiceDictationIdle: defineComponentFixture({ render: context => renderChatInput(context, { voiceControl: 'dictationIdle' }) }), + VoiceDictationRecording: defineComponentFixture({ render: context => renderChatInput(context, { voiceControl: 'dictationRecording' }) }), + VoiceDictationPreparing: defineComponentFixture({ render: context => renderChatInput(context, { voiceControl: 'dictationPreparing' }) }), + VoiceModeIdle: defineComponentFixture({ render: context => renderChatInput(context, { voiceControl: 'voiceIdle' }) }), + VoiceModeConnecting: defineComponentFixture({ render: context => renderChatInput(context, { voiceControl: 'voiceConnecting' }) }), + VoiceModeListening: defineComponentFixture({ render: context => renderChatInput(context, { voiceControl: 'voiceListening' }) }), + VoiceModeSpeaking: defineComponentFixture({ render: context => renderChatInput(context, { voiceControl: 'voiceSpeaking' }) }), + VoiceModeDisconnect: defineComponentFixture({ render: context => renderChatInput(context, { voiceControl: 'voiceDisconnect' }) }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts b/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts index 7eccf605e337b..2183180050430 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts @@ -8,6 +8,7 @@ import { observableValue } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { Codicon } from '../../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; @@ -23,6 +24,42 @@ import { AgentSandboxEnabledValue, AgentSandboxSettingId } from '../../../../../ import { ComponentFixtureContext, createEditorServices } from '../fixtureUtils.js'; import { FixtureMenuService, registerChatFixtureServices } from './chatFixtureUtils.js'; +/** + * A standalone dictation / Voice Mode control rendered in the execute toolbar, + * in one of the states its styling varies across. These controls are only shown + * when the segmented voice pill isn't active, and each state changes some part + * of the border / color / glow cascade in chat.css, so they are covered + * individually rather than as a single "voice on" screenshot. + */ +export type VoiceControlState = + | 'dictationIdle' + | 'dictationRecording' + | 'dictationPreparing' + | 'voiceIdle' + | 'voiceConnecting' + | 'voiceListening' + | 'voiceSpeaking' + | 'voiceDisconnect'; + +interface IVoiceControlRendering { + readonly icon: ThemeIcon; + /** Applied to `.chat-input-container`, mirroring what the voice session controller does. */ + readonly containerClasses?: readonly string[]; + /** Applied to the toolbar `.action-item`, mirroring `setupDictationMicGlow`. */ + readonly itemClasses?: readonly string[]; +} + +const voiceControlRenderings: Record = { + dictationIdle: { icon: Codicon.mic }, + dictationRecording: { icon: Codicon.micFilled, itemClasses: ['dictation-mic-active'] }, + dictationPreparing: { icon: Codicon.micDownloadCompact }, + voiceIdle: { icon: Codicon.voiceModeCompact }, + voiceConnecting: { icon: Codicon.loadingCompact }, + voiceListening: { icon: Codicon.voiceModeCompact, containerClasses: ['voice-active', 'voice-listening'] }, + voiceSpeaking: { icon: Codicon.voiceModeCompact, containerClasses: ['voice-active', 'voice-speaking'] }, + voiceDisconnect: { icon: Codicon.debugDisconnectCompact, containerClasses: ['voice-active'] }, +}; + export interface ChatInputFixtureOptions { readonly artifacts?: readonly { label: string; uri: string; type: 'devServer' | 'screenshot' | 'plan' | undefined }[]; readonly editingSession?: IChatEditingSession; @@ -44,11 +81,13 @@ export interface ChatInputFixtureOptions { readonly width?: number; /** Supplies models so the picker renders provider icons. */ readonly models?: readonly ILanguageModelChatMetadataAndIdentifier[]; + /** Renders a standalone dictation / Voice Mode control in the given state. */ + readonly voiceControl?: VoiceControlState; } export async function renderChatInput(context: ComponentFixtureContext, fixtureOptions: ChatInputFixtureOptions = {}): Promise { const { container, disposableStore } = context; - const { artifacts = [], editingSession, todos = [], isSessionsWindow = false, value, selection, sandboxingEnabled = false, width = 500, models = [] } = fixtureOptions; + const { artifacts = [], editingSession, todos = [], isSessionsWindow = false, value, selection, sandboxingEnabled = false, width = 500, models = [], voiceControl } = fixtureOptions; const artifactGroups: IArtifactSourceGroup[] = artifacts.length > 0 ? [{ source: { kind: 'agent' as const }, artifacts }] : []; const artifactsObs = observableValue('artifactGroups', artifactGroups); @@ -107,11 +146,16 @@ export async function renderChatInput(context: ComponentFixtureContext, fixtureO container.appendChild(session); const menuService = instantiationService.get(IMenuService) as FixtureMenuService; - menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.attachContext', title: '+', icon: Codicon.add }, group: 'navigation', order: -1 }); + menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.attachContext', title: '+', icon: Codicon.addCompact }, group: 'navigation', order: -1 }); menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.openModePicker', title: 'Agent' }, group: 'navigation', order: 1 }); menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.openModelPicker', title: 'GPT-5.3-Codex' }, group: 'navigation', order: 3 }); - menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.configureTools', title: '', icon: Codicon.settingsGear }, group: 'navigation', order: 100 }); - menuService.addItem(MenuId.ChatExecute, { command: { id: 'workbench.action.chat.submit', title: 'Send', icon: Codicon.newLine }, group: 'navigation', order: 4 }); + menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.configureTools', title: '', icon: Codicon.settingsCompact }, group: 'navigation', order: 100 }); + if (voiceControl) { + // Order 2 puts the voice control between the pickers and Send, where the + // real dictation / Voice Mode actions are contributed. + menuService.addItem(MenuId.ChatExecute, { command: { id: 'fixture.voiceControl', title: 'Voice', icon: voiceControlRenderings[voiceControl].icon }, group: 'navigation', order: 2 }); + } + menuService.addItem(MenuId.ChatExecute, { command: { id: 'workbench.action.chat.submit', title: 'Send', icon: Codicon.arrowUpCompact }, group: 'navigation', order: 4 }); menuService.addItem(MenuId.ChatInputSecondary, { command: { id: 'workbench.action.chat.openSessionTargetPicker', title: 'Local' }, group: 'navigation', order: 0 }); menuService.addItem(MenuId.ChatInputSecondary, { command: { id: 'workbench.action.chat.openPermissionPicker', title: 'Default Approvals' }, group: 'navigation', order: 10 }); @@ -162,4 +206,23 @@ export async function renderChatInput(context: ComponentFixtureContext, fixtureO await new Promise(r => setTimeout(r, 50)); inputPart.layout(width); } + + if (voiceControl) { + // Apply the state classes the voice session controller and the mic-glow + // helper set at runtime. They are the contract between those components + // and the border / color / glow rules in chat.css, so driving them here + // is what lets a screenshot lock that cascade without standing up the + // whole speech service stack. + const { containerClasses, itemClasses } = voiceControlRenderings[voiceControl]; + if (containerClasses?.length) { + session.querySelector('.chat-input-container')?.classList.add(...containerClasses); + } + if (itemClasses?.length) { + const item = session.querySelector('.chat-execute-toolbar .monaco-action-bar .action-item'); + item?.classList.add(...itemClasses); + // The glow's intensity is audio-reactive at runtime; pin it so the + // screenshot is deterministic. + (item as HTMLElement | null)?.style.setProperty('--dictation-mic-level', '0.6'); + } + } } diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index 4e891a2aedbbf..d0ba982af23a2 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -19,10 +19,10 @@ ![screenshot](https://hediet-screenshots.azurewebsites.net/images/62651503e0db22d7800352a848d4c6db4cdc56499fa40d5eac681958e9aea19a) #### editor/inlineChatZoneWidget/InlineChatZoneWidget/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/2401c41f5ef5e73d2e076f82c28462bdb80affcf3a7c088b903685a1cda7247d) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/7700bb9cad18d064af94493b4ae0a4f75e3c855df7ba4eb1d8a4a562eaa41dc6) #### editor/inlineChatZoneWidget/InlineChatZoneWidget/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/47716cdd8c034534ed13b7bc7717e2abe55d78d4e716b415d606a174dfeee70e) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/7f3cd7b0e664da973a1bb4c80f5d22005261f2eee798ffee0d3d95b48bf431b3) #### editor/inlineChatZoneWidget/InlineChatZoneWidgetTerminated/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/0752cf02ae3a4e21fce84b62859df32a5f41c13622bdec0083a3fd46832c2e0a)