@@ -72,6 +72,8 @@ export interface Activity {
72
72
temporary ?: boolean ;
73
73
/** Cluster of the launched activity */
74
74
cluster ?: string ;
75
+ /** Whether this activity is pinned (won't close on click-outside) */
76
+ pinned ?: boolean ;
75
77
}
76
78
77
79
export interface ActivityState {
@@ -178,6 +180,21 @@ export const Activity = {
178
180
update ( id : string , diff : Partial < Activity > ) {
179
181
store . dispatch ( activitySlice . actions . update ( { ...diff , id } ) ) ;
180
182
} ,
183
+ /**
184
+ * Closes or minimizes activity based on pinned state
185
+ * - Pinned activities: minimized (kept in ActivityBar)
186
+ * - Regular activities: closed completely
187
+ */
188
+ closeOrMinimize ( id : string ) {
189
+ const state = store . getState ( ) ;
190
+ const activity = state . activity . activities [ id ] ;
191
+
192
+ if ( activity ?. pinned ) {
193
+ this . update ( id , { minimized : true } ) ;
194
+ } else {
195
+ this . close ( id ) ;
196
+ }
197
+ } ,
181
198
reset ( ) {
182
199
store . dispatch ( activitySlice . actions . reset ( ) ) ;
183
200
} ,
@@ -335,6 +352,125 @@ export function SingleActivityRenderer({
335
352
} ;
336
353
} , [ location ] ) ;
337
354
355
+ // Close or minimize activity when clicking outside (only for split modes)
356
+ useEffect ( ( ) => {
357
+ if ( isOverview || minimized || ( location !== 'split-left' && location !== 'split-right' ) ) {
358
+ return ;
359
+ }
360
+
361
+ // Record when the listener is registered to ignore immediate clicks
362
+ const listenerRegistrationTime = Date . now ( ) ;
363
+
364
+ const handleClickOutside = ( event : MouseEvent ) => {
365
+ // Ignore clicks that happened within 100ms of listener registration
366
+ // This prevents the click that opened the activity from closing it
367
+ if ( event . timeStamp && Date . now ( ) - listenerRegistrationTime < 100 ) {
368
+ return ;
369
+ }
370
+
371
+ const activityElement = activityElementRef . current ;
372
+ if ( ! activityElement ) return ;
373
+
374
+ // Check if click is outside the activity panel
375
+ if ( ! activityElement . contains ( event . target as Node ) ) {
376
+ const target = event . target as HTMLElement ;
377
+
378
+ // Don't close if clicking on:
379
+ // 1. Another activity panel (let that activity handle it)
380
+ const isAnotherActivity = ! ! target . closest ( '[role="complementary"]' ) ;
381
+
382
+ // 2. Resource links (let Link.tsx handle the transition)
383
+ const isResourceLink = target . closest ( 'a[href*="/"], a[role="button"]' ) ;
384
+
385
+ // 3. ActivityBar (taskbar at the bottom)
386
+ const isInActivityBar = ! ! target . closest ( '[data-activity-bar="true"]' ) ;
387
+
388
+ // 4. Pagination buttons and table controls
389
+ const isPaginationButton = ! ! target . closest (
390
+ 'button[aria-label*="page"], button[aria-label*="Page"], ' +
391
+ 'button[title*="page"], button[title*="Page"], ' +
392
+ '.MuiPagination-root, .MuiPagination-root *, ' +
393
+ '[role="navigation"], [role="navigation"] *, ' +
394
+ 'button[aria-label*="next"], button[aria-label*="previous"], ' +
395
+ 'button[aria-label*="first"], button[aria-label*="last"]'
396
+ ) ;
397
+
398
+ // 5. Table control buttons (sort, filter, search, etc.)
399
+ const isTableControl = ! ! target . closest (
400
+ // Table header and controls
401
+ 'thead, thead *, ' +
402
+ '.MuiTableHead-root, .MuiTableHead-root *, ' +
403
+ // Sort buttons
404
+ 'button[aria-label*="sort"], button[aria-label*="Sort"], ' +
405
+ '[role="columnheader"], [role="columnheader"] *, ' +
406
+ // Filter buttons and inputs
407
+ 'button[aria-label*="filter"], button[aria-label*="Filter"], ' +
408
+ 'button[title*="filter"], button[title*="Filter"], ' +
409
+ '[aria-label*="filter"], [aria-label*="Filter"], ' +
410
+ '[aria-label*="Namespace"], [aria-label*="namespace"], ' +
411
+ // Search inputs and toggle buttons
412
+ 'input[type="search"], input[type="text"][placeholder*="Search"], ' +
413
+ 'input[type="text"][placeholder*="search"], ' +
414
+ 'input[type="text"][aria-label*="Search"], ' +
415
+ 'input[type="text"][aria-label*="search"], ' +
416
+ 'button[aria-label*="Search"], button[aria-label*="search"], ' +
417
+ 'button[title*="Search"], button[title*="search"], ' +
418
+ // Show/Hide buttons (columns, search, etc.)
419
+ 'button[aria-label*="show"], button[aria-label*="Show"], ' +
420
+ 'button[aria-label*="hide"], button[aria-label*="Hide"], ' +
421
+ 'button[title*="show"], button[title*="Show"], ' +
422
+ 'button[title*="hide"], button[title*="Hide"], ' +
423
+ 'button[aria-label*="column"], button[aria-label*="Column"], ' +
424
+ // MUI Select and Autocomplete components
425
+ '.MuiSelect-root, .MuiSelect-root *, ' +
426
+ '.MuiAutocomplete-root, .MuiAutocomplete-root *, ' +
427
+ '.MuiAutocomplete-popper, .MuiAutocomplete-popper *, ' +
428
+ '.MuiAutocomplete-listbox, .MuiAutocomplete-listbox *, ' +
429
+ '[role="combobox"], [role="combobox"] *, ' +
430
+ '[role="listbox"], [role="listbox"] *, ' +
431
+ '[role="option"], [role="option"] *, ' +
432
+ // MUI Input and FormControl
433
+ '.MuiInputBase-root, .MuiInputBase-root *, ' +
434
+ '.MuiFormControl-root, .MuiFormControl-root *, ' +
435
+ '.MuiOutlinedInput-root, .MuiOutlinedInput-root *, ' +
436
+ // MUI Table components
437
+ '.MuiTablePagination-root, .MuiTablePagination-root *, ' +
438
+ '.MuiTableSortLabel-root, .MuiTableSortLabel-root *, ' +
439
+ // Toolbar and action areas
440
+ '.MuiToolbar-root, .MuiToolbar-root *, ' +
441
+ '[role="toolbar"], [role="toolbar"] *, ' +
442
+ // Popover, Menu, Dialog (for filters, column selection, etc.)
443
+ '.MuiPopover-root, .MuiPopover-root *, ' +
444
+ '.MuiMenu-root, .MuiMenu-root *, ' +
445
+ '.MuiDialog-root, .MuiDialog-root *, ' +
446
+ '.MuiPaper-root[role="dialog"], .MuiPaper-root[role="dialog"] *, ' +
447
+ '[role="menu"], [role="menu"] *, ' +
448
+ '[role="dialog"], [role="dialog"] *, ' +
449
+ '[role="presentation"], [role="presentation"] *'
450
+ ) ;
451
+
452
+ // If clicking UI controls or another activity panel, don't close
453
+ // Otherwise, close or minimize based on pinned state
454
+ if (
455
+ ! isAnotherActivity &&
456
+ ! isResourceLink &&
457
+ ! isInActivityBar &&
458
+ ! isPaginationButton &&
459
+ ! isTableControl
460
+ ) {
461
+ Activity . closeOrMinimize ( id ) ;
462
+ }
463
+ }
464
+ } ;
465
+
466
+ // Add listener immediately (no delay)
467
+ document . addEventListener ( 'mousedown' , handleClickOutside ) ;
468
+
469
+ return ( ) => {
470
+ document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
471
+ } ;
472
+ } , [ id , isOverview , minimized , location , activity . pinned ] ) ;
473
+
338
474
return (
339
475
< ActivityContext . Provider value = { activity } >
340
476
< Box
@@ -850,8 +986,16 @@ export const ActivitiesRenderer = React.memo(function ActivitiesRenderer() {
850
986
}
851
987
} ) ;
852
988
989
+ // Close or minimize the last activity when ESC is pressed
990
+ useHotkeys ( 'Escape' , ( ) => {
991
+ if ( lastElement && ! isOverview ) {
992
+ Activity . closeOrMinimize ( lastElement ) ;
993
+ }
994
+ } ) ;
995
+
853
996
return (
854
997
< >
998
+ { /* Backdrop for overview mode */ }
855
999
< Box
856
1000
sx = { {
857
1001
background : 'rgba(0,0,0,0.1)' ,
@@ -927,6 +1071,7 @@ export const ActivityBar = React.memo(function ({
927
1071
928
1072
return (
929
1073
< Box
1074
+ data-activity-bar = "true"
930
1075
sx = { theme => ( {
931
1076
background : theme . palette . background . muted ,
932
1077
borderTop : '1px solid' ,
@@ -1004,6 +1149,26 @@ export const ActivityBar = React.memo(function ({
1004
1149
</ Box >
1005
1150
</ Box >
1006
1151
</ Button >
1152
+ < Tooltip title = { it . pinned ? t ( 'Unpin' ) : t ( 'Pin' ) } >
1153
+ < IconButton
1154
+ size = "small"
1155
+ onClick = { e => {
1156
+ e . preventDefault ( ) ;
1157
+ e . stopPropagation ( ) ;
1158
+ Activity . update ( it . id , { pinned : ! it . pinned } ) ;
1159
+ } }
1160
+ sx = { {
1161
+ width : '42px' ,
1162
+ height : '100%' ,
1163
+ borderRadius : 1 ,
1164
+ flexShrink : 0 ,
1165
+ color : it . pinned ? 'primary.main' : undefined ,
1166
+ } }
1167
+ aria-label = { it . pinned ? t ( 'Unpin' ) : t ( 'Pin' ) }
1168
+ >
1169
+ < Icon icon = { it . pinned ? 'mdi:pin' : 'mdi:pin-outline' } />
1170
+ </ IconButton >
1171
+ </ Tooltip >
1007
1172
< IconButton
1008
1173
size = "small"
1009
1174
onClick = { e => {
0 commit comments