diff --git a/src/tui/App.tsx b/src/tui/App.tsx index 5949ec3e..308a4fe9 100644 --- a/src/tui/App.tsx +++ b/src/tui/App.tsx @@ -8,6 +8,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Box, Text, useApp, useInput, useStdout } from 'ink'; +import { execSync, spawnSync } from 'child_process'; import { StartScreen } from './components/StartScreen.js'; import { TabBar } from './components/TabBar.js'; import { TerminalView } from './components/TerminalView.js'; @@ -176,6 +177,28 @@ export function App(): React.ReactElement { } }, [createSession]); + /** + * Attach directly to a screen session + * This exits the TUI and attaches to GNU screen + */ + const handleAttachSession = useCallback((session: ScreenSession) => { + // Clear screen and restore terminal + process.stdout.write('\x1b[2J\x1b[H'); + console.log(`Attaching to screen: ${session.screenName}`); + console.log('Press Ctrl+A D to detach and return to terminal\n'); + + // Use spawnSync to attach to screen (inherits stdio) + const result = spawnSync('screen', ['-r', session.screenName], { + stdio: 'inherit', + }); + + // After detaching, exit the TUI + if (result.status === 0) { + console.log('\nDetached from screen. Run "claudeman tui" to return.'); + } + exit(); + }, [exit]); + // Render help overlay if shown if (showHelp) { return setShowHelp(false)} />; @@ -187,6 +210,7 @@ export function App(): React.ReactElement { void; + onAttachSession: (session: ScreenSession) => void; onCreateSession: () => void; onRefresh: () => void; onExit: () => void; @@ -41,16 +43,43 @@ function formatDuration(ms: number): string { } /** - * Start screen component showing session list + * Start screen component showing session list with arrow key navigation */ export function StartScreen({ sessions, onSelectSession, + onAttachSession, onCreateSession, onRefresh, onExit, }: StartScreenProps): React.ReactElement { const now = Date.now(); + const [selectedIndex, setSelectedIndex] = useState(0); + + // Handle keyboard input for navigation + useInput((input, key) => { + // Arrow key navigation + if (key.upArrow && sessions.length > 0) { + setSelectedIndex((prev) => (prev > 0 ? prev - 1 : sessions.length - 1)); + return; + } + if (key.downArrow && sessions.length > 0) { + setSelectedIndex((prev) => (prev < sessions.length - 1 ? prev + 1 : 0)); + return; + } + + // Enter to view session in TUI + if (key.return && sessions.length > 0) { + onSelectSession(sessions[selectedIndex]); + return; + } + + // 'a' to attach directly to screen + if (input === 'a' && sessions.length > 0 && sessions[selectedIndex].attached) { + onAttachSession(sessions[selectedIndex]); + return; + } + }); return ( @@ -82,7 +111,7 @@ export function StartScreen({ {/* Table header */} - {' # '} + {' '} {'NAME'.padEnd(22)} {'RUNTIME'.padEnd(12)} {'STATUS'.padEnd(10)} @@ -97,19 +126,31 @@ export function StartScreen({ const statusIcon = session.attached ? '\u25CF' : '\u25CB'; const statusText = session.attached ? 'alive' : 'dead'; const name = (session.name || 'unnamed').slice(0, 20); + const isSelected = index === selectedIndex; return ( - - [{index + 1}] - - {name.padEnd(22)} - {runtime.padEnd(12)} - - {statusIcon} {statusText.padEnd(8)} + {isSelected ? ( + + {' \u25B6 '} + {name.padEnd(22)} + {runtime.padEnd(12)} + + {statusIcon} {statusText.padEnd(8)} + + {session.mode.padEnd(10)} - {session.mode.padEnd(10)} - + ) : ( + + {' '} + {name.padEnd(22)} + {runtime.padEnd(12)} + + {statusIcon} {statusText.padEnd(8)} + + {session.mode.padEnd(10)} + + )} ); })} @@ -121,15 +162,17 @@ export function StartScreen({ [n] - New session - [1-9] - Select + New + [{'\u2191\u2193'}] + Navigate + [Enter] + View + [a] + Attach [r] Refresh [q] - Quit - [?] - Help + Quit