feat: enhance TUI with colors, command hints, and improved rendering
This commit is contained in:
@@ -3,7 +3,7 @@ import { Box, useApp, useInput } from 'ink';
|
||||
import { StatusBar } from './StatusBar.js';
|
||||
import { MessageList } from './MessageList.js';
|
||||
import { InputBar } from './InputBar.js';
|
||||
import { parseCommand, getHelpText, resolveModelAlias } from '../commands.js';
|
||||
import { parseCommand, getHelpText, resolveModelAlias, getCommandCompletions } from '../commands.js';
|
||||
import type { Message, ModelClient, TokenUsage } from '../../../models/types.js';
|
||||
import type { ModelRouter } from '../../../models/router.js';
|
||||
import type { ManagedSession } from '../../../session/index.js';
|
||||
@@ -43,6 +43,21 @@ export function App({
|
||||
onExit?.();
|
||||
exit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Tab completion for slash commands
|
||||
if (key.tab && !isStreaming) {
|
||||
const completions = getCommandCompletions(input);
|
||||
if (completions.length > 0) {
|
||||
setInput(completions[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only handle scroll keys, ignore regular typing
|
||||
if (!key.upArrow && !key.downArrow && !key.pageUp && !key.pageDown && !key.tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll handling
|
||||
@@ -80,42 +95,60 @@ export function App({
|
||||
setScrollOffset(0);
|
||||
return;
|
||||
|
||||
case 'help':
|
||||
case 'help': {
|
||||
// Show help as system message
|
||||
setMessages(prev => [...prev, { role: 'assistant', content: getHelpText() }]);
|
||||
const helpMsg: Message = { role: 'assistant', content: getHelpText() };
|
||||
const helpWithTs = session.addMessage(helpMsg);
|
||||
setMessages(prev => [...prev, helpWithTs]);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'status':
|
||||
case 'status': {
|
||||
const status = `Session: ${session.id}\nMessages: ${messages.length}\nTokens: ${tokenUsage.inputTokens} in / ${tokenUsage.outputTokens} out`;
|
||||
setMessages(prev => [...prev, { role: 'assistant', content: status }]);
|
||||
const statusMsg: Message = { role: 'assistant', content: status };
|
||||
const statusWithTs = session.addMessage(statusMsg);
|
||||
setMessages(prev => [...prev, statusWithTs]);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'model':
|
||||
case 'model': {
|
||||
if (!modelRouter) {
|
||||
setMessages(prev => [...prev, { role: 'assistant', content: 'Model switching not available.' }]);
|
||||
const errMsg: Message = { role: 'assistant', content: 'Model switching not available.' };
|
||||
const errWithTs = session.addMessage(errMsg);
|
||||
setMessages(prev => [...prev, errWithTs]);
|
||||
return;
|
||||
}
|
||||
if (!command.name) {
|
||||
const info = `Current: ${modelRouter.getTier()}\nAvailable: ${modelRouter.getAvailableTiers().join(', ')}`;
|
||||
setMessages(prev => [...prev, { role: 'assistant', content: info }]);
|
||||
const infoMsg: Message = { role: 'assistant', content: info };
|
||||
const infoWithTs = session.addMessage(infoMsg);
|
||||
setMessages(prev => [...prev, infoWithTs]);
|
||||
return;
|
||||
}
|
||||
const tier = resolveModelAlias(command.name);
|
||||
if (modelRouter.setTier(tier)) {
|
||||
setCurrentModel(tier);
|
||||
setMessages(prev => [...prev, { role: 'assistant', content: `Switched to model: ${tier}` }]);
|
||||
const successMsg: Message = { role: 'assistant', content: `Switched to model: ${tier}` };
|
||||
const successWithTs = session.addMessage(successMsg);
|
||||
setMessages(prev => [...prev, successWithTs]);
|
||||
} else {
|
||||
setMessages(prev => [...prev, { role: 'assistant', content: `Model not available: ${command.name}` }]);
|
||||
const failMsg: Message = { role: 'assistant', content: `Model not available: ${command.name}` };
|
||||
const failWithTs = session.addMessage(failMsg);
|
||||
setMessages(prev => [...prev, failWithTs]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case 'fullscreen':
|
||||
// Already in fullscreen
|
||||
return;
|
||||
|
||||
case 'transfer':
|
||||
setMessages(prev => [...prev, { role: 'assistant', content: `Transfer not supported in fullscreen mode.` }]);
|
||||
case 'transfer': {
|
||||
const xferMsg: Message = { role: 'assistant', content: `Transfer not supported in fullscreen mode.` };
|
||||
const xferWithTs = session.addMessage(xferMsg);
|
||||
setMessages(prev => [...prev, xferWithTs]);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'message':
|
||||
break; // Continue to message handling
|
||||
@@ -125,8 +158,8 @@ export function App({
|
||||
|
||||
// Add user message
|
||||
const userMessage: Message = { role: 'user', content: command.content };
|
||||
session.addMessage(userMessage);
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
const messageWithTimestamp = session.addMessage(userMessage);
|
||||
setMessages(prev => [...prev, messageWithTimestamp]);
|
||||
setScrollOffset(0); // Auto-scroll to bottom
|
||||
|
||||
// Stream response
|
||||
@@ -163,8 +196,8 @@ export function App({
|
||||
}
|
||||
|
||||
const assistantMessage: Message = { role: 'assistant', content: fullContent };
|
||||
session.addMessage(assistantMessage);
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
const assistantWithTimestamp = session.addMessage(assistantMessage);
|
||||
setMessages(prev => [...prev, assistantWithTimestamp]);
|
||||
} else {
|
||||
// Fallback to non-streaming
|
||||
const response = await modelClient.chat({
|
||||
@@ -178,15 +211,16 @@ export function App({
|
||||
}));
|
||||
|
||||
const assistantMessage: Message = { role: 'assistant', content: response.content };
|
||||
session.addMessage(assistantMessage);
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
const assistantWithTimestamp = session.addMessage(assistantMessage);
|
||||
setMessages(prev => [...prev, assistantWithTimestamp]);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
};
|
||||
setMessages(prev => [...prev, errorMessage]);
|
||||
const errorWithTimestamp = session.addMessage(errorMessage);
|
||||
setMessages(prev => [...prev, errorWithTimestamp]);
|
||||
} finally {
|
||||
setIsStreaming(false);
|
||||
setStreamingContent('');
|
||||
@@ -195,13 +229,6 @@ export function App({
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" height="100%">
|
||||
<StatusBar
|
||||
sessionId={session.id}
|
||||
messageCount={messages.length}
|
||||
model={currentModel}
|
||||
tokenUsage={{ input: tokenUsage.inputTokens, output: tokenUsage.outputTokens }}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
<MessageList
|
||||
messages={messages}
|
||||
scrollOffset={scrollOffset}
|
||||
@@ -214,6 +241,13 @@ export function App({
|
||||
isLoading={isStreaming}
|
||||
placeholder={isStreaming ? 'Flynn is typing... (Esc to cancel)' : 'Type a message... (Esc=exit, /help)'}
|
||||
/>
|
||||
<StatusBar
|
||||
sessionId={session.id}
|
||||
messageCount={messages.length}
|
||||
model={currentModel}
|
||||
tokenUsage={{ input: tokenUsage.inputTokens, output: tokenUsage.outputTokens }}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user