From b96ff0580d1acf1c14472ea967c669d0b4577e83 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 21 Mar 2026 22:20:43 +0000 Subject: [PATCH] Add wallet balance display to TUI overview tab - Fetch balance from /keys/balance endpoint showing wallet, cached tokens, and API key balances - Display total balance and individual balances per source with provider URLs - Show 'No funds available' message when balance is zero --- src/tui/usage/app.ts | 6 ++++-- src/tui/usage/data.ts | 39 ++++++++++++++++++++++++++++++++++++ src/tui/usage/render.ts | 44 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/tui/usage/app.ts b/src/tui/usage/app.ts index 8d6f38a..ba7114d 100644 --- a/src/tui/usage/app.ts +++ b/src/tui/usage/app.ts @@ -1,5 +1,5 @@ import { TABS } from "./constants.ts"; -import { fetchUsage } from "./data.ts"; +import { fetchBalance, fetchUsage, type BalanceInfo } from "./data.ts"; import { applyScrollToContent, exitSearchMode, @@ -44,6 +44,7 @@ export async function runUsageTui(): Promise { let currentTab: TabId = "overview"; let stats: UsageStats | null = null; + let balance: BalanceInfo | null = null; let refreshInterval: ReturnType | null = null; let shouldUpdate = true; let autoRefresh = true; @@ -86,6 +87,7 @@ export async function runUsageTui(): Promise { if (forceFetch || shouldUpdate) { stats = await fetchUsage(1000); + balance = await fetchBalance(); shouldUpdate = false; } @@ -100,7 +102,7 @@ export async function runUsageTui(): Promise { return; } - const content = renderTabContent(currentTab, stats, width); + const content = renderTabContent(currentTab, stats, balance, width); const footer = `${COLORS.dim}Press [Q] to quit, [R] to refresh, [A] to toggle auto-refresh${autoRefresh ? " (on)" : " (off)"} scroll:${vimState.scrollPos}${COLORS.reset}${vimState.mode === "normal" ? ` ${COLORS.yellow}vim: hjkl/arrows, / search, g top, gg bottom${COLORS.reset}` : ""}`; const chrome = renderHeader(currentTab, width) + renderTabs(currentTab) + renderSeparator(width) + renderSearchBar(); const chromeLines = chrome.split("\n").length - 1; diff --git a/src/tui/usage/data.ts b/src/tui/usage/data.ts index c94d6e9..40ca494 100644 --- a/src/tui/usage/data.ts +++ b/src/tui/usage/data.ts @@ -2,6 +2,45 @@ import type { UsageTrackingEntry } from "../../daemon/types.ts"; import { callDaemon, isDaemonRunning } from "../../cli-shared.ts"; import type { ClientStats, DayStats, ModelStats, ProviderStats, UsageStats } from "./types.ts"; +export interface BalanceKey { + id: string; + name: string; + balance: number; +} + +export interface BalanceInfo { + keys: BalanceKey[]; + total: number; + unit: "sat"; + apikeysCalled: number; +} + +export async function fetchBalance(): Promise { + try { + const running = await isDaemonRunning(); + if (!running) return null; + + const result = await callDaemon("/keys/balance"); + if (result.error) return null; + + const output = result.output as { + keys?: BalanceKey[]; + total?: number; + unit?: string; + apikeysCalled?: number; + }; + + return { + keys: output?.keys || [], + total: output?.total || 0, + unit: (output?.unit as "sat") || "sat", + apikeysCalled: output?.apikeysCalled || 0, + }; + } catch { + return null; + } +} + export async function fetchUsage(limit = 1000): Promise { try { const running = await isDaemonRunning(); diff --git a/src/tui/usage/render.ts b/src/tui/usage/render.ts index 0ea05a8..94235ee 100644 --- a/src/tui/usage/render.ts +++ b/src/tui/usage/render.ts @@ -13,6 +13,7 @@ import { } from "./data.ts"; import { vimState } from "./state.ts"; import { stripAnsi } from "./terminal.ts"; +import type { BalanceInfo } from "./data.ts"; import type { TabId, UsageStats } from "./types.ts"; /** Format a cost value: 0.12, 1.23, 12.34, 123.45, 1.23k, 1.23m */ @@ -106,7 +107,7 @@ export function renderBarChart( } -export function renderOverview(stats: UsageStats, width: number): string { +export function renderOverview(stats: UsageStats, balance: BalanceInfo | null, width: number): string { const totals = getTotals(stats.entries); const entryCount = stats.entries.length; const totalVisibleCost = totals.satsCost; @@ -129,6 +130,43 @@ export function renderOverview(stats: UsageStats, width: number): string { let output = renderBox(leftBox, width, "Summary Stats"); output += "\n" + renderBox(rightBox, width, "Token Stats"); + // Display all balances (wallet, cached tokens, API keys) if available + if (balance && balance.keys.length > 0) { + const balanceLines: string[] = []; + const totalBalance = balance.total; + + if (totalBalance > 0) { + balanceLines.push(`${COLORS.bold}Total Balance:${COLORS.reset} ${COLORS.green}${totalBalance.toLocaleString()} sat${COLORS.reset}`); + } else { + balanceLines.push(`${COLORS.bold}Total Balance:${COLORS.reset} ${COLORS.red}0 sat${COLORS.reset}`); + } + + for (const key of balance.keys) { + const color = key.id === "wallet" ? COLORS.green : COLORS.cyan; + if (key.id === "wallet") { + balanceLines.push(`${color}Wallet:${COLORS.reset} ${key.balance.toLocaleString()} sat`); + } else { + // Extract provider URL from name (e.g., "API Key: https://..." or "Cached: https://...") + const providerUrl = key.name.replace(/^(API Key|Cached):\s*/, ""); + const shortProvider = providerUrl.replace("https://", "").replace("http://", ""); + const label = key.id.startsWith("cached:") ? "Cached" : "API Key"; + balanceLines.push(`${color}${label}:${COLORS.reset} ${shortProvider} (${key.balance.toLocaleString()} sat)`); + } + } + + if (totalBalance === 0) { + balanceLines.push(`${COLORS.dim}No funds available${COLORS.reset}`); + } + + output = renderBox(balanceLines, width, "Balance") + "\n" + output; + } else if (balance && balance.keys.length === 0) { + const balanceLines: string[] = [ + `${COLORS.bold}Total Balance:${COLORS.reset} ${COLORS.red}0 sat${COLORS.reset}`, + `${COLORS.dim}No funds available${COLORS.reset}`, + ]; + output = renderBox(balanceLines, width, "Balance") + "\n" + output; + } + const modelStats = getModelStats(stats.entries); if (modelStats.length > 0) { const maxCost = modelStats[0]!.satsCost; @@ -404,9 +442,9 @@ export function renderRecent(stats: UsageStats, width: number): string { return renderBox(lines, width, `Recent Requests (${stats.entries.length} shown)`); } -export function renderTabContent(activeTab: TabId, stats: UsageStats, width: number): string { +export function renderTabContent(activeTab: TabId, stats: UsageStats, balance: BalanceInfo | null, width: number): string { switch (activeTab) { - case "overview": return renderOverview(stats, width); + case "overview": return renderOverview(stats, balance, width); case "today": return renderToday(stats, width); case "models": return renderModels(stats, width); case "providers": return renderProviders(stats, width);