import React, { useState, useCallback, useEffect, useRef } from 'react'; import { Box, Text, useInput, useApp } from 'ink'; import TextInput from 'ink-text-input'; import Spinner from 'ink-spinner'; import { io, type Socket } from 'socket.io-client'; interface Message { role: 'user' | 'assistant'; content: string; } interface TuiAppProps { gatewayUrl: string; conversationId?: string; } export function TuiApp({ gatewayUrl, conversationId: initialConversationId }: TuiAppProps) { const { exit } = useApp(); const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isStreaming, setIsStreaming] = useState(false); const [connected, setConnected] = useState(false); const [conversationId, setConversationId] = useState(initialConversationId); const [currentStreamText, setCurrentStreamText] = useState(''); const socketRef = useRef(null); useEffect(() => { const socket = io(`${gatewayUrl}/chat`, { transports: ['websocket'], }); socketRef.current = socket; socket.on('connect', () => setConnected(true)); 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 }) => { setConversationId(data.conversationId); }); socket.on('agent:start', () => { setIsStreaming(true); setCurrentStreamText(''); }); socket.on('agent:text', (data: { text: string }) => { setCurrentStreamText((prev) => prev + data.text); }); socket.on('agent:end', () => { setCurrentStreamText((prev) => { if (prev) { setMessages((msgs) => [...msgs, { role: 'assistant', content: prev }]); } return ''; }); setIsStreaming(false); }); socket.on('error', (data: { error: string }) => { setMessages((msgs) => [...msgs, { role: 'assistant', content: `Error: ${data.error}` }]); setIsStreaming(false); }); return () => { socket.disconnect(); }; }, [gatewayUrl]); const handleSubmit = useCallback( (value: string) => { if (!value.trim() || isStreaming) 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 }]); setInput(''); socketRef.current.emit('message', { conversationId, content: value, }); }, [conversationId, isStreaming], ); useInput((ch, key) => { if (key.ctrl && ch === 'c') { exit(); } }); return ( Mosaic {connected ? `connected` : 'connecting...'} {conversationId && | {conversationId.slice(0, 8)}} {messages.map((msg, i) => ( {msg.role === 'user' ? '> ' : ' '} {msg.content} ))} {isStreaming && currentStreamText && ( {' '} {currentStreamText} )} {isStreaming && !currentStreamText && ( thinking... )} {'> '} ); }