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
This commit is contained in:
Ben Villalobos
2026-07-10 18:41:18 -07:00
committed by GitHub
parent 2b8fc296f2
commit fbf54ced95
7 changed files with 47 additions and 161 deletions
@@ -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<void> { /* 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<void> { /* 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));
@@ -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();
@@ -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<void> {
// 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));
@@ -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;
}
@@ -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.
@@ -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 ?? {
@@ -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);
}
}