From e9d4e4ba1a6e87c650f13bbcc3176284a2c7d285 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Mon, 24 Aug 2026 15:29:35 +1200 Subject: [PATCH 1/3] feat: add separate v2 display toggles --- .changeset/separate-visibility-toggles.md | 5 +++++ README.md | 4 ++++ __tests__/config.test.ts | 4 ++++ __tests__/coordinator.test.ts | 2 ++ __tests__/plugin.test.tsx | 14 ++++++++++++++ __tests__/providers/index.test.ts | 2 ++ examples/usage-limits.jsonc | 2 ++ src/config-schema.ts | 2 ++ src/config.ts | 2 ++ src/coordinator.ts | 8 ++++++++ src/plugin.tsx | 23 ++++++++++++++--------- src/types.ts | 2 ++ usage-limits.schema.json | 2 ++ 13 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 .changeset/separate-visibility-toggles.md diff --git a/.changeset/separate-visibility-toggles.md b/.changeset/separate-visibility-toggles.md new file mode 100644 index 0000000..1534d80 --- /dev/null +++ b/.changeset/separate-visibility-toggles.md @@ -0,0 +1,5 @@ +--- +"oc-usage-limits-plugin": minor +--- + +Add separate `showSidebar` and `showFooter` configuration toggles for the v2 TUI displays. diff --git a/README.md b/README.md index 6ef776a..2df9566 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ Create `~/.config/opencode/usage-limits.jsonc`. The same file lives at [`example "enabled": true, "refreshIntervalSeconds": 60, "requestTimeoutMs": 10000, + "showSidebar": true, + "showFooter": true, "showErrors": true, "providers": { "codex": { @@ -169,6 +171,8 @@ Disabled providers are hidden: } ``` +Set `showSidebar` or `showFooter` to `false` to hide that usage display independently. Both default to `true`. + ## Providers | Provider ID | Service | Env var | Auth header | Default base URL | diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index d58bf9f..2c4d49e 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -30,6 +30,8 @@ describe("configuration parsing", () => { refreshIntervalSeconds: 15, requestTimeoutMs: 1000, showErrors: false, + showFooter: true, + showSidebar: false, }); expect(Result.isSuccess(result)).toBe(true); @@ -40,6 +42,8 @@ describe("configuration parsing", () => { refreshIntervalSeconds: 15, requestTimeoutMs: 1000, showErrors: false, + showFooter: true, + showSidebar: false, }); } }); diff --git a/__tests__/coordinator.test.ts b/__tests__/coordinator.test.ts index 7849073..f84240f 100644 --- a/__tests__/coordinator.test.ts +++ b/__tests__/coordinator.test.ts @@ -21,6 +21,8 @@ const config: ResolvedUsageLimitsConfig = { refreshIntervalSeconds: 15, requestTimeoutMs: 1000, showErrors: true, + showFooter: true, + showSidebar: true, }; const usage = (id: ProviderID): ProviderUsage => ({ diff --git a/__tests__/plugin.test.tsx b/__tests__/plugin.test.tsx index 825d5f0..9567f9f 100644 --- a/__tests__/plugin.test.tsx +++ b/__tests__/plugin.test.tsx @@ -59,6 +59,8 @@ const config = ( refreshIntervalSeconds: 20, requestTimeoutMs: 5000, showErrors: true, + showFooter: true, + showSidebar: true, ...overrides, }); @@ -243,6 +245,18 @@ describe("usage-limits TUI lifecycle", () => { ); }); + test.each([ + [{ showSidebar: false }, "sidebar.content", "Usage Limits"], + [{ showFooter: false }, "prompt.footer.status", "42%"], + ])("hides the configured %s display", async (overrides, slot, text) => { + const harness = createHarness(config(overrides)); + const registered = await initialize(harness); + + expect( + await renderSlot(registered, slot as keyof CharacterizedSlots) + ).not.toContain(text); + }); + test("does not render footer usage for shell mode or missing sessions", async () => { const harness = createHarness(); const registered = await initialize(harness); diff --git a/__tests__/providers/index.test.ts b/__tests__/providers/index.test.ts index 24652dd..2af444c 100644 --- a/__tests__/providers/index.test.ts +++ b/__tests__/providers/index.test.ts @@ -66,6 +66,8 @@ describe("provider manifest", () => { refreshIntervalSeconds: 60, requestTimeoutMs: 1000, showErrors: true, + showFooter: true, + showSidebar: true, }) ).toEqual([ ["codex", { enabled: true, label: "Codex" }], diff --git a/examples/usage-limits.jsonc b/examples/usage-limits.jsonc index 94fd494..fe472e1 100644 --- a/examples/usage-limits.jsonc +++ b/examples/usage-limits.jsonc @@ -3,6 +3,8 @@ "enabled": true, "refreshIntervalSeconds": 60, "requestTimeoutMs": 10000, + "showSidebar": true, + "showFooter": true, "showErrors": true, "providers": { "codex": { diff --git a/src/config-schema.ts b/src/config-schema.ts index 03f9068..de53ec7 100644 --- a/src/config-schema.ts +++ b/src/config-schema.ts @@ -83,6 +83,8 @@ const configSchema = Schema.Struct({ 10_000 ), showErrors: defaultKey(Schema.Boolean, true), + showFooter: defaultKey(Schema.Boolean, true), + showSidebar: defaultKey(Schema.Boolean, true), }); const decodeConfig = Schema.decodeUnknownResult(configSchema, { diff --git a/src/config.ts b/src/config.ts index e0fae0e..86ead7e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -31,6 +31,8 @@ export const DEFAULT_CONFIG: ResolvedUsageLimitsConfig = { refreshIntervalSeconds: 60, requestTimeoutMs: 10_000, showErrors: true, + showFooter: true, + showSidebar: true, }; const isMissingFile = (error: unknown): boolean => diff --git a/src/coordinator.ts b/src/coordinator.ts index c74fdd8..a738b0f 100644 --- a/src/coordinator.ts +++ b/src/coordinator.ts @@ -16,6 +16,8 @@ import type { export interface CoordinatorSnapshot { readonly states: readonly ProviderState[]; + readonly showSidebar: boolean; + readonly showFooter: boolean; readonly showErrors: boolean; readonly lastRefreshAt: Date | null; } @@ -74,6 +76,8 @@ export const usageCoordinator = ( yield* dependencies.publish({ lastRefreshAt: null, showErrors: config.showErrors, + showFooter: config.showFooter, + showSidebar: config.showSidebar, states: providers.map(([id, provider]) => loadingState(id, provider)), }); @@ -124,6 +128,8 @@ export const usageCoordinator = ( yield* dependencies.publish({ lastRefreshAt: now, showErrors: config.showErrors, + showFooter: config.showFooter, + showSidebar: config.showSidebar, states: terminalStates.map((state) => state.status === "ready" ? { @@ -139,6 +145,8 @@ export const usageCoordinator = ( yield* dependencies.publish({ lastRefreshAt: null, showErrors: config.showErrors, + showFooter: config.showFooter, + showSidebar: config.showSidebar, states: [], }); } diff --git a/src/plugin.tsx b/src/plugin.tsx index e8dc7fa..7642caa 100644 --- a/src/plugin.tsx +++ b/src/plugin.tsx @@ -61,23 +61,26 @@ export const createUsageLimitsPlugin = (dependencies: UsageLimitsTuiDependencies) => (context: Context): (() => void) => { const [states, setStates] = createSignal([]); + const [showSidebar, setShowSidebar] = createSignal(true); + const [showFooter, setShowFooter] = createSignal(true); const [showErrors, setShowErrors] = createSignal(true); const [lastRefreshAt, setLastRefreshAt] = createSignal(null); const disposeSidebar = context.ui.slot({ append: "sidebar.content", - render: () => ( - - ), + render: () => + showSidebar() ? ( + + ) : null, }); const disposeFooter = context.ui.slot({ append: "prompt.footer.status", render: (slot) => { - if (!slot.sessionID || slot.mode === "shell") { + if (!showFooter() || !slot.sessionID || slot.mode === "shell") { return null; } const providerID = currentProviderID( @@ -99,6 +102,8 @@ export const createUsageLimitsPlugin = now: Effect.sync(dependencies.now), publish: (snapshot) => Effect.sync(() => { + setShowSidebar(snapshot.showSidebar); + setShowFooter(snapshot.showFooter); setShowErrors(snapshot.showErrors); setStates([...snapshot.states]); setLastRefreshAt(snapshot.lastRefreshAt); diff --git a/src/types.ts b/src/types.ts index 10850dd..d28f76f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -146,6 +146,8 @@ export interface ResolvedUsageLimitsConfig { readonly providers: Readonly>; readonly refreshIntervalSeconds: number; readonly requestTimeoutMs: number; + readonly showSidebar: boolean; + readonly showFooter: boolean; readonly showErrors: boolean; } diff --git a/usage-limits.schema.json b/usage-limits.schema.json index 26d5586..6b505fe 100644 --- a/usage-limits.schema.json +++ b/usage-limits.schema.json @@ -9,6 +9,8 @@ "enabled": { "type": "boolean" }, "refreshIntervalSeconds": { "type": "number", "minimum": 15 }, "requestTimeoutMs": { "type": "number", "minimum": 1000 }, + "showSidebar": { "type": "boolean", "default": true }, + "showFooter": { "type": "boolean", "default": true }, "showErrors": { "type": "boolean" }, "providers": { "type": "object", From d5e6bedef58a4f775e3dcf6bff0a3f437df3b1a7 Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Mon, 24 Aug 2026 22:47:46 +1200 Subject: [PATCH 2/3] feat: expand v2 display selection --- .changeset/expand-display-selection.md | 5 +++++ README.md | 7 +++++- __tests__/components.test.tsx | 29 ++++++++++++++++++++++++- __tests__/config.test.ts | 6 ++++++ __tests__/coordinator.test.ts | 2 ++ __tests__/plugin.test.tsx | 15 +++++++++++++ __tests__/providers/index.test.ts | 2 ++ __tests__/session.test.ts | 24 +++++++++++++++++++++ examples/usage-limits.jsonc | 3 +++ src/components.tsx | 30 ++++++++++++++++++++------ src/config-schema.ts | 25 +++++++++++++++++++++ src/config.ts | 2 ++ src/coordinator.ts | 21 ++++++++++++++++++ src/plugin.tsx | 23 +++++++++++++++++--- src/session.ts | 24 ++++++++++++++++----- src/types.ts | 10 +++++++++ usage-limits.schema.json | 25 +++++++++++++++++++++ 17 files changed, 237 insertions(+), 16 deletions(-) create mode 100644 .changeset/expand-display-selection.md diff --git a/.changeset/expand-display-selection.md b/.changeset/expand-display-selection.md new file mode 100644 index 0000000..b9e29c5 --- /dev/null +++ b/.changeset/expand-display-selection.md @@ -0,0 +1,5 @@ +--- +"oc-usage-limits-plugin": minor +--- + +Add typed master display, sidebar window filtering, and per-provider footer window selection configuration. diff --git a/README.md b/README.md index 2df9566..47c247e 100644 --- a/README.md +++ b/README.md @@ -120,13 +120,16 @@ Create `~/.config/opencode/usage-limits.jsonc`. The same file lives at [`example "enabled": true, "refreshIntervalSeconds": 60, "requestTimeoutMs": 10000, + "show": true, "showSidebar": true, "showFooter": true, + "sidebarWindow": "all", "showErrors": true, "providers": { "codex": { "enabled": true, "label": "Codex", + "footerWindow": "auto", }, "zai": { "enabled": true, @@ -171,7 +174,9 @@ Disabled providers are hidden: } ``` -Set `showSidebar` or `showFooter` to `false` to hide that usage display independently. Both default to `true`. +`enabled` controls whether a provider is fetched and displayed. Set `show` to `false` to hide both displays without stopping provider refreshes. `showSidebar` and `showFooter` independently control their displays; all three display switches default to `true`. + +Set `sidebarWindow` to `all`, `rolling`, `daily`, `weekly`, `monthly`, `credits`, or `other` to filter every sidebar provider. Rolling includes legacy `5h` labels. Each provider accepts `footerWindow` with `auto` (the provider's normal selection), or one of the same window kinds. An unavailable requested footer window falls back to the provider's automatic selection and then its first available window. ## Providers diff --git a/__tests__/components.test.tsx b/__tests__/components.test.tsx index c201da4..94dc4b2 100644 --- a/__tests__/components.test.tsx +++ b/__tests__/components.test.tsx @@ -42,7 +42,8 @@ const usage = (overrides: Partial = {}): ProviderUsage => ({ const renderPanelText = async ( states: ProviderState[], showErrors: boolean, - lastRefreshAt: Date | null = null + lastRefreshAt: Date | null = null, + sidebarWindow: "all" | "weekly" = "all" ): Promise => { const setup = await testRender( () => ( @@ -51,6 +52,7 @@ const renderPanelText = async ( states={states} theme={theme} lastRefreshAt={lastRefreshAt} + sidebarWindow={sidebarWindow} /> ), { height: 12, width: 80 } @@ -86,6 +88,31 @@ describe("UsageLimitsPanel", () => { expect(text).toContain("[█████░░░░░░░]"); }); + test("filters windows by the global sidebar window", async () => { + const text = await renderPanelText( + [ + { + data: usage({ + windows: [ + usageWindow(), + usageWindow({ kind: "weekly", label: "weekly" }), + ], + }), + id: "codex", + label: "Codex", + stale: false, + status: "ready", + }, + ], + true, + null, + "weekly" + ); + + expect(text).toContain("weekly"); + expect(text).not.toContain("5h"); + }); + test("renders previous windows and error text when errors are visible", async () => { const text = await renderPanelText( [ diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index 2c4d49e..d81229f 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -29,9 +29,11 @@ describe("configuration parsing", () => { providers: {}, refreshIntervalSeconds: 15, requestTimeoutMs: 1000, + show: false, showErrors: false, showFooter: true, showSidebar: false, + sidebarWindow: "weekly", }); expect(Result.isSuccess(result)).toBe(true); @@ -41,9 +43,11 @@ describe("configuration parsing", () => { providers: {}, refreshIntervalSeconds: 15, requestTimeoutMs: 1000, + show: false, showErrors: false, showFooter: true, showSidebar: false, + sidebarWindow: "weekly", }); } }); @@ -57,6 +61,7 @@ describe("configuration parsing", () => { authorizationScheme: "bearer", baseUrl: "https://example.com", enabled: true, + footerWindow: "weekly", label: "Work", }, }, @@ -72,6 +77,7 @@ describe("configuration parsing", () => { authorizationScheme: "bearer", baseUrl: "https://example.com", enabled: true, + footerWindow: "weekly", label: "Work", }); } diff --git a/__tests__/coordinator.test.ts b/__tests__/coordinator.test.ts index f84240f..71d0fe0 100644 --- a/__tests__/coordinator.test.ts +++ b/__tests__/coordinator.test.ts @@ -20,9 +20,11 @@ const config: ResolvedUsageLimitsConfig = { }, refreshIntervalSeconds: 15, requestTimeoutMs: 1000, + show: true, showErrors: true, showFooter: true, showSidebar: true, + sidebarWindow: "all", }; const usage = (id: ProviderID): ProviderUsage => ({ diff --git a/__tests__/plugin.test.tsx b/__tests__/plugin.test.tsx index 9567f9f..84b45dc 100644 --- a/__tests__/plugin.test.tsx +++ b/__tests__/plugin.test.tsx @@ -58,9 +58,11 @@ const config = ( providers: { codex: { enabled: true, label: "Codex Work" } }, refreshIntervalSeconds: 20, requestTimeoutMs: 5000, + show: true, showErrors: true, showFooter: true, showSidebar: true, + sidebarWindow: "all", ...overrides, }); @@ -245,6 +247,19 @@ describe("usage-limits TUI lifecycle", () => { ); }); + test("hides both displays without stopping provider refreshes", async () => { + const harness = createHarness(config({ show: false })); + const registered = await initialize(harness); + + expect(harness.fetches).toEqual(["codex"]); + expect(await renderSlot(registered, "sidebar.content")).not.toContain( + "Usage Limits" + ); + expect(await renderSlot(registered, "prompt.footer.status")).not.toContain( + "42%" + ); + }); + test.each([ [{ showSidebar: false }, "sidebar.content", "Usage Limits"], [{ showFooter: false }, "prompt.footer.status", "42%"], diff --git a/__tests__/providers/index.test.ts b/__tests__/providers/index.test.ts index 2af444c..1c5b66e 100644 --- a/__tests__/providers/index.test.ts +++ b/__tests__/providers/index.test.ts @@ -65,9 +65,11 @@ describe("provider manifest", () => { }, refreshIntervalSeconds: 60, requestTimeoutMs: 1000, + show: true, showErrors: true, showFooter: true, showSidebar: true, + sidebarWindow: "all", }) ).toEqual([ ["codex", { enabled: true, label: "Codex" }], diff --git a/__tests__/session.test.ts b/__tests__/session.test.ts index 2a7b5ce..e198de2 100644 --- a/__tests__/session.test.ts +++ b/__tests__/session.test.ts @@ -130,6 +130,30 @@ describe("session helpers", () => { expect(Number(usage ? quotaUsedPercent(usage.quota) : null)).toBe(88); }); + test("selects a requested footer window and falls back to auto", () => { + const states: ProviderState[] = [ + { + data: { + capturedAt: new Date(), + id: "codex", + label: "Codex", + windows: [window("5h", 75), window("weekly", 88)], + }, + id: "codex", + label: "Codex", + stale: false, + status: "ready", + }, + ]; + + expect(usageForProvider(states, "openai", { codex: "weekly" })?.label).toBe( + "weekly" + ); + expect( + usageForProvider(states, "openai", { codex: "monthly" })?.label + ).toBe("5h"); + }); + test("returns null for unknown providers or unavailable data", () => { expect(usageForProvider([], "anthropic")).toBeNull(); expect( diff --git a/examples/usage-limits.jsonc b/examples/usage-limits.jsonc index fe472e1..377b323 100644 --- a/examples/usage-limits.jsonc +++ b/examples/usage-limits.jsonc @@ -3,13 +3,16 @@ "enabled": true, "refreshIntervalSeconds": 60, "requestTimeoutMs": 10000, + "show": true, "showSidebar": true, "showFooter": true, + "sidebarWindow": "all", "showErrors": true, "providers": { "codex": { "enabled": true, "label": "Codex", + "footerWindow": "auto", }, "zai": { "enabled": true, diff --git a/src/components.tsx b/src/components.tsx index 96df1b6..3c5bffe 100644 --- a/src/components.tsx +++ b/src/components.tsx @@ -10,7 +10,7 @@ import { windowResetText, windowResetTime, } from "@/format.ts"; -import type { ProviderState, UsageWindow } from "@/types.ts"; +import type { ProviderState, SidebarWindow, UsageWindow } from "@/types.ts"; import { quotaUsedPercent } from "@/usage.ts"; /** @@ -160,12 +160,30 @@ export const UsageLimitsPanel = (props: { showErrors: boolean; theme: UsageTheme; lastRefreshAt: Date | null; + sidebarWindow?: SidebarWindow; }) => { const colors = resolveTheme(props.theme); + const windowsForSidebar = (windows: readonly UsageWindow[]): UsageWindow[] => + props.sidebarWindow === undefined || props.sidebarWindow === "all" + ? [...windows] + : windows.filter( + (window) => + window.kind === props.sidebarWindow || + (props.sidebarWindow === "rolling" && window.label === "5h") + ); const visibleStates = createMemo(() => - props.states.filter((state) => - shouldRenderProviderState(state, props.showErrors) - ) + props.states.filter((state) => { + if (!shouldRenderProviderState(state, props.showErrors)) { + return false; + } + if (state.status === "ready") { + return windowsForSidebar(state.data.windows).length > 0; + } + if (state.status === "error" && state.previous) { + return windowsForSidebar(state.previous.windows).length > 0; + } + return true; + }) ); return ( @@ -210,13 +228,13 @@ export const UsageLimitsPanel = (props: { {state.status === "ready" ? ( ) : null} {state.status === "error" && state.previous ? ( ) : null} {state.status === "error" && props.showErrors ? ( diff --git a/src/config-schema.ts b/src/config-schema.ts index de53ec7..e1c4572 100644 --- a/src/config-schema.ts +++ b/src/config-schema.ts @@ -14,6 +14,18 @@ const secret = Schema.RedactedFromValue(Schema.String, { const commonProviderFields = { enabled: Schema.optionalKey(Schema.Boolean), + footerWindow: defaultKey( + Schema.Literals([ + "auto", + "rolling", + "daily", + "weekly", + "monthly", + "credits", + "other", + ]), + "auto" + ), label: Schema.optionalKey(Schema.String), }; @@ -82,9 +94,22 @@ const configSchema = Schema.Struct({ Schema.Finite.check(Schema.isGreaterThanOrEqualTo(1000)), 10_000 ), + show: defaultKey(Schema.Boolean, true), showErrors: defaultKey(Schema.Boolean, true), showFooter: defaultKey(Schema.Boolean, true), showSidebar: defaultKey(Schema.Boolean, true), + sidebarWindow: defaultKey( + Schema.Literals([ + "all", + "rolling", + "daily", + "weekly", + "monthly", + "credits", + "other", + ]), + "all" + ), }); const decodeConfig = Schema.decodeUnknownResult(configSchema, { diff --git a/src/config.ts b/src/config.ts index 86ead7e..a32231f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,9 +30,11 @@ export const DEFAULT_CONFIG: ResolvedUsageLimitsConfig = { providers: {}, refreshIntervalSeconds: 60, requestTimeoutMs: 10_000, + show: true, showErrors: true, showFooter: true, showSidebar: true, + sidebarWindow: "all", }; const isMissingFile = (error: unknown): boolean => diff --git a/src/coordinator.ts b/src/coordinator.ts index a738b0f..17b0d7f 100644 --- a/src/coordinator.ts +++ b/src/coordinator.ts @@ -8,16 +8,21 @@ import { defaultLabelFor } from "@/providers/index.ts"; import type { OpenCodeAuth, ProviderConfigMap, + FooterWindow, ProviderID, ProviderState, ProviderUsage, ResolvedUsageLimitsConfig, + SidebarWindow, } from "@/types.ts"; export interface CoordinatorSnapshot { readonly states: readonly ProviderState[]; + readonly footerWindows: Readonly>>; + readonly show: boolean; readonly showSidebar: boolean; readonly showFooter: boolean; + readonly sidebarWindow: SidebarWindow; readonly showErrors: boolean; readonly lastRefreshAt: Date | null; } @@ -56,6 +61,13 @@ const loadingState = ( status: "loading", }); +const footerWindowsFor = ( + providers: readonly (readonly [ProviderID, ProviderConfigMap[ProviderID]])[] +): Partial> => + Object.fromEntries( + providers.map(([id, provider]) => [id, provider.footerWindow ?? "auto"]) + ); + export const usageCoordinator = ( dependencies: UsageCoordinatorDependencies ): Effect.Effect => @@ -74,10 +86,13 @@ export const usageCoordinator = ( intervalMs = intervalMilliseconds(config.refreshIntervalSeconds); const providers = config.enabled ? getProviderConfigs(config) : []; yield* dependencies.publish({ + footerWindows: footerWindowsFor(providers), lastRefreshAt: null, + show: config.show, showErrors: config.showErrors, showFooter: config.showFooter, showSidebar: config.showSidebar, + sidebarWindow: config.sidebarWindow, states: providers.map(([id, provider]) => loadingState(id, provider)), }); @@ -126,10 +141,13 @@ export const usageCoordinator = ( const now = yield* dependencies.now; const staleAfterMs = intervalMs * 2; yield* dependencies.publish({ + footerWindows: footerWindowsFor(providers), lastRefreshAt: now, + show: config.show, showErrors: config.showErrors, showFooter: config.showFooter, showSidebar: config.showSidebar, + sidebarWindow: config.sidebarWindow, states: terminalStates.map((state) => state.status === "ready" ? { @@ -143,10 +161,13 @@ export const usageCoordinator = ( }); } else { yield* dependencies.publish({ + footerWindows: {}, lastRefreshAt: null, + show: config.show, showErrors: config.showErrors, showFooter: config.showFooter, showSidebar: config.showSidebar, + sidebarWindow: config.sidebarWindow, states: [], }); } diff --git a/src/plugin.tsx b/src/plugin.tsx index 7642caa..f347810 100644 --- a/src/plugin.tsx +++ b/src/plugin.tsx @@ -11,11 +11,13 @@ import { fetchProviderEffect } from "@/providers.ts"; import { ProviderRuntimeLive } from "@/providers/runtime/index.ts"; import { currentProviderID, usageForProvider } from "@/session.ts"; import type { + FooterWindow, ProviderID, OpenCodeAuth, ProviderConfigMap, ProviderState, ProviderUsage, + SidebarWindow, } from "@/types.ts"; /** Runtime dependencies used by the usage-limits TUI lifecycle. */ @@ -61,26 +63,38 @@ export const createUsageLimitsPlugin = (dependencies: UsageLimitsTuiDependencies) => (context: Context): (() => void) => { const [states, setStates] = createSignal([]); + const [show, setShow] = createSignal(true); const [showSidebar, setShowSidebar] = createSignal(true); const [showFooter, setShowFooter] = createSignal(true); + const [sidebarWindow, setSidebarWindow] = + createSignal("all"); + const [footerWindows, setFooterWindows] = createSignal< + Readonly>> + >({}); const [showErrors, setShowErrors] = createSignal(true); const [lastRefreshAt, setLastRefreshAt] = createSignal(null); const disposeSidebar = context.ui.slot({ append: "sidebar.content", render: () => - showSidebar() ? ( + show() && showSidebar() ? ( ) : null, }); const disposeFooter = context.ui.slot({ append: "prompt.footer.status", render: (slot) => { - if (!showFooter() || !slot.sessionID || slot.mode === "shell") { + if ( + !show() || + !showFooter() || + !slot.sessionID || + slot.mode === "shell" + ) { return null; } const providerID = currentProviderID( @@ -89,7 +103,7 @@ export const createUsageLimitsPlugin = return ( ); }, @@ -102,8 +116,11 @@ export const createUsageLimitsPlugin = now: Effect.sync(dependencies.now), publish: (snapshot) => Effect.sync(() => { + setShow(snapshot.show); + setFooterWindows(snapshot.footerWindows); setShowSidebar(snapshot.showSidebar); setShowFooter(snapshot.showFooter); + setSidebarWindow(snapshot.sidebarWindow); setShowErrors(snapshot.showErrors); setStates([...snapshot.states]); setLastRefreshAt(snapshot.lastRefreshAt); diff --git a/src/session.ts b/src/session.ts index 54e60d7..9c94c60 100644 --- a/src/session.ts +++ b/src/session.ts @@ -3,11 +3,13 @@ import { PROVIDER_REGISTRY, } from "@/providers/index.ts"; import type { + FooterWindow, ProviderID, ProviderState, ProviderUsage, UsageWindow, } from "@/types.ts"; +import type { UsageWindowKind } from "@/usage.ts"; import { isRecord } from "@/utils.ts"; /** @@ -104,7 +106,8 @@ const windowFromState = ( */ export const usageForProvider = ( states: readonly ProviderState[], - providerID: string | undefined + providerID: string | undefined, + footerWindows: Readonly>> = {} ): UsageWindow | null => { const usageID = providerID ? (pluginProviderForOpenCode(providerID) as ProviderID | null) @@ -116,12 +119,23 @@ export const usageForProvider = ( if (!data) { return null; } + const requestedWindow = footerWindows[id] ?? "auto"; const footerWindowKind = PROVIDER_REGISTRY[id]?.footerWindowKind; - const legacyLabel = - footerWindowKind === "rolling" ? "5h" : footerWindowKind; + const findForKind = (kind: UsageWindowKind | undefined) => { + if (!kind) { + return; + } + return ( + (kind === "rolling" + ? data.windows.find((window) => window.label === "5h") + : undefined) ?? data.windows.find((window) => window.kind === kind) + ); + }; + const requestedKind = + requestedWindow === "auto" ? footerWindowKind : requestedWindow; return ( - data.windows.find((window) => window.label === legacyLabel) ?? - data.windows.find((window) => window.kind === footerWindowKind) ?? + findForKind(requestedKind) ?? + (requestedWindow === "auto" ? null : findForKind(footerWindowKind)) ?? data.windows[0] ?? null ); diff --git a/src/types.ts b/src/types.ts index d28f76f..f588c04 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,12 @@ import type { Redacted } from "effect"; import type { ResetInstant, UsageQuota, UsageWindowKind } from "@/usage.ts"; +/** Requested sidebar window filter, or all available windows. */ +export type SidebarWindow = "all" | UsageWindowKind; + +/** Requested provider footer window, or the provider's automatic choice. */ +export type FooterWindow = "auto" | UsageWindowKind; + /** Provider adapters supported by the usage-limits plugin. */ export type ProviderID = | "codex" @@ -84,6 +90,8 @@ interface CommonProviderConfig { readonly enabled?: boolean; /** Optional provider display label override. */ readonly label?: string; + /** Preferred usage window for this provider's prompt footer. */ + readonly footerWindow?: FooterWindow; } /** Codex provider configuration. */ @@ -146,8 +154,10 @@ export interface ResolvedUsageLimitsConfig { readonly providers: Readonly>; readonly refreshIntervalSeconds: number; readonly requestTimeoutMs: number; + readonly show: boolean; readonly showSidebar: boolean; readonly showFooter: boolean; + readonly sidebarWindow: SidebarWindow; readonly showErrors: boolean; } diff --git a/usage-limits.schema.json b/usage-limits.schema.json index 6b505fe..2e2f0a9 100644 --- a/usage-limits.schema.json +++ b/usage-limits.schema.json @@ -9,8 +9,21 @@ "enabled": { "type": "boolean" }, "refreshIntervalSeconds": { "type": "number", "minimum": 15 }, "requestTimeoutMs": { "type": "number", "minimum": 1000 }, + "show": { "type": "boolean", "default": true }, "showSidebar": { "type": "boolean", "default": true }, "showFooter": { "type": "boolean", "default": true }, + "sidebarWindow": { + "enum": [ + "all", + "rolling", + "daily", + "weekly", + "monthly", + "credits", + "other" + ], + "default": "all" + }, "showErrors": { "type": "boolean" }, "providers": { "type": "object", @@ -31,6 +44,18 @@ "additionalProperties": false, "properties": { "enabled": { "type": "boolean" }, + "footerWindow": { + "enum": [ + "auto", + "rolling", + "daily", + "weekly", + "monthly", + "credits", + "other" + ], + "default": "auto" + }, "label": { "type": "string" }, "authPath": { "type": "string", From af75e0282b8d6b753c64fede90e23762192dff0d Mon Sep 17 00:00:00 2001 From: My Name is Tito Date: Mon, 24 Aug 2026 23:05:32 +1200 Subject: [PATCH 3/3] feat: move display settings to providers --- .changeset/provider-display-bars.md | 5 ++++ README.md | 11 ++++--- __tests__/components.test.tsx | 24 +++++++++++---- __tests__/config.test.ts | 14 ++++----- __tests__/coordinator.test.ts | 4 --- __tests__/plugin.test.tsx | 28 +++++++++++++----- __tests__/providers/index.test.ts | 4 --- __tests__/session.test.ts | 22 +++++++++++--- examples/usage-limits.jsonc | 7 ++--- src/components.tsx | 42 +++++++++++++++++++------- src/config-schema.ts | 29 +++++++++--------- src/config.ts | 4 --- src/coordinator.ts | 43 +++++++++++---------------- src/plugin.tsx | 46 ++++++++++------------------- src/session.ts | 13 ++++++-- src/types.ts | 15 +++++++--- usage-limits.schema.json | 29 +++++++++--------- 17 files changed, 191 insertions(+), 149 deletions(-) create mode 100644 .changeset/provider-display-bars.md diff --git a/.changeset/provider-display-bars.md b/.changeset/provider-display-bars.md new file mode 100644 index 0000000..ab95f8b --- /dev/null +++ b/.changeset/provider-display-bars.md @@ -0,0 +1,5 @@ +--- +"oc-usage-limits-plugin": minor +--- + +Move display visibility and window selection to typed provider-level configuration fields. diff --git a/README.md b/README.md index 47c247e..4a2cd03 100644 --- a/README.md +++ b/README.md @@ -120,15 +120,14 @@ Create `~/.config/opencode/usage-limits.jsonc`. The same file lives at [`example "enabled": true, "refreshIntervalSeconds": 60, "requestTimeoutMs": 10000, - "show": true, - "showSidebar": true, - "showFooter": true, - "sidebarWindow": "all", "showErrors": true, "providers": { "codex": { "enabled": true, "label": "Codex", + "showSidebarBar": true, + "showFooterBar": true, + "sidebarWindow": "all", "footerWindow": "auto", }, "zai": { @@ -174,9 +173,9 @@ Disabled providers are hidden: } ``` -`enabled` controls whether a provider is fetched and displayed. Set `show` to `false` to hide both displays without stopping provider refreshes. `showSidebar` and `showFooter` independently control their displays; all three display switches default to `true`. +Top-level `enabled` is the plugin master switch, and `showErrors` controls error text globally. Each provider's `enabled` controls fetching. Provider `showSidebarBar` and `showFooterBar` independently control its sidebar and footer displays without stopping refreshes; both default to `true`. -Set `sidebarWindow` to `all`, `rolling`, `daily`, `weekly`, `monthly`, `credits`, or `other` to filter every sidebar provider. Rolling includes legacy `5h` labels. Each provider accepts `footerWindow` with `auto` (the provider's normal selection), or one of the same window kinds. An unavailable requested footer window falls back to the provider's automatic selection and then its first available window. +Each provider's `sidebarWindow` can be `all`, `rolling`, `daily`, `weekly`, `monthly`, `credits`, or `other`. Rolling includes legacy `5h` labels. Each provider accepts `footerWindow` with `auto` (the provider's normal selection), or one of the same window kinds. An unavailable requested footer window falls back to the provider's automatic selection and then its first available window. ## Providers diff --git a/__tests__/components.test.tsx b/__tests__/components.test.tsx index 94dc4b2..68c090f 100644 --- a/__tests__/components.test.tsx +++ b/__tests__/components.test.tsx @@ -6,7 +6,12 @@ import { testRender } from "@opentui/solid"; import { Result } from "effect"; import { CompactStatusLine, UsageLimitsPanel } from "@/components.tsx"; -import type { ProviderState, ProviderUsage, UsageWindow } from "@/types.ts"; +import type { + ProviderDisplayConfig, + ProviderState, + ProviderUsage, + UsageWindow, +} from "@/types.ts"; import { parseUsagePercentage, percentageQuota } from "@/usage.ts"; const color = RGBA.fromValues(1, 2, 3, 255); @@ -43,7 +48,9 @@ const renderPanelText = async ( states: ProviderState[], showErrors: boolean, lastRefreshAt: Date | null = null, - sidebarWindow: "all" | "weekly" = "all" + providerDisplays: Readonly< + Partial> + > = {} ): Promise => { const setup = await testRender( () => ( @@ -52,7 +59,7 @@ const renderPanelText = async ( states={states} theme={theme} lastRefreshAt={lastRefreshAt} - sidebarWindow={sidebarWindow} + providerDisplays={providerDisplays} /> ), { height: 12, width: 80 } @@ -88,7 +95,7 @@ describe("UsageLimitsPanel", () => { expect(text).toContain("[█████░░░░░░░]"); }); - test("filters windows by the global sidebar window", async () => { + test("filters windows by the provider sidebar window", async () => { const text = await renderPanelText( [ { @@ -106,7 +113,14 @@ describe("UsageLimitsPanel", () => { ], true, null, - "weekly" + { + codex: { + footerWindow: "auto", + showFooterBar: true, + showSidebarBar: true, + sidebarWindow: "weekly", + }, + } ); expect(text).toContain("weekly"); diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index d81229f..213c198 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -29,11 +29,7 @@ describe("configuration parsing", () => { providers: {}, refreshIntervalSeconds: 15, requestTimeoutMs: 1000, - show: false, showErrors: false, - showFooter: true, - showSidebar: false, - sidebarWindow: "weekly", }); expect(Result.isSuccess(result)).toBe(true); @@ -43,11 +39,7 @@ describe("configuration parsing", () => { providers: {}, refreshIntervalSeconds: 15, requestTimeoutMs: 1000, - show: false, showErrors: false, - showFooter: true, - showSidebar: false, - sidebarWindow: "weekly", }); } }); @@ -63,6 +55,9 @@ describe("configuration parsing", () => { enabled: true, footerWindow: "weekly", label: "Work", + showFooterBar: false, + showSidebarBar: true, + sidebarWindow: "weekly", }, }, }); @@ -79,6 +74,9 @@ describe("configuration parsing", () => { enabled: true, footerWindow: "weekly", label: "Work", + showFooterBar: false, + showSidebarBar: true, + sidebarWindow: "weekly", }); } }); diff --git a/__tests__/coordinator.test.ts b/__tests__/coordinator.test.ts index 71d0fe0..7849073 100644 --- a/__tests__/coordinator.test.ts +++ b/__tests__/coordinator.test.ts @@ -20,11 +20,7 @@ const config: ResolvedUsageLimitsConfig = { }, refreshIntervalSeconds: 15, requestTimeoutMs: 1000, - show: true, showErrors: true, - showFooter: true, - showSidebar: true, - sidebarWindow: "all", }; const usage = (id: ProviderID): ProviderUsage => ({ diff --git a/__tests__/plugin.test.tsx b/__tests__/plugin.test.tsx index 84b45dc..791f8fa 100644 --- a/__tests__/plugin.test.tsx +++ b/__tests__/plugin.test.tsx @@ -58,11 +58,7 @@ const config = ( providers: { codex: { enabled: true, label: "Codex Work" } }, refreshIntervalSeconds: 20, requestTimeoutMs: 5000, - show: true, showErrors: true, - showFooter: true, - showSidebar: true, - sidebarWindow: "all", ...overrides, }); @@ -248,7 +244,17 @@ describe("usage-limits TUI lifecycle", () => { }); test("hides both displays without stopping provider refreshes", async () => { - const harness = createHarness(config({ show: false })); + const harness = createHarness( + config({ + providers: { + codex: { + enabled: true, + showFooterBar: false, + showSidebarBar: false, + }, + }, + }) + ); const registered = await initialize(harness); expect(harness.fetches).toEqual(["codex"]); @@ -261,8 +267,16 @@ describe("usage-limits TUI lifecycle", () => { }); test.each([ - [{ showSidebar: false }, "sidebar.content", "Usage Limits"], - [{ showFooter: false }, "prompt.footer.status", "42%"], + [ + { providers: { codex: { enabled: true, showSidebarBar: false } } }, + "sidebar.content", + "Usage Limits", + ], + [ + { providers: { codex: { enabled: true, showFooterBar: false } } }, + "prompt.footer.status", + "42%", + ], ])("hides the configured %s display", async (overrides, slot, text) => { const harness = createHarness(config(overrides)); const registered = await initialize(harness); diff --git a/__tests__/providers/index.test.ts b/__tests__/providers/index.test.ts index 1c5b66e..24652dd 100644 --- a/__tests__/providers/index.test.ts +++ b/__tests__/providers/index.test.ts @@ -65,11 +65,7 @@ describe("provider manifest", () => { }, refreshIntervalSeconds: 60, requestTimeoutMs: 1000, - show: true, showErrors: true, - showFooter: true, - showSidebar: true, - sidebarWindow: "all", }) ).toEqual([ ["codex", { enabled: true, label: "Codex" }], diff --git a/__tests__/session.test.ts b/__tests__/session.test.ts index e198de2..d6ed4ed 100644 --- a/__tests__/session.test.ts +++ b/__tests__/session.test.ts @@ -146,11 +146,25 @@ describe("session helpers", () => { }, ]; - expect(usageForProvider(states, "openai", { codex: "weekly" })?.label).toBe( - "weekly" - ); expect( - usageForProvider(states, "openai", { codex: "monthly" })?.label + usageForProvider(states, "openai", { + codex: { + footerWindow: "weekly", + showFooterBar: true, + showSidebarBar: true, + sidebarWindow: "all", + }, + })?.label + ).toBe("weekly"); + expect( + usageForProvider(states, "openai", { + codex: { + footerWindow: "monthly", + showFooterBar: true, + showSidebarBar: true, + sidebarWindow: "all", + }, + })?.label ).toBe("5h"); }); diff --git a/examples/usage-limits.jsonc b/examples/usage-limits.jsonc index 377b323..7ecd21f 100644 --- a/examples/usage-limits.jsonc +++ b/examples/usage-limits.jsonc @@ -3,15 +3,14 @@ "enabled": true, "refreshIntervalSeconds": 60, "requestTimeoutMs": 10000, - "show": true, - "showSidebar": true, - "showFooter": true, - "sidebarWindow": "all", "showErrors": true, "providers": { "codex": { "enabled": true, "label": "Codex", + "showSidebarBar": true, + "showFooterBar": true, + "sidebarWindow": "all", "footerWindow": "auto", }, "zai": { diff --git a/src/components.tsx b/src/components.tsx index 3c5bffe..ae6168a 100644 --- a/src/components.tsx +++ b/src/components.tsx @@ -10,7 +10,12 @@ import { windowResetText, windowResetTime, } from "@/format.ts"; -import type { ProviderState, SidebarWindow, UsageWindow } from "@/types.ts"; +import type { + ProviderDisplayConfig, + ProviderState, + SidebarWindow, + UsageWindow, +} from "@/types.ts"; import { quotaUsedPercent } from "@/usage.ts"; /** @@ -160,27 +165,44 @@ export const UsageLimitsPanel = (props: { showErrors: boolean; theme: UsageTheme; lastRefreshAt: Date | null; - sidebarWindow?: SidebarWindow; + providerDisplays: Readonly< + Partial> + >; }) => { const colors = resolveTheme(props.theme); - const windowsForSidebar = (windows: readonly UsageWindow[]): UsageWindow[] => - props.sidebarWindow === undefined || props.sidebarWindow === "all" + const displayConfigFor = (state: ProviderState): ProviderDisplayConfig => + props.providerDisplays[state.id] ?? { + footerWindow: "auto", + showFooterBar: true, + showSidebarBar: true, + sidebarWindow: "all", + }; + const filteredWindowsFor = ( + state: ProviderState, + windows: readonly UsageWindow[] + ): UsageWindow[] => { + const sidebarWindow: SidebarWindow = displayConfigFor(state).sidebarWindow; + return sidebarWindow === "all" ? [...windows] : windows.filter( (window) => - window.kind === props.sidebarWindow || - (props.sidebarWindow === "rolling" && window.label === "5h") + window.kind === sidebarWindow || + (sidebarWindow === "rolling" && window.label === "5h") ); + }; const visibleStates = createMemo(() => props.states.filter((state) => { if (!shouldRenderProviderState(state, props.showErrors)) { return false; } + if (!displayConfigFor(state).showSidebarBar) { + return false; + } if (state.status === "ready") { - return windowsForSidebar(state.data.windows).length > 0; + return filteredWindowsFor(state, state.data.windows).length > 0; } if (state.status === "error" && state.previous) { - return windowsForSidebar(state.previous.windows).length > 0; + return filteredWindowsFor(state, state.previous.windows).length > 0; } return true; }) @@ -228,13 +250,13 @@ export const UsageLimitsPanel = (props: { {state.status === "ready" ? ( ) : null} {state.status === "error" && state.previous ? ( ) : null} {state.status === "error" && props.showErrors ? ( diff --git a/src/config-schema.ts b/src/config-schema.ts index e1c4572..d72c685 100644 --- a/src/config-schema.ts +++ b/src/config-schema.ts @@ -27,6 +27,20 @@ const commonProviderFields = { "auto" ), label: Schema.optionalKey(Schema.String), + showFooterBar: defaultKey(Schema.Boolean, true), + showSidebarBar: defaultKey(Schema.Boolean, true), + sidebarWindow: defaultKey( + Schema.Literals([ + "all", + "rolling", + "daily", + "weekly", + "monthly", + "credits", + "other", + ]), + "all" + ), }; /** Schema for Codex provider configuration. */ @@ -94,22 +108,7 @@ const configSchema = Schema.Struct({ Schema.Finite.check(Schema.isGreaterThanOrEqualTo(1000)), 10_000 ), - show: defaultKey(Schema.Boolean, true), showErrors: defaultKey(Schema.Boolean, true), - showFooter: defaultKey(Schema.Boolean, true), - showSidebar: defaultKey(Schema.Boolean, true), - sidebarWindow: defaultKey( - Schema.Literals([ - "all", - "rolling", - "daily", - "weekly", - "monthly", - "credits", - "other", - ]), - "all" - ), }); const decodeConfig = Schema.decodeUnknownResult(configSchema, { diff --git a/src/config.ts b/src/config.ts index a32231f..e0fae0e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,11 +30,7 @@ export const DEFAULT_CONFIG: ResolvedUsageLimitsConfig = { providers: {}, refreshIntervalSeconds: 60, requestTimeoutMs: 10_000, - show: true, showErrors: true, - showFooter: true, - showSidebar: true, - sidebarWindow: "all", }; const isMissingFile = (error: unknown): boolean => diff --git a/src/coordinator.ts b/src/coordinator.ts index 17b0d7f..d8a8606 100644 --- a/src/coordinator.ts +++ b/src/coordinator.ts @@ -8,21 +8,18 @@ import { defaultLabelFor } from "@/providers/index.ts"; import type { OpenCodeAuth, ProviderConfigMap, - FooterWindow, + ProviderDisplayConfig, ProviderID, ProviderState, ProviderUsage, ResolvedUsageLimitsConfig, - SidebarWindow, } from "@/types.ts"; export interface CoordinatorSnapshot { readonly states: readonly ProviderState[]; - readonly footerWindows: Readonly>>; - readonly show: boolean; - readonly showSidebar: boolean; - readonly showFooter: boolean; - readonly sidebarWindow: SidebarWindow; + readonly providerDisplays: Readonly< + Partial> + >; readonly showErrors: boolean; readonly lastRefreshAt: Date | null; } @@ -61,11 +58,19 @@ const loadingState = ( status: "loading", }); -const footerWindowsFor = ( +const providerDisplaysFor = ( providers: readonly (readonly [ProviderID, ProviderConfigMap[ProviderID]])[] -): Partial> => +): Partial> => Object.fromEntries( - providers.map(([id, provider]) => [id, provider.footerWindow ?? "auto"]) + providers.map(([id, provider]) => [ + id, + { + footerWindow: provider.footerWindow ?? "auto", + showFooterBar: provider.showFooterBar ?? true, + showSidebarBar: provider.showSidebarBar ?? true, + sidebarWindow: provider.sidebarWindow ?? "all", + }, + ]) ); export const usageCoordinator = ( @@ -86,13 +91,9 @@ export const usageCoordinator = ( intervalMs = intervalMilliseconds(config.refreshIntervalSeconds); const providers = config.enabled ? getProviderConfigs(config) : []; yield* dependencies.publish({ - footerWindows: footerWindowsFor(providers), lastRefreshAt: null, - show: config.show, + providerDisplays: providerDisplaysFor(providers), showErrors: config.showErrors, - showFooter: config.showFooter, - showSidebar: config.showSidebar, - sidebarWindow: config.sidebarWindow, states: providers.map(([id, provider]) => loadingState(id, provider)), }); @@ -141,13 +142,9 @@ export const usageCoordinator = ( const now = yield* dependencies.now; const staleAfterMs = intervalMs * 2; yield* dependencies.publish({ - footerWindows: footerWindowsFor(providers), lastRefreshAt: now, - show: config.show, + providerDisplays: providerDisplaysFor(providers), showErrors: config.showErrors, - showFooter: config.showFooter, - showSidebar: config.showSidebar, - sidebarWindow: config.sidebarWindow, states: terminalStates.map((state) => state.status === "ready" ? { @@ -161,13 +158,9 @@ export const usageCoordinator = ( }); } else { yield* dependencies.publish({ - footerWindows: {}, lastRefreshAt: null, - show: config.show, + providerDisplays: {}, showErrors: config.showErrors, - showFooter: config.showFooter, - showSidebar: config.showSidebar, - sidebarWindow: config.sidebarWindow, states: [], }); } diff --git a/src/plugin.tsx b/src/plugin.tsx index f347810..f9145b1 100644 --- a/src/plugin.tsx +++ b/src/plugin.tsx @@ -11,13 +11,12 @@ import { fetchProviderEffect } from "@/providers.ts"; import { ProviderRuntimeLive } from "@/providers/runtime/index.ts"; import { currentProviderID, usageForProvider } from "@/session.ts"; import type { - FooterWindow, ProviderID, + ProviderDisplayConfig, OpenCodeAuth, ProviderConfigMap, ProviderState, ProviderUsage, - SidebarWindow, } from "@/types.ts"; /** Runtime dependencies used by the usage-limits TUI lifecycle. */ @@ -63,38 +62,27 @@ export const createUsageLimitsPlugin = (dependencies: UsageLimitsTuiDependencies) => (context: Context): (() => void) => { const [states, setStates] = createSignal([]); - const [show, setShow] = createSignal(true); - const [showSidebar, setShowSidebar] = createSignal(true); - const [showFooter, setShowFooter] = createSignal(true); - const [sidebarWindow, setSidebarWindow] = - createSignal("all"); - const [footerWindows, setFooterWindows] = createSignal< - Readonly>> + const [providerDisplays, setProviderDisplays] = createSignal< + Readonly>> >({}); const [showErrors, setShowErrors] = createSignal(true); const [lastRefreshAt, setLastRefreshAt] = createSignal(null); const disposeSidebar = context.ui.slot({ append: "sidebar.content", - render: () => - show() && showSidebar() ? ( - - ) : null, + render: () => ( + + ), }); const disposeFooter = context.ui.slot({ append: "prompt.footer.status", render: (slot) => { - if ( - !show() || - !showFooter() || - !slot.sessionID || - slot.mode === "shell" - ) { + if (!slot.sessionID || slot.mode === "shell") { return null; } const providerID = currentProviderID( @@ -103,7 +91,7 @@ export const createUsageLimitsPlugin = return ( ); }, @@ -116,11 +104,7 @@ export const createUsageLimitsPlugin = now: Effect.sync(dependencies.now), publish: (snapshot) => Effect.sync(() => { - setShow(snapshot.show); - setFooterWindows(snapshot.footerWindows); - setShowSidebar(snapshot.showSidebar); - setShowFooter(snapshot.showFooter); - setSidebarWindow(snapshot.sidebarWindow); + setProviderDisplays(snapshot.providerDisplays); setShowErrors(snapshot.showErrors); setStates([...snapshot.states]); setLastRefreshAt(snapshot.lastRefreshAt); diff --git a/src/session.ts b/src/session.ts index 9c94c60..d3e2f9a 100644 --- a/src/session.ts +++ b/src/session.ts @@ -3,8 +3,9 @@ import { PROVIDER_REGISTRY, } from "@/providers/index.ts"; import type { - FooterWindow, + ProviderDisplayConfig, ProviderID, + FooterWindow, ProviderState, ProviderUsage, UsageWindow, @@ -107,7 +108,9 @@ const windowFromState = ( export const usageForProvider = ( states: readonly ProviderState[], providerID: string | undefined, - footerWindows: Readonly>> = {} + providerDisplays: Readonly< + Partial> + > = {} ): UsageWindow | null => { const usageID = providerID ? (pluginProviderForOpenCode(providerID) as ProviderID | null) @@ -119,7 +122,11 @@ export const usageForProvider = ( if (!data) { return null; } - const requestedWindow = footerWindows[id] ?? "auto"; + const displayConfig = providerDisplays[id]; + if (displayConfig?.showFooterBar === false) { + return null; + } + const requestedWindow: FooterWindow = displayConfig?.footerWindow ?? "auto"; const footerWindowKind = PROVIDER_REGISTRY[id]?.footerWindowKind; const findForKind = (kind: UsageWindowKind | undefined) => { if (!kind) { diff --git a/src/types.ts b/src/types.ts index f588c04..84f8d86 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,6 +8,14 @@ export type SidebarWindow = "all" | UsageWindowKind; /** Requested provider footer window, or the provider's automatic choice. */ export type FooterWindow = "auto" | UsageWindowKind; +/** Resolved display settings for one provider. */ +export interface ProviderDisplayConfig { + readonly showSidebarBar: boolean; + readonly showFooterBar: boolean; + readonly sidebarWindow: SidebarWindow; + readonly footerWindow: FooterWindow; +} + /** Provider adapters supported by the usage-limits plugin. */ export type ProviderID = | "codex" @@ -90,6 +98,9 @@ interface CommonProviderConfig { readonly enabled?: boolean; /** Optional provider display label override. */ readonly label?: string; + readonly showSidebarBar?: boolean; + readonly showFooterBar?: boolean; + readonly sidebarWindow?: SidebarWindow; /** Preferred usage window for this provider's prompt footer. */ readonly footerWindow?: FooterWindow; } @@ -154,10 +165,6 @@ export interface ResolvedUsageLimitsConfig { readonly providers: Readonly>; readonly refreshIntervalSeconds: number; readonly requestTimeoutMs: number; - readonly show: boolean; - readonly showSidebar: boolean; - readonly showFooter: boolean; - readonly sidebarWindow: SidebarWindow; readonly showErrors: boolean; } diff --git a/usage-limits.schema.json b/usage-limits.schema.json index 2e2f0a9..68e2b9b 100644 --- a/usage-limits.schema.json +++ b/usage-limits.schema.json @@ -9,21 +9,6 @@ "enabled": { "type": "boolean" }, "refreshIntervalSeconds": { "type": "number", "minimum": 15 }, "requestTimeoutMs": { "type": "number", "minimum": 1000 }, - "show": { "type": "boolean", "default": true }, - "showSidebar": { "type": "boolean", "default": true }, - "showFooter": { "type": "boolean", "default": true }, - "sidebarWindow": { - "enum": [ - "all", - "rolling", - "daily", - "weekly", - "monthly", - "credits", - "other" - ], - "default": "all" - }, "showErrors": { "type": "boolean" }, "providers": { "type": "object", @@ -44,6 +29,20 @@ "additionalProperties": false, "properties": { "enabled": { "type": "boolean" }, + "showSidebarBar": { "type": "boolean", "default": true }, + "showFooterBar": { "type": "boolean", "default": true }, + "sidebarWindow": { + "enum": [ + "all", + "rolling", + "daily", + "weekly", + "monthly", + "credits", + "other" + ], + "default": "all" + }, "footerWindow": { "enum": [ "auto",