@slkiser/opencode-quota 4.5.1 → 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.
Files changed (46) hide show
  1. package/README.md +2 -0
  2. package/dist/data/modelsdev-pricing.min.json +5 -0
  3. package/dist/lib/config.d.ts +1 -1
  4. package/dist/lib/config.d.ts.map +1 -1
  5. package/dist/lib/config.js +75 -0
  6. package/dist/lib/config.js.map +1 -1
  7. package/dist/lib/maintainer-announcements.d.ts +3 -0
  8. package/dist/lib/maintainer-announcements.d.ts.map +1 -1
  9. package/dist/lib/maintainer-announcements.js +13 -0
  10. package/dist/lib/maintainer-announcements.js.map +1 -1
  11. package/dist/lib/quota-export.d.ts +1 -0
  12. package/dist/lib/quota-export.d.ts.map +1 -1
  13. package/dist/lib/quota-export.js +4 -0
  14. package/dist/lib/quota-export.js.map +1 -1
  15. package/dist/lib/quota-render-data.d.ts +2 -0
  16. package/dist/lib/quota-render-data.d.ts.map +1 -1
  17. package/dist/lib/quota-render-data.js +7 -0
  18. package/dist/lib/quota-render-data.js.map +1 -1
  19. package/dist/lib/quota-reset-notifications.d.ts +20 -0
  20. package/dist/lib/quota-reset-notifications.d.ts.map +1 -0
  21. package/dist/lib/quota-reset-notifications.js +328 -0
  22. package/dist/lib/quota-reset-notifications.js.map +1 -0
  23. package/dist/lib/quota-stats.d.ts.map +1 -1
  24. package/dist/lib/quota-stats.js +4 -0
  25. package/dist/lib/quota-stats.js.map +1 -1
  26. package/dist/lib/tui-panel-state.d.ts +15 -0
  27. package/dist/lib/tui-panel-state.d.ts.map +1 -1
  28. package/dist/lib/tui-panel-state.js.map +1 -1
  29. package/dist/lib/tui-refresh-lifecycle.d.ts.map +1 -1
  30. package/dist/lib/tui-refresh-lifecycle.js +0 -1
  31. package/dist/lib/tui-refresh-lifecycle.js.map +1 -1
  32. package/dist/lib/tui-runtime.d.ts +14 -2
  33. package/dist/lib/tui-runtime.d.ts.map +1 -1
  34. package/dist/lib/tui-runtime.js +87 -6
  35. package/dist/lib/tui-runtime.js.map +1 -1
  36. package/dist/lib/types.d.ts +14 -0
  37. package/dist/lib/types.d.ts.map +1 -1
  38. package/dist/lib/types.js +7 -0
  39. package/dist/lib/types.js.map +1 -1
  40. package/dist/plugin.d.ts.map +1 -1
  41. package/dist/plugin.js +38 -1
  42. package/dist/plugin.js.map +1 -1
  43. package/dist/tui.d.ts.map +1 -1
  44. package/dist/tui.js +472 -181
  45. package/dist/tui.tsx +395 -73
  46. package/package.json +1 -1
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 {
@@ -40,6 +43,8 @@ import {
40
43
  loadTuiSessionQuotaSurfaces,
41
44
  normalizeTuiSessionID,
42
45
  resolveTuiSurfaceRegistration,
46
+ type TuiInitialRuntimeSeed,
47
+ type TuiSurfaceRegistration,
43
48
  writeTuiQuotaExportIfEnabled,
44
49
  } from "./lib/tui-runtime.js";
45
50
  import type { TuiCommandDisplay } from "./lib/types.js";
@@ -63,6 +68,7 @@ type SessionQuotaResource = {
63
68
  sessionID: string;
64
69
  sidebar: () => SidebarPanelState;
65
70
  compact: () => CompactStatusState;
71
+ promptBar: () => PromptBarState;
66
72
  retain: () => SessionQuotaResource;
67
73
  release: () => void;
68
74
  };
@@ -73,6 +79,78 @@ type HomeBottomResource = {
73
79
  release: () => void;
74
80
  };
75
81
 
82
+ type TuiInitialLoadCoordinator = {
83
+ takeInitialSession: () => TuiInitialRuntimeSeed | undefined;
84
+ takeInitialHome: () => TuiInitialRuntimeSeed | undefined;
85
+ };
86
+
87
+ type TuiRegistrationState =
88
+ | { status: "pending" }
89
+ | {
90
+ status: "active";
91
+ registration: TuiSurfaceRegistration;
92
+ initialLoads?: TuiInitialLoadCoordinator;
93
+ }
94
+ | { status: "disposed" };
95
+
96
+ type TuiRegistrationGate = {
97
+ current: () => TuiRegistrationState;
98
+ activate: (
99
+ registration: TuiSurfaceRegistration,
100
+ initialLoads?: TuiInitialLoadCoordinator,
101
+ ) => void;
102
+ dispose: () => void;
103
+ };
104
+
105
+ const FALLBACK_SURFACE_REGISTRATION: TuiSurfaceRegistration = {
106
+ commandDisplay: "inline",
107
+ sidebar: { enabled: true },
108
+ compact: {
109
+ enabled: false,
110
+ homeBottom: false,
111
+ sessionPrompt: false,
112
+ hasNativeProviderQuota: false,
113
+ suppressedByNativeProviderQuota: false,
114
+ },
115
+ promptBar: { enabled: false },
116
+ announcements: { homeBottom: false },
117
+ homeBottom: false,
118
+ };
119
+
120
+ function createTuiInitialLoadCoordinator(seed: TuiInitialRuntimeSeed): TuiInitialLoadCoordinator {
121
+ let sessionAvailable = true;
122
+ let homeAvailable = true;
123
+
124
+ return {
125
+ takeInitialSession() {
126
+ if (!sessionAvailable) return undefined;
127
+ sessionAvailable = false;
128
+ return seed;
129
+ },
130
+ takeInitialHome() {
131
+ if (!homeAvailable) return undefined;
132
+ homeAvailable = false;
133
+ return seed;
134
+ },
135
+ };
136
+ }
137
+
138
+ function createTuiRegistrationGate(): TuiRegistrationGate {
139
+ const [current, setCurrent] = createSignal<TuiRegistrationState>({ status: "pending" });
140
+
141
+ return {
142
+ current,
143
+ activate(registration, initialLoads) {
144
+ if (current().status !== "pending") return;
145
+ setCurrent({ status: "active", registration, initialLoads });
146
+ },
147
+ dispose() {
148
+ if (current().status === "disposed") return;
149
+ setCurrent({ status: "disposed" });
150
+ },
151
+ };
152
+ }
153
+
76
154
  const sessionResources = new WeakMap<TuiPluginApi, Map<string, SessionQuotaResource>>();
77
155
  const homeResources = new WeakMap<TuiPluginApi, HomeBottomResource>();
78
156
 
@@ -85,18 +163,33 @@ function getSessionResourceMap(api: TuiPluginApi): Map<string, SessionQuotaResou
85
163
  return next;
86
164
  }
