opencode-copilot-budget 1.0.1 → 1.2.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/README.md +3 -1
- package/package.json +2 -2
- package/src/index.tsx +117 -41
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@ Shows your GitHub Copilot premium request budget in the [OpenCode](https://openc
|
|
|
8
8
|
|
|
9
9
|
- Progress bar that turns **red** when you reach 90 % of your budget
|
|
10
10
|
- Request count and percentage used
|
|
11
|
-
-
|
|
11
|
+
- `↻ Refresh` button in the header row, with pointer cursor on hover
|
|
12
|
+
- Reset date, updated automatically after prompt submit and after every AI response
|
|
12
13
|
- 5-minute cache to avoid unnecessary API calls
|
|
13
14
|
- Works with paid and free Copilot plans
|
|
14
15
|
|
|
@@ -81,6 +82,7 @@ export GITHUB_TOKEN=$(gh auth token)
|
|
|
81
82
|
| Unlimited plan | `62 used (unlimited)` |
|
|
82
83
|
| Overage consumed | `+5 overage` (shown below usage) |
|
|
83
84
|
| Reset date known | `Resets on 1 May` (date in bold) |
|
|
85
|
+
| Manual refresh | inline `🔄 Refresh` next to the first usage line |
|
|
84
86
|
| Token missing / network error | `sync unavailable` |
|
|
85
87
|
| First load | `syncing...` |
|
|
86
88
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-copilot-budget",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "GitHub Copilot premium budget in the OpenCode TUI sidebar",
|
|
5
5
|
"author": "Bhaskar Melkani",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/bhaskarmelkani/opencode-copilot-budget.git"
|
|
8
|
+
"url": "git+https://github.com/bhaskarmelkani/opencode-copilot-budget.git"
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://github.com/bhaskarmelkani/opencode-copilot-budget#readme",
|
|
11
11
|
"bugs": {
|
package/src/index.tsx
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* opencode-copilot-budget
|
|
4
4
|
*
|
|
5
5
|
* Displays your GitHub Copilot premium request budget in the OpenCode TUI
|
|
6
|
-
* sidebar.
|
|
7
|
-
* the active provider is
|
|
6
|
+
* sidebar. Refreshes after prompt submit, after each AI response, and via an
|
|
7
|
+
* inline manual refresh action. Only visible when the active provider is
|
|
8
|
+
* `github-copilot`.
|
|
8
9
|
*
|
|
9
10
|
* Display format:
|
|
10
|
-
* Copilot Budget
|
|
11
|
-
* ████████░░░░░░░░ 12% Used
|
|
11
|
+
* Copilot Budget ↻ Refresh
|
|
12
|
+
* ████████░░░░░░░░ 12% Used
|
|
12
13
|
* 117 / 1000 Premium Requests
|
|
13
14
|
* Resets on 1 May
|
|
14
15
|
*
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
28
|
-
import { createMemo, createResource, Match, onCleanup, onMount, Show, Switch } from "solid-js"
|
|
29
|
+
import { createMemo, createResource, createSignal, Match, onCleanup, onMount, Show, Switch } from "solid-js"
|
|
30
|
+
import { RGBA } from "@opentui/core"
|
|
29
31
|
import { execFile } from "node:child_process"
|
|
30
32
|
import { promisify } from "node:util"
|
|
31
33
|
|
|
@@ -217,64 +219,138 @@ function ProgressBar(props: { percent: number }) {
|
|
|
217
219
|
)
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
function RefreshButton(props: { api: TuiPluginApi; refresh: () => void; disabled: boolean }) {
|
|
223
|
+
const theme = () => props.api.theme.current
|
|
224
|
+
const color = () => props.disabled ? theme().textMuted : theme().primary
|
|
225
|
+
const bgTint = () => {
|
|
226
|
+
const c = color()
|
|
227
|
+
return RGBA.fromValues(c.r, c.g, c.b, 0.15)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<box
|
|
232
|
+
onMouseUp={() => {
|
|
233
|
+
if (props.disabled) return
|
|
234
|
+
props.refresh()
|
|
235
|
+
}}
|
|
236
|
+
onMouseOver={() => {
|
|
237
|
+
if (!props.disabled) props.api.renderer.setMousePointer("pointer")
|
|
238
|
+
}}
|
|
239
|
+
onMouseOut={() => props.api.renderer.setMousePointer("default")}
|
|
240
|
+
marginLeft={2}
|
|
241
|
+
flexDirection="row"
|
|
242
|
+
>
|
|
243
|
+
<text fg={bgTint()} bg={theme().background}>{"▐"}</text>
|
|
244
|
+
<text fg={color()} bg={bgTint()}>
|
|
245
|
+
<b>{"↻ Refresh"}</b>
|
|
246
|
+
</text>
|
|
247
|
+
<text fg={bgTint()} bg={theme().background}>{"▌"}</text>
|
|
248
|
+
</box>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
220
252
|
function UsageDetail(props: { api: TuiPluginApi }) {
|
|
221
253
|
const theme = () => props.api.theme.current
|
|
222
254
|
const [usage, { refetch }] = createResource(fetchCopilotUsage)
|
|
255
|
+
const [manualRefreshing, setManualRefreshing] = createSignal(false)
|
|
256
|
+
let refreshInFlight: Promise<void> | null = null
|
|
257
|
+
|
|
258
|
+
const doRefresh = (manual: boolean): Promise<void> => {
|
|
259
|
+
if (refreshInFlight) return refreshInFlight
|
|
260
|
+
if (manual) setManualRefreshing(true)
|
|
261
|
+
bustCache()
|
|
262
|
+
refreshInFlight = (async () => {
|
|
263
|
+
try {
|
|
264
|
+
await refetch()
|
|
265
|
+
} finally {
|
|
266
|
+
if (manual) setManualRefreshing(false)
|
|
267
|
+
refreshInFlight = null
|
|
268
|
+
}
|
|
269
|
+
})()
|
|
270
|
+
return refreshInFlight
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const autosync = () => {
|
|
274
|
+
void doRefresh(false)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const triggerRefresh = () => {
|
|
278
|
+
void doRefresh(true)
|
|
279
|
+
}
|
|
223
280
|
|
|
224
281
|
onMount(() => {
|
|
282
|
+
const offPromptSubmit = props.api.event.on("tui.command.execute", (event) => {
|
|
283
|
+
if (event.properties.command !== "prompt.submit") return
|
|
284
|
+
autosync()
|
|
285
|
+
})
|
|
286
|
+
|
|
225
287
|
// Refetch whenever the AI finishes responding — exactly when a Copilot
|
|
226
288
|
// request has been consumed. Bust the cache first so we always hit the
|
|
227
289
|
// network and get a fresh count.
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
290
|
+
const offSessionIdle = props.api.event.on("session.idle", () => {
|
|
291
|
+
autosync()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
onCleanup(() => {
|
|
295
|
+
offPromptSubmit()
|
|
296
|
+
offSessionIdle()
|
|
231
297
|
})
|
|
232
|
-
onCleanup(off)
|
|
233
298
|
})
|
|
234
299
|
|
|
235
300
|
return (
|
|
236
|
-
<
|
|
237
|
-
<
|
|
238
|
-
{(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
301
|
+
<box flexDirection="column" gap={1}>
|
|
302
|
+
<box flexDirection="row">
|
|
303
|
+
<text fg={theme().text}><b>Copilot Budget</b></text>
|
|
304
|
+
<RefreshButton
|
|
305
|
+
api={props.api}
|
|
306
|
+
refresh={triggerRefresh}
|
|
307
|
+
disabled={usage.loading || manualRefreshing()}
|
|
308
|
+
/>
|
|
309
|
+
</box>
|
|
310
|
+
<Switch>
|
|
311
|
+
<Match when={manualRefreshing()}>
|
|
312
|
+
<text fg={theme().textMuted}>syncing...</text>
|
|
313
|
+
</Match>
|
|
314
|
+
<Match when={usage()}>
|
|
315
|
+
{(data) => (
|
|
316
|
+
<box flexDirection="column">
|
|
317
|
+
<Show
|
|
318
|
+
when={!data().unlimited}
|
|
319
|
+
fallback={
|
|
320
|
+
<text fg={theme().textMuted}>{`${data().used} used (unlimited)`}</text>
|
|
321
|
+
}
|
|
322
|
+
>
|
|
323
|
+
<ProgressBar percent={data().percent} />
|
|
324
|
+
<text fg={theme().textMuted}>{`${data().used} / ${data().entitlement} Premium Requests`}</text>
|
|
325
|
+
</Show>
|
|
326
|
+
<Show when={data().overageCount > 0}>
|
|
327
|
+
<text fg={theme().warning}>{`+${data().overageCount} overage`}</text>
|
|
328
|
+
</Show>
|
|
329
|
+
<Show when={data().resetDate}>
|
|
330
|
+
<text fg={theme().textMuted}>{"Resets on "}<b>{formatResetDate(data().resetDate!)}</b></text>
|
|
331
|
+
</Show>
|
|
332
|
+
</box>
|
|
333
|
+
)}
|
|
334
|
+
</Match>
|
|
335
|
+
<Match when={usage.loading}>
|
|
336
|
+
<text fg={theme().textMuted}>syncing...</text>
|
|
337
|
+
</Match>
|
|
338
|
+
<Match when={true}>
|
|
339
|
+
<text fg={theme().textMuted}>sync unavailable</text>
|
|
340
|
+
</Match>
|
|
341
|
+
</Switch>
|
|
342
|
+
</box>
|
|
263
343
|
)
|
|
264
344
|
}
|
|
265
345
|
|
|
266
346
|
function View(props: { api: TuiPluginApi }) {
|
|
267
|
-
const theme = () => props.api.theme.current
|
|
268
347
|
const isCopilot = createMemo(() =>
|
|
269
348
|
props.api.state.provider.some((p) => p.id === "github-copilot"),
|
|
270
349
|
)
|
|
271
350
|
|
|
272
351
|
return (
|
|
273
352
|
<Show when={isCopilot()}>
|
|
274
|
-
<
|
|
275
|
-
<text fg={theme().text}><b>Copilot Budget</b></text>
|
|
276
|
-
<UsageDetail api={props.api} />
|
|
277
|
-
</box>
|
|
353
|
+
<UsageDetail api={props.api} />
|
|
278
354
|
</Show>
|
|
279
355
|
)
|
|
280
356
|
}
|