@slkiser/opencode-quota 4.6.0 → 4.6.1

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/dist/tui.tsx CHANGED
@@ -8,18 +8,21 @@ import type {
8
8
  } from "@opencode-ai/plugin/tui";
9
9
  import type { JSX } from "@opentui/solid";
10
10
  import { createEffect, createSignal, onCleanup, Show } from "solid-js";
11
+ import { formatDisplayedPercentLabel, formatResetCountdown } from "./lib/format-utils.js";
11
12
  import {
12
13
  buildQuotaDialogCommandOutput,
13
14
  QUOTA_DIALOG_COMMANDS,
14
15
  type QuotaDialogCommandId,
15
16
  type QuotaDialogCommandSpec,
16
17
  } from "./lib/quota-dialog-commands.js";
18
+ import { extractSingleWindowWindowLabel } from "./lib/quota-entry-display.js";
17
19
  import type { SessionTokenError } from "./lib/quota-status.js";
18
20
  import { disposeQuotaTelemetryOwner } from "./lib/quota-telemetry.js";
19
21
  import { getSidebarBodyLineColor } from "./lib/tui-line-style.js";
20
22
  import type {
21
23
  CompactStatusState,
22
24
  HomeBottomState,
25
+ PromptBarState,
23
26
  SidebarPanelState,
24
27
  } from "./lib/tui-panel-state.js";
25
28
  import {
@@ -65,6 +68,7 @@ type SessionQuotaResource = {
65
68
  sessionID: string;
66
69
  sidebar: () => SidebarPanelState;
67
70
  compact: () => CompactStatusState;
71
+ promptBar: () => PromptBarState;
68
72
  retain: () => SessionQuotaResource;
69
73
  release: () => void;
70
74
  };
@@ -108,6 +112,7 @@ const FALLBACK_SURFACE_REGISTRATION: TuiSurfaceRegistration = {
108
112
  hasNativeProviderQuota: false,
109
113
  suppressedByNativeProviderQuota: false,
110
114
  },
115
+ promptBar: { enabled: false },
111
116
  announcements: { homeBottom: false },
112
117
  homeBottom: false,
113
118
  };
@@ -168,6 +173,7 @@ function createSessionQuotaResource(
168
173
  lines: [],
169
174
  });
170
175
  const [compact, setCompact] = createSignal<CompactStatusState>({ status: "loading" });
176
+ const [promptBar, setPromptBar] = createSignal<PromptBarState>({ status: "loading" });
171
177
 
172
178
  let loadOrdinal = 0;
