Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 56 additions & 14 deletions vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as vscode from 'vscode';
import type { AiFluencyExtensionApi, ExtensionPointButton } from './extensionPoints';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
Expand Down Expand Up @@ -239,6 +240,9 @@ class CopilotTokenTracker implements vscode.Disposable {
/**
* Scan a workspace folder for customization files according to `customizationPatterns.json`.
*/
/** Buttons registered by companion extensions via the extension points API. */
private readonly _extensionPointButtons = new Map<string, { config: ExtensionPointButton; handler: () => void | Promise<void> }>();

private _disposed = false;
private updateInterval: NodeJS.Timeout | undefined;
private detailsPanel: vscode.WebviewPanel | undefined;
Expand Down Expand Up @@ -432,11 +436,35 @@ class CopilotTokenTracker implements vscode.Disposable {
}
}

public registerExtensionPointButton(button: ExtensionPointButton, handler: () => void | Promise<void>): { dispose(): void } {
this._extensionPointButtons.set(button.id, { config: button, handler });
return {
dispose: () => {
this._extensionPointButtons.delete(button.id);
},
};
}

private extensionPointButtonsScript(nonce: string): string {
const data = [...this._extensionPointButtons.values()].map(e => ({ id: e.config.id, label: e.config.label }));
return `<script nonce="${nonce}">window.__EXTENSION_POINT_BUTTONS__ = ${JSON.stringify(data)};</script>`;
}

private async handleExtensionPointAction(buttonId: string): Promise<boolean> {
const entry = this._extensionPointButtons.get(buttonId);
if (!entry) { return false; }
await this.dispatch(`extensionPoint:${buttonId}`, () => entry.handler());
return true;
}

