From 4398f65da43113509f52605f2b88e689528b5b9a Mon Sep 17 00:00:00 2001 From: Elijah King Date: Wed, 29 Jul 2026 17:16:02 -0700 Subject: [PATCH 1/4] chat: circular send button, ringed standalone mic / Voice Mode buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the chat composer's execute toolbar read as one uniform set of 22px circular controls. - Send button is a circle rather than a rounded square, in both its idle and enabled states and in its focus ring, so the shape never changes with state. - The standalone dictation / Voice Mode buttons lose the segmented pill's enclosure when they render on their own, so each now carries a circular outline of its own. The ring is drawn in `currentColor` so it matches the glyph it encircles in every state — resting icon foreground, and the blue / purple / green the glyph takes while Voice Mode is listening, speaking or offering to disconnect. A border token can't track that, and the obvious ones aren't even visible here: both `input.border` and `widget.border` are `null` outside high contrast and fall back to a 20%-alpha color that all but disappears on a 1px circle. High-contrast themes still take `contrastBorder`. - The dictation mic glow follows the button's radius so the light no longer spills past its own edge. - Configure Tools moves to the `settingsCompact` glyph. It was the only icon in the primary toolbar still at the base 16px, towering over the 11-12px text chips beside it. The dependent selector in the automation dialog is updated with it. - Icon-only chips in the primary toolbar are sized and centered by one rule. The base action-bar rule gives them a fixed 16px box with no inline centering, so a compact glyph would otherwise sit flush left. This also retires an off-ramp `14px` codicon size and a `:has()` selector on `.action-item` (microsoft/vscode#324985). The chat input fixture was still constructing its toolbar with `newLine` / `settingsGear`, so it wasn't exercising the shipping icons. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../chat/browser/actions/chatToolActions.ts | 2 +- .../media/aiCustomizationManagement.css | 6 +-- .../speechToText/media/dictationMicGlow.css | 6 +++ .../chat/browser/widget/media/chat.css | 54 +++++++++++-------- .../componentFixtures/chat/renderChatInput.ts | 4 +- 5 files changed, 45 insertions(+), 27 deletions(-) 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 d120ed73aca99..eba7adc06f8eb 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -2679,8 +2679,8 @@ 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. + * `.codicon-settings-compact`) — 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. @@ -2701,7 +2701,7 @@ 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-settings-compact, .automation-form-prompt-host .chat-input-toolbar .action-label.codicon-add, .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..39656245f3922 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,12 @@ --dictation-mic-glow-radius: var(--vscode-cornerRadius-circle); } +/* The standalone chat-toolbar dictation button is a disc, so the glow has to be + one too — otherwise the light spills past the button's own edge. */ +.chat-execute-toolbar .monaco-action-bar .action-item.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 87fc24ade4fdd..a84e7d3d7c67a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1288,7 +1288,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 { @@ -1297,9 +1297,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; } @@ -1318,7 +1318,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, @@ -2096,15 +2096,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, @@ -4672,11 +4673,21 @@ 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 is drawn in `currentColor` so it always matches + the glyph it encircles: the resting icon foreground, and the same blue / + purple / green the glyph takes while Voice Mode is listening, speaking or + offering to disconnect. A border *token* can't track that — and the obvious + ones can't even be seen here, since `input.border` and `widget.border` are + both `null` outside high contrast and resolve to a 20%-alpha fallback that + all but disappears on a 1px circle. High-contrast themes still take + `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, @@ -4687,7 +4698,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-contrastBorder, currentColor); + border-radius: var(--vscode-cornerRadius-circle); font-size: var(--vscode-codiconFontSize-compact); } @@ -4704,11 +4716,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 { @@ -4719,7 +4731,7 @@ 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 { diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts b/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts index 7eccf605e337b..be14ffe0f1dd6 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/renderChatInput.ts @@ -110,8 +110,8 @@ export async function renderChatInput(context: ComponentFixtureContext, fixtureO 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.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 }); + 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 }); From 10af51104ba2dfa8679052710f21a8bab236b02f Mon Sep 17 00:00:00 2001 From: Elijah King Date: Wed, 29 Jul 2026 17:23:58 -0700 Subject: [PATCH 2/4] chat: match the standalone mic / Voice Mode ring to the segmented pill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ring was drawn in `currentColor` so it would track the glyph through Voice Mode's state colors, but that made it far too heavy at rest — in light themes `currentColor` is a dark icon foreground, so the circle read as a hard dark outline next to the pill's soft one. These buttons are single-cell stand-ins for the segmented pill, so the enclosure should be indistinguishable when the UI swaps between them. Use the pill's exact border declaration instead. Measured against a real pill: dark rgb(60,60,60) vs rgb(58,58,58), light rgb(206,206,206) vs rgb(210,210,210) — the deltas are antialiasing on the curve. This also restores correct high-contrast behaviour through the same token, since `input.border` resolves to `contrastBorder` there. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../contrib/chat/browser/widget/media/chat.css | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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 a84e7d3d7c67a..1c868919cfdfa 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -4680,13 +4680,10 @@ have to be updated for changes to the rules above, or to support more deeply nes 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 is drawn in `currentColor` so it always matches - the glyph it encircles: the resting icon foreground, and the same blue / - purple / green the glyph takes while Voice Mode is listening, speaking or - offering to disconnect. A border *token* can't track that — and the obvious - ones can't even be seen here, since `input.border` and `widget.border` are - both `null` outside high contrast and resolve to a 20%-alpha fallback that - all but disappears on a 1px circle. High-contrast themes still take + 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, @@ -4698,7 +4695,7 @@ have to be updated for changes to the rules above, or to support more deeply nes width: 22px; height: 22px; justify-content: center; - border: var(--vscode-strokeThickness) solid var(--vscode-contrastBorder, currentColor); + 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); } From 6d1d6070d047df399599543a86fd1da1c8f8b025 Mon Sep 17 00:00:00 2001 From: Elijah King Date: Thu, 30 Jul 2026 09:08:37 -0700 Subject: [PATCH 3/4] =?UTF-8?q?chat:=20address=20PR=20feedback=20=E2=80=94?= =?UTF-8?q?=20Agents=20window=20parity,=20compact=20add=20glyph,=20voice?= =?UTF-8?q?=20fixtures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the circular send styling across every chat composer, not just the editor one: - The sessions part re-declared the send button's radius as `cornerRadius-small`, overriding the base rule and leaving the Agents window's active chat a rounded square. Drop those overrides so it inherits from chat.css — duplicating the radius per surface is what let the two drift apart in the first place. - The Agents window's new-chat composer is a distinct widget with its own send button, so give `.sessions-chat-send-button` the circular radius too, and carry the ringed-circle treatment to its voice toolbar and dictation mic so the row stays consistent with the editor. Use the compact glyph for Add Context. `Codicon.addCompact` exists, and the convention is to swap to the optically tuned glyph rather than only scaling the base one. Updated in both composers plus the automation dialog's selector, and the Agents attach button drops an off-ramp 14px size in the process. The design-tokens doc listed "plus" as having no compact variant, which is no longer true. Add screenshot fixture variants for the standalone dictation and Voice Mode controls across all eight states they style differently: dictation idle / recording / preparing, and Voice Mode idle / connecting / listening / speaking / disconnect. The glow's audio-reactive level is pinned so the screenshots stay deterministic. Those fixtures immediately caught a live bug: the green disconnect rule was specificity (0,3,0) while the blanket `icon-foreground !important` rule that colors every toolbar codicon is (0,5,0). With equal `!important` the higher specificity wins, so the disconnect button had been rendering grey rather than green. Scoped the rule to the full toolbar path so it wins, rather than baselining the bug. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../design-tokens.instructions.md | 4 +- .../browser/parts/media/sessionsPart.css | 8 --- .../contrib/chat/browser/media/chatInput.css | 27 +++++--- .../contrib/chat/browser/newChatInput.ts | 2 +- .../browser/actions/chatContextActions.ts | 2 +- .../media/aiCustomizationManagement.css | 8 +-- .../speechToText/media/dictationMicGlow.css | 8 ++- .../chat/browser/widget/media/chat.css | 8 ++- .../chat/chatInput.fixture.ts | 11 +++ .../componentFixtures/chat/renderChatInput.ts | 67 ++++++++++++++++++- 10 files changed, 112 insertions(+), 33 deletions(-) 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 3fe703282651e..36ed8bf12416d 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/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index eba7adc06f8eb..c4c1f09d6a05d 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -2681,9 +2681,9 @@ pane is first mounted. View switches inside the modal are not animated. */ * - "Configure Tools" (`workbench.action.chat.configureTools`, * `.codicon-settings-compact`) — 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. + * - "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. @@ -2702,7 +2702,7 @@ pane is first mounted. View switches inside the modal are not animated. */ * renders these `MenuId.ChatInput` chips at all. */ .automation-form-prompt-host .chat-input-toolbar .action-label.codicon-settings-compact, -.automation-form-prompt-host .chat-input-toolbar .action-label.codicon-add, +.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 39656245f3922..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,9 +55,11 @@ --dictation-mic-glow-radius: var(--vscode-cornerRadius-circle); } -/* The standalone chat-toolbar dictation button is a disc, so the glow has to be - one too — otherwise the light spills past the button's own edge. */ -.chat-execute-toolbar .monaco-action-bar .action-item.dictation-mic-active { +/* 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); } 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 91b7490bdf0a5..7a4530d60a98b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -4735,8 +4735,12 @@ have to be updated for changes to the rules above, or to support more deeply nes 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 be14ffe0f1dd6..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,10 +146,15 @@ 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.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'); + } + } } From 5ad453cece3a6a261fdadcd94dd79ae703ba71fa Mon Sep 17 00:00:00 2001 From: Elijah King Date: Thu, 30 Jul 2026 14:13:44 -0700 Subject: [PATCH 4/4] chat: update blocks-ci screenshot baseline for the compact add glyph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switching Add Context to `Codicon.addCompact` changes the `+` in the inline chat zone widget, which is a blocks-ci fixture. Verified the change is intentional and confined before accepting the new hashes: rendering the fixture on this branch and on its merge-base and diffing them locally shows a single 12×12px region differing — the `+` glyph going from 11×11 to 10×10 of ink. That is the optically tuned compact glyph replacing the scaled-down base one, at a call site already rendering at `codiconFontSize-compact`, which is the pairing the design tokens guidance asks for. Nothing else in the widget moved. Hashes are exactly the ones the failing run computed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/componentFixtures/blocks-ci-screenshots.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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)