feat(cli): TUI component architecture — status bars, message list, input bar

- Split monolithic app.tsx into composable components:
  - TopBar: connection indicator (●/○), gateway URL, model name, conversation ID
  - BottomBar: cwd, git branch, token usage
  - MessageList: timestamped messages, tool call indicators, thinking display
  - InputBar: context-aware prompt with streaming/disconnect states
- Extract socket logic into useSocket hook with typed events
- Extract git/cwd info into useGitInfo hook
- Quiet disconnect: single indicator instead of error flood
- Add @mosaic/types dependency for typed Socket.IO events
- Add PRD and task tracking docs

Tasks: TUI-001 through TUI-007 (Wave 1)
This commit is contained in:
2026-03-15 13:33:37 -05:00
parent 09e649fc7e
commit 79ff308aad
11 changed files with 668 additions and 153 deletions

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { Box, Text } from 'ink';
import type { TokenUsage } from '../hooks/use-socket.js';
import type { GitInfo } from '../hooks/use-git-info.js';
export interface BottomBarProps {
gitInfo: GitInfo;
tokenUsage: TokenUsage;
}
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`;
return String(n);
}
/** Compact the cwd — show ~ for home, truncate long paths */
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;
}
export function BottomBar({ gitInfo, tokenUsage }: BottomBarProps) {
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>
<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>
)}
</Box>
</Box>
);
}