From 05459a641690d420e9d483747c795363a9ba1f3c Mon Sep 17 00:00:00 2001 From: akasai Date: Thu, 30 Apr 2026 16:01:05 +0900 Subject: [PATCH 1/2] Add bar display mode and collapsible sidebar toggle --- README.md | 14 ++++++- src/format.ts | 23 +++++++++++ src/tui.tsx | 105 +++++++++++++++++++++++++++++++------------------- src/types.ts | 3 ++ 4 files changed, 104 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index c71390a..8f6df36 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,24 @@ An [opencode](https://opencode.ai) TUI sidebar plugin that displays your Claude account usage. Shows session and weekly rate limits with reset countdowns. +**Text mode** (default): ``` -Claude Usage +▼ Claude Usage juneyoung.kang@wantedlab.com via cli Session 31% resets in 3h 16m Weekly 11% resets in 4d 5h ``` +**Bar mode** (`"displayMode": "bar"`): +``` +▼ Claude Usage +juneyoung.kang@wantedlab.com +via oauth + Session █████░░░░░░░░░ 31% (3h 16m) + Weekly ██░░░░░░░░░░░░ 11% (4d 5h) +``` + ## Install Paste this into your LLM agent (Claude Code, opencode, Cursor, etc.): @@ -41,6 +51,7 @@ opencode resolves the npm package on startup automatically. "plugin": [["opencode-claude-usage", { "enabled": true, "refreshInterval": 60, + "displayMode": "text", "headerColor": "#E07A3A", "valueColor": "#82AAFF", "dimColor": "#546E7A" @@ -51,6 +62,7 @@ opencode resolves the npm package on startup automatically. | Option | Default | Description | |---|---|---| | `refreshInterval` | `60` | Seconds between data refreshes | +| `displayMode` | `"text"` | `"text"` shows percentage + reset time, `"bar"` shows progress bar + percentage + reset time | | `headerColor` | theme text | Color of window labels (Session, Weekly, etc.) | | `valueColor` | `#82AAFF` | Color of percentage values | | `dimColor` | theme muted | Color of reset times and secondary text | diff --git a/src/format.ts b/src/format.ts index 9ab61e4..4f04bef 100644 --- a/src/format.ts +++ b/src/format.ts @@ -167,6 +167,29 @@ export function formatCost( return `${symbol}${used} / ${symbol}${limit}` } +/** + * Build a progress bar string of the given width. + * Returns { filled, empty } strings for separate coloring in the TUI. + */ +const BAR_WIDTH = 14 +const FILLED_CHAR = "█" +const EMPTY_CHAR = "░" + +export function formatBar( + utilization: number | null | undefined, + width: number = BAR_WIDTH, +): { filled: string; empty: string } { + if (utilization === null || utilization === undefined) { + return { filled: "", empty: EMPTY_CHAR.repeat(width) } + } + const clamped = Math.max(0, Math.min(100, utilization)) + const filledCount = Math.round((clamped / 100) * width) + return { + filled: FILLED_CHAR.repeat(filledCount), + empty: EMPTY_CHAR.repeat(width - filledCount), + } +} + /** * Map an OAuthUsageResponse field key to a short display label. */ diff --git a/src/tui.tsx b/src/tui.tsx index 7d2371a..6b4ee8f 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -3,7 +3,7 @@ import { createSignal } from "solid-js" import type { TuiPlugin, TuiPluginModule, TuiSlotContext } from "@opencode-ai/plugin/tui" import type { UsageState, PluginOptions } from "./types" import { createRefreshLoop } from "./fetcher" -import { formatRelativeTime, formatPercentage, formatCost, windowLabel } from "./format" +import { formatRelativeTime, formatPercentage, formatBar, formatCost, windowLabel } from "./format" const CLAUDE_ORANGE = "#E07A3A" @@ -24,6 +24,7 @@ const DEFAULT_REFRESH_INTERVAL_S = 60 const tui: TuiPlugin = async (api, rawOptions, _meta) => { const options = (rawOptions as PluginOptions | undefined) ?? {} const refreshIntervalMs = (options.refreshInterval ?? DEFAULT_REFRESH_INTERVAL_S) * 1000 + const displayMode = options.displayMode ?? "text" const [state, setState] = createSignal({ status: "idle", @@ -32,6 +33,7 @@ const tui: TuiPlugin = async (api, rawOptions, _meta) => { authMethod: "none", error: null, }) + const [open, setOpen] = createSignal(true) const EXPECTED_LOAD_S = 25 const [countdown, setCountdown] = createSignal(EXPECTED_LOAD_S) @@ -111,50 +113,73 @@ const tui: TuiPlugin = async (api, rawOptions, _meta) => { const data = s.data const profile = s.profile + const isOpen = open() return ( - {"Claude Usage"} - {profile?.email ? ( - - {profile.email} - - ) : null} - - {profile?.email ? ( - - {`via ${s.authMethod}`} - - ) : null} + setOpen(!open())}> + + {isOpen ? "\u25BC" : "\u25B6"}{" Claude Usage"} + + - {data ? ( + {isOpen ? ( - {WINDOW_KEYS.map((key) => { - const w = data[key as WindowKey] - if (!w) return null - const pct = w.utilization - const reset = w.resetsAt - const label = windowLabel(key) - const pctColor = pct === null ? valueFg - : pct >= 80 ? CLAUDE_ORANGE - : pct >= 51 ? "#F0A875" - : valueFg - const resetStr = formatRelativeTime(reset) - return ( - - {label.padEnd(10)} - {formatPercentage(pct).padStart(5)} - {` resets in ${resetStr}`} - - ) - })} - - {data.extraUsage?.isEnabled ? ( - - {"Credit "} - - {formatCost(data.extraUsage.usedCredits, data.extraUsage.monthlyLimit, data.extraUsage.currency)} - + {profile?.email ? ( + + {profile.email} + + ) : null} + + {profile?.email ? ( + + {`via ${s.authMethod}`} + + ) : null} + + {data ? ( + + {WINDOW_KEYS.map((key) => { + const w = data[key as WindowKey] + if (!w) return null + const pct = w.utilization + const label = windowLabel(key) + const pctColor = pct === null ? valueFg + : pct >= 80 ? CLAUDE_ORANGE + : pct >= 51 ? "#F0A875" + : valueFg + + if (displayMode === "bar") { + const bar = formatBar(pct) + const resetStr = formatRelativeTime(w.resetsAt) + const resetSuffix = resetStr && resetStr !== "—" ? ` (${resetStr})` : "" + return ( + + {` ${label.padEnd(8)}`} + {bar.filled + bar.empty + formatPercentage(pct).padStart(4)} + {resetSuffix} + + ) + } + + const resetStr = formatRelativeTime(w.resetsAt) + return ( + + {label.padEnd(10)} + {formatPercentage(pct).padStart(5)} + {` resets in ${resetStr}`} + + ) + })} + + {data.extraUsage?.isEnabled ? ( + + {"Credit "} + + {formatCost(data.extraUsage.usedCredits, data.extraUsage.monthlyLimit, data.extraUsage.currency)} + + + ) : null} ) : null} diff --git a/src/types.ts b/src/types.ts index 9c278fc..a831d2f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -77,8 +77,11 @@ export interface UsageState { } // Plugin configuration options (from tui.json) +export type DisplayMode = "text" | "bar" + export interface PluginOptions { refreshInterval?: number + displayMode?: DisplayMode headerColor?: string valueColor?: string dimColor?: string From 394e10c61c4c2ddbe49671d902d4add37006dbe0 Mon Sep 17 00:00:00 2001 From: stevejkang Date: Sun, 3 May 2026 21:09:04 +0900 Subject: [PATCH 2/2] Add consistent 1-char indent to email and via lines in sidebar Email and via-authMethod lines were missing the leading space that Session/Weekly rows already had. Apply uniform 1-char indent to all content lines for visual consistency. Update README examples to match. --- README.md | 12 ++++++------ src/tui.tsx | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8f6df36..b29dd14 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,17 @@ An [opencode](https://opencode.ai) TUI sidebar plugin that displays your Claude **Text mode** (default): ``` ▼ Claude Usage -juneyoung.kang@wantedlab.com -via cli -Session 31% resets in 3h 16m -Weekly 11% resets in 4d 5h + juneyoung.kang@wantedlab.com + via cli + Session 31% resets in 3h 16m + Weekly 11% resets in 4d 5h ``` **Bar mode** (`"displayMode": "bar"`): ``` ▼ Claude Usage -juneyoung.kang@wantedlab.com -via oauth + juneyoung.kang@wantedlab.com + via cli Session █████░░░░░░░░░ 31% (3h 16m) Weekly ██░░░░░░░░░░░░ 11% (4d 5h) ``` diff --git a/src/tui.tsx b/src/tui.tsx index 6b4ee8f..6d8e39a 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -127,13 +127,13 @@ const tui: TuiPlugin = async (api, rawOptions, _meta) => { {profile?.email ? ( - {profile.email} + {` ${profile.email}`} ) : null} {profile?.email ? ( - {`via ${s.authMethod}`} + {` via ${s.authMethod}`} ) : null} @@ -165,7 +165,7 @@ const tui: TuiPlugin = async (api, rawOptions, _meta) => { const resetStr = formatRelativeTime(w.resetsAt) return ( - {label.padEnd(10)} + {` ${label.padEnd(9)}`} {formatPercentage(pct).padStart(5)} {` resets in ${resetStr}`}