@@ -36,6 +36,9 @@ define(function (require, exports, module) {
3636 const ExtensionUtils = require ( "utils/ExtensionUtils" ) ;
3737 const NodeConnector = require ( "NodeConnector" ) ;
3838 const Mustache = require ( "thirdparty/mustache/mustache" ) ;
39+ const Dialogs = require ( "widgets/Dialogs" ) ;
40+ const Strings = require ( "strings" ) ;
41+ const StringUtils = require ( "utils/StringUtils" ) ;
3942
4043 const TerminalInstance = require ( "./TerminalInstance" ) ;
4144 const ShellProfiles = require ( "./ShellProfiles" ) ;
@@ -50,12 +53,31 @@ define(function (require, exports, module) {
5053 const PANEL_ID = "terminal-panel" ;
5154 const PANEL_MIN_SIZE = 100 ;
5255
56+ // Shell process names — if the foreground process is one of these, no child is running
57+ const SHELL_NAMES = new Set ( [
58+ "bash" , "zsh" , "fish" , "sh" , "dash" , "ksh" , "csh" , "tcsh" ,
59+ "pwsh" , "powershell" , "cmd.exe" , "nu" , "elvish" , "xonsh" ,
60+ "login"
61+ ] ) ;
62+
63+ /**
64+ * Check if a process name is a shell (handles full paths like /bin/bash)
65+ */
66+ function _isShellProcess ( processName ) {
67+ if ( ! processName ) {
68+ return true ;
69+ }
70+ const basename = processName . split ( "/" ) . pop ( ) . split ( "\\" ) . pop ( ) ;
71+ return SHELL_NAMES . has ( basename ) ;
72+ }
73+
5374 // State
5475 let panel = null ;
5576 let nodeConnector = null ;
5677 let terminalInstances = [ ] ; // All terminal instances
5778 let activeTerminalId = null ; // Currently visible terminal
58- let $panel , $tabsList , $contentArea , $shellDropdown ;
79+ let processInfo = { } ; // id -> processName from PTY
80+ let $panel , $contentArea , $shellDropdown , $flyoutList ;
5981
6082 /**
6183 * Create a new NodeConnector for terminal communication
@@ -81,18 +103,21 @@ define(function (require, exports, module) {
81103 panel = WorkspaceManager . createBottomPanel ( PANEL_ID , $panel , PANEL_MIN_SIZE ) ;
82104
83105 // Cache DOM references
84- $tabsList = $panel . find ( ".terminal-tabs-list" ) ;
85106 $contentArea = $panel . find ( ".terminal-content-area" ) ;
86107 $shellDropdown = $panel . find ( ".terminal-shell-dropdown" ) ;
108+ $flyoutList = $panel . find ( ".terminal-flyout-list" ) ;
87109
88- // "+" button always creates a new terminal with the default shell
89- $panel . find ( ".terminal-tab -new-btn" ) . on ( "click" , function ( e ) {
110+ // "+" button creates a new terminal with the default shell
111+ $panel . find ( ".terminal-flyout -new-btn" ) . on ( "click" , function ( e ) {
90112 e . stopPropagation ( ) ;
91113 _createNewTerminal ( ) ;
92114 } ) ;
93115
94116 // Dropdown chevron button toggles shell selector
95- $panel . find ( ".terminal-tab-dropdown-btn" ) . on ( "click" , _onDropdownButtonClick ) ;
117+ $panel . find ( ".terminal-flyout-dropdown-btn" ) . on ( "click" , _onDropdownButtonClick ) ;
118+
119+ // Refresh process info when user hovers over the flyout
120+ $panel . find ( ".terminal-tab-flyout" ) . on ( "mouseenter" , _refreshAllProcesses ) ;
96121
97122 // Listen for panel resize
98123 WorkspaceManager . on ( "workspaceUpdateLayout" , _handleResize ) ;
@@ -204,10 +229,7 @@ define(function (require, exports, module) {
204229 // Add to list
205230 terminalInstances . push ( instance ) ;
206231
207- // Create tab
208- _createTab ( instance ) ;
209-
210- // Activate this terminal
232+ // Activate this terminal (also updates flyout)
211233 _activateTerminal ( instance . id ) ;
212234
213235 // Show panel if hidden
@@ -220,39 +242,12 @@ define(function (require, exports, module) {
220242 await instance . spawn ( ) ;
221243 }
222244
223- /**
224- * Create a tab element for a terminal instance
225- */
226- function _createTab ( instance ) {
227- const $tab = $ ( '<div class="terminal-tab" data-terminal-id="' + instance . id + '" title="' + _escapeHtml ( instance . title ) + '">' +
228- '<i class="fa-solid fa-terminal terminal-tab-icon"></i>' +
229- '<span class="terminal-tab-close"><i class="fa-solid fa-xmark"></i></span>' +
230- '</div>' ) ;
231-
232- $tab . on ( "click" , function ( e ) {
233- if ( ! $ ( e . target ) . closest ( ".terminal-tab-close" ) . length ) {
234- _activateTerminal ( instance . id ) ;
235- }
236- } ) ;
237-
238- $tab . find ( ".terminal-tab-close" ) . on ( "click" , function ( e ) {
239- e . stopPropagation ( ) ;
240- _closeTerminal ( instance . id ) ;
241- } ) ;
242-
243- $tabsList . append ( $tab ) ;
244- }
245-
246245 /**
247246 * Activate a terminal tab (show it, hide others)
248247 */
249248 function _activateTerminal ( id ) {
250249 activeTerminalId = id ;
251250
252- // Update tabs
253- $tabsList . find ( ".terminal-tab" ) . removeClass ( "active" ) ;
254- $tabsList . find ( '.terminal-tab[data-terminal-id="' + id + '"]' ) . addClass ( "active" ) ;
255-
256251 // Show/hide terminal containers
257252 for ( const inst of terminalInstances ) {
258253 if ( inst . id === id ) {
@@ -261,23 +256,46 @@ define(function (require, exports, module) {
261256 inst . hide ( ) ;
262257 }
263258 }
259+
260+ _updateFlyout ( ) ;
264261 }
265262
266263 /**
267- * Close a terminal instance
264+ * Close a terminal instance, confirming first if a child process is running
268265 */
269- function _closeTerminal ( id ) {
266+ async function _closeTerminal ( id ) {
270267 const idx = terminalInstances . findIndex ( t => t . id === id ) ;
271268 if ( idx === - 1 ) {
272269 return ;
273270 }
274271
275272 const instance = terminalInstances [ idx ] ;
273+
274+ // Check for active child process before closing
275+ if ( instance . isAlive ) {
276+ try {
277+ const result = await nodeConnector . execPeer ( "getTerminalProcess" , { id} ) ;
278+ const processName = result . process || "" ;
279+ if ( processName && ! _isShellProcess ( processName ) ) {
280+ const message = StringUtils . format (
281+ Strings . TERMINAL_CLOSE_CONFIRM_MSG , _escapeHtml ( processName )
282+ ) ;
283+ const dialog = Dialogs . showConfirmDialog (
284+ Strings . TERMINAL_CLOSE_CONFIRM_TITLE , message
285+ ) ;
286+ const buttonId = await dialog . getPromise ( ) ;
287+ if ( buttonId !== Dialogs . DIALOG_BTN_OK ) {
288+ return ;
289+ }
290+ }
291+ } catch ( e ) {
292+ // Terminal may already be dead; proceed with close
293+ }
294+ }
295+
276296 instance . dispose ( ) ;
277297 terminalInstances . splice ( idx , 1 ) ;
278-
279- // Remove tab
280- $tabsList . find ( '.terminal-tab[data-terminal-id="' + id + '"]' ) . remove ( ) ;
298+ delete processInfo [ id ] ;
281299
282300 // If we closed the active terminal, activate another
283301 if ( activeTerminalId === id ) {
@@ -294,6 +312,8 @@ define(function (require, exports, module) {
294312 panel . hide ( ) ;
295313 _updateToolbarIcon ( false ) ;
296314 }
315+
316+ _updateFlyout ( ) ;
297317 }
298318
299319 /**
@@ -326,20 +346,105 @@ define(function (require, exports, module) {
326346 }
327347
328348 /**
329- * Handle terminal title change
349+ * Handle terminal title change — also fetches and displays the foreground process
330350 */
331351 function _onTerminalTitleChanged ( id , title ) {
332- const $tab = $tabsList . find ( '.terminal-tab[data-terminal-id="' + id + '"]' ) ;
333- $tab . attr ( "title" , title ) ;
352+ _updateFlyout ( ) ;
353+ _updateTabProcess ( id ) ;
354+ }
355+
356+ /**
357+ * Fetch and display the foreground process for a terminal tab
358+ */
359+ function _updateTabProcess ( id ) {
360+ const instance = terminalInstances . find ( t => t . id === id ) ;
361+ if ( ! instance || ! instance . isAlive ) {
362+ return ;
363+ }
364+ nodeConnector . execPeer ( "getTerminalProcess" , { id} ) . then ( function ( result ) {
365+ const newProc = result . process || "" ;
366+ if ( processInfo [ id ] !== newProc ) {
367+ processInfo [ id ] = newProc ;
368+ _updateFlyout ( ) ;
369+ }
370+ } ) . catch ( function ( ) {
371+ // Terminal may have been closed; ignore
372+ } ) ;
373+ }
374+
375+ /**
376+ * Refresh process info for all alive terminals.
377+ * Called on flyout hover so the tab bar is up-to-date when the user looks.
378+ */
379+ function _refreshAllProcesses ( ) {
380+ for ( const inst of terminalInstances ) {
381+ if ( inst . isAlive ) {
382+ _updateTabProcess ( inst . id ) ;
383+ }
384+ }
385+ }
386+
387+ /**
388+ * Rebuild the flyout panel to reflect current tabs
389+ */
390+ /**
391+ * Extract the last directory name from a terminal title.
392+ * Title format is typically "user@host: /path/to/dir" or "user@host: ~/path/to/dir".
393+ */
394+ function _extractCwdBasename ( title ) {
395+ const colonIdx = title . indexOf ( ": " ) ;
396+ const pathPart = colonIdx >= 0 ? title . slice ( colonIdx + 2 ) : title ;
397+ const trimmed = pathPart . replace ( / \/ + $ / , "" ) ;
398+ const lastSlash = trimmed . lastIndexOf ( "/" ) ;
399+ return lastSlash >= 0 ? trimmed . slice ( lastSlash + 1 ) : trimmed ;
400+ }
401+
402+ function _updateFlyout ( ) {
403+ $flyoutList . empty ( ) ;
404+ for ( const inst of terminalInstances ) {
405+ const proc = processInfo [ inst . id ] || "" ;
406+ const basename = proc ? proc . split ( "/" ) . pop ( ) . split ( "\\" ) . pop ( ) : "" ;
407+
408+ // Label: process basename; right side: cwd basename; tooltip: full title
409+ const label = basename || "Terminal" ;
410+ const cwdName = _extractCwdBasename ( inst . title ) ;
411+
412+ const $item = $ ( '<div class="terminal-flyout-item"></div>' )
413+ . attr ( "data-terminal-id" , inst . id )
414+ . attr ( "title" , inst . title )
415+ . toggleClass ( "active" , inst . id === activeTerminalId ) ;
416+
417+ if ( ! inst . isAlive ) {
418+ $item . css ( "opacity" , "0.6" ) ;
419+ }
420+
421+ $item . append ( '<span class="terminal-flyout-icon"><i class="fa-solid fa-terminal"></i></span>' ) ;
422+ $item . append ( $ ( '<span class="terminal-flyout-title"></span>' ) . text ( label ) ) ;
423+ if ( cwdName ) {
424+ $item . append ( $ ( '<span class="terminal-flyout-cwd"></span>' ) . text ( cwdName ) ) ;
425+ }
426+ $item . append ( '<span class="terminal-flyout-close"><i class="fa-solid fa-xmark"></i></span>' ) ;
427+
428+ $item . on ( "click" , function ( e ) {
429+ if ( ! $ ( e . target ) . closest ( ".terminal-flyout-close" ) . length ) {
430+ _activateTerminal ( inst . id ) ;
431+ }
432+ } ) ;
433+ $item . find ( ".terminal-flyout-close" ) . on ( "click" , function ( e ) {
434+ e . stopPropagation ( ) ;
435+ _closeTerminal ( inst . id ) ;
436+ } ) ;
437+
438+ $flyoutList . append ( $item ) ;
439+ }
334440 }
335441
336442 /**
337443 * Handle terminal process exit
338444 */
339445 function _onTerminalProcessExit ( id , exitCode ) {
340- // Update tab styling to indicate dead process
341- const $tab = $tabsList . find ( '.terminal-tab[data-terminal-id="' + id + '"]' ) ;
342- $tab . css ( "opacity" , "0.6" ) ;
446+ delete processInfo [ id ] ;
447+ _updateFlyout ( ) ;
343448 }
344449
345450 /**
@@ -412,6 +517,7 @@ define(function (require, exports, module) {
412517 inst . dispose ( ) ;
413518 }
414519 terminalInstances = [ ] ;
520+ processInfo = { } ;
415521 }
416522
417523 // Register commands
@@ -443,7 +549,7 @@ define(function (require, exports, module) {
443549 ShellProfiles . init ( nodeConnector ) . then ( function ( ) {
444550 const shells = ShellProfiles . getShells ( ) ;
445551 if ( shells . length <= 1 ) {
446- $panel . find ( ".terminal-tab -dropdown-btn" ) . addClass ( "forced-hidden" ) ;
552+ $panel . find ( ".terminal-flyout -dropdown-btn" ) . addClass ( "forced-hidden" ) ;
447553 }
448554 _populateShellDropdown ( ) ;
449555 } ) ;
0 commit comments