Skip to content

feat(copilot): plan-usage chip row for the monthly premium-request quota - #662

Closed
opticon454 wants to merge 6 commits into
Ark0N:masterfrom
opticon454:feat/copilot-plan-usage
Closed

opticon454 wants to merge 6 commits into
Ark0N:masterfrom
opticon454:feat/copilot-plan-usage

Conversation

@opticon454

Copy link
Copy Markdown
Contributor

What

A Copilot row in the header plan-usage chip: the calendar-month premium-request quota, with the request counts and the reset in the tooltip. #592 landed the Copilot run mode without it, so this is the split-off follow-up.

Why

The Copilot CLI has no scriptable usage command (copilot billing exists only inside the TUI). The quota the editors show is GET https://api.github.com/copilot_internal/user, readable with the CLI's own sign-in token.

How

  • src/utils/copilot-plan-usage.ts reads the quota. The host polls it every 10 minutes, read-only like the Codex poll, and only when the CLI is installed, a token resolves (COPILOT_GITHUB_TOKEN, GH_TOKEN, GITHUB_TOKEN, then authTokens in <COPILOT_HOME or ~/.copilot>/config.json) and plan-usage display is on. Turning the chip off also stops the request.
  • The endpoint is undocumented, so every failure (offline, 401, a changed shape, an unlimited quota) yields no row.
  • The token is never logged and is sent only to api.github.com, with redirects refused.
  • This is the one request Codeman makes on its own, so FAQ, Installation and Security now state that exception, when it happens and how to stop it.

Tests

test/copilot-plan-usage.test.ts (parsing, token resolution order, each failure mode, no redirect) and test/plan-usage-chip.test.ts (the row renders and is omitted when absent). Changeset included.

@Ark0N

Ark0N commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Thanks @opticon454! This adds the Copilot row to the header plan-usage chip, reading the monthly premium-request quota from GitHub with the CLI's own token, and closes the gap left after #592. The code is careful: read-only, silent on every failure, redirects refused, admin-only on the wire, and the token never lands in state or logs. CI is green and the row renders correctly in all three header styles.

A few things to fix before merge:

  1. Change detection misses a quota-limit change (src/usage-telemetry.ts:347). The monthly signature keys only on used and resetAt, so a plan change mid-month (300 to 1500 with 100 used) produces the same signature and the chip keeps the old percentage until the next premium request. Please push Math.round(t.monthly.usedPercentage), t.monthly.used ?? null, t.monthly.limit ?? null and t.monthly.resetAt, and add a test with two payloads that differ only in entitlement.

  2. Test for the new merge branch (src/web/plan-usage-latest.ts:18). test/plan-usage-latest.test.ts only covers Codex. Please add the same case for Copilot: a later Claude statusline sample must keep the copilot key.

  3. Docs wording (docs/wiki/Security.md:104-112, docs/wiki/FAQ.md:33, docs/wiki/Installation.md:16, .changeset/copilot-plan-usage.md:5). Having the request on by default is fine with me, the same as the Codex usage poll, so no setting change is needed; the docs just have to say so. The request is on by default (an absent showPlanUsageLimits reads as ON, including for a phone-only user whose chip is off), so "optional" reads wrong. "The one request Codeman makes on its own" also overlooks the Codex usage poll, web push and webhooks. Please say it plainly, for example: "On by default while Copilot CLI is installed and signed in: every 10 minutes the server asks api.github.com for your premium-request quota with Copilot's own token. Turn off Plan Usage Limits in App Settings to stop it." Also adjust the sentence at Security.md:104, which still says the only traffic is browser to server.

  4. Wiki pages the new text links to (docs/wiki/Agent-CLIs.md:75 and its Copilot section at line 277, docs/wiki/Keeping-Agents-Running.md:114, docs/wiki/The-Dashboard.md:151). FAQ and Installation send readers to Agent CLIs, which says nothing about the quota read, and these pages still call the chip Claude-only. A short paragraph in the Copilot section plus the table row and the two chip descriptions would cover it.

Smaller items, take them or leave them:

  • src/web/server.ts:2925: check readPlanUsageTelemetryEnabled() before isCopilotAvailable() (the resolver can run a synchronous login-shell probe), and read config.json with readFile from node:fs/promises in src/utils/copilot-plan-usage.ts:52.
  • src/usage-telemetry.ts:229: return null when entitlement is not greater than 0, so a non-unlimited zero quota cannot show a stuck 0% or 100% row.
  • src/web/public/index.html:200: the chip's default title still says "Claude and Codex plan usage limits".

Once 1 to 4 are in, this is ready to merge. I will add the matching line to CLAUDE.md and docs/architecture-invariants.md myself.

@opticon454
opticon454 force-pushed the feat/copilot-plan-usage branch from aa9e5ce to 89a6aec Compare October 20, 2026 03:32
@opticon454

