feat(cli): match TUI footer to reference design
- 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
This commit is contained in:
@@ -47,13 +47,20 @@ export function TuiApp({ gatewayUrl, conversationId, sessionCookie }: TuiAppProp
|
|||||||
activeToolCalls={socket.activeToolCalls}
|
activeToolCalls={socket.activeToolCalls}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BottomBar gitInfo={gitInfo} tokenUsage={socket.tokenUsage} />
|
|
||||||
|
|
||||||
<InputBar
|
<InputBar
|
||||||
onSubmit={socket.sendMessage}
|
onSubmit={socket.sendMessage}
|
||||||
isStreaming={socket.isStreaming}
|
isStreaming={socket.isStreaming}
|
||||||
connected={socket.connected}
|
connected={socket.connected}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<BottomBar
|
||||||
|
gitInfo={gitInfo}
|
||||||
|
tokenUsage={socket.tokenUsage}
|
||||||
|
connected={socket.connected}
|
||||||
|
connecting={socket.connecting}
|
||||||
|
modelName={socket.modelName}
|
||||||
|
providerName={socket.providerName}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,55 +6,101 @@ import type { GitInfo } from '../hooks/use-git-info.js';
|
|||||||
export interface BottomBarProps {
|
export interface BottomBarProps {
|
||||||
gitInfo: GitInfo;
|
gitInfo: GitInfo;
|
||||||
tokenUsage: TokenUsage;
|
tokenUsage: TokenUsage;
|
||||||
|
connected: boolean;
|
||||||
|
connecting: boolean;
|
||||||
|
modelName: string | null;
|
||||||
|
providerName: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTokens(n: number): string {
|
function formatTokens(n: number): string {
|
||||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
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);
|
return String(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compact the cwd — show ~ for home, truncate long paths */
|
/** Compact the cwd — replace home with ~ */
|
||||||
function compactCwd(cwd: string): string {
|
function compactCwd(cwd: string): string {
|
||||||
const home = process.env['HOME'] ?? '';
|
const home = process.env['HOME'] ?? '';
|
||||||
if (home && cwd.startsWith(home)) {
|
if (home && cwd.startsWith(home)) {
|
||||||
cwd = '~' + cwd.slice(home.length);
|
return '~' + 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;
|
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;
|
const hasTokens = tokenUsage.total > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box borderStyle="single" borderColor="gray" paddingX={1} justifyContent="space-between">
|
<Box flexDirection="column" paddingX={0} marginTop={0}>
|
||||||
<Box>
|
{/* Line 1: path (branch) ····· Gateway: Status */}
|
||||||
<Text dimColor>cwd: </Text>
|
<Box justifyContent="space-between">
|
||||||
<Text>{compactCwd(gitInfo.cwd)}</Text>
|
<Box>
|
||||||
{gitInfo.branch && (
|
<Text dimColor>{compactCwd(gitInfo.cwd)}</Text>
|
||||||
<>
|
{gitInfo.branch && <Text dimColor> ({gitInfo.branch})</Text>}
|
||||||
<Text dimColor> ⎇ </Text>
|
</Box>
|
||||||
<Text color="cyan">{gitInfo.branch}</Text>
|
<Box>
|
||||||
</>
|
<Text dimColor>Gateway: </Text>
|
||||||
)}
|
<Text color={gatewayColor}>{gatewayStatus}</Text>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
|
||||||
{hasTokens ? (
|
{/* Line 2: token stats ····· (provider) model */}
|
||||||
<>
|
<Box justifyContent="space-between">
|
||||||
<Text dimColor>tokens: </Text>
|
<Box>
|
||||||
<Text color="green">↑{formatTokens(tokenUsage.input)}</Text>
|
{hasTokens ? (
|
||||||
<Text dimColor> / </Text>
|
<>
|
||||||
<Text color="yellow">↓{formatTokens(tokenUsage.output)}</Text>
|
<Text dimColor>^{formatTokens(tokenUsage.input)}</Text>
|
||||||
<Text dimColor> ({formatTokens(tokenUsage.total)})</Text>
|
<Text dimColor>{' '}</Text>
|
||||||
</>
|
<Text dimColor>v{formatTokens(tokenUsage.output)}</Text>
|
||||||
) : (
|
{tokenUsage.cacheRead > 0 && (
|
||||||
<Text dimColor>tokens: —</Text>
|
<>
|
||||||
)}
|
<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>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,12 +26,10 @@ export function InputBar({ onSubmit, isStreaming, connected }: InputBarProps) {
|
|||||||
? 'waiting for response…'
|
? 'waiting for response…'
|
||||||
: 'message mosaic…';
|
: 'message mosaic…';
|
||||||
|
|
||||||
const promptColor = !connected ? 'red' : isStreaming ? 'yellow' : 'green';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box paddingX={1} borderStyle="single" borderColor="gray">
|
<Box paddingX={0}>
|
||||||
<Text bold color={promptColor}>
|
<Text bold color="green">
|
||||||
❯{' '}
|
{'❯ '}
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={input}
|
value={input}
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ export interface TokenUsage {
|
|||||||
input: number;
|
input: number;
|
||||||
output: number;
|
output: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
cost: number;
|
||||||
|
contextPercent: number;
|
||||||
|
contextWindow: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseSocketOptions {
|
export interface UseSocketOptions {
|
||||||
@@ -47,6 +52,7 @@ export interface UseSocketReturn {
|
|||||||
activeToolCalls: ToolCall[];
|
activeToolCalls: ToolCall[];
|
||||||
tokenUsage: TokenUsage;
|
tokenUsage: TokenUsage;
|
||||||
modelName: string | null;
|
modelName: string | null;
|
||||||
|
providerName: string | null;
|
||||||
sendMessage: (content: string) => void;
|
sendMessage: (content: string) => void;
|
||||||
connectionError: string | null;
|
connectionError: string | null;
|
||||||
}
|
}
|
||||||
@@ -65,8 +71,18 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn {
|
|||||||
const [currentThinkingText, setCurrentThinkingText] = useState('');
|
const [currentThinkingText, setCurrentThinkingText] = useState('');
|
||||||
const [activeToolCalls, setActiveToolCalls] = useState<ToolCall[]>([]);
|
const [activeToolCalls, setActiveToolCalls] = useState<ToolCall[]>([]);
|
||||||
// TODO: wire up once gateway emits token-usage and model-info events
|
// 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 modelName: string | null = null;
|
||||||
|
const providerName: string | null = null;
|
||||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||||
|
|
||||||
const socketRef = useRef<TypedSocket | null>(null);
|
const socketRef = useRef<TypedSocket | null>(null);
|
||||||
@@ -191,6 +207,7 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn {
|
|||||||
activeToolCalls,
|
activeToolCalls,
|
||||||
tokenUsage,
|
tokenUsage,
|
||||||
modelName,
|
modelName,
|
||||||
|
providerName,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
connectionError,
|
connectionError,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user