From e42d6eadff49520c60adbd4db922ecb30c474c7b Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 15 Mar 2026 13:53:54 -0500 Subject: [PATCH] feat(cli): match TUI footer to reference design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove borders from input bar — clean '❯ message mosaic…' prompt - Two-line footer without borders: - Line 1: compact cwd (branch) | Gateway: Connected/Disconnected - Line 2: token stats (^in v_out R_cache W_cache $cost ctx%) | (provider) model - Extend TokenUsage with cacheRead, cacheWrite, cost, contextPercent, contextWindow - Add providerName to socket hook return - Reorder layout: top bar → messages → input → footer --- .../cli/src/tui/components/bottom-bar.tsx | 108 +++++++++++++----- packages/cli/src/tui/components/input-bar.tsx | 8 +- packages/cli/src/tui/hooks/use-socket.ts | 19 ++- 3 files changed, 98 insertions(+), 37 deletions(-) diff --git a/packages/cli/src/tui/components/bottom-bar.tsx b/packages/cli/src/tui/components/bottom-bar.tsx index 6d00e21..7c494db 100644 --- a/packages/cli/src/tui/components/bottom-bar.tsx +++ b/packages/cli/src/tui/components/bottom-bar.tsx @@ -6,55 +6,101 @@ import type { GitInfo } from '../hooks/use-git-info.js'; export interface BottomBarProps { gitInfo: GitInfo; tokenUsage: TokenUsage; + connected: boolean; + connecting: boolean; + modelName: string | null; + providerName: string | null; } function formatTokens(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; - if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`; + if (n >= 1_000) return `${(n / 1_000).toFixed(0)}k`; return String(n); } -/** Compact the cwd — show ~ for home, truncate long paths */ +/** Compact the cwd — replace home with ~ */ function compactCwd(cwd: string): string { const home = process.env['HOME'] ?? ''; if (home && cwd.startsWith(home)) { - cwd = '~' + cwd.slice(home.length); - } - // If still very long, show last 3 segments - const parts = cwd.split('/'); - if (parts.length > 4) { - return '…/' + parts.slice(-3).join('/'); + return '~' + cwd.slice(home.length); } return cwd; } -export function BottomBar({ gitInfo, tokenUsage }: BottomBarProps) { +export function BottomBar({ + gitInfo, + tokenUsage, + connected, + connecting, + modelName, + providerName, +}: BottomBarProps) { + const gatewayStatus = connected ? 'Connected' : connecting ? 'Connecting…' : 'Disconnected'; + const gatewayColor = connected ? 'green' : connecting ? 'yellow' : 'red'; + const hasTokens = tokenUsage.total > 0; return ( - - - cwd: - {compactCwd(gitInfo.cwd)} - {gitInfo.branch && ( - <> - - {gitInfo.branch} - - )} + + {/* Line 1: path (branch) ····· Gateway: Status */} + + + {compactCwd(gitInfo.cwd)} + {gitInfo.branch && ({gitInfo.branch})} + + + Gateway: + {gatewayStatus} + - - {hasTokens ? ( - <> - tokens: - ↑{formatTokens(tokenUsage.input)} - / - ↓{formatTokens(tokenUsage.output)} - ({formatTokens(tokenUsage.total)}) - - ) : ( - tokens: — - )} + + {/* Line 2: token stats ····· (provider) model */} + + + {hasTokens ? ( + <> + ^{formatTokens(tokenUsage.input)} + {' '} + v{formatTokens(tokenUsage.output)} + {tokenUsage.cacheRead > 0 && ( + <> + {' '} + R{formatTokens(tokenUsage.cacheRead)} + + )} + {tokenUsage.cacheWrite > 0 && ( + <> + {' '} + W{formatTokens(tokenUsage.cacheWrite)} + + )} + {tokenUsage.cost > 0 && ( + <> + {' '} + ${tokenUsage.cost.toFixed(3)} + + )} + {tokenUsage.contextPercent > 0 && ( + <> + {' '} + + {tokenUsage.contextPercent.toFixed(1)}%/{formatTokens(tokenUsage.contextWindow)} + + + )} + + ) : ( + + )} + + + {(providerName ?? modelName) && ( + + {providerName ? `(${providerName}) ` : ''} + {modelName ?? ''} + + )} + ); diff --git a/packages/cli/src/tui/components/input-bar.tsx b/packages/cli/src/tui/components/input-bar.tsx index 6f65ef6..ab02c2f 100644 --- a/packages/cli/src/tui/components/input-bar.tsx +++ b/packages/cli/src/tui/components/input-bar.tsx @@ -26,12 +26,10 @@ export function InputBar({ onSubmit, isStreaming, connected }: InputBarProps) { ? 'waiting for response…' : 'message mosaic…'; - const promptColor = !connected ? 'red' : isStreaming ? 'yellow' : 'green'; - return ( - - - ❯{' '} + + + {'❯ '} void; connectionError: string | null; } @@ -65,8 +71,18 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn { const [currentThinkingText, setCurrentThinkingText] = useState(''); const [activeToolCalls, setActiveToolCalls] = useState([]); // TODO: wire up once gateway emits token-usage and model-info events - const tokenUsage: TokenUsage = { input: 0, output: 0, total: 0 }; + const tokenUsage: TokenUsage = { + input: 0, + output: 0, + total: 0, + cacheRead: 0, + cacheWrite: 0, + cost: 0, + contextPercent: 0, + contextWindow: 0, + }; const modelName: string | null = null; + const providerName: string | null = null; const [connectionError, setConnectionError] = useState(null); const socketRef = useRef(null); @@ -191,6 +207,7 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn { activeToolCalls, tokenUsage, modelName, + providerName, sendMessage, connectionError, };