opencode-copilot-budget 1.0.0
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/LICENSE +21 -0
- package/README.md +141 -0
- package/package.json +35 -0
- package/src/index.tsx +296 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 opencode-copilot-budget contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# opencode-copilot-budget
|
|
2
|
+
|
|
3
|
+
Shows your GitHub Copilot premium request budget in the [OpenCode](https://opencode.ai) TUI sidebar. Only visible when the active provider is `github-copilot`.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Progress bar that turns **red** when you reach 90 % of your budget
|
|
10
|
+
- Request count and percentage used
|
|
11
|
+
- Reset date, updated automatically after every AI response
|
|
12
|
+
- 5-minute cache to avoid unnecessary API calls
|
|
13
|
+
- Works with paid and free Copilot plans
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- [OpenCode](https://opencode.ai) with `github-copilot` as your active provider
|
|
18
|
+
- A GitHub token via one of:
|
|
19
|
+
- `GITHUB_TOKEN` or `GH_TOKEN` environment variable
|
|
20
|
+
- [GitHub CLI](https://cli.github.com) (`gh auth login`)
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
### Option A — OpenCode plugin manager
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
opencode plugin opencode-copilot-budget
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This installs the plugin and adds it to your `~/.config/opencode/tui.json` automatically.
|
|
31
|
+
|
|
32
|
+
### Option B — manual
|
|
33
|
+
|
|
34
|
+
Add to `~/.config/opencode/tui.json` (create the file if it doesn't exist):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
39
|
+
"plugin": ["opencode-copilot-budget"]
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Option C — local path (no npm)
|
|
44
|
+
|
|
45
|
+
Point OpenCode directly at the source file by absolute path:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
50
|
+
"plugin": ["/absolute/path/to/opencode-copilot-budget"]
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Token setup
|
|
55
|
+
|
|
56
|
+
The plugin discovers your GitHub token in this order:
|
|
57
|
+
|
|
58
|
+
1. `GITHUB_TOKEN` environment variable
|
|
59
|
+
2. `GH_TOKEN` environment variable
|
|
60
|
+
3. Output of `gh auth token` (GitHub CLI)
|
|
61
|
+
|
|
62
|
+
If none of the above are available, add the token to your shell profile:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# ~/.zshrc or ~/.bashrc
|
|
66
|
+
export GITHUB_TOKEN="ghp_your_token_here"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
To pull it from the GitHub CLI:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
export GITHUB_TOKEN=$(gh auth token)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## What it shows
|
|
76
|
+
|
|
77
|
+
| Situation | Display |
|
|
78
|
+
|---|---|
|
|
79
|
+
| Capped plan | progress bar + `117 / 1000 Premium Requests` |
|
|
80
|
+
| ≥ 90 % used | progress bar turns red |
|
|
81
|
+
| Unlimited plan | `62 used (unlimited)` |
|
|
82
|
+
| Overage consumed | `+5 overage` (shown below usage) |
|
|
83
|
+
| Reset date known | `Resets on 1 May` (date in bold) |
|
|
84
|
+
| Token missing / network error | `sync unavailable` |
|
|
85
|
+
| First load | `syncing...` |
|
|
86
|
+
|
|
87
|
+
## Uninstall
|
|
88
|
+
|
|
89
|
+
Remove `opencode-copilot-budget` from the `plugin` array in `~/.config/opencode/tui.json`.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Contributing
|
|
94
|
+
|
|
95
|
+
All logic lives in a single file — `src/index.tsx` — so it's easy to get started.
|
|
96
|
+
|
|
97
|
+
### Local development setup
|
|
98
|
+
|
|
99
|
+
1. Clone the repo:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone https://github.com/bhaskarmelkani/opencode-copilot-budget
|
|
103
|
+
cd opencode-copilot-budget
|
|
104
|
+
npm install
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
2. Point OpenCode at your local clone via an absolute path in `~/.config/opencode/tui.json`:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
112
|
+
"plugin": ["/absolute/path/to/opencode-copilot-budget"]
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
3. Edit `src/index.tsx` and restart OpenCode to see changes.
|
|
117
|
+
|
|
118
|
+
### Codebase overview
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
src/index.tsx — entire plugin: token discovery, API fetch, caching, and UI
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
| Area | Function / component |
|
|
125
|
+
|---|---|
|
|
126
|
+
| Token discovery | `discoverToken()` |
|
|
127
|
+
| API response parsing (paid + free tiers) | `parseResponse()` |
|
|
128
|
+
| Fetch with 5-minute cache | `fetchCopilotUsage()` |
|
|
129
|
+
| Progress bar | `ProgressBar` |
|
|
130
|
+
| Usage display (SolidJS) | `UsageDetail`, `View` |
|
|
131
|
+
| Plugin registration | bottom of file |
|
|
132
|
+
|
|
133
|
+
### Submitting changes
|
|
134
|
+
|
|
135
|
+
- Open an issue first for non-trivial changes so we can align on direction
|
|
136
|
+
- Keep PRs focused — one concern per PR
|
|
137
|
+
- If you add a new display state, update the "What it shows" table above
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT — see [LICENSE](LICENSE).
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-copilot-budget",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "GitHub Copilot premium budget in the OpenCode TUI sidebar",
|
|
5
|
+
"author": "Bhaskar Melkani",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/bhaskarmelkani/opencode-copilot-budget.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/bhaskarmelkani/opencode-copilot-budget#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/bhaskarmelkani/opencode-copilot-budget/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
"./tui": "./src/index.tsx"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src/",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@opencode-ai/plugin": "*",
|
|
25
|
+
"@opentui/solid": "*",
|
|
26
|
+
"solid-js": "*"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"opencode",
|
|
30
|
+
"opencode-plugin",
|
|
31
|
+
"opencode-tui-plugin",
|
|
32
|
+
"github-copilot"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
/**
|
|
3
|
+
* opencode-copilot-budget
|
|
4
|
+
*
|
|
5
|
+
* Displays your GitHub Copilot premium request budget in the OpenCode TUI
|
|
6
|
+
* sidebar. Automatically refreshes after each AI response. Only visible when
|
|
7
|
+
* the active provider is `github-copilot`.
|
|
8
|
+
*
|
|
9
|
+
* Display format:
|
|
10
|
+
* Copilot Budget
|
|
11
|
+
* ████████░░░░░░░░ 12% Used ← green; turns red at ≥ 90 %
|
|
12
|
+
* 117 / 1000 Premium Requests
|
|
13
|
+
* Resets on 1 May
|
|
14
|
+
*
|
|
15
|
+
* Token discovery (in priority order):
|
|
16
|
+
* 1. GITHUB_TOKEN environment variable
|
|
17
|
+
* 2. GH_TOKEN environment variable
|
|
18
|
+
* 3. `gh auth token` (GitHub CLI)
|
|
19
|
+
*
|
|
20
|
+
* Install:
|
|
21
|
+
* opencode plugin opencode-copilot-budget
|
|
22
|
+
*
|
|
23
|
+
* Or add manually to ~/.config/opencode/tui.json:
|
|
24
|
+
* { "plugin": ["opencode-copilot-budget"] }
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
28
|
+
import { createMemo, createResource, Match, onCleanup, onMount, Show, Switch } from "solid-js"
|
|
29
|
+
import { execFile } from "node:child_process"
|
|
30
|
+
import { promisify } from "node:util"
|
|
31
|
+
|
|
32
|
+
const id = "copilot-budget.sidebar"
|
|
33
|
+
|
|
34
|
+
const execFileAsync = promisify(execFile)
|
|
35
|
+
|
|
36
|
+
const COPILOT_USER_ENDPOINT = "https://api.github.com/copilot_internal/user"
|
|
37
|
+
const CACHE_TTL_MS = 5 * 60 * 1000
|
|
38
|
+
const REQUEST_TIMEOUT_MS = 10_000
|
|
39
|
+
const BAR_WIDTH = 16
|
|
40
|
+
const BAR_FILL_COLOR = "#3fb950" // GitHub green
|
|
41
|
+
const BAR_DANGER_COLOR = "#f85149" // red when >= 90%
|
|
42
|
+
|
|
43
|
+
// VS Code impersonation headers — kept as constants for easy version updates
|
|
44
|
+
const EDITOR_VERSION = "vscode/1.96.2"
|
|
45
|
+
const EDITOR_PLUGIN_VERSION = "copilot-chat/0.26.7"
|
|
46
|
+
const USER_AGENT = "GitHubCopilotChat/0.26.7"
|
|
47
|
+
const GITHUB_API_VERSION = "2026-01-01"
|
|
48
|
+
|
|
49
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
type CopilotUsageData = {
|
|
52
|
+
used: number
|
|
53
|
+
entitlement: number
|
|
54
|
+
percent: number
|
|
55
|
+
unlimited: boolean
|
|
56
|
+
overageCount: number
|
|
57
|
+
overagePermitted: boolean
|
|
58
|
+
resetDate: string | null
|
|
59
|
+
tier: "paid" | "free"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Cache ───────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
type CacheEntry = { data: CopilotUsageData | null; timestamp: number }
|
|
65
|
+
let _cache: CacheEntry | null = null
|
|
66
|
+
|
|
67
|
+
function bustCache() {
|
|
68
|
+
_cache = null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Token Discovery ─────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
async function discoverToken(): Promise<string | null> {
|
|
74
|
+
// 1. GITHUB_TOKEN — standard env var (CI / devcontainers / manual export)
|
|
75
|
+
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN
|
|
76
|
+
|
|
77
|
+
// 2. GH_TOKEN — alias used by GitHub CLI and many CI environments
|
|
78
|
+
if (process.env.GH_TOKEN) return process.env.GH_TOKEN
|
|
79
|
+
|
|
80
|
+
// 3. GitHub CLI — for users who ran `gh auth login`
|
|
81
|
+
try {
|
|
82
|
+
const { stdout } = await execFileAsync("gh", ["auth", "token"], { timeout: 5_000 })
|
|
83
|
+
const token = stdout.trim()
|
|
84
|
+
if (token) return token
|
|
85
|
+
} catch {
|
|
86
|
+
// gh not installed or not authenticated — silent fallthrough
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Response Parsing ────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function parseResponse(body: unknown): CopilotUsageData | null {
|
|
95
|
+
if (!body || typeof body !== "object") return null
|
|
96
|
+
const data = body as Record<string, unknown>
|
|
97
|
+
|
|
98
|
+
// Paid tier: quota_snapshots.premium_interactions
|
|
99
|
+
const snapshots = data.quota_snapshots as Record<string, unknown> | undefined
|
|
100
|
+
if (snapshots?.premium_interactions && typeof snapshots.premium_interactions === "object") {
|
|
101
|
+
const pi = snapshots.premium_interactions as Record<string, unknown>
|
|
102
|
+
const entitlement = Number(pi.entitlement ?? 0)
|
|
103
|
+
const remaining = Number(pi.remaining ?? 0)
|
|
104
|
+
const unlimited = Boolean(pi.unlimited)
|
|
105
|
+
const used = unlimited
|
|
106
|
+
? Number(pi.used ?? Math.max(0, entitlement - remaining))
|
|
107
|
+
: Math.max(0, entitlement - remaining)
|
|
108
|
+
const percent = unlimited || entitlement === 0 ? 0 : Math.round((used / entitlement) * 100)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
used: Math.round(used),
|
|
112
|
+
entitlement,
|
|
113
|
+
percent,
|
|
114
|
+
unlimited,
|
|
115
|
+
overageCount: Number(pi.overage_count ?? 0),
|
|
116
|
+
overagePermitted: Boolean(pi.overage_permitted),
|
|
117
|
+
resetDate: (data.quota_reset_date_utc as string | undefined) ?? null,
|
|
118
|
+
tier: "paid",
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Free tier: limited_user_quotas or monthly_quotas
|
|
123
|
+
const luq = data.limited_user_quotas as Record<string, unknown> | undefined
|
|
124
|
+
const mq = data.monthly_quotas as Record<string, unknown> | undefined
|
|
125
|
+
if (luq || mq) {
|
|
126
|
+
const piLuq = (luq?.premium_interactions as Record<string, unknown> | undefined) ?? {}
|
|
127
|
+
const piMq = (mq?.premium_interactions as Record<string, unknown> | undefined) ?? {}
|
|
128
|
+
const pi = Object.keys(piLuq).length ? piLuq : piMq
|
|
129
|
+
|
|
130
|
+
// Both sources empty — can't derive meaningful usage data
|
|
131
|
+
if (!Object.keys(pi).length) return null
|
|
132
|
+
|
|
133
|
+
const entitlement = Number(pi.entitlement ?? 0)
|
|
134
|
+
const remaining = Number(pi.remaining ?? 0)
|
|
135
|
+
const unlimited = Boolean(pi.unlimited)
|
|
136
|
+
const used = Math.max(0, entitlement - remaining)
|
|
137
|
+
const percent = unlimited || entitlement === 0 ? 0 : Math.round((used / entitlement) * 100)
|
|
138
|
+
const resetDate =
|
|
139
|
+
(data.limited_user_reset_date as string | undefined) ??
|
|
140
|
+
(data.quota_reset_date_utc as string | undefined) ??
|
|
141
|
+
null
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
used: Math.round(used),
|
|
145
|
+
entitlement,
|
|
146
|
+
percent,
|
|
147
|
+
unlimited,
|
|
148
|
+
overageCount: 0,
|
|
149
|
+
overagePermitted: false,
|
|
150
|
+
resetDate,
|
|
151
|
+
tier: "free",
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─── Fetch (with caching) ────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
async function fetchCopilotUsage(): Promise<CopilotUsageData | null> {
|
|
161
|
+
if (_cache !== null && Date.now() - _cache.timestamp < CACHE_TTL_MS) {
|
|
162
|
+
return _cache.data
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const token = await discoverToken()
|
|
166
|
+
if (!token) {
|
|
167
|
+
_cache = { data: null, timestamp: Date.now() }
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch(COPILOT_USER_ENDPOINT, {
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `token ${token}`,
|
|
175
|
+
Accept: "application/json",
|
|
176
|
+
// Match the headers sent by VS Code's official Copilot Chat extension.
|
|
177
|
+
"Editor-Version": EDITOR_VERSION,
|
|
178
|
+
"Editor-Plugin-Version": EDITOR_PLUGIN_VERSION,
|
|
179
|
+
"User-Agent": USER_AGENT,
|
|
180
|
+
"X-Github-Api-Version": GITHUB_API_VERSION,
|
|
181
|
+
},
|
|
182
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
_cache = { data: null, timestamp: Date.now() }
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const body = (await response.json()) as unknown
|
|
191
|
+
const parsed = parseResponse(body)
|
|
192
|
+
_cache = { data: parsed, timestamp: Date.now() }
|
|
193
|
+
return parsed
|
|
194
|
+
} catch {
|
|
195
|
+
_cache = { data: null, timestamp: Date.now() }
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── UI ──────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
function formatResetDate(dateStr: string): string {
|
|
203
|
+
try {
|
|
204
|
+
return new Date(dateStr).toLocaleDateString(undefined, { day: "numeric", month: "long" })
|
|
205
|
+
} catch {
|
|
206
|
+
return dateStr
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function ProgressBar(props: { percent: number }) {
|
|
211
|
+
const clampedPercent = Math.min(100, Math.max(0, props.percent))
|
|
212
|
+
const filled = Math.round((clampedPercent / 100) * BAR_WIDTH)
|
|
213
|
+
const empty = BAR_WIDTH - filled
|
|
214
|
+
const color = clampedPercent >= 90 ? BAR_DANGER_COLOR : BAR_FILL_COLOR
|
|
215
|
+
return (
|
|
216
|
+
<text fg={color}>{`${"█".repeat(filled)}${"░".repeat(empty)} ${clampedPercent}% Used`}</text>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function UsageDetail(props: { api: TuiPluginApi }) {
|
|
221
|
+
const theme = () => props.api.theme.current
|
|
222
|
+
const [usage, { refetch }] = createResource(fetchCopilotUsage)
|
|
223
|
+
|
|
224
|
+
onMount(() => {
|
|
225
|
+
// Refetch whenever the AI finishes responding — exactly when a Copilot
|
|
226
|
+
// request has been consumed. Bust the cache first so we always hit the
|
|
227
|
+
// network and get a fresh count.
|
|
228
|
+
const off = props.api.event.on("session.idle", () => {
|
|
229
|
+
bustCache()
|
|
230
|
+
refetch()
|
|
231
|
+
})
|
|
232
|
+
onCleanup(off)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<Switch>
|
|
237
|
+
<Match when={usage()}>
|
|
238
|
+
{(data) => (
|
|
239
|
+
<box direction="column">
|
|
240
|
+
<Show
|
|
241
|
+
when={!data().unlimited}
|
|
242
|
+
fallback={<text fg={theme().textMuted}>{`${data().used} used (unlimited)`}</text>}
|
|
243
|
+
>
|
|
244
|
+
<ProgressBar percent={data().percent} />
|
|
245
|
+
<text fg={theme().textMuted}>{`${data().used} / ${data().entitlement} Premium Requests`}</text>
|
|
246
|
+
</Show>
|
|
247
|
+
<Show when={data().overageCount > 0}>
|
|
248
|
+
<text fg={theme().warning}>{`+${data().overageCount} overage`}</text>
|
|
249
|
+
</Show>
|
|
250
|
+
<Show when={data().resetDate}>
|
|
251
|
+
<text fg={theme().textMuted}>{"Resets on "}<b>{formatResetDate(data().resetDate!)}</b></text>
|
|
252
|
+
</Show>
|
|
253
|
+
</box>
|
|
254
|
+
)}
|
|
255
|
+
</Match>
|
|
256
|
+
<Match when={usage.loading}>
|
|
257
|
+
<text fg={theme().textMuted}>syncing...</text>
|
|
258
|
+
</Match>
|
|
259
|
+
<Match when={true}>
|
|
260
|
+
<text fg={theme().textMuted}>sync unavailable</text>
|
|
261
|
+
</Match>
|
|
262
|
+
</Switch>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function View(props: { api: TuiPluginApi }) {
|
|
267
|
+
const theme = () => props.api.theme.current
|
|
268
|
+
const isCopilot = createMemo(() =>
|
|
269
|
+
props.api.state.provider.some((p) => p.id === "github-copilot"),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<Show when={isCopilot()}>
|
|
274
|
+
<box direction="column">
|
|
275
|
+
<text fg={theme().text}><b>Copilot Budget</b></text>
|
|
276
|
+
<UsageDetail api={props.api} />
|
|
277
|
+
</box>
|
|
278
|
+
</Show>
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─── Plugin Registration ──────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
const tui: TuiPlugin = async (api) => {
|
|
285
|
+
api.slots.register({
|
|
286
|
+
order: 50, // top of sidebar — before Context (100), MCP (200), LSP (300), etc.
|
|
287
|
+
slots: {
|
|
288
|
+
sidebar_content() {
|
|
289
|
+
return <View api={api} />
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const plugin: TuiPluginModule & { id: string } = { id, tui }
|
|
296
|
+
export default plugin
|