mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
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:
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user