Path: blob/main/src/vs/sessions/contrib/chat/browser/runScriptAction.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { $, addDisposableGenericMouseDownListener, addDisposableListener, append, EventType } from '../../../../base/browser/dom.js';6import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';7import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';8import { Action, IAction } from '../../../../base/common/actions.js';9import { equals } from '../../../../base/common/arrays.js';10import { Codicon } from '../../../../base/common/codicons.js';11import { KeyCode } from '../../../../base/common/keyCodes.js';12import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';13import { autorun, derivedOpts, IObservable } from '../../../../base/common/observable.js';14import { ThemeIcon } from '../../../../base/common/themables.js';15import { localize, localize2 } from '../../../../nls.js';16import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';17import { ActionWidgetDropdownActionViewItem } from '../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js';18import { MenuId, registerAction2, Action2, MenuRegistry, SubmenuItemAction } from '../../../../platform/actions/common/actions.js';19import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';20import { IActionWidgetDropdownAction } from '../../../../platform/actionWidget/browser/actionWidgetDropdown.js';21import { ICommandService } from '../../../../platform/commands/common/commands.js';22import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';23import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';24import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';25import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';26import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';27import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js';28import { logSessionsInteraction } from '../../../common/sessionsTelemetry.js';29import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';30import { SessionsCategories } from '../../../common/categories.js';31import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';32import { IsActiveSessionBackgroundProviderContext, SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js';33import { ISession } from '../../../services/sessions/common/session.js';34import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js';35import { Menus } from '../../../browser/menus.js';36import { INonSessionTaskEntry, ISessionsConfigurationService, ISessionTaskWithTarget, ITaskEntry, TaskStorageTarget } from './sessionsConfigurationService.js';37import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js';38import { IRunScriptCustomTaskWidgetResult, RunScriptCustomTaskWidget } from './runScriptCustomTaskWidget.js';394041// Menu IDs - exported for use in auxiliary bar part42export const RunScriptDropdownMenuId = MenuId.for('AgentSessionsRunScriptDropdown');43const RUN_SCRIPT_ACTION_MODAL_VISIBLE_CLASS = 'run-script-action-modal-visible';4445// Action IDs46const RUN_SCRIPT_ACTION_PRIMARY_ID = 'workbench.action.agentSessions.runScriptPrimary';47const CONFIGURE_DEFAULT_RUN_ACTION_ID = 'workbench.action.agentSessions.configureDefaultRunAction';48const GENERATE_RUN_ACTION_ID = 'workbench.action.agentSessions.generateRunAction';49const closeQuickWidgetButton: IQuickInputButton = {50iconClass: ThemeIcon.asClassName(Codicon.close),51tooltip: localize('closeQuickWidget', "Close"),52alwaysVisible: true,53};5455function getTaskDisplayLabel(task: ITaskEntry): string {56if (task.label && task.label.length > 0) {57return task.label;58}59if (task.script && task.script.length > 0) {60return task.script;61}62if (task.command && task.command.length > 0) {63return task.command;64}65if (task.task && task.task.toString().length > 0) {66return task.task.toString();67}68return '';69}7071function getTaskCommandPreview(task: ITaskEntry): string {72if (task.command && task.command.length > 0) {73return task.command;74}75if (task.script && task.script.length > 0) {76return localize('npmTaskCommandPreview', "npm run {0}", task.script);77}78if (task.task && task.task.toString().length > 0) {79return task.task.toString();80}81return getTaskDisplayLabel(task);82}8384function getPrimaryTask(tasks: readonly ISessionTaskWithTarget[], pinnedTaskLabel: string | undefined): ISessionTaskWithTarget | undefined {85if (tasks.length === 0) {86return undefined;87}8889if (pinnedTaskLabel) {90const pinnedTask = tasks.find(task => task.task.label === pinnedTaskLabel);91if (pinnedTask) {92return pinnedTask;93}94}9596return tasks[0];97}9899interface IRunScriptActionContext {100readonly session: ISession;101readonly tasks: readonly ISessionTaskWithTarget[];102readonly pinnedTaskLabel: string | undefined;103}104105type TaskConfigurationMode = 'add' | 'configure';106107/**108* Workbench contribution that adds a split dropdown action to the auxiliary bar title109* for running a task via tasks.json.110*/111export class RunScriptContribution extends Disposable implements IWorkbenchContribution {112113static readonly ID = 'workbench.contrib.agentSessions.runScript';114115private readonly _activeRunState: IObservable<IRunScriptActionContext | undefined>;116117constructor(118@ISessionsManagementService private readonly _sessionManagementService: ISessionsManagementService,119@IKeybindingService _keybindingService: IKeybindingService,120@IQuickInputService private readonly _quickInputService: IQuickInputService,121@ISessionsConfigurationService private readonly _sessionsConfigService: ISessionsConfigurationService,122@IActionViewItemService private readonly _actionViewItemService: IActionViewItemService,123@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService,124@ITelemetryService private readonly _telemetryService: ITelemetryService,125@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,126) {127super();128129this._activeRunState = derivedOpts<IRunScriptActionContext | undefined>({130owner: this,131equalsFn: (a, b) => {132if (a === b) { return true; }133if (!a || !b) { return false; }134return a.session === b.session135&& a.pinnedTaskLabel === b.pinnedTaskLabel136&& equals(a.tasks, b.tasks, (t1, t2) =>137t1.task.label === t2.task.label138&& t1.task.command === t2.task.command139&& t1.target === t2.target140&& t1.task.runOptions?.runOn === t2.task.runOptions?.runOn);141}142}, reader => {143const activeSession = this._sessionManagementService.activeSession.read(reader);144if (!activeSession) {145return undefined;146}147148const tasks = this._sessionsConfigService.getSessionTasks(activeSession).read(reader);149const repo = activeSession.workspace.read(reader)?.repositories[0];150const pinnedTaskLabel = this._sessionsConfigService.getPinnedTaskLabel(repo?.uri).read(reader);151return { session: activeSession, tasks, pinnedTaskLabel };152}).recomputeInitiallyAndOnChange(this._store);153154this._registerActionViewItemProvider();155this._registerActions();156}157158private _registerActionViewItemProvider(): void {159const that = this;160this._register(this._actionViewItemService.register(161Menus.TitleBarSessionMenu,162RunScriptDropdownMenuId,163(action, options, instantiationService) => {164if (!(action instanceof SubmenuItemAction)) {165return undefined;166}167return instantiationService.createInstance(168RunScriptActionViewItem,169action,170options,171that._activeRunState,172(session: ISession) => that._showConfigureQuickPick(session),173(session: ISession, existingTask: INonSessionTaskEntry, mode?: TaskConfigurationMode) => that._showCustomCommandInput(session, existingTask, mode),174(session: ISession) => that._generateNewTask(session),175);176},177));178}179180private _registerActions(): void {181const that = this;182183this._register(registerAction2(class extends Action2 {184constructor() {185super({186id: RUN_SCRIPT_ACTION_PRIMARY_ID,187title: { value: localize('runPrimaryTask', 'Run Primary Task'), original: 'Run Primary Task' },188icon: Codicon.play,189category: SessionsCategories.Sessions,190f1: true,191});192}193194async run(): Promise<void> {195const activeState = that._activeRunState.get();196if (!activeState) {197return;198}199200logSessionsInteraction(that._telemetryService, 'runPrimaryTask');201202const { tasks, session } = activeState;203if (tasks.length === 0) {204const task = await that._showConfigureQuickPick(session);205if (task) {206await that._sessionsConfigService.runTask(task, session);207}208return;209}210211const primaryTask = getPrimaryTask(tasks, activeState.pinnedTaskLabel);212if (!primaryTask) {213return;214}215await that._sessionsConfigService.runTask(primaryTask.task, session);216}217}));218219this._register(autorun(reader => {220const activeState = this._activeRunState.read(reader);221if (!activeState) {222return;223}224225const { session, tasks } = activeState;226const repo = session.workspace.read(reader)?.repositories[0];227const configureScriptPrecondition = repo?.workingDirectory ?? repo?.uri ? ContextKeyExpr.true() : ContextKeyExpr.false();228229reader.store.add(registerAction2(class extends Action2 {230constructor() {231super({232id: CONFIGURE_DEFAULT_RUN_ACTION_ID,233title: localize2('configureDefaultRunAction', "Add Task..."),234category: SessionsCategories.Sessions,235icon: Codicon.add,236precondition: configureScriptPrecondition,237menu: [{238id: RunScriptDropdownMenuId,239group: tasks.length === 0 ? 'navigation' : '1_configure',240order: 0241}]242});243}244245async run(): Promise<void> {246logSessionsInteraction(that._telemetryService, 'addTask', 'menu');247const task = await that._showConfigureQuickPick(session);248if (task) {249await that._sessionsConfigService.runTask(task, session);250}251}252}));253254reader.store.add(registerAction2(class extends Action2 {255constructor() {256super({257id: GENERATE_RUN_ACTION_ID,258title: localize2('generateRunAction', "Generate New Task..."),259category: SessionsCategories.Sessions,260precondition: IsActiveSessionBackgroundProviderContext,261menu: [{262id: RunScriptDropdownMenuId,263group: tasks.length === 0 ? 'navigation' : '1_configure',264order: 1265}]266});267}268269async run(): Promise<void> {270logSessionsInteraction(that._telemetryService, 'generateNewTask', 'menu');271await that._generateNewTask(session);272}273}));274}));275}276277private async _generateNewTask(session: ISession): Promise<void> {278const query = '/generate-run-commands';279// Prefer sending to the already-open chat widget for the session;280// fall back to sendAndCreateChat for untitled sessions or when no widget is loaded.281const widget = this._chatWidgetService.getWidgetBySessionResource(session.mainChat.resource);282if (widget) {283await widget.acceptInput(query);284} else {285await this._sessionManagementService.sendAndCreateChat(session, { query });286}287}288289private async _showConfigureQuickPick(session: ISession): Promise<ITaskEntry | undefined> {290const nonSessionTasks = await this._sessionsConfigService.getNonSessionTasks(session);291if (nonSessionTasks.length === 0) {292// No existing tasks, go straight to custom command input293return this._showCustomCommandInput(session);294}295296interface ITaskPickItem extends IQuickPickItem {297readonly task?: ITaskEntry;298readonly source?: TaskStorageTarget;299}300301const items: (ITaskPickItem | IQuickPickSeparator)[] = [];302303items.push({ type: 'separator', label: localize('custom', "Custom") });304items.push({305label: localize('createNewTask', "Create new task..."),306description: localize('enterCustomCommandDesc', "Create a new shell task"),307});308309if (nonSessionTasks.length > 0) {310items.push({ type: 'separator', label: localize('existingTasks', "Existing Tasks") });311for (const { task, target } of nonSessionTasks) {312items.push({313label: getTaskDisplayLabel(task),314description: task.command,315task,316source: target,317});318}319}320321const picked = await this._quickInputService.pick(items, {322placeHolder: localize('pickRunAction', "Select or create a task"),323});324325if (!picked) {326return undefined;327}328329const pickedItem = picked as ITaskPickItem;330if (pickedItem.task) {331return this._showCustomCommandInput(session, { task: pickedItem.task, target: pickedItem.source ?? 'workspace' }, 'add', true);332} else {333// Custom command path334return this._showCustomCommandInput(session, undefined, 'add', true);335}336}337338private async _showCustomCommandInput(session: ISession, existingTask?: INonSessionTaskEntry, mode: TaskConfigurationMode = 'add', allowBackNavigation = false): Promise<ITaskEntry | undefined> {339const taskConfiguration = await this._showCustomCommandWidget(session, existingTask, mode, allowBackNavigation);340if (!taskConfiguration) {341return undefined;342}343if (taskConfiguration === 'back') {344return this._showConfigureQuickPick(session);345}346347if (existingTask) {348if (mode === 'configure') {349const newLabel = taskConfiguration.label?.trim() || existingTask.task.label || taskConfiguration.command;350351let updatedTask: ITaskEntry = {352...existingTask.task,353label: newLabel,354inAgents: true,355};356357if (taskConfiguration.command && existingTask.task.command !== undefined) {358updatedTask = {359...updatedTask,360command: taskConfiguration.command,361};362}363364if (taskConfiguration.runOn) {365updatedTask = {366...updatedTask,367runOptions: {368...(existingTask.task.runOptions ?? {}),369runOn: taskConfiguration.runOn,370},371};372}373374await this._sessionsConfigService.updateTask(existingTask.task.label, updatedTask, session, existingTask.target, taskConfiguration.target);375return updatedTask;376}377378await this._sessionsConfigService.addTaskToSessions(existingTask.task, session, existingTask.target, { runOn: taskConfiguration.runOn ?? 'default' });379return {380...existingTask.task,381inAgents: true,382...(taskConfiguration.runOn ? { runOptions: { runOn: taskConfiguration.runOn } } : {}),383};384}385386return this._sessionsConfigService.createAndAddTask(387taskConfiguration.label,388taskConfiguration.command,389session,390taskConfiguration.target,391taskConfiguration.runOn ? { runOn: taskConfiguration.runOn } : undefined392);393}394395private _showCustomCommandWidget(session: ISession, existingTask?: INonSessionTaskEntry, mode: TaskConfigurationMode = 'add', allowBackNavigation = false): Promise<IRunScriptCustomTaskWidgetResult | 'back' | undefined> {396const repo = session.workspace.get()?.repositories[0];397const workspaceTargetDisabledReason = !(repo?.workingDirectory ?? repo?.uri)398? localize('workspaceStorageUnavailableTooltip', "Workspace storage is unavailable for this session")399: undefined;400const isConfigureMode = mode === 'configure';401402return new Promise<IRunScriptCustomTaskWidgetResult | 'back' | undefined>(resolve => {403const disposables = new DisposableStore();404let settled = false;405406const quickWidget = disposables.add(this._quickInputService.createQuickWidget());407quickWidget.title = isConfigureMode408? localize('configureActionWidgetTitle', "Configure Task")409: existingTask410? localize('addExistingActionWidgetTitle', "Add Existing Task")411: localize('addActionWidgetTitle', "Add Task");412quickWidget.description = isConfigureMode413? localize('configureActionWidgetDescription', "Update how this task is named, saved, and run.")414: existingTask415? localize('addExistingActionWidgetDescription', "Enable an existing task for sessions and configure when it should run.")416: localize('addActionWidgetDescription', "Create a shell task and configure how it should be saved and run.");417quickWidget.ignoreFocusOut = true;418quickWidget.buttons = allowBackNavigation419? [this._quickInputService.backButton, closeQuickWidgetButton]420: [closeQuickWidgetButton];421const widget = disposables.add(new RunScriptCustomTaskWidget({422label: existingTask?.task.label,423labelDisabledReason: existingTask && !isConfigureMode ? localize('existingTaskLabelLocked', "This name comes from an existing task and cannot be changed here.") : undefined,424command: existingTask ? getTaskCommandPreview(existingTask.task) : undefined,425commandDisabledReason: existingTask && !isConfigureMode ? localize('existingTaskCommandLocked', "This command comes from an existing task and cannot be changed here.") : undefined,426target: existingTask?.target,427targetDisabledReason: existingTask && !isConfigureMode ? localize('existingTaskTargetLocked', "This existing task cannot be moved between workspace and user storage.") : workspaceTargetDisabledReason,428runOn: existingTask?.task.runOptions?.runOn === 'worktreeCreated' ? 'worktreeCreated' : undefined,429mode: isConfigureMode ? 'configure' : existingTask ? 'add-existing' : 'add',430}));431quickWidget.widget = widget.domNode;432this._layoutService.mainContainer.classList.add(RUN_SCRIPT_ACTION_MODAL_VISIBLE_CLASS);433const backdrop = append(this._layoutService.mainContainer, $('.run-script-action-modal-backdrop'));434disposables.add(addDisposableGenericMouseDownListener(backdrop, e => {435e.preventDefault();436e.stopPropagation();437complete(undefined);438}));439disposables.add({ dispose: () => backdrop.remove() });440disposables.add({ dispose: () => this._layoutService.mainContainer.classList.remove(RUN_SCRIPT_ACTION_MODAL_VISIBLE_CLASS) });441442const complete = (result: IRunScriptCustomTaskWidgetResult | undefined) => {443if (settled) {444return;445}446settled = true;447resolve(result);448quickWidget.hide();449};450451disposables.add(widget.onDidSubmit(result => complete(result)));452disposables.add(widget.onDidCancel(() => complete(undefined)));453disposables.add(quickWidget.onDidTriggerButton(button => {454if (allowBackNavigation && button === this._quickInputService.backButton) {455settled = true;456resolve('back');457quickWidget.hide();458return;459}460if (button === closeQuickWidgetButton) {461complete(undefined);462}463}));464disposables.add(quickWidget.onDidHide(() => {465if (!settled) {466settled = true;467resolve(undefined);468}469disposables.dispose();470}));471472quickWidget.show();473widget.focus();474});475}476}477478/**479* Split-button action view item for the run script picker in the sessions titlebar.480* The primary button runs the pinned task, or the first task if none is pinned.481* The dropdown arrow opens a custom action widget with categories and per-item482* toolbar actions (pin, configure, remove).483*/484class RunScriptActionViewItem extends BaseActionViewItem {485486private readonly _primaryActionAction: Action;487private readonly _primaryAction: ActionViewItem;488private readonly _dropdown: ChevronActionWidgetDropdown;489490constructor(491action: IAction,492_options: IActionViewItemOptions,493private readonly _activeRunState: IObservable<IRunScriptActionContext | undefined>,494private readonly _showConfigureQuickPick: (session: ISession) => Promise<ITaskEntry | undefined>,495private readonly _showCustomCommandInput: (session: ISession, existingTask: INonSessionTaskEntry, mode?: TaskConfigurationMode) => Promise<ITaskEntry | undefined>,496private readonly _generateNewTask: (session: ISession) => Promise<void>,497@ICommandService private readonly _commandService: ICommandService,498@ISessionsConfigurationService private readonly _sessionsConfigService: ISessionsConfigurationService,499@IKeybindingService private readonly _keybindingService: IKeybindingService,500@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService,501@IContextKeyService contextKeyService: IContextKeyService,502@ITelemetryService private readonly _telemetryService: ITelemetryService,503) {504super(undefined, action);505506const state = this._activeRunState.get();507const hasTasks = state && state.tasks.length > 0;508509// Primary action button - runs the pinned task (or first task when none is pinned)510this._primaryActionAction = this._register(new Action(511'agentSessions.runScriptPrimary',512this._getPrimaryActionTooltip(state),513ThemeIcon.asClassName(Codicon.play),514hasTasks,515() => this._commandService.executeCommand(RUN_SCRIPT_ACTION_PRIMARY_ID)516));517this._primaryAction = this._register(new ActionViewItem(undefined, this._primaryActionAction, { icon: true, label: false }));518519// Update enabled state when tasks change520this._register(autorun(reader => {521const runState = this._activeRunState.read(reader);522this._primaryActionAction.enabled = !!runState && runState.tasks.length > 0;523this._primaryActionAction.label = this._getPrimaryActionTooltip(runState);524}));525526// Dropdown with categorized task actions and per-item toolbars527const dropdownAction = this._register(new Action('agentSessions.runScriptDropdown', localize('runDropdown', "More Tasks...")));528this._dropdown = this._register(new ChevronActionWidgetDropdown(529dropdownAction,530{531actionProvider: { getActions: () => this._getDropdownActions() },532showItemKeybindings: true,533},534this._actionWidgetService,535this._keybindingService,536contextKeyService,537this._telemetryService,538));539}540541override render(container: HTMLElement): void {542super.render(container);543container.classList.add('monaco-dropdown-with-default');544545// Primary action button546const primaryContainer = $('.action-container');547this._primaryAction.render(append(container, primaryContainer));548this._register(addDisposableListener(primaryContainer, EventType.KEY_DOWN, (e: KeyboardEvent) => {549const event = new StandardKeyboardEvent(e);550if (event.equals(KeyCode.RightArrow)) {551this._primaryAction.blur();552this._dropdown.focus();553event.stopPropagation();554}555}));556557// Dropdown arrow button558const dropdownContainer = $('.dropdown-action-container');559this._dropdown.render(append(container, dropdownContainer));560this._register(addDisposableListener(dropdownContainer, EventType.KEY_DOWN, (e: KeyboardEvent) => {561const event = new StandardKeyboardEvent(e);562if (event.equals(KeyCode.LeftArrow)) {563this._dropdown.setFocusable(false);564this._primaryAction.focus();565event.stopPropagation();566}567}));568}569570override focus(fromRight?: boolean): void {571if (fromRight) {572this._dropdown.focus();573} else {574this._primaryAction.focus();575}576}577578override blur(): void {579this._primaryAction.blur();580this._dropdown.blur();581}582583override setFocusable(focusable: boolean): void {584this._primaryAction.setFocusable(focusable);585if (!focusable) {586this._dropdown.setFocusable(false);587}588}589590private _getPrimaryActionTooltip(state: IRunScriptActionContext | undefined): string {591if (!state || state.tasks.length === 0) {592return localize('runPrimaryTaskTooltip', "Run Primary Task");593}594595const primaryTask = getPrimaryTask(state.tasks, state.pinnedTaskLabel)?.task;596if (!primaryTask) {597return localize('runPrimaryTaskTooltip', "Run Primary Task");598}599600const keybindingLabel = this._keybindingService.lookupKeybinding(RUN_SCRIPT_ACTION_PRIMARY_ID)?.getLabel();601return keybindingLabel602? localize('runActionTooltipKeybinding', "{0} ({1})", getTaskDisplayLabel(primaryTask), keybindingLabel)603: getTaskDisplayLabel(primaryTask);604}605606private _getDropdownActions(): IActionWidgetDropdownAction[] {607const state = this._activeRunState.get();608if (!state) {609return [];610}611612const { tasks, session, pinnedTaskLabel } = state;613const repo = session.workspace.get()?.repositories[0];614const actions: IActionWidgetDropdownAction[] = [];615616// Category for normal tasks (no header shown)617const defaultCategory = { label: '', order: 0, showHeader: false };618// Category for worktree-creation tasks619const worktreeCategory = { label: localize('worktreeCreationCategory', "Run on Worktree Creation"), order: 1, showHeader: true };620// Category for task creation and management621const tasksCategory = { label: localize('tasksActionsCategory', "Tasks"), order: 2, showHeader: true };622623for (let i = 0; i < tasks.length; i++) {624const entry = tasks[i];625const task = entry.task;626const isWorktreeTask = task.runOptions?.runOn === 'worktreeCreated';627const isPinned = task.label === pinnedTaskLabel;628629const toolbarActions: IAction[] = [630{631id: `runScript.pin.${i}`,632label: isPinned ? localize('unpinTask', "Unpin") : localize('pinTask', "Pin"),633tooltip: isPinned ? localize('unpinTaskTooltip', "Unpin") : localize('pinTaskTooltip', "Pin"),634class: ThemeIcon.asClassName(isPinned ? Codicon.pinned : Codicon.pin),635enabled: !!repo?.uri,636run: async () => {637this._actionWidgetService.hide();638this._sessionsConfigService.setPinnedTaskLabel(repo?.uri, isPinned ? undefined : task.label);639}640},641{642id: `runScript.configure.${i}`,643label: localize('configureTask', "Configure"),644tooltip: localize('configureTask', "Configure"),645class: ThemeIcon.asClassName(Codicon.gear),646enabled: true,647run: async () => {648this._actionWidgetService.hide();649await this._showCustomCommandInput(session, { task, target: entry.target }, 'configure');650}651},652{653id: `runScript.remove.${i}`,654label: localize('removeTask', "Remove"),655tooltip: localize('removeTask', "Remove"),656class: ThemeIcon.asClassName(Codicon.close),657enabled: true,658run: async () => {659this._actionWidgetService.hide();660await this._sessionsConfigService.removeTask(task.label, session, entry.target);661}662}663];664665actions.push({666id: `runScript.task.${i}`,667label: getTaskDisplayLabel(task),668tooltip: '',669hover: {670content: localize('runActionTooltip', "Run '{0}' in terminal", getTaskDisplayLabel(task)),671},672icon: Codicon.play,673enabled: true,674class: undefined,675category: isWorktreeTask ? worktreeCategory : defaultCategory,676toolbarActions,677run: async () => {678await this._sessionsConfigService.runTask(task, session);679},680});681}682683// "Add Task..." action684const canConfigure = !!(repo?.workingDirectory ?? repo?.uri);685actions.push({686id: 'runScript.addAction',687label: localize('configureDefaultRunAction', "Add Task..."),688tooltip: '',689hover: {690content: canConfigure691? localize('addActionTooltip', "Add a new task")692: localize('addActionTooltipDisabled', "Cannot add tasks to this session because workspace storage is unavailable"),693},694icon: Codicon.add,695enabled: canConfigure,696class: undefined,697category: tasksCategory,698run: async () => {699logSessionsInteraction(this._telemetryService, 'addTask', 'actionWidget');700const task = await this._showConfigureQuickPick(session);701if (task) {702await this._sessionsConfigService.runTask(task, session);703}704},705});706707// "Generate New Task..." action708actions.push({709id: 'runScript.generateAction',710label: localize('generateRunAction', "Generate New Task..."),711tooltip: '',712hover: {713content: localize('generateRunActionTooltip', "Generate a new workspace task"),714},715icon: Codicon.sparkle,716enabled: true,717class: undefined,718category: tasksCategory,719run: async () => {720logSessionsInteraction(this._telemetryService, 'generateNewTask', 'actionWidget');721await this._generateNewTask(session);722},723});724725return actions;726}727}728729/**730* {@link ActionWidgetDropdownActionViewItem} that renders a chevron-down icon731* for the split button dropdown in the titlebar.732*/733class ChevronActionWidgetDropdown extends ActionWidgetDropdownActionViewItem {734protected override renderLabel(element: HTMLElement): IDisposable | null {735element.classList.add('codicon', 'codicon-chevron-down');736return null;737}738}739740// Register the Run split button submenu on the workbench title bar (background sessions only)741MenuRegistry.appendMenuItem(Menus.TitleBarSessionMenu, {742submenu: RunScriptDropdownMenuId,743isSplitButton: true,744title: localize2('run', "Run"),745icon: Codicon.play,746group: 'navigation',747order: 8,748when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext)749});750751// Disabled placeholder shown in the titlebar when the active session does not support running scripts752class RunScriptNotAvailableAction extends Action2 {753constructor() {754super({755id: 'workbench.action.agentSessions.runScript.notAvailable',756title: localize2('run', "Run"),757tooltip: localize('runScriptNotAvailableTooltip', "Run Task is not available for this session type"),758icon: Codicon.play,759precondition: ContextKeyExpr.false(),760menu: [{761id: Menus.TitleBarSessionMenu,762group: 'navigation',763order: 8,764when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext.toNegated())765}]766});767}768769override run(): void { }770}771772registerAction2(RunScriptNotAvailableAction);773774// Register F5 keybinding at module level to ensure it's in the registry775// before the keybinding resolver is cached. The command handler is776// registered later by RunScriptContribution.777KeybindingsRegistry.registerKeybindingRule({778id: RUN_SCRIPT_ACTION_PRIMARY_ID,779primary: KeyCode.F5,780weight: KeybindingWeight.WorkbenchContrib + 100,781when: IsAuxiliaryWindowContext.toNegated()782});783784785