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
+128
View File
@@ -0,0 +1,128 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Important Rules
- **No backward compatibility**: This is a new project. Do not add any backward-compatible code, deprecated APIs, migration shims, or legacy support. Remove old code entirely when refactoring instead of keeping it around "just in case".
- **Git commits**: When asked to commit, only create the commit (no push). Commit messages must not contain any Claude-related information (no "Co-Authored-By", no "Generated with Claude", etc.).
- **Documentation**: All documentation must be stored under `docs/`. When implementing features based on design documents, update the corresponding document after coding is complete to reflect the current implementation status.
## Project Overview
AI Terminal Assistant is a terminal-based AI coding assistant built with Claude API. It's structured as a **pnpm monorepo** with four packages:
- **@ai-assistant/core** - Agent engine, tools, LSP integration, checkpoint system
- **@ai-assistant/server** - HTTP Server (Hono + Bun) with REST API, WebSocket, SSE
- **@ai-assistant/cli** - Command-line interface with `serve` and `attach` commands
- **@ai-assistant/web** - React frontend (Vite + Tailwind CSS)
## Build & Development Commands
```bash
# Install dependencies (use pnpm, not npm)
pnpm install
# Build all packages
pnpm build
# Run tests
pnpm test # All packages
pnpm --filter @ai-assistant/core test # Single package
pnpm --filter @ai-assistant/core test:watch # Watch mode
# Start development
pnpm server:dev # Start HTTP server (port 3000)
cd packages/web && pnpm dev # Start web UI (port 5173)
# CLI commands
cd packages/cli && bun run src/index.ts serve --port 3000
cd packages/cli && bun run src/index.ts attach http://localhost:3000
# Type checking
cd packages/web && pnpm typecheck
cd packages/server && pnpm build
```
## Architecture
### Package Dependencies
```
web / cli
server → REST API + WebSocket + SSE
core → Agent / Tools / LSP / Checkpoint / Hooks / MCP
```
### Core Package (`packages/core/src/`)
Key modules:
- `core/agent.ts` - Main Agent class using AI SDK's `streamText`
- `tools/` - Tool registry with bash, file operations, search, etc.
- `editors/` - Unified edit mode system (whole/diff/search-replace)
- `lsp/` - Language Server Protocol integration
- `checkpoint/` - Shadow Git checkpoint system
- `hooks/` - Pre/post execution hooks
- `mcp/` - Model Context Protocol client
- `repomap/` - Repository map generation with tree-sitter
### Server Package (`packages/server/src/`)
- `index.ts` - Hono app setup with CORS, auth middleware
- `routes/` - REST endpoints (sessions, tools, config)
- `ws.ts` - WebSocket handler for real-time chat
- `sse.ts` - Server-Sent Events for status updates
- `agent/adapter.ts` - Bridges core Agent to server (dynamic import to avoid build dependency)
- `auth/token.ts` - Token-based authentication (auto-enabled for non-localhost)
### Communication Flow
1. Client connects via WebSocket to `/api/ws/:sessionId`
2. Client sends `{ type: 'message', payload: { content } }`
3. Server calls `processMessage()` → Agent.chat() with streaming callback
4. Server pushes `chunk` events back via WebSocket
5. On completion, server sends `done` event with full response
## Key Design Patterns
### Dynamic Core Module Loading
Server uses dynamic import to avoid build-time dependency on core:
```typescript
const corePath = '@ai-assistant/core';
const core = await import(corePath) as CoreModule;
```
### Tool Registration
Tools are registered in `toolRegistry` with Zod schemas:
```typescript
toolRegistry.register({
name: 'read_file',
description: '...',
parameters: z.object({ path: z.string() }),
execute: async (params) => { ... }
});
```
### Session Management
Each WebSocket connection binds to a session. Sessions hold:
- Message history
- Status (idle/busy)
- Agent instance (cached per session)
## Configuration
- **Environment**: `ANTHROPIC_API_KEY` required in `.env`
- **Default model**: `claude-sonnet-4-20250514`
- **Server auth**: Auto-generates token when host is not localhost
## Design Documentation
Architecture details are in `docs/design/architecture/gui-server-client.md`, including:
- Implementation roadmap with completion status
- API endpoint specifications
- Communication protocol definitions
+3 -1
View File
@@ -20,9 +20,11 @@
"commander": "^12.1.0",
"chalk": "^5.3.0",
"ora": "^8.0.1",
"inquirer": "^9.2.12"
"inquirer": "^9.2.12",
"blessed": "^0.1.81"
},
"devDependencies": {
"@types/blessed": "^0.1.25",
"@types/bun": "^1.1.0",
"@types/node": "^20.11.0",
"typescript": "^5.3.3"
+14 -1
View File
@@ -16,8 +16,10 @@ export function registerAttachCommand(program: Command): void {
.argument('<url>', 'Server URL (e.g., http://192.168.1.100:3000)')
.option('-t, --token <token>', 'Authentication token')
.option('-s, --session <id>', 'Session ID to connect to')
.option('--tui', 'Use TUI mode (default)', true)
.option('--no-tui', 'Use simple CLI mode')
.action(async (url: string, options) => {
const { token, session: sessionId } = options;
const { token, session: sessionId, tui } = options;
const spinner = ora('Connecting to server...').start();
@@ -32,6 +34,16 @@ export function registerAttachCommand(program: Command): void {
const health = await client.health();
spinner.succeed(`Connected to server at ${url}`);
if (tui) {
// TUI 模式
const { TUIApp } = await import('../tui/index.js');
const app = new TUIApp({
client,
sessionId,
});
await app.start();
} else {
// 简单 CLI 模式
console.log(chalk.gray('─'.repeat(50)));
console.log(chalk.bold('Server Status:'));
console.log(` Status: ${chalk.green(health.status)}`);
@@ -48,6 +60,7 @@ export function registerAttachCommand(program: Command): void {
// 显示可用 sessions 或创建新的
await showSessionMenu(client);
}
}
} catch (error) {
spinner.fail('Failed to connect to server');
if (error instanceof Error) {
+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);
}
}
+236
View File
@@ -0,0 +1,236 @@
/**
* ChatView Component
*
* 显示聊天消息和输入框
*/
import blessed from 'blessed';
import type { Widgets } from 'blessed';
import type { ChatMessage } from '../types.js';
export interface ChatViewOptions {
parent: Widgets.Screen;
left: number | string;
top: number | string;
width: number | string;
height: number | string;
}
export class ChatView {
private container: Widgets.BoxElement;
private messageList: Widgets.BoxElement;
private inputBox: Widgets.TextboxElement;
private messages: ChatMessage[] = [];
private streamingContent = '';
private onSend?: (content: string) => void;
constructor(options: ChatViewOptions) {
// 主容器
this.container = blessed.box({
parent: options.parent,
left: options.left,
top: options.top,
width: options.width,
height: options.height,
border: { type: 'line' },
label: ' Chat ',
style: {
border: { fg: 'cyan' },
label: { fg: 'cyan', bold: true },
},
});
// 消息列表区域
this.messageList = blessed.box({
parent: this.container,
top: 0,
left: 0,
right: 0,
bottom: 3,
scrollable: true,
alwaysScroll: true,
scrollbar: {
ch: '│',
style: { fg: 'cyan' },
},
mouse: true,
keys: true,
vi: true,
tags: true,
style: {
fg: 'white',
},
});
// 输入框
this.inputBox = blessed.textbox({
parent: this.container,
bottom: 0,
left: 0,
right: 0,
height: 3,
border: { type: 'line' },
label: ' Message (Enter to send, Esc to cancel) ',
style: {
border: { fg: 'green' },
label: { fg: 'green' },
focus: {
border: { fg: 'yellow' },
},
},
inputOnFocus: true,
});
this.setupKeyBindings();
}
private setupKeyBindings(): void {
// 输入框提交
this.inputBox.on('submit', (value) => {
const content = value.trim();
if (content && this.onSend) {
this.onSend(content);
}
this.inputBox.clearValue();
this.inputBox.focus();
this.render();
});
// 取消输入
this.inputBox.on('cancel', () => {
this.inputBox.clearValue();
this.render();
});
}
/**
* 设置消息发送回调
*/
setOnSend(callback: (content: string) => void): void {
this.onSend = callback;
}
/**
* 添加消息
*/
addMessage(message: ChatMessage): void {
this.messages.push(message);
this.updateMessageList();
}
/**
* 设置所有消息
*/
setMessages(messages: ChatMessage[]): void {
this.messages = messages;
this.updateMessageList();
}
/**
* 更新流式内容
*/
appendStreamingContent(content: string): void {
this.streamingContent += content;
this.updateMessageList();
}
/**
* 完成流式输出
*/
finishStreaming(finalMessage: ChatMessage): void {
this.streamingContent = '';
// 替换或添加最终消息
const lastMsg = this.messages[this.messages.length - 1];
if (lastMsg && lastMsg.isStreaming) {
this.messages[this.messages.length - 1] = finalMessage;
} else {
this.messages.push(finalMessage);
}
this.updateMessageList();
}
/**
* 清空消息
*/
clearMessages(): void {
this.messages = [];
this.streamingContent = '';
this.updateMessageList();
}
/**
* 更新消息列表显示
*/
private updateMessageList(): void {
const lines: string[] = [];
for (const msg of this.messages) {
const prefix = msg.role === 'user'
? '{blue-fg}{bold}You:{/bold}{/blue-fg} '
: '{green-fg}{bold}AI:{/bold}{/green-fg} ';
// 处理多行消息
const contentLines = msg.content.split('\n');
contentLines.forEach((line, i) => {
if (i === 0) {
lines.push(prefix + line);
} else {
lines.push(' ' + line);
}
});
lines.push('');
}
// 添加流式内容
if (this.streamingContent) {
const streamLines = this.streamingContent.split('\n');
streamLines.forEach((line, i) => {
if (i === 0) {
lines.push('{green-fg}{bold}AI:{/bold}{/green-fg} ' + line);
} else {
lines.push(' ' + line);
}
});
lines.push('{yellow-fg}▌{/yellow-fg}'); // 光标指示
}
this.messageList.setContent(lines.join('\n'));
this.scrollToBottom();
this.render();
}
/**
* 滚动到底部
*/
private scrollToBottom(): void {
this.messageList.setScrollPerc(100);
}
/**
* 聚焦输入框
*/
focus(): void {
this.inputBox.focus();
}
/**
* 渲染
*/
render(): void {
this.container.screen.render();
}
/**
* 销毁
*/
destroy(): void {
this.container.destroy();
}
/**
* 获取容器元素
*/
getElement(): Widgets.BoxElement {
return this.container;
}
}
@@ -0,0 +1,197 @@
/**
* SessionList Component
*
* 显示会话列表
*/
import blessed from 'blessed';
import type { Widgets } from 'blessed';
import type { Session } from '../../client/api.js';
export interface SessionListOptions {
parent: Widgets.Screen;
left: number | string;
top: number | string;
width: number | string;
height: number | string;
}
export class SessionList {
private container: Widgets.BoxElement;
private list: Widgets.ListElement;
private sessions: Session[] = [];
private onSelect?: (session: Session) => void;
private onCreate?: () => void;
private onDelete?: (session: Session) => void;
constructor(options: SessionListOptions) {
// 主容器
this.container = blessed.box({
parent: options.parent,
left: options.left,
top: options.top,
width: options.width,
height: options.height,
border: { type: 'line' },
label: ' Sessions ',
style: {
border: { fg: 'cyan' },
label: { fg: 'cyan', bold: true },
},
});
// 会话列表
this.list = blessed.list({
parent: this.container,
top: 0,
left: 0,
right: 0,
bottom: 2,
keys: true,
vi: true,
mouse: true,
style: {
selected: {
bg: 'blue',
fg: 'white',
bold: true,
},
item: {
fg: 'white',
},
},
scrollbar: {
ch: '│',
style: { fg: 'cyan' },
},
});
// 底部提示
blessed.box({
parent: this.container,
bottom: 0,
left: 0,
right: 0,
height: 1,
content: '{gray-fg}n: New d: Delete Enter: Select{/gray-fg}',
tags: true,
style: {
fg: 'gray',
},
});
this.setupKeyBindings();
}
private setupKeyBindings(): void {
// 选择会话
this.list.on('select', (_item, index) => {
if (index === 0 && this.onCreate) {
// 第一项是 "New Session"
this.onCreate();
} else if (this.sessions[index - 1] && this.onSelect) {
this.onSelect(this.sessions[index - 1]);
}
});
// 快捷键: n 创建新会话
this.list.key('n', () => {
if (this.onCreate) {
this.onCreate();
}
});
// 快捷键: d 删除会话
this.list.key('d', () => {
const selectedIndex = (this.list as any).selected;
if (selectedIndex > 0 && this.sessions[selectedIndex - 1] && this.onDelete) {
this.onDelete(this.sessions[selectedIndex - 1]);
}
});
}
/**
* 设置会话选择回调
*/
setOnSelect(callback: (session: Session) => void): void {
this.onSelect = callback;
}
/**
* 设置创建会话回调
*/
setOnCreate(callback: () => void): void {
this.onCreate = callback;
}
/**
* 设置删除会话回调
*/
setOnDelete(callback: (session: Session) => void): void {
this.onDelete = callback;
}
/**
* 设置会话列表
*/
setSessions(sessions: Session[]): void {
this.sessions = sessions;
this.updateList();
}
/**
* 更新列表显示
*/
private updateList(): void {
const items = [
'{green-fg}+ New Session{/green-fg}',
...this.sessions.map((s) => {
const name = s.name || s.id.slice(0, 8);
const count = s.messageCount || 0;
return `${name} (${count} msgs)`;
}),
];
this.list.setItems(items);
this.render();
}
/**
* 高亮指定会话
*/
selectSession(sessionId: string): void {
const index = this.sessions.findIndex((s) => s.id === sessionId);
if (index >= 0) {
this.list.select(index + 1); // +1 因为第一项是 "New Session"
this.render();
}
}
/**
* 聚焦列表
*/
focus(): void {
this.list.focus();
}
/**
* 渲染
*/
render(): void {
this.container.screen.render();
}
/**
* 销毁
*/
destroy(): void {
this.container.destroy();
}
/**
* 获取容器元素
*/
getElement(): Widgets.BoxElement {
return this.container;
}
}
@@ -0,0 +1,105 @@
/**
* StatusBar Component
*
* 显示状态栏
*/
import blessed from 'blessed';
import type { Widgets } from 'blessed';
export interface StatusBarOptions {
parent: Widgets.Screen;
bottom: number;
}
export interface StatusInfo {
serverUrl: string;
connected: boolean;
sessionId?: string;
sessionName?: string;
agentAvailable: boolean;
}
export class StatusBar {
private container: Widgets.BoxElement;
private status: StatusInfo = {
serverUrl: '',
connected: false,
agentAvailable: false,
};
constructor(options: StatusBarOptions) {
this.container = blessed.box({
parent: options.parent,
bottom: options.bottom,
left: 0,
right: 0,
height: 1,
tags: true,
style: {
bg: 'blue',
fg: 'white',
},
});
this.updateContent();
}
/**
* 更新状态
*/
setStatus(status: Partial<StatusInfo>): void {
this.status = { ...this.status, ...status };
this.updateContent();
}
/**
* 更新显示内容
*/
private updateContent(): void {
const parts: string[] = [];
// 连接状态
const connIcon = this.status.connected ? '{green-fg}●{/green-fg}' : '{red-fg}●{/red-fg}';
parts.push(`${connIcon} ${this.status.serverUrl || 'Not connected'}`);
// Agent 状态
if (this.status.connected) {
const agentIcon = this.status.agentAvailable ? '{green-fg}✓{/green-fg}' : '{yellow-fg}✗{/yellow-fg}';
parts.push(`Agent: ${agentIcon}`);
}
// 当前会话
if (this.status.sessionId) {
const name = this.status.sessionName || this.status.sessionId.slice(0, 8);
parts.push(`Session: ${name}`);
}
// 快捷键提示
parts.push('{gray-fg}Tab: Switch q: Quit{/gray-fg}');
this.container.setContent(' ' + parts.join(' │ '));
this.render();
}
/**
* 渲染
*/
render(): void {
this.container.screen.render();
}
/**
* 销毁
*/
destroy(): void {
this.container.destroy();
}
/**
* 获取容器元素
*/
getElement(): Widgets.BoxElement {
return this.container;
}
}
+7
View File
@@ -0,0 +1,7 @@
/**
* TUI Components Export
*/
export { ChatView, type ChatViewOptions } from './ChatView.js';
export { SessionList, type SessionListOptions } from './SessionList.js';
export { StatusBar, type StatusBarOptions, type StatusInfo } from './StatusBar.js';
+7
View File
@@ -0,0 +1,7 @@
/**
* TUI Module Export
*/
export { TUIApp } from './App.js';
export { ChatView, SessionList, StatusBar } from './components/index.js';
export type { TUIConfig, TUIState, ChatMessage, TUIEvent, TUIEventType } from './types.js';
+45
View File
@@ -0,0 +1,45 @@
/**
* TUI Types
*/
import type { APIClient, Session, Message, HealthStatus } from '../client/api.js';
export interface TUIConfig {
client: APIClient;
sessionId?: string;
}
export interface TUIState {
sessions: Session[];
currentSession: Session | null;
messages: Message[];
health: HealthStatus | null;
isConnected: boolean;
isLoading: boolean;
error: string | null;
}
export interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: string;
isStreaming?: boolean;
}
export type TUIEventType =
| 'session:select'
| 'session:create'
| 'session:delete'
| 'message:send'
| 'message:chunk'
| 'message:done'
| 'connection:open'
| 'connection:close'
| 'connection:error'
| 'quit';
export interface TUIEvent<T = unknown> {
type: TUIEventType;
payload?: T;
}
+1 -1
View File
@@ -23,7 +23,7 @@
/* Message content */
.message-content {
@apply prose prose-invert max-w-none;
@apply max-w-none text-gray-100;
}
.message-content pre {
+20
View File
@@ -17,6 +17,9 @@ importers:
'@ai-assistant/server':
specifier: workspace:*
version: link:../server
blessed:
specifier: ^0.1.81
version: 0.1.81
chalk:
specifier: ^5.3.0
version: 5.6.2
@@ -30,6 +33,9 @@ importers:
specifier: ^8.0.1
version: 8.2.0
devDependencies:
'@types/blessed':
specifier: ^0.1.25
version: 0.1.27
'@types/bun':
specifier: ^1.1.0
version: 1.3.4
@@ -808,6 +814,9 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/blessed@0.1.27':
resolution: {integrity: sha512-ZOQGjLvWDclAXp0rW5iuUBXeD6Gr1PkitN7tj7/G8FCoSzTsij6OhXusOzMKhwrZ9YlL2Pmu0d6xJ9zVvk+Hsg==}
'@types/bun@1.3.4':
resolution: {integrity: sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA==}
@@ -967,6 +976,11 @@ packages:
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
blessed@0.1.81:
resolution: {integrity: sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==}
engines: {node: '>= 0.8.0'}
hasBin: true
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
@@ -2411,6 +2425,10 @@ snapshots:
dependencies:
'@babel/types': 7.28.5
'@types/blessed@0.1.27':
dependencies:
'@types/node': 22.19.2
'@types/bun@1.3.4':
dependencies:
bun-types: 1.3.4
@@ -2592,6 +2610,8 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
blessed@0.1.81: {}
braces@3.0.3:
dependencies:
fill-range: 7.1.1