@@ -16,9 +16,10 @@ import { ClaudeDesktopCoworkDataAccess } from '../../vscode-extension/src/claude
1616import { MistralVibeDataAccess } from '../../vscode-extension/src/mistralvibe' ;
1717import type { IEcosystemAdapter } from '../../vscode-extension/src/ecosystemAdapter' ;
1818import { OpenCodeAdapter , CrushAdapter , ContinueAdapter , ClaudeDesktopAdapter , ClaudeCodeAdapter , VisualStudioAdapter , MistralVibeAdapter , CopilotChatAdapter , CopilotCliAdapter } from '../../vscode-extension/src/adapters' ;
19+ import { isMcpTool , extractMcpServerName } from '../../vscode-extension/src/workspaceHelpers' ;
1920import { parseSessionFileContent } from '../../vscode-extension/src/sessionParser' ;
2021import { estimateTokensFromText , getModelFromRequest , isJsonlContent , estimateTokensFromJsonlSession , calculateEstimatedCost , getModelTier } from '../../vscode-extension/src/tokenEstimation' ;
21- import type { DetailedStats , PeriodStats , ModelUsage , EditorUsage , SessionFileCache , UsageAnalysisStats , UsageAnalysisPeriod } from '../../vscode-extension/src/types' ;
22+ import type { DetailedStats , PeriodStats , ModelUsage , EditorUsage , SessionFileCache , UsageAnalysisStats , UsageAnalysisPeriod , WorkspaceCustomizationMatrix } from '../../vscode-extension/src/types' ;
2223import { analyzeSessionUsage , mergeUsageAnalysis , calculateModelSwitching , trackEnhancedMetrics } from '../../vscode-extension/src/usageAnalysis' ;
2324import { createEmptyContextRefs } from '../../vscode-extension/src/tokenEstimation' ;
2425import * as vscodeStub from './vscode-stub' ;
@@ -95,7 +96,12 @@ const _ecosystems: IEcosystemAdapter[] = [
9596 new CrushAdapter ( _crushInstance ) ,
9697 new VisualStudioAdapter ( _visualStudioInstance , ( t , m ) => estimateTokensFromText ( t , m ?? 'gpt-4' , tokenEstimators ) ) ,
9798 new ContinueAdapter ( _continueInstance ) ,
98- new ClaudeDesktopAdapter ( _claudeDesktopCoworkInstance ) ,
99+ new ClaudeDesktopAdapter (
100+ _claudeDesktopCoworkInstance ,
101+ isMcpTool ,
102+ extractMcpServerName ,
103+ ( t , m ) => estimateTokensFromText ( t , m ?? 'gpt-4' , tokenEstimators )
104+ ) ,
99105 new ClaudeCodeAdapter ( _claudeCodeInstance ) ,
100106 new MistralVibeAdapter ( _mistralVibeInstance ) ,
101107 // Copilot Chat / CLI adapters: discovery-only. Their handles() returns
@@ -116,6 +122,79 @@ export async function discoverSessionFiles(): Promise<string[]> {
116122 return discovery . getCopilotSessionFiles ( ) ;
117123}
118124
125+ /**
126+ * Builds a WorkspaceCustomizationMatrix from session file paths.
127+ *
128+ * - For VS Code sessions: derives workspace folder from workspaceStorage/<hash>/workspace.json,
129+ * then checks for .github/copilot-instructions.md, agents.md, or CLAUDE.md.
130+ * - For Claude Code sessions (~/.claude/projects/<hash>/): reads the JSONL to extract the
131+ * `cwd` workspace path, then checks for CLAUDE.md there.
132+ */
133+ export async function buildCustomizationMatrix ( sessionFiles : string [ ] ) : Promise < WorkspaceCustomizationMatrix | undefined > {
134+ const workspacePaths = new Set < string > ( ) ;
135+ const claudeBasePath = path . join ( os . homedir ( ) , '.claude' , 'projects' ) ;
136+
137+ for ( const sessionFile of sessionFiles ) {
138+ // Claude Code session: ~/.claude/projects/<hash>/<uuid>.jsonl
139+ if ( sessionFile . startsWith ( claudeBasePath + path . sep ) || sessionFile . startsWith ( claudeBasePath + '/' ) ) {
140+ try {
141+ const content = await fs . promises . readFile ( sessionFile , 'utf-8' ) ;
142+ const lines = content . split ( '\n' ) . slice ( 0 , 30 ) ;
143+ for ( const line of lines ) {
144+ if ( ! line . trim ( ) ) { continue ; }
145+ try {
146+ const event = JSON . parse ( line ) ;
147+ if ( event . cwd && typeof event . cwd === 'string' ) {
148+ workspacePaths . add ( event . cwd ) ;
149+ break ;
150+ }
151+ } catch { /* skip malformed lines */ }
152+ }
153+ } catch { /* skip unreadable files */ }
154+ continue ;
155+ }
156+
157+ // VS Code session: .../workspaceStorage/<hash>/chatSessions/<file>
158+ const chatSessionsDir = path . dirname ( sessionFile ) ;
159+ if ( path . basename ( chatSessionsDir ) !== 'chatSessions' ) { continue ; }
160+ const hashDir = path . dirname ( chatSessionsDir ) ;
161+ const workspaceJsonPath = path . join ( hashDir , 'workspace.json' ) ;
162+
163+ try {
164+ if ( ! fs . existsSync ( workspaceJsonPath ) ) { continue ; }
165+ const content = JSON . parse ( await fs . promises . readFile ( workspaceJsonPath , 'utf-8' ) ) ;
166+ const folderUri : string | undefined = content . folder ;
167+ if ( ! folderUri || ! folderUri . startsWith ( 'file://' ) ) { continue ; }
168+
169+ let folderPath = decodeURIComponent ( folderUri . replace ( / ^ f i l e : \/ \/ / , '' ) ) ;
170+ // On Windows, file:///C:/... becomes /C:/... — strip the leading slash
171+ if ( / ^ \/ [ A - Z a - z ] : / . test ( folderPath ) ) { folderPath = folderPath . slice ( 1 ) ; }
172+ workspacePaths . add ( folderPath ) ;
173+ } catch { /* skip unreadable workspace.json files */ }
174+ }
175+
176+ if ( workspacePaths . size === 0 ) { return undefined ; }
177+
178+ let workspacesWithIssues = 0 ;
179+ for ( const wsPath of workspacePaths ) {
180+ try {
181+ const hasInstructions = fs . existsSync ( path . join ( wsPath , '.github' , 'copilot-instructions.md' ) ) ;
182+ const hasAgentsMd = fs . existsSync ( path . join ( wsPath , 'agents.md' ) ) ;
183+ const hasClaudeMd = fs . existsSync ( path . join ( wsPath , 'CLAUDE.md' ) ) ;
184+ if ( ! hasInstructions && ! hasAgentsMd && ! hasClaudeMd ) { workspacesWithIssues ++ ; }
185+ } catch {
186+ workspacesWithIssues ++ ;
187+ }
188+ }
189+
190+ return {
191+ customizationTypes : [ ] ,
192+ workspaces : [ ] ,
193+ totalWorkspaces : workspacePaths . size ,
194+ workspacesWithIssues,
195+ } ;
196+ }
197+
119198/** Get diagnostic candidate paths info */
120199export function getDiagnosticPaths ( ) : { path : string ; exists : boolean ; source : string } [ ] {
121200 const discovery = createSessionDiscovery ( ) ;
0 commit comments