Path: blob/main/src/vs/sessions/contrib/chat/browser/runScriptCustomTaskWidget.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 './media/runScriptAction.css';67import * as dom from '../../../../base/browser/dom.js';8import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';9import { Button } from '../../../../base/browser/ui/button/button.js';10import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';11import { Radio } from '../../../../base/browser/ui/radio/radio.js';12import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js';13import { Emitter, Event } from '../../../../base/common/event.js';14import { Disposable } from '../../../../base/common/lifecycle.js';15import { KeyCode } from '../../../../base/common/keyCodes.js';16import { localize } from '../../../../nls.js';17import { defaultButtonStyles, defaultCheckboxStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';18import { TaskStorageTarget } from './sessionsConfigurationService.js';1920export const WORKTREE_CREATED_RUN_ON = 'worktreeCreated' as const;2122export interface IRunScriptCustomTaskWidgetState {23readonly label?: string;24readonly labelDisabledReason?: string;25readonly command?: string;26readonly commandDisabledReason?: string;27readonly target?: TaskStorageTarget;28readonly targetDisabledReason?: string;29readonly runOn?: typeof WORKTREE_CREATED_RUN_ON;30readonly mode?: 'add' | 'add-existing' | 'configure';31}3233export interface IRunScriptCustomTaskWidgetResult {34readonly label?: string;35readonly command: string;36readonly target: TaskStorageTarget;37readonly runOn?: typeof WORKTREE_CREATED_RUN_ON;38}3940export class RunScriptCustomTaskWidget extends Disposable {4142readonly domNode: HTMLElement;4344private readonly _labelInput: InputBox;45private readonly _commandInput: InputBox;46private readonly _runOnCheckbox: Checkbox;47private readonly _storageOptions: Radio;48private readonly _submitButton: Button;49private readonly _cancelButton: Button;50private readonly _labelLocked: boolean;51private readonly _commandLocked: boolean;52private readonly _targetLocked: boolean;53private readonly _isExistingTask: boolean;54private readonly _isAddExistingTask: boolean;55private readonly _initialLabel: string;56private readonly _initialCommand: string;57private readonly _initialRunOn: boolean;58private readonly _initialTarget: TaskStorageTarget;59private _selectedTarget: TaskStorageTarget;6061private readonly _onDidSubmit = this._register(new Emitter<IRunScriptCustomTaskWidgetResult>());62readonly onDidSubmit: Event<IRunScriptCustomTaskWidgetResult> = this._onDidSubmit.event;6364private readonly _onDidCancel = this._register(new Emitter<void>());65readonly onDidCancel: Event<void> = this._onDidCancel.event;6667constructor(state: IRunScriptCustomTaskWidgetState) {68super();6970this._labelLocked = !!state.labelDisabledReason;71this._commandLocked = !!state.commandDisabledReason;72this._targetLocked = !!state.targetDisabledReason && state.target !== undefined;73this._isExistingTask = state.mode === 'configure';74this._isAddExistingTask = state.mode === 'add-existing';75this._selectedTarget = state.target ?? (state.targetDisabledReason ? 'user' : 'workspace');76this._initialLabel = state.label ?? '';77this._initialCommand = state.command ?? '';78this._initialRunOn = state.runOn === WORKTREE_CREATED_RUN_ON;79this._initialTarget = this._selectedTarget;8081this.domNode = dom.$('.run-script-action-widget');8283const labelSection = dom.append(this.domNode, dom.$('.run-script-action-section'));84dom.append(labelSection, dom.$('label.run-script-action-label', undefined, localize('labelFieldLabel', "Name")));85const labelInputContainer = dom.append(labelSection, dom.$('.run-script-action-input'));86this._labelInput = this._register(new InputBox(labelInputContainer, undefined, {87placeholder: localize('enterLabelPlaceholder', "Enter a name for this task (optional)"),88tooltip: state.labelDisabledReason,89ariaLabel: localize('enterLabelAriaLabel', "Task name"),90inputBoxStyles: defaultInputBoxStyles,91}));92this._labelInput.value = state.label ?? '';93if (state.labelDisabledReason) {94this._labelInput.disable();95}9697const commandSection = dom.append(this.domNode, dom.$('.run-script-action-section'));98dom.append(commandSection, dom.$('label.run-script-action-label', undefined, localize('commandFieldLabel', "Command")));99const commandInputContainer = dom.append(commandSection, dom.$('.run-script-action-input'));100this._commandInput = this._register(new InputBox(commandInputContainer, undefined, {101placeholder: localize('enterCommandPlaceholder', "Enter command (for example, npm run dev)"),102tooltip: state.commandDisabledReason,103ariaLabel: localize('enterCommandAriaLabel', "Task command"),104inputBoxStyles: defaultInputBoxStyles,105}));106this._commandInput.value = state.command ?? '';107if (state.commandDisabledReason) {108this._commandInput.disable();109}110111const runOnSection = dom.append(this.domNode, dom.$('.run-script-action-section'));112dom.append(runOnSection, dom.$('div.run-script-action-label', undefined, localize('runOptionsLabel', "Run Options")));113const runOnRow = dom.append(runOnSection, dom.$('.run-script-action-option-row'));114this._runOnCheckbox = this._register(new Checkbox(localize('runOnWorktreeCreated', "Run When Worktree Is Created"), state.runOn === WORKTREE_CREATED_RUN_ON, defaultCheckboxStyles));115runOnRow.appendChild(this._runOnCheckbox.domNode);116const runOnText = dom.append(runOnRow, dom.$('span.run-script-action-option-text', undefined, localize('runOnWorktreeCreatedDescription', "Automatically run this task when the session worktree is created")));117this._register(dom.addDisposableListener(runOnText, dom.EventType.CLICK, () => this._runOnCheckbox.checked = !this._runOnCheckbox.checked));118119const storageSection = dom.append(this.domNode, dom.$('.run-script-action-section'));120dom.append(storageSection, dom.$('div.run-script-action-label', undefined, localize('storageLabel', "Save In")));121const storageDisabledReason = state.targetDisabledReason;122if (storageDisabledReason) {123dom.append(storageSection, dom.$('div.run-script-action-hint', undefined, storageDisabledReason));124}125const workspaceTargetDisabled = !!storageDisabledReason;126this._storageOptions = this._register(new Radio({127items: [128{129text: localize('workspaceStorageLabel', "Workspace"),130tooltip: storageDisabledReason ?? localize('workspaceStorageTooltip', "Save this task in the current workspace"),131isActive: this._selectedTarget === 'workspace',132disabled: workspaceTargetDisabled,133},134{135text: localize('userStorageLabel', "User"),136tooltip: this._targetLocked ? storageDisabledReason : localize('userStorageTooltip', "Save this task in your user tasks and make it available in all sessions"),137isActive: this._selectedTarget === 'user',138disabled: this._targetLocked,139}140]141}));142this._storageOptions.domNode.setAttribute('aria-label', localize('storageAriaLabel', "Task storage target"));143this._storageOptions.domNode.classList.toggle('run-script-action-radio-disabled', this._targetLocked);144this._storageOptions.setEnabled(!this._targetLocked);145storageSection.appendChild(this._storageOptions.domNode);146147const buttonRow = dom.append(this.domNode, dom.$('.run-script-action-buttons'));148this._cancelButton = this._register(new Button(buttonRow, { ...defaultButtonStyles, secondary: true }));149this._cancelButton.label = localize('cancelAddAction', "Cancel");150this._submitButton = this._register(new Button(buttonRow, defaultButtonStyles));151this._submitButton.label = this._getSubmitLabel();152153this._register(this._labelInput.onDidChange(() => this._updateButtonState()));154this._register(this._commandInput.onDidChange(() => this._updateButtonState()));155this._register(this._storageOptions.onDidSelect(index => {156this._selectedTarget = index === 0 ? 'workspace' : 'user';157this._updateButtonState();158}));159this._register(this._runOnCheckbox.onChange(() => this._updateButtonState()));160this._register(this._submitButton.onDidClick(() => this._submit()));161this._register(this._cancelButton.onDidClick(() => this._onDidCancel.fire()));162this._register(dom.addDisposableListener(this._labelInput.inputElement, dom.EventType.KEY_DOWN, event => {163const keyboardEvent = new StandardKeyboardEvent(event);164if (keyboardEvent.equals(KeyCode.Enter)) {165keyboardEvent.preventDefault();166keyboardEvent.stopPropagation();167this._submit();168}169}));170this._register(dom.addDisposableListener(this._commandInput.inputElement, dom.EventType.KEY_DOWN, event => {171const keyboardEvent = new StandardKeyboardEvent(event);172if (keyboardEvent.equals(KeyCode.Enter)) {173keyboardEvent.preventDefault();174keyboardEvent.stopPropagation();175this._submit();176}177}));178this._register(dom.addDisposableListener(this.domNode, dom.EventType.KEY_DOWN, event => {179const keyboardEvent = new StandardKeyboardEvent(event);180if (keyboardEvent.equals(KeyCode.Escape)) {181keyboardEvent.preventDefault();182keyboardEvent.stopPropagation();183this._onDidCancel.fire();184}185}));186187this._updateButtonState();188}189190focus(): void {191if (!this._labelLocked) {192this._labelInput.focus();193return;194}195if (this._commandLocked) {196this._runOnCheckbox.focus();197return;198}199this._commandInput.focus();200}201202private _submit(): void {203const label = this._labelInput.value.trim();204const command = this._commandInput.value.trim();205if (!command) {206return;207}208209this._onDidSubmit.fire({210label: label.length > 0 ? label : undefined,211command,212target: this._selectedTarget,213runOn: this._runOnCheckbox.checked ? WORKTREE_CREATED_RUN_ON : undefined,214});215}216217private _updateButtonState(): void {218this._submitButton.enabled = this._commandInput.value.trim().length > 0;219this._submitButton.label = this._getSubmitLabel();220}221222private _getSubmitLabel(): string {223if (this._isAddExistingTask) {224return localize('confirmAddToAgents', "Add to Agents Window");225}226if (!this._isExistingTask) {227return localize('confirmAddTask', "Add Task");228}229230const targetChanged = this._selectedTarget !== this._initialTarget;231const labelChanged = this._labelInput.value !== this._initialLabel;232const commandChanged = this._commandInput.value !== this._initialCommand;233const runOnChanged = this._runOnCheckbox.checked !== this._initialRunOn;234const otherChanged = labelChanged || commandChanged || runOnChanged;235236if (targetChanged && otherChanged) {237return localize('confirmMoveAndUpdateTask', "Move and Update Task");238}239if (targetChanged) {240return localize('confirmMoveTask', "Move Task");241}242return localize('confirmUpdateTask', "Update Task");243}244}245246247