Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/chat/browser/runScriptAction.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { $, addDisposableGenericMouseDownListener, addDisposableListener, append, EventType } from '../../../../base/browser/dom.js';
7
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
8
import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
9
import { Action, IAction } from '../../../../base/common/actions.js';
10
import { equals } from '../../../../base/common/arrays.js';
11
import { Codicon } from '../../../../base/common/codicons.js';
12
import { KeyCode } from '../../../../base/common/keyCodes.js';
13
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
14
import { autorun, derivedOpts, IObservable } from '../../../../base/common/observable.js';
15
import { ThemeIcon } from '../../../../base/common/themables.js';
16
import { localize, localize2 } from '../../../../nls.js';
17
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
18
import { ActionWidgetDropdownActionViewItem } from '../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js';
19
import { MenuId, registerAction2, Action2, MenuRegistry, SubmenuItemAction } from '../../../../platform/actions/common/actions.js';
20
import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';
21
import { IActionWidgetDropdownAction } from '../../../../platform/actionWidget/browser/actionWidgetDropdown.js';
22
import { ICommandService } from '../../../../platform/commands/common/commands.js';
23
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
24
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
25
import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
26
import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
27
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
28
import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js';
29
import { logSessionsInteraction } from '../../../common/sessionsTelemetry.js';
30
import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';
31
import { SessionsCategories } from '../../../common/categories.js';
32
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
33
import { IsActiveSessionBackgroundProviderContext, SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js';
34
import { ISession } from '../../../services/sessions/common/session.js';
35
import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js';
36
import { Menus } from '../../../browser/menus.js';
37
import { INonSessionTaskEntry, ISessionsConfigurationService, ISessionTaskWithTarget, ITaskEntry, TaskStorageTarget } from './sessionsConfigurationService.js';
38
import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js';
39
import { IRunScriptCustomTaskWidgetResult, RunScriptCustomTaskWidget } from './runScriptCustomTaskWidget.js';
40
41
42
// Menu IDs - exported for use in auxiliary bar part
43
export const RunScriptDropdownMenuId = MenuId.for('AgentSessionsRunScriptDropdown');
44
const RUN_SCRIPT_ACTION_MODAL_VISIBLE_CLASS = 'run-script-action-modal-visible';
45
46
// Action IDs
47
const RUN_SCRIPT_ACTION_PRIMARY_ID = 'workbench.action.agentSessions.runScriptPrimary';
48
const CONFIGURE_DEFAULT_RUN_ACTION_ID = 'workbench.action.agentSessions.configureDefaultRunAction';
49
const GENERATE_RUN_ACTION_ID = 'workbench.action.agentSessions.generateRunAction';
50
const closeQuickWidgetButton: IQuickInputButton = {
51
iconClass: ThemeIcon.asClassName(Codicon.close),
52
tooltip: localize('closeQuickWidget', "Close"),
53
alwaysVisible: true,
54
};
55
56
function getTaskDisplayLabel(task: ITaskEntry): string {
57
if (task.label && task.label.length > 0) {
58
return task.label;
59
}
60
if (task.script && task.script.length > 0) {
61
return task.script;
62
}
63
if (task.command && task.command.length > 0) {
64
return task.command;
65
}
66
if (task.task && task.task.toString().length > 0) {
67
return task.task.toString();
68
}
69
return '';
70
}
71
72
function getTaskCommandPreview(task: ITaskEntry): string {
73
if (task.command && task.command.length > 0) {
74
return task.command;
75
}
76
if (task.script && task.script.length > 0) {
77
return localize('npmTaskCommandPreview', "npm run {0}", task.script);
78
}
79
if (task.task && task.task.toString().length > 0) {
80
return task.task.toString();
81
}
82
return getTaskDisplayLabel(task);
83
}
84
85
function getPrimaryTask(tasks: readonly ISessionTaskWithTarget[], pinnedTaskLabel: string | undefined): ISessionTaskWithTarget | undefined {
86
if (tasks.length === 0) {
87
return undefined;
88
}
89
90
if (pinnedTaskLabel) {
91
const pinnedTask = tasks.find(task => task.task.label === pinnedTaskLabel);
92
if (pinnedTask) {
93
return pinnedTask;
94
}
95
}
96
97
return tasks[0];
98
}
99
100
interface IRunScriptActionContext {
101
readonly session: ISession;
102
readonly tasks: readonly ISessionTaskWithTarget[];
103
readonly pinnedTaskLabel: string | undefined;
104
}
105
106
type TaskConfigurationMode = 'add' | 'configure';
107
108
/**
109
* Workbench contribution that adds a split dropdown action to the auxiliary bar title
110
* for running a task via tasks.json.
111
*/
112
export class RunScriptContribution extends Disposable implements IWorkbenchContribution {
113
114
static readonly ID = 'workbench.contrib.agentSessions.runScript';
115
116
private readonly _activeRunState: IObservable<IRunScriptActionContext | undefined>;
117
118
constructor(
119
@ISessionsManagementService private readonly _sessionManagementService: ISessionsManagementService,
120
@IKeybindingService _keybindingService: IKeybindingService,
121
@IQuickInputService private readonly _quickInputService: IQuickInputService,
122
@ISessionsConfigurationService private readonly _sessionsConfigService: ISessionsConfigurationService,
123
@IActionViewItemService private readonly _actionViewItemService: IActionViewItemService,
124
@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService,
125
@ITelemetryService private readonly _telemetryService: ITelemetryService,
126
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
127
) {
128
super();
129
130
this._activeRunState = derivedOpts<IRunScriptActionContext | undefined>({
131
owner: this,
132
equalsFn: (a, b) => {
133
if (a === b) { return true; }
134
if (!a || !b) { return false; }
135
return a.session === b.session
136
&& a.pinnedTaskLabel === b.pinnedTaskLabel
137
&& equals(a.tasks, b.tasks, (t1, t2) =>
138
t1.task.label === t2.task.label
139
&& t1.task.command === t2.task.command
140
&& t1.target === t2.target
141
&& t1.task.runOptions?.runOn === t2.task.runOptions?.runOn);
142
}
143
}, reader => {
144
const activeSession = this._sessionManagementService.activeSession.read(reader);
145
if (!activeSession) {
146
return undefined;
147
}
148
149
const tasks = this._sessionsConfigService.getSessionTasks(activeSession).read(reader);
150
const repo = activeSession.workspace.read(reader)?.repositories[0];
151
const pinnedTaskLabel = this._sessionsConfigService.getPinnedTaskLabel(repo?.uri).read(reader);
152
return { session: activeSession, tasks, pinnedTaskLabel };
153
}).recomputeInitiallyAndOnChange(this._store);
154
155
this._registerActionViewItemProvider();
156
this._registerActions();
157
}
158
159
private _registerActionViewItemProvider(): void {
160
const that = this;
161
this._register(this._actionViewItemService.register(
162
Menus.TitleBarSessionMenu,
163
RunScriptDropdownMenuId,
164
(action, options, instantiationService) => {
165
if (!(action instanceof SubmenuItemAction)) {
166
return undefined;
167
}
168
return instantiationService.createInstance(
169
RunScriptActionViewItem,
170
action,
171
options,
172
that._activeRunState,
173
(session: ISession) => that._showConfigureQuickPick(session),
174
(session: ISession, existingTask: INonSessionTaskEntry, mode?: TaskConfigurationMode) => that._showCustomCommandInput(session, existingTask, mode),
175
(session: ISession) => that._generateNewTask(session),
176
);
177
},
178
));
179
}
180
181
private _registerActions(): void {
182
const that = this;
183
184
this._register(registerAction2(class extends Action2 {
185
constructor() {
186
super({
187
id: RUN_SCRIPT_ACTION_PRIMARY_ID,
188
title: { value: localize('runPrimaryTask', 'Run Primary Task'), original: 'Run Primary Task' },
189
icon: Codicon.play,
190
category: SessionsCategories.Sessions,
191
f1: true,
192
});
193
}
194
195
async run(): Promise<void> {
196
const activeState = that._activeRunState.get();
197
if (!activeState) {
198
return;
199
}
200
201
logSessionsInteraction(that._telemetryService, 'runPrimaryTask');
202
203
const { tasks, session } = activeState;
204
if (tasks.length === 0) {
205
const task = await that._showConfigureQuickPick(session);
206
if (task) {
207
await that._sessionsConfigService.runTask(task, session);
208
}
209
return;
210
}
211
212
const primaryTask = getPrimaryTask(tasks, activeState.pinnedTaskLabel);
213
if (!primaryTask) {
214
return;
215
}
216
await that._sessionsConfigService.runTask(primaryTask.task, session);
217
}
218
}));
219
220
this._register(autorun(reader => {
221
const activeState = this._activeRunState.read(reader);
222
if (!activeState) {
223
return;
224
}
225
226
const { session, tasks } = activeState;
227
const repo = session.workspace.read(reader)?.repositories[0];
228
const configureScriptPrecondition = repo?.workingDirectory ?? repo?.uri ? ContextKeyExpr.true() : ContextKeyExpr.false();
229
230
reader.store.add(registerAction2(class extends Action2 {
231
constructor() {
232
super({
233
id: CONFIGURE_DEFAULT_RUN_ACTION_ID,
234
title: localize2('configureDefaultRunAction', "Add Task..."),
235
category: SessionsCategories.Sessions,
236
icon: Codicon.add,
237
precondition: configureScriptPrecondition,
238
menu: [{
239
id: RunScriptDropdownMenuId,
240
group: tasks.length === 0 ? 'navigation' : '1_configure',
241
order: 0
242
}]
243
});
244
}
245
246
async run(): Promise<void> {
247
logSessionsInteraction(that._telemetryService, 'addTask', 'menu');
248
const task = await that._showConfigureQuickPick(session);
249
if (task) {
250
await that._sessionsConfigService.runTask(task, session);
251
}
252
}
253
}));
254
255
reader.store.add(registerAction2(class extends Action2 {
256
constructor() {
257
super({
258
id: GENERATE_RUN_ACTION_ID,
259
title: localize2('generateRunAction', "Generate New Task..."),
260
category: SessionsCategories.Sessions,
261
precondition: IsActiveSessionBackgroundProviderContext,
262
menu: [{
263
id: RunScriptDropdownMenuId,
264
group: tasks.length === 0 ? 'navigation' : '1_configure',
265
order: 1
266
}]
267
});
268
}
269
270
async run(): Promise<void> {
271
logSessionsInteraction(that._telemetryService, 'generateNewTask', 'menu');
272
await that._generateNewTask(session);
273
}
274
}));
275
}));
276
}
277
278
private async _generateNewTask(session: ISession): Promise<void> {
279
const query = '/generate-run-commands';
280
// Prefer sending to the already-open chat widget for the session;
281
// fall back to sendAndCreateChat for untitled sessions or when no widget is loaded.
282
const widget = this._chatWidgetService.getWidgetBySessionResource(session.mainChat.resource);
283
if (widget) {
284
await widget.acceptInput(query);
285
} else {
286
await this._sessionManagementService.sendAndCreateChat(session, { query });
287
}
288
}
289
290
private async _showConfigureQuickPick(session: ISession): Promise<ITaskEntry | undefined> {
291
const nonSessionTasks = await this._sessionsConfigService.getNonSessionTasks(session);
292
if (nonSessionTasks.length === 0) {
293
// No existing tasks, go straight to custom command input
294
return this._showCustomCommandInput(session);
295
}
296
297
interface ITaskPickItem extends IQuickPickItem {
298
readonly task?: ITaskEntry;
299
readonly source?: TaskStorageTarget;
300
}
301
302
const items: (ITaskPickItem | IQuickPickSeparator)[] = [];
303
304
items.push({ type: 'separator', label: localize('custom', "Custom") });
305
items.push({
306
label: localize('createNewTask', "Create new task..."),
307
description: localize('enterCustomCommandDesc', "Create a new shell task"),
308
});
309
310
if (nonSessionTasks.length > 0) {
311
items.push({ type: 'separator', label: localize('existingTasks', "Existing Tasks") });
312
for (const { task, target } of nonSessionTasks) {
313
items.push({
314
label: getTaskDisplayLabel(task),
315
description: task.command,
316
task,
317
source: target,
318
});
319
}
320
}
321
322
const picked = await this._quickInputService.pick(items, {
323
placeHolder: localize('pickRunAction', "Select or create a task"),
324
});
325
326
if (!picked) {
327
return undefined;
328
}
329
330
const pickedItem = picked as ITaskPickItem;
331
if (pickedItem.task) {
332
return this._showCustomCommandInput(session, { task: pickedItem.task, target: pickedItem.source ?? 'workspace' }, 'add', true);
333
} else {
334
// Custom command path
335
return this._showCustomCommandInput(session, undefined, 'add', true);
336
}
337
}
338
339
private async _showCustomCommandInput(session: ISession, existingTask?: INonSessionTaskEntry, mode: TaskConfigurationMode = 'add', allowBackNavigation = false): Promise<ITaskEntry | undefined> {
340
const taskConfiguration = await this._showCustomCommandWidget(session, existingTask, mode, allowBackNavigation);
341
if (!taskConfiguration) {
342
return undefined;
343
}
344
if (taskConfiguration === 'back') {
345
return this._showConfigureQuickPick(session);
346
}
347
348
if (existingTask) {
349
if (mode === 'configure') {
350
const newLabel = taskConfiguration.label?.trim() || existingTask.task.label || taskConfiguration.command;
351
352
let updatedTask: ITaskEntry = {
353
...existingTask.task,
354
label: newLabel,
355
inAgents: true,
356
};
357
358
if (taskConfiguration.command && existingTask.task.command !== undefined) {
359
updatedTask = {
360
...updatedTask,
361
command: taskConfiguration.command,
362
};
363
}
364
365
if (taskConfiguration.runOn) {
366
updatedTask = {
367
...updatedTask,
368
runOptions: {
369
...(existingTask.task.runOptions ?? {}),
370
runOn: taskConfiguration.runOn,
371
},
372
};
373
}
374
375
await this._sessionsConfigService.updateTask(existingTask.task.label, updatedTask, session, existingTask.target, taskConfiguration.target);
376
return updatedTask;
377
}
378
379
await this._sessionsConfigService.addTaskToSessions(existingTask.task, session, existingTask.target, { runOn: taskConfiguration.runOn ?? 'default' });
380
return {
381
...existingTask.task,
382
inAgents: true,
383
...(taskConfiguration.runOn ? { runOptions: { runOn: taskConfiguration.runOn } } : {}),
384
};
385
}
386
387
return this._sessionsConfigService.createAndAddTask(
388
taskConfiguration.label,
389
taskConfiguration.command,
390
session,
391
taskConfiguration.target,
392
taskConfiguration.runOn ? { runOn: taskConfiguration.runOn } : undefined
393
);
394
}
395
396
private _showCustomCommandWidget(session: ISession, existingTask?: INonSessionTaskEntry, mode: TaskConfigurationMode = 'add', allowBackNavigation = false): Promise<IRunScriptCustomTaskWidgetResult | 'back' | undefined> {
397
const repo = session.workspace.get()?.repositories[0];
398
const workspaceTargetDisabledReason = !(repo?.workingDirectory ?? repo?.uri)
399
? localize('workspaceStorageUnavailableTooltip', "Workspace storage is unavailable for this session")
400
: undefined;
401
const isConfigureMode = mode === 'configure';
402
403
return new Promise<IRunScriptCustomTaskWidgetResult | 'back' | undefined>(resolve => {
404
const disposables = new DisposableStore();
405
let settled = false;
406
407
const quickWidget = disposables.add(this._quickInputService.createQuickWidget());
408
quickWidget.title = isConfigureMode
409
? localize('configureActionWidgetTitle', "Configure Task")
410
: existingTask
411
? localize('addExistingActionWidgetTitle', "Add Existing Task")
412
: localize('addActionWidgetTitle', "Add Task");
413
quickWidget.description = isConfigureMode
414
? localize('configureActionWidgetDescription', "Update how this task is named, saved, and run.")
415
: existingTask
416
? localize('addExistingActionWidgetDescription', "Enable an existing task for sessions and configure when it should run.")
417
: localize('addActionWidgetDescription', "Create a shell task and configure how it should be saved and run.");
418
quickWidget.ignoreFocusOut = true;
419
quickWidget.buttons = allowBackNavigation
420
? [this._quickInputService.backButton, closeQuickWidgetButton]
421
: [closeQuickWidgetButton];
422
const widget = disposables.add(new RunScriptCustomTaskWidget({
423
label: existingTask?.task.label,
424
labelDisabledReason: existingTask && !isConfigureMode ? localize('existingTaskLabelLocked', "This name comes from an existing task and cannot be changed here.") : undefined,
425
command: existingTask ? getTaskCommandPreview(existingTask.task) : undefined,
426
commandDisabledReason: existingTask && !isConfigureMode ? localize('existingTaskCommandLocked', "This command comes from an existing task and cannot be changed here.") : undefined,
427
target: existingTask?.target,
428
targetDisabledReason: existingTask && !isConfigureMode ? localize('existingTaskTargetLocked', "This existing task cannot be moved between workspace and user storage.") : workspaceTargetDisabledReason,
429
runOn: existingTask?.task.runOptions?.runOn === 'worktreeCreated' ? 'worktreeCreated' : undefined,
430
mode: isConfigureMode ? 'configure' : existingTask ? 'add-existing' : 'add',
431
}));
432
quickWidget.widget = widget.domNode;
433
this._layoutService.mainContainer.classList.add(RUN_SCRIPT_ACTION_MODAL_VISIBLE_CLASS);
434
const backdrop = append(this._layoutService.mainContainer, $('.run-script-action-modal-backdrop'));
435
disposables.add(addDisposableGenericMouseDownListener(backdrop, e => {
436
e.preventDefault();
437
e.stopPropagation();
438
complete(undefined);
439
}));
440
disposables.add({ dispose: () => backdrop.remove() });
441
disposables.add({ dispose: () => this._layoutService.mainContainer.classList.remove(RUN_SCRIPT_ACTION_MODAL_VISIBLE_CLASS) });
442
443
const complete = (result: IRunScriptCustomTaskWidgetResult | undefined) => {
444
if (settled) {
445
return;
446
}
447
settled = true;
448
resolve(result);
449
quickWidget.hide();
450
};
451
452
disposables.add(widget.onDidSubmit(result => complete(result)));
453
disposables.add(widget.onDidCancel(() => complete(undefined)));
454
disposables.add(quickWidget.onDidTriggerButton(button => {
455
if (allowBackNavigation && button === this._quickInputService.backButton) {
456
settled = true;
457
resolve('back');
458
quickWidget.hide();
459
return;
460
}
461
if (button === closeQuickWidgetButton) {
462
complete(undefined);
463
}
464
}));
465
disposables.add(quickWidget.onDidHide(() => {
466
if (!settled) {
467
settled = true;
468
resolve(undefined);
469
}
470
disposables.dispose();
471
}));
472
473
quickWidget.show();
474
widget.focus();
475
});
476
}
477
}
478
479
/**
480
* Split-button action view item for the run script picker in the sessions titlebar.
481
* The primary button runs the pinned task, or the first task if none is pinned.
482
* The dropdown arrow opens a custom action widget with categories and per-item
483
* toolbar actions (pin, configure, remove).
484
*/
485
class RunScriptActionViewItem extends BaseActionViewItem {
486
487
private readonly _primaryActionAction: Action;
488
private readonly _primaryAction: ActionViewItem;
489
private readonly _dropdown: ChevronActionWidgetDropdown;
490
491
constructor(
492
action: IAction,
493
_options: IActionViewItemOptions,
494
private readonly _activeRunState: IObservable<IRunScriptActionContext | undefined>,
495
private readonly _showConfigureQuickPick: (session: ISession) => Promise<ITaskEntry | undefined>,
496
private readonly _showCustomCommandInput: (session: ISession, existingTask: INonSessionTaskEntry, mode?: TaskConfigurationMode) => Promise<ITaskEntry | undefined>,
497
private readonly _generateNewTask: (session: ISession) => Promise<void>,
498
@ICommandService private readonly _commandService: ICommandService,
499
@ISessionsConfigurationService private readonly _sessionsConfigService: ISessionsConfigurationService,
500
@IKeybindingService private readonly _keybindingService: IKeybindingService,
501
@IActionWidgetService private readonly _actionWidgetService: IActionWidgetService,
502
@IContextKeyService contextKeyService: IContextKeyService,
503
@ITelemetryService private readonly _telemetryService: ITelemetryService,
504
) {
505
super(undefined, action);
506
507
const state = this._activeRunState.get();
508
const hasTasks = state && state.tasks.length > 0;
509
510
// Primary action button - runs the pinned task (or first task when none is pinned)
511
this._primaryActionAction = this._register(new Action(
512
'agentSessions.runScriptPrimary',
513
this._getPrimaryActionTooltip(state),
514
ThemeIcon.asClassName(Codicon.play),
515
hasTasks,
516
() => this._commandService.executeCommand(RUN_SCRIPT_ACTION_PRIMARY_ID)
517
));
518
this._primaryAction = this._register(new ActionViewItem(undefined, this._primaryActionAction, { icon: true, label: false }));
519
520
// Update enabled state when tasks change
521
this._register(autorun(reader => {
522
const runState = this._activeRunState.read(reader);
523
this._primaryActionAction.enabled = !!runState && runState.tasks.length > 0;
524
this._primaryActionAction.label = this._getPrimaryActionTooltip(runState);
525
}));
526
527
// Dropdown with categorized task actions and per-item toolbars
528
const dropdownAction = this._register(new Action('agentSessions.runScriptDropdown', localize('runDropdown', "More Tasks...")));
529
this._dropdown = this._register(new ChevronActionWidgetDropdown(
530
dropdownAction,
531
{
532
actionProvider: { getActions: () => this._getDropdownActions() },
533
showItemKeybindings: true,
534
},
535
this._actionWidgetService,
536
this._keybindingService,
537
contextKeyService,
538
this._telemetryService,
539
));
540
}
541
542
override render(container: HTMLElement): void {
543
super.render(container);
544
container.classList.add('monaco-dropdown-with-default');
545
546
// Primary action button
547
const primaryContainer = $('.action-container');
548
this._primaryAction.render(append(container, primaryContainer));
549
this._register(addDisposableListener(primaryContainer, EventType.KEY_DOWN, (e: KeyboardEvent) => {
550
const event = new StandardKeyboardEvent(e);
551
if (event.equals(KeyCode.RightArrow)) {
552
this._primaryAction.blur();
553
this._dropdown.focus();
554
event.stopPropagation();
555
}
556
}));
557
558
// Dropdown arrow button
559
const dropdownContainer = $('.dropdown-action-container');
560
this._dropdown.render(append(container, dropdownContainer));
561
this._register(addDisposableListener(dropdownContainer, EventType.KEY_DOWN, (e: KeyboardEvent) => {
562
const event = new StandardKeyboardEvent(e);
563
if (event.equals(KeyCode.LeftArrow)) {
564
this._dropdown.setFocusable(false);
565
this._primaryAction.focus();
566
event.stopPropagation();
567
}
568
}));
569
}
570
571
override focus(fromRight?: boolean): void {
572
if (fromRight) {
573
this._dropdown.focus();
574
} else {
575
this._primaryAction.focus();
576
}
577
}
578
579
override blur(): void {
580
this._primaryAction.blur();
581
this._dropdown.blur();
582
}
583
584
override setFocusable(focusable: boolean): void {
585
this._primaryAction.setFocusable(focusable);
586
if (!focusable) {
587
this._dropdown.setFocusable(false);
588
}
589
}
590
591
private _getPrimaryActionTooltip(state: IRunScriptActionContext | undefined): string {
592
if (!state || state.tasks.length === 0) {
593
return localize('runPrimaryTaskTooltip', "Run Primary Task");
594
}
595
596
const primaryTask = getPrimaryTask(state.tasks, state.pinnedTaskLabel)?.task;
597
if (!primaryTask) {
598
return localize('runPrimaryTaskTooltip', "Run Primary Task");
599
}
600
601
const keybindingLabel = this._keybindingService.lookupKeybinding(RUN_SCRIPT_ACTION_PRIMARY_ID)?.getLabel();
602
return keybindingLabel
603
? localize('runActionTooltipKeybinding', "{0} ({1})", getTaskDisplayLabel(primaryTask), keybindingLabel)
604
: getTaskDisplayLabel(primaryTask);
605
}
606
607
private _getDropdownActions(): IActionWidgetDropdownAction[] {
608
const state = this._activeRunState.get();
609
if (!state) {
610
return [];
611
}
612
613
const { tasks, session, pinnedTaskLabel } = state;
614
const repo = session.workspace.get()?.repositories[0];
615
const actions: IActionWidgetDropdownAction[] = [];
616
617
// Category for normal tasks (no header shown)
618
const defaultCategory = { label: '', order: 0, showHeader: false };
619
// Category for worktree-creation tasks
620
const worktreeCategory = { label: localize('worktreeCreationCategory', "Run on Worktree Creation"), order: 1, showHeader: true };
621
// Category for task creation and management
622
const tasksCategory = { label: localize('tasksActionsCategory', "Tasks"), order: 2, showHeader: true };
623
624
for (let i = 0; i < tasks.length; i++) {
625
const entry = tasks[i];
626
const task = entry.task;
627
const isWorktreeTask = task.runOptions?.runOn === 'worktreeCreated';
628
const isPinned = task.label === pinnedTaskLabel;
629
630
const toolbarActions: IAction[] = [
631
{
632
id: `runScript.pin.${i}`,
633
label: isPinned ? localize('unpinTask', "Unpin") : localize('pinTask', "Pin"),
634
tooltip: isPinned ? localize('unpinTaskTooltip', "Unpin") : localize('pinTaskTooltip', "Pin"),
635
class: ThemeIcon.asClassName(isPinned ? Codicon.pinned : Codicon.pin),
636
enabled: !!repo?.uri,
637
run: async () => {
638
this._actionWidgetService.hide();
639
this._sessionsConfigService.setPinnedTaskLabel(repo?.uri, isPinned ? undefined : task.label);
640
}
641
},
642
{
643
id: `runScript.configure.${i}`,
644
label: localize('configureTask', "Configure"),
645
tooltip: localize('configureTask', "Configure"),
646
class: ThemeIcon.asClassName(Codicon.gear),
647
enabled: true,
648
run: async () => {
649
this._actionWidgetService.hide();
650
await this._showCustomCommandInput(session, { task, target: entry.target }, 'configure');
651
}
652
},
653
{
654
id: `runScript.remove.${i}`,
655
label: localize('removeTask', "Remove"),
656
tooltip: localize('removeTask', "Remove"),
657
class: ThemeIcon.asClassName(Codicon.close),
658
enabled: true,
659
run: async () => {
660
this._actionWidgetService.hide();
661
await this._sessionsConfigService.removeTask(task.label, session, entry.target);
662
}
663
}
664
];
665
666
actions.push({
667
id: `runScript.task.${i}`,
668
label: getTaskDisplayLabel(task),
669
tooltip: '',
670
hover: {
671
content: localize('runActionTooltip', "Run '{0}' in terminal", getTaskDisplayLabel(task)),
672
},
673
icon: Codicon.play,
674
enabled: true,
675
class: undefined,
676
category: isWorktreeTask ? worktreeCategory : defaultCategory,
677
toolbarActions,
678
run: async () => {
679
await this._sessionsConfigService.runTask(task, session);
680
},
681
});
682
}
683
684
// "Add Task..." action
685
const canConfigure = !!(repo?.workingDirectory ?? repo?.uri);
686
actions.push({
687
id: 'runScript.addAction',
688
label: localize('configureDefaultRunAction', "Add Task..."),
689
tooltip: '',
690
hover: {
691
content: canConfigure
692
? localize('addActionTooltip', "Add a new task")
693
: localize('addActionTooltipDisabled', "Cannot add tasks to this session because workspace storage is unavailable"),
694
},
695
icon: Codicon.add,
696
enabled: canConfigure,
697
class: undefined,
698
category: tasksCategory,
699
run: async () => {
700
logSessionsInteraction(this._telemetryService, 'addTask', 'actionWidget');
701
const task = await this._showConfigureQuickPick(session);
702
if (task) {
703
await this._sessionsConfigService.runTask(task, session);
704
}
705
},
706
});
707
708
// "Generate New Task..." action
709
actions.push({
710
id: 'runScript.generateAction',
711
label: localize('generateRunAction', "Generate New Task..."),
712
tooltip: '',
713
hover: {
714
content: localize('generateRunActionTooltip', "Generate a new workspace task"),
715
},
716
icon: Codicon.sparkle,
717
enabled: true,
718
class: undefined,
719
category: tasksCategory,
720
run: async () => {
721
logSessionsInteraction(this._telemetryService, 'generateNewTask', 'actionWidget');
722
await this._generateNewTask(session);
723
},
724
});
725
726
return actions;
727
}
728
}
729
730
/**
731
* {@link ActionWidgetDropdownActionViewItem} that renders a chevron-down icon
732
* for the split button dropdown in the titlebar.
733
*/
734
class ChevronActionWidgetDropdown extends ActionWidgetDropdownActionViewItem {
735
protected override renderLabel(element: HTMLElement): IDisposable | null {
736
element.classList.add('codicon', 'codicon-chevron-down');
737
return null;
738
}
739
}
740
741
// Register the Run split button submenu on the workbench title bar (background sessions only)
742
MenuRegistry.appendMenuItem(Menus.TitleBarSessionMenu, {
743
submenu: RunScriptDropdownMenuId,
744
isSplitButton: true,
745
title: localize2('run', "Run"),
746
icon: Codicon.play,
747
group: 'navigation',
748
order: 8,
749
when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext)
750
});
751
752
// Disabled placeholder shown in the titlebar when the active session does not support running scripts
753
class RunScriptNotAvailableAction extends Action2 {
754
constructor() {
755
super({
756
id: 'workbench.action.agentSessions.runScript.notAvailable',
757
title: localize2('run', "Run"),
758
tooltip: localize('runScriptNotAvailableTooltip', "Run Task is not available for this session type"),
759
icon: Codicon.play,
760
precondition: ContextKeyExpr.false(),
761
menu: [{
762
id: Menus.TitleBarSessionMenu,
763
group: 'navigation',
764
order: 8,
765
when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext.toNegated())
766
}]
767
});
768
}
769
770
override run(): void { }
771
}
772
773
registerAction2(RunScriptNotAvailableAction);
774
775
// Register F5 keybinding at module level to ensure it's in the registry
776
// before the keybinding resolver is cached. The command handler is
777
// registered later by RunScriptContribution.
778
KeybindingsRegistry.registerKeybindingRule({
779
id: RUN_SCRIPT_ACTION_PRIMARY_ID,
780
primary: KeyCode.F5,
781
weight: KeybindingWeight.WorkbenchContrib + 100,
782
when: IsAuxiliaryWindowContext.toNegated()
783
});
784
785