@@ -201,6 +201,8 @@ class CopilotTokenTracker implements vscode.Disposable {
201201 private diagnosticsHasLoadedFiles : boolean = false ;
202202 // Cache of the last loaded detailed session files for diagnostics view
203203 private diagnosticsCachedFiles : SessionFileDetails [ ] = [ ] ;
204+ // Cache of the last diagnostic report text for copy/issue operations
205+ private lastDiagnosticReport : string = '' ;
204206 private logViewerPanel ?: vscode . WebviewPanel ;
205207 private statusBarItem : vscode . StatusBarItem ;
206208 private readonly extensionUri : vscode . Uri ;
@@ -3469,91 +3471,16 @@ class CopilotTokenTracker implements vscode.Disposable {
34693471 public async showDiagnosticReport ( ) : Promise < void > {
34703472 this . log ( '🔍 Opening Diagnostic Report' ) ;
34713473
3472- // If panel already exists, just reveal it and update content
3474+ // If panel already exists, just reveal it and trigger a refresh in the background
34733475 if ( this . diagnosticsPanel ) {
34743476 this . diagnosticsPanel . reveal ( ) ;
34753477 this . log ( '🔍 Diagnostic Report revealed (already exists)' ) ;
3476- // Optionally, refresh content if needed
3477- const report = await this . generateDiagnosticReport ( ) ;
3478- const sessionFiles = await this . getCopilotSessionFiles ( ) ;
3479- const sessionFileData : { file : string ; size : number ; modified : string } [ ] = [ ] ;
3480- for ( const file of sessionFiles . slice ( 0 , 20 ) ) {
3481- try {
3482- const stat = await fs . promises . stat ( file ) ;
3483- sessionFileData . push ( {
3484- file,
3485- size : stat . size ,
3486- modified : stat . mtime . toISOString ( )
3487- } ) ;
3488- } catch {
3489- // Skip inaccessible files
3490- }
3491- }
3492- // Build folder counts grouped by top-level VS Code user folder (editor roots)
3493- const dirCounts = new Map < string , number > ( ) ;
3494- const pathModule = require ( 'path' ) ;
3495- for ( const file of sessionFiles ) {
3496- // Walk up the path to find the 'User' directory which is the canonical editor folder root
3497- const parts = file . split ( / [ \\ \/ ] / ) ;
3498- // Find index of 'User' folder in path parts (case-insensitive)
3499- const userIdx = parts . findIndex ( p => p . toLowerCase ( ) === 'user' ) ;
3500- let editorRoot = '' ;
3501- if ( userIdx > 0 ) {
3502- // Reconstruct path including 'User' and the next folder (e.g., .../Roaming/Code/User/workspaceStorage)
3503- // Include two extra levels after the 'User' segment so we can distinguish
3504- // between 'User\\workspaceStorage' and 'User\\globalStorage'.
3505- const rootParts = parts . slice ( 0 , Math . min ( parts . length , userIdx + 2 ) ) ;
3506- editorRoot = pathModule . join ( ...rootParts ) ;
3507- } else {
3508- // Fallback: use parent dir of the file
3509- editorRoot = pathModule . dirname ( file ) ;
3510- }
3511-
3512- dirCounts . set ( editorRoot , ( dirCounts . get ( editorRoot ) || 0 ) + 1 ) ;
3513- }
3514- const sessionFolders = Array . from ( dirCounts . entries ( ) ) . map ( ( [ dir , count ] ) => ( { dir, count, editorName : this . getEditorTypeFromPath ( dir ) } ) ) ;
3515- const backendStorageInfo = await this . getBackendStorageInfo ( ) ;
3516- this . diagnosticsPanel . webview . html = this . getDiagnosticReportHtml ( this . diagnosticsPanel . webview , report , sessionFileData , [ ] , sessionFolders , backendStorageInfo ) ;
3517- this . loadSessionFilesInBackground ( this . diagnosticsPanel , sessionFiles ) ;
3478+ // Load data in background and update the webview
3479+ this . loadDiagnosticDataInBackground ( this . diagnosticsPanel ) ;
35183480 return ;
35193481 }
35203482
3521- const report = await this . generateDiagnosticReport ( ) ;
3522- const sessionFiles = await this . getCopilotSessionFiles ( ) ;
3523- const sessionFileData : { file : string ; size : number ; modified : string } [ ] = [ ] ;
3524- for ( const file of sessionFiles . slice ( 0 , 20 ) ) {
3525- try {
3526- const stat = await fs . promises . stat ( file ) ;
3527- sessionFileData . push ( {
3528- file,
3529- size : stat . size ,
3530- modified : stat . mtime . toISOString ( )
3531- } ) ;
3532- } catch {
3533- // Skip inaccessible files
3534- }
3535- }
3536-
3537- // Build folder counts grouped by top-level VS Code user folder (editor roots)
3538- const dirCounts = new Map < string , number > ( ) ;
3539- const pathModule = require ( 'path' ) ;
3540- for ( const file of sessionFiles ) {
3541- const parts = file . split ( / [ \\ \/ ] / ) ;
3542- const userIdx = parts . findIndex ( p => p . toLowerCase ( ) === 'user' ) ;
3543- let editorRoot = '' ;
3544- if ( userIdx > 0 ) {
3545- // Include 'User' plus one following folder (e.g., 'User\\workspaceStorage' or 'User\\globalStorage')
3546- const rootParts = parts . slice ( 0 , Math . min ( parts . length , userIdx + 2 ) ) ;
3547- editorRoot = pathModule . join ( ...rootParts ) ;
3548- } else {
3549- editorRoot = pathModule . dirname ( file ) ;
3550- }
3551- dirCounts . set ( editorRoot , ( dirCounts . get ( editorRoot ) || 0 ) + 1 ) ;
3552- }
3553- const sessionFolders = Array . from ( dirCounts . entries ( ) ) . map ( ( [ dir , count ] ) => ( { dir, count, editorName : this . getEditorNameFromRoot ( dir ) } ) ) ;
3554-
3555- const backendStorageInfo = await this . getBackendStorageInfo ( ) ;
3556-
3483+ // Create the panel immediately with loading state
35573484 this . diagnosticsPanel = vscode . window . createWebviewPanel (
35583485 'copilotTokenDiagnostics' ,
35593486 'Diagnostic Report' ,
@@ -3568,21 +3495,30 @@ class CopilotTokenTracker implements vscode.Disposable {
35683495 }
35693496 ) ;
35703497
3571- this . log ( '✅ Diagnostic Report created successfully' ) ;
3572-
3573- // Set the HTML content immediately with empty session files (shows loading state)
3574- this . diagnosticsPanel . webview . html = this . getDiagnosticReportHtml ( this . diagnosticsPanel . webview , report , sessionFileData , [ ] , sessionFolders , backendStorageInfo ) ;
3498+ this . log ( '✅ Diagnostic Report panel created' ) ;
3499+
3500+ // Set the HTML content immediately with loading state
3501+ // Note: "Loading..." is the agreed contract between backend and frontend
3502+ // The webview checks for this value to show a loading indicator
3503+ this . diagnosticsPanel . webview . html = this . getDiagnosticReportHtml (
3504+ this . diagnosticsPanel . webview ,
3505+ 'Loading...' , // Placeholder report
3506+ [ ] , // Empty session files
3507+ [ ] , // Empty detailed session files
3508+ [ ] , // Empty session folders
3509+ null // No backend info yet
3510+ ) ;
35753511
35763512 // Handle messages from the webview
35773513 this . diagnosticsPanel . webview . onDidReceiveMessage ( async ( message ) => {
35783514 this . log ( `DEBUG Diagnostics webview message: ${ JSON . stringify ( message ) } ` ) ;
35793515 switch ( message . command ) {
35803516 case 'copyReport' :
3581- await vscode . env . clipboard . writeText ( report ) ;
3517+ await vscode . env . clipboard . writeText ( this . lastDiagnosticReport ) ;
35823518 vscode . window . showInformationMessage ( 'Diagnostic report copied to clipboard' ) ;
35833519 break ;
35843520 case 'openIssue' :
3585- await vscode . env . clipboard . writeText ( report ) ;
3521+ await vscode . env . clipboard . writeText ( this . lastDiagnosticReport ) ;
35863522 vscode . window . showInformationMessage ( 'Diagnostic report copied to clipboard. Please paste it into the GitHub issue.' ) ;
35873523 const shortBody = encodeURIComponent ( 'The diagnostic report has been copied to the clipboard. Please paste it below.' ) ;
35883524 const issueUrl = `${ this . getRepositoryUrl ( ) } /issues/new?body=${ shortBody } ` ;
@@ -3676,8 +3612,100 @@ class CopilotTokenTracker implements vscode.Disposable {
36763612 this . diagnosticsPanel = undefined ;
36773613 } ) ;
36783614
3679- // Load detailed session files in the background and send to webview when ready
3680- this . loadSessionFilesInBackground ( this . diagnosticsPanel , sessionFiles ) ;
3615+ // Load data in background and update the webview when ready
3616+ this . loadDiagnosticDataInBackground ( this . diagnosticsPanel ) ;
3617+ }
3618+
3619+ /**
3620+ * Load all diagnostic data in the background and update the webview progressively.
3621+ */
3622+ private async loadDiagnosticDataInBackground ( panel : vscode . WebviewPanel ) : Promise < void > {
3623+ try {
3624+ this . log ( '🔄 Loading diagnostic data in background...' ) ;
3625+
3626+ // Load the diagnostic report
3627+ const report = await this . generateDiagnosticReport ( ) ;
3628+ this . lastDiagnosticReport = report ;
3629+
3630+ // Get session files
3631+ const sessionFiles = await this . getCopilotSessionFiles ( ) ;
3632+
3633+ // Get first 20 session files with stats (quick preview)
3634+ const sessionFileData : { file : string ; size : number ; modified : string } [ ] = [ ] ;
3635+ for ( const file of sessionFiles . slice ( 0 , 20 ) ) {
3636+ try {
3637+ const stat = await fs . promises . stat ( file ) ;
3638+ sessionFileData . push ( {
3639+ file,
3640+ size : stat . size ,
3641+ modified : stat . mtime . toISOString ( )
3642+ } ) ;
3643+ } catch {
3644+ // Skip inaccessible files
3645+ }
3646+ }
3647+
3648+ // Build folder counts grouped by top-level VS Code user folder (editor roots)
3649+ const dirCounts = new Map < string , number > ( ) ;
3650+ const pathModule = require ( 'path' ) ;
3651+ for ( const file of sessionFiles ) {
3652+ const parts = file . split ( / [ \\ \/ ] / ) ;
3653+ const userIdx = parts . findIndex ( ( p : string ) => p . toLowerCase ( ) === 'user' ) ;
3654+ let editorRoot = '' ;
3655+ if ( userIdx > 0 ) {
3656+ const rootParts = parts . slice ( 0 , Math . min ( parts . length , userIdx + 2 ) ) ;
3657+ editorRoot = pathModule . join ( ...rootParts ) ;
3658+ } else {
3659+ editorRoot = pathModule . dirname ( file ) ;
3660+ }
3661+ dirCounts . set ( editorRoot , ( dirCounts . get ( editorRoot ) || 0 ) + 1 ) ;
3662+ }
3663+ const sessionFolders = Array . from ( dirCounts . entries ( ) ) . map ( ( [ dir , count ] ) => ( {
3664+ dir,
3665+ count,
3666+ editorName : this . getEditorNameFromRoot ( dir )
3667+ } ) ) ;
3668+
3669+ // Get backend storage info
3670+ const backendStorageInfo = await this . getBackendStorageInfo ( ) ;
3671+
3672+ // Check if panel is still open before updating
3673+ if ( ! this . isPanelOpen ( panel ) ) {
3674+ this . log ( 'Diagnostic panel closed during data load, aborting update' ) ;
3675+ return ;
3676+ }
3677+
3678+ // Send the loaded data to the webview
3679+ panel . webview . postMessage ( {
3680+ command : 'diagnosticDataLoaded' ,
3681+ report,
3682+ sessionFiles : sessionFileData ,
3683+ sessionFolders,
3684+ backendStorageInfo
3685+ } ) ;
3686+
3687+ this . log ( '✅ Diagnostic data loaded and sent to webview' ) ;
3688+
3689+ // Now load detailed session files in the background
3690+ this . loadSessionFilesInBackground ( panel , sessionFiles ) ;
3691+ } catch ( error ) {
3692+ this . error ( `Failed to load diagnostic data: ${ error } ` ) ;
3693+ // Send error to webview if panel is still open
3694+ if ( this . isPanelOpen ( panel ) ) {
3695+ panel . webview . postMessage ( {
3696+ command : 'diagnosticDataError' ,
3697+ error : String ( error )
3698+ } ) ;
3699+ }
3700+ }
3701+ }
3702+
3703+ /**
3704+ * Check if a webview panel is still open and accessible.
3705+ * A panel is considered open if its viewColumn is defined.
3706+ */
3707+ private isPanelOpen ( panel : vscode . WebviewPanel ) : boolean {
3708+ return panel . viewColumn !== undefined ;
36813709 }
36823710
36833711 /**
@@ -3711,7 +3739,7 @@ class CopilotTokenTracker implements vscode.Disposable {
37113739 // Process up to 500 most recent session files
37123740 for ( const file of sortedFiles . slice ( 0 , 500 ) ) {
37133741 // Check if panel was disposed
3714- if ( ! panel . visible && panel . viewColumn === undefined ) {
3742+ if ( ! this . isPanelOpen ( panel ) ) {
37153743 this . log ( 'Diagnostic panel closed, stopping background load' ) ;
37163744 return ;
37173745 }
0 commit comments