Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions docs/README.plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
84 changes: 84 additions & 0 deletions extensions/usage-insights/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 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`
- 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
Binary file added extensions/usage-insights/assets/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions extensions/usage-insights/copilot-extension.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "usage-insights",
"version": 1
}
206 changes: 206 additions & 0 deletions extensions/usage-insights/extension.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
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 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 clients = new Set();
const server = createServer(async (req, res) => {
const url = new URL(req.url || "/", "http://127.0.0.1");

if (req.method === "GET" && url.pathname === "/") {
try {
const initialData = await buildDashboardData(defaults);
Comment thread
davidkaya marked this conversation as resolved.
res.writeHead(200, {
"Cache-Control": "no-store",
"Content-Type": "text/html; charset=utf-8",
});
res.end(renderDashboardHtml({ instanceId, defaults, 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}/` };
}

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();
});
Loading
Loading