feat: add Ink-based fullscreen TUI components

This commit is contained in:
William Valentin
2026-02-05 00:39:53 -08:00
parent c0deeb5cf0
commit 53a8bd97eb
6 changed files with 197 additions and 1 deletions
+98
View File
@@ -0,0 +1,98 @@
import React, { useState, useCallback } from 'react';
import { Box, useApp, useInput } from 'ink';
import { StatusBar } from './StatusBar.js';
import { MessageList } from './MessageList.js';
import { InputBar } from './InputBar.js';
import type { Message, ModelClient } from '../../../models/types.js';
import type { ManagedSession } from '../../../session/index.js';
export interface AppProps {
session: ManagedSession;
modelClient: ModelClient;
systemPrompt: string;
model: string;
onExit?: () => void;
}
export function App({
session,
modelClient,
systemPrompt,
model,
onExit,
}: AppProps): React.ReactElement {
const { exit } = useApp();
const [input, setInput] = useState('');
const [messages, setMessages] = useState<Message[]>(session.getHistory());
const [isLoading, setIsLoading] = useState(false);
useInput((inputChar, key) => {
if (key.escape) {
onExit?.();
exit();
}
});
const handleSubmit = useCallback(async (value: string) => {
const trimmed = value.trim();
if (!trimmed || isLoading) return;
// Handle commands
if (trimmed === '/quit' || trimmed === '/exit') {
onExit?.();
exit();
return;
}
if (trimmed === '/reset' || trimmed === '/clear') {
session.clear();
setMessages([]);
setInput('');
return;
}
// Regular message
const userMessage: Message = { role: 'user', content: trimmed };
session.addMessage(userMessage);
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsLoading(true);
try {
const response = await modelClient.chat({
messages: session.getHistory(),
system: systemPrompt,
});
const assistantMessage: Message = { role: 'assistant', content: response.content };
session.addMessage(assistantMessage);
setMessages(prev => [...prev, assistantMessage]);
} catch (error) {
const errorMessage: Message = {
role: 'assistant',
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
}, [isLoading, session, modelClient, systemPrompt, exit, onExit]);
return (
<Box flexDirection="column" height="100%">
<StatusBar
sessionId={session.id}
messageCount={messages.length}
model={model}
/>
<MessageList messages={messages} />
<InputBar
value={input}
onChange={setInput}
onSubmit={handleSubmit}
isLoading={isLoading}
placeholder="Type a message... (Esc to exit)"
/>
</Box>
);
}