diff --git a/vscode-extension/src/cacheManager.ts b/vscode-extension/src/cacheManager.ts index b8f1daa7..13cddb47 100644 --- a/vscode-extension/src/cacheManager.ts +++ b/vscode-extension/src/cacheManager.ts @@ -60,10 +60,10 @@ export class CacheManager { } this.sessionFileCache.set(filePath, data); - // Limit cache size to prevent memory issues (keep last 1000 files) + // Limit cache size to prevent memory issues (keep last 3000 files) // Only trigger cleanup when size exceeds limit by 100 to avoid frequent operations - if (this.sessionFileCache.size > 1100) { - // Remove 100 oldest entries to bring size back to 1000 + if (this.sessionFileCache.size > 3100) { + // Remove 100 oldest entries to bring size back to 3000 // Maps maintain insertion order, so the first entries are the oldest const keysToDelete: string[] = []; let count = 0; diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 1e92a8a5..39eb400a 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -168,7 +168,7 @@ type RepoPrStatsResult = { class CopilotTokenTracker implements vscode.Disposable { // Cache version - increment this when making changes that require cache invalidation - private static readonly CACHE_VERSION = 37; // Add thinking effort (reasoning effort) tracking + private static readonly CACHE_VERSION = 38; // Fix repo detection for CLI worktree sessions // Maximum length for displaying workspace IDs in diagnostics/customization matrix private static readonly WORKSPACE_ID_DISPLAY_LENGTH = 8; @@ -241,6 +241,8 @@ class CopilotTokenTracker implements vscode.Disposable { private lastDailyStats: DailyTokenStats[] | undefined; /** Full-year daily stats (up to 365 days) for the chart Week/Month period views. */ private lastFullDailyStats: DailyTokenStats[] | undefined; + /** Last period selected by the user in the chart view; restored on next open. */ + private lastChartPeriod: 'day' | 'week' | 'month' = 'day'; private lastUsageAnalysisStats: UsageAnalysisStats | undefined; private lastDashboardData: any | undefined; private tokenEstimators: { [key: string]: number } = tokenEstimatorsData.estimators; @@ -3473,6 +3475,16 @@ class CopilotTokenTracker implements vscode.Disposable { if (event.type === 'tool.execution_start' && event.data?.toolName === 'rename_session') { if (event.data?.arguments?.title) { details.title = event.data.arguments.title; } } + + // Collect file paths from tool arguments for repository detection + if (event.type === 'tool.execution_start' && event.data?.arguments) { + const args = event.data.arguments as Record; + for (const val of Object.values(args)) { + if (typeof val === 'string' && val.length > 3 && (val.includes('/') || val.includes('\\'))) { + allContentReferences.push({ kind: 'reference', reference: { fsPath: val } }); + } + } + } } catch { // Skip malformed lines } @@ -4768,6 +4780,12 @@ class CopilotTokenTracker implements vscode.Disposable { if (message.command === 'refresh') { await this.dispatch('refresh:chart', () => this.refreshChartPanel()); } + if (message.command === 'setPeriodPreference') { + const p = message.period; + if (p === 'day' || p === 'week' || p === 'month') { + this.lastChartPeriod = p; + } + } }); // Render immediately; Week/Month buttons are shown as loading if full-year data isn't ready @@ -4801,10 +4819,7 @@ class CopilotTokenTracker implements vscode.Disposable { this.analysisPanel = undefined; } - // Get usage analysis stats (use cached version for fast loading) - const analysisStats = await this.calculateUsageAnalysisStats(true); - - // Create webview panel + // Create webview panel immediately so the user sees something right away this.analysisPanel = vscode.window.createWebviewPanel( 'copilotUsageAnalysis', 'AI Usage Analysis', @@ -4859,8 +4874,31 @@ class CopilotTokenTracker implements vscode.Disposable { } }); - // Set the HTML content - this.analysisPanel.webview.html = this.getUsageAnalysisHtml(this.analysisPanel.webview, analysisStats); + // Set HTML immediately — use cached stats if available, else show loading spinner + this.analysisPanel.webview.html = this.getUsageAnalysisHtml(this.analysisPanel.webview, this.lastUsageAnalysisStats ?? null); + + // If no cached stats, compute in the background and push via updateStats + if (!this.lastUsageAnalysisStats) { + this.calculateUsageAnalysisStats(true).then(analysisStats => { + if (!this.analysisPanel) { return; } + void this.analysisPanel.webview.postMessage({ + command: 'updateStats', + data: { + today: analysisStats.today, + last30Days: analysisStats.last30Days, + month: analysisStats.month, + locale: analysisStats.locale, + customizationMatrix: analysisStats.customizationMatrix || null, + missedPotential: analysisStats.missedPotential || [], + lastUpdated: analysisStats.lastUpdated.toISOString(), + backendConfigured: this.isBackendConfigured(), + currentWorkspacePaths: vscode.workspace.workspaceFolders?.map(f => f.uri.fsPath) ?? [], + }, + }); + }).catch(err => { + this.error(`Failed to load usage analysis stats: ${err}`); + }); + } // Handle panel disposal this.analysisPanel.onDidDispose(() => { @@ -7997,7 +8035,21 @@ ${hashtag}`; const allModels = new Set(); entries.forEach(e => Object.keys(e.modelUsage).forEach(m => allModels.add(m))); - const modelDatasets = Array.from(allModels).map((model, idx) => { + + // Rank models by total tokens across the period; keep top 5, group the rest + const modelTotals = new Map(); + for (const model of allModels) { + const total = entries.reduce((sum, e) => { + const u = e.modelUsage[model]; + return sum + (u ? u.inputTokens + u.outputTokens : 0); + }, 0); + modelTotals.set(model, total); + } + const sortedModels = Array.from(allModels).sort((a, b) => (modelTotals.get(b) || 0) - (modelTotals.get(a) || 0)); + const topModels = sortedModels.slice(0, 5); + const otherModels = sortedModels.slice(5); + + const modelDatasets = topModels.map((model, idx) => { const color = modelColors[idx % modelColors.length]; return { label: getModelDisplayName(model), @@ -8005,6 +8057,18 @@ ${hashtag}`; backgroundColor: color.bg, borderColor: color.border, borderWidth: 1, }; }); + if (otherModels.length > 0) { + modelDatasets.push({ + label: 'Other models', + data: entries.map(e => otherModels.reduce((sum, m) => { + const u = e.modelUsage[m]; + return sum + (u ? u.inputTokens + u.outputTokens : 0); + }, 0)), + backgroundColor: 'rgba(150, 150, 150, 0.5)', + borderColor: 'rgba(150, 150, 150, 0.8)', + borderWidth: 1, + }); + } const allEditors = new Set(); entries.forEach(e => Object.keys(e.editorUsage).forEach(ed => allEditors.add(ed))); @@ -8018,7 +8082,9 @@ ${hashtag}`; }); const allRepos = new Set(); - entries.forEach(e => Object.keys(e.repositoryUsage).forEach(r => allRepos.add(r))); + entries.forEach(e => Object.keys(e.repositoryUsage) + .filter(r => r !== 'Unknown') + .forEach(r => allRepos.add(r))); const repositoryDatasets = Array.from(allRepos).map((repo, idx) => { const color = modelColors[idx % modelColors.length]; return { @@ -8111,7 +8177,9 @@ ${hashtag}`; }); const repositoryTotalsMap: Record = {}; dailyBuckets.forEach(b => { - Object.entries(b.stats.repositoryUsage).forEach(([repo, usage]) => { + Object.entries(b.stats.repositoryUsage) + .filter(([repo]) => repo !== 'Unknown') + .forEach(([repo, usage]) => { const displayName = this.getRepoDisplayName(repo); repositoryTotalsMap[displayName] = (repositoryTotalsMap[displayName] || 0) + usage.tokens; }); @@ -8160,7 +8228,7 @@ ${hashtag}`; `script-src 'nonce-${nonce}'`, ].join("; "); - const chartData = { ...this.buildChartData(dailyStats), periodsReady }; + const chartData = { ...this.buildChartData(dailyStats), periodsReady, initialPeriod: this.lastChartPeriod }; const initialData = JSON.stringify(chartData).replace(/('suppressedUnknownTools', []); - const initialData = JSON.stringify({ + const initialData = stats ? JSON.stringify({ today: stats.today, last30Days: stats.last30Days, month: stats.month, @@ -8231,7 +8299,7 @@ ${hashtag}`; backendConfigured: this.isBackendConfigured(), currentWorkspacePaths: vscode.workspace.workspaceFolders?.map(f => f.uri.fsPath) ?? [], suppressedUnknownTools, - }).replace(/ diff --git a/vscode-extension/src/webview/chart/main.ts b/vscode-extension/src/webview/chart/main.ts index 48d3a986..542c1045 100644 --- a/vscode-extension/src/webview/chart/main.ts +++ b/vscode-extension/src/webview/chart/main.ts @@ -47,6 +47,7 @@ type InitialChartData = { backendConfigured?: boolean; compactNumbers?: boolean; periodsReady?: boolean; + initialPeriod?: ChartPeriod; periods?: { day: ChartPeriodData; week: ChartPeriodData; @@ -168,12 +169,12 @@ function renderLayout(data: InitialChartData): void { } const chartSection = el('div', 'section'); - chartSection.append(el('h3', '', '📊 Charts')); + // Chart section header: title left, period toggles right + const chartSectionHeader = el('div', 'chart-section-header'); + chartSectionHeader.append(el('h3', '', '📊 Charts')); - const chartShell = el('div', 'chart-shell'); - - // Period toggle row - const periodToggles = el('div', 'chart-controls period-controls'); + // Period toggles (compact, inline with section heading) + const periodToggles = el('div', 'period-controls'); const periodsReady = data.periodsReady !== false; const dayBtn = el('button', `toggle${currentPeriod === 'day' ? ' active' : ''}`, '📅 Day'); dayBtn.id = 'period-day'; @@ -190,6 +191,10 @@ function renderLayout(data: InitialChartData): void { monthBtn.title = 'Loading historical data…'; } periodToggles.append(dayBtn, weekBtn, monthBtn); + chartSectionHeader.append(periodToggles); + chartSection.append(chartSectionHeader); + + const chartShell = el('div', 'chart-shell'); // Chart view toggle row const toggles = el('div', 'chart-controls'); @@ -208,7 +213,7 @@ function renderLayout(data: InitialChartData): void { canvas.id = 'token-chart'; canvasWrap.append(canvas); - chartShell.append(periodToggles, toggles, canvasWrap); + chartShell.append(toggles, canvasWrap); chartSection.append(chartShell); const footer = el('div', 'footer', @@ -346,6 +351,7 @@ async function switchPeriod(period: ChartPeriod, data: InitialChartData): Promis return; } currentPeriod = period; + vscode.postMessage({ command: 'setPeriodPreference', period }); setActivePeriod(period); updateSummaryCards(data); if (!chart) { @@ -551,6 +557,9 @@ async function bootstrap(): Promise { } return; } + if (initialData.initialPeriod) { + currentPeriod = initialData.initialPeriod; + } renderLayout(initialData); } diff --git a/vscode-extension/src/webview/chart/styles.css b/vscode-extension/src/webview/chart/styles.css index bf4ccd54..ad3d15a3 100644 --- a/vscode-extension/src/webview/chart/styles.css +++ b/vscode-extension/src/webview/chart/styles.css @@ -103,8 +103,18 @@ body { margin-top: 2px; } -.chart-shell { - background: var(--bg-tertiary); +.chart-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.chart-section-header h3 { + margin: 0; +} + +.chart-shell { background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: 10px; padding: 12px; @@ -121,9 +131,14 @@ body { } .period-controls { - margin-bottom: 4px; - padding-bottom: 8px; - border-bottom: 1px solid var(--border-subtle); + display: flex; + gap: 4px; + align-items: center; +} + +.period-controls .toggle { + font-size: 11px; + padding: 4px 9px; } .toggle { diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index 918ba093..9dae01f5 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -1997,8 +1997,9 @@ async function bootstrap(): Promise { if (!initialData) { const root = document.getElementById('root'); if (root) { - root.textContent = 'No data available.'; + root.innerHTML = '
⏳ Loading usage analysis…
'; } + // Stats will arrive via the updateStats message; the module-level listener will call renderLayout then. return; } console.log('[Usage Analysis] Browser default locale:', Intl.DateTimeFormat().resolvedOptions().locale); diff --git a/vscode-extension/src/workspaceHelpers.ts b/vscode-extension/src/workspaceHelpers.ts index c4f7fb9a..8341ae48 100644 --- a/vscode-extension/src/workspaceHelpers.ts +++ b/vscode-extension/src/workspaceHelpers.ts @@ -586,6 +586,31 @@ export async function extractRepositoryFromContentReferences(contentReferences: } catch { // No .git/config at this level, continue up the tree } + + // Also check if .git is a file (git worktree) — contains "gitdir: " + const gitFilePath = path.join(potentialRoot, '.git'); + try { + const gitFileContent = await fs.promises.readFile(gitFilePath, 'utf8'); + const match = gitFileContent.match(/^gitdir:\s*(.+)$/m); + if (match) { + const gitdirPath = match[1].trim(); + const basePath = potentialRoot.replace(/\//g, path.sep); + const resolvedGitdir = path.isAbsolute(gitdirPath) + ? gitdirPath + : path.resolve(basePath, gitdirPath); + // Standard worktree: gitdir =
/.git/worktrees/ + // Main .git dir is 2 levels up; its config holds the remote URL + const mainGitDir = path.resolve(resolvedGitdir, '..', '..'); + const mainConfigPath = path.join(mainGitDir, 'config'); + const gitConfig = await fs.promises.readFile(mainConfigPath, 'utf8'); + const remoteUrl = parseGitRemoteUrl(gitConfig); + if (remoteUrl) { + return remoteUrl; + } + } + } catch { + // Not a worktree or can't read gitdir, continue + } } }