opencode-usage-report 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,334 +4,145 @@
4
4
  [![CI](https://github.com/kaanchinar/opencode-usage-report/actions/workflows/ci.yml/badge.svg)](https://github.com/kaanchinar/opencode-usage-report/actions/workflows/ci.yml)
5
5
  [![License: MIT](https://img.shields.io/npm/l/opencode-usage-report)](./LICENSE)
6
6
 
7
- An [opencode](https://opencode.ai) plugin that shows how much of your inference
8
- subscriptions you have left. It adds a `/usage` command and a `usage_report`
9
- tool covering both the **context window** of the current session and the
10
- **quota windows** (5-hour, weekly, monthly) of your provider plans —
11
- **Kimi Code** (`kimi-code-plan-global` on kimi.ai and `kimi-code-plan-cn` on
12
- kimi.com), **OpenCode Go** (`opencode-go`), **GitHub Copilot**
13
- (`github-copilot`) and **ChatGPT** (`openai`). Results are cached on disk, fall
14
- back to a local estimate when an API is unreachable, and raise background
15
- low-quota toasts in the TUI.
7
+ See how much of your AI subscriptions you have left, right inside [opencode](https://opencode.ai).
8
+
9
+ Adds a `/usage` dialog, a sidebar panel and a `usage_report` tool that show your plan's quota windows and how full the current session's context is.
10
+
11
+ ```text
12
+ Context
13
+ Kimi K2 (High) · 42,318 / 200,000 tokens (21.2%)
14
+ Auto-compacts at 180,000 · 137,682 headroom $0.42 spent
15
+
16
+ Kimi Code (kimi.ai)
17
+ 5h limit █████████░░░░░ 62% resets in 2h 14m
18
+ Weekly limit ████░░░░░░░░░░ 31% resets in 4d 6h
19
+
20
+ GitHub Copilot
21
+ Monthly limit ██░░░░░░░░░░░░ 12% resets in 18d 3h
22
+ ```
16
23
 
17
24
  ## Features
18
25
 
19
- - `/usage` command and `usage_report` tool for on-demand quota reports, with
20
- JSON output and single-provider filtering.
21
- - **Context usage panel** (new in 0.4.0) — the exact prompt-token total against
22
- the model's context window, a cell grid that fills by category, the
23
- auto-compaction point with live headroom, and session cost.
24
- - TUI sidebar panel with live progress bars, `NN%` usage and reset countdowns
25
- for every quota window.
26
- - On-disk caching with a configurable TTL, plus an explicit refresh that
27
- bypasses it.
28
- - Local fallback estimate when a provider API is unreachable and no cache
29
- exists.
30
- - Low-quota warnings on startup and on `session.idle`.
31
- - Privacy-first: API keys are never logged, cached, or rendered.
32
-
33
- ## Install
34
-
35
- The plugin exposes **two entrypoints** and opencode loads them from two
36
- different config files. Install both for the full experience:
37
-
38
- | Config | Entrypoint | Provides |
39
- | --------------- | ------------- | ------------------------------------------------------- |
40
- | `opencode.json` | server plugin | `usage_report` tool, low-quota warnings, prompt capture |
41
- | `tui.json` | TUI plugin | `/usage` command, sidebar panel |
42
-
43
- The quickest path is opencode's own installer, which detects both entrypoints
44
- and writes both configs for you:
26
+ - Quota windows with progress bars and reset countdowns
27
+ - Context breakdown by category, with auto-compaction headroom and session cost
28
+ - `/usage` runs locally and costs no model tokens
29
+ - Low-quota toasts in the TUI
30
+ - Disk cache, with a local estimate when a provider API is unreachable
31
+
32
+ ## Supported providers
33
+
34
+ | Provider | ID | Usage API |
35
+ | -------------------- | ----------------------- | ---------------- |
36
+ | Kimi Code (kimi.ai) | `kimi-code-plan-global` | `api.kimi.ai` |
37
+ | Kimi Code (kimi.com) | `kimi-code-plan-cn` | `api.kimi.com` |
38
+ | OpenCode Go | `opencode-go` | `opencode.ai` |
39
+ | GitHub Copilot | `github-copilot` | `api.github.com` |
40
+ | ChatGPT | `openai` | `chatgpt.com` |
41
+
42
+ ## Installation
43
+
44
+ Requires opencode 1.18.29 or later, including 2.x.
45
45
 
46
46
  ```sh
47
- opencode plugin opencode-usage-report # project scope (./.opencode)
48
- opencode plugin -g opencode-usage-report # global scope
47
+ opencode plugin -g opencode-usage-report
49
48
  ```
50
49
 
51
- Or add the plugin manually to each file and restart opencode:
50
+ Omit `-g` to install for the current project only. Restart opencode afterwards.
51
+
52
+ <details>
53
+ <summary>Manual installation</summary>
54
+
55
+ The plugin has two entrypoints, so add it to both config files:
52
56
 
53
57
  ```jsonc
54
- // opencode.json — server plugin
55
- {
56
- "plugin": ["opencode-usage-report"],
57
- }
58
+ // opencode.json: usage_report tool, low-quota warnings
59
+ { "plugin": ["opencode-usage-report"] }
58
60
  ```
59
61
 
60
62
  ```jsonc
61
- // tui.json — TUI plugin
62
- {
63
- "plugin": ["opencode-usage-report"],
64
- }
63
+ // tui.json: /usage dialog, sidebar panel
64
+ { "plugin": ["opencode-usage-report"] }
65
65
  ```
66
66
 
67
- > **Use the bare package name in both files.** opencode reads the package
68
- > `exports` map itself and resolves `exports["./tui"]` for the TUI and
69
- > `exports["./server"]` (or `main`) for the server. A subpath spec such as
70
- > `opencode-usage-report/tui` is **not** valid and will silently load nothing.
71
- > `opencode plugin opencode-usage-report` prints `Detected server + tui targets`
72
- > when the resolution works.
67
+ Use the bare package name in both files. A subpath such as `opencode-usage-report/tui` silently loads nothing.
73
68
 
74
- > **OpenCode 2.x (0.6.0).** Both entrypoints now default-export a dual
75
- > definition. The server entrypoint exports `{ id, setup, server }` — V2 calls
76
- > `setup`, V1 calls `server`. The TUI entrypoint exports `{ id, setup, tui }` —
77
- > V2 calls `setup` (its `keymap.layer` runs from a headless `app`-slot
78
- > component), V1 calls `tui`. OpenCode 2's TUI loader rejects a default export
79
- > without a `setup` function (`Invalid V2 TUI plugin module`); before 0.6.0 the
80
- > TUI module exported only the V1 `tui` shape and failed to load on 2.x.
81
- > OpenCode 1.18.29+ and 2.x both load either entrypoint.
82
-
83
- Options can be passed in the tuple form, independently per entrypoint:
84
-
85
- ```jsonc
86
- {
87
- "plugin": [["opencode-usage-report", { "thresholdPercent": 75 }]],
88
- }
89
- ```
69
+ </details>
90
70
 
91
- ### Local development
71
+ ## Usage
92
72
 
93
- Point the configs at the source instead of the published package. Config is read
94
- once at startup and is not hot-reloaded, so restart opencode after every edit:
73
+ ### `/usage`
95
74
 
96
- ```jsonc
97
- // opencode.json
98
- { "plugin": ["file:///abs/path/to/opencode-usage-report/src/index.ts"] }
99
- // tui.json
100
- { "plugin": ["file:///abs/path/to/opencode-usage-report/src/tui.tsx"] }
101
- ```
75
+ Opens the report dialog. Providers without a credential are skipped.
102
76
 
103
- ## Commands
77
+ | Key | Action |
78
+ | ----- | ------------------------ |
79
+ | `r` | Refresh now |
80
+ | `tab` | Cycle provider scope |
81
+ | `j` | Toggle the raw JSON view |
82
+ | `esc` | Close |
104
83
 
105
- - `/usage` — open the report dialog: the context panel for the current session
106
- on top, and every configured provider's quota windows below. This is a local
107
- TUI command, so it costs no model tokens. Requires the TUI plugin. Providers
108
- whose credential cannot be resolved are skipped in this view.
109
- - `/usage` dialog keys — `esc` close, `r` refresh now, `tab` cycle provider
110
- scope (all → each provider), `j` toggle the raw JSON view.
111
- - `usage_report` tool — the same report for agents and headless runs. Takes
112
- `provider` (exact id: `kimi-code-plan-global`, `kimi-code-plan-cn`,
113
- `opencode-go`, `github-copilot` or `openai`; an unknown id returns an error
114
- listing the known ids), `json` (raw `ProviderReport[]`) and `refresh` (bypass
115
- the on-disk cache).
84
+ Press `ctrl+shift+u` to refresh the sidebar panel.
116
85
 
117
- > **Changed in 0.4.0.** `/usage` used to be an LLM-mediated prompt template that
118
- > routed through the model, and it accepted `--json` / `--refresh` / a provider
119
- > id as slash arguments. The TUI command now owns the name, so `/usage` reaches
120
- > no model and those capabilities moved to the dialog keys and the
121
- > `usage_report` tool arguments. Running `/usage --json` is no longer supported.
86
+ ### `usage_report` tool
122
87
 
123
- ## Context usage panel
88
+ The same report for agents and headless runs.
124
89
 
125
- The top of the `/usage` dialog reports how full the model's context window is
126
- for the current session:
90
+ | Argument | Type | Description |
91
+ | ---------- | --------- | ---------------------------------- |
92
+ | `provider` | `string` | Report one provider, by exact ID |
93
+ | `json` | `boolean` | Return raw `ProviderReport[]` JSON |
94
+ | `refresh` | `boolean` | Bypass the disk cache |
127
95
 
128
- ```
129
- Context
130
- Kimi K2 (High) · 42,318 / 200,000 tokens (21.2%)
131
-
132
- ████████████████████████████ ████████████████████████████████ ████████████ ░░░░░░░░░░░░░░
133
- ████████████████████████████ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░
134
- ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░
135
- ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░
136
- ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░
137
- ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░
138
-
139
- ● User messages 8,204 4.1% ● System & tools 8,144 4.1%
140
- ● Agent responses 11,650 5.8% ● Free space 157,682 78.8%
141
- ● Reasoning 2,140 1.1%
142
- ● Tool calls 12,180 6.1%
143
-
144
- Auto-compacts at 180,000 · 137,682 headroom $0.42 spent
145
- ```
96
+ ### Context accuracy
146
97
 
147
- **What is exact.** The headline total, the percentage, free space, the
148
- compaction point, the cost and the number of pruned tool outputs all come from
149
- numbers the provider and opencode report. The total is the prompt size —
150
- `input + cache.read + cache.write` — which is what actually occupies the context
151
- window. opencode's own sidebar Context block sums five fields including the
152
- completion, so it reads slightly higher than this panel by design.
153
-
154
- **What is estimated.** The per-category rows. opencode records no token count
155
- per message or part, so each category is estimated with the same `characters / 4`
156
- heuristic opencode itself uses, then normalized so the rows agree with the exact
157
- total.
158
-
159
- **System & tools.** opencode assembles the system prompt and tool definitions on
160
- every request and never persists them. The server plugin measures the real
161
- system string through the `experimental.chat.system.transform` hook and stores
162
- only its size, so the row can be split into `system prompt` and
163
- `tools & framing`. That hook is undocumented, so it is treated as best-effort:
164
- if it stops firing, the sidecar goes stale, or anything fails to validate, the
165
- panel falls back to the derived residual and labels the row `(derived)`. The
166
- plugin never breaks a request over it.
167
-
168
- **Compaction headroom.** `Auto-compacts at N` mirrors opencode's own overflow
169
- check, including the configured `compaction.reserved` buffer. This is the point
170
- at which opencode automatically summarizes the session — a number opencode
171
- exposes nowhere else.
172
-
173
- ## Options
174
-
175
- Shared by both entrypoints:
176
-
177
- | Option | Type | Default | Meaning |
178
- | ------------------ | ------------------ | ------- | ------------------------------------------------------------ |
179
- | `thresholdPercent` | `number` | `80` | Warn when a window is at/above this percent used. |
180
- | `cacheTtlSeconds` | `number` | `120` | How long a cached API result is considered fresh. |
181
- | `providers` | `string[] \| null` | `null` | Providers to report; `null` = every registered adapter. |
182
- | `fallback` | `boolean` | `true` | Use a local estimate when the API fails and no cache exists. |
183
-
184
- TUI only:
185
-
186
- | Option | Type | Default | Meaning |
187
- | ------------------------ | -------- | ------- | -------------------------------------------------- |
188
- | `refreshIntervalSeconds` | `number` | `60` | Sidebar/dialog refresh cadence, clamped to 5–3600. |
189
- | `barWidth` | `number` | `14` | Progress-bar width in cells, clamped to 4–40. |
190
-
191
- `fallback` is fixed at `true` for the TUI entrypoint. Malformed option values
192
- are ignored and the defaults are kept.
193
-
194
- ## Warnings
195
-
196
- On startup and again on `session.idle` (both throttled to at most once every
197
- 10 minutes, sharing one timer), the plugin checks the cached reports. Any window
198
- at or above `thresholdPercent`, or whose status is `rate-limited` / `frozen`,
199
- produces a TUI toast. A warning fires once per window until that window resets;
200
- it re-arms after usage drops below `thresholdPercent - 10`. Toast failures
201
- (headless/server mode) are swallowed.
202
-
203
- ## Data sources & privacy
204
-
205
- - **Credential resolution order.** First `OPENCODE_USAGE_<ID>_KEY` (optionally
206
- paired with `OPENCODE_USAGE_<ID>_ACCOUNT_ID`, e.g.
207
- `OPENCODE_USAGE_OPENAI_ACCOUNT_ID`, to supply the ChatGPT account id), then
208
- `~/.local/share/opencode/auth.json`: a `type: "api"` entry's `.key`, or a
209
- `type: "oauth"` entry's `.access` (falling back to `.refresh` for GitHub
210
- Copilot, which stores its token there). For ChatGPT the oauth entry's
211
- `.accountId` is read automatically. `$OPENCODE_DATA_HOME` overrides the data
212
- directory.
213
- - **APIs.** `https://api.kimi.ai/coding/v1/usages` (global plan),
214
- `https://api.kimi.com/coding/v1/usages` (China plan),
215
- `https://opencode.ai/zen/go/v1/usage` (requires a custom `User-Agent`),
216
- `https://api.github.com/copilot_internal/user` (GitHub Copilot) and
217
- `https://chatgpt.com/backend-api/wham/usage` (ChatGPT).
218
- - **Auth.** Run `opencode auth login` and pick **GitHub Copilot** or **OpenAI
219
- ChatGPT**. ChatGPT additionally needs the account id that login writes to
220
- `auth.json`; the plugin reads it automatically.
221
- - **Cache and state** live in `<data-home>/usage-report/`: the TTL cache, the
222
- session id, the warn state, and `context/<sessionID>.json` — the system-prompt
223
- size sidecar, which holds counts only and never prompt text.
224
- - **Local fallback** reads `opencode.db` read-only.
225
- - **API keys are never logged, cached, or rendered.** Adapter errors are
226
- sanitized (key substrings replaced with `<redacted>`) before they reach any
227
- output, and the live smoke script never prints credentials.
98
+ - **Exact:** the total, percentage, free space, compaction point and cost. The total is the prompt size (`input + cache.read + cache.write`), so it reads slightly lower than opencode's own sidebar, which also counts the completion.
99
+ - **Estimated:** the per-category rows, because opencode records no per-message token counts. They use `characters / 4`, then scale to match the exact total.
100
+ - **`(derived)`:** the system prompt size could not be measured, so the row is the remainder of the total.
228
101
 
229
- ## Troubleshooting
102
+ ## Configuration
230
103
 
231
- **`/usage` does not appear in the command list.**
232
- Both configs are required — the command comes from the TUI plugin, not the
233
- server plugin. Check that `tui.json` lists `"opencode-usage-report"` (bare name)
234
- and restart opencode; config is not hot-reloaded. To confirm the package exposes
235
- what opencode expects, run `opencode plugin opencode-usage-report` — it should
236
- print `Detected server + tui targets`.
104
+ Pass options with the tuple form. Each entrypoint reads its own options, so set them in `opencode.json`, `tui.json`, or both.
237
105
 
238
- **The sidebar is empty and no providers are reported.**
239
- Every window belongs to a provider whose credential could not be resolved, so it
240
- is skipped. Run `opencode auth login`, or set
241
- `OPENCODE_USAGE_<ID>_KEY` for the provider in question.
106
+ ```jsonc
107
+ {
108
+ "plugin": [["opencode-usage-report", { "thresholdPercent": 75 }]],
109
+ }
110
+ ```
242
111
 
243
- **An old version keeps loading after you upgrade.**
244
- opencode caches installed plugin packages under
245
- `~/.cache/opencode/packages/<name>@latest/` and re-resolves versions on install,
246
- not on every start. Re-run `opencode plugin opencode-usage-report`, or delete
247
- that cache directory, to force a refresh. This is the usual cause of "I
248
- published a new version but opencode still runs the old one".
112
+ | Option | Type | Default | Entrypoint | Description |
113
+ | ------------------------ | ------------------ | ------- | ---------- | ----------------------------------------------------------- |
114
+ | `thresholdPercent` | `number` | `80` | both | Warn when a window reaches this percentage |
115
+ | `cacheTtlSeconds` | `number` | `120` | both | How long a cached API result stays fresh |
116
+ | `providers` | `string[] \| null` | `null` | both | Providers to report; `null` means all |
117
+ | `fallback` | `boolean` | `true` | server | Use a local estimate when the API fails and no cache exists |
118
+ | `refreshIntervalSeconds` | `number` | `60` | TUI | Refresh cadence, clamped to 5–3600 |
119
+ | `barWidth` | `number` | `14` | TUI | Progress bar width in cells, clamped to 4–40 |
249
120
 
250
- **Quota numbers look stale or marked `(stale)` / `(est)`.**
251
- `(stale)` means a cached result was served past its TTL without a successful
252
- refresh; `(est)` means the API was unreachable and a local estimate was used.
253
- Press `r` in the dialog (or `ctrl+shift+u` in the sidebar) to force a refetch.
121
+ Invalid values are ignored and the default is kept.
254
122
 
255
- ## Extending
123
+ ## Credentials and privacy
256
124
 
257
- New providers are trivial:
125
+ Run `opencode auth login` to sign in. The plugin reads credentials in this order:
258
126
 
259
- 1. Implement `ProviderAdapter` (`src/types.ts`) — an `id`, `displayName`, and
260
- `fetch(cred, opts)` returning `{ windows, extras? }`.
261
- 2. Register it in `src/providers/index.ts` (`adapters` array).
262
- 3. Add fixture-driven tests under `test/`.
127
+ 1. `OPENCODE_USAGE_<ID>_KEY`, where `<ID>` is the provider ID in uppercase with non-alphanumeric characters replaced by `_`, for example `OPENCODE_USAGE_KIMI_CODE_PLAN_GLOBAL_KEY`. For ChatGPT, also set `OPENCODE_USAGE_OPENAI_ACCOUNT_ID`.
128
+ 2. `~/.local/share/opencode/auth.json`. Set `$OPENCODE_DATA_HOME` to use another data directory.
263
129
 
264
- Reference endpoints for future adapters (not built in v1):
130
+ Keys are never logged, cached or rendered, and adapter errors are redacted before output. The plugin contacts only the usage APIs listed above. Cached reports and plugin state live in `<data-home>/usage-report/`, and the local estimate reads `opencode.db` read-only. The plugin stores the size of your system prompt, never its text.
265
131
 
266
- - **Gemini**: `cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota`
267
- (requires Google OAuth).
268
- - **Claude**: `api.anthropic.com/api/oauth/usage` (requires
269
- `anthropic-beta: oauth-2025-04-20`, the claude-code User-Agent, and
270
- `~/.claude/.credentials.json`).
132
+ ## Troubleshooting
271
133
 
272
- ## Development
134
+ **`/usage` is missing.** The command comes from the TUI entrypoint. Check that `tui.json` lists the bare package name, then restart opencode. Running `opencode plugin opencode-usage-report` should print `Detected server + tui targets`.
273
135
 
274
- ```sh
275
- npm run lint # oxlint
276
- npm run lint:fix # oxlint --fix
277
- npm run format # oxfmt (write in place)
278
- npm run format:check # oxfmt --check
279
- npm test # vitest, no network
280
- npm run typecheck # tsc --noEmit
281
- npm run check # lint + format:check + typecheck + test
282
- npm run smoke -- --yes-live # manual live check (real keys; opt-in)
283
- ```
136
+ **No providers are reported.** No credential could be resolved. Run `opencode auth login` or set `OPENCODE_USAGE_<ID>_KEY`.
284
137
 
285
- ## Releasing
138
+ **An old version keeps loading.** opencode caches packages in `~/.cache/opencode/packages/<name>@latest/`. Re-run `opencode plugin opencode-usage-report` or delete that directory.
286
139
 
287
- Releases are **staged**, not published directly. Pushing a `v*` tag triggers
288
- `.github/workflows/publish.yml`, which verifies that the tag matches
289
- `package.json`, runs `npm ci`, and uploads the tarball with
290
- `npm stage publish --provenance --access public` using npm Trusted Publishing
291
- (OIDC — no `NPM_TOKEN` secret). Nothing goes live until a maintainer approves
292
- the staged release with 2FA:
140
+ **Numbers show `(stale)` or `(est)`.** `(stale)` is a cached result past its TTL. `(est)` is a local estimate used because the API was unreachable. Press `r` in the dialog to refetch.
293
141
 
294
- ```sh
295
- git tag v0.4.1 && git push origin main v0.4.1
296
- npm stage list # find the stage id
297
- npm stage approve <stage-id> # 2FA prompt; or approve in the npm UI
298
- ```
142
+ ## Contributing
299
143
 
300
- `prepublishOnly` runs lint, format check, typecheck and tests before the upload,
301
- so a failing check blocks the stage.
302
-
303
- ## Changelog
304
-
305
- - **0.6.3** — Kimi Code CN (`kimi-code-plan-cn`): the 5-hour window falls back
306
- to the `usages.limit_5h` ratio when the `limits[]` row carries no counts,
307
- instead of rendering an empty `—` bar; a monthly window is also read from a
308
- counted 30-day `limits[]` entry when the endpoint reports it there. Includes
309
- the outstanding Dependabot dev-dependency bumps.
310
- - **0.6.2** — OpenCode 2 context panel fixes: the V2 adapter now reads the
311
- model id from the store's `model.id`, so the model lookup succeeds and the
312
- cell grid, per-category percentages and compaction-headroom line render
313
- again instead of `—`; the translated messages also carry the session id, and
314
- the system-prompt capture anchors its freshness check to the turn's user
315
- message — OpenCode 2 fires the capture hook just before the assistant
316
- message is stamped, which previously marked every capture `(derived)`.
317
- - **0.6.1** — Kimi Code adapter tracks the 2026-09 quota-model API change:
318
- usage windows now come from the `usages.limit_5h` / `limit_7d` /
319
- `limit_month_total` ratios (with a `limit_month_code` breakdown), and Extra
320
- Usage is read from the camelCase `boosterWallet` (fixed-point `BOOSTER`
321
- balance). The pre-2026-09 `usage` / `limits[]` / `totalQuota` /
322
- `booster_wallet` shapes remain as fallbacks.
323
- - **0.4.1** — internal: shared provider HTTP plumbing (`src/providers/http.ts`)
324
- replaces the copy-pasted fetch/retry/error code; context estimates simplified;
325
- reset times render as `Wed, Sep 16`.
326
- - **0.4.0** — context usage panel (`/usage` now shows context breakdown, grid,
327
- compaction headroom and cost); `/usage` became a local TUI command that costs
328
- no model tokens; sidebar rendering extracted to `src/quota-lines.ts`.
329
- - **0.3.0** — GitHub Copilot and ChatGPT (OpenAI) adapters.
330
- - **0.2.0** — Kimi Code split into the kimi.ai (`kimi-code-plan-global`) and
331
- kimi.com (`kimi-code-plan-cn`) plans.
144
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and adding a provider, and [CHANGELOG.md](./CHANGELOG.md) for release history. Report vulnerabilities as described in [SECURITY.md](./SECURITY.md).
332
145
 
333
146
  ## License
334
147
 
335
- MIT © [Kaan Chinar](https://github.com/kaanchinar)
336
-
337
- Security policy: [SECURITY.md](./SECURITY.md)
148
+ [MIT](./LICENSE) © [Kaan Chinar](https://github.com/kaanchinar)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-usage-report",
3
- "version": "0.6.3",
4
- "description": "opencode plugin: /usage command showing subscription quota windows for configured providers",
3
+ "version": "0.6.4",
4
+ "description": "opencode plugin: /usage dialog, sidebar panel and usage_report tool for subscription quota and context usage",
5
5
  "keywords": [
6
6
  "kimi",
7
7
  "opencode",
@@ -66,6 +66,8 @@
66
66
  },
67
67
  "overrides": {
68
68
  "seroval": "1.6.9",
69
- "seroval-plugins": "1.6.9"
69
+ "seroval-plugins": "1.6.9",
70
+ "toml": "5.0.1",
71
+ "zod": "4.6.7"
70
72
  }
71
73
  }
@@ -1,7 +1,7 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
3
3
  import type { AssistantMessage, Model } from "@opencode-ai/sdk/v2";
4
- import { For, Show, createMemo } from "solid-js";
4
+ import { For, Show, createMemo, onCleanup } from "solid-js";
5
5
  import { buildContextLines, type ContextTone } from "./format";
6
6
  import { collectContext } from "./collect";
7
7
  import { captureIsFresh, captureSince } from "./system";
@@ -24,6 +24,10 @@ export interface UsageDialogProps {
24
24
  system: () => SystemCapture | null;
25
25
  scope: () => number;
26
26
  showJson: () => boolean;
27
+ /** Fires on any panel change the dialog must re-read. */
28
+ subscribe: (listener: () => void) => () => void;
29
+ /** Re-renders the whole dialog; OpenCode loads this module on a separate Solid runtime. */
30
+ rerender: () => void;
27
31
  barWidth: number;
28
32
  }
29
33
 
@@ -73,6 +77,11 @@ function jsonLines(value: unknown): RenderedLine[] {
73
77
  export function UsageDialog(props: UsageDialogProps) {
74
78
  const theme = () => props.api.theme.current;
75
79
 
80
+ // OpenCode loads this module against a different Solid runtime than the
81
+ // plugin entry, so signals never track across the two. Ask the host to
82
+ // re-render the dialog instead.
83
+ onCleanup(props.subscribe(props.rerender));
84
+
76
85
  const messages = createMemo(() =>
77
86
  props.sessionID === null ? [] : props.api.state.session.messages(props.sessionID),
78
87
  );
package/src/tui.tsx CHANGED
@@ -30,10 +30,33 @@ function createUsagePanel(getSessionID: () => string | null, options: ResolvedTu
30
30
  const [error, setError] = createSignal<string | null>(null);
31
31
  const [tick, setTick] = createSignal(Date.now());
32
32
  const [capture, setCapture] = createSignal<SystemCapture | null>(null);
33
- const [scope, setScope] = createSignal(0);
34
- const [showJson, setShowJson] = createSignal(false);
33
+ const [scope, setScopeSignal] = createSignal(0);
34
+ const [showJson, setShowJsonSignal] = createSignal(false);
35
35
  let disposed = false;
36
36
 
37
+ // The dialog lives in another module, which OpenCode can load against a
38
+ // second Solid runtime; its memos then never track these signals. Plain
39
+ // subscribers let it bump a signal it owns instead.
40
+ const listeners = new Set<() => void>();
41
+ const notify = (): void => {
42
+ // Snapshot: a listener may re-render the dialog, which resubscribes.
43
+ for (const listener of Array.from(listeners)) listener();
44
+ };
45
+ const subscribe = (listener: () => void): (() => void) => {
46
+ listeners.add(listener);
47
+ return () => {
48
+ listeners.delete(listener);
49
+ };
50
+ };
51
+ const setScope = (value: number | ((n: number) => number)): void => {
52
+ setScopeSignal(value);
53
+ notify();
54
+ };
55
+ const setShowJson = (value: boolean | ((v: boolean) => boolean)): void => {
56
+ setShowJsonSignal(value);
57
+ notify();
58
+ };
59
+
37
60
  const load = async (refresh = false): Promise<void> => {
38
61
  if (disposed) return;
39
62
  setInFlight((n) => n + 1);
@@ -55,6 +78,7 @@ function createUsagePanel(getSessionID: () => string | null, options: ResolvedTu
55
78
  setError(err instanceof Error ? err.message : String(err));
56
79
  } finally {
57
80
  setInFlight((n) => Math.max(0, n - 1));
81
+ notify();
58
82
  }
59
83
  };
60
84
 
@@ -63,13 +87,16 @@ function createUsagePanel(getSessionID: () => string | null, options: ResolvedTu
63
87
  const sessionID = getSessionID();
64
88
  if (sessionID === null) {
65
89
  setCapture(null);
90
+ notify();
66
91
  return;
67
92
  }
68
93
  setCapture(await readCapture(sessionID));
94
+ notify();
69
95
  };
70
96
 
71
97
  const refresh = (): void => {
72
98
  setTick(Date.now());
99
+ notify();
73
100
  void load();
74
101
  void refreshCapture();
75
102
  };
@@ -88,6 +115,7 @@ function createUsagePanel(getSessionID: () => string | null, options: ResolvedTu
88
115
  showJson,
89
116
  setScope,
90
117
  setShowJson,
118
+ subscribe,
91
119
  load,
92
120
  refreshCapture,
93
121
  refresh,
@@ -96,6 +124,7 @@ function createUsagePanel(getSessionID: () => string | null, options: ResolvedTu
96
124
  dispose: (): void => {
97
125
  disposed = true;
98
126
  clearInterval(timer);
127
+ listeners.clear();
99
128
  },
100
129
  };
101
130
  }
@@ -153,28 +182,37 @@ export const tui: TuiPlugin = async (api, rawOptions) => {
153
182
  api.lifecycle.onDispose(panel.dispose);
154
183
  api.lifecycle.onDispose(api.event.on("session.idle", panel.refresh));
155
184
 
185
+ // V1's replace re-invokes this, so the dialog reads the current panel state.
186
+ const showDialog = (sessionID: string | null): void => {
187
+ api.ui.dialog.replace(() => (
188
+ <UsageDialog
189
+ api={api}
190
+ sessionID={sessionID}
191
+ reports={panel.reports}
192
+ busy={panel.busy}
193
+ error={panel.error}
194
+ now={() => new Date(panel.tick())}
195
+ system={panel.capture}
196
+ scope={panel.scope}
197
+ showJson={panel.showJson}
198
+ subscribe={panel.subscribe}
199
+ rerender={() => {
200
+ if (!panel.isDisposed()) showDialog(sessionID);
201
+ }}
202
+ barWidth={options.barWidth}
203
+ />
204
+ ));
205
+ // Must follow replace(): the stack resets the size to "medium".
206
+ api.ui.dialog.setSize("xlarge");
207
+ };
208
+
156
209
  const openDialog = (): void => {
157
210
  const sessionID = activeSessionID(api);
158
211
  void panel.refreshCapture().finally(() => {
159
212
  if (panel.isDisposed()) return;
160
213
  panel.setScope(0);
161
214
  panel.setShowJson(false);
162
- api.ui.dialog.replace(() => (
163
- <UsageDialog
164
- api={api}
165
- sessionID={sessionID}
166
- reports={panel.reports}
167
- busy={panel.busy}
168
- error={panel.error}
169
- now={() => new Date(panel.tick())}
170
- system={panel.capture}
171
- scope={panel.scope}
172
- showJson={panel.showJson}
173
- barWidth={options.barWidth}
174
- />
175
- ));
176
- // Must follow replace(): the stack resets the size to "medium".
177
- api.ui.dialog.setSize("xlarge");
215
+ showDialog(sessionID);
178
216
  });
179
217
  };
180
218
 
@@ -316,27 +354,35 @@ function setup(ctx: V2Context): () => void {
316
354
 
317
355
  const offIdle = ctx.data.on("session.idle", () => panel.refresh());
318
356
 
357
+ const showDialog = (sessionID: string | null): void => {
358
+ ctx.ui.dialog.show(() => (
359
+ <UsageDialog
360
+ api={api}
361
+ sessionID={sessionID}
362
+ reports={panel.reports}
363
+ busy={panel.busy}
364
+ error={panel.error}
365
+ now={() => new Date(panel.tick())}
366
+ system={panel.capture}
367
+ scope={panel.scope}
368
+ showJson={panel.showJson}
369
+ subscribe={panel.subscribe}
370
+ rerender={() => {
371
+ if (!panel.isDisposed()) showDialog(sessionID);
372
+ }}
373
+ barWidth={options.barWidth}
374
+ />
375
+ ));
376
+ ctx.ui.dialog.set({ size: "xlarge" });
377
+ };
378
+
319
379
  const openDialog = (): void => {
320
380
  const sessionID = activeSessionID(api);
321
381
  void panel.refreshCapture().finally(() => {
322
382
  if (panel.isDisposed()) return;
323
383
  panel.setScope(0);
324
384
  panel.setShowJson(false);
325
- ctx.ui.dialog.show(() => (
326
- <UsageDialog
327
- api={api}
328
- sessionID={sessionID}
329
- reports={panel.reports}
330
- busy={panel.busy}
331
- error={panel.error}
332
- now={() => new Date(panel.tick())}
333
- system={panel.capture}
334
- scope={panel.scope}
335
- showJson={panel.showJson}
336
- barWidth={options.barWidth}
337
- />
338
- ));
339
- ctx.ui.dialog.set({ size: "xlarge" });
385
+ showDialog(sessionID);
340
386
  });
341
387
  };
342
388