173
179
  const lifecycle = createTuiRefreshLifecycle({
@@ -183,6 +189,7 @@ function createSessionQuotaResource(
183
189
  apply: (next) => {
184
190
  setSidebar(next.sidebar);
185
191
  setCompact(next.compact);
192
+ setPromptBar(next.promptBar ?? { status: "loading" });
186
193
  },
187
194
  intervalMs: REFRESH_INTERVAL_MS,
188
195
  eventRefreshDelaysMs: EVENT_REFRESH_DELAYS_MS,
@@ -220,6 +227,7 @@ function createSessionQuotaResource(
220
227
  sessionID,
221
228
  sidebar,
222
229
  compact,
230
+ promptBar,
223
231
  retain: () => {
224
232
  lifecycle.retain();
225
233
  return resource;
@@ -447,6 +455,166 @@ function SessionPromptWithCompactStatus(props: {
447
455
  );
448
456
  }
449
457
 
458
+ const PROMPT_BAR_WIDTH = 12;
459
+
460
+ function shouldRenderPromptBar(
461
+ bar: PromptBarState,
462
+ ): bar is Extract<PromptBarState, { status: "ready" }> {
463
+ return bar.status === "ready" && Boolean(bar.entry);
464
+ }
465
+
466
+ function useSessionRunning(api: TuiPluginApi, sessionID: () => string): () => boolean {
467
+ const [running, setRunning] = createSignal(false);
468
+ createEffect(() => {
469
+ const id = sessionID();
470
+ if (!id) {
471
+ setRunning(false);
472
+ return;
473
+ }
474
+ const update = () => {
475
+ try {
476
+ const sessionState = api.state.session as {
477
+ status?: (sessionID: string) => { type?: string } | undefined;
478
+ };
479
+ const status = sessionState.status?.(id);
480
+ setRunning(status?.type === "busy" || status?.type === "retry");
481
+ } catch {
482
+ setRunning(false);
483
+ }
484
+ };
485
+ update();
486
+ const disposers = [
487
+ api.event.on("session.status", (event) => {
488
+ if (event.properties?.sessionID === id) {
489
+ update();
490
+ }
491
+ }),
492
+ api.event.on("session.updated", (event) => {
493
+ if (event.properties?.info?.id === id) {
494
+ update();
495
+ }
496
+ }),
497
+ ];
498
+ onCleanup(() => {
499
+ for (const dispose of disposers) {
500
+ if (typeof dispose === "function") {
501
+ dispose();
502
+ }
503
+ }
504
+ });
505
+ });
506
+ return running;
507
+ }
508
+
509
+ function buildPromptBarParts(params: {
510
+ bar: () => PromptBarState;
511
+ running: () => boolean;
512
+ phase: () => number;
513
+ }): { label: string; barText: string; meta: string } | undefined {
514
+ const bar = params.bar();
515
+ if (!shouldRenderPromptBar(bar)) return undefined;
516
+ const entry = bar.entry;
517
+ if (!entry) return undefined;
518
+ const windowLabel =
519
+ extractSingleWindowWindowLabel(entry.label ?? "") ??
520
+ extractSingleWindowWindowLabel(entry.name ?? "") ??
521
+ "Quota";
522
+ const percent = formatDisplayedPercentLabel(
523
+ entry.percentRemaining ?? 0,
524
+ bar.percentDisplayMode ?? "remaining",
525
+ );
526
+ const reset = entry.resetTimeIso
527
+ ? formatResetCountdown(entry.resetTimeIso, {
528
+ compactRounded: true,
529
+ decimals: bar.resetTimeDecimals,
530
+ })
531
+ : "";
532
+ const p = Math.max(0, Math.min(100, Math.round(entry.percentRemaining ?? 0)));
533
+ const filled = Math.round((p / 100) * PROMPT_BAR_WIDTH);
534
+ const empty = PROMPT_BAR_WIDTH - filled;
535
+ let barText = "█".repeat(filled) + "░".repeat(empty);
536
+ if (params.running() && filled > 0) {
537
+ const cells = Array(filled).fill("▓");
538
+ const center = params.phase() % filled;
539
+ const gradient = ["▒", "▓", "█", "▓", "▒"];
540
+ for (let offset = -2; offset <= 2; offset++) {
541
+ const position = (center + offset + filled) % filled;
542
+ cells[position] = gradient[offset + 2];
543
+ }
544
+ barText = cells.join("") + "░".repeat(empty);
545
+ }
546
+ return {
547
+ label: windowLabel,
548
+ barText,
549
+ meta: [percent.replace(/\s+left$/u, ""), reset].filter(Boolean).join(" | "),
550
+ };
551
+ }
552
+
553
+ function PromptQuotaHint(props: {
554
+ api: TuiPluginApi;
555
+ bar: () => PromptBarState;
556
+ running: () => boolean;
557
+ phase: () => number;
558
+ }) {
559
+ const parts = () => buildPromptBarParts(props);
560
+ const barColor = () => props.api.theme.current.textMuted;
561
+ const label = () => parts()?.label ?? "";
562
+ const bar = () => parts()?.barText ?? "";
563
+ const meta = () => parts()?.meta ?? "";
564
+
565
+ return (
566
+ <Show when={parts()}>
567
+ <box flexDirection="row" justifyContent="flex-end" gap={1}>
568
+ <text fg={props.api.theme.current.textMuted} wrapMode="none">
569
+ {label()}
570
+ </text>
571
+ <text fg={barColor()} wrapMode="none">
572
+ {bar()}
573
+ </text>
574
+ <text fg={props.api.theme.current.textMuted} wrapMode="none">
575
+ {meta()}
576
+ </text>
577
+ </box>
578
+ </Show>
579
+ );
580
+ }
581
+
582
+ function SessionQuotaPromptBar(props: {
583
+ api: TuiPluginApi;
584
+ sessionID: string;
585
+ initialLoads?: TuiInitialLoadCoordinator;
586
+ visible?: boolean;
587
+ disabled?: boolean;
588
+ onSubmit?: () => void;
589
+ promptRef?: TuiPromptRefCallback;
590
+ }) {
591
+ const resource = useSessionQuotaResource(props.api, () => props.sessionID, props.initialLoads);
592
+ const promptBar = () => resource().promptBar();
593
+ const running = useSessionRunning(props.api, () => props.sessionID);
594
+ const [phase, setPhase] = createSignal(0);
595
+ createEffect(() => {
596
+ if (!running() || !shouldRenderPromptBar(promptBar())) {
597
+ setPhase(0);
598
+ return;
599
+ }
600
+ const interval = setInterval(() => setPhase((p) => p + 1), 160);
601
+ onCleanup(() => clearInterval(interval));
602
+ });
603
+
604
+ return (
605
+ <box gap={0}>
606
+ <props.api.ui.Prompt
607
+ sessionID={props.sessionID}
608
+ visible={props.visible}
609
+ disabled={props.disabled}
610
+ onSubmit={props.onSubmit}
611
+ ref={props.promptRef}
612
+ />
613
+ <PromptQuotaHint api={props.api} bar={promptBar} running={running} phase={phase} />
614
+ </box>
615
+ );
616
+ }
617
+
450
618
  function HomeBottomView(props: {
451
619
  api: TuiPluginApi;
452
620
  compactHomeBottomEnabled: boolean;
@@ -740,7 +908,21 @@ function registerStableTuiSlots(api: TuiPluginApi, current: () => TuiRegistratio
740
908
  },
741
909
  ) {
742
910
  const state = current();
743
- if (state.status !== "active" || !state.registration.compact.sessionPrompt) return null;
911
+ if (state.status !== "active") return null;
912
+ if (state.registration.promptBar.enabled) {
913
+ return (
914
+ <SessionQuotaPromptBar
915
+ api={api}
916
+ sessionID={props.session_id}
917
+ initialLoads={state.initialLoads}
918
+ visible={props.visible}
919
+ disabled={props.disabled}
920
+ onSubmit={props.on_submit}
921
+ promptRef={props.ref}
922
+ />
923
+ );
924
+ }
925
+ if (!state.registration.compact.sessionPrompt) return null;
744
926
  return (
745
927
  <SessionPromptWithCompactStatus
746
928
  api={api}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slkiser/opencode-quota",
3
- "version": "4.6.0",
3
+ "version": "4.6.1",
4
4
  "description": "OpenCode quota & tokens usage with zero context window pollution. Supports GitHub Copilot, OpenAI (Plus/Pro), Qwen Code, Chutes AI, Synthetic, Google Antigravity, Z.ai coding plan and more.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",