import React, { useState, useEffect } from 'react';
import { Box, Text, useApp, useInput } from 'ink';
import TextInput from 'ink-text-input';
import { Header } from './components/Header.js';
import { StepList } from './components/StepList.js';
import { Footer } from './components/Footer.js';
import { useSession } from './hooks/useSession.js';
import type { Step } from './types.js';
interface AppProps {
sessionId?: string;
}
function isStepBlocked(step: Step, allSteps: Step[]): boolean {
if (!step.dependsOn || step.dependsOn.length === 0) {
return false;
}
return step.dependsOn.some((depId) => {
const depStep = allSteps.find((s) => s.id === depId);
return depStep && depStep.status !== 'completed';
});
}
export function App({ sessionId }: AppProps) {
const { exit } = useApp();
const { session, loading, error, updateStep, cancelSession } = useSession(sessionId);
const [selectedIndex, setSelectedIndex] = useState(0);
const [feedbackMode, setFeedbackMode] = useState(false);
const [feedbackValue, setFeedbackValue] = useState('');
useInput((input, key) => {
if (!session || session.status !== 'active') {
return;
}
// Handle escape in feedback mode
if (feedbackMode && key.escape) {
setFeedbackMode(false);
setFeedbackValue('');
return;
}
// Don't process other keys in feedback mode (let TextInput handle them)
if (feedbackMode) {
return;
}
const currentStep = session.steps[selectedIndex];
if (key.upArrow) {
setSelectedIndex((i) => Math.max(0, i - 1));
} else if (key.downArrow) {
setSelectedIndex((i) => Math.min(session.steps.length - 1, i + 1));
} else if (input === ' ' || key.return) {
// Toggle step completion
if (currentStep && !isStepBlocked(currentStep, session.steps)) {
const newStatus = currentStep.status === 'completed' ? 'pending' : 'completed';
updateStep(currentStep.id, newStatus, currentStep.notes);
}
} else if (input === 's') {
// Toggle skip step
if (currentStep && !isStepBlocked(currentStep, session.steps)) {
const newStatus = currentStep.status === 'skipped' ? 'pending' : 'skipped';
updateStep(currentStep.id, newStatus, currentStep.notes);
}
} else if (input === 'f') {
// Enter feedback mode
if (currentStep) {
setFeedbackValue(currentStep.notes || '');
setFeedbackMode(true);
}
} else if (input === 'c') {
// Cancel session
cancelSession();
}
});
const handleFeedbackSubmit = async (value: string) => {
if (!session) return;
const currentStep = session.steps[selectedIndex];
if (currentStep) {
await updateStep(currentStep.id, currentStep.status, value || undefined);
}
setFeedbackMode(false);
setFeedbackValue('');
};
// Auto-exit when complete or cancelled
useEffect(() => {
if (session?.status === 'completed' || session?.status === 'cancelled') {
const timer = setTimeout(() => exit(), 1500);
return () => clearTimeout(timer);
}
}, [session?.status, exit]);
if (loading) {
return (
<Box padding={1}>
<Text>Loading session...</Text>
</Box>
);
}
if (error) {
return (
<Box padding={1}>
<Text color="red">Error: {error.message}</Text>
</Box>
);
}
if (!session) {
return (
<Box flexDirection="column" padding={1}>
<Text color="yellow">No active session found.</Text>
<Text dimColor>Waiting for Claude to create a step list...</Text>
</Box>
);
}
const completedCount = session.steps.filter(s => s.status === 'completed').length;
const currentStep = session.steps[selectedIndex];
return (
<Box flexDirection="column" padding={1}>
<Header title={session.title} description={session.description} />
<StepList steps={session.steps} selectedIndex={selectedIndex} />
{feedbackMode && currentStep && (
<Box flexDirection="column" marginTop={1} borderStyle="round" borderColor="cyan" padding={1}>
<Text color="cyan" bold>Add feedback for: {currentStep.title}</Text>
<Box marginTop={1}>
<Text color="gray">💬 </Text>
<TextInput
value={feedbackValue}
onChange={setFeedbackValue}
onSubmit={handleFeedbackSubmit}
placeholder="Enter feedback (press Enter to save, Esc to cancel)"
/>
</Box>
</Box>
)}
<Footer
completedCount={completedCount}
totalCount={session.steps.length}
status={session.status}
feedbackMode={feedbackMode}
/>
</Box>
);
}