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 { 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 <HelpOverlay onClose={() => setShowHelp(false)} />;
@@ -187,6 +210,7 @@ export function App(): React.ReactElement {
<StartScreen
sessions={sessions}
onSelectSession={handleSelectSession}
onAttachSession={handleAttachSession}
onCreateSession={handleCreateSession}
onRefresh={refreshSessions}
onExit={exit}
+63 -20
View File
@@ -4,16 +4,18 @@
* Initial screen shown when TUI launches. Displays:
* - List of existing screen sessions from ~/.claudeman/screens.json
* - 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 { Box, Text } from 'ink';
import React, { useState } from 'react';
import { Box, Text, useInput } from 'ink';
import type { ScreenSession } from '../../types.js';
interface StartScreenProps {
sessions: ScreenSession[];
onSelectSession: (session: ScreenSession) => 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 (
<Box flexDirection="column" padding={1}>
@@ -82,7 +111,7 @@ export function StartScreen({
{/* Table header */}
<Box marginBottom={1}>
<Text bold>
<Text color="gray">{' # '}</Text>
<Text color="gray">{' '}</Text>
<Text>{'NAME'.padEnd(22)}</Text>
<Text>{'RUNTIME'.padEnd(12)}</Text>
<Text>{'STATUS'.padEnd(10)}</Text>
@@ -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 (
<Box key={session.sessionId}>
<Text>
<Text color="cyan">[{index + 1}]</Text>
<Text> </Text>
<Text>{name.padEnd(22)}</Text>
<Text dimColor>{runtime.padEnd(12)}</Text>
<Text color={statusColor}>
{statusIcon} {statusText.padEnd(8)}
{isSelected ? (
<Text backgroundColor="blue" color="white">
<Text color="cyan" bold>{' \u25B6 '}</Text>
<Text bold>{name.padEnd(22)}</Text>
<Text>{runtime.padEnd(12)}</Text>
<Text color={statusColor}>
{statusIcon} {statusText.padEnd(8)}
</Text>
<Text>{session.mode.padEnd(10)}</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>
);
})}
@@ -121,15 +162,17 @@ export function StartScreen({
<Box borderStyle="single" borderColor="gray" paddingX={1}>
<Text>
<Text color="green">[n]</Text>
<Text> New session </Text>
<Text color="green">[1-9]</Text>
<Text> Select </Text>
<Text> New </Text>
<Text color="green">[{'\u2191\u2193'}]</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> Refresh </Text>
<Text color="yellow">[q]</Text>
<Text> Quit </Text>
<Text color="blue">[?]</Text>
<Text> Help</Text>
<Text> Quit</Text>
</Text>
</Box>
</Box>