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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/package.json +2 -2
  3. 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
- - Reset date, updated automatically after every AI response
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.1",
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. 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
11
  * Copilot Budget
11
- * ████████░░░░░░░░ 12% Used green; turns red at ≥ 90 %
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 off = props.api.event.on("session.idle", () => {
229
- bustCache()
230
- refetch()
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
- <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>
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
- </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>
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
- <box direction="column">
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
  }