feat: 实现 TUI 组件系统

- 添加 blessed 终端 UI 库
- 创建 ChatView 组件:支持消息列表和流式输出
- 创建 SessionList 组件:会话管理和快捷键
- 创建 StatusBar 组件:连接状态显示
- 创建 TUIApp 主应用整合所有组件
- 更新 attach 命令支持 --tui/--no-tui 选项
- 添加 CLAUDE.md 项目规范文件
- 修复 Web 前端 CSS prose 类缺失问题
This commit is contained in:
2025-12-12 11:47:24 +08:00
parent 168996a475
commit da1773b950
12 changed files with 1145 additions and 17 deletions
+368
View File
@@ -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);
}
}