/** * TUI Application * * 主 TUI 应用,整合所有组件 */ import blessed from 'blessed'; import type { Widgets } from 'blessed'; import type { APIClient, Session, Message } from '../client/api.js'; import { ChatView, SessionList, StatusBar } from './components/index.js'; import type { ChatMessage, TUIConfig } from './types.js'; export class TUIApp { private screen: Widgets.Screen; private client: APIClient; private sessionList: SessionList; private chatView: ChatView; private statusBar: StatusBar; private currentSession: Session | null = null; private ws: WebSocket | null = null; private focusTarget: 'sessions' | 'chat' = 'sessions'; constructor(config: TUIConfig) { this.client = config.client; // 创建屏幕 this.screen = blessed.screen({ smartCSR: true, title: 'AI Terminal Assistant', cursor: { artificial: true, shape: 'line', blink: true, color: 'white', }, }); // 创建组件 this.sessionList = new SessionList({ parent: this.screen, left: 0, top: 0, width: '25%', height: '100%-1', }); this.chatView = new ChatView({ parent: this.screen, left: '25%', top: 0, width: '75%', height: '100%-1', }); this.statusBar = new StatusBar({ parent: this.screen, bottom: 0, }); this.setupCallbacks(); this.setupKeyBindings(); // 如果有初始 sessionId,连接到该会话 if (config.sessionId) { this.connectToSession(config.sessionId); } } /** * 设置组件回调 */ private setupCallbacks(): void { // 会话列表回调 this.sessionList.setOnSelect((session) => { this.connectToSession(session.id); }); this.sessionList.setOnCreate(async () => { await this.createSession(); }); this.sessionList.setOnDelete(async (session) => { await this.deleteSession(session); }); // 聊天视图回调 this.chatView.setOnSend((content) => { this.sendMessage(content); }); } /** * 设置全局快捷键 */ private setupKeyBindings(): void { // 退出 this.screen.key(['q', 'C-c'], () => { this.quit(); }); // Tab 切换焦点 this.screen.key('tab', () => { this.toggleFocus(); }); // Escape 切换到会话列表 this.screen.key('escape', () => { this.focusTarget = 'sessions'; this.sessionList.focus(); this.screen.render(); }); } /** * 切换焦点 */ private toggleFocus(): void { if (this.focusTarget === 'sessions') { this.focusTarget = 'chat'; this.chatView.focus(); } else { this.focusTarget = 'sessions'; this.sessionList.focus(); } this.screen.render(); } /** * 启动应用 */ async start(): Promise { // 获取服务器健康状态 try { const health = await this.client.health(); this.statusBar.setStatus({ serverUrl: (this.client as any).baseUrl, connected: true, agentAvailable: health.agent.coreAvailable, }); } catch { this.statusBar.setStatus({ connected: false, }); } // 加载会话列表 await this.loadSessions(); // 聚焦会话列表 this.sessionList.focus(); this.screen.render(); } /** * 加载会话列表 */ private async loadSessions(): Promise { try { const { data: sessions } = await this.client.listSessions(); this.sessionList.setSessions(sessions); } catch (error) { this.showError('Failed to load sessions'); } } /** * 创建新会话 */ private async createSession(): Promise { try { const { data: session } = await this.client.createSession(); await this.loadSessions(); this.connectToSession(session.id); } catch (error) { this.showError('Failed to create session'); } } /** * 删除会话 */ private async deleteSession(session: Session): Promise { try { await this.client.deleteSession(session.id); if (this.currentSession?.id === session.id) { this.disconnectWebSocket(); this.currentSession = null; this.chatView.clearMessages(); this.statusBar.setStatus({ sessionId: undefined, sessionName: undefined }); } await this.loadSessions(); } catch (error) { this.showError('Failed to delete session'); } } /** * 连接到会话 */ private async connectToSession(sessionId: string): Promise { // 断开现有连接 this.disconnectWebSocket(); try { // 获取会话信息 const { data: session } = await this.client.getSession(sessionId); this.currentSession = session; // 更新状态栏 this.statusBar.setStatus({ sessionId: session.id, sessionName: session.name, }); // 高亮选中的会话 this.sessionList.selectSession(sessionId); // 加载历史消息 const { data: messages } = await this.client.getMessages(sessionId); this.chatView.setMessages( messages.map((m) => this.toDisplayMessage(m)) ); // 连接 WebSocket this.connectWebSocket(sessionId); // 切换焦点到聊天视图 this.focusTarget = 'chat'; this.chatView.focus(); this.screen.render(); } catch (error) { this.showError('Failed to connect to session'); } } /** * 连接 WebSocket */ private connectWebSocket(sessionId: string): void { this.ws = this.client.connectWebSocket(sessionId); this.ws.onopen = () => { this.statusBar.setStatus({ connected: true }); }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data as string); this.handleWebSocketMessage(message); } catch { // 忽略解析错误 } }; this.ws.onerror = () => { this.showError('WebSocket error'); }; this.ws.onclose = () => { this.ws = null; }; } /** * 断开 WebSocket */ private disconnectWebSocket(): void { if (this.ws) { this.ws.close(); this.ws = null; } } /** * 处理 WebSocket 消息 */ private handleWebSocketMessage(message: { type: string; payload?: any }): void { switch (message.type) { case 'chunk': this.chatView.appendStreamingContent(message.payload?.content || ''); break; case 'done': if (message.payload?.message) { this.chatView.finishStreaming(this.toDisplayMessage(message.payload.message)); } // 刷新会话列表以更新消息计数 this.loadSessions(); break; case 'error': this.showError(message.payload?.message || 'Unknown error'); break; } } /** * 发送消息 */ private sendMessage(content: string): void { if (!this.ws || !this.currentSession) { this.showError('Not connected to a session'); return; } // 添加用户消息到视图 this.chatView.addMessage({ id: Date.now().toString(), role: 'user', content, timestamp: new Date().toISOString(), }); // 通过 WebSocket 发送 this.ws.send(JSON.stringify({ type: 'message', sessionId: this.currentSession.id, payload: { content }, })); } /** * 转换消息格式 */ private toDisplayMessage(message: Message): ChatMessage { return { id: message.id, role: message.role, content: message.content, timestamp: message.timestamp, }; } /** * 显示错误 */ private showError(message: string): void { const dialog = blessed.message({ parent: this.screen, border: 'line', height: 'shrink', width: 'half', top: 'center', left: 'center', label: ' Error ', tags: true, style: { border: { fg: 'red' }, label: { fg: 'red', bold: true }, }, }); dialog.error(message, () => { dialog.destroy(); this.screen.render(); }); } /** * 退出应用 */ quit(): void { this.disconnectWebSocket(); this.screen.destroy(); process.exit(0); } }