From e5f73380c2c01260f7a72c755c94236e9eb66802 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Mon, 5 Jan 2026 13:31:30 +0900 Subject: [PATCH 1/4] feat: display Anthropic and OpenAI OAuth usage in status dialog and sidebar - Add usage/anthropic.ts: fetch usage from Anthropic OAuth API - Add usage/openai.ts: fetch usage from OpenAI/ChatGPT backend API - Update dialog-status.tsx: show usage bars with reset times - Update sidebar.tsx: show compact usage display with progress bars Shows rate limit utilization percentages and reset times for: - Anthropic: 5-hour and 7-day windows - OpenAI: primary/secondary windows with plan type and credits Related to #6298 (plan usage tracking) --- .../cli/cmd/tui/component/dialog-status.tsx | 127 +++++++++++++++- .../cli/cmd/tui/routes/session/sidebar.tsx | 84 +++++++++- packages/opencode/src/usage/anthropic.ts | 72 +++++++++ packages/opencode/src/usage/openai.ts | 143 ++++++++++++++++++ 4 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 packages/opencode/src/usage/anthropic.ts create mode 100644 packages/opencode/src/usage/openai.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index b85cd5c6542..96592aebfcf 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -1,16 +1,40 @@ -import { TextAttributes } from "@opentui/core" +import { TextAttributes, RGBA } from "@opentui/core" import { useTheme } from "../context/theme" import { useSync } from "@tui/context/sync" -import { For, Match, Switch, Show, createMemo } from "solid-js" +import { For, Match, Switch, Show, createMemo, createResource } from "solid-js" +import { AnthropicUsage } from "@/usage/anthropic" +import { OpenAIUsage } from "@/usage/openai" export type DialogStatusProps = {} +function UsageBar(props: { percent: number; fg: RGBA; bgMuted: RGBA }) { + const width = 20 + const filled = Math.round((props.percent / 100) * width) + const empty = width - filled + return ( + + {"█".repeat(filled)} + {"░".repeat(empty)} + + ) +} + export function DialogStatus() { const sync = useSync() const { theme } = useTheme() const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled)) + const [anthropicUsage, { refetch: refetchAnthropicUsage }] = createResource( + () => AnthropicUsage.fetch(), + { initialValue: null } + ) + + const [openaiUsage, { refetch: refetchOpenAIUsage }] = createResource( + () => OpenAIUsage.fetch(), + { initialValue: null } + ) + const plugins = createMemo(() => { const list = sync.data.config.plugin ?? [] const result = list.map((value) => { @@ -36,6 +60,12 @@ export function DialogStatus() { return result.toSorted((a, b) => a.name.localeCompare(b.name)) }) + const getUsageColor = (percent: number) => { + if (percent >= 90) return theme.error + if (percent >= 70) return theme.warning + return theme.success + } + return ( @@ -44,6 +74,99 @@ export function DialogStatus() { esc + + + {(usage) => ( + + + Anthropic Usage + + + {(fiveHour) => ( + + 5h: + + {fiveHour().utilization}% + + (reset: {AnthropicUsage.formatResetTime(fiveHour().resets_at)}) + + + )} + + + {(sevenDay) => ( + + 7d: + + {sevenDay().utilization}% + + (reset: {AnthropicUsage.formatResetTime(sevenDay().resets_at)}) + + + )} + + + {(opus) => ( + + Opus 7d: + + {opus().utilization}% + + (reset: {AnthropicUsage.formatResetTime(opus().resets_at)}) + + + )} + + + )} + + + + {(usage) => ( + + + OpenAI Usage ({OpenAIUsage.getPlanDisplayName(usage().plan_type)}) + + + {(primary) => ( + + {OpenAIUsage.formatWindowDuration(primary().limit_window_seconds)}: + + {Math.round(primary().used_percent)}% + + (reset: {OpenAIUsage.formatResetTime(primary().reset_at)}) + + + )} + + + {(secondary) => ( + + {OpenAIUsage.formatWindowDuration(secondary().limit_window_seconds)}: + + {Math.round(secondary().used_percent)}% + + (reset: {OpenAIUsage.formatResetTime(secondary().reset_at)}) + + + )} + + + {(credits) => ( + + Credits: + {OpenAIUsage.formatCredits(credits().balance)} + }> + Unlimited + + + )} + + + )} + + 0} fallback={No MCP Servers}> {Object.keys(sync.data.mcp).length} MCP Servers diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index a9ed042d1bb..33dbf00c696 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -1,5 +1,5 @@ import { useSync } from "@tui/context/sync" -import { createMemo, For, Show, Switch, Match } from "solid-js" +import { createMemo, For, Show, Switch, Match, createResource } from "solid-js" import { createStore } from "solid-js/store" import { useTheme } from "../../context/theme" import { Locale } from "@/util/locale" @@ -11,6 +11,8 @@ import { useKeybind } from "../../context/keybind" import { useDirectory } from "../../context/directory" import { useKV } from "../../context/kv" import { TodoItem } from "../../component/todo-item" +import { AnthropicUsage } from "@/usage/anthropic" +import { OpenAIUsage } from "@/usage/openai" export function Sidebar(props: { sessionID: string }) { const sync = useSync() @@ -25,8 +27,26 @@ export function Sidebar(props: { sessionID: string }) { diff: true, todo: true, lsp: true, + usage: true, }) + const [anthropicUsage] = createResource(() => AnthropicUsage.fetch(), { initialValue: null }) + const [openaiUsage] = createResource(() => OpenAIUsage.fetch(), { initialValue: null }) + + const getUsageColor = (percent: number) => { + if (percent >= 90) return theme.error + if (percent >= 70) return theme.warning + return theme.success + } + + const usageBar = (percent: number) => { + const width = 10 + const filled = Math.round((percent / 100) * width) + return "█".repeat(filled) + "░".repeat(width - filled) + } + + const hasUsageData = createMemo(() => anthropicUsage() || openaiUsage()) + // Sort MCP servers alphabetically for consistent display order const mcpEntries = createMemo(() => Object.entries(sync.data.mcp).sort(([a], [b]) => a.localeCompare(b))) @@ -96,6 +116,68 @@ export function Sidebar(props: { sessionID: string }) { {context()?.percentage ?? 0}% used {cost()} spent + + + setExpanded("usage", !expanded.usage)} + > + {expanded.usage ? "▼" : "▶"} + + Usage + + + + + {(usage) => ( + <> + Anthropic + + {(w) => ( + + 5h {usageBar(w().utilization)} + {" "}{w().utilization}% ({AnthropicUsage.formatResetTime(w().resets_at)}) + + )} + + + {(w) => ( + + 7d {usageBar(w().utilization)} + {" "}{w().utilization}% ({AnthropicUsage.formatResetTime(w().resets_at)}) + + )} + + + )} + + + {(usage) => ( + <> + OpenAI ({OpenAIUsage.getPlanDisplayName(usage().plan_type)}) + + {(w) => ( + + {OpenAIUsage.formatWindowDuration(w().limit_window_seconds)} {usageBar(w().used_percent)} + {" "}{Math.round(w().used_percent)}% ({OpenAIUsage.formatResetTime(w().reset_at)}) + + )} + + + {(w) => ( + + {OpenAIUsage.formatWindowDuration(w().limit_window_seconds)} {usageBar(w().used_percent)} + {" "}{Math.round(w().used_percent)}% ({OpenAIUsage.formatResetTime(w().reset_at)}) + + )} + + + )} + + + + 0}> + + export async function fetch(): Promise { + const auth = await Auth.get("anthropic") + if (!auth || auth.type !== "oauth") { + return null + } + + try { + const response = await globalThis.fetch("https://api.anthropic.com/api/oauth/usage", { + method: "GET", + headers: { + Authorization: `Bearer ${auth.access}`, + "anthropic-beta": "oauth-2025-04-20", + Accept: "application/json", + }, + }) + + if (!response.ok) { + console.error(`Anthropic usage API error: ${response.status}`) + return null + } + + const data = await response.json() + const parsed = UsageData.safeParse(data) + if (!parsed.success) { + console.error("Failed to parse Anthropic usage data:", parsed.error) + return null + } + + return parsed.data + } catch (error) { + console.error("Failed to fetch Anthropic usage:", error) + return null + } + } + + export function formatResetTime(isoString: string | null): string { + if (!isoString) return "N/A" + const date = new Date(isoString) + const now = new Date() + const diffMs = date.getTime() - now.getTime() + const diffMins = Math.floor(diffMs / 60000) + const diffHours = Math.floor(diffMins / 60) + + if (diffHours >= 24) { + const days = Math.floor(diffHours / 24) + return `${days}d ${diffHours % 24}h` + } + if (diffHours > 0) { + return `${diffHours}h ${diffMins % 60}m` + } + if (diffMins > 0) { + return `${diffMins}m` + } + return "soon" + } +} diff --git a/packages/opencode/src/usage/openai.ts b/packages/opencode/src/usage/openai.ts new file mode 100644 index 00000000000..42eee63cd21 --- /dev/null +++ b/packages/opencode/src/usage/openai.ts @@ -0,0 +1,143 @@ +import { Auth } from "@/auth" +import z from "zod" + +export namespace OpenAIUsage { + export const PlanType = z.enum([ + "free", + "plus", + "pro", + "team", + "business", + "enterprise", + "edu", + "education", + "guest", + "go", + "free_workspace", + "quorum", + "k12", + ]) + export type PlanType = z.infer + + export const RateLimitWindow = z.object({ + used_percent: z.number(), + limit_window_seconds: z.number(), + reset_at: z.number(), + }) + + export const RateLimitDetails = z.object({ + allowed: z.boolean().optional(), + limit_reached: z.boolean().optional(), + primary_window: RateLimitWindow.optional().nullable(), + secondary_window: RateLimitWindow.optional().nullable(), + }) + + export const CreditStatus = z.object({ + has_credits: z.boolean().optional(), + unlimited: z.boolean().optional(), + balance: z.union([z.string(), z.number()]).optional().nullable(), + }) + + export const UsageData = z.object({ + plan_type: PlanType, + rate_limit: RateLimitDetails.optional().nullable(), + credits: CreditStatus.optional().nullable(), + }) + export type UsageData = z.infer + + export function getPlanDisplayName(planType: PlanType): string { + const names: Record = { + free: "Free", + plus: "Plus", + pro: "Pro", + team: "Team", + business: "Business", + enterprise: "Enterprise", + edu: "Education", + education: "Education", + guest: "Guest", + go: "Go", + free_workspace: "Free Workspace", + quorum: "Quorum", + k12: "K-12", + } + return names[planType] || planType + } + + export async function fetch(): Promise { + const auth = await Auth.get("openai") + if (!auth || auth.type !== "oauth") { + return null + } + + try { + const response = await globalThis.fetch("https://chatgpt.com/backend-api/wham/usage", { + method: "GET", + headers: { + Authorization: `Bearer ${auth.access}`, + Accept: "application/json", + "User-Agent": "opencode-cli", + }, + }) + + if (!response.ok) { + console.error(`OpenAI usage API error: ${response.status}`) + return null + } + + const data = await response.json() + const parsed = UsageData.safeParse(data) + if (!parsed.success) { + console.error("Failed to parse OpenAI usage data:", parsed.error) + return null + } + + return parsed.data + } catch (error) { + console.error("Failed to fetch OpenAI usage:", error) + return null + } + } + + export function formatWindowDuration(seconds: number): string { + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + + if (days >= 1) { + return `${days}d` + } + if (hours >= 1) { + return `${hours}h` + } + return `${minutes}m` + } + + export function formatResetTime(unixTimestamp: number): string { + const date = new Date(unixTimestamp * 1000) + const now = new Date() + const diffMs = date.getTime() - now.getTime() + const diffMins = Math.floor(diffMs / 60000) + const diffHours = Math.floor(diffMins / 60) + + if (diffHours >= 24) { + const days = Math.floor(diffHours / 24) + return `${days}d ${diffHours % 24}h` + } + if (diffHours > 0) { + return `${diffHours}h ${diffMins % 60}m` + } + if (diffMins > 0) { + return `${diffMins}m` + } + return "soon" + } + + export function formatCredits(balance: string | number | null | undefined): string { + if (balance === null || balance === undefined) { + return "N/A" + } + const num = typeof balance === "string" ? parseFloat(balance) : balance + return `$${num.toFixed(2)}` + } +} From 2c78b2918f6a7a13faddd17763974c5f9b2f5783 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Mon, 5 Jan 2026 13:46:42 +0900 Subject: [PATCH 2/4] fix: address bot review feedback - Add negative time handling in formatResetTime (return 'refreshing') - Add clamp for percent values in usage bars (0-100) - Add 10s timeout to fetch calls (AbortSignal.timeout) - Add NaN handling in formatCredits - Extract shared utils (getUsageColor, clampPercent, usageBarString) - Add refetch on message change for sidebar usage display - Remove unused refetch destructuring in dialog-status --- .../cli/cmd/tui/component/dialog-status.tsx | 34 +++++++++--------- .../cli/cmd/tui/routes/session/sidebar.tsx | 35 ++++++++++--------- packages/opencode/src/usage/anthropic.ts | 4 +++ packages/opencode/src/usage/openai.ts | 9 ++++- packages/opencode/src/usage/utils.ts | 17 +++++++++ 5 files changed, 63 insertions(+), 36 deletions(-) create mode 100644 packages/opencode/src/usage/utils.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index 96592aebfcf..1ce0c2935f3 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -4,12 +4,14 @@ import { useSync } from "@tui/context/sync" import { For, Match, Switch, Show, createMemo, createResource } from "solid-js" import { AnthropicUsage } from "@/usage/anthropic" import { OpenAIUsage } from "@/usage/openai" +import { getUsageColor, clampPercent } from "@/usage/utils" export type DialogStatusProps = {} function UsageBar(props: { percent: number; fg: RGBA; bgMuted: RGBA }) { const width = 20 - const filled = Math.round((props.percent / 100) * width) + const clamped = clampPercent(props.percent) + const filled = Math.round((clamped / 100) * width) const empty = width - filled return ( @@ -25,12 +27,12 @@ export function DialogStatus() { const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled)) - const [anthropicUsage, { refetch: refetchAnthropicUsage }] = createResource( + const [anthropicUsage] = createResource( () => AnthropicUsage.fetch(), { initialValue: null } ) - const [openaiUsage, { refetch: refetchOpenAIUsage }] = createResource( + const [openaiUsage] = createResource( () => OpenAIUsage.fetch(), { initialValue: null } ) @@ -60,11 +62,7 @@ export function DialogStatus() { return result.toSorted((a, b) => a.name.localeCompare(b.name)) }) - const getUsageColor = (percent: number) => { - if (percent >= 90) return theme.error - if (percent >= 70) return theme.warning - return theme.success - } + const colorFor = (percent: number) => colorFor(percent, theme) return ( @@ -85,8 +83,8 @@ export function DialogStatus() { {(fiveHour) => ( 5h: - - {fiveHour().utilization}% + + {fiveHour().utilization}% (reset: {AnthropicUsage.formatResetTime(fiveHour().resets_at)}) @@ -97,8 +95,8 @@ export function DialogStatus() { {(sevenDay) => ( 7d: - - {sevenDay().utilization}% + + {sevenDay().utilization}% (reset: {AnthropicUsage.formatResetTime(sevenDay().resets_at)}) @@ -109,8 +107,8 @@ export function DialogStatus() { {(opus) => ( Opus 7d: - - {opus().utilization}% + + {opus().utilization}% (reset: {AnthropicUsage.formatResetTime(opus().resets_at)}) @@ -131,8 +129,8 @@ export function DialogStatus() { {(primary) => ( {OpenAIUsage.formatWindowDuration(primary().limit_window_seconds)}: - - {Math.round(primary().used_percent)}% + + {Math.round(primary().used_percent)}% (reset: {OpenAIUsage.formatResetTime(primary().reset_at)}) @@ -143,8 +141,8 @@ export function DialogStatus() { {(secondary) => ( {OpenAIUsage.formatWindowDuration(secondary().limit_window_seconds)}: - - {Math.round(secondary().used_percent)}% + + {Math.round(secondary().used_percent)}% (reset: {OpenAIUsage.formatResetTime(secondary().reset_at)}) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 33dbf00c696..0f6b11b040d 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -1,5 +1,5 @@ import { useSync } from "@tui/context/sync" -import { createMemo, For, Show, Switch, Match, createResource } from "solid-js" +import { createMemo, For, Show, Switch, Match, createResource, createEffect, on } from "solid-js" import { createStore } from "solid-js/store" import { useTheme } from "../../context/theme" import { Locale } from "@/util/locale" @@ -13,6 +13,7 @@ import { useKV } from "../../context/kv" import { TodoItem } from "../../component/todo-item" import { AnthropicUsage } from "@/usage/anthropic" import { OpenAIUsage } from "@/usage/openai" +import { getUsageColor, usageBarString } from "@/usage/utils" export function Sidebar(props: { sessionID: string }) { const sync = useSync() @@ -30,20 +31,20 @@ export function Sidebar(props: { sessionID: string }) { usage: true, }) - const [anthropicUsage] = createResource(() => AnthropicUsage.fetch(), { initialValue: null }) - const [openaiUsage] = createResource(() => OpenAIUsage.fetch(), { initialValue: null }) + const [anthropicUsage, { refetch: refetchAnthropic }] = createResource(() => AnthropicUsage.fetch(), { initialValue: null }) + const [openaiUsage, { refetch: refetchOpenAI }] = createResource(() => OpenAIUsage.fetch(), { initialValue: null }) - const getUsageColor = (percent: number) => { - if (percent >= 90) return theme.error - if (percent >= 70) return theme.warning - return theme.success - } + createEffect(on( + () => messages().length, + () => { + refetchAnthropic() + refetchOpenAI() + }, + { defer: true } + )) - const usageBar = (percent: number) => { - const width = 10 - const filled = Math.round((percent / 100) * width) - return "█".repeat(filled) + "░".repeat(width - filled) - } + const colorFor = (percent: number) => colorFor(percent, theme) + const usageBar = (percent: number) => usageBarString(percent, 10) const hasUsageData = createMemo(() => anthropicUsage() || openaiUsage()) @@ -136,7 +137,7 @@ export function Sidebar(props: { sessionID: string }) { {(w) => ( - 5h {usageBar(w().utilization)} + 5h {usageBar(w().utilization)} {" "}{w().utilization}% ({AnthropicUsage.formatResetTime(w().resets_at)}) )} @@ -144,7 +145,7 @@ export function Sidebar(props: { sessionID: string }) { {(w) => ( - 7d {usageBar(w().utilization)} + 7d {usageBar(w().utilization)} {" "}{w().utilization}% ({AnthropicUsage.formatResetTime(w().resets_at)}) )} @@ -159,7 +160,7 @@ export function Sidebar(props: { sessionID: string }) { {(w) => ( - {OpenAIUsage.formatWindowDuration(w().limit_window_seconds)} {usageBar(w().used_percent)} + {OpenAIUsage.formatWindowDuration(w().limit_window_seconds)} {usageBar(w().used_percent)} {" "}{Math.round(w().used_percent)}% ({OpenAIUsage.formatResetTime(w().reset_at)}) )} @@ -167,7 +168,7 @@ export function Sidebar(props: { sessionID: string }) { {(w) => ( - {OpenAIUsage.formatWindowDuration(w().limit_window_seconds)} {usageBar(w().used_percent)} + {OpenAIUsage.formatWindowDuration(w().limit_window_seconds)} {usageBar(w().used_percent)} {" "}{Math.round(w().used_percent)}% ({OpenAIUsage.formatResetTime(w().reset_at)}) )} diff --git a/packages/opencode/src/usage/anthropic.ts b/packages/opencode/src/usage/anthropic.ts index a4f4af1e9c0..60e2c9bff8c 100644 --- a/packages/opencode/src/usage/anthropic.ts +++ b/packages/opencode/src/usage/anthropic.ts @@ -28,6 +28,7 @@ export namespace AnthropicUsage { "anthropic-beta": "oauth-2025-04-20", Accept: "application/json", }, + signal: AbortSignal.timeout(10_000), }) if (!response.ok) { @@ -54,6 +55,9 @@ export namespace AnthropicUsage { const date = new Date(isoString) const now = new Date() const diffMs = date.getTime() - now.getTime() + + if (diffMs <= 0) return "refreshing" + const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMins / 60) diff --git a/packages/opencode/src/usage/openai.ts b/packages/opencode/src/usage/openai.ts index 42eee63cd21..23cee116f0d 100644 --- a/packages/opencode/src/usage/openai.ts +++ b/packages/opencode/src/usage/openai.ts @@ -78,6 +78,7 @@ export namespace OpenAIUsage { Accept: "application/json", "User-Agent": "opencode-cli", }, + signal: AbortSignal.timeout(10_000), }) if (!response.ok) { @@ -117,6 +118,9 @@ export namespace OpenAIUsage { const date = new Date(unixTimestamp * 1000) const now = new Date() const diffMs = date.getTime() - now.getTime() + + if (diffMs <= 0) return "refreshing" + const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMins / 60) @@ -137,7 +141,10 @@ export namespace OpenAIUsage { if (balance === null || balance === undefined) { return "N/A" } - const num = typeof balance === "string" ? parseFloat(balance) : balance + const num = typeof balance === "string" ? Number(balance) : balance + if (typeof num !== "number" || !Number.isFinite(num)) { + return "N/A" + } return `$${num.toFixed(2)}` } } diff --git a/packages/opencode/src/usage/utils.ts b/packages/opencode/src/usage/utils.ts new file mode 100644 index 00000000000..a0983e9ee71 --- /dev/null +++ b/packages/opencode/src/usage/utils.ts @@ -0,0 +1,17 @@ +import type { RGBA } from "@opentui/core" + +export function getUsageColor(percent: number, theme: { error: RGBA; warning: RGBA; success: RGBA }): RGBA { + if (percent >= 90) return theme.error + if (percent >= 70) return theme.warning + return theme.success +} + +export function clampPercent(percent: number): number { + return Math.max(0, Math.min(100, percent)) +} + +export function usageBarString(percent: number, width: number = 10): string { + const clamped = clampPercent(percent) + const filled = Math.round((clamped / 100) * width) + return "\u2588".repeat(filled) + "\u2591".repeat(width - filled) +} From 83e36d7b3e3e7bfef9da3ba4b01d4989d41789ea Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Mon, 5 Jan 2026 13:50:01 +0900 Subject: [PATCH 3/4] fix: resolve infinite recursion in colorFor function --- packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 0f6b11b040d..cfcf3075b39 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -43,7 +43,7 @@ export function Sidebar(props: { sessionID: string }) { { defer: true } )) - const colorFor = (percent: number) => colorFor(percent, theme) + const colorFor = (percent: number) => getUsageColor(percent, theme) const usageBar = (percent: number) => usageBarString(percent, 10) const hasUsageData = createMemo(() => anthropicUsage() || openaiUsage()) From 9762c3e74a5bb8f945fe502105eaf0c7e423baad Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Mon, 5 Jan 2026 14:06:52 +0900 Subject: [PATCH 4/4] fix: correct function name to getUsageColor in dialog-status --- .../cli/cmd/tui/component/dialog-status.tsx | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index 1ce0c2935f3..c4a593085a6 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -27,15 +27,9 @@ export function DialogStatus() { const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled)) - const [anthropicUsage] = createResource( - () => AnthropicUsage.fetch(), - { initialValue: null } - ) + const [anthropicUsage] = createResource(() => AnthropicUsage.fetch(), { initialValue: null }) - const [openaiUsage] = createResource( - () => OpenAIUsage.fetch(), - { initialValue: null } - ) + const [openaiUsage] = createResource(() => OpenAIUsage.fetch(), { initialValue: null }) const plugins = createMemo(() => { const list = sync.data.config.plugin ?? [] @@ -62,7 +56,7 @@ export function DialogStatus() { return result.toSorted((a, b) => a.name.localeCompare(b.name)) }) - const colorFor = (percent: number) => colorFor(percent, theme) + const colorFor = (percent: number) => getUsageColor(percent, theme) return ( @@ -83,11 +77,13 @@ export function DialogStatus() { {(fiveHour) => ( 5h: - + {fiveHour().utilization}% - - (reset: {AnthropicUsage.formatResetTime(fiveHour().resets_at)}) - + (reset: {AnthropicUsage.formatResetTime(fiveHour().resets_at)}) )} @@ -95,11 +91,13 @@ export function DialogStatus() { {(sevenDay) => ( 7d: - + {sevenDay().utilization}% - - (reset: {AnthropicUsage.formatResetTime(sevenDay().resets_at)}) - + (reset: {AnthropicUsage.formatResetTime(sevenDay().resets_at)}) )} @@ -109,9 +107,7 @@ export function DialogStatus() { Opus 7d: {opus().utilization}% - - (reset: {AnthropicUsage.formatResetTime(opus().resets_at)}) - + (reset: {AnthropicUsage.formatResetTime(opus().resets_at)}) )} @@ -129,11 +125,13 @@ export function DialogStatus() { {(primary) => ( {OpenAIUsage.formatWindowDuration(primary().limit_window_seconds)}: - + {Math.round(primary().used_percent)}% - - (reset: {OpenAIUsage.formatResetTime(primary().reset_at)}) - + (reset: {OpenAIUsage.formatResetTime(primary().reset_at)}) )} @@ -141,11 +139,13 @@ export function DialogStatus() { {(secondary) => ( {OpenAIUsage.formatWindowDuration(secondary().limit_window_seconds)}: - + {Math.round(secondary().used_percent)}% - - (reset: {OpenAIUsage.formatResetTime(secondary().reset_at)}) - + (reset: {OpenAIUsage.formatResetTime(secondary().reset_at)}) )} @@ -153,9 +153,10 @@ export function DialogStatus() { {(credits) => ( Credits: - {OpenAIUsage.formatCredits(credits().balance)} - }> + {OpenAIUsage.formatCredits(credits().balance)}} + > Unlimited