@@ -1494,6 +1494,95 @@ class CopilotTokenTracker implements vscode.Disposable {
14941494 }
14951495 }
14961496
1497+ // Deduplicate workspace paths that resolve to the same physical repository.
1498+ // Two sources of duplication are handled:
1499+ //
1500+ // 1. Case differences on case-insensitive filesystems (Windows/macOS):
1501+ // Different VS Code variants may store the same folder as "C:\Users\..." vs "c:\users\...".
1502+ // Detected by lowercasing the full path.
1503+ //
1504+ // 2. Remote/devcontainer paths for the same local repo:
1505+ // Opening a devcontainer for a local project stores a vscode-remote:// URI whose
1506+ // resolved fsPath is the *container-internal* path (e.g. "/workspaces/my-repo"),
1507+ // while normal sessions store the local Windows path.
1508+ // Both have the same basename, and one of them is a non-local path
1509+ // (starts with "/workspaces/" or is a Unix-style absolute path on Windows).
1510+ // Detected by matching basename case-insensitively when one entry is a remote path.
1511+ //
1512+ // In both cases: session/interaction counts are summed; customization file scan results
1513+ // are kept from whichever path has more files (the local path wins for scanning).
1514+ {
1515+ const mergeInto = ( winner : string , loser : string ) => {
1516+ workspaceSessionCounts . set ( winner ,
1517+ ( workspaceSessionCounts . get ( winner ) || 0 ) + ( workspaceSessionCounts . get ( loser ) || 0 ) ) ;
1518+ workspaceInteractionCounts . set ( winner ,
1519+ ( workspaceInteractionCounts . get ( winner ) || 0 ) + ( workspaceInteractionCounts . get ( loser ) || 0 ) ) ;
1520+ workspaceSessionCounts . delete ( loser ) ;
1521+ workspaceInteractionCounts . delete ( loser ) ;
1522+ const winnerFiles = this . _customizationFilesCache . get ( winner ) || [ ] ;
1523+ const loserFiles = this . _customizationFilesCache . get ( loser ) || [ ] ;
1524+ if ( winnerFiles . length === 0 && loserFiles . length > 0 ) {
1525+ this . _customizationFilesCache . set ( winner , loserFiles ) ;
1526+ }
1527+ this . _customizationFilesCache . delete ( loser ) ;
1528+ } ;
1529+
1530+ // Helper: true when path looks like a remote/devcontainer path on Windows
1531+ // (Unix-style absolute path, e.g. "/workspaces/repo" or "/home/user/repo")
1532+ const isRemotePath = ( p : string ) => {
1533+ if ( process . platform !== 'win32' ) { return false ; }
1534+ const normalized = p . replace ( / \\ / g, '/' ) ;
1535+ return normalized . startsWith ( '/' ) ;
1536+ } ;
1537+
1538+ // Pass 1 — case-insensitive dedup (covers casing differences between editor variants)
1539+ if ( process . platform === 'win32' || process . platform === 'darwin' ) {
1540+ const lowerToCanonical = new Map < string , string > ( ) ;
1541+ for ( const key of Array . from ( workspaceSessionCounts . keys ( ) ) ) {
1542+ const lower = key . toLowerCase ( ) ;
1543+ if ( ! lowerToCanonical . has ( lower ) ) {
1544+ lowerToCanonical . set ( lower , key ) ;
1545+ } else {
1546+ const canonical = lowerToCanonical . get ( lower ) ! ;
1547+ // Prefer the local (non-remote) path as winner; otherwise more sessions wins
1548+ const canonicalIsRemote = isRemotePath ( canonical ) ;
1549+ const keyIsRemote = isRemotePath ( key ) ;
1550+ const winner = ( ! keyIsRemote && canonicalIsRemote )
1551+ ? key
1552+ : ( ! canonicalIsRemote && keyIsRemote )
1553+ ? canonical
1554+ : ( workspaceSessionCounts . get ( key ) || 0 ) >= ( workspaceSessionCounts . get ( canonical ) || 0 )
1555+ ? key : canonical ;
1556+ const loser = winner === key ? canonical : key ;
1557+ mergeInto ( winner , loser ) ;
1558+ lowerToCanonical . set ( lower , winner ) ;
1559+ }
1560+ }
1561+ }
1562+
1563+ // Pass 2 — basename dedup for remote/devcontainer paths.
1564+ // When one path is a remote (Unix-style) path and another is a local path with the
1565+ // same basename, they represent the same physical repo opened via a devcontainer.
1566+ if ( process . platform === 'win32' ) {
1567+ const basenameToLocal = new Map < string , string > ( ) ; // lower-basename → local path key
1568+ for ( const key of Array . from ( workspaceSessionCounts . keys ( ) ) ) {
1569+ if ( ! isRemotePath ( key ) ) {
1570+ basenameToLocal . set ( path . basename ( key ) . toLowerCase ( ) , key ) ;
1571+ }
1572+ }
1573+ for ( const key of Array . from ( workspaceSessionCounts . keys ( ) ) ) {
1574+ if ( isRemotePath ( key ) ) {
1575+ const base = path . basename ( key ) . toLowerCase ( ) ;
1576+ const localKey = basenameToLocal . get ( base ) ;
1577+ if ( localKey && workspaceSessionCounts . has ( key ) ) {
1578+ // Merge remote into local — local wins because we can scan its files
1579+ mergeInto ( localKey , key ) ;
1580+ }
1581+ }
1582+ }
1583+ }
1584+ }
1585+
14971586 // Build the customization matrix using scanned workspace data and session counts
14981587 try {
14991588 // Unique customization types based on Copilot patterns only
@@ -6570,7 +6659,7 @@ export function activate(context: vscode.ExtensionContext) {
65706659 waterUsagePer1kTokens : 0.3 ,
65716660 co2AbsorptionPerTreePerYear : 21000 ,
65726661 getCopilotSessionFiles : ( ) =>
6573- ( tokenTracker as any ) . getCopilotSessionFiles ( ) ,
6662+ ( tokenTracker as any ) . sessionDiscovery . getCopilotSessionFiles ( ) ,
65746663 estimateTokensFromText : ( text : string , model ?: string ) =>
65756664 ( tokenTracker as any ) . estimateTokensFromText ( text , model ) ,
65766665 getModelFromRequest : ( req : any ) =>
0 commit comments