diff --git a/src/vs/sessions/LAYOUT.md b/src/vs/sessions/LAYOUT.md index 1164fe3aab122..2473717c9a53d 100644 --- a/src/vs/sessions/LAYOUT.md +++ b/src/vs/sessions/LAYOUT.md @@ -98,7 +98,7 @@ Which view is shown is owned by `ICustomViewService` ([services/customView/brows **Dismissal.** Opening a session (`SessionsService._startOpenSession`, which every explicit open gesture funnels through) hides the custom view. On phone layouts showing one pushes a `MobileNavigationStack` layer, so the Android back button dismisses it. Actions that operate on the hidden parts — Toggle Side Panel, Open Terminal, and the secondary side bar toggle — are disabled while it is shown (`CustomViewVisibleContext`). -**Chrome.** Each grid leaf is a `CustomViewNode` ([browser/parts/customViewNode.ts](src/vs/sessions/browser/parts/customViewNode.ts)) that owns the shared header — title, optional description and the contributed actions rendered either as an icon toolbar or a button bar — above a scroll container that grows a bottom border on the header as soon as the content is scrolled. The header band and the content are centred and capped to `AGENTS_CENTERED_CONTENT_MAX_WIDTH` (the same measure the session views use); a view may override it with `AbstractCustomView.maxWidth`. Views only fill the content container and are disposed when hidden. On phone-class viewports `CustomViewGridParts` selects `MobileCustomViewGridPart` instead, mirroring `SessionsParts`/`MobileSessionsPart`. +**Chrome.** Each grid leaf is a `CustomViewNode` ([browser/parts/customViewNode.ts](src/vs/sessions/browser/parts/customViewNode.ts)) that owns the shared header — title, optional description and the contributed actions rendered either as an icon toolbar or a button bar — above a scroll container. The header always has a bottom divider, independent of the content's scroll position. The header band and the content are centred and capped to `AGENTS_CENTERED_CONTENT_MAX_WIDTH` (the same measure the session views use); a view may override it with `AbstractCustomView.maxWidth`. Views only fill the content container and are disposed when hidden. On phone-class viewports `CustomViewGridParts` selects `MobileCustomViewGridPart` instead, mirroring `SessionsParts`/`MobileSessionsPart`. **Card chrome is shared.** The Sessions Part and the Custom View Grid both carry the `agents-part-card` class (`AGENTS_PART_CARD_CLASS`) and use `agentsPartCard.ts` for their metrics, themed colors and content-box math, so their padding, margins, background, border and corner radius are defined once and are identical. diff --git a/src/vs/sessions/browser/parts/customViewNode.ts b/src/vs/sessions/browser/parts/customViewNode.ts index 918c10cf301b1..91c58b4739c41 100644 --- a/src/vs/sessions/browser/parts/customViewNode.ts +++ b/src/vs/sessions/browser/parts/customViewNode.ts @@ -21,9 +21,7 @@ import { SessionHeaderMetaActionViewItem } from './sessionHeaderMetaActionViewIt /** * A leaf of the custom view grid. Owns the shared chrome — a header with the * title, an optional description and the contributed actions, above a scroll - * container — and hosts one {@link AbstractCustomView} inside it. The header - * stays put while the content scrolls beneath it and grows a bottom border as - * soon as the content is scrolled. + * container — and hosts one {@link AbstractCustomView} inside it. */ export class CustomViewNode extends Disposable { @@ -54,6 +52,7 @@ export class CustomViewNode extends Disposable { this.element.style.setProperty('--session-view-background', asCssVariable(activeSessionViewBackground)); this.element.style.setProperty('--session-view-foreground', asCssVariable(activeSessionViewForeground)); this.element.setAttribute('role', 'region'); + this.element.setAttribute('data-view-id', descriptor.id); this._headerEl = $('.custom-view-header'); this.element.appendChild(this._headerEl); @@ -101,9 +100,6 @@ export class CustomViewNode extends Disposable { })); this._scrollable.getDomNode().classList.add('custom-view-body'); this.element.appendChild(this._scrollable.getDomNode()); - this._register(this._scrollable.onScroll(e => { - this._headerEl.classList.toggle('scrolled', e.scrollTop > 0); - })); this._view.render(this._contentEl); diff --git a/src/vs/sessions/browser/parts/media/customViewGridPart.css b/src/vs/sessions/browser/parts/media/customViewGridPart.css index 556516e140a02..33f336971b4e1 100644 --- a/src/vs/sessions/browser/parts/media/customViewGridPart.css +++ b/src/vs/sessions/browser/parts/media/customViewGridPart.css @@ -41,11 +41,7 @@ box-sizing: border-box; margin: 0 auto; padding: 6px 10px; - border-bottom: 1px solid transparent; -} - -.custom-view-header.scrolled .custom-view-header-band { - border-bottom-color: color-mix(in srgb, var(--session-view-foreground) 12%, transparent); + border-bottom: var(--vscode-strokeThickness) solid color-mix(in srgb, var(--session-view-foreground) 12%, transparent); } .custom-view-header-title-row { diff --git a/src/vs/sessions/common/contextkeys.ts b/src/vs/sessions/common/contextkeys.ts index b9a666dfb75e7..f39217b64182a 100644 --- a/src/vs/sessions/common/contextkeys.ts +++ b/src/vs/sessions/common/contextkeys.ts @@ -58,6 +58,7 @@ export const MultipleSessionsVisibleContext = new RawContextKey('multip export const CustomViewVisibleContext = new RawContextKey('customViewVisible', false, localize('customViewVisible', "Whether a custom view is shown in place of the sessions grid. The side panel and the panel are hidden while it is.")); export const AutomationsCustomViewFocusContext = new RawContextKey('automationsCustomViewFocus', false, localize('automationsCustomViewFocus', "Whether the Automations custom view has keyboard focus")); +export const AutomationsHasItemsContext = new RawContextKey('automationsHasItems', false, localize('automationsHasItems', "Whether there is at least one automation")); //#endregion diff --git a/src/vs/sessions/contrib/automations/browser/automationDialog.ts b/src/vs/sessions/contrib/automations/browser/automationDialog.ts index be6e3246c5491..401a8d304bfb1 100644 --- a/src/vs/sessions/contrib/automations/browser/automationDialog.ts +++ b/src/vs/sessions/contrib/automations/browser/automationDialog.ts @@ -709,7 +709,7 @@ export function renderForm( const useCustomDrawn = !hasNativeContextMenu(configurationService); const intervalGroup = DOM.append(scheduleRow, $('.automation-form-schedule-group')); - DOM.append(intervalGroup, $('label.automation-form-label', undefined, localize('automation.form.interval', "Schedule"))); + DOM.append(intervalGroup, $('span.automation-form-label', undefined, localize('automation.form.interval', "Schedule"))); const intervalOptions: ISelectOptionItem[] = INTERVALS.map(item => ({ text: item.label })); const intervalIndex = Math.max(0, INTERVALS.findIndex(item => item.value === state.interval)); const intervalSelect = disposables.add(new SelectBox( @@ -723,7 +723,7 @@ export function renderForm( intervalSelect.render(intervalSelectContainer); const timeGroup = DOM.append(scheduleRow, $('.automation-form-schedule-group.automation-form-time-group')); - DOM.append(timeGroup, $('label.automation-form-label', undefined, localize('automation.form.time', "Time"))); + DOM.append(timeGroup, $('span.automation-form-label', undefined, localize('automation.form.time', "Time"))); const timeOptions = buildTimeOptions(); const initialTimeIndex = nearestTimeOptionIndex(state.hour, state.minute); state.hour = timeOptions[initialTimeIndex].hour; @@ -744,7 +744,7 @@ export function renderForm( })); const dayGroup = DOM.append(scheduleRow, $('.automation-form-schedule-group.automation-form-day-group')); - DOM.append(dayGroup, $('label.automation-form-label', undefined, localize('automation.form.day', "Day of week"))); + DOM.append(dayGroup, $('span.automation-form-label', undefined, localize('automation.form.day', "Day of week"))); const dayOptions: ISelectOptionItem[] = DAYS_OF_WEEK.map(d => ({ text: d })); const daySelect = disposables.add(new SelectBox( dayOptions, @@ -838,7 +838,7 @@ export function renderForm( })); const promptRow = DOM.append(form, $('.automation-form-row')); - DOM.append(promptRow, $('label.automation-form-label', undefined, localize('automation.form.prompt', "Prompt"))); + DOM.append(promptRow, $('span.automation-form-label', undefined, localize('automation.form.prompt', "Prompt"))); const promptHost = DOM.append(promptRow, $('.automation-form-prompt-host.interactive-session')); const chatInputStyles: IChatInputStyles = { diff --git a/src/vs/sessions/contrib/automations/browser/automationDialogService.ts b/src/vs/sessions/contrib/automations/browser/automationDialogService.ts index fb1623d0b284a..4c6e30e733a16 100644 --- a/src/vs/sessions/contrib/automations/browser/automationDialogService.ts +++ b/src/vs/sessions/contrib/automations/browser/automationDialogService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import './media/automationDialog.css'; import * as DOM from '../../../../base/browser/dom.js'; import { IButton } from '../../../../base/browser/ui/button/button.js'; import { Dialog } from '../../../../base/browser/ui/dialog/dialog.js'; diff --git a/src/vs/sessions/contrib/automations/browser/media/automationDialog.css b/src/vs/sessions/contrib/automations/browser/media/automationDialog.css new file mode 100644 index 0000000000000..4fe57c08e5ab0 --- /dev/null +++ b/src/vs/sessions/contrib/automations/browser/media/automationDialog.css @@ -0,0 +1,722 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Dialog */ +.monaco-dialog-box.automation-dialog .dialog-message-container > .dialog-message-detail { + display: none; +} + +/* + * The dialog's built-in title (rendered into `.dialog-message-text`) + * stays in the DOM so `aria-labelledby="monaco-dialog-message-text"` + * still binds for screen readers, but is visually hidden. Our own + * `.automation-titlebar` provides the visible chrome. Visually-hidden + * pattern (not `display: none`) because some AT/screen-reader combos + * skip aria-labelledby targets when `display: none` is in effect. + * Same treatment for the unused dialog icon slot. + */ +.monaco-dialog-box.automation-dialog .dialog-message-container > .dialog-message { + margin: 0; + padding: 0; +} + +.monaco-dialog-box.automation-dialog .dialog-message-container > .dialog-message .dialog-message-text { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.monaco-dialog-box.automation-dialog .dialog-message-row > .dialog-icon.codicon { + display: none; +} + +/* + * Lock the dialog box to a stable width. Without this rule the dialog + * inherits `width: min-content` from `dialog.css` and re-flows as the + * embedded chat input's chips change size (e.g. when the model picker + * resolves a shorter label). Use min(640px, 92vw) so the dialog still + * shrinks gracefully on narrow viewports. + * + * Padding is stripped (vs. the base 8px) so our `.automation-titlebar` + * stripe can paint edge-to-edge inside the dialog body. The form pane + * re-adds its own padding. + * + * Note: `extraClasses` on Dialog adds the class to `.monaco-dialog-box` + * itself (not an ancestor), so we attach the rule directly here. + */ +.monaco-dialog-box.automation-dialog { + width: min(640px, 92vw); + min-width: min(520px, 92vw); + max-width: 92vw; + padding: 0; + background-color: var(--vscode-editorWidget-background); + color: var(--vscode-editorWidget-foreground); + /* Anchor for the absolutely-positioned close-X (see below). */ + position: relative; +} + +.monaco-workbench .monaco-dialog-box.automation-dialog:focus:not(:focus-visible) { + outline: none; +} + +/* + * Float the close-X over the titlebar so the title text sits flush + * with the top edge of the modal (QuickInput-style). Without this, + * `.dialog-toolbar-row` reserves ~24px of dead space above the title. + * `z-index` keeps the X above the sticky titlebar. + */ +.monaco-dialog-box.automation-dialog .dialog-toolbar-row { + position: absolute; + top: 0; + right: 0; + height: auto; + padding: 8px 10px 0 0; + z-index: 2; +} + +.automation-dialog-body { + display: flex; + flex-direction: column; + min-height: 0; +} + +.monaco-dialog-box.automation-dialog > .dialog-buttons-row { + padding: 8px 10px 10px; +} + +/* + * Strip the base dialog padding on the message row and container so + * our titlebar can full-bleed. The form pane below re-adds horizontal + * padding so the form fields don't crash into the dialog edge. + * + * Container still owns the scrolling (per base/.../dialog.css), so + * the titlebar uses `position: sticky; top: 0` to stay visible as the + * form scrolls underneath it. + */ +.monaco-dialog-box.automation-dialog .dialog-message-row { + padding: 0; +} + +.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container { + padding: 0; + /* + * The base dialog leaves the message container at content size + * (`align-self: stretch` only stretches height in a horizontal flex + * row). Our form fields are `width: 100%` of this container, so they + * track the container's intrinsic min-content width. It shrinks + * over the first second as the embedded chat input's chip labels + * (model name, mode name, etc.) resolve and the toolbar's + * min-content drops. Forcing the container to fill the row width + * keeps every field at the dialog's locked width from first paint + * onwards. `min-width: 0` lets the container shrink inside the row + * (which has `overflow: hidden`) instead of being pushed by long + * chip labels. + */ + flex: 1 1 auto; + min-width: 0; +} + +/* + * Titlebar background matches the dialog body so the header reads as + * part of the modal, with no contrasting stripe. Both are anchored to + * `editorWidget.background`, the dialog widget's own native background + * (see defaultDialogStyles), so the header and body are always the same + * color in every theme. Set explicitly (not transparent) because the + * titlebar is sticky. A transparent background would let scrolled form + * content show through. No `border-bottom`. Sticky so the title stays + * in view as the form scrolls (the dialog's message-container is the + * scroll host). + */ +.automation-titlebar { + position: sticky; + top: 0; + z-index: 1; + /* Right padding reserves room for the floating close-X chip. */ + padding: 8px 36px 8px 14px; + text-align: left; + font-weight: var(--vscode-agents-fontWeight-semiBold); + font-size: var(--vscode-agents-fontSize-heading2); + color: var(--vscode-editorWidget-foreground); + background-color: var(--vscode-editorWidget-background); +} + +.automation-description { + padding: 8px 14px; + font-size: var(--vscode-agents-fontSize-body2, 12px); + color: var(--vscode-descriptionForeground); + line-height: 1.4; +} + +.automation-form-pane { + flex: 1 1 auto; + min-width: 0; + min-height: 0; + padding: 4px 14px 6px; +} + +/* + * Theme the native overflow scrollbar on the dialog body so it matches + * the rest of VS Code. .dialog-message-container is the scroll host + * (see base/browser/ui/dialog/dialog.css). Without this the dialog + * shows the platform's default scrollbar which clashes visually with + * the chat input embedded above. + */ +.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar { + width: 10px; +} + +.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar-track { + background-color: transparent; +} + +.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar-thumb { + min-height: 20px; + background-color: var(--vscode-scrollbarSlider-background); +} + +.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar-thumb:hover { + background-color: var(--vscode-scrollbarSlider-hoverBackground); +} + +.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar-thumb:active { + background-color: var(--vscode-scrollbarSlider-activeBackground); +} + +.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar-corner { + display: none; +} + +/* + * Z-index bridge for popups while the automation dialog is open. + * The dialog modal block is z-index 2575 (see base/.../dialog.css) + * and shares that value with .context-view (set inline in + * contextview.ts as `2575 + layer`). DOM order means the dialog + * shows on top, hiding any popup the chat input chips, the quick + * input, or a context menu try to open. We can't change the inline + * z-index from outside, so use `!important` while the dialog service + * marks the active container as automation-dialog-open. + */ +.automation-dialog-open .context-view.monaco-component { + z-index: 2600 !important; +} + +.automation-dialog-open .quick-input-widget { + z-index: 2600 !important; +} + +.automation-dialog-open .monaco-menu-container { + z-index: 2600 !important; +} + +.automation-form { + display: flex; + flex-direction: column; + gap: 10px; + padding: 2px 2px 4px; +} + +/* + * Host for the embedded ChatInputPart. The composer brings its own + * background, border, and rounded corners (see chat.css + * `.interactive-input-part`), so we just give it room to breathe and let + * it fill the dialog's column width. `min-height` is sized for ~5 lines + * of body text + the toolbar row so the editor doesn't feel cramped on + * first open; the editor itself grows as the user types. + */ +.automation-form-prompt-host { + display: flex; + flex-direction: column; + min-height: 140px; + /* + * The host carries the `.interactive-session` class so the shared + * chat input CSS (chip colors, focus borders, etc.) matches what + * users see in a real chat session. That class also sets + * `max-width: 950px; margin: auto; height: 100%` (chat.css), which + * is meant for the full chat panel. In our flex column it makes + * the host take its content's min-content width and center, + * collapsing the chat input to ~chip-bar width. Re-assert + * full-width sizing so the host fills the dialog's form column. + */ + width: auto; + max-width: 100%; + margin: 0; + height: auto; + align-self: stretch; +} + +.automation-form-prompt-host .interactive-input-part { + /* + * `.interactive-input-part` in chat.css has `margin: 0 12px`, which + * (combined with the default `width: auto`) is designed to inset the + * composer inside a wider chat panel. In our dialog the prompt host + * is already the form column, so the margin pushes the composer 12px + * past the host on each side. Its right border ends up sitting + * outside the dialog. Zero out the margin so the composer aligns + * with the other form fields. + */ + margin: 0; + /* chat.css sets `padding: 4px 0` — zero both ends so the container + * sits flush with the label above and the Enabled separator below. */ + padding-top: 0; + padding-bottom: 0; + /* Don't let long custom-mode names or model identifiers push the + * modal wider than its declared max-width. */ + max-width: 100%; + min-width: 0; +} + +.automation-form-prompt-host .chat-input-container { + /* chat.css sets `padding: 0 6px 6px` — the 6px bottom gap between the + * picker chips and the container border looks excessive in the compact + * dialog. Tighten it to 2px. */ + padding-bottom: 2px; +} + +.automation-form-prompt-host .interactive-input-editor, +.automation-form-prompt-host .monaco-editor, +.automation-form-prompt-host .monaco-editor-background, +.automation-form-prompt-host .monaco-editor .margin { + background-color: var(--vscode-settings-textInputBackground, var(--vscode-input-background)); +} + +.automation-form-prompt-host .interactive-input-and-side-toolbar { + /* Allow the input column to shrink below its content's preferred + * width so flex children (chips, custom-mode names) don't push the + * modal wider than its declared max-width. */ + min-width: 0; +} + +/* + * 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-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. + * + * We hide the `.action-label` (which carries the codicon class) rather than + * the parent `.action-item`. Targeting the `.action-item` would require a + * `:has()` selector, whose invalidation bookkeeping is paid across every + * `.action-item` in the workbench (see microsoft/vscode#324985). Since + * `.action-item` has no intrinsic padding/margin (actionbar.css), hiding the + * label collapses the slot to zero width, so there is no visual gap. + * + * Known limitation: the hidden chips remain in the toolbar's arrow-key focus + * rotation (the action bar skips only disabled/separator items, not + * `display:none` ones). This is an accepted temporary trade-off — the dialog + * 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-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; +} + +/* + * Suppress every inter-chip divider drawn by chat.css inside the dialog, + * in both the primary (`.chat-input-toolbar`) and secondary + * (`.chat-secondary-input-toolbar`) toolbars. CSS `+` matches by DOM + * order regardless of `display:none`, so the divider would otherwise + * appear as an orphan vertical line on whichever visible chip follows + * our hidden `+`/settings items. For example the bar drawn to the left + * of the Folder/Worktree isolation chip because it follows the Copilot + * CLI chip in the secondary toolbar (microsoft/vscode-internalbacklog#8304). The remaining visible + * chips (Agent, model, mode, isolation) already have enough intrinsic + * padding to read as separate without the divider. Dropping dividers + * entirely is simpler and more resilient than per-pair suppression rules + * that depend on DOM order. + */ +.automation-form-prompt-host .chat-input-toolbar .monaco-action-bar .actions-container > .action-item + .action-item::before, +.automation-form-prompt-host .chat-secondary-input-toolbar .monaco-action-bar .actions-container > .action-item + .action-item::before { + display: none; +} + +/* + * Cancel the dialog's `
    ` indent for the chat input toolbars. + * + * `dialog.css:120` applies `padding-inline-start: 20px` to every `
      ` + * inside `.dialog-message-container` (meant to reduce the excessive + * default indent on bulleted lists in dialog body text). The + * `monaco-action-bar`'s `.actions-container` is rendered as a `
        `, so + * the chat input's primary and secondary toolbars inherit that 20px + * left indent. This pushes the Agent / Copilot CLI chips ~20px right of + * the chat input's bottom-left corner. + * + * `actionbar.css` sets `.actions-container { padding: 0 }`, but the + * dialog rule uses a logical property (`padding-inline-start`), which + * cascades-after the physical shorthand and wins regardless of selector + * specificity. Override the logical property explicitly here. + * + * Scoped to `.automation-form-prompt-host` so the regular chat panel + * (which isn't inside a dialog) is unaffected. Any `
          ` + * elements in the dialog's actual message body text retain the + * intended 20px indent. + */ +.automation-form-prompt-host .chat-input-toolbar .monaco-action-bar .actions-container, +.automation-form-prompt-host .chat-secondary-toolbar .monaco-action-bar .actions-container { + padding-inline-start: 0; + margin-left: 0; +} + +/* + * Sessions-layer workspace picker visual override. + * + * Two cascading sources need to be defeated to match Mode/Model: + * + * 1. `.sessions-chat-picker-slot.sessions-chat-workspace-picker + * .action-label` (sessions-layer `chatWidget.css:244-269`) sets a + * welcome-flow visual: label 18px, inner span 18px, icons 16px, + * chevron, generous padding, `color: var(--vscode-foreground)`. + * + * 2. `.monaco-action-bar .action-label` (actionbar.css:48) sets the + * base toolbar font-size to **11px**. This is what Mode/Model + * inherit. They don't set font-size themselves. The action bar + * base rule wins for their `.action-label`. The workspace picker's + * own sessions-layer rule (source 1) overrides that base, so we + * need to put the 11px back explicitly. `font-size: inherit` + * does NOT work because none of `.sessions-chat-picker-slot`'s + * ancestors carry an 11px font-size. The 11px lives on sibling + * `.action-label` elements via `.monaco-action-bar .action-label`, + * not on an ancestor we can inherit from. + * + * Result: match the 11px base, neutralize the welcome-flow oversizing, + * keep the Mode/Model color (`var(--vscode-icon-foreground)`), match + * the chip metrics from `.chat-secondary-toolbar .chat-input-picker-item + * .action-label` (chat.css:1728), and hide the chevron (the neighboring + * secondary-toolbar pickers don't render one). + * + * Scoped to `.automation-form-prompt-host` so the welcome view's + * standalone workspace picker keeps its larger sizing. + */ +.automation-form-prompt-host .chat-secondary-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label { + height: 16px; + padding: 3px 6px; + font-size: 11px; + line-height: normal; + color: var(--vscode-icon-foreground); +} + +.automation-form-prompt-host .chat-secondary-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .sessions-chat-dropdown-label { + font-size: inherit; + line-height: inherit; +} + +.automation-form-prompt-host .chat-secondary-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label > .codicon:not(.sessions-chat-dropdown-chevron) { + font-size: 12px; +} + +/* + * The neighboring secondary-toolbar pickers don't render a dropdown + * chevron. They are recognized as chips by their padding + hover. Hide + * the chevron that `_renderTriggerLabel` always appends so the + * workspace chip reads the same way. + */ +.automation-form-prompt-host .chat-secondary-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .sessions-chat-dropdown-chevron { + display: none; +} + +.automation-form-row { + display: flex; + flex-direction: column; + gap: 4px; +} + +.automation-form-row.automation-form-checkbox-row { + flex-direction: row; + align-items: center; + gap: 8px; + margin-top: 2px; + padding-top: 10px; + border-top: 1px solid var(--vscode-widget-border, transparent); +} + +.automation-form-row.automation-form-checkbox-row > .monaco-checkbox { + margin-right: 0; +} + +/* + * Lay the Schedule / Time / Day controls along a single horizontal axis. + * Each control lives in its own `.automation-form-schedule-group` + * (label-above-control flex column). Time and Day join the row only + * when the interval is daily or weekly; flex-wrap keeps the layout + * stable if a translation pushes the row past the dialog width. + */ +.automation-form-row.automation-form-schedule-row { + flex-direction: row; + align-items: flex-end; + flex-wrap: wrap; + gap: 12px; +} + +.automation-form-schedule-group { + display: flex; + flex-direction: column; + gap: 4px; + /* + * Each visible group splits the schedule row equally. Hidden + * groups (`display: none` from `applyIntervalVisibility`) drop out + * of flex distribution, so: + * - manual / hourly → Schedule fills 100% of the row + * - daily → Schedule + Time split 50/50 + * - weekly → Schedule + Time + Day split 33/33/33 + * `min-width: 0` lets the contained SelectBox shrink past its + * intrinsic content width when the row is narrow, before the + * row's `flex-wrap: wrap` kicks in as a final safety net. + */ + flex: 1 1 0; + min-width: 0; +} + +/* SelectBox container sizing. Give Schedule / Time / Day the full + * width of their parent group (which itself flex-shares the row). + * + * `.monaco-select-box` ships with no intrinsic padding or height (the + * only baked-in metrics live in the `.monaco-action-bar .action-item` + * selector, which we're not inside of). Mirror the standalone-form + * pattern used by the settings editor (`settingsEditor2.css:689-694`). + * It uses `height: 26px` and `padding: 2px 6px`. This makes the dropdowns match the + * Name InputBox above them instead of reading as toolbar widgets + * crammed into a form. */ +.automation-form-schedule-select-container { + display: flex; + min-width: 0; + width: 100%; +} + +.automation-form-schedule-select-container .monaco-select-box { + height: 26px; + padding: 2px 8px; +} + +/* + * Form-row labels read as section titles ("Name", "Schedule", "Time", + * "Day of week", "Prompt"). Mirror the established agents + * design-system section-label pattern (see `.overview-section + * .section-label` at line ~734: label1 / fontWeight-medium / + * foreground) so the labels carry visual weight as section headings + * and clearly out-rank their controls. + * + * `margin-bottom` adds breathing room between the title text and + * its control so each form row reads as a discrete section. + */ +.automation-form-label { + font-size: var(--vscode-agents-fontSize-label1); + font-weight: var(--vscode-agents-fontWeight-semiBold); + color: var(--vscode-foreground); + line-height: 1.4; + margin-bottom: 2px; + cursor: default; +} + +.automation-form-hint { + font-size: var(--vscode-agents-fontSize-body2, 11px); + color: var(--vscode-descriptionForeground); + line-height: 1.4; + min-height: 1em; +} + +.automation-form-checkbox-label { + font-size: var(--vscode-agents-fontSize-body1, 13px); + color: var(--vscode-foreground); + cursor: pointer; +} + +/* + * Host for the native `InputBox` widget. The widget owns its own + * `` and styling (border, padding, focus ring); the host just + * lets it grow to the row width. + */ +.automation-form-input-host { + display: flex; + width: 100%; +} + +.automation-form-input-host > .monaco-inputbox { + flex: 1 1 auto; + min-width: 0; +} + +.automation-form-input, +.automation-form-select, +.automation-form-textarea { + font-family: inherit; + font-size: var(--vscode-agents-fontSize-body1, 13px); + color: var(--vscode-settings-textInputForeground, var(--vscode-input-foreground)); + background-color: var(--vscode-settings-textInputBackground, var(--vscode-input-background)); + border: 1px solid var(--vscode-settings-textInputBorder, var(--vscode-input-border, var(--vscode-contrastBorder, transparent))); + border-radius: 2px; + padding: 4px 6px; + box-sizing: border-box; + width: 100%; + min-height: 26px; +} + +.automation-form-select { + color: var(--vscode-settings-dropdownForeground, var(--vscode-dropdown-foreground)); + background-color: var(--vscode-settings-dropdownBackground, var(--vscode-dropdown-background)); + border-color: var(--vscode-settings-dropdownBorder, var(--vscode-dropdown-border, var(--vscode-contrastBorder, transparent))); +} + +.automation-form-textarea { + resize: vertical; + min-height: 60px; + line-height: 1.4; +} + +.automation-form-input:focus, +.automation-form-select:focus, +.automation-form-textarea:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; + border-color: var(--vscode-focusBorder); +} + +.automation-form-empty { + font-size: 12px; + color: var(--vscode-errorForeground); + padding: 6px 0; +} + +/* + * Folder picker. This uses the shared `WorkspacePicker` from the sessions + * layer (see `sessionWorkspacePicker.ts`). The picker renders an + * `` chip trigger inside + * `.sessions-chat-picker-slot.sessions-chat-workspace-picker`. We restyle + * it to fit the dialog's compact, 13px form scale. The upstream styling + * lives in `sessions/contrib/chat/browser/media/chatWidget.css` and is + * sized for the new-session "hero" UI (18px font, larger chip) which is + * too large for our modal. + * + * The dropdown content itself renders through `IActionWidgetService`, + * which has its own global CSS. No overrides needed here. + */ +/* + * Isolation + branch group: parented into the chat input's + * `.chat-secondary-toolbar` so it sits at the bottom-right of the chat + * input, sharing a row with `Copilot CLI | Default Permissions`. + * `margin-left: auto` pushes the group to the right edge within that + * flex row. + * + * Visual styling mirrors the new-session view's + * `.new-chat-bottom-container` chip vocabulary (see + * `sessions/contrib/chat/browser/media/chatWidget.css` lines 178-209): + * compact label text and codicons, icon-foreground color, no border. + */ +.automation-form-prompt-host .chat-secondary-toolbar .automation-form-isolation-group { + display: inline-flex; + align-items: center; + margin-left: auto; + min-width: 0; + gap: var(--vscode-spacing-size60); +} + +.automation-form-prompt-host .automation-form-branch-picker-slot { + display: inline-flex; + min-width: 0; +} + +.automation-form-prompt-host .automation-form-branch-slot { + display: inline-flex; + align-items: center; + gap: var(--vscode-spacing-size40); + height: 16px; + padding: var(--vscode-spacing-size20) var(--vscode-spacing-size60); + font-size: var(--vscode-agents-fontSize-label2); + color: var(--vscode-icon-foreground); + background: transparent; + border: none; + min-width: 0; + border-radius: var(--vscode-cornerRadius-small); + touch-action: manipulation; +} + +.automation-form-prompt-host .automation-form-branch-slot > .codicon { + font-size: var(--vscode-codiconFontSize-compact); + color: var(--vscode-icon-foreground); + flex-shrink: 0; +} + +.automation-form-prompt-host .automation-form-branch-slot:not(.branch-picker-disabled) { + cursor: pointer; +} + +.automation-form-prompt-host .automation-form-branch-slot:not(.branch-picker-disabled):hover { + background-color: var(--vscode-toolbar-hoverBackground); + color: var(--vscode-foreground); +} + +.automation-form-prompt-host .automation-form-branch-slot:focus-visible { + outline: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); + outline-offset: calc(-1 * var(--vscode-strokeThickness)); +} + +.automation-form-prompt-host .automation-form-branch-slot.branch-picker-disabled, +.automation-form-prompt-host .automation-form-branch-slot.branch-picker-disabled > .codicon { + cursor: default; + color: var(--vscode-descriptionForeground); +} + +.automation-form-prompt-host .automation-form-branch-name { + font-size: var(--vscode-agents-fontSize-label2); +} + +.automation-form-prompt-host .automation-form-branch-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.automation-form-prompt-host .chat-secondary-toolbar .automation-form-harness-chip { + display: inline-flex; + align-items: center; + gap: 4px; + height: 16px; + padding: 3px 6px; + font-size: 11px; + color: var(--vscode-icon-foreground); + background: transparent; + border: none; + opacity: 0.6; + cursor: default; +} + +.automation-form-prompt-host .automation-form-harness-chip > .codicon { + font-size: 12px; + flex-shrink: 0; +} + +.automation-form-prompt-host .automation-form-harness-label { + font-size: 11px; +} + +/* Missing-branch state (no folder / no git repo / detached / empty): use + * description-foreground + italic so it visually reads as "informational + * placeholder, not a real ref name". */ +.automation-form-prompt-host .automation-form-branch-slot.branch-picker-missing, +.automation-form-prompt-host .automation-form-branch-slot.branch-picker-missing > .codicon { + color: var(--vscode-descriptionForeground); +} + +.automation-form-prompt-host .automation-form-branch-slot.branch-picker-missing .automation-form-branch-name { + font-style: italic; +} + +.hc-black .automation-form-prompt-host .automation-form-branch-slot:focus-visible, +.hc-light .automation-form-prompt-host .automation-form-branch-slot:focus-visible { + outline-color: var(--vscode-contrastActiveBorder); +} \ No newline at end of file diff --git a/src/vs/sessions/contrib/sessions/browser/media/automationsCards.css b/src/vs/sessions/contrib/sessions/browser/media/automationsCards.css index b6ffd28e768cc..df7aa720e42ad 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/automationsCards.css +++ b/src/vs/sessions/contrib/sessions/browser/media/automationsCards.css @@ -97,7 +97,7 @@ align-items: flex-start; gap: 12px; padding: 12px 16px; - border: 1px solid var(--vscode-widget-border); + border: 1px solid var(--vscode-editorWidget-border); border-radius: 6px; height: 100%; cursor: pointer; @@ -140,13 +140,17 @@ flex-direction: row; align-items: center; gap: 8px; - word-break: break-word; } .automations-card-name-text { + flex: 1 1 0; + min-width: 0; font-size: 13px; font-weight: 600; color: var(--vscode-foreground); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .automations-card-disabled-badge { @@ -423,3 +427,8 @@ text-overflow: ellipsis; white-space: nowrap; } + +.hc-black .automations-card, +.hc-light .automations-card { + border-color: var(--vscode-contrastBorder); +} diff --git a/src/vs/sessions/contrib/sessions/browser/views/automationsView.ts b/src/vs/sessions/contrib/sessions/browser/views/automationsView.ts index 66387a53f8428..7a12d496aea7e 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/automationsView.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/automationsView.ts @@ -28,7 +28,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { status } from '../../../../../base/browser/ui/aria/aria.js'; import { createPixelSpinner } from '../../../../../base/browser/ui/pixelSpinner/pixelSpinner.js'; import { Gesture, GestureEvent, EventType as TouchEventType } from '../../../../../base/browser/touch.js'; @@ -46,7 +46,7 @@ import { Action2, MenuItemAction, registerAction2 } from '../../../../../platfor import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; import { BaseActionViewItem, IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../../base/common/actions.js'; -import { AutomationsCustomViewFocusContext } from '../../../../common/contextkeys.js'; +import { AutomationsCustomViewFocusContext, AutomationsHasItemsContext } from '../../../../common/contextkeys.js'; const $ = DOM.$; @@ -278,9 +278,9 @@ class AutomationCardsSection extends Disposable { const createButton = this.disposables.add(new Button(this.emptyContainer, { ...defaultButtonStyles, - title: localize('createAutomation', "Create automation"), + title: localize('createAutomation', "Create Automation"), })); - createButton.label = localize('createAutomation', "Create automation"); + createButton.label = localize('createAutomation', "Create Automation"); createButton.element.classList.add('automations-cards-create-button'); this.disposables.add(createButton.onDidClick(() => this.openCreateDialog())); } @@ -742,7 +742,7 @@ export class AutomationsCustomView extends AbstractCustomView { /** * Registers the Automations custom view with the custom view service. */ -class AutomationsCustomViewContribution extends Disposable { +export class AutomationsCustomViewContribution extends Disposable { static readonly ID = 'sessions.contrib.automationsCustomView'; @@ -750,9 +750,15 @@ class AutomationsCustomViewContribution extends Disposable { @ICustomViewService customViewService: ICustomViewService, @IActionViewItemService actionViewItemService: IActionViewItemService, @IContextKeyService contextKeyService: IContextKeyService, + @IAutomationService automationService: IAutomationService, ) { super(); + const hasItemsContext = AutomationsHasItemsContext.bindTo(contextKeyService); + this._register(autorun(reader => { + hasItemsContext.set(automationService.automations.read(reader).length > 0); + })); + this._register(customViewService.registerCustomView({ id: AUTOMATIONS_CUSTOM_VIEW_ID, ctor: new SyncDescriptor(AutomationsCustomView), @@ -822,7 +828,7 @@ registerAction2(class NewAutomationAction extends Action2 { id: 'sessionsView.newAutomation', title: localize2('newAutomation', "New Automation"), precondition: ChatAutomationsEnabledContext, - menu: [{ id: Menus.CustomViewAutomations, group: 'navigation', order: 1, when: ChatAutomationsEnabledContext }], + menu: [{ id: Menus.CustomViewAutomations, group: 'navigation', order: 1, when: ContextKeyExpr.and(ChatAutomationsEnabledContext, AutomationsHasItemsContext) }], }); } override async run(accessor: ServicesAccessor): Promise { diff --git a/src/vs/sessions/contrib/sessions/test/browser/automationsView.fixture.ts b/src/vs/sessions/contrib/sessions/test/browser/automationsView.fixture.ts new file mode 100644 index 0000000000000..91e2188d6bc78 --- /dev/null +++ b/src/vs/sessions/contrib/sessions/test/browser/automationsView.fixture.ts @@ -0,0 +1,246 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { constObservable, IObservable } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { mock, upcastPartial } from '../../../../../base/test/common/mock.js'; +import { IActionViewItemFactory, IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; +import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { MenuService } from '../../../../../platform/actions/common/menuService.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; +import { IAutomation, IAutomationRun } from '../../../../../workbench/contrib/chat/common/automations/automation.js'; +import { IAutomationDialogService } from '../../../../../workbench/contrib/chat/common/automations/automationDialogService.js'; +import { ChatAutomationsEnabledContext } from '../../../../../workbench/contrib/chat/common/automations/automationsEnabled.js'; +import { IAutomationRunner } from '../../../../../workbench/contrib/chat/common/automations/automationRunner.js'; +import { IAutomationService } from '../../../../../workbench/contrib/chat/common/automations/automationService.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; +import { CustomViewNode } from '../../../../browser/parts/customViewNode.js'; +import { CustomViewService, ICustomViewService } from '../../../../services/customView/browser/customViewService.js'; +import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; +import { ISession } from '../../../../services/sessions/common/session.js'; +import { ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js'; +import { AUTOMATIONS_CUSTOM_VIEW_ID, AutomationsCustomViewContribution } from '../../browser/views/automationsView.js'; + +const WORKSPACE = URI.parse('file:///workspaces/vscode'); + +class FixtureActionViewItemService extends Disposable implements IActionViewItemService { + + declare _serviceBrand: undefined; + + private readonly providers = new Map(); + private readonly changeEmitter = this._register(new Emitter()); + readonly onDidChange = this.changeEmitter.event; + + register(menu: MenuId, commandId: string | MenuId, provider: IActionViewItemFactory, event?: Event): IDisposable { + const key = this.getKey(menu, commandId); + if (this.providers.has(key)) { + throw new Error(`Duplicate action view item provider for ${key}`); + } + this.providers.set(key, provider); + const listener = event?.(() => this.changeEmitter.fire(menu)); + return toDisposable(() => { + listener?.dispose(); + this.providers.delete(key); + }); + } + + lookUp(menu: MenuId, commandId: string | MenuId): IActionViewItemFactory | undefined { + return this.providers.get(this.getKey(menu, commandId)); + } + + private getKey(menu: MenuId, commandId: string | MenuId): string { + return `${menu.id}/${commandId instanceof MenuId ? commandId.id : commandId}`; + } +} + +class FixtureAutomationService extends mock() { + + override readonly automations: IObservable; + override readonly runs: IObservable; + + constructor(automations: readonly IAutomation[], runs: readonly IAutomationRun[]) { + super(); + this.automations = constObservable(automations); + this.runs = constObservable(runs); + } +} + +class FixtureSessionsManagementService extends mock() { + + private readonly sessions = new Map(); + + constructor(runs: readonly IAutomationRun[]) { + super(); + for (const [index, run] of runs.entries()) { + if (!run.sessionResource) { + continue; + } + const resource = URI.parse(run.sessionResource); + this.sessions.set(run.sessionResource, upcastPartial({ + resource, + sessionId: `fixture-session-${index + 1}`, + isRead: constObservable(index !== 0), + })); + } + } + + override getSession(resource: URI): ISession | undefined { + return this.sessions.get(resource.toString()); + } + + override async markAllRead(): Promise { } +} + +interface IAutomationsFixtureData { + readonly automations: readonly IAutomation[]; + readonly runs: readonly IAutomationRun[]; +} + +interface IAutomationsFixtureOptions { + readonly width: number; + readonly height: number; + readonly populated: boolean; +} + +export default defineThemedFixtureGroup({ path: 'sessions/automations/' }, { + Populated: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAutomations(ctx, { width: 1000, height: 720, populated: true }), + }), + Empty: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAutomations(ctx, { width: 1000, height: 520, populated: false }), + }), + Narrow: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderAutomations(ctx, { width: 520, height: 720, populated: true }), + }), +}); + +function renderAutomations(ctx: ComponentFixtureContext, options: IAutomationsFixtureOptions): void { + const data = options.populated ? createPopulatedData() : { automations: [], runs: [] }; + const configurationService = new TestConfigurationService({ + chat: { automations: { enabled: true } }, + }); + const contextKeyService = new ContextKeyService(configurationService); + const actionViewItemService = new FixtureActionViewItemService(); + const customViewService = new CustomViewService(new NullLogService()); + const automationService = new FixtureAutomationService(data.automations, data.runs); + const sessionsManagementService = new FixtureSessionsManagementService(data.runs); + ChatAutomationsEnabledContext.bindTo(contextKeyService).set(true); + ChatAutomationsEnabledContext.bindTo(contextKeyService).set(true); + + const instantiationService = createEditorServices(ctx.disposableStore, { + colorTheme: ctx.theme, + additionalServices: reg => { + registerWorkbenchServices(reg); + reg.defineInstance(IActionViewItemService, actionViewItemService); + reg.define(IMenuService, MenuService); + reg.defineInstance(IConfigurationService, configurationService); + reg.defineInstance(IContextKeyService, contextKeyService); + reg.defineInstance(IAutomationService, automationService); + reg.defineInstance(IAutomationRunner, new class extends mock() { }()); + reg.defineInstance(IAutomationDialogService, new class extends mock() { }()); + reg.defineInstance(ICustomViewService, customViewService); + reg.defineInstance(ISessionsManagementService, sessionsManagementService); + reg.defineInstance(ISessionsService, new class extends mock() { + override async openSession(): Promise { } + }()); + }, + }); + + ctx.disposableStore.add(instantiationService.createInstance(AutomationsCustomViewContribution)); + customViewService.showCustomView(AUTOMATIONS_CUSTOM_VIEW_ID); + const descriptor = customViewService.activeCustomView.get(); + if (!descriptor) { + throw new Error('Automations custom view was not registered'); + } + + ctx.container.classList.add('monaco-workbench'); + ctx.container.style.width = `${options.width}px`; + ctx.container.style.height = `${options.height}px`; + ctx.container.style.setProperty('--session-view-background', 'var(--vscode-agentsPanel-background, var(--vscode-sideBar-background))'); + ctx.container.style.setProperty('--session-view-foreground', 'var(--vscode-agentsPanel-foreground, var(--vscode-sideBar-foreground))'); + ctx.container.style.backgroundColor = 'var(--session-view-background)'; + + const node = ctx.disposableStore.add(instantiationService.createInstance(CustomViewNode, descriptor)); + node.element.style.height = '100%'; + ctx.container.appendChild(node.element); + node.layout(options.width, options.height); +} + +function createPopulatedData(): IAutomationsFixtureData { + const today = new Date(); + today.setHours(9, 15, 0, 0); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + yesterday.setHours(15, 30, 0, 0); + + const automations: readonly IAutomation[] = [ + createAutomation({ + id: 'daily-review', + name: 'Daily code review', + prompt: 'Review recent changes for correctness, missing tests, and regressions.', + schedule: { interval: 'daily', scheduleHour: 9, scheduleMinute: 0, scheduleDay: 0 }, + }), + createAutomation({ + id: 'dependency-audit', + name: 'Dependency audit', + prompt: 'Check dependencies for available security updates and summarize recommended changes.', + schedule: { interval: 'weekly', scheduleHour: 10, scheduleMinute: 30, scheduleDay: 1 }, + enabled: false, + }), + createAutomation({ + id: 'issue-triage', + name: 'Issue triage', + prompt: 'Review new issues, group duplicates, and suggest labels for the maintainers.', + schedule: { interval: 'hourly', scheduleHour: 0, scheduleMinute: 0, scheduleDay: 0 }, + target: { kind: 'quickChat', providerId: 'fixture', sessionTypeId: 'fixture' }, + }), + ]; + + const runs: readonly IAutomationRun[] = [ + createRun('daily-review-run', 'daily-review', 'completed', today), + createRun('dependency-audit-run', 'dependency-audit', 'running', today), + createRun('issue-triage-run', 'issue-triage', 'failed', yesterday, 'The repository could not be reached.'), + ]; + + return { automations, runs }; +} + +function createAutomation(overrides: Partial): IAutomation { + const now = new Date().toISOString(); + return { + id: 'automation', + name: 'Automation', + prompt: 'Run the automation.', + schedule: { interval: 'manual', scheduleHour: 0, scheduleMinute: 0, scheduleDay: 0 }, + target: { kind: 'workspace', folderUri: WORKSPACE, isolation: { kind: 'default' } }, + enabled: true, + createdAt: now, + updatedAt: now, + ...overrides, + }; +} + +function createRun(id: string, automationId: string, status: IAutomationRun['status'], startedAt: Date, errorMessage?: string): IAutomationRun { + return { + id, + automationId, + status, + trigger: 'schedule', + sessionResource: URI.parse(`vscode-chat-session://fixture/${id}`).toString(), + startedAt: startedAt.toISOString(), + completedAt: status === 'completed' || status === 'failed' ? startedAt.toISOString() : undefined, + errorMessage, + leaderWindowId: 1, + }; +} diff --git a/src/vs/sessions/contrib/sessions/test/browser/automationsView.test.ts b/src/vs/sessions/contrib/sessions/test/browser/automationsView.test.ts index a5af330e058ea..5d02c5353dcc1 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/automationsView.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/automationsView.test.ts @@ -28,8 +28,12 @@ import { AutomationMutationGuard, IAutomationRunClaim, IAutomationService, ICrea import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; import { ISession } from '../../../../services/sessions/common/session.js'; import { ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js'; +import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; +import { ICustomViewService } from '../../../../services/customView/browser/customViewService.js'; +import { AutomationsHasItemsContext } from '../../../../common/contextkeys.js'; import { buildAutomationsAccessibleContent } from '../../browser/views/automationsAccessibility.js'; -import { AutomationsCardsWidget } from '../../browser/views/automationsView.js'; +import { AutomationsCardsWidget, AutomationsCustomViewContribution } from '../../browser/views/automationsView.js'; + const AUTOMATION_ID = 'automation-1'; const RUN_ID = 'run-1'; @@ -593,3 +597,37 @@ suite('AutomationsCardsWidget', () => { ); }); }); + +suite('AutomationsCustomViewContribution — context key', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + function setup() { + const automationService = new FakeAutomationService(); + const contextKeyService = new MockContextKeyService(); + const instantiationService = disposables.add(new TestInstantiationService()); + instantiationService.stub(IAutomationService, automationService); + instantiationService.stub(IContextKeyService, contextKeyService); + instantiationService.stub(ICustomViewService, new class extends mock() { + override readonly activeCustomView = constObservable(undefined); + override registerCustomView() { return { dispose() { } }; } + override hideCustomView() { } + }()); + instantiationService.stub(IActionViewItemService, new class extends mock() { + override register() { return { dispose() { } }; } + }()); + const contribution = disposables.add(instantiationService.createInstance(AutomationsCustomViewContribution)); + return { automationService, contextKeyService, contribution }; + } + + test('AutomationsHasItemsContext follows the automations observable (empty → non-empty → empty)', () => { + const { automationService, contextKeyService } = setup(); + + assert.strictEqual(contextKeyService.getContextKeyValue(AutomationsHasItemsContext.key), false, 'initially false'); + + automationService.setAutomations([automation()]); + assert.strictEqual(contextKeyService.getContextKeyValue(AutomationsHasItemsContext.key), true, 'true when non-empty'); + + automationService.setAutomations([]); + assert.strictEqual(contextKeyService.getContextKeyValue(AutomationsHasItemsContext.key), false, 'false when empty again'); + }); +}); 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 e50452d6230ae..99158cfc058e5 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -2021,707 +2021,3 @@ pane is first mounted. View switches inside the modal are not animated. */ } /* Welcome page styles live in the welcome page variant stylesheets. */ - - -/* Dialog */ -.monaco-dialog-box.automation-dialog .dialog-message-container > .dialog-message-detail { - display: none; -} - -/* - * The dialog's built-in title (rendered into `.dialog-message-text`) - * stays in the DOM so `aria-labelledby="monaco-dialog-message-text"` - * still binds for screen readers, but is visually hidden. Our own - * `.automation-titlebar` provides the visible chrome. Visually-hidden - * pattern (not `display: none`) because some AT/screen-reader combos - * skip aria-labelledby targets when `display: none` is in effect. - * Same treatment for the unused dialog icon slot. - */ -.monaco-dialog-box.automation-dialog .dialog-message-container > .dialog-message { - margin: 0; - padding: 0; -} - -.monaco-dialog-box.automation-dialog .dialog-message-container > .dialog-message .dialog-message-text { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -.monaco-dialog-box.automation-dialog .dialog-message-row > .dialog-icon.codicon { - display: none; -} - -/* - * Lock the dialog box to a stable width. Without this rule the dialog - * inherits `width: min-content` from `dialog.css` and re-flows as the - * embedded chat input's chips change size (e.g. when the model picker - * resolves a shorter label). Use min(640px, 92vw) so the dialog still - * shrinks gracefully on narrow viewports. - * - * Padding is stripped (vs. the base 8px) so our `.automation-titlebar` - * stripe can paint edge-to-edge inside the dialog body. The form pane - * re-adds its own padding. - * - * Note: `extraClasses` on Dialog adds the class to `.monaco-dialog-box` - * itself (not an ancestor), so we attach the rule directly here. - */ -.monaco-dialog-box.automation-dialog { - width: min(640px, 92vw); - min-width: min(520px, 92vw); - max-width: 92vw; - padding: 0; - background-color: var(--vscode-editorWidget-background); - color: var(--vscode-editorWidget-foreground); - /* Anchor for the absolutely-positioned close-X (see below). */ - position: relative; -} - -.monaco-workbench .monaco-dialog-box.automation-dialog:focus:not(:focus-visible) { - outline: none; -} - -/* - * Float the close-X over the titlebar so the title text sits flush - * with the top edge of the modal (QuickInput-style). Without this, - * `.dialog-toolbar-row` reserves ~24px of dead space above the title. - * `z-index` keeps the X above the sticky titlebar. - */ -.monaco-dialog-box.automation-dialog .dialog-toolbar-row { - position: absolute; - top: 0; - right: 0; - height: auto; - padding: 4px 6px 0 0; - z-index: 2; -} - -.automation-dialog-body { - display: flex; - flex-direction: column; - min-height: 0; -} - -/* - * Strip the base dialog padding on the message row and container so - * our titlebar can full-bleed. The form pane below re-adds horizontal - * padding so the form fields don't crash into the dialog edge. - * - * Container still owns the scrolling (per base/.../dialog.css), so - * the titlebar uses `position: sticky; top: 0` to stay visible as the - * form scrolls underneath it. - */ -.monaco-dialog-box.automation-dialog .dialog-message-row { - padding: 0; -} - -.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container { - padding: 0; - /* - * The base dialog leaves the message container at content size - * (`align-self: stretch` only stretches height in a horizontal flex - * row). Our form fields are `width: 100%` of this container, so they - * track the container's intrinsic min-content width. It shrinks - * over the first second as the embedded chat input's chip labels - * (model name, mode name, etc.) resolve and the toolbar's - * min-content drops. Forcing the container to fill the row width - * keeps every field at the dialog's locked width from first paint - * onwards. `min-width: 0` lets the container shrink inside the row - * (which has `overflow: hidden`) instead of being pushed by long - * chip labels. - */ - flex: 1 1 auto; - min-width: 0; -} - -/* - * Titlebar background matches the dialog body so the header reads as - * part of the modal, with no contrasting stripe. Both are anchored to - * `editorWidget.background`, the dialog widget's own native background - * (see defaultDialogStyles), so the header and body are always the same - * color in every theme. Set explicitly (not transparent) because the - * titlebar is sticky. A transparent background would let scrolled form - * content show through. No `border-bottom`. Sticky so the title stays - * in view as the form scrolls (the dialog's message-container is the - * scroll host). - */ -.automation-titlebar { - position: sticky; - top: 0; - z-index: 1; - /* Right padding reserves room for the floating close-X chip. */ - padding: 8px 36px 8px 14px; - text-align: left; - font-weight: var(--vscode-agents-fontWeight-semiBold); - font-size: var(--vscode-agents-fontSize-heading2); - color: var(--vscode-editorWidget-foreground); - background-color: var(--vscode-editorWidget-background); -} - -.automation-description { - padding: 8px 14px; - font-size: var(--vscode-agents-fontSize-body2, 12px); - color: var(--vscode-descriptionForeground); - line-height: 1.4; -} - -.automation-form-pane { - flex: 1 1 auto; - min-width: 0; - min-height: 0; - padding: 4px 14px 6px; -} - -/* - * Theme the native overflow scrollbar on the dialog body so it matches - * the rest of VS Code. .dialog-message-container is the scroll host - * (see base/browser/ui/dialog/dialog.css). Without this the dialog - * shows the platform's default scrollbar which clashes visually with - * the chat input embedded above. - */ -.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar { - width: 10px; -} - -.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar-track { - background-color: transparent; -} - -.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar-thumb { - min-height: 20px; - background-color: var(--vscode-scrollbarSlider-background); -} - -.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar-thumb:hover { - background-color: var(--vscode-scrollbarSlider-hoverBackground); -} - -.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar-thumb:active { - background-color: var(--vscode-scrollbarSlider-activeBackground); -} - -.monaco-dialog-box.automation-dialog .dialog-message-row .dialog-message-container::-webkit-scrollbar-corner { - display: none; -} - -/* - * Z-index bridge for popups while the automation dialog is open. - * The dialog modal block is z-index 2575 (see base/.../dialog.css) - * and shares that value with .context-view (set inline in - * contextview.ts as `2575 + layer`). DOM order means the dialog - * shows on top, hiding any popup the chat input chips, the quick - * input, or a context menu try to open. We can't change the inline - * z-index from outside, so use `!important` while the dialog service - * marks the active container as automation-dialog-open. - */ -.automation-dialog-open .context-view.monaco-component { - z-index: 2600 !important; -} - -.automation-dialog-open .quick-input-widget { - z-index: 2600 !important; -} - -.automation-dialog-open .monaco-menu-container { - z-index: 2600 !important; -} - -.automation-form { - display: flex; - flex-direction: column; - gap: 10px; - padding: 2px 2px 4px; -} - -/* - * Host for the embedded ChatInputPart. The composer brings its own - * background, border, and rounded corners (see chat.css - * `.interactive-input-part`), so we just give it room to breathe and let - * it fill the dialog's column width. `min-height` is sized for ~5 lines - * of body text + the toolbar row so the editor doesn't feel cramped on - * first open; the editor itself grows as the user types. - */ -.automation-form-prompt-host { - display: flex; - flex-direction: column; - min-height: 140px; - /* - * The host carries the `.interactive-session` class so the shared - * chat input CSS (chip colors, focus borders, etc.) matches what - * users see in a real chat session. That class also sets - * `max-width: 950px; margin: auto; height: 100%` (chat.css), which - * is meant for the full chat panel. In our flex column it makes - * the host take its content's min-content width and center, - * collapsing the chat input to ~chip-bar width. Re-assert - * full-width sizing so the host fills the dialog's form column. - */ - width: auto; - max-width: 100%; - margin: 0; - height: auto; - align-self: stretch; -} - -.automation-form-prompt-host .interactive-input-part { - /* - * `.interactive-input-part` in chat.css has `margin: 0 12px`, which - * (combined with the default `width: auto`) is designed to inset the - * composer inside a wider chat panel. In our dialog the prompt host - * is already the form column, so the margin pushes the composer 12px - * past the host on each side. Its right border ends up sitting - * outside the dialog. Zero out the margin so the composer aligns - * with the other form fields. - */ - margin: 0; - /* Don't let long custom-mode names or model identifiers push the - * modal wider than its declared max-width. */ - max-width: 100%; - min-width: 0; -} - -.automation-form-prompt-host .interactive-input-editor, -.automation-form-prompt-host .monaco-editor, -.automation-form-prompt-host .monaco-editor-background, -.automation-form-prompt-host .monaco-editor .margin { - background-color: var(--vscode-settings-textInputBackground, var(--vscode-input-background)); -} - -.automation-form-prompt-host .interactive-input-and-side-toolbar { - /* Allow the input column to shrink below its content's preferred - * width so flex children (chips, custom-mode names) don't push the - * modal wider than its declared max-width. */ - min-width: 0; -} - -/* - * 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-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. - * - * We hide the `.action-label` (which carries the codicon class) rather than - * the parent `.action-item`. Targeting the `.action-item` would require a - * `:has()` selector, whose invalidation bookkeeping is paid across every - * `.action-item` in the workbench (see microsoft/vscode#324985). Since - * `.action-item` has no intrinsic padding/margin (actionbar.css), hiding the - * label collapses the slot to zero width, so there is no visual gap. - * - * Known limitation: the hidden chips remain in the toolbar's arrow-key focus - * rotation (the action bar skips only disabled/separator items, not - * `display:none` ones). This is an accepted temporary trade-off — the dialog - * 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-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; -} - -/* - * Suppress every inter-chip divider drawn by chat.css inside the dialog, - * in both the primary (`.chat-input-toolbar`) and secondary - * (`.chat-secondary-input-toolbar`) toolbars. CSS `+` matches by DOM - * order regardless of `display:none`, so the divider would otherwise - * appear as an orphan vertical line on whichever visible chip follows - * our hidden `+`/settings items. For example the bar drawn to the left - * of the Folder/Worktree isolation chip because it follows the Copilot - * CLI chip in the secondary toolbar (microsoft/vscode-internalbacklog#8304). The remaining visible - * chips (Agent, model, mode, isolation) already have enough intrinsic - * padding to read as separate without the divider. Dropping dividers - * entirely is simpler and more resilient than per-pair suppression rules - * that depend on DOM order. - */ -.automation-form-prompt-host .chat-input-toolbar .monaco-action-bar .actions-container > .action-item + .action-item::before, -.automation-form-prompt-host .chat-secondary-input-toolbar .monaco-action-bar .actions-container > .action-item + .action-item::before { - display: none; -} - -/* - * Cancel the dialog's `
            ` indent for the chat input toolbars. - * - * `dialog.css:120` applies `padding-inline-start: 20px` to every `
              ` - * inside `.dialog-message-container` (meant to reduce the excessive - * default indent on bulleted lists in dialog body text). The - * `monaco-action-bar`'s `.actions-container` is rendered as a `
                `, so - * the chat input's primary and secondary toolbars inherit that 20px - * left indent. This pushes the Agent / Copilot CLI chips ~20px right of - * the chat input's bottom-left corner. - * - * `actionbar.css` sets `.actions-container { padding: 0 }`, but the - * dialog rule uses a logical property (`padding-inline-start`), which - * cascades-after the physical shorthand and wins regardless of selector - * specificity. Override the logical property explicitly here. - * - * Scoped to `.automation-form-prompt-host` so the regular chat panel - * (which isn't inside a dialog) is unaffected. Any `
                  ` - * elements in the dialog's actual message body text retain the - * intended 20px indent. - */ -.automation-form-prompt-host .chat-input-toolbar .monaco-action-bar .actions-container, -.automation-form-prompt-host .chat-secondary-toolbar .monaco-action-bar .actions-container { - padding-inline-start: 0; - margin-left: 0; -} - -/* - * Sessions-layer workspace picker visual override. - * - * Two cascading sources need to be defeated to match Mode/Model: - * - * 1. `.sessions-chat-picker-slot.sessions-chat-workspace-picker - * .action-label` (sessions-layer `chatWidget.css:244-269`) sets a - * welcome-flow visual: label 18px, inner span 18px, icons 16px, - * chevron, generous padding, `color: var(--vscode-foreground)`. - * - * 2. `.monaco-action-bar .action-label` (actionbar.css:48) sets the - * base toolbar font-size to **11px**. This is what Mode/Model - * inherit. They don't set font-size themselves. The action bar - * base rule wins for their `.action-label`. The workspace picker's - * own sessions-layer rule (source 1) overrides that base, so we - * need to put the 11px back explicitly. `font-size: inherit` - * does NOT work because none of `.sessions-chat-picker-slot`'s - * ancestors carry an 11px font-size. The 11px lives on sibling - * `.action-label` elements via `.monaco-action-bar .action-label`, - * not on an ancestor we can inherit from. - * - * Result: match the 11px base, neutralize the welcome-flow oversizing, - * keep the Mode/Model color (`var(--vscode-icon-foreground)`), match - * the chip metrics from `.chat-secondary-toolbar .chat-input-picker-item - * .action-label` (chat.css:1728), and hide the chevron (the neighboring - * secondary-toolbar pickers don't render one). - * - * Scoped to `.automation-form-prompt-host` so the welcome view's - * standalone workspace picker keeps its larger sizing. - */ -.automation-form-prompt-host .chat-secondary-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label { - height: 16px; - padding: 3px 6px; - font-size: 11px; - line-height: normal; - color: var(--vscode-icon-foreground); -} - -.automation-form-prompt-host .chat-secondary-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .sessions-chat-dropdown-label { - font-size: inherit; - line-height: inherit; -} - -.automation-form-prompt-host .chat-secondary-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label > .codicon:not(.sessions-chat-dropdown-chevron) { - font-size: 12px; -} - -/* - * The neighboring secondary-toolbar pickers don't render a dropdown - * chevron. They are recognized as chips by their padding + hover. Hide - * the chevron that `_renderTriggerLabel` always appends so the - * workspace chip reads the same way. - */ -.automation-form-prompt-host .chat-secondary-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .sessions-chat-dropdown-chevron { - display: none; -} - -.automation-form-row { - display: flex; - flex-direction: column; - gap: 4px; -} - -.automation-form-row.automation-form-checkbox-row { - flex-direction: row; - align-items: center; - gap: 8px; - margin-top: 2px; - padding-top: 10px; - border-top: 1px solid var(--vscode-widget-border, transparent); -} - -.automation-form-row.automation-form-checkbox-row > .monaco-checkbox { - margin-right: 0; -} - -/* - * Lay the Schedule / Time / Day controls along a single horizontal axis. - * Each control lives in its own `.automation-form-schedule-group` - * (label-above-control flex column). Time and Day join the row only - * when the interval is daily or weekly; flex-wrap keeps the layout - * stable if a translation pushes the row past the dialog width. - */ -.automation-form-row.automation-form-schedule-row { - flex-direction: row; - align-items: flex-end; - flex-wrap: wrap; - gap: 12px; -} - -.automation-form-schedule-group { - display: flex; - flex-direction: column; - gap: 4px; - /* - * Each visible group splits the schedule row equally. Hidden - * groups (`display: none` from `applyIntervalVisibility`) drop out - * of flex distribution, so: - * - manual / hourly → Schedule fills 100% of the row - * - daily → Schedule + Time split 50/50 - * - weekly → Schedule + Time + Day split 33/33/33 - * `min-width: 0` lets the contained SelectBox shrink past its - * intrinsic content width when the row is narrow, before the - * row's `flex-wrap: wrap` kicks in as a final safety net. - */ - flex: 1 1 0; - min-width: 0; -} - -/* SelectBox container sizing. Give Schedule / Time / Day the full - * width of their parent group (which itself flex-shares the row). - * - * `.monaco-select-box` ships with no intrinsic padding or height (the - * only baked-in metrics live in the `.monaco-action-bar .action-item` - * selector, which we're not inside of). Mirror the standalone-form - * pattern used by the settings editor (`settingsEditor2.css:689-694`). - * It uses `height: 26px` and `padding: 2px 6px`. This makes the dropdowns match the - * Name InputBox above them instead of reading as toolbar widgets - * crammed into a form. */ -.automation-form-schedule-select-container { - display: flex; - min-width: 0; - width: 100%; -} - -.automation-form-schedule-select-container .monaco-select-box { - height: 26px; - padding: 2px 8px; -} - -/* - * Form-row labels read as section titles ("Name", "Schedule", "Time", - * "Day of week", "Prompt"). Mirror the established agents - * design-system section-label pattern (see `.overview-section - * .section-label` at line ~734: label1 / fontWeight-medium / - * foreground) so the labels carry visual weight as section headings - * and clearly out-rank their controls. - * - * `margin-bottom` adds breathing room between the title text and - * its control so each form row reads as a discrete section. - */ -.automation-form-label { - font-size: var(--vscode-agents-fontSize-label1); - font-weight: var(--vscode-agents-fontWeight-semiBold); - color: var(--vscode-foreground); - line-height: 1.4; - margin-bottom: 2px; - cursor: default; -} - -.automation-form-hint { - font-size: var(--vscode-agents-fontSize-body2, 11px); - color: var(--vscode-descriptionForeground); - line-height: 1.4; - min-height: 1em; -} - -.automation-form-checkbox-label { - font-size: var(--vscode-agents-fontSize-body1, 13px); - color: var(--vscode-foreground); - cursor: pointer; -} - -/* - * Host for the native `InputBox` widget. The widget owns its own - * `` and styling (border, padding, focus ring); the host just - * lets it grow to the row width. - */ -.automation-form-input-host { - display: flex; - width: 100%; -} - -.automation-form-input-host > .monaco-inputbox { - flex: 1 1 auto; - min-width: 0; -} - -.automation-form-input, -.automation-form-select, -.automation-form-textarea { - font-family: inherit; - font-size: var(--vscode-agents-fontSize-body1, 13px); - color: var(--vscode-settings-textInputForeground, var(--vscode-input-foreground)); - background-color: var(--vscode-settings-textInputBackground, var(--vscode-input-background)); - border: 1px solid var(--vscode-settings-textInputBorder, var(--vscode-input-border, var(--vscode-contrastBorder, transparent))); - border-radius: 2px; - padding: 4px 6px; - box-sizing: border-box; - width: 100%; - min-height: 26px; -} - -.automation-form-select { - color: var(--vscode-settings-dropdownForeground, var(--vscode-dropdown-foreground)); - background-color: var(--vscode-settings-dropdownBackground, var(--vscode-dropdown-background)); - border-color: var(--vscode-settings-dropdownBorder, var(--vscode-dropdown-border, var(--vscode-contrastBorder, transparent))); -} - -.automation-form-textarea { - resize: vertical; - min-height: 60px; - line-height: 1.4; -} - -.automation-form-input:focus, -.automation-form-select:focus, -.automation-form-textarea:focus { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; - border-color: var(--vscode-focusBorder); -} - -.automation-form-empty { - font-size: 12px; - color: var(--vscode-errorForeground); - padding: 6px 0; -} - -/* - * Folder picker. This uses the shared `WorkspacePicker` from the sessions - * layer (see `sessionWorkspacePicker.ts`). The picker renders an - * `` chip trigger inside - * `.sessions-chat-picker-slot.sessions-chat-workspace-picker`. We restyle - * it to fit the dialog's compact, 13px form scale. The upstream styling - * lives in `sessions/contrib/chat/browser/media/chatWidget.css` and is - * sized for the new-session "hero" UI (18px font, larger chip) which is - * too large for our modal. - * - * The dropdown content itself renders through `IActionWidgetService`, - * which has its own global CSS. No overrides needed here. - */ -/* - * Isolation + branch group: parented into the chat input's - * `.chat-secondary-toolbar` so it sits at the bottom-right of the chat - * input, sharing a row with `Copilot CLI | Default Permissions`. - * `margin-left: auto` pushes the group to the right edge within that - * flex row. - * - * Visual styling mirrors the new-session view's - * `.new-chat-bottom-container` chip vocabulary (see - * `sessions/contrib/chat/browser/media/chatWidget.css` lines 178-209): - * compact label text and codicons, icon-foreground color, no border. - */ -.automation-form-prompt-host .chat-secondary-toolbar .automation-form-isolation-group { - display: inline-flex; - align-items: center; - margin-left: auto; - min-width: 0; - gap: var(--vscode-spacing-size60); -} - -.automation-form-prompt-host .automation-form-branch-picker-slot { - display: inline-flex; - min-width: 0; -} - -.automation-form-prompt-host .automation-form-branch-slot { - display: inline-flex; - align-items: center; - gap: var(--vscode-spacing-size40); - height: 16px; - padding: var(--vscode-spacing-size20) var(--vscode-spacing-size60); - font-size: var(--vscode-agents-fontSize-label2); - color: var(--vscode-icon-foreground); - background: transparent; - border: none; - min-width: 0; - border-radius: var(--vscode-cornerRadius-small); - touch-action: manipulation; -} - -.automation-form-prompt-host .automation-form-branch-slot > .codicon { - font-size: var(--vscode-codiconFontSize-compact); - color: var(--vscode-icon-foreground); - flex-shrink: 0; -} - -.automation-form-prompt-host .automation-form-branch-slot:not(.branch-picker-disabled) { - cursor: pointer; -} - -.automation-form-prompt-host .automation-form-branch-slot:not(.branch-picker-disabled):hover { - background-color: var(--vscode-toolbar-hoverBackground); - color: var(--vscode-foreground); -} - -.automation-form-prompt-host .automation-form-branch-slot:focus-visible { - outline: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); - outline-offset: calc(-1 * var(--vscode-strokeThickness)); -} - -.automation-form-prompt-host .automation-form-branch-slot.branch-picker-disabled, -.automation-form-prompt-host .automation-form-branch-slot.branch-picker-disabled > .codicon { - cursor: default; - color: var(--vscode-descriptionForeground); -} - -.automation-form-prompt-host .automation-form-branch-name { - font-size: var(--vscode-agents-fontSize-label2); -} - -.automation-form-prompt-host .automation-form-branch-name { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.automation-form-prompt-host .chat-secondary-toolbar .automation-form-harness-chip { - display: inline-flex; - align-items: center; - gap: 4px; - height: 16px; - padding: 3px 6px; - font-size: 11px; - color: var(--vscode-icon-foreground); - background: transparent; - border: none; - opacity: 0.6; - cursor: default; -} - -.automation-form-prompt-host .automation-form-harness-chip > .codicon { - font-size: 12px; - flex-shrink: 0; -} - -.automation-form-prompt-host .automation-form-harness-label { - font-size: 11px; -} - -/* Missing-branch state (no folder / no git repo / detached / empty): use - * description-foreground + italic so it visually reads as "informational - * placeholder, not a real ref name". */ -.automation-form-prompt-host .automation-form-branch-slot.branch-picker-missing, -.automation-form-prompt-host .automation-form-branch-slot.branch-picker-missing > .codicon { - color: var(--vscode-descriptionForeground); -} - -.automation-form-prompt-host .automation-form-branch-slot.branch-picker-missing .automation-form-branch-name { - font-style: italic; -} - -.hc-black .automation-form-prompt-host .automation-form-branch-slot:focus-visible, -.hc-light .automation-form-prompt-host .automation-form-branch-slot:focus-visible { - outline-color: var(--vscode-contrastActiveBorder); -}