From fbf54ced95148274ff70193864778ded9986cef2 Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Fri, 10 Jul 2026 18:41:18 -0700 Subject: [PATCH] Automations: Bring workspace picker under the chat input (#325397) * Debt: Remove workspacePickerInput from chatInputPart * Remove dead renderTrigger code * Cleanup: Consolidate into a single pickeractionviewitem class * signing commit --- .../automations/browser/automationDialog.ts | 41 +++++++++++++---- .../chat/browser/sessionWorkspacePicker.ts | 28 ++---------- .../browser/actions/chatExecuteActions.ts | 36 --------------- .../media/aiCustomizationManagement.css | 16 +++---- src/vs/workbench/contrib/chat/browser/chat.ts | 17 ------- .../browser/widget/input/chatInputPart.ts | 25 +---------- .../input/workspacePickerInputActionItem.ts | 45 ------------------- 7 files changed, 47 insertions(+), 161 deletions(-) delete mode 100644 src/vs/workbench/contrib/chat/browser/widget/input/workspacePickerInputActionItem.ts diff --git a/src/vs/sessions/contrib/automations/browser/automationDialog.ts b/src/vs/sessions/contrib/automations/browser/automationDialog.ts index 5a6a9819a37..68bfe9c0cf6 100644 --- a/src/vs/sessions/contrib/automations/browser/automationDialog.ts +++ b/src/vs/sessions/contrib/automations/browser/automationDialog.ts @@ -94,6 +94,7 @@ interface IRenderFormHandle { const AUTOMATIONS_HARNESS_CHIP_ACTION_ID = 'workbench.action.chat.renderAutomationsHarnessChip'; +const AUTOMATIONS_WORKSPACE_PICKER_ACTION_ID = 'workbench.action.chat.renderAutomationsWorkspacePicker'; const AUTOMATIONS_ISOLATION_GROUP_ACTION_ID = 'workbench.action.chat.renderAutomationsIsolationGroup'; class AutomationIsolationGroupActionViewItem extends BaseActionViewItem { @@ -263,15 +264,15 @@ class AutomationIsolationGroupActionViewItem extends BaseActionViewItem { } /** - * Hosts the shared {@link SessionTypePicker} inside the chat input's secondary - * toolbar, in the slot previously occupied by the hardcoded harness chip. The + * Renders a dialog-owned picker into a chat input secondary-toolbar slot. The * picker instance is owned by the dialog (registered on its disposables); this - * view item only renders it into the toolbar container. + * view item only injects the picker's DOM into the toolbar container via the + * supplied {@link renderPicker} callback. */ -class AutomationSessionTypePickerActionViewItem extends BaseActionViewItem { +class AutomationPickerActionViewItem extends BaseActionViewItem { constructor( action: IAction, - private readonly picker: MobileSessionTypePicker, + private readonly renderPicker: (container: HTMLElement) => void, options?: IBaseActionViewItemOptions, ) { super(undefined, action, options); @@ -280,7 +281,7 @@ class AutomationSessionTypePickerActionViewItem extends BaseActionViewItem { override render(container: HTMLElement): void { super.render(container); DOM.clearNode(container); - this.picker.render(container); + this.renderPicker(container); } } @@ -303,6 +304,25 @@ registerAction2(class OpenAutomationsHarnessChipAction extends Action2 { override async run(): Promise { /* handled by action view item */ } }); +registerAction2(class OpenAutomationsWorkspacePickerAction extends Action2 { + constructor() { + super({ + id: AUTOMATIONS_WORKSPACE_PICKER_ACTION_ID, + title: localize2('automation.form.workspacePicker.action', "Automations Workspace Picker"), + f1: false, + precondition: ChatContextKeys.enabled, + menu: [{ + id: MenuId.ChatInputSecondary, + group: 'navigation', + order: 0, + when: ChatContextKeys.inAutomationsDialog, + }], + }); + } + + override async run(): Promise { /* handled by action view item */ } +}); + registerAction2(class OpenAutomationsIsolationGroupAction extends Action2 { constructor() { super({ @@ -505,10 +525,15 @@ export function renderForm( // leaving its scrollbar floating ~24px in from the right wall. inputPartHorizontalPadding: 0, sessionTypePickerDelegate: sessionTypeDelegate, - workspacePickerInput: workspacePicker, secondaryToolbarActionViewItemProvider: (action, itemOptions) => { if (action.id === AUTOMATIONS_HARNESS_CHIP_ACTION_ID) { - return new AutomationSessionTypePickerActionViewItem(action, sessionTypePicker, itemOptions); + return new AutomationPickerActionViewItem(action, container => sessionTypePicker.render(container), itemOptions); + } + if (action.id === AUTOMATIONS_WORKSPACE_PICKER_ACTION_ID) { + return new AutomationPickerActionViewItem(action, container => { + container.classList.add('chat-input-picker-item'); + workspacePicker.render(container); + }, itemOptions); } if (action.id === AUTOMATIONS_ISOLATION_GROUP_ACTION_ID) { const actionWidgetService = instantiationService.invokeFunction(accessor => accessor.get(IActionWidgetService)); diff --git a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts index e8a09fc82f4..ce0f852d12c 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts @@ -274,10 +274,8 @@ export class WorkspacePicker extends Disposable { * Renders the project picker trigger button into the given container. * Returns the container element. * - * This is the single-trigger entry point. Calling it again replaces the - * trigger created by the previous {@link render} call. For multi-trigger - * use (e.g. mirroring the same picker into two surfaces) call - * {@link renderTrigger} instead. + * Calling it again replaces the trigger created by the previous + * {@link render} call. */ render(container: HTMLElement): HTMLElement { this._renderDisposables.clear(); @@ -290,26 +288,8 @@ export class WorkspacePicker extends Disposable { } /** - * Adds an additional trigger anchored to {@link container}. Unlike - * {@link render}, calling this does NOT remove triggers from earlier - * calls. Each trigger is independent and disposed via its own returned - * disposable. All live triggers share this picker's selection state and - * receive label updates from {@link _updateTriggerLabel}. - * - * Clicking any trigger anchors the popup to that specific trigger. - */ - renderTrigger(container: HTMLElement): IDisposable { - const slot = dom.append(container, dom.$('.sessions-chat-picker-slot.sessions-chat-workspace-picker')); - const triggerDisposables = new DisposableStore(); - triggerDisposables.add({ dispose: () => slot.remove() }); - triggerDisposables.add(this._addTrigger(slot)); - return triggerDisposables; - } - - /** - * Shared trigger-creation core for both {@link render} and - * {@link renderTrigger}. Wires up the click / keyboard / touch handlers - * and the per-trigger lifecycle. + * Shared trigger-creation core for {@link render}. Wires up the click / + * keyboard / touch handlers and the per-trigger lifecycle. */ private _addTrigger(slot: HTMLElement): IDisposable { const triggerDisposables = new DisposableStore(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 97095307900..e9a42fde550 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -666,41 +666,6 @@ export class OpenWorkspacePickerAction extends Action2 { } } -/** - * Workspace picker chip for the automations dialog. Sits between the mode - * picker (order 1) and the model picker (order 3) in the primary chat input - * toolbar. Visible only when the hosting `ChatInputPart` was constructed with - * a `workspacePickerInput` and the dialog has set - * {@link ChatContextKeys.inAutomationsDialog} on its scoped context-key - * service. - */ -export class OpenAutomationsWorkspacePickerAction extends Action2 { - static readonly ID = 'workbench.action.chat.openAutomationsWorkspacePicker'; - - constructor() { - super({ - id: OpenAutomationsWorkspacePickerAction.ID, - title: localize2('interactive.openAutomationsWorkspacePicker.label', "Open Automations Workspace Picker"), - tooltip: localize('selectAutomationsWorkspace', "Select Workspace Folder"), - category: CHAT_CATEGORY, - f1: false, - precondition: ChatContextKeys.enabled, - menu: [ - { - id: MenuId.ChatInput, - order: 2, - group: 'navigation', - when: ChatContextKeys.inAutomationsDialog, - }, - ] - }); - } - - override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { - // The picker is opened via the action view item's trigger. - } -} - export class ChatSessionPrimaryPickerAction extends Action2 { static readonly ID = 'workbench.action.chat.chatSessionPrimaryPicker'; constructor() { @@ -1267,7 +1232,6 @@ export function registerChatExecuteActions(): DisposableStore { store.add(registerAction2(OpenSessionTargetPickerAction)); store.add(registerAction2(OpenDelegationPickerAction)); store.add(registerAction2(OpenWorkspacePickerAction)); - store.add(registerAction2(OpenAutomationsWorkspacePickerAction)); store.add(registerAction2(ChatSessionPrimaryPickerAction)); store.add(registerAction2(ChangeChatModelAction)); store.add(registerAction2(CancelEdit)); 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 69bc80c32b9..91a13acda9e 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -2529,14 +2529,14 @@ body:has(.automation-dialog) .monaco-menu-container { * * 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-input-toolbar .chat-input-picker-item - * .action-label` (chat.css:1822), and hide the chevron (Mode/Model - * don't render one). + * 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-input-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label { +.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; @@ -2544,22 +2544,22 @@ body:has(.automation-dialog) .monaco-menu-container { color: var(--vscode-icon-foreground); } -.automation-form-prompt-host .chat-input-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .sessions-chat-dropdown-label { +.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-input-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label > .codicon:not(.sessions-chat-dropdown-chevron) { +.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; } /* - * Mode and Model pickers in this toolbar don't render a dropdown + * 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-input-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .sessions-chat-dropdown-chevron { +.automation-form-prompt-host .chat-secondary-toolbar .sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .sessions-chat-dropdown-chevron { display: none; } diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 72ab23ef08d..e2d63973252 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -39,23 +39,6 @@ export interface IWorkspacePickerItem { readonly isFolder: boolean; } -/** - * Narrow contract for a workspace picker hosted as a chip in - * {@link ChatInputPart}'s primary toolbar. Implementers own all selection - * state; the chip is purely a rendering / interaction surface. - * - * Used by the automations dialog so a single picker instance can drive both - * the form-row trigger and a toolbar chip without duplicating state. - */ -export interface IChatInputWorkspacePicker { - /** - * Renders a trigger chip into the given container. The returned disposable - * removes only this trigger; sibling triggers (e.g. the form-row trigger - * in the automations dialog) keep working. - */ - renderTrigger(container: HTMLElement): IDisposable; -} - /** * Delegate interface for the workspace picker. * Allows consumers to get and set the target workspace for chat submissions in empty window contexts. diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index c6ce1c8ae17..a43d582be84 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -101,7 +101,7 @@ import { IChatResponseViewModel, isResponseVM } from '../../../common/model/chat import { IChatAgentService } from '../../../common/participants/chatAgents.js'; import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; import { ChatHistoryNavigator } from '../../../common/widget/chatWidgetHistoryService.js'; -import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenAutomationsWorkspacePickerAction, OpenDelegationPickerAction, OpenModelPickerAction, OpenModePickerAction, OpenPermissionPickerAction, OpenSessionTargetPickerAction, OpenWorkspacePickerAction } from '../../actions/chatExecuteActions.js'; +import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenDelegationPickerAction, OpenModelPickerAction, OpenModePickerAction, OpenPermissionPickerAction, OpenSessionTargetPickerAction, OpenWorkspacePickerAction } from '../../actions/chatExecuteActions.js'; import { AgentSessionProviders, AgentSessionTarget, getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; @@ -109,7 +109,7 @@ import { IChatAttachmentWidgetRegistry } from '../../attachments/chatAttachmentW import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, BrowserViewAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../../attachments/chatAttachmentWidgets.js'; import { ChatImplicitContexts } from '../../attachments/chatImplicitContext.js'; import { ImplicitContextAttachmentWidget } from '../../attachments/implicitContextAttachment.js'; -import { IChatWidget, IChatWidgetViewModelChangeEvent, ISessionTypePickerDelegate, isIChatResourceViewContext, isIChatViewViewContext, IWorkspacePickerDelegate, IChatInputWorkspacePicker } from '../../chat.js'; +import { IChatWidget, IChatWidgetViewModelChangeEvent, ISessionTypePickerDelegate, isIChatResourceViewContext, isIChatViewViewContext, IWorkspacePickerDelegate } from '../../chat.js'; import { ChatEditingShowChangesAction, ViewPreviousEditsAction } from '../../chatEditing/chatEditingActions.js'; import { resizeImage } from '../../chatImageUtils.js'; import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../chatSessions/chatSessionPickerActionItem.js'; @@ -144,7 +144,6 @@ import { IModePickerDelegate, isModeConsideredBuiltIn, ModePickerActionItem } fr import { IPermissionPickerDelegate, PermissionPickerActionItem } from './permissionPickerActionItem.js'; import { SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; import { WorkspacePickerActionItem } from './workspacePickerActionItem.js'; -import { WorkspacePickerInputActionItem } from './workspacePickerInputActionItem.js'; import { ChatContextUsageWidget } from '../../widgetHosts/viewPane/chatContextUsageWidget.js'; import { Target } from '../../../common/promptSyntax/promptTypes.js'; import { findLast } from '../../../../../../base/common/arraysFind.js'; @@ -195,15 +194,6 @@ export interface IChatInputPartOptions { * for their chat request. This is useful for empty window contexts. */ workspacePickerDelegate?: IWorkspacePickerDelegate; - /** - * Optional workspace picker rendered as a chip in the PRIMARY toolbar - * (between the mode picker and the model picker). Used by the automations - * dialog so its existing `WorkspacePicker` instance can drive both the - * form-row trigger and a toolbar chip from a single source of truth. - * Gated by {@link ChatContextKeys.inAutomationsDialog} on the menu side - * and a defensive null-check in `actionViewItemProvider`. - */ - workspacePickerInput?: IChatInputWorkspacePicker; /** * Optional action view item provider for host-owned secondary toolbar * chips registered on {@link MenuId.ChatInputSecondary}. Used by the @@ -3435,17 +3425,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } else if (action.id === OpenModePickerAction.ID && action instanceof MenuItemAction) { const delegate: IModePickerDelegate = this._createModePickerDelegate(); return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate, pickerOptions); - } else if (action.id === OpenAutomationsWorkspacePickerAction.ID && action instanceof MenuItemAction) { - // Toolbar chip for an externally-owned workspace picker - // (today: the automations dialog's single picker instance, - // also rendered in the dialog form row). Defensive guard: - // the menu's `when` clause should already gate visibility, - // but if a host registers the menu contribution without - // supplying the picker we hide rather than throw. - if (this.options.workspacePickerInput) { - return this.instantiationService.createInstance(WorkspacePickerInputActionItem, action, this.options.workspacePickerInput, undefined); - } - return new HiddenActionViewItem(action); } else if ((action.id === OpenSessionTargetPickerAction.ID || action.id === OpenDelegationPickerAction.ID) && action instanceof MenuItemAction) { // Use provided delegate if available, otherwise create default delegate const delegate: ISessionTypePickerDelegate = this.options.sessionTypePickerDelegate ?? { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/workspacePickerInputActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/workspacePickerInputActionItem.ts deleted file mode 100644 index d83ebb8b225..00000000000 --- a/src/vs/workbench/contrib/chat/browser/widget/input/workspacePickerInputActionItem.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { MutableDisposable } from '../../../../../../base/common/lifecycle.js'; -import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; -import { IChatInputWorkspacePicker } from '../../chat.js'; - -/** - * Toolbar chip that delegates rendering to an externally-owned - * {@link IChatInputWorkspacePicker}. The picker owns selection state and - * popup behavior; this view item is a thin lifecycle wrapper so the trigger - * participates in `MenuWorkbenchToolBar` responsive overflow, refresh, and - * disposal. - * - * Used by the automations dialog: its single `AutomationsWorkspacePicker` - * instance renders one trigger into the form row and another into the chat - * input's primary toolbar via this view item. - */ -export class WorkspacePickerInputActionItem extends BaseActionViewItem { - - private readonly _triggerDisposable = this._register(new MutableDisposable()); - - constructor( - action: MenuItemAction, - private readonly picker: IChatInputWorkspacePicker, - options?: IBaseActionViewItemOptions, - ) { - super(undefined, action, options); - } - - override render(container: HTMLElement): void { - super.render(container); - // Add `chat-input-picker-item` so the container picks up the standard - // chat input toolbar chip layout (height, padding, dividers) used by - // the Mode and Model picker neighbors. Visual sizing of the inner - // `.sessions-chat-picker-slot` label/icon is overridden in - // `aiCustomizationManagement.css`. The default sessions-layer - // styling targets a 18px welcome-flow chip, which is too big here. - container.classList.add('chat-input-picker-item'); - this._triggerDisposable.value = this.picker.renderTrigger(container); - } -}