feat(tui): add terminal user interface with Ink

Add a full TUI for Claudeman using Ink (React for CLI):
- StartScreen: Shows existing sessions from screens.json
- TabBar: Session tabs with status indicators
- TerminalView: PTY output display with scrolling
- StatusBar: Session info and keyboard shortcuts
- HelpOverlay: Full keyboard shortcut reference

Features:
- Session discovery from ~/.claudeman/screens.json
- Tab navigation (Ctrl+Tab, Ctrl+1-9)
- Create new sessions (connects to web API)
- Kill sessions via screen commands
- Keyboard-driven navigation

Usage: claudeman tui

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-21 06:19:02 +01:00
co-authored by Claude Opus 4.5
parent b3dd0bcacb
commit 84dab25f38
15 changed files with 1906 additions and 2 deletions
+84
View File
@@ -0,0 +1,84 @@
/**
* @fileoverview TerminalView component
*
* Displays PTY output from the active session.
* Features:
* - Scrollable viewport showing last N lines
* - ANSI color support (handled by Ink)
* - Visual border indicating focus
*/
import React, { useMemo } from 'react';
import { Box, Text } from 'ink';
import type { ScreenSession } from '../../types.js';
interface TerminalViewProps {
output: string;
height: number;
session: ScreenSession | null;
}
/**
* TerminalView component for PTY output display
*/
export function TerminalView({
output,
height,
session,
}: TerminalViewProps): React.ReactElement {
// Split output into lines and get the last N lines that fit
const displayLines = useMemo(() => {
if (!output) return [];
const lines = output.split('\n');
// Reserve 2 lines for border
const visibleLines = height - 2;
if (lines.length <= visibleLines) {
return lines;
}
// Return last N lines
return lines.slice(-visibleLines);
}, [output, height]);
if (!session) {
return (
<Box
flexDirection="column"
borderStyle="single"
borderColor="gray"
height={height}
paddingX={1}
>
<Box flexGrow={1} alignItems="center" justifyContent="center">
<Text dimColor>No session selected</Text>
</Box>
</Box>
);
}
return (
<Box
flexDirection="column"
borderStyle="single"
borderColor="green"
height={height}
paddingX={1}
overflow="hidden"
>
{/* Terminal content */}
<Box flexDirection="column" flexGrow={1}>
{displayLines.length === 0 ? (
<Text dimColor>Waiting for output...</Text>
) : (
displayLines.map((line, index) => (
<Text key={index} wrap="truncate">
{line}
</Text>
))
)}
</Box>
</Box>
);
}