Copy link
Copy Markdown
Contributor Author

Thanks @Ark0N, all four items and the smaller ones are in the new commit (rebased on current master first).

  1. telemetrySignature now pushes the rounded percentage, used, limit and resetAt for the monthly window. New test: two payloads with 100 requests used and the same reported percentage that differ only in entitlement (300 and 1500) get different signatures.
  2. test/plan-usage-latest.test.ts has the Copilot case: a later Claude status-line sample keeps the copilot key.
  3. Wording: the Security privacy section, FAQ, Installation and the changeset now say the quota read is on by default while Copilot CLI is installed and signed in (an absent setting reads as on, even for a phone-only user), every 10 minutes to api.github.com, and that turning Plan Usage Limits off in App Settings stops it. Security no longer says the only traffic is browser to server; it names the Codex poll, web push and webhooks.
  4. Agent-CLIs.md has a short paragraph in the Copilot section plus the table row; Keeping-Agents-Running.md and The-Dashboard.md no longer call the chip Claude-only.

Smaller items, all taken: readPlanUsageTelemetryEnabled() is checked before isCopilotAvailable(); resolveCopilotToken is async and reads config.json with readFile from node:fs/promises (its tests await it); parseCopilotQuotaResponse returns null for an entitlement of 0 or less that is not unlimited; the chip's default title names Copilot too.

Typecheck, lint and the Copilot, usage-telemetry and plan-usage-latest tests pass locally.

Ark0N pushed a commit that referenced this pull request Oct 20, 2026
`node_modules/` with a trailing slash matches directories only, so a
worktree that links node_modules to a shared install shows the link as
untracked and a plain `git add` commits it. Two PR heads did (#658,
#662), which also broke checking them out next to a real install.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Thanks @opticon454 for turning this round so quickly. One thing in the new commit (89a6aec0) has to come out: it adds node_modules to the repository, as a symlink to ../cfg/node_modules (I guess from a worktree that shares one install). Please drop it with git rm --cached node_modules, commit and push.

It got in because master's .gitignore had node_modules/, and the trailing slash only matches a directory, so a symlink with that name showed up as an untracked file. Master ignores it now (3907728), so after your next rebase it cannot slip in again.

The symlink also stopped my review checkout of that head, which is why the rework has not had its review yet. It runs once the new head is up.

The Copilot CLI has no scriptable usage command (`copilot billing` exists only inside the TUI), but
the quota the editors show is `GET https://api.github.com/copilot_internal/user`, readable with the
CLI's own sign-in token. The header chip gets a `Copilot` row with one `mo` window (the
calendar-month premium-request quota) beside Claude's and Codex's, with request counts and the reset
in the tooltip.

The host polls it every 10 minutes, read-only like the Codex poll, and only when the CLI is
installed, a token resolves (COPILOT_GITHUB_TOKEN, GH_TOKEN, GITHUB_TOKEN, then authTokens in
<COPILOT_HOME or ~/.copilot>/config.json) and plan-usage display is on, so turning the chip off also
stops the request. The endpoint is undocumented, so every failure (offline, 401, a changed shape, an
unlimited quota) yields no row. The token is never logged and is sent only to api.github.com with
redirects refused.
FAQ, Installation and Security said the only network traffic is browser to server. The plan-usage
Copilot row asks api.github.com for the user's own quota, so each now states that one optional
exception, when it happens (CLI installed and signed in, chip on) and how to stop it.
@opticon454
opticon454 force-pushed the feat/copilot-plan-usage branch from 89a6aec to 7cfd631 Compare October 20, 2026 09:00
@opticon454

Copy link
Copy Markdown
Contributor Author

Thanks @Ark0N, and sorry about that: I share one install between my worktrees through a symlink and git add -A picked it up. The symlink is dropped in a new commit (git rm --cached node_modules) and the branch is rebased onto current master, so your .gitignore fix now covers it. The new head is up for the review.

@Ark0N

Ark0N commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Thanks @opticon454 for the quick turnaround on both rounds. This adds the Copilot row to the header plan-usage chip, reading the monthly premium-request quota from GitHub with Copilot's own token, and every item from the last review is in: the signature carries the limit with its test, the Copilot merge case is tested, the docs say the read is on by default and how to stop it, the chip pages no longer call it Claude-only, and the symlink is gone.

One thing came in with the switch to fs/promises:

  1. src/utils/copilot-plan-usage.ts:77: resolveCopilotToken() is async now, but the default branch does not await it, so readCopilotPlanUsage() called without a token sends Authorization: Bearer [object Promise] (I reproduced it with a fake GH_TOKEN and a recording fetch). The server passes { token } itself, so the poll is unaffected, but the function is exported from the utils barrel and its doc advertises the default. The fix is await resolveCopilotToken(), plus a test that omits token and checks the real token reaches the Authorization header (the current "no token" test passes token: null, so that branch has no coverage).

