feat: 实现 TUI 组件系统
- 添加 blessed 终端 UI 库 - 创建 ChatView 组件:支持消息列表和流式输出 - 创建 SessionList 组件:会话管理和快捷键 - 创建 StatusBar 组件:连接状态显示 - 创建 TUIApp 主应用整合所有组件 - 更新 attach 命令支持 --tui/--no-tui 选项 - 添加 CLAUDE.md 项目规范文件 - 修复 Web 前端 CSS prose 类缺失问题
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* 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<void> {
|
||||
// 获取服务器健康状态
|
||||
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<void> {
|
||||
try {
|
||||
const { data: sessions } = await this.client.listSessions();
|
||||
this.sessionList.setSessions(sessions);
|
||||
} catch (error) {
|
||||
this.showError('Failed to load sessions');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*/
|
||||
private async createSession(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
// 断开现有连接
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user