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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/package.json +2 -2
  3. 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
- - Reset date, updated automatically after every AI response
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.1",
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. Automatically refreshes after each AI response. Only visible when
7
- * the active provider is `github-copilot`.
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 ← green; turns red at ≥ 90 %
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 off = props.api.event.on("session.idle", () => {
229
- bustCache()
230
- refetch()
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
- <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>
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
- <box direction="column">
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
  }