diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index c1bd72f0831..1eb71223d04 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1001,6 +1001,8 @@ "--prompt-timeline-gutter-dot-gap", "--prompt-timeline-gutter-more-height", "--chat-input-notice-severity", + "--chat-input-notice-radius", + "--chat-input-own-radius", "--chat-input-notification-line-height", "--chat-editing-last-edit-shift", "--chat-voice-icon-glow-color", @@ -1148,6 +1150,7 @@ "--chat-input-anim-angle", "--chat-input-anim-duration", "--chat-input-radius", + "--chat-input-stack-radius-outer", "--chat-input-stack-radius-top", "--chat-input-working-border-color1", "--session-input-banner-anim-angle", diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatGoalBannerWidget.css b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatGoalBannerWidget.css index b7f90bb635f..baab284d8ac 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatGoalBannerWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatGoalBannerWidget.css @@ -11,8 +11,9 @@ box-sizing: border-box; border: 1px solid var(--vscode-input-border, transparent); border-bottom: none; - border-top-left-radius: var(--chat-input-stack-radius-top, var(--vscode-cornerRadius-small)); - border-top-right-radius: var(--chat-input-stack-radius-top, var(--vscode-cornerRadius-small)); + /* Follows the run it docks into; the fallback is its own tier outside one. */ + border-top-left-radius: var(--chat-input-stack-radius-top, var(--vscode-cornerRadius-large)); + border-top-right-radius: var(--chat-input-stack-radius-top, var(--vscode-cornerRadius-large)); background-color: color-mix(in srgb, var(--vscode-focusBorder) 6%, var(--vscode-editorWidget-background)); color: var(--vscode-foreground); font-size: var(--vscode-agents-fontSize-label1); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatInputNotice.css b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatInputNotice.css index e28d273ae3a..c8ac3a524ff 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatInputNotice.css +++ b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatInputNotice.css @@ -80,10 +80,12 @@ } /* A card closes its own box and sits clear of the input, so it reports itself - standalone and the input below keeps its own frame. */ + standalone and the input below keeps its own frame. Its bottom corners are its + own, so they take the run's radius directly rather than through the stack. */ .chat-input-notice.chat-input-notice-onboarding { + --chat-input-notice-radius: var(--chat-input-stack-radius-outer, var(--vscode-cornerRadius-large)); border-bottom: var(--vscode-strokeThickness) solid var(--vscode-input-border, var(--vscode-widget-border, transparent)); - border-radius: var(--chat-input-stack-radius-top, var(--vscode-cornerRadius-large)) var(--chat-input-stack-radius-top, var(--vscode-cornerRadius-large)) var(--vscode-cornerRadius-large) var(--vscode-cornerRadius-large); + border-radius: var(--chat-input-stack-radius-top, var(--chat-input-notice-radius)) var(--chat-input-stack-radius-top, var(--chat-input-notice-radius)) var(--chat-input-notice-radius) var(--chat-input-notice-radius); margin-bottom: var(--vscode-spacing-size40); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatInputStack.css b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatInputStack.css index 10365a6fead..30d15b74fcf 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatInputStack.css +++ b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatInputStack.css @@ -21,16 +21,36 @@ .chat-input-stack { gap: 0; -} - -/* Reset first, so a nested stack does not inherit its container's squaring. */ -.chat-input-stack > * { - --chat-input-stack-radius-top: initial; + /* + * The outer radius of the run: how round the joined surface is on the + * corners it does not share with a neighbour. + * + * The input owns this value - a denser host rounds it tighter - but a custom + * property cannot travel from the input back up to the members docked above + * it, so the input part declares `--chat-input-radius` (see `chat.css`) and + * the stack republishes it here for the whole run to read. The fallback + * covers a stack used outside a chat input part. + */ + --chat-input-stack-radius-outer: var(--chat-input-radius, var(--vscode-cornerRadius-large)); } /* - * Members read `--chat-input-stack-radius-top` for their top corners. It is - * unset outside a stack, so a notice used elsewhere keeps its own rounding. + * What the stack dictates for a member's top corners: the run's outer radius for + * the member that opens the run, and a squared edge for every member that + * continues one. Reset here so a nested stack does not inherit its container's + * squaring; the rule below re-squares the ones that continue. + * + * Members already read this with their own value as the fallback, so a member + * used outside a stack keeps its own rounding and one inside a run follows the + * run - no member has to know which input it is docked to. + */ +.chat-input-stack > * { + --chat-input-stack-radius-top: var(--chat-input-stack-radius-outer); +} + +/* + * A member that continues the run meets the one above it, so it squares the + * shared edge. */ .chat-input-stack > .chat-input-stack-continues { --chat-input-stack-radius-top: 0px; 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 ecfb3b03bfe..d79a164a960 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1078,18 +1078,45 @@ have to be updated for changes to the rules above, or to support more deeply nes margin-right: 3px; } +/* + * How round the chat input run is. + * + * Declared on the part that wraps the whole stack rather than on the input + * itself. Everything docked above the input joins it into a single surface, so + * those members have to round their outer corners to the same value - and a + * custom property only reaches them from a shared ancestor. `chatInputStack.css` + * republishes this as the run's outer radius; members read it from there. + * + * The three rules below are ordered, not just specific: `.compact` and the + * quick input scope both match at the same specificity and a compact input + * inside the quick input widget must round like a compact one, so `.compact` + * comes last. Keep them adjacent and in this order. + */ +.interactive-session .interactive-input-part { + --chat-input-radius: var(--vscode-cornerRadius-large); +} + +.quick-input-widget .interactive-session .interactive-input-part { + --chat-input-radius: var(--vscode-cornerRadius-xSmall); +} + +.interactive-session .interactive-input-part.compact { + --chat-input-radius: var(--vscode-cornerRadius-small); +} + .monaco-workbench .interactive-session .chat-input-container { box-sizing: border-box; cursor: text; background-color: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); - /* Top corners follow the stack, so a docked widget can square them. */ - --chat-input-radius: var(--vscode-cornerRadius-large); + /* Top corners follow the stack, so a docked widget can square them. The + fallback keeps an input rendered outside a chat input part rounded. */ + --chat-input-own-radius: var(--chat-input-radius, var(--vscode-cornerRadius-large)); border-radius: - var(--chat-input-stack-radius-top, var(--chat-input-radius)) - var(--chat-input-stack-radius-top, var(--chat-input-radius)) - var(--chat-input-radius) - var(--chat-input-radius); + var(--chat-input-stack-radius-top, var(--chat-input-own-radius)) + var(--chat-input-stack-radius-top, var(--chat-input-own-radius)) + var(--chat-input-own-radius) + var(--chat-input-own-radius); padding: 0 6px 6px 6px; /* top padding is inside the editor widget */ width: 100%; @@ -1769,7 +1796,6 @@ have to be updated for changes to the rules above, or to support more deeply nes padding-bottom: 0; /* no scrollbar */ padding-right: 6px; - --chat-input-radius: var(--vscode-cornerRadius-small); } .interactive-session .interactive-input-and-side-toolbar { @@ -2585,7 +2611,6 @@ have to be updated for changes to the rules above, or to support more deeply nes .quick-input-widget .interactive-session .chat-input-container { margin: 0; - --chat-input-radius: var(--vscode-cornerRadius-xSmall); padding: 0 4px 0 6px; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index fee0c99a356..6fba7103d1a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -135,7 +135,7 @@ } .chat-input-stack > .chat-input-notification-container.chat-input-stack-docked ~ .interactive-input-and-side-toolbar { - --chat-input-stack-radius-top: var(--vscode-cornerRadius-large); + --chat-input-stack-radius-top: var(--chat-input-stack-radius-outer); } } diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatInputNotice.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatInputNotice.fixture.ts index 67d0d9089a0..589987ed619 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatInputNotice.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatInputNotice.fixture.ts @@ -11,12 +11,52 @@ import { chatInputStackClass, chatInputStackSlotClass, ChatInputStackSlot, setCh import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; /** - * The three notice variants side by side. Five producers share one frame, and - * the only way to see that they still agree - and that severity only tints what - * it is meant to - is to put the variants next to each other at one width. + * Mounts one notice above a stand-in chat input, inside a real stack. + * + * The notice is wrapped in a slot that reports its state, so the squared join + * between the two - and the rounding they share - is produced by the stack + * rather than hardcoded here, and this breaks if that mechanism does. */ -function renderNotices(context: ComponentFixtureContext): void { +function addNoticeStack(context: ComponentFixtureContext, variant: ChatInputNoticeVariant, className: string, severity?: string): ChatInputNoticeWidget { const { container, disposableStore } = context; + const stack = dom.append(container, dom.$(`.${chatInputStackClass}`)); + stack.style.display = 'flex'; + stack.style.flexDirection = 'column'; + + const slot = dom.append(stack, dom.$(`.${chatInputStackSlotClass}`)); + const notice = disposableStore.add(new ChatInputNoticeWidget({ + container: slot, + variant, + className, + ariaLabel: className, + })); + if (severity) { + notice.domNode.classList.add(severity); + } + + const input = dom.append(stack, dom.$('div')); + input.textContent = 'Chat input'; + // Match the real input's host radius and square its top corners when docked. + input.style.cssText = 'padding:10px;color:var(--vscode-descriptionForeground);' + + 'background:var(--vscode-agentsChatInput-background, var(--vscode-input-background));' + + 'border:var(--vscode-strokeThickness) solid var(--vscode-input-border);' + + 'border-radius:' + + 'var(--chat-input-stack-radius-top, var(--chat-input-radius, var(--vscode-cornerRadius-large)))' + + ' var(--chat-input-stack-radius-top, var(--chat-input-radius, var(--vscode-cornerRadius-large)))' + + ' var(--chat-input-radius, var(--vscode-cornerRadius-large))' + + ' var(--chat-input-radius, var(--vscode-cornerRadius-large));'; + + // Reported last, once the input is in the stack: reporting a slot rescans the + // stack's children, so doing it before the input exists would leave the input + // unmarked and its top corners rounded under a docked notice. + setChatInputStackSlot(slot, variant === ChatInputNoticeVariant.Onboarding + ? ChatInputStackSlot.Standalone + : ChatInputStackSlot.Docked); + + return notice; +} + +function prepareContainer(container: HTMLElement): void { container.classList.add('monaco-workbench'); container.style.width = '320px'; container.style.padding = '24px'; @@ -24,65 +64,28 @@ function renderNotices(context: ComponentFixtureContext): void { container.style.flexDirection = 'column'; container.style.gap = '16px'; container.style.background = 'var(--vscode-editor-background)'; +} - const addNotice = (variant: ChatInputNoticeVariant, className: string, severity?: string) => { - // Each notice sits in a real stack above a stand-in for the chat input, and - // the notice is wrapped in a slot that reports its state. The squared top - // corner on the input below is therefore produced by the stack rather than - // hardcoded here, so this breaks if the mechanism does. - const stack = dom.append(container, dom.$(`.${chatInputStackClass}`)); - stack.style.display = 'flex'; - stack.style.flexDirection = 'column'; - - const slot = dom.append(stack, dom.$(`.${chatInputStackSlotClass}`)); - const notice = disposableStore.add(new ChatInputNoticeWidget({ - container: slot, - variant, - className, - ariaLabel: className, - })); - if (severity) { - notice.domNode.classList.add(severity); - } - setChatInputStackSlot(slot, variant === ChatInputNoticeVariant.Onboarding - ? ChatInputStackSlot.Standalone - : ChatInputStackSlot.Docked); - - const input = dom.append(stack, dom.$('div')); - input.textContent = 'Chat input'; - // Reads the stack's radius the way a real input does: rounded on its own, - // squared on top while something is docked above it. - input.style.cssText = 'padding:10px;color:var(--vscode-descriptionForeground);' - + 'background:var(--vscode-agentsChatInput-background, var(--vscode-input-background));' - + 'border:var(--vscode-strokeThickness) solid var(--vscode-input-border);' - + 'border-radius:' - + 'var(--chat-input-stack-radius-top, var(--vscode-cornerRadius-large))' - + ' var(--chat-input-stack-radius-top, var(--vscode-cornerRadius-large))' - + ' var(--vscode-cornerRadius-large) var(--vscode-cornerRadius-large);'; - return notice; - }; - - // A tip: one row of prose, an icon, and a dismiss that sits in the flow. - const tip = addNotice(ChatInputNoticeVariant.Tip, 'fixture-tip'); +/** A tip: one row of prose, an icon, and a dismiss that sits in the flow. */ +function addTip(context: ComponentFixtureContext): void { + const tip = addNoticeStack(context, ChatInputNoticeVariant.Tip, 'fixture-tip'); dom.append(tip.domNode, dom.$(ThemeIcon.asCSSSelector(Codicon.lightbulb))); dom.append(tip.domNode, dom.$('span')).textContent = 'Start a parallel conversation to build on all the changes made in this session.'; tip.addDismissAction({ onActivate: () => { } }); +} - // A notification, at each severity: same frame, only the tint changes. - for (const [severity, icon, message] of [ - ['severity-info', Codicon.info, 'You are approaching your monthly limit.'], - ['severity-warning', Codicon.warning, 'This model is temporarily unavailable.'], - ['severity-error', Codicon.error, 'Sign in to keep using chat.'], - ] as const) { - const notification = addNotice(ChatInputNoticeVariant.Notification, 'chat-input-notification-widget', severity); - const header = dom.append(notification.domNode, dom.$('.chat-input-notification-header')); - dom.append(dom.append(header, dom.$('.chat-input-notification-icon')), dom.$(ThemeIcon.asCSSSelector(icon))); - dom.append(header, dom.$('.chat-input-notification-title')).textContent = message; - notification.addDismissAction({ parent: header, onActivate: () => { } }); - } +/** A notification: the same frame at every severity, only the tint changes. */ +function addNotification(context: ComponentFixtureContext, severity: string, icon: ThemeIcon, message: string): void { + const notification = addNoticeStack(context, ChatInputNoticeVariant.Notification, 'chat-input-notification-widget', severity); + const header = dom.append(notification.domNode, dom.$('.chat-input-notification-header')); + dom.append(dom.append(header, dom.$('.chat-input-notification-icon')), dom.$(ThemeIcon.asCSSSelector(icon))); + dom.append(header, dom.$('.chat-input-notification-title')).textContent = message; + notification.addDismissAction({ parent: header, onActivate: () => { } }); +} - // An onboarding card: a stack, with its close pinned to the corner. - const card = addNotice(ChatInputNoticeVariant.Onboarding, 'fixture-card'); +/** An onboarding card: a stack, with its close pinned to the corner. */ +function addOnboardingCard(context: ComponentFixtureContext): void { + const card = addNoticeStack(context, ChatInputNoticeVariant.Onboarding, 'fixture-card'); const copy = dom.append(card.domNode, dom.$('div')); copy.style.paddingRight = 'var(--vscode-spacing-size240)'; dom.append(copy, dom.$('div')).textContent = 'Welcome to Voice Mode'; @@ -90,6 +93,36 @@ function renderNotices(context: ComponentFixtureContext): void { card.addDismissAction({ onActivate: () => { } }); } +/** + * The three notice variants side by side. Five producers share one frame, and + * the only way to see that they still agree - and that severity only tints what + * it is meant to - is to put the variants next to each other at one width. + */ +function renderNotices(context: ComponentFixtureContext): void { + prepareContainer(context.container); + + addTip(context); + for (const [severity, icon, message] of [ + ['severity-info', Codicon.info, 'You are approaching your monthly limit.'], + ['severity-warning', Codicon.warning, 'This model is temporarily unavailable.'], + ['severity-error', Codicon.error, 'Sign in to keep using chat.'], + ] as const) { + addNotification(context, severity, icon, message); + } + addOnboardingCard(context); +} + +/** Renders notices at the compact input radius, declared inline because this fixture does not load `chat.css`. */ +function renderNoticesInCompactInput(context: ComponentFixtureContext): void { + prepareContainer(context.container); + context.container.style.setProperty('--chat-input-radius', 'var(--vscode-cornerRadius-small)'); + + addTip(context); + addNotification(context, 'severity-info', Codicon.info, 'You are approaching your monthly limit.'); + addOnboardingCard(context); +} + export default defineThemedFixtureGroup({ path: 'chat/input/' }, { 'Chat input notices': defineComponentFixture({ render: renderNotices }), + 'Chat input notices in a compact input': defineComponentFixture({ render: renderNoticesInCompactInput }), });