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
This commit is contained in:
redshift
2026-03-21 22:20:43 +00:00
parent 310e7076c3
commit b96ff0580d
3 changed files with 84 additions and 5 deletions
+4 -2
View File
@@ -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<void> {
let currentTab: TabId = "overview";
let stats: UsageStats | null = null;
let balance: BalanceInfo | null = null;
let refreshInterval: ReturnType<typeof setInterval> | null = null;
let shouldUpdate = true;
let autoRefresh = true;
@@ -86,6 +87,7 @@ export async function runUsageTui(): Promise<void> {
if (forceFetch || shouldUpdate) {
stats = await fetchUsage(1000);
balance = await fetchBalance();
shouldUpdate = false;
}
@@ -100,7 +102,7 @@ export async function runUsageTui(): Promise<void> {
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;
+39
View File
@@ -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<BalanceInfo | null> {
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<UsageStats | null> {
try {
const running = await isDaemonRunning();
+41 -3
View File
@@ -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);