87
165
 
88
- function createSessionQuotaResource(api: TuiPluginApi, sessionID: string): SessionQuotaResource {
166
+ function createSessionQuotaResource(
167
+ api: TuiPluginApi,
168
+ sessionID: string,
169
+ initialLoads?: TuiInitialLoadCoordinator,
170
+ ): SessionQuotaResource {
89
171
  const [sidebar, setSidebar] = createSignal<SidebarPanelState>({
90
172
  status: "loading",
91
173
  lines: [],
92
174
  });
93
175
  const [compact, setCompact] = createSignal<CompactStatusState>({ status: "loading" });
176
+ const [promptBar, setPromptBar] = createSignal<PromptBarState>({ status: "loading" });
94
177
 
178
+ let loadOrdinal = 0;
95
179
  const lifecycle = createTuiRefreshLifecycle({
96
- load: () => loadTuiSessionQuotaSurfaces({ api, sessionID }),
180
+ load: () => {
181
+ const initialRuntimeSeed = loadOrdinal === 0 ? initialLoads?.takeInitialSession() : undefined;
182
+ loadOrdinal += 1;
183
+ return loadTuiSessionQuotaSurfaces({
184
+ api,
185
+ sessionID,
186
+ ...(initialRuntimeSeed ? { initialRuntimeSeed } : {}),
187
+ });
188
+ },
97
189
  apply: (next) => {
98
190
  setSidebar(next.sidebar);
99
191
  setCompact(next.compact);
192
+ setPromptBar(next.promptBar ?? { status: "loading" });
100
193
  },
101
194
  intervalMs: REFRESH_INTERVAL_MS,
102
195
  eventRefreshDelaysMs: EVENT_REFRESH_DELAYS_MS,
@@ -134,6 +227,7 @@ function createSessionQuotaResource(api: TuiPluginApi, sessionID: string): Sessi
134
227
  sessionID,
135
228
  sidebar,
136
229
  compact,
230
+ promptBar,
137
231
  retain: () => {
138
232
  lifecycle.retain();
139
233
  return resource;
@@ -144,12 +238,16 @@ function createSessionQuotaResource(api: TuiPluginApi, sessionID: string): Sessi
144
238
  return resource;
145
239
  }
146
240
 
147
- function acquireSessionQuotaResource(api: TuiPluginApi, sessionID: string): SessionQuotaResource {
241
+ function acquireSessionQuotaResource(
242
+ api: TuiPluginApi,
243
+ sessionID: string,
244
+ initialLoads?: TuiInitialLoadCoordinator,
245
+ ): SessionQuotaResource {
148
246
  const resources = getSessionResourceMap(api);
149
247
  const existing = resources.get(sessionID);
150
248
  if (existing) return existing.retain();
151
249
 
152
- const next = createSessionQuotaResource(api, sessionID).retain();
250
+ const next = createSessionQuotaResource(api, sessionID, initialLoads).retain();
153
251
  resources.set(sessionID, next);
154
252
  return next;
155
253
  }
@@ -157,14 +255,23 @@ function acquireSessionQuotaResource(api: TuiPluginApi, sessionID: string): Sess
157
255
  function createHomeBottomResource(
158
256
  api: TuiPluginApi,
159
257
  compactHomeBottomEnabled: boolean,
258
+ initialLoads?: TuiInitialLoadCoordinator,
160
259
  ): HomeBottomResource {
161
260
  const [bottom, setBottom] = createSignal<HomeBottomState>({
162
261
  status: "loading",
163
262
  compact: compactHomeBottomEnabled ? { status: "loading" } : { status: "disabled" },
164
263
  });
165
264
 
265
+ let loadOrdinal = 0;
166
266
  const lifecycle = createTuiRefreshLifecycle({
167
- load: () => loadTuiHomeBottomStatus({ api }),
267
+ load: () => {
268
+ const initialRuntimeSeed = loadOrdinal === 0 ? initialLoads?.takeInitialHome() : undefined;
269
+ loadOrdinal += 1;
270
+ return loadTuiHomeBottomStatus({
271
+ api,
272
+ ...(initialRuntimeSeed ? { initialRuntimeSeed } : {}),
273
+ });
274
+ },
168
275
  apply: setBottom,
169
276
  afterApply: () => {
170
277
  // Fire-and-forget: write export file if enabled. A failed write must
@@ -201,11 +308,12 @@ function createHomeBottomResource(
201
308
  function acquireHomeBottomResource(
202
309
  api: TuiPluginApi,
203
310
  compactHomeBottomEnabled: boolean,
311
+ initialLoads?: TuiInitialLoadCoordinator,
204
312
  ): HomeBottomResource {
205
313
  const existing = homeResources.get(api);
206
314
  if (existing) return existing.retain();
207
315
 
208
- const next = createHomeBottomResource(api, compactHomeBottomEnabled).retain();
316
+ const next = createHomeBottomResource(api, compactHomeBottomEnabled, initialLoads).retain();
209
317
  homeResources.set(api, next);
210
318
  return next;
211
319
  }
@@ -213,8 +321,9 @@ function acquireHomeBottomResource(
213
321
  function useSessionQuotaResource(
214
322
  api: TuiPluginApi,
215
323
  sessionID: () => string,
324
+ initialLoads?: TuiInitialLoadCoordinator,
216
325
  ): () => SessionQuotaResource {
217
- let current = acquireSessionQuotaResource(api, sessionID());
326
+ let current = acquireSessionQuotaResource(api, sessionID(), initialLoads);
218
327
  const [resource, setResource] = createSignal(current);
219
328
 
220
329
  createEffect(() => {
@@ -222,7 +331,7 @@ function useSessionQuotaResource(
222
331
  if (current.sessionID === nextSessionID) return;
223
332
 
224
333
  const previous = current;
225
- current = acquireSessionQuotaResource(api, nextSessionID);
334
+ current = acquireSessionQuotaResource(api, nextSessionID, initialLoads);
226
335
  setResource(current);
227
336
  previous.release();
228
337
  });
@@ -234,8 +343,12 @@ function useSessionQuotaResource(
234
343
  return resource;
235
344
  }
236
345
 
237
- function SidebarContentView(props: { api: TuiPluginApi; sessionID: string }) {
238
- const resource = useSessionQuotaResource(props.api, () => props.sessionID);
346
+ function SidebarContentView(props: {
347
+ api: TuiPluginApi;
348
+ sessionID: string;
349
+ initialLoads?: TuiInitialLoadCoordinator;
350
+ }) {
351
+ const resource = useSessionQuotaResource(props.api, () => props.sessionID, props.initialLoads);
239
352
  const panel = () => resource().sidebar();
240
353
 
241
354
  const lines = () => getSidebarPanelLines(panel());
@@ -319,12 +432,13 @@ function CompactStatusLine(props: {
319
432
  function SessionPromptWithCompactStatus(props: {
320
433
  api: TuiPluginApi;
321
434
  sessionID: string;
435
+ initialLoads?: TuiInitialLoadCoordinator;
322
436
  visible?: boolean;
323
437
  disabled?: boolean;
324
438
  onSubmit?: () => void;
325
439
  promptRef?: TuiPromptRefCallback;
326
440
  }) {
327
- const resource = useSessionQuotaResource(props.api, () => props.sessionID);
441
+ const resource = useSessionQuotaResource(props.api, () => props.sessionID, props.initialLoads);
328
442
  const panel = () => resource().compact();
329
443
 
330
444
  return (
@@ -341,8 +455,176 @@ function SessionPromptWithCompactStatus(props: {
341
455
  );
342
456
  }
343
457
 
344
- function HomeBottomView(props: { api: TuiPluginApi; compactHomeBottomEnabled: boolean }) {
345
- const resource = acquireHomeBottomResource(props.api, props.compactHomeBottomEnabled);
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
+
618
+ function HomeBottomView(props: {
619
+ api: TuiPluginApi;
620
+ compactHomeBottomEnabled: boolean;
621
+ initialLoads?: TuiInitialLoadCoordinator;
622
+ }) {
623
+ const resource = acquireHomeBottomResource(
624
+ props.api,
625
+ props.compactHomeBottomEnabled,
626
+ props.initialLoads,
627
+ );
346
628
  onCleanup(() => resource.release());
347
629
 
348
630
  const announcement = () => getHomeBottomAnnouncementText(resource.bottom());
@@ -566,7 +848,7 @@ async function runQuotaDialogCommandAsync(
566
848
  }
567
849
  }
568
850
 
569
- function registerQuotaDialogCommands(api: TuiPluginApi, commandDisplay: TuiCommandDisplay): void {
851
+ function registerQuotaDialogCommands(api: TuiPluginApi, gate: TuiRegistrationGate): void {
570
852
  const commandState: QuotaDialogCommandState = {};
571
853
  const dispose = api.keymap.registerLayer({
572
854
  commands: QUOTA_DIALOG_COMMANDS.map((spec) => ({
@@ -577,7 +859,15 @@ function registerQuotaDialogCommands(api: TuiPluginApi, commandDisplay: TuiComma
577
859
  category: "OpenCode Quota",
578
860
  slashName: spec.slashName,
579
861
  run(input?: unknown) {
580
- void runQuotaDialogCommandAsync(api, spec.id, commandDisplay, input, commandState);
862
+ const state = gate.current();
863
+ if (state.status !== "active") return;
864
+ void runQuotaDialogCommandAsync(
865
+ api,
866
+ spec.id,
867
+ state.registration.commandDisplay,
868
+ input,
869
+ commandState,
870
+ );
581
871
  },
582
872
  })),
583
873
  bindings: [],
@@ -586,12 +876,75 @@ function registerQuotaDialogCommands(api: TuiPluginApi, commandDisplay: TuiComma
586
876
  api.lifecycle.onDispose(dispose);
587
877
  }
588
878
 
589
- function registerSidebarSlots(api: TuiPluginApi): void {
879
+ function registerStableTuiSlots(api: TuiPluginApi, current: () => TuiRegistrationState): void {
590
880
  api.slots.register({
591
881
  order: SIDEBAR_ORDER,
592
882
  slots: {
593
883
  sidebar_content(_ctx, props: { session_id: string }) {
594
- return <SidebarContentView api={api} sessionID={props.session_id} />;
884
+ const state = current();
885
+ if (state.status !== "active" || !state.registration.sidebar.enabled) return null;
886
+ return (
887
+ <SidebarContentView
888
+ api={api}
889
+ sessionID={props.session_id}
890
+ initialLoads={state.initialLoads}
891
+ />
892
+ );
893
+ },
894
+ },
895
+ });
896
+
897
+ api.slots.register({
898
+ order: COMPACT_ORDER,
899
+ slots: {
900
+ session_prompt(
901
+ _ctx,
902
+ props: {
903
+ session_id: string;
904
+ visible?: boolean;
905
+ disabled?: boolean;
906
+ on_submit?: () => void;
907
+ ref?: TuiPromptRefCallback;
908
+ },
909
+ ) {
910
+ const state = current();
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;
926
+ return (
927
+ <SessionPromptWithCompactStatus
928
+ api={api}
929
+ sessionID={props.session_id}
930
+ initialLoads={state.initialLoads}
931
+ visible={props.visible}
932
+ disabled={props.disabled}
933
+ onSubmit={props.on_submit}
934
+ promptRef={props.ref}
935
+ />
936
+ );
937
+ },
938
+ home_bottom() {
939
+ const state = current();
940
+ if (state.status !== "active" || !state.registration.homeBottom) return null;
941
+ return (
942
+ <HomeBottomView
943
+ api={api}
944
+ compactHomeBottomEnabled={state.registration.compact.homeBottom}
945
+ initialLoads={state.initialLoads}
946
+ />
947
+ );
595
948
  },
596
949
  },
597
950
  });
@@ -599,74 +952,43 @@ function registerSidebarSlots(api: TuiPluginApi): void {
599
952
 
600
953
  async function initializeTuiRegistration(
601
954
  api: TuiPluginApi,
602
- isDisposed: () => boolean,
955
+ gate: TuiRegistrationGate,
603
956
  ): Promise<void> {
604
- let surfaceRegistration;
957
+ let initialRuntimeSeed: TuiInitialRuntimeSeed | undefined;
958
+ let surfaceRegistration: Promise<{
959
+ registration: TuiSurfaceRegistration;
960
+ initialRuntimeSeed?: TuiInitialRuntimeSeed;
961
+ }>;
605
962
  try {
606
- surfaceRegistration = await resolveTuiSurfaceRegistration(api);
607
- } catch {
608
- if (isDisposed()) return;
609
- registerQuotaDialogCommands(api, "inline");
610
- registerSidebarSlots(api);
611
- return;
612
- }
613
-
614
- if (isDisposed()) return;
615
- registerQuotaDialogCommands(api, surfaceRegistration.commandDisplay);
616
-
617
- if (surfaceRegistration.sidebar.enabled) {
618
- registerSidebarSlots(api);
619
- }
620
-
621
- const compactRegistration = surfaceRegistration.compact;
622
- if (!compactRegistration.enabled && !surfaceRegistration.homeBottom) return;
623
-
624
- const compactSlots: Record<string, (ctx: any, props: any) => JSX.Element | null> = {};
625
-
626
- if (compactRegistration.sessionPrompt) {
627
- compactSlots.session_prompt = (
628
- _ctx,
629
- props: {
630
- session_id: string;
631
- visible?: boolean;
632
- disabled?: boolean;
633
- on_submit?: () => void;
634
- ref?: TuiPromptRefCallback;
963
+ surfaceRegistration = resolveTuiSurfaceRegistration(api, {
964
+ captureInitialRuntime(seed) {
965
+ initialRuntimeSeed = seed;
635
966
  },
636
- ) => (
637
- <SessionPromptWithCompactStatus
638
- api={api}
639
- sessionID={props.session_id}
640
- visible={props.visible}
641
- disabled={props.disabled}
642
- onSubmit={props.on_submit}
643
- promptRef={props.ref}
644
- />
645
- );
646
- }
647
-
648
- if (surfaceRegistration.homeBottom) {
649
- compactSlots.home_bottom = () => (
650
- <HomeBottomView api={api} compactHomeBottomEnabled={surfaceRegistration.compact.homeBottom} />
651
- );
967
+ })
968
+ .then((registration) => ({ registration, initialRuntimeSeed }))
969
+ .catch(() => ({ registration: FALLBACK_SURFACE_REGISTRATION }));
970
+ } catch {
971
+ surfaceRegistration = Promise.resolve({ registration: FALLBACK_SURFACE_REGISTRATION });
652
972
  }
653
973
 
654
- if (Object.keys(compactSlots).length > 0) {
655
- api.slots.register({
656
- order: COMPACT_ORDER,
657
- slots: compactSlots,
658
- });
659
- }
974
+ registerQuotaDialogCommands(api, gate);
975
+ void surfaceRegistration.then(({ registration, initialRuntimeSeed }) =>
976
+ gate.activate(
977
+ registration,
978
+ initialRuntimeSeed ? createTuiInitialLoadCoordinator(initialRuntimeSeed) : undefined,
979
+ ),
980
+ );
981
+ registerStableTuiSlots(api, gate.current);
660
982
  }
661
983
 
662
984
  const tui: TuiPlugin = async (api) => {
663
- let disposed = false;
985
+ const registrationGate = createTuiRegistrationGate();
664
986
  api.lifecycle.onDispose(() => {
665
- disposed = true;
987
+ registrationGate.dispose();
666
988
  disposeQuotaTelemetryOwner(createTuiQuotaClient(api));
667
989
  });
668
990
 
669
- void initializeTuiRegistration(api, () => disposed).catch(() => {});
991
+ void initializeTuiRegistration(api, registrationGate).catch(() => {});
670
992
  };
671
993
 
672
994
  const pluginModule: TuiPluginModule & { id: string } = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slkiser/opencode-quota",
3
- "version": "4.5.1",
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",