Session usage: — AI credits · — calls
+ ++ +
+Usage by agent
+Token breakdown
+— total tokens
+Session history
+Recent locally recorded usage
+Highest-cost sessions
+Select one to inspect its agent breakdown
+diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json
index cd19cc914..fd0f0e800 100644
--- a/.github/plugin/marketplace.json
+++ b/.github/plugin/marketplace.json
@@ -1832,6 +1832,12 @@
"sha": "f8abab778cfb5b57a3c745d540631b7aab15d5b3"
}
},
+ {
+ "name": "usage-insights",
+ "source": "plugins/usage-insights",
+ "description": "Inspect current and historical GitHub Copilot token and AI-credit usage with per-agent rollups and in-canvas session drill-down.",
+ "version": "1.0.0"
+ },
{
"name": "vercel-plugin",
"description": "Build and deploy web apps and agents. Comprehensive Vercel ecosystem plugin — relational knowledge graph, skills for every major product, specialized agents, and Vercel conventions. Turns any AI agent into a Vercel expert.",
diff --git a/docs/README.agents.md b/docs/README.agents.md
index eac43b3ef..608834918 100644
--- a/docs/README.agents.md
+++ b/docs/README.agents.md
@@ -90,7 +90,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-agents) for guidelines on how to
| [Doublecheck](../agents/doublecheck.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fdoublecheck.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fdoublecheck.agent.md) | Interactive verification agent for AI-generated output. Runs a three-layer pipeline (self-audit, source verification, adversarial review) and produces structured reports with source links for human review. | |
| [Droid](../agents/droid.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fdroid.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fdroid.agent.md) | Provides installation guidance, usage examples, and automation patterns for the Droid CLI, with emphasis on droid exec for CI/CD and non-interactive automation | |
| [Drupal Expert](../agents/drupal-expert.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fdrupal-expert.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fdrupal-expert.agent.md) | Expert assistant for Drupal development, architecture, and best practices using PHP 8.3+ and modern Drupal patterns | |
-| [Dynatrace Expert](../agents/dynatrace-expert.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fdynatrace-expert.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fdynatrace-expert.agent.md) | The Dynatrace Expert Agent integrates observability and security capabilities directly into GitHub workflows, enabling development teams to investigate incidents, validate deployments, triage errors, detect performance regressions, validate releases, and manage security vulnerabilities by autonomously analysing traces, logs, and Dynatrace findings. This enables targeted and precise remediation of identified issues directly within the repository. | [dynatrace](https://github.com/mcp/io.github.dynatrace-oss/Dynatrace-mcp)
[](https://aka.ms/awesome-copilot/install/mcp-vscode?name=dynatrace&config=%7B%22url%22%3A%22https%3A%2F%2Fpia1134d.dev.apps.dynatracelabs.com%2Fplatform-reserved%2Fmcp-gateway%2Fv0.1%2Fservers%2Fdynatrace-mcp%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20%24COPILOT_MCP_DT_API_TOKEN%22%7D%7D)
[](https://aka.ms/awesome-copilot/install/mcp-vscodeinsiders?name=dynatrace&config=%7B%22url%22%3A%22https%3A%2F%2Fpia1134d.dev.apps.dynatracelabs.com%2Fplatform-reserved%2Fmcp-gateway%2Fv0.1%2Fservers%2Fdynatrace-mcp%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20%24COPILOT_MCP_DT_API_TOKEN%22%7D%7D)
[](https://aka.ms/awesome-copilot/install/mcp-visualstudio/mcp-install?%7B%22url%22%3A%22https%3A%2F%2Fpia1134d.dev.apps.dynatracelabs.com%2Fplatform-reserved%2Fmcp-gateway%2Fv0.1%2Fservers%2Fdynatrace-mcp%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20%24COPILOT_MCP_DT_API_TOKEN%22%7D%7D) |
+| [Dynatrace Expert](../agents/dynatrace-expert.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fdynatrace-expert.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fdynatrace-expert.agent.md) | The Dynatrace Expert Agent integrates observability and security capabilities directly into GitHub workflows, enabling development teams to investigate incidents, validate deployments, triage errors, detect performance regressions, validate releases, and manage security vulnerabilities by autonomously analysing traces, logs, and Dynatrace findings. This enables targeted and precise remediation of identified issues directly within the repository. | [dynatrace](https://github.com/mcp/io.github.Dynatrace/dynatrace-for-ai)
[](https://aka.ms/awesome-copilot/install/mcp-vscode?name=dynatrace&config=%7B%22url%22%3A%22https%3A%2F%2Fpia1134d.dev.apps.dynatracelabs.com%2Fplatform-reserved%2Fmcp-gateway%2Fv0.1%2Fservers%2Fdynatrace-mcp%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20%24COPILOT_MCP_DT_API_TOKEN%22%7D%7D)
[](https://aka.ms/awesome-copilot/install/mcp-vscodeinsiders?name=dynatrace&config=%7B%22url%22%3A%22https%3A%2F%2Fpia1134d.dev.apps.dynatracelabs.com%2Fplatform-reserved%2Fmcp-gateway%2Fv0.1%2Fservers%2Fdynatrace-mcp%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20%24COPILOT_MCP_DT_API_TOKEN%22%7D%7D)
[](https://aka.ms/awesome-copilot/install/mcp-visualstudio/mcp-install?%7B%22url%22%3A%22https%3A%2F%2Fpia1134d.dev.apps.dynatracelabs.com%2Fplatform-reserved%2Fmcp-gateway%2Fv0.1%2Fservers%2Fdynatrace-mcp%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20%24COPILOT_MCP_DT_API_TOKEN%22%7D%7D) |
| [Elasticsearch Agent](../agents/elasticsearch-observability.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Felasticsearch-observability.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Felasticsearch-observability.agent.md) | Our expert AI assistant for debugging code (O11y), optimizing vector search (RAG), and remediating security threats using live Elastic data. | elastic-mcp
[](https://aka.ms/awesome-copilot/install/mcp-vscode?name=elastic-mcp&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22mcp-remote%22%2C%22https%253A%252F%252F%257BKIBANA_URL%257D%252Fapi%252Fagent_builder%252Fmcp%22%2C%22--header%22%2C%22Authorization%253A%2524%257BAUTH_HEADER%257D%22%5D%2C%22env%22%3A%7B%7D%7D)
[](https://aka.ms/awesome-copilot/install/mcp-vscodeinsiders?name=elastic-mcp&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22mcp-remote%22%2C%22https%253A%252F%252F%257BKIBANA_URL%257D%252Fapi%252Fagent_builder%252Fmcp%22%2C%22--header%22%2C%22Authorization%253A%2524%257BAUTH_HEADER%257D%22%5D%2C%22env%22%3A%7B%7D%7D)
[](https://aka.ms/awesome-copilot/install/mcp-visualstudio/mcp-install?%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22mcp-remote%22%2C%22https%253A%252F%252F%257BKIBANA_URL%257D%252Fapi%252Fagent_builder%252Fmcp%22%2C%22--header%22%2C%22Authorization%253A%2524%257BAUTH_HEADER%257D%22%5D%2C%22env%22%3A%7B%7D%7D) |
| [Electron Code Review Mode Instructions](../agents/electron-angular-native.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Felectron-angular-native.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Felectron-angular-native.agent.md) | Code Review Mode tailored for Electron app with Node.js backend (main), Angular frontend (render), and native integration layer (e.g., AppleScript, shell, or native tooling). Services in other repos are not reviewed here. | |
| [Ember](../agents/ember.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fember.agent.md)
[](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fember.agent.md) | An AI partner, not an assistant. Ember carries fire from person to person — helping humans discover that AI partnership isn't something you learn, it's something you find. | |
diff --git a/docs/README.plugins.md b/docs/README.plugins.md
index 274783eff..296ab2406 100644
--- a/docs/README.plugins.md
+++ b/docs/README.plugins.md
@@ -122,6 +122,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t
| [typescript-mcp-development](../plugins/typescript-mcp-development/README.md) | Complete toolkit for building Model Context Protocol (MCP) servers in TypeScript/Node.js using the official SDK. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance. | 2 items | typescript, mcp, model-context-protocol, nodejs, server-development |
| [typespec-m365-copilot](../plugins/typespec-m365-copilot/README.md) | Comprehensive collection of prompts, instructions, and resources for building declarative agents and API plugins using TypeSpec for Microsoft 365 Copilot extensibility. | 3 items | typespec, m365-copilot, declarative-agents, api-plugins, agent-development, microsoft-365 |
| [uizze](../plugins/uizze/README.md) | Build product-specific interfaces with a focused UIZZE workflow, optional real-screen evidence, and a practical finish check. | 1 items | ui, design, frontend, ios, web, design-review, quality-gate |
+| [usage-insights](../plugins/usage-insights/README.md) | Inspect current and historical GitHub Copilot token and AI-credit usage with per-agent rollups and in-canvas session drill-down. | 1 items | ai-credits, canvas, copilot-extension, session-analytics, token-usage, usage-insights |
| [visual-pr](../plugins/visual-pr/README.md) | Capture, annotate, and embed screenshots and animated GIF demos in pull request descriptions. Includes Playwright-based UI capture, PIL image annotations, PR embedding workflows for GitHub and Azure DevOps, and screen recording with variable timing. | 4 items | screenshots, pull-request, before-after, annotations, playwright, gif, screen-recording, visual |
| [where-was-i](../plugins/where-was-i/README.md) | Reconstruct your dev context (branch, commits, uncommitted work, PR clues) and trigger a resume prompt to continue quickly. | 1 items | branch-state, developer-context, git-history, interrupt-recovery, pull-request-context, resume-work |
| [windows-app-storage-inspector-cleanup](../plugins/windows-app-storage-inspector-cleanup/README.md) | Inspect Windows application storage, understand local disk usage, and safely move approved cleanup items to the Recycle Bin. | 1 items | app-storage, canvas, cleanup, storage, windows |
diff --git a/extensions/usage-insights/README.md b/extensions/usage-insights/README.md
new file mode 100644
index 000000000..accbb374e
--- /dev/null
+++ b/extensions/usage-insights/README.md
@@ -0,0 +1,85 @@
+# Usage Insights
+
+A GitHub Copilot canvas extension for inspecting local session usage:
+
+- current and historical AI-credit totals
+- input, output, reasoning, cache-read, and cache-write tokens
+- root-agent and per-sub-agent rollups
+- model-call counts and durations
+- recent and all-session aggregates
+- highest-cost local sessions with in-canvas drill-down
+
+The interface follows GitHub Copilot's native visual language and refreshes while
+the canvas is open.
+
+## Privacy
+
+All metrics are read locally from Copilot's session data. The extension:
+
+- binds its renderer to `127.0.0.1`
+- requires a per-instance capability token on every local HTTP and SSE route
+- opens local session databases read-only
+- does not send usage data to an external service
+- keeps historical session drill-down inside the canvas
+
+The published preview uses entirely synthetic sample data.
+
+## Installation
+
+```bash
+copilot plugin install usage-insights@awesome-copilot
+```
+
+Reload extensions, then ask Copilot to open the **Usage Insights** canvas.
+
+## Canvas actions
+
+### `refresh`
+
+Returns current metrics for an optional history range or selected session.
+
+```json
+{
+ "range": "7d",
+ "sessionId": "optional-session-id"
+}
+```
+
+Supported ranges are `24h`, `7d`, `30d`, and `all`.
+
+### `inspect_session`
+
+Returns overall and per-agent metrics for one local session.
+
+```json
+{
+ "sessionId": "required-session-id",
+ "range": "7d"
+}
+```
+
+## Structure
+
+- `extension.mjs` - canvas declaration, loopback server, actions, and refresh events
+- `stats.mjs` - read-only SQLite aggregation and sub-agent metadata resolution
+- `renderer.mjs` - responsive, theme-aware HTML renderer
+
+The extension intentionally has no `package.json`: `@github/copilot-sdk` is
+resolved by the Copilot extension runtime, and SQLite uses Node.js's built-in
+`node:sqlite` module.
+
+## Requirements
+
+- GitHub Copilot with extension canvas support
+- a runtime version that provides `node:sqlite`
+- local Copilot session data containing `assistant_usage_events`
+
+## Source
+
+Contributed from
+[`davidkaya/github-copilot-usage-insights`](https://github.com/davidkaya/github-copilot-usage-insights)
+at commit `206d53faab1d8a3331de96e203d0206ad21845aa`.
+
+## License
+
+MIT
diff --git a/extensions/usage-insights/assets/preview.png b/extensions/usage-insights/assets/preview.png
new file mode 100644
index 000000000..954fef320
Binary files /dev/null and b/extensions/usage-insights/assets/preview.png differ
diff --git a/extensions/usage-insights/copilot-extension.json b/extensions/usage-insights/copilot-extension.json
new file mode 100644
index 000000000..be5833b6b
--- /dev/null
+++ b/extensions/usage-insights/copilot-extension.json
@@ -0,0 +1,4 @@
+{
+ "name": "usage-insights",
+ "version": 1
+}
diff --git a/extensions/usage-insights/extension.mjs b/extensions/usage-insights/extension.mjs
new file mode 100644
index 000000000..2ab78ff3d
--- /dev/null
+++ b/extensions/usage-insights/extension.mjs
@@ -0,0 +1,228 @@
+import { randomBytes, timingSafeEqual } from "node:crypto";
+import { createServer } from "node:http";
+import { homedir } from "node:os";
+import { join } from "node:path";
+import { createCanvas, CanvasError, joinSession } from "@github/copilot-sdk/extension";
+import { AgentMetadataStore, UsageInsightsStore } from "./stats.mjs";
+import { renderDashboardHtml } from "./renderer.mjs";
+
+const RANGE_VALUES = ["24h", "7d", "30d", "all"];
+const servers = new Map();
+const copilotHome = process.env.COPILOT_HOME || join(homedir(), ".copilot");
+const statsStore = new UsageInsightsStore(copilotHome);
+const agentMetadata = new AgentMetadataStore(copilotHome);
+let session;
+
+function normalizeRange(value) {
+ return RANGE_VALUES.includes(value) ? value : "7d";
+}
+
+function json(res, status, value) {
+ res.writeHead(status, {
+ "Cache-Control": "no-store",
+ "Content-Type": "application/json; charset=utf-8",
+ });
+ res.end(JSON.stringify(value));
+}
+
+function hasCapabilityToken(url, capabilityToken) {
+ const supplied = Buffer.from(url.searchParams.get("token") || "");
+ const expected = Buffer.from(capabilityToken);
+ return supplied.length === expected.length && timingSafeEqual(supplied, expected);
+}
+
+function notifyCanvases() {
+ for (const entry of servers.values()) {
+ for (const client of entry.clients) {
+ client.write("event: refresh\ndata: {}\n\n");
+ }
+ }
+}
+
+async function buildDashboardData({ range = "7d", sessionId } = {}) {
+ if (!session) {
+ throw new CanvasError("session_not_ready", "The session metrics provider is still starting.");
+ }
+
+ const selectedSessionId = sessionId || session.sessionId;
+ const currentRuntime =
+ selectedSessionId === session.sessionId
+ ? await session.rpc.usage.getMetrics()
+ : undefined;
+ const metadata = await agentMetadata.get(selectedSessionId);
+
+ return statsStore.buildDashboard({
+ currentSessionId: session.sessionId,
+ currentRuntime,
+ range: normalizeRange(range),
+ selectedSessionId,
+ agentMetadata: metadata,
+ });
+}
+
+async function startServer(instanceId, defaults) {
+ const capabilityToken = randomBytes(32).toString("base64url");
+ const clients = new Set();
+ const server = createServer(async (req, res) => {
+ const url = new URL(req.url || "/", "http://127.0.0.1");
+ if (!hasCapabilityToken(url, capabilityToken)) {
+ json(res, 403, { error: "Forbidden" });
+ return;
+ }
+
+ if (req.method === "GET" && url.pathname === "/") {
+ try {
+ const initialData = await buildDashboardData(defaults);
+ res.writeHead(200, {
+ "Cache-Control": "no-store",
+ "Content-Type": "text/html; charset=utf-8",
+ });
+ res.end(
+ renderDashboardHtml({
+ instanceId,
+ defaults: { ...defaults, capabilityToken },
+ initialData,
+ }),
+ );
+ } catch (error) {
+ res.writeHead(500, {
+ "Cache-Control": "no-store",
+ "Content-Type": "text/plain; charset=utf-8",
+ });
+ res.end(error instanceof Error ? error.message : "Unable to load session metrics.");
+ }
+ return;
+ }
+
+ if (req.method === "GET" && url.pathname === "/api/stats") {
+ try {
+ const data = await buildDashboardData({
+ range: url.searchParams.get("range") || defaults.range,
+ sessionId: url.searchParams.get("sessionId") || defaults.sessionId,
+ });
+ json(res, 200, data);
+ } catch (error) {
+ json(res, 500, {
+ error: error instanceof Error ? error.message : "Unable to load session metrics.",
+ });
+ }
+ return;
+ }
+
+ if (req.method === "GET" && url.pathname === "/events") {
+ res.writeHead(200, {
+ "Cache-Control": "no-cache",
+ Connection: "keep-alive",
+ "Content-Type": "text/event-stream",
+ });
+ res.write("event: ready\ndata: {}\n\n");
+ clients.add(res);
+ req.on("close", () => clients.delete(res));
+ return;
+ }
+
+ json(res, 404, { error: "Not found" });
+ });
+
+ await new Promise((resolve, reject) => {
+ server.once("error", reject);
+ server.listen(0, "127.0.0.1", resolve);
+ });
+ const address = server.address();
+ const port = typeof address === "object" && address ? address.port : 0;
+ return {
+ clients,
+ server,
+ url: `http://127.0.0.1:${port}/?token=${encodeURIComponent(capabilityToken)}`,
+ };
+}
+
+const usageInsightsCanvas = createCanvas({
+ id: "usage-insights",
+ displayName: "Usage Insights",
+ description: "Inspect live token and AI-credit usage for the current session, its agents, and recent history.",
+ inputSchema: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ range: { type: "string", enum: RANGE_VALUES },
+ sessionId: { type: "string", minLength: 1 },
+ },
+ },
+ actions: [
+ {
+ name: "refresh",
+ description: "Return fresh metrics for the current or selected session and time range.",
+ inputSchema: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ range: { type: "string", enum: RANGE_VALUES },
+ sessionId: { type: "string", minLength: 1 },
+ },
+ },
+ handler: async (ctx) => buildDashboardData(ctx.input),
+ },
+ {
+ name: "inspect_session",
+ description: "Return the overall and per-agent metrics for one local session.",
+ inputSchema: {
+ type: "object",
+ additionalProperties: false,
+ required: ["sessionId"],
+ properties: {
+ sessionId: { type: "string", minLength: 1 },
+ range: { type: "string", enum: RANGE_VALUES },
+ },
+ },
+ handler: async (ctx) => buildDashboardData(ctx.input),
+ },
+ ],
+ open: async (ctx) => {
+ let entry = servers.get(ctx.instanceId);
+ if (!entry) {
+ const defaults = {
+ range: normalizeRange(ctx.input?.range),
+ sessionId: ctx.input?.sessionId || "",
+ };
+ entry = await startServer(ctx.instanceId, defaults);
+ servers.set(ctx.instanceId, entry);
+ }
+ return {
+ title: "Usage Insights",
+ status: "Live",
+ url: entry.url,
+ };
+ },
+ onClose: async (ctx) => {
+ const entry = servers.get(ctx.instanceId);
+ if (!entry) {
+ return;
+ }
+ servers.delete(ctx.instanceId);
+ for (const client of entry.clients) {
+ client.end();
+ }
+ await new Promise((resolve) => entry.server.close(resolve));
+ },
+});
+
+session = await joinSession({ canvases: [usageInsightsCanvas] });
+
+const initialEvents = await session.getEvents();
+agentMetadata.seed(session.sessionId, initialEvents);
+
+session.on("assistant.usage", notifyCanvases);
+session.on("session.usage_checkpoint", notifyCanvases);
+session.on("subagent.started", (event) => {
+ agentMetadata.update(session.sessionId, event);
+ notifyCanvases();
+});
+session.on("subagent.completed", (event) => {
+ agentMetadata.update(session.sessionId, event);
+ notifyCanvases();
+});
+session.on("subagent.failed", (event) => {
+ agentMetadata.update(session.sessionId, event);
+ notifyCanvases();
+});
diff --git a/extensions/usage-insights/renderer.mjs b/extensions/usage-insights/renderer.mjs
new file mode 100644
index 000000000..373918fb8
--- /dev/null
+++ b/extensions/usage-insights/renderer.mjs
@@ -0,0 +1,931 @@
+function safeJson(value) {
+ return JSON.stringify(value).replaceAll("<", "\\u003c");
+}
+
+export function renderDashboardHtml({ instanceId, defaults, initialData }) {
+ const initial = safeJson({ ...defaults, instanceId });
+ const bootstrap = safeJson(initialData || null);
+ return `
+
+
— total tokens
+Recent locally recorded usage
+Select one to inspect its agent breakdown
+