/**
* Dispatch a shared navigation command that is common across all webview panels.
* Returns true if the command was recognised and dispatched, false if it is panel-specific.
*/
private async dispatchSharedCommand(command: string): Promise<boolean> {
private async dispatchSharedCommand(message: { command: string; [key: string]: any }): Promise<boolean> {
if (message.command === 'extensionPointAction' && typeof message.buttonId === 'string') {
return this.handleExtensionPointAction(message.buttonId);
}
const handlers: Record<string, () => unknown> = {
showDetails: () => this.showDetails(),
showChart: () => this.showChart(),
Expand All @@ -447,9 +475,9 @@ class CopilotTokenTracker implements vscode.Disposable {
showEnvironmental: () => this.showEnvironmental(),
showFluencyLevelViewer: () => this.showFluencyLevelViewer(),
};
const handler = handlers[command];
const handler = handlers[message.command];
if (!handler) { return false; }
await this.dispatch(command, handler);
await this.dispatch(message.command, handler);
return true;
}

Expand Down Expand Up @@ -4156,7 +4184,7 @@ usageAnalysis: undefined
// Handle messages from the webview
this.detailsPanel.webview.onDidReceiveMessage(async (message) => {
if (this.handleLocalViewRegressionMessage(message)) { return; }
if (await this.dispatchSharedCommand(message.command)) { return; }
if (await this.dispatchSharedCommand(message)) { return; }
switch (message.command) {
case 'refresh':
await this.dispatch('refresh:details', () => this.refreshDetailsPanel());
Expand Down Expand Up @@ -4209,7 +4237,7 @@ usageAnalysis: undefined

this.environmentalPanel.webview.onDidReceiveMessage(async (message) => {
if (this.handleLocalViewRegressionMessage(message)) { return; }
if (await this.dispatchSharedCommand(message.command)) { return; }
if (await this.dispatchSharedCommand(message)) { return; }
if (message.command === 'refresh') {
await this.dispatch('refresh:environmental', async () => {
const refreshed = await this.updateTokenStats();
Expand Down Expand Up @@ -4260,6 +4288,7 @@ usageAnalysis: undefined
<body>
<div id="root"></div>
<script nonce="${nonce}">window.__INITIAL_ENVIRONMENTAL__ = ${initialData};</script>
${this.extensionPointButtonsScript(nonce)}
${this.getLocalViewRegressionProbeScript('environmental', nonce)}
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
Expand Down Expand Up @@ -4301,7 +4330,7 @@ usageAnalysis: undefined
// Handle messages from the webview
this.chartPanel.webview.onDidReceiveMessage(async (message) => {
if (this.handleLocalViewRegressionMessage(message)) { return; }
if (await this.dispatchSharedCommand(message.command)) { return; }
if (await this.dispatchSharedCommand(message)) { return; }
if (message.command === 'refresh') {
await this.dispatch('refresh:chart', () => this.refreshChartPanel());
}
Expand Down Expand Up @@ -4364,7 +4393,7 @@ usageAnalysis: undefined
// Handle messages from the webview
this.analysisPanel.webview.onDidReceiveMessage(async (message) => {
if (this.handleLocalViewRegressionMessage(message)) { return; }
if (await this.dispatchSharedCommand(message.command)) { return; }
if (await this.dispatchSharedCommand(message)) { return; }
switch (message.command) {
case 'refresh':
await this.dispatch('refresh:analysis', () => this.refreshAnalysisPanel());
Expand Down Expand Up @@ -4762,7 +4791,7 @@ Return ONLY the JSON object, no markdown formatting, no explanations.`;

// Handle messages from the webview
this.logViewerPanel.webview.onDidReceiveMessage(async (message) => {
if (await this.dispatchSharedCommand(message.command)) { return; }
if (await this.dispatchSharedCommand(message)) { return; }
switch (message.command) {
case 'openRawFile':
await this.dispatch('openRawFile:logviewer', async () => {
Expand Down Expand Up @@ -4990,6 +5019,7 @@ Return ONLY the JSON object, no markdown formatting, no explanations.`;
<body>
<div id="root"></div>
<script nonce="${nonce}">window.__INITIAL_LOGDATA__ = ${initialData};</script>
${this.extensionPointButtonsScript(nonce)}
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
Expand Down Expand Up @@ -5082,7 +5112,7 @@ Return ONLY the JSON object, no markdown formatting, no explanations.`;
const fluencyLevels = isDebugMode ? this.getFluencyLevelData(isDebugMode).categories : undefined;
this.maturityPanel.webview.onDidReceiveMessage(async (message) => {
if (this.handleLocalViewRegressionMessage(message)) { return; }
if (await this.dispatchSharedCommand(message.command)) { return; }
if (await this.dispatchSharedCommand(message)) { return; }
switch (message.command) {
case 'refresh':
await this.dispatch('refresh:maturity', () => this.refreshMaturityPanel());
Expand Down Expand Up @@ -5574,7 +5604,7 @@ ${hashtag}`;
this.fluencyLevelViewerPanel.webview.onDidReceiveMessage(
async (message) => {
if (this.handleLocalViewRegressionMessage(message)) { return; }
if (await this.dispatchSharedCommand(message.command)) { return; }
if (await this.dispatchSharedCommand(message)) { return; }
if (message.command === "refresh") {
await this.dispatch('refresh:fluencyLevelViewer', () => this.refreshFluencyLevelViewerPanel());
}
Expand Down Expand Up @@ -5607,6 +5637,8 @@ ${hashtag}`;
this.log("✅ Scoring Guide refreshed");
}



private getFluencyLevelData(isDebugMode: boolean): ReturnType<typeof _getFluencyLevelData> {
return _getFluencyLevelData(isDebugMode);
}
Expand Down Expand Up @@ -5666,6 +5698,7 @@ ${hashtag}`;
<body>
<div id="root"></div>
<script nonce="${nonce}">window.__INITIAL_FLUENCY_LEVEL_DATA__ = ${initialData};</script>
${this.extensionPointButtonsScript(nonce)}
${this.getLocalViewRegressionProbeScript('fluency-level-viewer', nonce)}
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
Expand Down Expand Up @@ -5734,6 +5767,7 @@ ${hashtag}`;
<body>
<div id="root"></div>
<script nonce="${nonce}">window.__INITIAL_MATURITY__ = ${initialData};</script>
${this.extensionPointButtonsScript(nonce)}
${this.getLocalViewRegressionProbeScript('maturity', nonce)}
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
Expand Down Expand Up @@ -5789,7 +5823,7 @@ ${hashtag}`;
);

this.dashboardPanel.webview.onDidReceiveMessage(async (message) => {
if (await this.dispatchSharedCommand(message.command)) { return; }
if (await this.dispatchSharedCommand(message)) { return; }
switch (message.command) {
case "refresh":
await this.dispatch('refresh:dashboard', () => this.refreshDashboardPanel());
Expand Down Expand Up @@ -6505,6 +6539,7 @@ ${hashtag}`;
<body>
<div id="root"></div>
${initialDataScript}
${this.extensionPointButtonsScript(nonce)}
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
Expand Down Expand Up @@ -6575,6 +6610,7 @@ ${hashtag}`;
<body>
<div id="root"></div>
<script nonce="${nonce}">window.__INITIAL_DETAILS__ = ${initialData};</script>
${this.extensionPointButtonsScript(nonce)}
${this.getLocalViewRegressionProbeScript('details', nonce)}
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
Expand Down Expand Up @@ -6888,7 +6924,7 @@ ${hashtag}`;
// Handle messages from the webview
this.diagnosticsPanel.webview.onDidReceiveMessage(async (message) => {
if (this.handleLocalViewRegressionMessage(message)) { return; }
if (await this.dispatchSharedCommand(message.command)) { return; }
if (await this.dispatchSharedCommand(message)) { return; }
switch (message.command) {
case "copyReport":
await this.dispatch('copyReport:diagnostics', async () => {
Expand Down Expand Up @@ -7790,6 +7826,7 @@ ${hashtag}`;
<body>
<div id="root"></div>
<script nonce="${nonce}">window.__INITIAL_DIAGNOSTICS__ = ${initialData};</script>
${this.extensionPointButtonsScript(nonce)}
${this.getLocalViewRegressionProbeScript('diagnostics', nonce)}
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
Expand Down Expand Up @@ -8070,6 +8107,7 @@ ${hashtag}`;
<body>
<div id="root"></div>
<script nonce="${nonce}">window.__INITIAL_CHART__ = ${initialData};</script>
${this.extensionPointButtonsScript(nonce)}
${this.getLocalViewRegressionProbeScript('chart', nonce)}
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
Expand Down Expand Up @@ -8139,6 +8177,7 @@ ${hashtag}`;
<body>
<div id="root"></div>
<script nonce="${nonce}">window.__INITIAL_USAGE__ = ${initialData};</script>
${this.extensionPointButtonsScript(nonce)}
${this.getLocalViewRegressionProbeScript('usage', nonce)}
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
Expand Down Expand Up @@ -8280,7 +8319,7 @@ async function checkForLegacyExtensionConflict(context: vscode.ExtensionContext)
}
}

export async function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext): Promise<AiFluencyExtensionApi> {
// Create the token tracker
const tokenTracker = new CopilotTokenTracker(context.extensionUri, context);

Expand Down Expand Up @@ -8520,7 +8559,10 @@ export async function activate(context: vscode.ExtensionContext) {

tokenTracker.log("Extension activation complete");

return {};
return {
registerButton: (button: ExtensionPointButton, handler: () => void | Promise<void>) =>
tokenTracker.registerExtensionPointButton(button, handler),
};
}

export function deactivate() {
Expand Down
22 changes: 22 additions & 0 deletions vscode-extension/src/extensionPoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* A button contribution registered via the extension points API.
*/
export interface ExtensionPointButton {
/** Stable unique identifier for this button. Used to route click events. */
readonly id: string;
/** Display label shown in the button. */
readonly label: string;
}

/**
* Public API exported by the AI Engineering Fluency extension.
* Companion extensions can acquire this via `vscode.extensions.getExtension(...).exports`.
*/
export interface AiFluencyExtensionApi {
/**
* Register a button to appear in the navigation toolbar of all webview panels.
* The handler is called when the user clicks the button.
* Returns a Disposable; call dispose() to remove the button.
*/
registerButton(button: ExtensionPointButton, handler: () => void | Promise<void>): { dispose(): void };
}
3 changes: 3 additions & 0 deletions vscode-extension/src/webview/chart/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { el, createButton } from '../shared/domUtils';
import { BUTTONS } from '../shared/buttonConfig';
import { formatCompact, setCompactNumbers } from '../shared/formatUtils';
import { wireExtensionPointButtons } from '../shared/extensionPoints';
// CSS imported as text via esbuild
import themeStyles from '../shared/theme.css';
import styles from './styles.css';
Expand Down Expand Up @@ -322,6 +323,8 @@ function wireInteractions(data: InitialChartData): void {
const environmental = document.getElementById('btn-environmental');
environmental?.addEventListener('click', () => vscode.postMessage({ command: 'showEnvironmental' }));

wireExtensionPointButtons(vscode);

// Period toggle buttons
const periodButtons: Array<{ id: string; period: ChartPeriod }> = [
{ id: 'period-day', period: 'day' },
Expand Down
2 changes: 2 additions & 0 deletions vscode-extension/src/webview/dashboard/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BUTTONS } from "../shared/buttonConfig";
import { createButton, el } from "../shared/domUtils";
import { formatCost, formatNumber, formatCompact, setCompactNumbers } from "../shared/formatUtils";
import { getModelDisplayName } from "../shared/modelUtils";
import { wireExtensionPointButtons } from "../shared/extensionPoints";
import themeStyles from "../shared/theme.css";
import styles from "./styles.css";

Expand Down Expand Up @@ -612,6 +613,7 @@ function wireButtons(): void {
});

// Note: No dashboard button handler - users are already on the dashboard
wireExtensionPointButtons(vscode);
}

// Listen for messages from the extension
Expand Down
3 changes: 3 additions & 0 deletions vscode-extension/src/webview/details/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getModelDisplayName } from '../shared/modelUtils';
import { getEditorIcon, getCharsPerToken, formatFixed, formatPercent, formatNumber, formatCost, formatCompact, setCompactNumbers } from '../shared/formatUtils';
import { el, createButton } from '../shared/domUtils';
import { BUTTONS } from '../shared/buttonConfig';
import { wireExtensionPointButtons } from '../shared/extensionPoints';
// Token estimators loaded from JSON
// @ts-ignore
import tokenEstimatorsJson from '../../tokenEstimators.json';
Expand Down Expand Up @@ -839,6 +840,8 @@ function wireButtons(): void {

const environmental = document.getElementById('btn-environmental');
environmental?.addEventListener('click', () => vscode.postMessage({ command: 'showEnvironmental' }));

wireExtensionPointButtons(vscode);
}

async function bootstrap(): Promise<void> {
Expand Down
2 changes: 2 additions & 0 deletions vscode-extension/src/webview/diagnostics/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Diagnostics Report webview with tabbed interface
import { buttonHtml } from "../shared/buttonConfig";
import { wireExtensionPointButtons } from "../shared/extensionPoints";
// CSS imported as text via esbuild
import themeStyles from "../shared/theme.css";
import styles from "./styles.css";
Expand Down Expand Up @@ -2331,6 +2332,7 @@ function renderLayout(data: DiagnosticsData): void {
?.addEventListener("click", () =>
vscode.postMessage({ command: "showEnvironmental" }),
);
wireExtensionPointButtons(vscode);

setupSortHandlers();
setupEditorFilterHandlers();
Expand Down
2 changes: 2 additions & 0 deletions vscode-extension/src/webview/environmental/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { el, createButton } from '../shared/domUtils';
import { BUTTONS } from '../shared/buttonConfig';
import { formatFixed, formatNumber, formatCompact, setCompactNumbers } from '../shared/formatUtils';
import { wireExtensionPointButtons } from '../shared/extensionPoints';
// CSS imported as text via esbuild
import themeStyles from '../shared/theme.css';
import styles from './styles.css';
Expand Down Expand Up @@ -298,6 +299,7 @@ function wireButtons(): void {
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' }));
wireExtensionPointButtons(vscode);
}

window.addEventListener('message', (event: MessageEvent) => {
Expand Down
2 changes: 2 additions & 0 deletions vscode-extension/src/webview/fluency-level-viewer/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Fluency Level Viewer webview
import { buttonHtml } from '../shared/buttonConfig';
import { wireExtensionPointButtons } from '../shared/extensionPoints';
import styles from './styles.css';

// ── Types ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -203,6 +204,7 @@ function renderLayout(data: FluencyLevelData): void {
document.getElementById('btn-dashboard')?.addEventListener('click', () => {
vscode.postMessage({ command: 'showDashboard' });
});
wireExtensionPointButtons(vscode);

// Wire up category selection buttons
document.querySelectorAll('.category-btn').forEach(btn => {
Expand Down
2 changes: 2 additions & 0 deletions vscode-extension/src/webview/maturity/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Maturity Score webview
import { buttonHtml } from '../shared/buttonConfig';
import type { ContextReferenceUsage } from '../shared/contextRefUtils';
import { wireExtensionPointButtons } from '../shared/extensionPoints';
import themeStyles from '../shared/theme.css';
import styles from './styles.css';

Expand Down Expand Up @@ -566,6 +567,7 @@ function renderLayout(data: MaturityData): void {
document.getElementById('btn-environmental')?.addEventListener('click', () => {
vscode.postMessage({ command: 'showEnvironmental' });
});
wireExtensionPointButtons(vscode);

// Wire up share to issue button
document.getElementById('btn-share-issue')?.addEventListener('click', () => {
Expand Down
Loading
Loading