Two small things:

  • docs/wiki/Agent-CLIs.md:283: the token is read from the three env vars or the plain-text ~/.copilot/config.json, so a sign-in kept in the OS keychain (a macOS or keyring Linux desktop) shows no row. One sentence in the Copilot paragraph saying so would save a few questions.
  • src/web/public/app.js:3560: the comment above _onSessionStatusTelemetry still says "Claude + Codex".

I will apply these three at merge time together with the CLAUDE.md and architecture-invariants lines, so nothing more is needed from you unless you would like to push them yourself first. Thanks again for seeing this through.

@opticon454

Copy link
Copy Markdown
Contributor Author

Thanks @Ark0N, good catch. All three are in the new commit.

  1. readCopilotPlanUsage now does await resolveCopilotToken() when no token is passed. New test: with COPILOT_GITHUB_TOKEN stubbed and no token option, the recording fetch sees Authorization: Bearer env-token.
  2. Agent-CLIs.md says the token comes from the three env vars or the plain-text ~/.copilot/config.json, so a sign-in kept in the OS keychain shows no row.
  3. The _onSessionStatusTelemetry comment in app.js says Claude, Codex and Copilot.

Typecheck and the Copilot tests pass locally. I'll leave the CLAUDE.md and architecture-invariants lines to you, as offered.

@Ark0N

Ark0N commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Thanks @opticon454 for the third round. This adds a Copilot row to the header plan-usage chip with the monthly premium-request quota, read from GitHub with Copilot's own sign-in token. Everything from the last review is in: readCopilotPlanUsage() awaits the token and the default branch has its test, Agent-CLIs has the keychain sentence, and the app.js comment names Copilot. CI is green.

I will merge this and apply the following on top at merge time, so nothing more is needed from you:

  1. CLAUDE.md and docs/architecture-invariants.md (plan-usage chip): the Copilot source and its rules, as promised.
  2. README.md:482 and the "What Codeman wires up" table in docs/copilot-integration.md: a line for the quota read.
  3. src/web/server.ts:2926: resolve the token before isCopilotAvailable(), so a host with no Copilot sign-in never runs the login-shell probe on each 10-minute tick, and move the three gates into a small helper with a test that Plan Usage Limits off means no request.
  4. src/utils/copilot-plan-usage.ts:86: drop X-GitHub-Api-Version, which the Copilot CLI itself does not send on this route.

Thanks again for seeing this through three rounds.

Ark0N pushed a commit that referenced this pull request Oct 22, 2026
…ota (#662)

Squash of PR #662. The header plan-usage chip gains a Copilot row: the
calendar-month premium-request quota, with the request counts and the
reset in the tooltip, read every 10 minutes from GitHub's
copilot_internal/user endpoint with the Copilot CLI's own sign-in token.
It rides showPlanUsageLimits and is on by default while the CLI is
installed and signed in; any failure leaves the chip without the row.

Merge-time fixes by the maintainer: the gates (setting, then token, then
the CLI probe) live in readCopilotPlanUsageIfEnabled() so a host with no
Copilot sign-in never runs the login-shell probe each tick, with a test
that Plan Usage Limits off means no request; the X-GitHub-Api-Version
header the Copilot CLI itself does not send is dropped; and CLAUDE.md,
architecture-invariants, README and copilot-integration describe the read.
The hooks-config import in server.ts was merged with #666's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 22, 2026
…h a configurable folder and retention

Conflicts in CLAUDE.md, README.md, docs/api-reference.md and
src/web/server.ts were additions beside #662's Copilot quota read and
#666's keep-warm sections; both sides kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Merged and shipped in 1.44.0, squashed into one commit (34cf798) so the node_modules symlink commit stays out of master's history. GitHub therefore shows this PR as closed rather than merged; everything is in. Thank you @opticon454 for the Copilot quota row and for three quick rounds.

What I applied at merge, as promised:

  1. CLAUDE.md and docs/architecture-invariants.md describe the Copilot source and its rules: the endpoint, that only premium_interactions counts, the token chain, the 10-minute poll, and that it rides showPlanUsageLimits and is on by default.
  2. A line in README.md and a "Plan usage" row in docs/copilot-integration.md.
  3. The gates now live in readCopilotPlanUsageIfEnabled(): the setting first, then the token, then the CLI probe. A host with no Copilot sign-in no longer runs the login-shell probe every 10 minutes. Tests include one that Plan Usage Limits off means no request and no token or CLI lookup.
  4. The X-GitHub-Api-Version header is dropped, since the Copilot CLI does not send it on this route.

@Ark0N Ark0N closed this Oct 22, 2026
@opticon454
opticon454 deleted the feat/copilot-plan-usage branch October 22, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants