@@ -72,6 +72,8 @@ export interface Activity {
7272 temporary ?: boolean ;
7373 /** Cluster of the launched activity */
7474 cluster ?: string ;
75+ /** Whether this activity is pinned (won't close on click-outside) */
76+ pinned ?: boolean ;
7577}
7678
7779export interface ActivityState {
@@ -178,6 +180,21 @@ export const Activity = {
178180 update ( id : string , diff : Partial < Activity > ) {
179181 store . dispatch ( activitySlice . actions . update ( { ...diff , id } ) ) ;
180182 } ,
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+ } ,
181198 reset ( ) {
182199 store . dispatch ( activitySlice . actions . reset ( ) ) ;
183200 } ,
@@ -335,6 +352,125 @@ export function SingleActivityRenderer({
335352 } ;
336353 } , [ location ] ) ;
337354
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+
338474 return (
339475 < ActivityContext . Provider value = { activity } >
340476 < Box
@@ -850,8 +986,16 @@ export const ActivitiesRenderer = React.memo(function ActivitiesRenderer() {
850986 }
851987 } ) ;
852988
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+
853996 return (
854997 < >
998+ { /* Backdrop for overview mode */ }
855999 < Box
8561000 sx = { {
8571001 background : 'rgba(0,0,0,0.1)' ,
@@ -927,6 +1071,7 @@ export const ActivityBar = React.memo(function ({
9271071
9281072 return (
9291073 < Box
1074+ data-activity-bar = "true"
9301075 sx = { theme => ( {
9311076 background : theme . palette . background . muted ,
9321077 borderTop : '1px solid' ,
@@ -1004,6 +1149,26 @@ export const ActivityBar = React.memo(function ({
10041149 </ Box >
10051150 </ Box >
10061151 </ 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 >
10071172 < IconButton
10081173 size = "small"
10091174 onClick = { e => {
0 commit comments