From 8b0ad8cdec0aae27cfea10cbc795a2040ad35e18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:50:30 +0000 Subject: [PATCH 1/7] Initial plan From ea521c98be7bb8ec484caa0cd74d1adccbcc1364 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:00:29 +0000 Subject: [PATCH 2/7] Add Team Dashboard webview with personal and team usage comparison Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- esbuild.js | 1 + package.json | 5 + src/extension.ts | 256 ++++++++++++++++++++++++++++- src/webview/dashboard/main.ts | 253 ++++++++++++++++++++++++++++ src/webview/dashboard/styles.css | 168 +++++++++++++++++++ src/webview/shared/buttonConfig.ts | 6 +- 6 files changed, 687 insertions(+), 2 deletions(-) create mode 100644 src/webview/dashboard/main.ts create mode 100644 src/webview/dashboard/styles.css diff --git a/esbuild.js b/esbuild.js index 5e68e51f..ca31826f 100644 --- a/esbuild.js +++ b/esbuild.js @@ -48,6 +48,7 @@ async function main() { diagnostics: 'src/webview/diagnostics/main.ts', logviewer: 'src/webview/logviewer/main.ts', maturity: 'src/webview/maturity/main.ts', + dashboard: 'src/webview/dashboard/main.ts', }, bundle: true, format: 'iife', diff --git a/package.json b/package.json index f1c598c7..933e4e93 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,11 @@ "command": "copilot-token-tracker.clearCache", "title": "Clear Cache", "category": "Copilot Token Tracker" + }, + { + "command": "copilot-token-tracker.showDashboard", + "title": "Show Team Dashboard", + "category": "Copilot Token Tracker" } ], "configuration": { diff --git a/src/extension.ts b/src/extension.ts index aa8b01bc..8fb1697d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -585,6 +585,7 @@ class CopilotTokenTracker implements vscode.Disposable { private chartPanel: vscode.WebviewPanel | undefined; private analysisPanel: vscode.WebviewPanel | undefined; private maturityPanel: vscode.WebviewPanel | undefined; + private dashboardPanel: vscode.WebviewPanel | undefined; private outputChannel: vscode.OutputChannel; private sessionFileCache: Map = new Map(); private lastDetailedStats: DetailedStats | undefined; @@ -618,6 +619,9 @@ class CopilotTokenTracker implements vscode.Disposable { // Tool name mapping - loaded from toolNames.json for friendly display names private toolNameMap: { [key: string]: string } = toolNamesData as { [key: string]: string }; + // Backend facade instance for accessing table storage data + private backend: BackendFacade | undefined; + // Helper method to get repository URL from package.json private getRepositoryUrl(): string { const repoUrl = packageJson.repository?.url?.replace(/^git\+/, '').replace(/\.git$/, ''); @@ -5960,6 +5964,247 @@ private getMaturityHtml(webview: vscode.Webview, data: { `; } + /** + * Opens the Team Dashboard panel showing personal and team usage comparison. + */ + public async showDashboard(): Promise { + this.log('📊 Opening Team Dashboard'); + + // Check if backend is configured + if (!this.backend) { + vscode.window.showWarningMessage('Team Dashboard requires backend sync to be configured. Please configure backend settings first.'); + return; + } + + const settings = this.backend.getSettings(); + if (!this.backend.isConfigured(settings)) { + vscode.window.showWarningMessage('Team Dashboard requires backend sync to be configured. Please configure backend settings first.'); + return; + } + + // If panel already exists, dispose and recreate with fresh data + if (this.dashboardPanel) { + this.dashboardPanel.dispose(); + this.dashboardPanel = undefined; + } + + try { + const dashboardData = await this.getDashboardData(); + + this.dashboardPanel = vscode.window.createWebviewPanel( + 'copilotDashboard', + 'Team Dashboard', + { viewColumn: vscode.ViewColumn.One, preserveFocus: true }, + { + enableScripts: true, + retainContextWhenHidden: false, + localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview')] + } + ); + + this.dashboardPanel.webview.html = this.getDashboardHtml(this.dashboardPanel.webview, dashboardData); + + this.dashboardPanel.webview.onDidReceiveMessage(async (message) => { + switch (message.command) { + case 'refresh': + await this.refreshDashboardPanel(); + break; + case 'showDetails': + await this.showDetails(); + break; + case 'showChart': + await this.showChart(); + break; + case 'showUsageAnalysis': + await this.showUsageAnalysis(); + break; + case 'showDiagnostics': + await this.showDiagnosticReport(); + break; + case 'showMaturity': + await this.showMaturity(); + break; + } + }); + + this.dashboardPanel.onDidDispose(() => { + this.log('📊 Team Dashboard closed'); + this.dashboardPanel = undefined; + }); + } catch (error) { + this.error('Failed to load dashboard data:', error); + vscode.window.showErrorMessage('Failed to load Team Dashboard. Please check backend configuration and try again.'); + } + } + + private async refreshDashboardPanel(): Promise { + if (!this.dashboardPanel) { + return; + } + + this.log('🔄 Refreshing Team Dashboard'); + try { + const dashboardData = await this.getDashboardData(); + this.dashboardPanel.webview.html = this.getDashboardHtml(this.dashboardPanel.webview, dashboardData); + this.log('✅ Team Dashboard refreshed'); + } catch (error) { + this.error('Failed to refresh dashboard:', error); + vscode.window.showErrorMessage('Failed to refresh Team Dashboard.'); + } + } + + /** + * Fetches and aggregates data for the Team Dashboard. + */ + private async getDashboardData(): Promise { + if (!this.backend) { + throw new Error('Backend not configured'); + } + + const { BackendUtility } = await import('./backend/services/utilityService.js'); + const settings = this.backend.getSettings(); + const currentMachineId = vscode.env.machineId; + const currentUserId = settings.userId; // Use the configured userId from settings + + // Query backend for last 30 days + const now = new Date(); + const todayKey = BackendUtility.toUtcDayKey(now); + const startKey = BackendUtility.addDaysUtc(todayKey, -29); + + // Fetch all entities for the dataset + const dataPlane = (this.backend as any).dataPlane; + const credentialService = (this.backend as any).credentialService; + const creds = await credentialService.getBackendDataPlaneCredentialsOrThrow(settings); + const tableClient = dataPlane.createTableClient(settings, creds.tableCredential); + + const allEntities = await dataPlane.listEntitiesForRange({ + tableClient: tableClient as any, + datasetId: settings.datasetId, + startDayKey: startKey, + endDayKey: todayKey + }); + + // Aggregate personal data (all machines and workspaces for current user) + const personalDevices = new Set(); + const personalWorkspaces = new Set(); + const personalModelUsage: { [model: string]: { inputTokens: number; outputTokens: number } } = {}; + let personalTotalTokens = 0; + let personalTotalInteractions = 0; + + // Aggregate team data (all users) + const userMap = new Map(); + + for (const entity of allEntities) { + const userId = (entity.userId ?? '').toString(); + const machineId = (entity.machineId ?? '').toString(); + const workspaceId = (entity.workspaceId ?? '').toString(); + const model = (entity.model ?? '').toString(); + const inputTokens = Number.isFinite(Number(entity.inputTokens)) ? Number(entity.inputTokens) : 0; + const outputTokens = Number.isFinite(Number(entity.outputTokens)) ? Number(entity.outputTokens) : 0; + const interactions = Number.isFinite(Number(entity.interactions)) ? Number(entity.interactions) : 0; + const tokens = inputTokens + outputTokens; + + // Personal data aggregation + if (userId === currentUserId) { + personalTotalTokens += tokens; + personalTotalInteractions += interactions; + personalDevices.add(machineId); + personalWorkspaces.add(workspaceId); + + if (!personalModelUsage[model]) { + personalModelUsage[model] = { inputTokens: 0, outputTokens: 0 }; + } + personalModelUsage[model].inputTokens += inputTokens; + personalModelUsage[model].outputTokens += outputTokens; + } + + // Team data aggregation + if (userId && userId.trim()) { + if (!userMap.has(userId)) { + userMap.set(userId, { tokens: 0, interactions: 0, cost: 0 }); + } + const userData = userMap.get(userId)!; + userData.tokens += tokens; + userData.interactions += interactions; + } + } + + // Calculate costs + const personalCost = this.calculateEstimatedCost(personalModelUsage); + for (const [userId, userData] of userMap.entries()) { + // Simple cost estimation based on total tokens + userData.cost = (userData.tokens / 1000000) * 0.05; // Rough estimate + } + + // Build team leaderboard + const teamMembers = Array.from(userMap.entries()) + .map(([userId, data]) => ({ + userId, + totalTokens: data.tokens, + totalInteractions: data.interactions, + totalCost: data.cost, + rank: 0 + })) + .sort((a, b) => b.totalTokens - a.totalTokens) + .map((member, index) => ({ + ...member, + rank: index + 1 + })); + + const teamTotalTokens = Array.from(userMap.values()).reduce((sum, u) => sum + u.tokens, 0); + const teamTotalInteractions = Array.from(userMap.values()).reduce((sum, u) => sum + u.interactions, 0); + const averageTokensPerUser = userMap.size > 0 ? teamTotalTokens / userMap.size : 0; + + return { + personal: { + userId: currentUserId, + totalTokens: personalTotalTokens, + totalInteractions: personalTotalInteractions, + totalCost: personalCost, + devices: Array.from(personalDevices), + workspaces: Array.from(personalWorkspaces), + modelUsage: personalModelUsage + }, + team: { + members: teamMembers, + totalTokens: teamTotalTokens, + totalInteractions: teamTotalInteractions, + averageTokensPerUser + }, + lastUpdated: new Date().toISOString() + }; + } + + private getDashboardHtml(webview: vscode.Webview, data: any): string { + const nonce = this.getNonce(); + const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview', 'dashboard.js')); + + const csp = [ + `default-src 'none'`, + `img-src ${webview.cspSource} https: data:`, + `style-src 'unsafe-inline' ${webview.cspSource}`, + `font-src ${webview.cspSource} https: data:`, + `script-src 'nonce-${nonce}'` + ].join('; '); + + const initialData = JSON.stringify(data).replace(/ + + + + + + Team Dashboard + + +
+ + + + `; + } + private getNonce(): string { const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let text = ''; @@ -6968,6 +7213,9 @@ export function activate(context: vscode.ExtensionContext) { log: (m: string) => (tokenTracker as any).log(m) }); + // Store backend facade in the tracker instance for dashboard access + (tokenTracker as any).backend = backendFacade; + const configureBackendCommand = vscode.commands.registerCommand('copilot-token-tracker.configureBackend', async () => { await backendHandler.handleConfigureBackend(); }); @@ -7009,6 +7257,12 @@ export function activate(context: vscode.ExtensionContext) { await tokenTracker.showMaturity(); }); + // Register the show dashboard command + const showDashboardCommand = vscode.commands.registerCommand('copilot-token-tracker.showDashboard', async () => { + tokenTracker.log('Show dashboard command called'); + await tokenTracker.showDashboard(); + }); + // Register the generate diagnostic report command const generateDiagnosticReportCommand = vscode.commands.registerCommand('copilot-token-tracker.generateDiagnosticReport', async () => { tokenTracker.log('Generate diagnostic report command called'); @@ -7022,7 +7276,7 @@ export function activate(context: vscode.ExtensionContext) { }); // Add to subscriptions for proper cleanup - context.subscriptions.push(refreshCommand, showDetailsCommand, showChartCommand, showUsageAnalysisCommand, showMaturityCommand, generateDiagnosticReportCommand, clearCacheCommand, tokenTracker); + context.subscriptions.push(refreshCommand, showDetailsCommand, showChartCommand, showUsageAnalysisCommand, showMaturityCommand, showDashboardCommand, generateDiagnosticReportCommand, clearCacheCommand, tokenTracker); tokenTracker.log('Extension activation complete'); } diff --git a/src/webview/dashboard/main.ts b/src/webview/dashboard/main.ts new file mode 100644 index 00000000..15079bff --- /dev/null +++ b/src/webview/dashboard/main.ts @@ -0,0 +1,253 @@ +// Import shared utilities +import { getModelDisplayName } from '../shared/modelUtils'; +import { formatNumber, formatCost, formatPercent } from '../shared/formatUtils'; +import { el, createButton } from '../shared/domUtils'; +import { BUTTONS } from '../shared/buttonConfig'; +import themeStyles from '../shared/theme.css'; +import styles from './styles.css'; + +type ModelUsage = Record; + +interface UserSummary { + userId: string; + totalTokens: number; + totalInteractions: number; + totalCost: number; + devices: string[]; + workspaces: string[]; + modelUsage: ModelUsage; +} + +interface TeamMemberStats { + userId: string; + totalTokens: number; + totalInteractions: number; + totalCost: number; + rank: number; +} + +interface DashboardStats { + // Personal data across all devices/workspaces + personal: UserSummary; + // Team data for comparison + team: { + members: TeamMemberStats[]; + totalTokens: number; + totalInteractions: number; + averageTokensPerUser: number; + }; + lastUpdated: string | Date; +} + +declare function acquireVsCodeApi(): { + postMessage: (message: any) => void; + setState: (newState: TState) => void; + getState: () => TState | undefined; +}; + +type VSCodeApi = ReturnType; + +declare global { + interface Window { + __INITIAL_DASHBOARD__?: DashboardStats; + } +} + +const vscode: VSCodeApi = acquireVsCodeApi(); +const initialData = window.__INITIAL_DASHBOARD__; +console.log('[CopilotTokenTracker] dashboard webview loaded'); +console.log('[CopilotTokenTracker] initialData:', initialData); + +function render(stats: DashboardStats): void { + const root = document.getElementById('root'); + if (!root) { return; } + + renderShell(root, stats); + wireButtons(); +} + +function renderShell(root: HTMLElement, stats: DashboardStats): void { + const lastUpdated = new Date(stats.lastUpdated); + + root.replaceChildren(); + + const themeStyle = document.createElement('style'); + themeStyle.textContent = themeStyles; + + const style = document.createElement('style'); + style.textContent = styles; + + const container = el('div', 'container'); + const header = el('div', 'header'); + const title = el('div', 'title', '📊 Team Dashboard'); + const buttonRow = el('div', 'button-row'); + + buttonRow.append( + createButton(BUTTONS['btn-refresh']), + createButton(BUTTONS['btn-details']), + createButton(BUTTONS['btn-chart']), + createButton(BUTTONS['btn-usage']), + createButton(BUTTONS['btn-diagnostics']), + createButton(BUTTONS['btn-maturity']) + ); + + header.append(title, buttonRow); + + const footer = el('div', 'footer', `Last updated: ${lastUpdated.toLocaleString()}`); + + const sections = el('div', 'sections'); + sections.append(buildPersonalSection(stats.personal)); + sections.append(buildTeamSection(stats)); + + container.append(header, sections, footer); + root.append(themeStyle, style, container); +} + +function buildPersonalSection(personal: UserSummary): HTMLElement { + const section = el('div', 'section'); + const sectionTitle = el('h2', '', '👤 Your Summary (All Devices & Workspaces)'); + + const grid = el('div', 'stats-grid'); + + grid.append( + buildStatCard('Total Tokens', formatNumber(personal.totalTokens)), + buildStatCard('Interactions', formatNumber(personal.totalInteractions)), + buildStatCard('Estimated Cost', formatCost(personal.totalCost)), + buildStatCard('Devices', personal.devices.length.toString()), + buildStatCard('Workspaces', personal.workspaces.length.toString()) + ); + + const modelSection = buildModelBreakdown(personal.modelUsage); + + section.append(sectionTitle, grid, modelSection); + return section; +} + +function buildTeamSection(stats: DashboardStats): HTMLElement { + const section = el('div', 'section'); + const sectionTitle = el('h2', '', '👥 Team Comparison'); + + const teamGrid = el('div', 'stats-grid'); + teamGrid.append( + buildStatCard('Team Total', formatNumber(stats.team.totalTokens) + ' tokens'), + buildStatCard('Team Members', stats.team.members.length.toString()), + buildStatCard('Avg per User', formatNumber(Math.round(stats.team.averageTokensPerUser)) + ' tokens') + ); + + const leaderboard = buildLeaderboard(stats); + + section.append(sectionTitle, teamGrid, leaderboard); + return section; +} + +function buildStatCard(label: string, value: string): HTMLElement { + const card = el('div', 'stat-card'); + const labelEl = el('div', 'stat-label', label); + const valueEl = el('div', 'stat-value', value); + card.append(labelEl, valueEl); + return card; +} + +function buildModelBreakdown(modelUsage: ModelUsage): HTMLElement { + const container = el('div', 'model-breakdown'); + const title = el('h3', '', 'Model Usage'); + + const modelList = el('div', 'model-list'); + + const models = Object.entries(modelUsage) + .map(([model, usage]) => ({ + model, + tokens: usage.inputTokens + usage.outputTokens + })) + .sort((a, b) => b.tokens - a.tokens); + + for (const { model, tokens } of models) { + const item = el('div', 'model-item'); + const modelName = el('span', 'model-name', getModelDisplayName(model)); + const tokenCount = el('span', 'token-count', formatNumber(tokens)); + item.append(modelName, tokenCount); + modelList.append(item); + } + + container.append(title, modelList); + return container; +} + +function buildLeaderboard(stats: DashboardStats): HTMLElement { + const container = el('div', 'leaderboard'); + const title = el('h3', '', 'Leaderboard'); + + const table = el('table', 'leaderboard-table'); + const thead = el('thead', ''); + const headerRow = el('tr', ''); + + ['Rank', 'User', 'Tokens', 'Interactions', 'Est. Cost'].forEach(text => { + const th = el('th', '', text); + headerRow.append(th); + }); + thead.append(headerRow); + + const tbody = el('tbody', ''); + + for (const member of stats.team.members) { + const row = el('tr', ''); + + // Highlight current user + const isCurrentUser = member.userId === stats.personal.userId; + if (isCurrentUser) { + row.classList.add('current-user'); + } + + const rankCell = el('td', 'rank-cell', `#${member.rank}`); + const userCell = el('td', '', isCurrentUser ? `${member.userId} (You)` : member.userId); + const tokensCell = el('td', 'number-cell', formatNumber(member.totalTokens)); + const interactionsCell = el('td', 'number-cell', formatNumber(member.totalInteractions)); + const costCell = el('td', 'number-cell', formatCost(member.totalCost)); + + row.append(rankCell, userCell, tokensCell, interactionsCell, costCell); + tbody.append(row); + } + + table.append(thead, tbody); + container.append(title, table); + return container; +} + +function wireButtons(): void { + document.getElementById('btn-refresh')?.addEventListener('click', () => { + vscode.postMessage({ command: 'refresh' }); + }); + + document.getElementById('btn-details')?.addEventListener('click', () => { + vscode.postMessage({ command: 'showDetails' }); + }); + + document.getElementById('btn-chart')?.addEventListener('click', () => { + vscode.postMessage({ command: 'showChart' }); + }); + + document.getElementById('btn-usage')?.addEventListener('click', () => { + vscode.postMessage({ command: 'showUsageAnalysis' }); + }); + + document.getElementById('btn-diagnostics')?.addEventListener('click', () => { + vscode.postMessage({ command: 'showDiagnostics' }); + }); + + document.getElementById('btn-maturity')?.addEventListener('click', () => { + vscode.postMessage({ command: 'showMaturity' }); + }); + + document.getElementById('btn-dashboard')?.addEventListener('click', () => { + vscode.postMessage({ command: 'showDashboard' }); + }); +} + +if (initialData) { + render(initialData); +} else { + const root = document.getElementById('root'); + if (root) { + root.textContent = 'No dashboard data available. Please configure backend sync.'; + } +} diff --git a/src/webview/dashboard/styles.css b/src/webview/dashboard/styles.css new file mode 100644 index 00000000..a6a3ac8f --- /dev/null +++ b/src/webview/dashboard/styles.css @@ -0,0 +1,168 @@ +.container { + padding: 20px; + max-width: 1200px; + margin: 0 auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + flex-wrap: wrap; + gap: 15px; +} + +.title { + font-size: 24px; + font-weight: bold; + color: var(--vscode-foreground); +} + +.button-row { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.sections { + display: flex; + flex-direction: column; + gap: 30px; +} + +.section { + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + padding: 20px; +} + +.section h2 { + margin: 0 0 20px 0; + font-size: 20px; + color: var(--vscode-foreground); + border-bottom: 1px solid var(--vscode-panel-border); + padding-bottom: 10px; +} + +.section h3 { + margin: 20px 0 15px 0; + font-size: 16px; + color: var(--vscode-foreground); +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + margin-bottom: 20px; +} + +.stat-card { + background-color: var(--vscode-button-secondaryBackground); + border: 1px solid var(--vscode-button-border); + border-radius: 4px; + padding: 15px; + display: flex; + flex-direction: column; + gap: 5px; +} + +.stat-label { + font-size: 12px; + color: var(--vscode-descriptionForeground); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.stat-value { + font-size: 24px; + font-weight: bold; + color: var(--vscode-foreground); +} + +.model-breakdown { + margin-top: 20px; +} + +.model-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.model-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + background-color: var(--vscode-list-hoverBackground); + border-radius: 4px; +} + +.model-name { + color: var(--vscode-foreground); +} + +.token-count { + color: var(--vscode-descriptionForeground); + font-family: var(--vscode-editor-font-family); +} + +.leaderboard { + margin-top: 20px; +} + +.leaderboard-table { + width: 100%; + border-collapse: collapse; + margin-top: 15px; +} + +.leaderboard-table th { + text-align: left; + padding: 12px; + background-color: var(--vscode-list-hoverBackground); + border-bottom: 2px solid var(--vscode-panel-border); + font-weight: 600; + color: var(--vscode-foreground); +} + +.leaderboard-table td { + padding: 12px; + border-bottom: 1px solid var(--vscode-panel-border); + color: var(--vscode-foreground); +} + +.leaderboard-table tr:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.leaderboard-table tr.current-user { + background-color: var(--vscode-list-activeSelectionBackground); + font-weight: bold; +} + +.leaderboard-table tr.current-user:hover { + background-color: var(--vscode-list-focusBackground); +} + +.rank-cell { + font-weight: bold; + color: var(--vscode-textLink-foreground); +} + +.number-cell { + text-align: right; + font-family: var(--vscode-editor-font-family); +} + +.footer { + margin-top: 30px; + text-align: center; + color: var(--vscode-descriptionForeground); + font-size: 12px; + padding: 20px 0; + border-top: 1px solid var(--vscode-panel-border); +} diff --git a/src/webview/shared/buttonConfig.ts b/src/webview/shared/buttonConfig.ts index 861bd6e4..cad8239b 100644 --- a/src/webview/shared/buttonConfig.ts +++ b/src/webview/shared/buttonConfig.ts @@ -3,7 +3,7 @@ * This ensures consistent button IDs, labels, and icons across all webviews. */ -export type ButtonId = 'btn-refresh' | 'btn-details' | 'btn-chart' | 'btn-usage' | 'btn-diagnostics' | 'btn-maturity'; +export type ButtonId = 'btn-refresh' | 'btn-details' | 'btn-chart' | 'btn-usage' | 'btn-diagnostics' | 'btn-maturity' | 'btn-dashboard'; export interface ButtonConfig { id: ButtonId; @@ -39,6 +39,10 @@ export const BUTTONS: Record = { 'btn-maturity': { id: 'btn-maturity', label: '🎯 Fluency Score' + }, + 'btn-dashboard': { + id: 'btn-dashboard', + label: '📊 Team Dashboard' } }; From 7380cd276a6eca6beaefa97eff7e78c7a00c867b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:03:18 +0000 Subject: [PATCH 3/7] Add dashboard navigation to all webviews Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- src/extension.ts | 15 +++++++++++++++ src/webview/details/main.ts | 6 +++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 8fb1697d..ee67dd78 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4974,6 +4974,9 @@ class CopilotTokenTracker implements vscode.Disposable { case 'showMaturity': await this.showMaturity(); break; + case 'showDashboard': + await this.showDashboard(); + break; } }); @@ -5035,6 +5038,9 @@ class CopilotTokenTracker implements vscode.Disposable { case 'showMaturity': await this.showMaturity(); break; + case 'showDashboard': + await this.showDashboard(); + break; } }); @@ -5096,6 +5102,9 @@ class CopilotTokenTracker implements vscode.Disposable { case 'showMaturity': await this.showMaturity(); break; + case 'showDashboard': + await this.showDashboard(); + break; } }); @@ -5893,6 +5902,9 @@ class CopilotTokenTracker implements vscode.Disposable { await this.refreshMaturityPanel(); } break; + case 'showDashboard': + await this.showDashboard(); + break; } }); @@ -6612,6 +6624,9 @@ private getMaturityHtml(webview: vscode.Webview, data: { case 'openSettings': await vscode.commands.executeCommand('workbench.action.openSettings', 'copilotTokenTracker.backend'); break; + case 'showDashboard': + await this.showDashboard(); + break; } }); diff --git a/src/webview/details/main.ts b/src/webview/details/main.ts index afec4130..a274ff6b 100644 --- a/src/webview/details/main.ts +++ b/src/webview/details/main.ts @@ -119,7 +119,8 @@ function renderShell( createButton(BUTTONS['btn-chart']), createButton(BUTTONS['btn-usage']), createButton(BUTTONS['btn-diagnostics']), - createButton(BUTTONS['btn-maturity']) + createButton(BUTTONS['btn-maturity']), + createButton(BUTTONS['btn-dashboard']) ); header.append(title, buttonRow); @@ -469,6 +470,9 @@ function wireButtons(): void { const maturity = document.getElementById('btn-maturity'); maturity?.addEventListener('click', () => vscode.postMessage({ command: 'showMaturity' })); + + const dashboard = document.getElementById('btn-dashboard'); + dashboard?.addEventListener('click', () => vscode.postMessage({ command: 'showDashboard' })); } async function bootstrap(): Promise { From 0054afd25c423538335f9c8b53b2e40aabe8dd84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:04:48 +0000 Subject: [PATCH 4/7] Address code review feedback: remove unused variable, improve cost calculation docs, fix dashboard button Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- src/extension.ts | 9 ++++++--- src/webview/dashboard/main.ts | 6 ++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index ee67dd78..69d14e35 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6075,7 +6075,6 @@ private getMaturityHtml(webview: vscode.Webview, data: { const { BackendUtility } = await import('./backend/services/utilityService.js'); const settings = this.backend.getSettings(); - const currentMachineId = vscode.env.machineId; const currentUserId = settings.userId; // Use the configured userId from settings // Query backend for last 30 days @@ -6143,9 +6142,13 @@ private getMaturityHtml(webview: vscode.Webview, data: { // Calculate costs const personalCost = this.calculateEstimatedCost(personalModelUsage); + + // For team members, use a simplified cost estimate since we don't track + // per-user model breakdown in the current aggregation. This provides a + // rough approximation assuming average model pricing (~$0.05 per 1M tokens). + // The personal cost uses the accurate model-aware calculation. for (const [userId, userData] of userMap.entries()) { - // Simple cost estimation based on total tokens - userData.cost = (userData.tokens / 1000000) * 0.05; // Rough estimate + userData.cost = (userData.tokens / 1000000) * 0.05; } // Build team leaderboard diff --git a/src/webview/dashboard/main.ts b/src/webview/dashboard/main.ts index 15079bff..ac2a7855 100644 --- a/src/webview/dashboard/main.ts +++ b/src/webview/dashboard/main.ts @@ -237,10 +237,8 @@ function wireButtons(): void { document.getElementById('btn-maturity')?.addEventListener('click', () => { vscode.postMessage({ command: 'showMaturity' }); }); - - document.getElementById('btn-dashboard')?.addEventListener('click', () => { - vscode.postMessage({ command: 'showDashboard' }); - }); + + // Note: No dashboard button handler - users are already on the dashboard } if (initialData) { From 301d9146aecd784062825943ac3ec7c49c1454de Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Tue, 17 Feb 2026 17:44:22 +0100 Subject: [PATCH 5/7] Enhance Team Dashboard: improve loading and error handling UI, update styles, and refactor data rendering logic --- src/extension.ts | 119 +++++++++++++++---------------- src/webview/dashboard/main.ts | 94 ++++++++++++++++++++++-- src/webview/dashboard/styles.css | 45 ++++++++++++ 3 files changed, 191 insertions(+), 67 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 69d14e35..2e04cac7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5994,58 +5994,62 @@ private getMaturityHtml(webview: vscode.Webview, data: { return; } - // If panel already exists, dispose and recreate with fresh data + // If panel already exists, just reveal it if (this.dashboardPanel) { - this.dashboardPanel.dispose(); - this.dashboardPanel = undefined; + this.dashboardPanel.reveal(); + this.log('📊 Team Dashboard revealed (already exists)'); + return; } - try { - const dashboardData = await this.getDashboardData(); + // Show panel immediately with loading state + this.dashboardPanel = vscode.window.createWebviewPanel( + 'copilotDashboard', + 'Team Dashboard', + { viewColumn: vscode.ViewColumn.One, preserveFocus: true }, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview')] + } + ); - this.dashboardPanel = vscode.window.createWebviewPanel( - 'copilotDashboard', - 'Team Dashboard', - { viewColumn: vscode.ViewColumn.One, preserveFocus: true }, - { - enableScripts: true, - retainContextWhenHidden: false, - localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview')] - } - ); + this.dashboardPanel.webview.html = this.getDashboardHtml(this.dashboardPanel.webview, undefined); - this.dashboardPanel.webview.html = this.getDashboardHtml(this.dashboardPanel.webview, dashboardData); - - this.dashboardPanel.webview.onDidReceiveMessage(async (message) => { - switch (message.command) { - case 'refresh': - await this.refreshDashboardPanel(); - break; - case 'showDetails': - await this.showDetails(); - break; - case 'showChart': - await this.showChart(); - break; - case 'showUsageAnalysis': - await this.showUsageAnalysis(); - break; - case 'showDiagnostics': - await this.showDiagnosticReport(); - break; - case 'showMaturity': - await this.showMaturity(); - break; - } - }); + this.dashboardPanel.webview.onDidReceiveMessage(async (message) => { + switch (message.command) { + case 'refresh': + await this.refreshDashboardPanel(); + break; + case 'showDetails': + await this.showDetails(); + break; + case 'showChart': + await this.showChart(); + break; + case 'showUsageAnalysis': + await this.showUsageAnalysis(); + break; + case 'showDiagnostics': + await this.showDiagnosticReport(); + break; + case 'showMaturity': + await this.showMaturity(); + break; + } + }); - this.dashboardPanel.onDidDispose(() => { - this.log('📊 Team Dashboard closed'); - this.dashboardPanel = undefined; - }); + this.dashboardPanel.onDidDispose(() => { + this.log('📊 Team Dashboard closed'); + this.dashboardPanel = undefined; + }); + + // Load data asynchronously and send to webview + try { + const dashboardData = await this.getDashboardData(); + this.dashboardPanel?.webview.postMessage({ command: 'dashboardData', data: dashboardData }); } catch (error) { this.error('Failed to load dashboard data:', error); - vscode.window.showErrorMessage('Failed to load Team Dashboard. Please check backend configuration and try again.'); + this.dashboardPanel?.webview.postMessage({ command: 'dashboardError', message: 'Failed to load dashboard data. Please check backend configuration and try again.' }); } } @@ -6055,13 +6059,14 @@ private getMaturityHtml(webview: vscode.Webview, data: { } this.log('🔄 Refreshing Team Dashboard'); + this.dashboardPanel.webview.postMessage({ command: 'dashboardLoading' }); try { const dashboardData = await this.getDashboardData(); - this.dashboardPanel.webview.html = this.getDashboardHtml(this.dashboardPanel.webview, dashboardData); + this.dashboardPanel?.webview.postMessage({ command: 'dashboardData', data: dashboardData }); this.log('✅ Team Dashboard refreshed'); } catch (error) { this.error('Failed to refresh dashboard:', error); - vscode.window.showErrorMessage('Failed to refresh Team Dashboard.'); + this.dashboardPanel?.webview.postMessage({ command: 'dashboardError', message: 'Failed to refresh dashboard data.' }); } } @@ -6082,18 +6087,8 @@ private getMaturityHtml(webview: vscode.Webview, data: { const todayKey = BackendUtility.toUtcDayKey(now); const startKey = BackendUtility.addDaysUtc(todayKey, -29); - // Fetch all entities for the dataset - const dataPlane = (this.backend as any).dataPlane; - const credentialService = (this.backend as any).credentialService; - const creds = await credentialService.getBackendDataPlaneCredentialsOrThrow(settings); - const tableClient = dataPlane.createTableClient(settings, creds.tableCredential); - - const allEntities = await dataPlane.listEntitiesForRange({ - tableClient: tableClient as any, - datasetId: settings.datasetId, - startDayKey: startKey, - endDayKey: todayKey - }); + // Fetch all entities for the dataset using the facade's public API + const allEntities = await this.backend.getAggEntitiesForRange(settings, startKey, todayKey); // Aggregate personal data (all machines and workspaces for current user) const personalDevices = new Set(); @@ -6190,7 +6185,7 @@ private getMaturityHtml(webview: vscode.Webview, data: { }; } - private getDashboardHtml(webview: vscode.Webview, data: any): string { + private getDashboardHtml(webview: vscode.Webview, data: any | undefined): string { const nonce = this.getNonce(); const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview', 'dashboard.js')); @@ -6202,7 +6197,9 @@ private getMaturityHtml(webview: vscode.Webview, data: { `script-src 'nonce-${nonce}'` ].join('; '); - const initialData = JSON.stringify(data).replace(/window.__INITIAL_DASHBOARD__ = ${JSON.stringify(data).replace(/` + : ''; return ` @@ -6214,7 +6211,7 @@ private getMaturityHtml(webview: vscode.Webview, data: {
- + ${initialDataScript} `; diff --git a/src/webview/dashboard/main.ts b/src/webview/dashboard/main.ts index ac2a7855..efa96a2e 100644 --- a/src/webview/dashboard/main.ts +++ b/src/webview/dashboard/main.ts @@ -58,6 +58,58 @@ const initialData = window.__INITIAL_DASHBOARD__; console.log('[CopilotTokenTracker] dashboard webview loaded'); console.log('[CopilotTokenTracker] initialData:', initialData); +function showLoading(): void { + const root = document.getElementById('root'); + if (!root) { return; } + + root.replaceChildren(); + + const themeStyle = document.createElement('style'); + themeStyle.textContent = themeStyles; + + const style = document.createElement('style'); + style.textContent = styles; + + const container = el('div', 'container'); + const header = el('div', 'header'); + const title = el('div', 'title', '📊 Team Dashboard'); + header.append(title); + + const loading = el('div', 'loading-indicator'); + const spinner = el('div', 'spinner'); + const loadingText = el('div', 'loading-text', 'Loading dashboard data...'); + loading.append(spinner, loadingText); + + container.append(header, loading); + root.append(themeStyle, style, container); +} + +function showError(message: string): void { + const root = document.getElementById('root'); + if (!root) { return; } + + root.replaceChildren(); + + const themeStyle = document.createElement('style'); + themeStyle.textContent = themeStyles; + + const style = document.createElement('style'); + style.textContent = styles; + + const container = el('div', 'container'); + const header = el('div', 'header'); + const title = el('div', 'title', '📊 Team Dashboard'); + const buttonRow = el('div', 'button-row'); + buttonRow.append(createButton(BUTTONS['btn-refresh'])); + header.append(title, buttonRow); + + const errorEl = el('div', 'error-message', message); + + container.append(header, errorEl); + root.append(themeStyle, style, container); + wireButtons(); +} + function render(stats: DashboardStats): void { const root = document.getElementById('root'); if (!root) { return; } @@ -79,7 +131,10 @@ function renderShell(root: HTMLElement, stats: DashboardStats): void { const container = el('div', 'container'); const header = el('div', 'header'); + const titleGroup = el('div', 'title-group'); const title = el('div', 'title', '📊 Team Dashboard'); + const period = el('div', 'period', 'Last 30 days'); + titleGroup.append(title, period); const buttonRow = el('div', 'button-row'); buttonRow.append( @@ -91,7 +146,7 @@ function renderShell(root: HTMLElement, stats: DashboardStats): void { createButton(BUTTONS['btn-maturity']) ); - header.append(title, buttonRow); + header.append(titleGroup, buttonRow); const footer = el('div', 'footer', `Last updated: ${lastUpdated.toLocaleString()}`); @@ -241,11 +296,38 @@ function wireButtons(): void { // Note: No dashboard button handler - users are already on the dashboard } -if (initialData) { - render(initialData); -} else { +// Listen for messages from the extension +window.addEventListener('message', (event) => { + const message = event.data; + switch (message.command) { + case 'dashboardData': + render(message.data); + break; + case 'dashboardLoading': + showLoading(); + break; + case 'dashboardError': + showError(message.message); + break; + } +}); + +async function bootstrap(): Promise { + console.log('[CopilotTokenTracker] dashboard bootstrap called'); + const { provideVSCodeDesignSystem, vsCodeButton } = await import('@vscode/webview-ui-toolkit'); + provideVSCodeDesignSystem().register(vsCodeButton()); + + if (initialData) { + render(initialData); + } else { + showLoading(); + } +} + +bootstrap().catch(err => { + console.error('[CopilotTokenTracker] Failed to bootstrap dashboard:', err); const root = document.getElementById('root'); if (root) { - root.textContent = 'No dashboard data available. Please configure backend sync.'; + root.textContent = 'Failed to initialize dashboard.'; } -} +}); diff --git a/src/webview/dashboard/styles.css b/src/webview/dashboard/styles.css index a6a3ac8f..fa44de9a 100644 --- a/src/webview/dashboard/styles.css +++ b/src/webview/dashboard/styles.css @@ -19,6 +19,17 @@ color: var(--vscode-foreground); } +.title-group { + display: flex; + flex-direction: column; + gap: 4px; +} + +.period { + font-size: 13px; + color: var(--vscode-descriptionForeground); +} + .button-row { display: flex; gap: 10px; @@ -166,3 +177,37 @@ padding: 20px 0; border-top: 1px solid var(--vscode-panel-border); } + +.loading-indicator { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + gap: 20px; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid var(--vscode-panel-border); + border-top-color: var(--vscode-textLink-foreground); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading-text { + color: var(--vscode-descriptionForeground); + font-size: 14px; +} + +.error-message { + text-align: center; + padding: 40px 20px; + color: var(--vscode-errorForeground); + font-size: 14px; +} From 288484481fcd8314c1bada62523749b799f02804 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Tue, 17 Feb 2026 18:13:56 +0100 Subject: [PATCH 6/7] Fix code issuess --- src/extension.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extension.ts b/src/extension.ts index e1d80d86..dd7f82e0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7934,6 +7934,7 @@ export function activate(context: vscode.ExtensionContext) { const showDashboardCommand = vscode.commands.registerCommand('copilot-token-tracker.showDashboard', async () => { tokenTracker.log('Show dashboard command called'); await tokenTracker.showDashboard(); + }); // Register the show fluency level viewer command (debug-only) const showFluencyLevelViewerCommand = vscode.commands.registerCommand('copilot-token-tracker.showFluencyLevelViewer', async () => { From 40a679c6136cea8ba5e16b9842f4e19586645687 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Tue, 17 Feb 2026 18:39:53 +0100 Subject: [PATCH 7/7] Add backend configuration checks and dashboard button visibility across webviews --- src/extension.ts | 34 ++++++++++++++++++------ src/webview/chart/main.ts | 7 +++++ src/webview/details/main.ts | 7 +++-- src/webview/diagnostics/main.ts | 7 +++++ src/webview/fluency-level-viewer/main.ts | 5 ++++ src/webview/maturity/main.ts | 7 +++-- src/webview/usage/main.ts | 5 ++++ 7 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index dd7f82e0..c6f9b221 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5910,6 +5910,7 @@ class CopilotTokenTracker implements vscode.Disposable { break; case 'showDashboard': await this.showDashboard(); + break; case 'shareToLinkedIn': await this.shareToSocialMedia('linkedin'); break; @@ -6573,7 +6574,8 @@ private getFluencyLevelViewerHtml(webview: vscode.Webview, data: { `script-src 'nonce-${nonce}'` ].join('; '); - const initialData = JSON.stringify(data).replace(/ @@ -6616,7 +6618,8 @@ private getMaturityHtml(webview: vscode.Webview, data: { `script-src 'nonce-${nonce}'` ].join('; '); - const initialData = JSON.stringify(data).replace(/ @@ -6855,8 +6858,9 @@ private getMaturityHtml(webview: vscode.Webview, data: { `script-src 'nonce-${nonce}'` ].join('; '); - const initialDataScript = data - ? `` + const dataWithBackend = data ? { ...data, backendConfigured: this.isBackendConfigured() } : undefined; + const initialDataScript = dataWithBackend + ? `` : ''; return ` @@ -6884,6 +6888,17 @@ private getMaturityHtml(webview: vscode.Webview, data: { return text; } + /** + * Check if backend sync is configured for Team Dashboard access. + */ + private isBackendConfigured(): boolean { + if (!this.backend) { + return false; + } + const settings = this.backend.getSettings(); + return this.backend.isConfigured(settings); + } + private getDetailsHtml(webview: vscode.Webview, stats: DetailedStats): string { const nonce = this.getNonce(); const scriptUri = webview.asWebviewUri( @@ -6898,7 +6913,8 @@ private getMaturityHtml(webview: vscode.Webview, data: { `script-src 'nonce-${nonce}'` ].join('; '); - const initialData = JSON.stringify(stats).replace(/ @@ -7620,7 +7636,7 @@ private getMaturityHtml(webview: vscode.Webview, data: { storagePath: storageFilePath }; - const initialData = JSON.stringify({ report, sessionFiles, detailedSessionFiles, sessionFolders, cacheInfo, backendStorageInfo }).replace(/ @@ -7750,7 +7766,8 @@ private getMaturityHtml(webview: vscode.Webview, data: { totalTokens, avgTokensPerDay: dailyStats.length > 0 ? Math.round(totalTokens / dailyStats.length) : 0, totalSessions, - lastUpdated: new Date().toISOString() + lastUpdated: new Date().toISOString(), + backendConfigured: this.isBackendConfigured() }; const initialData = JSON.stringify(chartData).replace(/ diff --git a/src/webview/chart/main.ts b/src/webview/chart/main.ts index 8943d77c..17037be9 100644 --- a/src/webview/chart/main.ts +++ b/src/webview/chart/main.ts @@ -28,6 +28,7 @@ type InitialChartData = { avgTokensPerDay: number; totalSessions: number; lastUpdated: string; + backendConfigured?: boolean; }; // VS Code injects this in the webview environment @@ -85,6 +86,9 @@ function renderLayout(data: InitialChartData): void { createButton(BUTTONS['btn-diagnostics']), createButton(BUTTONS['btn-maturity']) ); + if (data.backendConfigured) { + buttons.append(createButton(BUTTONS['btn-dashboard'])); + } header.append(headerLeft, buttons); const summarySection = el('div', 'section'); @@ -169,6 +173,9 @@ function wireInteractions(data: InitialChartData): void { const maturity = document.getElementById('btn-maturity'); maturity?.addEventListener('click', () => vscode.postMessage({ command: 'showMaturity' })); + const dashboard = document.getElementById('btn-dashboard'); + dashboard?.addEventListener('click', () => vscode.postMessage({ command: 'showDashboard' })); + const viewButtons = [ { id: 'view-total', view: 'total' as const }, { id: 'view-model', view: 'model' as const }, diff --git a/src/webview/details/main.ts b/src/webview/details/main.ts index a274ff6b..f08b7ba6 100644 --- a/src/webview/details/main.ts +++ b/src/webview/details/main.ts @@ -32,6 +32,7 @@ type DetailedStats = { lastMonth: PeriodStats; last30Days: PeriodStats; lastUpdated: string | Date; + backendConfigured?: boolean; }; // VS Code injects this in the webview environment @@ -119,9 +120,11 @@ function renderShell( createButton(BUTTONS['btn-chart']), createButton(BUTTONS['btn-usage']), createButton(BUTTONS['btn-diagnostics']), - createButton(BUTTONS['btn-maturity']), - createButton(BUTTONS['btn-dashboard']) + createButton(BUTTONS['btn-maturity']) ); + if (stats.backendConfigured) { + buttonRow.append(createButton(BUTTONS['btn-dashboard'])); + } header.append(title, buttonRow); diff --git a/src/webview/diagnostics/main.ts b/src/webview/diagnostics/main.ts index e49b3ebc..340c48e2 100644 --- a/src/webview/diagnostics/main.ts +++ b/src/webview/diagnostics/main.ts @@ -64,6 +64,7 @@ type DiagnosticsData = { detailedSessionFiles?: SessionFileDetails[]; cacheInfo?: CacheInfo; backendStorageInfo?: BackendStorageInfo; + backendConfigured?: boolean; }; type DiagnosticsViewState = { @@ -735,6 +736,7 @@ function renderLayout(data: DiagnosticsData): void { ${buttonHtml("btn-chart")} ${buttonHtml("btn-usage")} ${buttonHtml("btn-maturity")} + ${data?.backendConfigured ? buttonHtml("btn-dashboard") : ""} @@ -1393,6 +1395,11 @@ function renderLayout(data: DiagnosticsData): void { ?.addEventListener("click", () => vscode.postMessage({ command: "showMaturity" }), ); + document + .getElementById("btn-dashboard") + ?.addEventListener("click", () => + vscode.postMessage({ command: "showDashboard" }), + ); setupSortHandlers(); setupEditorFilterHandlers(); diff --git a/src/webview/fluency-level-viewer/main.ts b/src/webview/fluency-level-viewer/main.ts index 406ef22e..c23b687a 100644 --- a/src/webview/fluency-level-viewer/main.ts +++ b/src/webview/fluency-level-viewer/main.ts @@ -21,6 +21,7 @@ type LevelInfo = { type FluencyLevelData = { categories: CategoryLevelData[]; isDebugMode: boolean; + backendConfigured?: boolean; }; declare function acquireVsCodeApi(): { @@ -140,6 +141,7 @@ function renderLayout(data: FluencyLevelData): void { ${buttonHtml('btn-chart')} ${buttonHtml('btn-usage')} ${buttonHtml('btn-diagnostics')} + ${data.backendConfigured ? buttonHtml('btn-dashboard') : ''} @@ -185,6 +187,9 @@ function renderLayout(data: FluencyLevelData): void { document.getElementById('btn-diagnostics')?.addEventListener('click', () => { vscode.postMessage({ command: 'showDiagnostics' }); }); + document.getElementById('btn-dashboard')?.addEventListener('click', () => { + vscode.postMessage({ command: 'showDashboard' }); + }); // Wire up category selection buttons document.querySelectorAll('.category-btn').forEach(btn => { diff --git a/src/webview/maturity/main.ts b/src/webview/maturity/main.ts index 64b348fd..8c4945d4 100644 --- a/src/webview/maturity/main.ts +++ b/src/webview/maturity/main.ts @@ -59,6 +59,7 @@ type MaturityData = { dismissedTips?: string[]; isDebugMode?: boolean; fluencyLevels?: CategoryLevelData[]; + backendConfigured?: boolean; }; declare function acquireVsCodeApi(): { @@ -355,8 +356,7 @@ function renderLayout(data: MaturityData): void { ${buttonHtml('btn-details')} ${buttonHtml('btn-chart')} ${buttonHtml('btn-usage')} - ${buttonHtml('btn-diagnostics')} - + ${buttonHtml('btn-diagnostics')} ${data.backendConfigured ? buttonHtml('btn-dashboard') : ''}
@@ -487,6 +487,9 @@ function renderLayout(data: MaturityData): void { document.getElementById('btn-diagnostics')?.addEventListener('click', () => { vscode.postMessage({ command: 'showDiagnostics' }); }); + document.getElementById('btn-dashboard')?.addEventListener('click', () => { + vscode.postMessage({ command: 'showDashboard' }); + }); // Wire up share to issue button document.getElementById('btn-share-issue')?.addEventListener('click', () => { diff --git a/src/webview/usage/main.ts b/src/webview/usage/main.ts index 0b3b0cc4..defe22f7 100644 --- a/src/webview/usage/main.ts +++ b/src/webview/usage/main.ts @@ -42,6 +42,7 @@ type UsageAnalysisStats = { month: UsageAnalysisPeriod; lastUpdated: string; customizationMatrix?: WorkspaceCustomizationMatrix | null; + backendConfigured?: boolean; }; declare function acquireVsCodeApi(): { @@ -245,6 +246,7 @@ function renderLayout(stats: UsageAnalysisStats): void { ${buttonHtml('btn-chart')} ${buttonHtml('btn-diagnostics')} ${buttonHtml('btn-maturity')} + ${stats.backendConfigured ? buttonHtml('btn-dashboard') : ''}
@@ -666,6 +668,9 @@ function renderLayout(stats: UsageAnalysisStats): void { document.getElementById('btn-maturity')?.addEventListener('click', () => { vscode.postMessage({ command: 'showMaturity' }); }); + document.getElementById('btn-dashboard')?.addEventListener('click', () => { + vscode.postMessage({ command: 'showDashboard' }); + }); // Copy path buttons in customization list Array.from(document.getElementsByClassName('cf-copy')).forEach((el) => {