44 *--------------------------------------------------------------------------------------------*/
55
66import * as vscode from 'vscode' ;
7- import { runningFuncTaskMap } from '../funcCoreTools/funcHostTask' ;
7+ import { runningFuncTaskMap , stoppedFuncTasks , type IStoppedFuncTask } from '../funcCoreTools/funcHostTask' ;
88import { localize } from '../localize' ;
99
1010enum FuncHostDebugContextValue {
1111 HostTask = 'azFunc.funcHostDebug.hostTask' ,
12+ StoppedHostTask = 'azFunc.funcHostDebug.stoppedHostTask' ,
1213 HostError = 'azFunc.funcHostDebug.hostError' ,
1314}
1415
15- type FuncHostDebugNode = INoHostNode | IHostTaskNode | IHostErrorNode ;
16+ type FuncHostDebugNode = INoHostNode | IHostTaskNode | IStoppedHostNode | IHostErrorNode ;
1617
1718interface INoHostNode {
1819 kind : 'noHost' ;
@@ -23,6 +24,12 @@ export interface IHostTaskNode {
2324 workspaceFolder : vscode . WorkspaceFolder | vscode . TaskScope ;
2425 cwd ?: string ;
2526 portNumber : string ;
27+ startTime : Date ;
28+ }
29+
30+ export interface IStoppedHostNode {
31+ kind : 'stoppedHost' ;
32+ stoppedTask : IStoppedFuncTask ;
2633}
2734
2835export interface IHostErrorNode {
@@ -33,6 +40,28 @@ export interface IHostErrorNode {
3340 message : string ;
3441}
3542
43+ function formatTimestamp ( date : Date ) : string {
44+ return date . toLocaleTimeString ( undefined , { hour : '2-digit' , minute : '2-digit' , second : '2-digit' } ) ;
45+ }
46+
47+ function buildHostTooltip ( opts : { label : string ; scopeLabel : string ; portNumber : string ; startTime : Date ; stopTime ?: Date ; cwd ?: string ; pid ?: number } ) : vscode . MarkdownString {
48+ const tooltip = new vscode . MarkdownString ( undefined , true ) ;
49+ tooltip . appendMarkdown ( `**${ opts . label } **\n\n` ) ;
50+ tooltip . appendMarkdown ( `- ${ localize ( 'funcHostDebug.workspace' , 'Workspace' ) } : ${ opts . scopeLabel } \n` ) ;
51+ if ( opts . pid !== undefined ) {
52+ tooltip . appendMarkdown ( `- ${ localize ( 'funcHostDebug.pid' , 'PID' ) } : ${ opts . pid } \n` ) ;
53+ }
54+ tooltip . appendMarkdown ( `- ${ localize ( 'funcHostDebug.port' , 'Port' ) } : ${ opts . portNumber } \n` ) ;
55+ tooltip . appendMarkdown ( `- ${ localize ( 'funcHostDebug.started' , 'Started' ) } : ${ opts . startTime . toLocaleString ( ) } \n` ) ;
56+ if ( opts . stopTime ) {
57+ tooltip . appendMarkdown ( `- ${ localize ( 'funcHostDebug.stopped' , 'Stopped' ) } : ${ opts . stopTime . toLocaleString ( ) } \n` ) ;
58+ }
59+ if ( opts . cwd ) {
60+ tooltip . appendMarkdown ( `- ${ localize ( 'funcHostDebug.cwd' , 'CWD' ) } : ${ opts . cwd } \n` ) ;
61+ }
62+ return tooltip ;
63+ }
64+
3665function getNoHostTreeItem ( ) : vscode . TreeItem {
3766 const item = new vscode . TreeItem ( localize ( 'funcHostDebug.noneRunning' , 'No Function Host task is currently running.' ) , vscode . TreeItemCollapsibleState . None ) ;
3867 item . description = localize ( 'funcHostDebug.startDebuggingHint' , 'Start debugging (F5) to launch the host.' ) ;
@@ -59,23 +88,35 @@ function getHostTaskTreeItem(element: IHostTaskNode): vscode.TreeItem {
5988
6089 const label = localize ( 'funcHostDebug.hostLabel' , 'Function Host ({0})' , element . portNumber ) ;
6190
62- const tooltip = new vscode . MarkdownString ( undefined , true ) ;
63- tooltip . appendMarkdown ( `**${ label } **\n\n` ) ;
64- tooltip . appendMarkdown ( `- ${ localize ( 'funcHostDebug.workspace' , 'Workspace' ) } : ${ scopeLabel } \n` ) ;
65- tooltip . appendMarkdown ( `- ${ localize ( 'funcHostDebug.pid' , 'PID' ) } : ${ task ?. processId ?? localize ( 'funcHostDebug.unknown' , 'Unknown' ) } \n` ) ;
66- tooltip . appendMarkdown ( `- ${ localize ( 'funcHostDebug.port' , 'Port' ) } : ${ element . portNumber } \n` ) ;
67- if ( element . cwd ) {
68- tooltip . appendMarkdown ( `- ${ localize ( 'funcHostDebug.cwd' , 'CWD' ) } : ${ element . cwd } \n` ) ;
69- }
91+ const tooltip = buildHostTooltip ( { label, scopeLabel, portNumber : element . portNumber , startTime : element . startTime , cwd : element . cwd , pid : task ?. processId } ) ;
7092
7193 const item = new vscode . TreeItem ( label , vscode . TreeItemCollapsibleState . Expanded ) ;
72- item . description = scopeLabel ;
94+ item . description = ` ${ scopeLabel } - ${ formatTimestamp ( element . startTime ) } ` ;
7395 item . tooltip = tooltip ;
7496 item . contextValue = FuncHostDebugContextValue . HostTask ;
7597 item . iconPath = new vscode . ThemeIcon ( 'server-process' ) ;
7698 return item ;
7799}
78100
101+ function getStoppedHostTreeItem ( element : IStoppedHostNode ) : vscode . TreeItem {
102+ const stopped = element . stoppedTask ;
103+ const scopeLabel = typeof stopped . workspaceFolder === 'object'
104+ ? stopped . workspaceFolder . name
105+ : localize ( 'funcHostDebug.globalScope' , 'Global' ) ;
106+
107+ const label = localize ( 'funcHostDebug.stoppedHostLabel' , 'Function Host ({0}) — Stopped' , stopped . portNumber ) ;
108+
109+ const tooltip = buildHostTooltip ( { label, scopeLabel, portNumber : stopped . portNumber , startTime : stopped . startTime , stopTime : stopped . stopTime , cwd : stopped . cwd } ) ;
110+
111+ const errorCount = stopped . errorLogs . length ;
112+ const item = new vscode . TreeItem ( label , errorCount > 0 ? vscode . TreeItemCollapsibleState . Expanded : vscode . TreeItemCollapsibleState . None ) ;
113+ item . description = `${ scopeLabel } - ${ formatTimestamp ( stopped . startTime ) } → ${ formatTimestamp ( stopped . stopTime ) } ` ;
114+ item . tooltip = tooltip ;
115+ item . contextValue = FuncHostDebugContextValue . StoppedHostTask ;
116+ item . iconPath = new vscode . ThemeIcon ( 'debug-stop' , new vscode . ThemeColor ( 'disabledForeground' ) ) ;
117+ return item ;
118+ }
119+
79120export class FuncHostDebugViewProvider implements vscode . TreeDataProvider < FuncHostDebugNode > {
80121 private readonly _onDidChangeTreeDataEmitter = new vscode . EventEmitter < FuncHostDebugNode | undefined > ( ) ;
81122 public readonly onDidChangeTreeData = this . _onDidChangeTreeDataEmitter . event ;
@@ -92,6 +133,8 @@ export class FuncHostDebugViewProvider implements vscode.TreeDataProvider<FuncHo
92133 return getHostErrorTreeItem ( element ) ;
93134 case 'hostTask' :
94135 return getHostTaskTreeItem ( element ) ;
136+ case 'stoppedHost' :
137+ return getStoppedHostTreeItem ( element ) ;
95138 default : {
96139 // Exhaustive check: if we reach here, the FuncHostDebugNode union is out of sync with this switch.
97140 throw new Error ( `Unexpected FuncHostDebugNode kind: ${ ( element as { kind ?: unknown } ) . kind } ` ) ;
@@ -103,7 +146,6 @@ export class FuncHostDebugViewProvider implements vscode.TreeDataProvider<FuncHo
103146 if ( element ?. kind === 'hostTask' ) {
104147 const task = runningFuncTaskMap . get ( element . workspaceFolder , element . cwd ) ;
105148 const errors = task ?. errorLogs ?? [ ] ;
106- // Show most recent errors first.
107149 return errors
108150 . slice ( )
109151 . reverse ( )
@@ -114,26 +156,47 @@ export class FuncHostDebugViewProvider implements vscode.TreeDataProvider<FuncHo
114156 portNumber : element . portNumber ,
115157 message,
116158 } ) ) ;
159+ } else if ( element ?. kind === 'stoppedHost' ) {
160+ const stopped = element . stoppedTask ;
161+ return stopped . errorLogs
162+ . slice ( )
163+ . reverse ( )
164+ . map ( ( message ) : IHostErrorNode => ( {
165+ kind : 'hostError' ,
166+ workspaceFolder : stopped . workspaceFolder ,
167+ cwd : stopped . cwd ,
168+ portNumber : stopped . portNumber ,
169+ message,
170+ } ) ) ;
117171 } else if ( element ) {
118172 return [ ] ;
119173 }
120174
121- const hostTasks : IHostTaskNode [ ] = [ ] ;
175+ const nodes : FuncHostDebugNode [ ] = [ ] ;
176+ let hasRunning = false ;
122177
178+ // Running sessions first (newest on top by insertion order).
123179 for ( const folder of vscode . workspace . workspaceFolders ?? [ ] ) {
124180 for ( const t of runningFuncTaskMap . getAll ( folder ) ) {
125181 if ( ! t ) {
126182 continue ;
127183 }
128184 const cwd = ( t . taskExecution . task . execution as vscode . ShellExecution | undefined ) ?. options ?. cwd ;
129- hostTasks . push ( { kind : 'hostTask' , workspaceFolder : folder , cwd, portNumber : t . portNumber } ) ;
185+ nodes . push ( { kind : 'hostTask' , workspaceFolder : folder , cwd, portNumber : t . portNumber , startTime : t . startTime } ) ;
186+ hasRunning = true ;
130187 }
131188 }
132189
133- if ( hostTasks . length === 0 ) {
134- return [ { kind : 'noHost' } ] ;
190+ // Always show the hint node when no host is actively running.
191+ if ( ! hasRunning ) {
192+ nodes . push ( { kind : 'noHost' } ) ;
193+ }
194+
195+ // Stopped sessions (already newest-first in the array).
196+ for ( const stopped of stoppedFuncTasks ) {
197+ nodes . push ( { kind : 'stoppedHost' , stoppedTask : stopped } ) ;
135198 }
136199
137- return hostTasks ;
200+ return nodes ;
138201 }
139202}
0 commit comments