feat(tui): add arrow key navigation and screen attach

- Add arrow key navigation in StartScreen (up/down to move)
- Highlight selected session with blue background
- Press Enter to view session in TUI mode
- Press 'a' to attach directly to GNU screen session
- Attach exits TUI and inherits stdio for full terminal control

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-21 06:24:07 +01:00
co-authored by Claude Opus 4.5
parent 451de8485a
commit 8da6dcedfa
2 changed files with 87 additions and 20 deletions
+24
View File
@@ -8,6 +8,7 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text, useApp, useInput, useStdout } from 'ink'; import { Box, Text, useApp, useInput, useStdout } from 'ink';
import { execSync, spawnSync } from 'child_process';
import { StartScreen } from './components/StartScreen.js'; import { StartScreen } from './components/StartScreen.js';
import { TabBar } from './components/TabBar.js'; import { TabBar } from './components/TabBar.js';
import { TerminalView } from './components/TerminalView.js'; import { TerminalView } from './components/TerminalView.js';
@@ -176,6 +177,28 @@ export function App(): React.ReactElement {
} }
}, [createSession]); }, [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 // Render help overlay if shown
if (showHelp) { if (showHelp) {
return <HelpOverlay onClose={() => setShowHelp(false)} />; return <HelpOverlay onClose={() => setShowHelp(false)} />;
@@ -187,6 +210,7 @@ export function App(): React.ReactElement {
<StartScreen <StartScreen
sessions={sessions} sessions={sessions}
onSelectSession={handleSelectSession} onSelectSession={handleSelectSession}
onAttachSession={handleAttachSession}
onCreateSession={handleCreateSession} onCreateSession={handleCreateSession}
onRefresh={refreshSessions} onRefresh={refreshSessions}
onExit={exit} onExit={exit}
+63 -20
View File
@@ -4,16 +4,18 @@
* Initial screen shown when TUI launches. Displays: * Initial screen shown when TUI launches. Displays:
* - List of existing screen sessions from ~/.claudeman/screens.json * - List of existing screen sessions from ~/.claudeman/screens.json
* - Session status (alive/dead), runtime, mode * - Session status (alive/dead), runtime, mode
* - Options to select, create, or refresh sessions * - Arrow key navigation with highlighted selection
* - Options to select, create, attach, or refresh sessions
*/ */
import React from 'react'; import React, { useState } from 'react';
import { Box, Text } from 'ink'; import { Box, Text, useInput } from 'ink';
import type { ScreenSession } from '../../types.js'; import type { ScreenSession } from '../../types.js';
interface StartScreenProps { interface StartScreenProps {
sessions: ScreenSession[]; sessions: ScreenSession[];
onSelectSession: (session: ScreenSession) => void; onSelectSession: (session: ScreenSession) => void;
onAttachSession: (session: ScreenSession) => void;
onCreateSession: () => void; onCreateSession: () => void;
onRefresh: () => void; onRefresh: () => void;
onExit: () => 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({ export function StartScreen({
sessions, sessions,
onSelectSession, onSelectSession,
onAttachSession,
onCreateSession, onCreateSession,
onRefresh, onRefresh,
onExit, onExit,
}: StartScreenProps): React.ReactElement { }: StartScreenProps): React.ReactElement {
const now = Date.now(); 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 ( return (
<Box flexDirection="column" padding={1}> <Box flexDirection="column" padding={1}>
@@ -82,7 +111,7 @@ export function StartScreen({
{/* Table header */} {/* Table header */}
<Box marginBottom={1}> <Box marginBottom={1}>
<Text bold> <Text bold>
<Text color="gray">{' # '}</Text> <Text color="gray">{' '}</Text>
<Text>{'NAME'.padEnd(22)}</Text> <Text>{'NAME'.padEnd(22)}</Text>
<Text>{'RUNTIME'.padEnd(12)}</Text> <Text>{'RUNTIME'.padEnd(12)}</Text>
<Text>{'STATUS'.padEnd(10)}</Text> <Text>{'STATUS'.padEnd(10)}</Text>
@@ -97,19 +126,31 @@ export function StartScreen({
const statusIcon = session.attached ? '\u25CF' : '\u25CB'; const statusIcon = session.attached ? '\u25CF' : '\u25CB';
const statusText = session.attached ? 'alive' : 'dead'; const statusText = session.attached ? 'alive' : 'dead';
const name = (session.name || 'unnamed').slice(0, 20); const name = (session.name || 'unnamed').slice(0, 20);
const isSelected = index === selectedIndex;
return ( return (
<Box key={session.sessionId}> <Box key={session.sessionId}>
<Text> {isSelected ? (
<Text color="cyan">[{index + 1}]</Text> <Text backgroundColor="blue" color="white">
<Text> </Text> <Text color="cyan" bold>{' \u25B6 '}</Text>
<Text>{name.padEnd(22)}</Text> <Text bold>{name.padEnd(22)}</Text>
<Text dimColor>{runtime.padEnd(12)}</Text> <Text>{runtime.padEnd(12)}</Text>
<Text color={statusColor}> <Text color={statusColor}>
{statusIcon} {statusText.padEnd(8)} {statusIcon} {statusText.padEnd(8)}
</Text>
<Text>{session.mode.padEnd(10)}</Text>
</Text> </Text>
<Text>{session.mode.padEnd(10)}</Text> ) : (
</Text> <Text>
<Text color="gray">{' '}</Text>
<Text>{name.padEnd(22)}</Text>
<Text dimColor>{runtime.padEnd(12)}</Text>
<Text color={statusColor}>
{statusIcon} {statusText.padEnd(8)}
</Text>
<Text>{session.mode.padEnd(10)}</Text>
</Text>
)}
</Box> </Box>
); );
})} })}
@@ -121,15 +162,17 @@ export function StartScreen({
<Box borderStyle="single" borderColor="gray" paddingX={1}> <Box borderStyle="single" borderColor="gray" paddingX={1}>
<Text> <Text>
<Text color="green">[n]</Text> <Text color="green">[n]</Text>
<Text> New session </Text> <Text> New </Text>
<Text color="green">[1-9]</Text> <Text color="green">[{'\u2191\u2193'}]</Text>
<Text> Select </Text> <Text> Navigate </Text>
<Text color="green">[Enter]</Text>
<Text> View </Text>
<Text color="green">[a]</Text>
<Text> Attach </Text>
<Text color="green">[r]</Text> <Text color="green">[r]</Text>
<Text> Refresh </Text> <Text> Refresh </Text>
<Text color="yellow">[q]</Text> <Text color="yellow">[q]</Text>
<Text> Quit </Text> <Text> Quit</Text>
<Text color="blue">[?]</Text>
<Text> Help</Text>
</Text> </Text>
</Box> </Box>
</Box> </Box>