66import '../media/automationsCards.css' ;
77import './automationsAccessibility.js' ;
88import * as DOM from '../../../../../base/browser/dom.js' ;
9+ import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js' ;
910import { Button , ButtonBar , IButton } from '../../../../../base/browser/ui/button/button.js' ;
1011import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js' ;
1112import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js' ;
@@ -31,7 +32,8 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js';
3132import { ILogService } from '../../../../../platform/log/common/log.js' ;
3233import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js' ;
3334import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js' ;
34- import { ContextKeyExpr , IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js' ;
35+ import { ContextKeyExpr , IContextKey , IContextKeyService , RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js' ;
36+ import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js' ;
3537import { status } from '../../../../../base/browser/ui/aria/aria.js' ;
3638import { Gesture , GestureEvent , EventType as TouchEventType } from '../../../../../base/browser/touch.js' ;
3739import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js' ;
@@ -56,6 +58,8 @@ import { ARCHIVE_SESSION_COMMAND_ID, MARK_SESSION_READ_COMMAND_ID, MARK_SESSION_
5658const $ = DOM . $ ;
5759const STOP_AUTOMATION_RUN_SESSION_COMMAND_ID = 'sessions.automations.stopRunSession' ;
5860const DELETE_AUTOMATION_RUN_SESSION_COMMAND_ID = 'sessions.automations.deleteRunSession' ;
61+ const AutomationCardCanDeleteContext = new RawContextKey < boolean > ( 'sessionsAutomationCardCanDelete' , false ) ;
62+ const AutomationCardCanDisableContext = new RawContextKey < boolean > ( 'sessionsAutomationCardCanDisable' , false ) ;
5963
6064interface IAutomationCardEntry {
6165 readonly element : HTMLElement ;
@@ -64,6 +68,8 @@ interface IAutomationCardEntry {
6468 readonly actions : HTMLElement ;
6569 readonly runButton : IButton ;
6670 readonly deleteButton : IButton ;
71+ readonly canDeleteContext : IContextKey < boolean > ;
72+ readonly canDisableContext : IContextKey < boolean > ;
6773 readonly nameText : HTMLElement ;
6874 readonly scheduleEl : HTMLElement ;
6975 readonly folderEl : HTMLElement ;
@@ -195,6 +201,8 @@ class AutomationCardsSection extends Disposable {
195201 @ILogService private readonly logService : ILogService ,
196202 @IDialogService private readonly dialogService : IDialogService ,
197203 @IConfigurationService private readonly configurationService : IConfigurationService ,
204+ @IContextKeyService private readonly contextKeyService : IContextKeyService ,
205+ @IContextMenuService private readonly contextMenuService : IContextMenuService ,
198206 ) {
199207 super ( ) ;
200208 this . container = DOM . append ( parent , $ ( '.automations-cards-grid' ) ) ;
@@ -260,7 +268,24 @@ class AutomationCardsSection extends Disposable {
260268 const wrapper = $ ( '.automations-card-wrapper' ) ;
261269 const card = DOM . append ( wrapper , $ ( '.automations-card' ) ) ;
262270 card . setAttribute ( 'role' , 'group' ) ;
271+ const cardContextKeyService = disposables . add ( this . contextKeyService . createScoped ( card ) ) ;
272+ const canDeleteContext = AutomationCardCanDeleteContext . bindTo ( cardContextKeyService ) ;
273+ const canDisableContext = AutomationCardCanDisableContext . bindTo ( cardContextKeyService ) ;
263274 disposables . add ( Gesture . addTarget ( card ) ) ;
275+ disposables . add ( DOM . addDisposableListener ( card , DOM . EventType . CONTEXT_MENU , ( event : MouseEvent ) => {
276+ const currentAutomation = this . latestAutomations . get ( automation . id ) ;
277+ if ( ! currentAutomation ) {
278+ return ;
279+ }
280+ event . preventDefault ( ) ;
281+ event . stopPropagation ( ) ;
282+ this . contextMenuService . showContextMenu ( {
283+ menuId : Menus . AutomationCardContext ,
284+ menuActionOptions : { shouldForwardArgs : true , arg : currentAutomation } ,
285+ getAnchor : ( ) => DOM . isMouseEvent ( event ) ? new StandardMouseEvent ( DOM . getWindow ( card ) , event ) : card ,
286+ contextKeyService : cardContextKeyService ,
287+ } ) ;
288+ } ) ) ;
264289
265290 const main = DOM . append ( card , $ < HTMLButtonElement > ( 'button.automations-card-main' , {
266291 type : 'button' ,
@@ -332,6 +357,8 @@ class AutomationCardsSection extends Disposable {
332357 actions,
333358 runButton : runBtn ,
334359 deleteButton : deleteBtn ,
360+ canDeleteContext,
361+ canDisableContext,
335362 nameText : nameTextEl ,
336363 scheduleEl,
337364 folderEl,
@@ -348,6 +375,8 @@ class AutomationCardsSection extends Disposable {
348375 card . main . disabled = this . automationService . canUpdateAutomation ?.( automation . id ) === false ;
349376 card . runButton . enabled = this . automationService . canRunAutomation ?.( automation . id ) !== false ;
350377 card . deleteButton . enabled = this . automationService . canDeleteAutomation ?.( automation . id ) !== false ;
378+ card . canDeleteContext . set ( this . automationService . canDeleteAutomation ?.( automation . id ) !== false ) ;
379+ card . canDisableContext . set ( automation . enabled && this . automationService . canUpdateAutomation ?.( automation . id ) !== false ) ;
351380 const schedule = formatSchedule ( automation ) ;
352381 const scheduleChanged = ! previous || formatSchedule ( previous ) !== schedule ;
353382 const nameChanged = ! previous || previous . name !== automation . name ;
@@ -487,30 +516,7 @@ class AutomationCardsSection extends Disposable {
487516 }
488517
489518 private async confirmDelete ( automation : IAutomationDescriptor ) : Promise < void > {
490- if ( ! await this . ensureEnabled ( ) ) {
491- return ;
492- }
493- const confirmed = await this . dialogService . confirm ( {
494- message : localize ( 'confirmDeleteAutomation' , "Delete automation \"{0}\"?" , automation . name ) ,
495- detail : localize ( 'confirmDeleteDetail' , "This will permanently delete the automation and its run history." ) ,
496- primaryButton : localize ( 'delete' , "Delete" ) ,
497- } ) ;
498- if ( ! confirmed . confirmed ) {
499- return ;
500- }
501- if ( ! await this . ensureEnabled ( ) ) {
502- return ;
503- }
504- try {
505- await this . automationService . deleteAutomation ( automation . id , ( ) => this . throwIfDisabled ( ) ) ;
506- status ( localize ( 'automationDeletedStatus' , "Deleted automation {0}" , automation . name ) ) ;
507- } catch ( err ) {
508- this . logService . error ( '[AutomationsCards] Failed to delete automation' , err ) ;
509- await this . dialogService . error (
510- localize ( 'automationDeleteFailed' , "Failed to delete automation." ) ,
511- getErrorMessage ( err ) ,
512- ) ;
513- }
519+ await confirmAndDeleteAutomation ( automation , this . automationService , this . configurationService , this . dialogService , this . logService ) ;
514520 }
515521
516522 private isEnabled ( ) : boolean {
@@ -1089,6 +1095,49 @@ async function showAutomationsDisabled(dialogService: IDialogService): Promise<v
10891095 ) ;
10901096}
10911097
1098+ async function confirmAndDeleteAutomation (
1099+ automation : IAutomationDescriptor ,
1100+ automationService : IAutomationService ,
1101+ configurationService : IConfigurationService ,
1102+ dialogService : IDialogService ,
1103+ logService : ILogService ,
1104+ ) : Promise < void > {
1105+ if ( automationService . canDeleteAutomation ?.( automation . id ) === false ) {
1106+ return ;
1107+ }
1108+ const isEnabled = ( ) => configurationService . getValue < boolean > ( CHAT_AUTOMATIONS_ENABLED_SETTING ) === true ;
1109+ if ( ! isEnabled ( ) ) {
1110+ await showAutomationsDisabled ( dialogService ) ;
1111+ return ;
1112+ }
1113+ const confirmed = await dialogService . confirm ( {
1114+ message : localize ( 'confirmDeleteAutomation' , "Delete automation \"{0}\"?" , automation . name ) ,
1115+ detail : localize ( 'confirmDeleteDetail' , "This will permanently delete the automation and its run history." ) ,
1116+ primaryButton : localize ( 'delete' , "Delete" ) ,
1117+ } ) ;
1118+ if ( ! confirmed . confirmed ) {
1119+ return ;
1120+ }
1121+ if ( ! isEnabled ( ) ) {
1122+ await showAutomationsDisabled ( dialogService ) ;
1123+ return ;
1124+ }
1125+ try {
1126+ await automationService . deleteAutomation ( automation . id , ( ) => {
1127+ if ( ! isEnabled ( ) ) {
1128+ throw new Error ( localize ( 'automationsDisabledBeforeDelete' , "Automations were disabled before the automation could be deleted." ) ) ;
1129+ }
1130+ } ) ;
1131+ status ( localize ( 'automationDeletedStatus' , "Deleted automation {0}" , automation . name ) ) ;
1132+ } catch ( error ) {
1133+ logService . error ( '[AutomationsCards] Failed to delete automation' , error ) ;
1134+ await dialogService . error (
1135+ localize ( 'automationDeleteFailed' , "Failed to delete automation." ) ,
1136+ getErrorMessage ( error ) ,
1137+ ) ;
1138+ }
1139+ }
1140+
10921141//#endregion
10931142
10941143//#region AutomationsView (Custom View)
@@ -1362,4 +1411,143 @@ registerAction2(class NewAutomationAction extends Action2 {
13621411 }
13631412} ) ;
13641413
1414+ registerAction2 ( class DuplicateAutomationAction extends Action2 {
1415+ constructor ( ) {
1416+ super ( {
1417+ id : 'sessions.automations.duplicate' ,
1418+ title : localize2 ( 'duplicateAutomation' , "Duplicate" ) ,
1419+ precondition : ChatAutomationsEnabledContext ,
1420+ menu : [ { id : Menus . AutomationCardContext , group : 'navigation' , order : 1 , when : ChatAutomationsEnabledContext } ] ,
1421+ } ) ;
1422+ }
1423+
1424+ override async run ( accessor : ServicesAccessor , automation : IAutomationDescriptor ) : Promise < void > {
1425+ const automationDialogService = accessor . get ( IAutomationDialogService ) ;
1426+ const automationService = accessor . get ( IAutomationService ) ;
1427+ const configurationService = accessor . get ( IConfigurationService ) ;
1428+ const dialogService = accessor . get ( IDialogService ) ;
1429+ const logService = accessor . get ( ILogService ) ;
1430+ const isEnabled = ( ) => configurationService . getValue < boolean > ( CHAT_AUTOMATIONS_ENABLED_SETTING ) === true ;
1431+ if ( ! isEnabled ( ) ) {
1432+ await showAutomationsDisabled ( dialogService ) ;
1433+ return ;
1434+ }
1435+
1436+ try {
1437+ const name = getDuplicateAutomationName ( automation . name , automationService . automations . get ( ) ) ;
1438+ const result = await automationDialogService . showAutomationDialog ( {
1439+ initialValues : {
1440+ name,
1441+ prompt : automation . prompt ,
1442+ schedule : automation . schedule ,
1443+ target : automation . target ,
1444+ modelId : automation . modelId ,
1445+ mode : automation . mode ,
1446+ permissionLevel : automation . permissionLevel ,
1447+ enabled : automation . enabled ,
1448+ } ,
1449+ } ) ;
1450+ if ( ! result || result . kind !== 'create' ) {
1451+ return ;
1452+ }
1453+ if ( ! isEnabled ( ) ) {
1454+ await showAutomationsDisabled ( dialogService ) ;
1455+ return ;
1456+ }
1457+ const duplicate = await automationService . createAutomation ( result . value , ( ) => {
1458+ if ( ! isEnabled ( ) ) {
1459+ throw new Error ( localize ( 'automationsDisabledBeforeDuplicate' , "Automations were disabled before the duplicate could be saved." ) ) ;
1460+ }
1461+ } ) ;
1462+ status ( localize ( 'automationDuplicatedStatus' , "Created duplicate automation {0}" , duplicate . name ) ) ;
1463+ } catch ( error ) {
1464+ logService . error ( '[Automations] Failed to duplicate automation' , error ) ;
1465+ await dialogService . error (
1466+ localize ( 'automationDuplicateFailed' , "Failed to duplicate automation." ) ,
1467+ getErrorMessage ( error ) ,
1468+ ) ;
1469+ }
1470+ }
1471+ } ) ;
1472+
1473+ registerAction2 ( class DeleteAutomationAction extends Action2 {
1474+ constructor ( ) {
1475+ super ( {
1476+ id : 'sessions.automations.delete' ,
1477+ title : localize2 ( 'deleteAutomationContextMenu' , "Delete" ) ,
1478+ precondition : ContextKeyExpr . and ( ChatAutomationsEnabledContext , AutomationCardCanDeleteContext ) ,
1479+ menu : [ { id : Menus . AutomationCardContext , group : 'navigation' , order : 3 , when : ChatAutomationsEnabledContext } ] ,
1480+ } ) ;
1481+ }
1482+
1483+ override async run ( accessor : ServicesAccessor , automation : IAutomationDescriptor ) : Promise < void > {
1484+ await confirmAndDeleteAutomation (
1485+ automation ,
1486+ accessor . get ( IAutomationService ) ,
1487+ accessor . get ( IConfigurationService ) ,
1488+ accessor . get ( IDialogService ) ,
1489+ accessor . get ( ILogService ) ,
1490+ ) ;
1491+ }
1492+ } ) ;
1493+
1494+ registerAction2 ( class DisableAutomationAction extends Action2 {
1495+ constructor ( ) {
1496+ super ( {
1497+ id : 'sessions.automations.disable' ,
1498+ title : localize2 ( 'disableAutomationContextMenu' , "Disable" ) ,
1499+ precondition : ContextKeyExpr . and ( ChatAutomationsEnabledContext , AutomationCardCanDisableContext ) ,
1500+ menu : [ { id : Menus . AutomationCardContext , group : 'navigation' , order : 2 , when : ChatAutomationsEnabledContext } ] ,
1501+ } ) ;
1502+ }
1503+
1504+ override async run ( accessor : ServicesAccessor , automation : IAutomationDescriptor ) : Promise < void > {
1505+ const automationService = accessor . get ( IAutomationService ) ;
1506+ if ( ! automation . enabled || automationService . canUpdateAutomation ?.( automation . id ) === false ) {
1507+ return ;
1508+ }
1509+ const configurationService = accessor . get ( IConfigurationService ) ;
1510+ const dialogService = accessor . get ( IDialogService ) ;
1511+ const logService = accessor . get ( ILogService ) ;
1512+ const isEnabled = ( ) => configurationService . getValue < boolean > ( CHAT_AUTOMATIONS_ENABLED_SETTING ) === true ;
1513+ if ( ! isEnabled ( ) ) {
1514+ await showAutomationsDisabled ( dialogService ) ;
1515+ return ;
1516+ }
1517+ try {
1518+ const result = await automationService . updateAutomationIfUnchanged ( automation . id , { enabled : false } , automation , ( ) => {
1519+ if ( ! isEnabled ( ) ) {
1520+ throw new Error ( localize ( 'automationsDisabledBeforeDisable' , "Automations were disabled before the automation could be updated." ) ) ;
1521+ }
1522+ } ) ;
1523+ if ( result . kind === 'conflict' ) {
1524+ throw new Error ( result . current
1525+ ? localize ( 'automationChangedDuringDisable' , "This automation changed before it could be disabled. Try again." )
1526+ : localize ( 'automationDeletedDuringDisable' , "This automation was deleted before it could be disabled." ) ) ;
1527+ }
1528+ status ( localize ( 'automationDisabledStatus' , "Disabled automation {0}" , automation . name ) ) ;
1529+ } catch ( error ) {
1530+ logService . error ( '[Automations] Failed to disable automation' , error ) ;
1531+ await dialogService . error (
1532+ localize ( 'automationDisableFailed' , "Failed to disable automation." ) ,
1533+ getErrorMessage ( error ) ,
1534+ ) ;
1535+ }
1536+ }
1537+ } ) ;
1538+
1539+ function getDuplicateAutomationName ( name : string , automations : readonly IAutomationDescriptor [ ] ) : string {
1540+ const existingNames = new Set ( automations . map ( automation => automation . name ) ) ;
1541+ const copyName = localize ( 'automationCopyName' , "{0} Copy" , name ) ;
1542+ if ( ! existingNames . has ( copyName ) ) {
1543+ return copyName ;
1544+ }
1545+ for ( let index = 2 ; ; index ++ ) {
1546+ const indexedCopyName = localize ( 'automationIndexedCopyName' , "{0} Copy {1}" , name , index ) ;
1547+ if ( ! existingNames . has ( indexedCopyName ) ) {
1548+ return indexedCopyName ;
1549+ }
1550+ }
1551+ }
1552+
13651553//#endregion
0 commit comments