import * as readline from 'readline'; import chalk from 'chalk'; import type { Agent } from '../core/agent.js'; import { agentRegistry } from '../agent/index.js'; export class TerminalUI { private agent: Agent; private rl: readline.Interface; private isClosed = false; constructor(agent: Agent) { this.agent = agent; this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true, }); // 监听关闭事件 this.rl.on('close', () => { this.isClosed = true; }); } // 显示欢迎信息 private showWelcome(): void { console.log(chalk.cyan('\n╔════════════════════════════════════════╗')); console.log(chalk.cyan('║') + chalk.bold.white(' 🤖 AI Terminal Assistant ') + chalk.cyan('║')); console.log(chalk.cyan('║') + chalk.gray(' Powered by DeepSeek / Claude ') + chalk.cyan('║')); console.log(chalk.cyan('╚════════════════════════════════════════╝\n')); console.log(chalk.gray('输入你的问题,或使用以下命令:')); console.log(chalk.yellow(' /help') + chalk.gray(' - 显示帮助')); console.log(chalk.yellow(' /agent') + chalk.gray(' - 切换 Agent 模式')); console.log(chalk.yellow(' /clear') + chalk.gray(' - 清空对话历史')); console.log(chalk.yellow(' /compact') + chalk.gray(' - 压缩对话历史')); console.log(chalk.yellow(' /context') + chalk.gray(' - 查看上下文使用情况')); console.log(chalk.yellow(' /exit') + chalk.gray(' - 退出程序')); console.log(''); } // 格式化上下文使用情况(带颜色) private formatContextUsage(): string { const usage = this.agent.getContextUsage(); const percent = usage.usagePercent; // 根据使用率选择颜色 let colorFn: (text: string) => string; if (percent < 50) { colorFn = chalk.green; } else if (percent < 80) { colorFn = chalk.yellow; } else { colorFn = chalk.red; } const used = usage.input >= 1000 ? `${(usage.input / 1000).toFixed(1)}k` : `${usage.input}`; const limit = `${(usage.available / 1000).toFixed(0)}k`; return colorFn(`[${used}/${limit}]`); } // 处理 /agent 命令 private handleAgentCommand(args: string): boolean { const agentName = args.trim(); // 无参数:显示当前模式和可用 Agent if (!agentName) { const currentMode = this.agent.getAgentModeName(); const availableAgents = agentRegistry.listPrimaryAgents(); console.log(chalk.cyan('\n🤖 Agent 模式:')); console.log(chalk.white(` 当前: ${currentMode === 'default' ? chalk.green('default (通用助手)') : chalk.yellow(currentMode)}`)); console.log(''); console.log(chalk.white(' 可用 Agent:')); console.log(chalk.gray(' • default - 通用编程助手')); for (const agent of availableAgents) { const marker = agent.name === currentMode ? chalk.green(' ✓') : ''; console.log(chalk.gray(` • ${agent.name} - ${agent.description}${marker}`)); } console.log(''); console.log(chalk.gray(' 用法: /agent 切换到指定 Agent')); console.log(chalk.gray(' /agent default 切换回默认模式')); console.log(''); return true; } // 切换到默认模式 if (agentName === 'default') { this.agent.setAgentMode(null); console.log(chalk.green('\n✓ 已切换到 default (通用助手) 模式\n')); return true; } // 切换到指定 Agent const agent = agentRegistry.get(agentName); if (!agent) { const availableNames = ['default', ...agentRegistry.listPrimaryAgents().map(a => a.name)]; console.log(chalk.red(`\n✗ 未找到 Agent: ${agentName}`)); console.log(chalk.gray(` 可用: ${availableNames.join(', ')}\n`)); return true; } // 检查是否为 subagent 模式(不能作为主交互 Agent) if (agent.mode === 'subagent') { console.log(chalk.yellow(`\n⚠ Agent "${agentName}" 是 subagent 模式,只能通过 task 工具调用`)); console.log(chalk.gray(' 提示: 使用 /agent 查看可交互的 Agent 列表\n')); return true; } this.agent.setAgentMode(agent); console.log(chalk.green(`\n✓ 已切换到 ${agent.name} 模式`)); console.log(chalk.gray(` ${agent.description}\n`)); return true; } // 处理特殊命令 private async handleCommand(input: string): Promise { const command = input.toLowerCase().trim(); // 检查 /agent 命令(带参数) if (command.startsWith('/agent')) { const args = input.substring(6).trim(); return this.handleAgentCommand(args); } switch (command) { case '/help': console.log(chalk.cyan('\n📖 帮助信息:')); console.log(chalk.white(' 这是一个 AI 编程助手,可以帮你:')); console.log(chalk.gray(' • 读写文件')); console.log(chalk.gray(' • 执行 bash 命令')); console.log(chalk.gray(' • 搜索代码')); console.log(chalk.gray(' • 回答编程问题')); console.log(''); console.log(chalk.white(' 命令:')); console.log(chalk.gray(' • /help - 显示此帮助')); console.log(chalk.gray(' • /agent - 切换 Agent 模式')); console.log(chalk.gray(' • /clear - 清空对话历史')); console.log(chalk.gray(' • /compact - 压缩对话历史,释放上下文空间')); console.log(chalk.gray(' • /context - 显示当前上下文使用情况')); console.log(chalk.gray(' • /exit - 退出程序')); console.log(''); return true; case '/clear': await this.agent.clearHistory(); console.log(chalk.green('✓ 对话历史已清空\n')); return true; case '/compact': console.log(chalk.yellow('正在压缩对话历史...\n')); try { const beforeUsage = this.agent.getContextUsage(); const result = await this.agent.compactHistory(); const afterUsage = this.agent.getContextUsage(); if (result.freedTokens > 0) { console.log(chalk.green(`✓ 压缩完成!`)); console.log(chalk.gray(` 策略: ${result.type}`)); console.log(chalk.gray(` 释放: ${(result.freedTokens / 1000).toFixed(1)}k tokens`)); console.log(chalk.gray(` 之前: ${(beforeUsage.input / 1000).toFixed(1)}k`)); console.log(chalk.gray(` 之后: ${(afterUsage.input / 1000).toFixed(1)}k`)); } else { console.log(chalk.yellow('没有可压缩的内容')); } console.log(''); } catch (error) { console.log(chalk.red(`压缩失败: ${error instanceof Error ? error.message : String(error)}\n`)); } return true; case '/context': const usage = this.agent.getContextUsage(); console.log(chalk.cyan('\n📊 上下文使用情况:')); console.log(chalk.gray(` 已使用: ${(usage.input / 1000).toFixed(1)}k tokens`)); console.log(chalk.gray(` 可用: ${(usage.available / 1000).toFixed(0)}k tokens`)); console.log(chalk.gray(` 上下文限制: ${(usage.contextLimit / 1000).toFixed(0)}k tokens`)); console.log(chalk.gray(` 使用率: ${usage.usagePercent.toFixed(1)}%`)); console.log(''); return true; case '/exit': case '/quit': console.log(chalk.cyan('\n👋 再见!\n')); this.close(); process.exit(0); default: return false; } } // 提问并获取用户输入 private prompt(): Promise { return new Promise((resolve, reject) => { if (this.isClosed) { reject(new Error('readline closed')); return; } // 显示带上下文使用情况的提示符 const contextInfo = this.formatContextUsage(); const agentMode = this.agent.getAgentModeName(); const agentIndicator = agentMode === 'default' ? '' : chalk.magenta(`@${agentMode} `); this.rl.question(`${contextInfo} ${agentIndicator}${chalk.green('You >')} `, (answer) => { resolve(answer ?? ''); }); }); } // 主循环 async start(): Promise { this.showWelcome(); while (!this.isClosed) { try { const input = await this.prompt(); // 跳过空输入 if (!input.trim()) { continue; } // 处理命令 if (input.startsWith('/')) { if (await this.handleCommand(input)) { continue; } } // 发送给 AI process.stdout.write(chalk.gray('思考中...')); try { let isFirstChunk = true; await this.agent.chat(input, (text) => { if (isFirstChunk) { // 清除 "思考中..." 并显示 AI 前缀 process.stdout.write('\r' + ' '.repeat(20) + '\r'); process.stdout.write(chalk.blue('AI > ')); isFirstChunk = false; } // 处理工具调用的输出 if (text.startsWith('\n[调用工具:')) { process.stdout.write(chalk.yellow(text)); } else if (text.startsWith('[结果:') || text.startsWith('[错误:')) { process.stdout.write(chalk.gray(text)); } else { process.stdout.write(text); } }); console.log('\n'); } catch (error) { // 清除 "思考中..." process.stdout.write('\r' + ' '.repeat(20) + '\r'); console.log( chalk.red( `❌ 错误: ${error instanceof Error ? error.message : String(error)}\n` ) ); } } catch { // readline 关闭,退出循环 break; } } console.log(chalk.cyan('\n👋 再见!\n')); } // 关闭 close(): void { if (!this.isClosed) { this.isClosed = true; this.rl.close(); } } }