You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add the AI Credits Breakdown canvas extension - #5871
My contribution adds a new instruction, prompt, agent, skill, workflow, or canvas extension file in the correct directory.
The file follows the required naming convention.
The content is clearly structured and follows the example format.
I have tested my instructions, prompt, agent, skill, workflow, or canvas extension with GitHub Copilot.
I have run npm start and verified that README.md is up to date.
I am targeting the main branch for this pull request.
Description
AI Credits Breakdown is a canvas extension that shows a person their own GitHub Copilot AI credit usage in detail (1 AI credit = 1 AIU = $0.01).
It is built on the user's computer from the telemetry that the Copilot CLI and the Copilot app already keep in COPILOT_HOME (default ~/.copilot). One page, with a sticky filter bar, and every panel follows every filter:
Dollars spent in a period, the daily average, the pace to month end, the previous comparable period and the busiest day.
Daily credits as a bar chart over a running-total curve. Dragging across the chart zooms the date range.
Spend by repository, owner, model, surface and call type. Any row is a click-filter.
What the credits paid for (cache reads, cache writes, output, fresh input) and an hour-of-week heat map in the local time zone.
Prompt cache: reused or rebuilt. Each metered model call is classed by what it found in the prompt cache, so you can see how much goes to rewriting a conversation into the cache after a pause.
Heaviest sessions, spend by Copilot app automation, and CSV export of the filtered rows.
The canvas has one agent action, get_summary, which returns totals and top entries for a UTC day range so the agent can answer "how much did I spend on Opus this month?". It does not return session ids, prompts or titles.
Update to existing instruction, prompt, agent, plugin, skill, workflow, or canvas extension.
Other (please specify):
Additional Notes
Privacy and security
The extension reads local files only and makes no network requests.
The databases and logs are opened read-only. Prompts, session titles, file paths and URLs are never read into the view. Session ids are shortened to 8 characters. Only repository names (owner/name), model ids and automation names are shown.
The canvas page is served from a server that listens only on 127.0.0.1. The page and its data are behind a random secret path that changes every time the canvas opens, and the server checks the Host and Origin headers.
The page loads no scripts or styles from other servers (Content-Security-Policy allows only itself).
The extension uses no paid services. The static analysis lists these capabilities: file system access, a local server, host information (node:os) and process.env (COPILOT_HOME).
Requires Node 22.13+ for node:sqlite, the same as chronicle-stats. collector.mjs loads it with process.getBuiltinModule("node:sqlite") instead of a static import. The canvas smoke test runs on Node 22, where module.builtinModules does not list node:-only modules, so it reports a static import "node:sqlite" as an undeclared dependency. I reproduced that failure locally with Node 22.23 on the already merged chronicle-stats as well, so this looks like a pre-existing checker issue rather than something specific to this extension. I did not change eng/ in this pull request.
Relation to chronicle-stats
chronicle-stats (My AI Usage) covers sessions, turns and token coverage with coaching. This extension is a different view of the same telemetry: money (USD), repositories, prompt-cache behaviour and automations, with cross-filtering across every dimension. From the Chronicle store it reads only assistant_usage_events and the id, repository and timestamps of sessions.
Testing
npm run plugin:validate and npm start pass.
node eng/canvas-smoke-test.mjs --changed-files <files> --install require passes, including the install from an ephemeral local marketplace with the Copilot CLI. (On macOS, set TMPDIR to a real path such as /private/tmp; the materialize step compares a symlinked temp path and fails for existing extensions too.)
node --test collector.test.mjs server.test.mjs passes in extensions/ai-credits-breakdown on Node 22 and Node 25 (29 tests). The tests use only fictional telemetry that sample-data.mjs writes into a temporary COPILOT_HOME.
I installed the branch into a Copilot app session, opened the canvas and called get_summary.
The ledger builder is a Node port of the Python script I used for a personal dashboard. On my own telemetry (leaving out the cloud-only sessions that script also read from a separate file, which this extension does not have) it matched the original to within 0.02 credits on a total of about 340,000, for every day, model, repository, call type, prompt-cache state, automation, and every cell of the hour-of-week grid. That comparison was made before the review fixes below. After them, the only change on the same telemetry is 12 small sessions (8.4 credits) that the original also dropped, and the token split of every call is unchanged.
The dashboard was loaded in headless Chrome across 14 filter states (and the empty and error states) with no console errors and no NaN or undefined in the page. I also drove it with a script: filters survive the "Skip to content" and "where these numbers come from" links and a refresh, the arrow keys fill the live status, an owner named constructor renders correctly, and per-call averages show decimals. The same script fails on the code before the review fixes.
Code spelling (codespell) and line endings are clean.
Preview image
The preview image shows fictional sample data from sample-data.mjs. No real usage, repository names or session ids are in this pull request.
By submitting this pull request, I confirm that my contribution abides by the Code of Conduct and will be licensed under the MIT License.
A detailed, private view of a user's own GitHub Copilot AI credit usage:
dollars by day, repository, model, surface, call type and prompt-cache
state, plus heaviest sessions and spend by automation.
The ledger is built locally and read-only from the telemetry the Copilot
CLI and app already keep in COPILOT_HOME. Nothing is sent over the
network, and prompts, titles and file paths are never read. The canvas is
served from a loopback-only server behind a per-open secret path with
Host/Origin checks and a strict CSP.
Includes unit tests that run on fictional sample telemetry.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Risk tier:merge-risk:medium — New or substantially changed resource without privileged execution Required to merge: passing submission-gate checks plus 1 approval from reviewers with write access.
Why this tier
Adds 15 file(s), e.g. extensions/ai-credits-breakdown/LICENSE
Node 22's module.builtinModules omits "node:"-only modules, so the canvas
smoke test, which runs on Node 22, reported a static import of
node:sqlite as an undeclared dependency. process.getBuiltinModule is
available from Node 22.3, below the 22.13 minimum that node:sqlite needs,
and gives a clearer error on older Node versions.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
See exactly where your own GitHub Copilot AI credits go: dollars by day, repository, model, call type and prompt-cache state, built privately on your computer from your Copilot telemetry.
Permissions and capabilities (static analysis of modules reachable from extension.mjs)
File system access (node:fs)
Network sockets / local server (node:http, node:net, ...)
Host information (node:os)
Reads environment variables (process.env)
Warnings
collector.test.mjs: import "node:test" is not a Node.js builtin, host-provided package, or declared dependency (module is not reachable from extension.mjs)
collector.test.mjs: import "node:sqlite" is not a Node.js builtin, host-provided package, or declared dependency (module is not reachable from extension.mjs)
sample-data.mjs: import "node:sqlite" is not a Node.js builtin, host-provided package, or declared dependency (module is not reachable from extension.mjs)
server.test.mjs: import "node:test" is not a Node.js builtin, host-provided package, or declared dependency (module is not reachable from extension.mjs)
Static checks compile modules without executing them. Minimum preview size is configurable with CANVAS_PREVIEW_MIN_WIDTH/CANVAS_PREVIEW_MIN_HEIGHT. Workflow run
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Unresolved defects can discard valid usage, misreport token totals and date ranges, clear active filters, and leave chart details inaccessible to keyboard and screen-reader users.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The collector drops valid session-only totals of one credit or less, while stale date-range anchors distort current-period spending reports, averages, and projections.
After the three-second ledger cache expires, each refresh or get_summary rereads every full transcript synchronously, including unchanged historical files. This work grows with total transcript history and blocks the extension's event loop during reads and scans. Cache parsed shutdown/checkpoint data by file identity, size and modification time, and use asynchronous reads for changed files.
Restore filters after skip and provenance navigation
extensions/ai-credits-breakdown/ui/app.js:130
Clicking “Skip to content” or “where these numbers come from” changes the fragment to #main or #provenance. This handler treats those anchors as filter state and clears every dimension filter, silently changing the displayed totals. Handle these navigation anchors separately and restore the serialized filter hash so a refresh preserves the selection.
Link keyboard day details to an accessible live status
extensions/ai-credits-breakdown/ui/app.js:447
The arrow-key handler updates only the visual tooltip, which is not associated with the focused chart. Screen-reader users therefore receive no selected date, credits or running total. Link a persistent live status to the chart and update it when fromKey is true.
Show attribution failures beside fallback automation data
extensions/ai-credits-breakdown/ui/app.js:515
When automation attribution fails but automation sessions exist, the collector still creates an “Unattributed automation sessions” series. That bypasses the no-series branch that displays M.automationsUnavailable, so users see missing named automations without an explanation. Show the failure message alongside the fallback series.
Format fractional per-call credits accurately
extensions/ai-credits-breakdown/ui/app.js:709
cu() rounds to whole credits and cents, so a positive 0.43-credit average appears as 0 ($0.00) in “Per call.” The comparison below can then claim a multiple of an apparently zero-cost call. Use a fractional-credit, sub-cent formatter here and for the averages at lines 708–709.
Label the column as first matching usage
extensions/ai-credits-breakdown/ui/app.js:727
This value is the earliest usage day left after all filters, not the session's creation date. A session created on October 1 can therefore appear to have started on October 28 when that day is selected. Rename the column to “First matching usage,” or include the actual creation date in the ledger.
Collector
- Keep a call's billed token details as the token split and use the
call's own counters only when it has none. The earlier change applied
the counters to every call and moved about 3.4M tokens from cache
writes to fresh input on real telemetry, away from what is billed.
- Count a session whose whole total is one credit or less. The
reconciliation tolerance now applies only against usage that is already
counted, for both the per-call residual and the session-total gap.
- Cache what is parsed from each session log by inode, size and
modification time, read changed logs asynchronously, and drop entries
for logs that have gone, so a refresh no longer rereads all history.
- Look up rates, call types, surfaces and model name parts with
Object.hasOwn, and keep model-keyed log data in null-prototype objects.
Dashboard
- Group totals by repository, owner, model and so on in null-prototype
objects so an owner named "constructor" no longer renders as NaN.
- Announce the day picked with the arrow keys through a polite live
status linked to the daily chart.
- Show per-call averages with decimals and sub-cent dollars instead of
rounding a fraction of a credit to 0 ($0.00).
- Rename the sessions column to "First matching usage".
Adds regression tests for the UTC timestamp handling, the token
fallback, small session totals, the tolerance, the log cache and
inherited-property names.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Ledger rebuilds synchronously reread every session’s complete log after the three-second cache expires, risking repeated stalls of the shared server and agent actions.
No open issues 🎉
6 previously missed issues
In code that hasn't changed since last review
Cache and asynchronously process unchanged event logs
Once the three-second ledger cache expires, rebuilding reads every session's complete events.jsonl synchronously, including unchanged logs. Large histories can repeatedly stall the shared canvas server and agent actions while allocating a buffer as large as each log. Reuse per-file results keyed by size/modification time, and stream changed or appended data asynchronously.
Do not infer cache reuse from missing legacy counts
With the supported older schema, loadEvents() defaults missing prompt and cache-read counts to zero, so the second valid call satisfies 0 >= 0 and is labelled “Cache reused” without any measurement. Preserve token-data availability and classify these calls as unknown/not itemised rather than inferring reuse, expiry or rebuilding. Add a regression case with two valid older-schema calls.
This tolerance drops entire app-only or shutdown-only sessions costing one credit or less, even though no usage has been counted yet. Their spend and session counts disappear, and a home containing only these sessions incorrectly shows no usage. Apply the tolerance only after some usage has been accounted for, both here and in the gap guard at line 592; retain every positive standalone total.
Use prototype-free accumulators for owner totals
extensions/ai-credits-breakdown/ui/app.js:177
constructor is a valid repository owner, but this ordinary object inherits a property under that key, so adding credits creates a string and the Owner dropdown displays $NaN. Use prototype-free accumulators here and in groupSum at line 614, which otherwise corrupts the Owners ranking in the same way.
Preserve focus after click-filter rerender
extensions/ai-credits-breakdown/ui/app.js:196
Activating a click-filter calls render(), which replaces the focused button through the panels’ innerHTML assignments. Keyboard users consequently lose focus on every repository/model/cache selection instead of remaining on the control whose pressed state changed. Please preserve focus on the corresponding replacement control, with an explicit fallback when removing a filter also removes its button.
Preserve fractional credits in cost formatting
extensions/ai-credits-breakdown/ui/app.js:723
Both the “Per call” column and the pause/reuse insight use cu(), which rounds fractional credits to whole credits and cents. For example, 0.13 and 0.42 credits both display as 0 ($0.00), hiding the difference even when the insight reports a cost ratio. Use a fractional-credit formatter in all three places.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Human assessment is needed because omitted findings reveal spend-attribution, cache-classification, and keyboard-accessibility risks not fully represented in the published review.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Human assessment is needed because findings omitted from the published review raise concerns about spend accuracy, cache attribution, and keyboard accessibility.
The reason will be displayed to describe this comment to others. Learn more.
Thanks — this adds useful credit accounting beyond My AI Usage, particularly the multi-source reconciliation, cache-cost breakdown and automation attribution. The local-only implementation and documented coverage limits are clear, and the Copilot-app validation is helpful. The implementation checks and smoke installation are passing; the remaining submission gate is waiting for reviewer approval rather than reporting a contribution failure.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request Checklist
npm startand verified thatREADME.mdis up to date.mainbranch for this pull request.Description
AI Credits Breakdown is a canvas extension that shows a person their own GitHub Copilot AI credit usage in detail (1 AI credit = 1 AIU = $0.01).
It is built on the user's computer from the telemetry that the Copilot CLI and the Copilot app already keep in
COPILOT_HOME(default~/.copilot). One page, with a sticky filter bar, and every panel follows every filter:The canvas has one agent action,
get_summary, which returns totals and top entries for a UTC day range so the agent can answer "how much did I spend on Opus this month?". It does not return session ids, prompts or titles.Install:
Type of Contribution
Additional Notes
Privacy and security
owner/name), model ids and automation names are shown.127.0.0.1. The page and its data are behind a random secret path that changes every time the canvas opens, and the server checks theHostandOriginheaders.Content-Security-Policyallows only itself).node:os) andprocess.env(COPILOT_HOME).node:sqlite, the same aschronicle-stats.collector.mjsloads it withprocess.getBuiltinModule("node:sqlite")instead of a static import. The canvas smoke test runs on Node 22, wheremodule.builtinModulesdoes not listnode:-only modules, so it reports a staticimport "node:sqlite"as an undeclared dependency. I reproduced that failure locally with Node 22.23 on the already mergedchronicle-statsas well, so this looks like a pre-existing checker issue rather than something specific to this extension. I did not changeeng/in this pull request.Relation to
chronicle-statschronicle-stats(My AI Usage) covers sessions, turns and token coverage with coaching. This extension is a different view of the same telemetry: money (USD), repositories, prompt-cache behaviour and automations, with cross-filtering across every dimension. From the Chronicle store it reads onlyassistant_usage_eventsand the id, repository and timestamps ofsessions.Testing
npm run plugin:validateandnpm startpass.node eng/canvas-smoke-test.mjs --changed-files <files> --install requirepasses, including the install from an ephemeral local marketplace with the Copilot CLI. (On macOS, setTMPDIRto a real path such as/private/tmp; the materialize step compares a symlinked temp path and fails for existing extensions too.)node --test collector.test.mjs server.test.mjspasses inextensions/ai-credits-breakdownon Node 22 and Node 25 (29 tests). The tests use only fictional telemetry thatsample-data.mjswrites into a temporaryCOPILOT_HOME.get_summary.NaNorundefinedin the page. I also drove it with a script: filters survive the "Skip to content" and "where these numbers come from" links and a refresh, the arrow keys fill the live status, an owner namedconstructorrenders correctly, and per-call averages show decimals. The same script fails on the code before the review fixes.codespell) and line endings are clean.Preview image
The preview image shows fictional sample data from
sample-data.mjs. No real usage, repository names or session ids are in this pull request.By submitting this pull request, I confirm that my contribution abides by the Code of Conduct and will be licensed under the MIT License.