opencode-copilot-budget 1.0.1 → 1.1.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 +116 -39
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
|
+
- Inline `🔄 Refresh` action next to the first usage line
|
|
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.1.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
11
|
* Copilot Budget
|
|
11
|
-
* ████████░░░░░░░░ 12% Used
|
|
12
|
+
* ████████░░░░░░░░ 12% Used 🔄 Refresh
|
|
12
13
|
* 117 / 1000 Premium Requests
|
|
13
14
|
* Resets on 1 May
|
|
14
15
|
*
|
|
@@ -25,7 +26,7 @@
|
|
|
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"
|
|
29
30
|
import { execFile } from "node:child_process"
|
|
30
31
|
import { promisify } from "node:util"
|
|
31
32
|
|
|
@@ -217,64 +218,140 @@ function ProgressBar(props: { percent: number }) {
|
|
|
217
218
|
)
|
|
218
219
|
}
|
|
219
220
|
|
|
221
|
+
function RefreshButton(props: { api: TuiPluginApi; refresh: () => void; disabled: boolean }) {
|
|
222
|
+
const theme = () => props.api.theme.current
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<box
|
|
226
|
+
onMouseUp={() => {
|
|
227
|
+
if (props.disabled) return
|
|
228
|
+
props.refresh()
|
|
229
|
+
}}
|
|
230
|
+
paddingLeft={1}
|
|
231
|
+
>
|
|
232
|
+
<text fg={props.disabled ? theme().textMuted : theme().primary}>
|
|
233
|
+
<b>🔄 Refresh</b>
|
|
234
|
+
</text>
|
|
235
|
+
</box>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
220
239
|
function UsageDetail(props: { api: TuiPluginApi }) {
|
|
221
240
|
const theme = () => props.api.theme.current
|
|
222
241
|
const [usage, { refetch }] = createResource(fetchCopilotUsage)
|
|
242
|
+
const [manualRefreshing, setManualRefreshing] = createSignal(false)
|
|
243
|
+
let refreshInFlight: Promise<void> | null = null
|
|
244
|
+
|
|
245
|
+
const doRefresh = (manual: boolean): Promise<void> => {
|
|
246
|
+
if (refreshInFlight) return refreshInFlight
|
|
247
|
+
if (manual) setManualRefreshing(true)
|
|
248
|
+
bustCache()
|
|
249
|
+
refreshInFlight = (async () => {
|
|
250
|
+
try {
|
|
251
|
+
await refetch()
|
|
252
|
+
} finally {
|
|
253
|
+
if (manual) setManualRefreshing(false)
|
|
254
|
+
refreshInFlight = null
|
|
255
|
+
}
|
|
256
|
+
})()
|
|
257
|
+
return refreshInFlight
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const autosync = () => {
|
|
261
|
+
void doRefresh(false)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const triggerRefresh = () => {
|
|
265
|
+
void doRefresh(true)
|
|
266
|
+
}
|
|
223
267
|
|
|
224
268
|
onMount(() => {
|
|
269
|
+
const offPromptSubmit = props.api.event.on("tui.command.execute", (event) => {
|
|
270
|
+
if (event.properties.command !== "prompt.submit") return
|
|
271
|
+
autosync()
|
|
272
|
+
})
|
|
273
|
+
|
|
225
274
|
// Refetch whenever the AI finishes responding — exactly when a Copilot
|
|
226
275
|
// request has been consumed. Bust the cache first so we always hit the
|
|
227
276
|
// network and get a fresh count.
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
277
|
+
const offSessionIdle = props.api.event.on("session.idle", () => {
|
|
278
|
+
autosync()
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
onCleanup(() => {
|
|
282
|
+
offPromptSubmit()
|
|
283
|
+
offSessionIdle()
|
|
231
284
|
})
|
|
232
|
-
onCleanup(off)
|
|
233
285
|
})
|
|
234
286
|
|
|
235
287
|
return (
|
|
236
|
-
<
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
288
|
+
<box flexDirection="column" gap={1}>
|
|
289
|
+
<text fg={theme().text}><b>Copilot Budget</b></text>
|
|
290
|
+
<Switch>
|
|
291
|
+
<Match when={manualRefreshing()}>
|
|
292
|
+
<text fg={theme().textMuted}>syncing...</text>
|
|
293
|
+
</Match>
|
|
294
|
+
<Match when={usage()}>
|
|
295
|
+
{(data) => (
|
|
296
|
+
<box flexDirection="column">
|
|
297
|
+
<Show
|
|
298
|
+
when={!data().unlimited}
|
|
299
|
+
fallback={
|
|
300
|
+
<box flexDirection="row">
|
|
301
|
+
<text fg={theme().textMuted}>{`${data().used} used (unlimited)`}</text>
|
|
302
|
+
<RefreshButton
|
|
303
|
+
api={props.api}
|
|
304
|
+
refresh={triggerRefresh}
|
|
305
|
+
disabled={usage.loading || manualRefreshing()}
|
|
306
|
+
/>
|
|
307
|
+
</box>
|
|
308
|
+
}
|
|
309
|
+
>
|
|
310
|
+
<box flexDirection="row">
|
|
311
|
+
<ProgressBar percent={data().percent} />
|
|
312
|
+
<RefreshButton
|
|
313
|
+
api={props.api}
|
|
314
|
+
refresh={triggerRefresh}
|
|
315
|
+
disabled={usage.loading || manualRefreshing()}
|
|
316
|
+
/>
|
|
317
|
+
</box>
|
|
318
|
+
<text fg={theme().textMuted}>{`${data().used} / ${data().entitlement} Premium Requests`}</text>
|
|
319
|
+
</Show>
|
|
320
|
+
<Show when={data().overageCount > 0}>
|
|
321
|
+
<text fg={theme().warning}>{`+${data().overageCount} overage`}</text>
|
|
322
|
+
</Show>
|
|
323
|
+
<Show when={data().resetDate}>
|
|
324
|
+
<text fg={theme().textMuted}>{"Resets on "}<b>{formatResetDate(data().resetDate!)}</b></text>
|
|
325
|
+
</Show>
|
|
326
|
+
</box>
|
|
327
|
+
)}
|
|
328
|
+
</Match>
|
|
329
|
+
<Match when={usage.loading}>
|
|
330
|
+
<text fg={theme().textMuted}>syncing...</text>
|
|
331
|
+
</Match>
|
|
332
|
+
<Match when={true}>
|
|
333
|
+
<box flexDirection="row">
|
|
334
|
+
<text fg={theme().textMuted}>sync unavailable</text>
|
|
335
|
+
<RefreshButton
|
|
336
|
+
api={props.api}
|
|
337
|
+
refresh={triggerRefresh}
|
|
338
|
+
disabled={usage.loading || manualRefreshing()}
|
|
339
|
+
/>
|
|
253
340
|
</box>
|
|
254
|
-
|
|
255
|
-
</
|
|
256
|
-
|
|
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>
|
|
341
|
+
</Match>
|
|
342
|
+
</Switch>
|
|
343
|
+
</box>
|
|
263
344
|
)
|
|
264
345
|
}
|
|
265
346
|
|
|
266
347
|
function View(props: { api: TuiPluginApi }) {
|
|
267
|
-
const theme = () => props.api.theme.current
|
|
268
348
|
const isCopilot = createMemo(() =>
|
|
269
349
|
props.api.state.provider.some((p) => p.id === "github-copilot"),
|
|
270
350
|
)
|
|
271
351
|
|
|
272
352
|
return (
|
|
273
353
|
<Show when={isCopilot()}>
|
|
274
|
-
<
|
|
275
|
-
<text fg={theme().text}><b>Copilot Budget</b></text>
|
|
276
|
-
<UsageDetail api={props.api} />
|
|
277
|
-
</box>
|
|
354
|
+
<UsageDetail api={props.api} />
|
|
278
355
|
</Show>
|
|
279
356
|
)
|
|
280
357
|
}
|