fix(cli): restore componentized app.tsx after rebase, accept initialModel/initialProvider props
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
This commit is contained in:
@@ -1,16 +1,19 @@
|
|||||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Box, Text, useInput, useApp } from 'ink';
|
import { Box, useApp, useInput } from 'ink';
|
||||||
import TextInput from 'ink-text-input';
|
import { TopBar } from './components/top-bar.js';
|
||||||
import Spinner from 'ink-spinner';
|
import { BottomBar } from './components/bottom-bar.js';
|
||||||
import { io, type Socket } from 'socket.io-client';
|
import { MessageList } from './components/message-list.js';
|
||||||
import { fetchAvailableModels, type ModelInfo } from './gateway-api.js';
|
import { InputBar } from './components/input-bar.js';
|
||||||
|
import { Sidebar } from './components/sidebar.js';
|
||||||
|
import { SearchBar } from './components/search-bar.js';
|
||||||
|
import { useSocket } from './hooks/use-socket.js';
|
||||||
|
import { useGitInfo } from './hooks/use-git-info.js';
|
||||||
|
import { useViewport } from './hooks/use-viewport.js';
|
||||||
|
import { useAppMode } from './hooks/use-app-mode.js';
|
||||||
|
import { useConversations } from './hooks/use-conversations.js';
|
||||||
|
import { useSearch } from './hooks/use-search.js';
|
||||||
|
|
||||||
interface Message {
|
export interface TuiAppProps {
|
||||||
role: 'user' | 'assistant' | 'system';
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TuiAppProps {
|
|
||||||
gatewayUrl: string;
|
gatewayUrl: string;
|
||||||
conversationId?: string;
|
conversationId?: string;
|
||||||
sessionCookie?: string;
|
sessionCookie?: string;
|
||||||
@@ -18,375 +21,222 @@ interface TuiAppProps {
|
|||||||
initialProvider?: string;
|
initialProvider?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a slash command from user input.
|
|
||||||
* Returns null if the input is not a slash command.
|
|
||||||
*/
|
|
||||||
function parseSlashCommand(value: string): { command: string; args: string[] } | null {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed.startsWith('/')) return null;
|
|
||||||
const parts = trimmed.slice(1).split(/\s+/);
|
|
||||||
const command = parts[0]?.toLowerCase() ?? '';
|
|
||||||
const args = parts.slice(1);
|
|
||||||
return { command, args };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TuiApp({
|
export function TuiApp({
|
||||||
gatewayUrl,
|
gatewayUrl,
|
||||||
conversationId: initialConversationId,
|
conversationId,
|
||||||
sessionCookie,
|
sessionCookie,
|
||||||
initialModel,
|
initialModel: _initialModel,
|
||||||
initialProvider,
|
initialProvider: _initialProvider,
|
||||||
}: TuiAppProps) {
|
}: TuiAppProps) {
|
||||||
const { exit } = useApp();
|
const { exit } = useApp();
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const gitInfo = useGitInfo();
|
||||||
const [input, setInput] = useState('');
|
const appMode = useAppMode();
|
||||||
const [isStreaming, setIsStreaming] = useState(false);
|
|
||||||
const [connected, setConnected] = useState(false);
|
|
||||||
const [conversationId, setConversationId] = useState(initialConversationId);
|
|
||||||
const [currentStreamText, setCurrentStreamText] = useState('');
|
|
||||||
|
|
||||||
// Model/provider state
|
const socket = useSocket({
|
||||||
const [currentModel, setCurrentModel] = useState<string | undefined>(initialModel);
|
gatewayUrl,
|
||||||
const [currentProvider, setCurrentProvider] = useState<string | undefined>(initialProvider);
|
sessionCookie,
|
||||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
initialConversationId: conversationId,
|
||||||
|
});
|
||||||
|
|
||||||
const socketRef = useRef<Socket | null>(null);
|
const conversations = useConversations({ gatewayUrl, sessionCookie });
|
||||||
const currentStreamTextRef = useRef('');
|
|
||||||
|
|
||||||
// Fetch available models on mount
|
const viewport = useViewport({ totalItems: socket.messages.length });
|
||||||
|
|
||||||
|
const search = useSearch(socket.messages);
|
||||||
|
|
||||||
|
// Scroll to current match when it changes
|
||||||
|
const currentMatch = search.matches[search.currentMatchIndex];
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAvailableModels(gatewayUrl, sessionCookie)
|
if (currentMatch && appMode.mode === 'search') {
|
||||||
.then((models) => {
|
viewport.scrollTo(currentMatch.messageIndex);
|
||||||
setAvailableModels(models);
|
}
|
||||||
// If no model/provider specified and models are available, show the default
|
}, [currentMatch, appMode.mode, viewport]);
|
||||||
if (!initialModel && !initialProvider && models.length > 0) {
|
|
||||||
const first = models[0];
|
|
||||||
if (first) {
|
|
||||||
setCurrentModel(first.id);
|
|
||||||
setCurrentProvider(first.provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Non-fatal: TUI works without model list
|
|
||||||
});
|
|
||||||
}, [gatewayUrl, sessionCookie, initialModel, initialProvider]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Compute highlighted message indices for MessageList
|
||||||
const socket = io(`${gatewayUrl}/chat`, {
|
const highlightedMessageIndices = useMemo(() => {
|
||||||
transports: ['websocket'],
|
if (search.matches.length === 0) return undefined;
|
||||||
extraHeaders: sessionCookie ? { Cookie: sessionCookie } : undefined,
|
return new Set(search.matches.map((m) => m.messageIndex));
|
||||||
});
|
}, [search.matches]);
|
||||||
|
|
||||||
socketRef.current = socket;
|
const currentHighlightIndex = currentMatch?.messageIndex;
|
||||||
|
|
||||||
socket.on('connect', () => setConnected(true));
|
const [sidebarSelectedIndex, setSidebarSelectedIndex] = useState(0);
|
||||||
socket.on('disconnect', () => {
|
|
||||||
setConnected(false);
|
|
||||||
setIsStreaming(false);
|
|
||||||
setCurrentStreamText('');
|
|
||||||
});
|
|
||||||
socket.on('connect_error', (err: Error) => {
|
|
||||||
setMessages((msgs) => [
|
|
||||||
...msgs,
|
|
||||||
{
|
|
||||||
role: 'assistant',
|
|
||||||
content: `Connection failed: ${err.message}. Check that the gateway is running at ${gatewayUrl}.`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('message:ack', (data: { conversationId: string }) => {
|
const handleSwitchConversation = useCallback(
|
||||||
setConversationId(data.conversationId);
|
(id: string) => {
|
||||||
});
|
socket.switchConversation(id);
|
||||||
|
appMode.setMode('chat');
|
||||||
socket.on('agent:start', () => {
|
|
||||||
setIsStreaming(true);
|
|
||||||
currentStreamTextRef.current = '';
|
|
||||||
setCurrentStreamText('');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('agent:text', (data: { text: string }) => {
|
|
||||||
currentStreamTextRef.current += data.text;
|
|
||||||
setCurrentStreamText(currentStreamTextRef.current);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('agent:end', () => {
|
|
||||||
const finalText = currentStreamTextRef.current;
|
|
||||||
currentStreamTextRef.current = '';
|
|
||||||
setCurrentStreamText('');
|
|
||||||
if (finalText) {
|
|
||||||
setMessages((msgs) => [...msgs, { role: 'assistant', content: finalText }]);
|
|
||||||
}
|
|
||||||
setIsStreaming(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('error', (data: { error: string }) => {
|
|
||||||
setMessages((msgs) => [...msgs, { role: 'assistant', content: `Error: ${data.error}` }]);
|
|
||||||
setIsStreaming(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.disconnect();
|
|
||||||
};
|
|
||||||
}, [gatewayUrl]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle /model and /provider slash commands.
|
|
||||||
* Returns true if the input was a handled slash command (should not be sent to gateway).
|
|
||||||
*/
|
|
||||||
const handleSlashCommand = useCallback(
|
|
||||||
(value: string): boolean => {
|
|
||||||
const parsed = parseSlashCommand(value);
|
|
||||||
if (!parsed) return false;
|
|
||||||
|
|
||||||
const { command, args } = parsed;
|
|
||||||
|
|
||||||
if (command === 'model') {
|
|
||||||
if (args.length === 0) {
|
|
||||||
// List available models
|
|
||||||
if (availableModels.length === 0) {
|
|
||||||
setMessages((msgs) => [
|
|
||||||
...msgs,
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content:
|
|
||||||
'No models available (could not reach gateway). Use /model <modelId> to set one manually.',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
const lines = availableModels.map(
|
|
||||||
(m) =>
|
|
||||||
` ${m.provider}/${m.id}${m.id === currentModel && m.provider === currentProvider ? ' (active)' : ''}`,
|
|
||||||
);
|
|
||||||
setMessages((msgs) => [
|
|
||||||
...msgs,
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `Available models:\n${lines.join('\n')}`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Switch model: /model <modelId> or /model <provider>/<modelId>
|
|
||||||
const arg = args[0]!;
|
|
||||||
const slashIdx = arg.indexOf('/');
|
|
||||||
let newProvider: string | undefined;
|
|
||||||
let newModelId: string;
|
|
||||||
|
|
||||||
if (slashIdx !== -1) {
|
|
||||||
newProvider = arg.slice(0, slashIdx);
|
|
||||||
newModelId = arg.slice(slashIdx + 1);
|
|
||||||
} else {
|
|
||||||
newModelId = arg;
|
|
||||||
// Try to find provider from available models list
|
|
||||||
const match = availableModels.find((m) => m.id === newModelId);
|
|
||||||
newProvider = match?.provider ?? currentProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentModel(newModelId);
|
|
||||||
if (newProvider) setCurrentProvider(newProvider);
|
|
||||||
|
|
||||||
setMessages((msgs) => [
|
|
||||||
...msgs,
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `Switched to model: ${newProvider ? `${newProvider}/` : ''}${newModelId}. Takes effect on next message.`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'provider') {
|
|
||||||
if (args.length === 0) {
|
|
||||||
// List providers from available models
|
|
||||||
const providers = [...new Set(availableModels.map((m) => m.provider))];
|
|
||||||
if (providers.length === 0) {
|
|
||||||
setMessages((msgs) => [
|
|
||||||
...msgs,
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content:
|
|
||||||
'No providers available (could not reach gateway). Use /provider <name> to set one manually.',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
const lines = providers.map((p) => ` ${p}${p === currentProvider ? ' (active)' : ''}`);
|
|
||||||
setMessages((msgs) => [
|
|
||||||
...msgs,
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `Available providers:\n${lines.join('\n')}`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const newProvider = args[0]!;
|
|
||||||
setCurrentProvider(newProvider);
|
|
||||||
// If switching provider, auto-select first model for that provider
|
|
||||||
const providerModels = availableModels.filter((m) => m.provider === newProvider);
|
|
||||||
if (providerModels.length > 0 && providerModels[0]) {
|
|
||||||
setCurrentModel(providerModels[0].id);
|
|
||||||
setMessages((msgs) => [
|
|
||||||
...msgs,
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `Switched to provider: ${newProvider} (model: ${providerModels[0]!.id}). Takes effect on next message.`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
setMessages((msgs) => [
|
|
||||||
...msgs,
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `Switched to provider: ${newProvider}. Takes effect on next message.`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === 'help') {
|
|
||||||
setMessages((msgs) => [
|
|
||||||
...msgs,
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: [
|
|
||||||
'Available commands:',
|
|
||||||
' /model — list available models',
|
|
||||||
' /model <id> — switch model (e.g. /model gpt-4o)',
|
|
||||||
' /model <p>/<id> — switch model with provider (e.g. /model ollama/llama3.2)',
|
|
||||||
' /provider — list available providers',
|
|
||||||
' /provider <name> — switch provider (e.g. /provider ollama)',
|
|
||||||
' /help — show this help',
|
|
||||||
].join('\n'),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown slash command — let the user know
|
|
||||||
setMessages((msgs) => [
|
|
||||||
...msgs,
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `Unknown command: /${command}. Type /help for available commands.`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
[availableModels, currentModel, currentProvider],
|
[socket, appMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleDeleteConversation = useCallback(
|
||||||
(value: string) => {
|
(id: string) => {
|
||||||
if (!value.trim() || isStreaming) return;
|
void conversations.deleteConversation(id).then((ok) => {
|
||||||
|
if (ok && id === socket.conversationId) {
|
||||||
setInput('');
|
socket.clearMessages();
|
||||||
|
}
|
||||||
// Handle slash commands first
|
|
||||||
if (handleSlashCommand(value)) return;
|
|
||||||
|
|
||||||
if (!socketRef.current?.connected) {
|
|
||||||
setMessages((msgs) => [
|
|
||||||
...msgs,
|
|
||||||
{ role: 'assistant', content: 'Not connected to gateway. Message not sent.' },
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMessages((msgs) => [...msgs, { role: 'user', content: value }]);
|
|
||||||
|
|
||||||
socketRef.current.emit('message', {
|
|
||||||
conversationId,
|
|
||||||
content: value,
|
|
||||||
provider: currentProvider,
|
|
||||||
modelId: currentModel,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[conversationId, isStreaming, currentModel, currentProvider, handleSlashCommand],
|
[conversations, socket],
|
||||||
);
|
);
|
||||||
|
|
||||||
useInput((ch, key) => {
|
useInput((ch, key) => {
|
||||||
if (key.ctrl && ch === 'c') {
|
if (key.ctrl && ch === 'c') {
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
// Ctrl+L: toggle sidebar (refresh on open)
|
||||||
|
if (key.ctrl && ch === 'l') {
|
||||||
|
const willOpen = !appMode.sidebarOpen;
|
||||||
|
appMode.toggleSidebar();
|
||||||
|
if (willOpen) {
|
||||||
|
void conversations.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ctrl+N: create new conversation and switch to it
|
||||||
|
if (key.ctrl && ch === 'n') {
|
||||||
|
void conversations.createConversation().then((conv) => {
|
||||||
|
if (conv) {
|
||||||
|
socket.switchConversation(conv.id);
|
||||||
|
appMode.setMode('chat');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Ctrl+K: toggle search mode
|
||||||
|
if (key.ctrl && ch === 'k') {
|
||||||
|
if (appMode.mode === 'search') {
|
||||||
|
search.clear();
|
||||||
|
appMode.setMode('chat');
|
||||||
|
} else {
|
||||||
|
appMode.setMode('search');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Page Up / Page Down: scroll message history (only in chat mode)
|
||||||
|
if (appMode.mode === 'chat') {
|
||||||
|
if (key.pageUp) {
|
||||||
|
viewport.scrollBy(-viewport.viewportSize);
|
||||||
|
}
|
||||||
|
if (key.pageDown) {
|
||||||
|
viewport.scrollBy(viewport.viewportSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ctrl+T: cycle thinking level
|
||||||
|
if (key.ctrl && ch === 't') {
|
||||||
|
const levels = socket.availableThinkingLevels;
|
||||||
|
if (levels.length > 0) {
|
||||||
|
const currentIdx = levels.indexOf(socket.thinkingLevel);
|
||||||
|
const nextIdx = (currentIdx + 1) % levels.length;
|
||||||
|
const next = levels[nextIdx];
|
||||||
|
if (next) {
|
||||||
|
socket.setThinkingLevel(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Escape: return to chat from sidebar/search; in chat, scroll to bottom
|
||||||
|
if (key.escape) {
|
||||||
|
if (appMode.mode === 'search') {
|
||||||
|
search.clear();
|
||||||
|
appMode.setMode('chat');
|
||||||
|
} else if (appMode.mode === 'sidebar') {
|
||||||
|
appMode.setMode('chat');
|
||||||
|
} else if (appMode.mode === 'chat') {
|
||||||
|
viewport.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const modelLabel = currentModel
|
const inputPlaceholder =
|
||||||
? currentProvider
|
appMode.mode === 'sidebar'
|
||||||
? `${currentProvider}/${currentModel}`
|
? 'focus is on sidebar… press Esc to return'
|
||||||
: currentModel
|
: appMode.mode === 'search'
|
||||||
: null;
|
? 'search mode… press Esc to return'
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const isSearchMode = appMode.mode === 'search';
|
||||||
|
|
||||||
|
const messageArea = (
|
||||||
|
<Box flexDirection="column" flexGrow={1}>
|
||||||
|
<MessageList
|
||||||
|
messages={socket.messages}
|
||||||
|
isStreaming={socket.isStreaming}
|
||||||
|
currentStreamText={socket.currentStreamText}
|
||||||
|
currentThinkingText={socket.currentThinkingText}
|
||||||
|
activeToolCalls={socket.activeToolCalls}
|
||||||
|
scrollOffset={viewport.scrollOffset}
|
||||||
|
viewportSize={viewport.viewportSize}
|
||||||
|
isScrolledUp={viewport.isScrolledUp}
|
||||||
|
highlightedMessageIndices={highlightedMessageIndices}
|
||||||
|
currentHighlightIndex={currentHighlightIndex}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isSearchMode && (
|
||||||
|
<SearchBar
|
||||||
|
query={search.query}
|
||||||
|
onQueryChange={search.setQuery}
|
||||||
|
totalMatches={search.totalMatches}
|
||||||
|
currentMatch={search.currentMatchIndex}
|
||||||
|
onNext={search.nextMatch}
|
||||||
|
onPrev={search.prevMatch}
|
||||||
|
onClose={() => {
|
||||||
|
search.clear();
|
||||||
|
appMode.setMode('chat');
|
||||||
|
}}
|
||||||
|
focused={isSearchMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<InputBar
|
||||||
|
onSubmit={socket.sendMessage}
|
||||||
|
isStreaming={socket.isStreaming}
|
||||||
|
connected={socket.connected}
|
||||||
|
placeholder={inputPlaceholder}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" padding={1}>
|
<Box flexDirection="column" height="100%">
|
||||||
<Box marginBottom={1}>
|
<Box marginTop={1} />
|
||||||
<Text bold color="blue">
|
<TopBar
|
||||||
Mosaic
|
gatewayUrl={gatewayUrl}
|
||||||
</Text>
|
version="0.0.0"
|
||||||
<Text> </Text>
|
modelName={socket.modelName}
|
||||||
<Text dimColor>{connected ? `connected` : 'connecting...'}</Text>
|
thinkingLevel={socket.thinkingLevel}
|
||||||
{conversationId && <Text dimColor> | {conversationId.slice(0, 8)}</Text>}
|
contextWindow={socket.tokenUsage.contextWindow}
|
||||||
{modelLabel && (
|
agentName="default"
|
||||||
<>
|
connected={socket.connected}
|
||||||
<Text dimColor> | </Text>
|
connecting={socket.connecting}
|
||||||
<Text color="yellow">{modelLabel}</Text>
|
/>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
{appMode.sidebarOpen ? (
|
||||||
{messages.map((msg, i) => (
|
<Box flexDirection="row" flexGrow={1}>
|
||||||
<Box key={i} marginBottom={1}>
|
<Sidebar
|
||||||
{msg.role === 'system' ? (
|
conversations={conversations.conversations}
|
||||||
<Text dimColor italic>
|
activeConversationId={socket.conversationId}
|
||||||
{msg.content}
|
selectedIndex={sidebarSelectedIndex}
|
||||||
</Text>
|
onSelectIndex={setSidebarSelectedIndex}
|
||||||
) : (
|
onSwitchConversation={handleSwitchConversation}
|
||||||
<>
|
onDeleteConversation={handleDeleteConversation}
|
||||||
<Text bold color={msg.role === 'user' ? 'green' : 'cyan'}>
|
loading={conversations.loading}
|
||||||
{msg.role === 'user' ? '> ' : ' '}
|
focused={appMode.mode === 'sidebar'}
|
||||||
</Text>
|
width={30}
|
||||||
<Text wrap="wrap">{msg.content}</Text>
|
/>
|
||||||
</>
|
{messageArea}
|
||||||
)}
|
</Box>
|
||||||
</Box>
|
) : (
|
||||||
))}
|
<Box flexGrow={1}>{messageArea}</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{isStreaming && currentStreamText && (
|
<BottomBar
|
||||||
<Box marginBottom={1}>
|
gitInfo={gitInfo}
|
||||||
<Text bold color="cyan">
|
tokenUsage={socket.tokenUsage}
|
||||||
{' '}
|
connected={socket.connected}
|
||||||
</Text>
|
connecting={socket.connecting}
|
||||||
<Text wrap="wrap">{currentStreamText}</Text>
|
modelName={socket.modelName}
|
||||||
</Box>
|
providerName={socket.providerName}
|
||||||
)}
|
thinkingLevel={socket.thinkingLevel}
|
||||||
|
conversationId={socket.conversationId}
|
||||||
{isStreaming && !currentStreamText && (
|
/>
|
||||||
<Box>
|
|
||||||
<Text color="cyan">
|
|
||||||
<Spinner type="dots" />
|
|
||||||
</Text>
|
|
||||||
<Text dimColor> thinking...</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Text bold color="green">
|
|
||||||
{'> '}
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
|
||||||
value={input}
|
|
||||||
onChange={setInput}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
placeholder={isStreaming ? 'waiting...' : 'type a message or /help'}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user