feat(cli): TUI complete overhaul — components, sidebar, search, branding #157
@@ -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 (
|
||||
<Box borderStyle="single" borderColor="gray" paddingX={1} justifyContent="space-between">
|
||||
<Box>
|
||||
<Text dimColor>cwd: </Text>
|
||||
<Text>{compactCwd(gitInfo.cwd)}</Text>
|
||||
{gitInfo.branch && (
|
||||
<>
|
||||
<Text dimColor> ⎇ </Text>
|
||||
<Text color="cyan">{gitInfo.branch}</Text>
|
||||
</>
|
||||
)}
|
||||
<Box flexDirection="column" paddingX={0} marginTop={0}>
|
||||
{/* Line 1: path (branch) ····· Gateway: Status */}
|
||||
<Box justifyContent="space-between">
|
||||
<Box>
|
||||
<Text dimColor>{compactCwd(gitInfo.cwd)}</Text>
|
||||
{gitInfo.branch && <Text dimColor> ({gitInfo.branch})</Text>}
|
||||
</Box>
|
||||
<Box>
|
||||
<Text dimColor>Gateway: </Text>
|
||||
<Text color={gatewayColor}>{gatewayStatus}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
{hasTokens ? (
|
||||
<>
|
||||
<Text dimColor>tokens: </Text>
|
||||
<Text color="green">↑{formatTokens(tokenUsage.input)}</Text>
|
||||
<Text dimColor> / </Text>
|
||||
<Text color="yellow">↓{formatTokens(tokenUsage.output)}</Text>
|
||||
<Text dimColor> ({formatTokens(tokenUsage.total)})</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text dimColor>tokens: —</Text>
|
||||
)}
|
||||
|
||||
{/* Line 2: token stats ····· (provider) model */}
|
||||
<Box justifyContent="space-between">
|
||||
<Box>
|
||||
{hasTokens ? (
|
||||
<>
|
||||
<Text dimColor>^{formatTokens(tokenUsage.input)}</Text>
|
||||
<Text dimColor>{' '}</Text>
|
||||
<Text dimColor>v{formatTokens(tokenUsage.output)}</Text>
|
||||
{tokenUsage.cacheRead > 0 && (
|
||||
<>
|
||||
<Text dimColor>{' '}</Text>
|
||||
<Text dimColor>R{formatTokens(tokenUsage.cacheRead)}</Text>
|
||||
</>
|
||||
)}
|
||||
{tokenUsage.cacheWrite > 0 && (
|
||||
<>
|
||||
<Text dimColor>{' '}</Text>
|
||||
<Text dimColor>W{formatTokens(tokenUsage.cacheWrite)}</Text>
|
||||
</>
|
||||
)}
|
||||
{tokenUsage.cost > 0 && (
|
||||
<>
|
||||
<Text dimColor>{' '}</Text>
|
||||
<Text dimColor>${tokenUsage.cost.toFixed(3)}</Text>
|
||||
</>
|
||||
)}
|
||||
{tokenUsage.contextPercent > 0 && (
|
||||
<>
|
||||
<Text dimColor>{' '}</Text>
|
||||
<Text dimColor>
|
||||
{tokenUsage.contextPercent.toFixed(1)}%/{formatTokens(tokenUsage.contextWindow)}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Text dimColor>—</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
{(providerName ?? modelName) && (
|
||||
<Text dimColor>
|
||||
{providerName ? `(${providerName}) ` : ''}
|
||||
{modelName ?? ''}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -26,12 +26,10 @@ export function InputBar({ onSubmit, isStreaming, connected }: InputBarProps) {
|
||||
? 'waiting for response…'
|
||||
: 'message mosaic…';
|
||||
|
||||
const promptColor = !connected ? 'red' : isStreaming ? 'yellow' : 'green';
|
||||
|
||||
return (
|
||||
<Box paddingX={1} borderStyle="single" borderColor="gray">
|
||||
<Text bold color={promptColor}>
|
||||
❯{' '}
|
||||
<Box paddingX={0}>
|
||||
<Text bold color="green">
|
||||
{'❯ '}
|
||||
</Text>
|
||||
<TextInput
|
||||
value={input}
|
||||
|
||||
@@ -28,6 +28,11 @@ export interface TokenUsage {
|
||||
input: number;
|
||||
output: number;
|
||||
total: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
cost: number;
|
||||
contextPercent: number;
|
||||
contextWindow: number;
|
||||
}
|
||||
|
||||
export interface UseSocketOptions {
|
||||
@@ -47,6 +52,7 @@ export interface UseSocketReturn {
|
||||
activeToolCalls: ToolCall[];
|
||||
tokenUsage: TokenUsage;
|
||||
modelName: string | null;
|
||||
providerName: string | null;
|
||||
sendMessage: (content: string) => void;
|
||||
connectionError: string | null;
|
||||
}
|
||||
@@ -65,8 +71,18 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn {
|
||||
const [currentThinkingText, setCurrentThinkingText] = useState('');
|
||||
const [activeToolCalls, setActiveToolCalls] = useState<ToolCall[]>([]);
|
||||
// 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<string | null>(null);
|
||||
|
||||
const socketRef = useRef<TypedSocket | null>(null);
|
||||
@@ -191,6 +207,7 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn {
|
||||
activeToolCalls,
|
||||
tokenUsage,
|
||||
modelName,
|
||||
providerName,
|
||||
sendMessage,
|
||||
connectionError,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user