UNPKG

openai-cli-unofficial

Version:

A powerful OpenAI CLI Coding Agent built with TypeScript

714 lines 31.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MainPage = void 0; const boxen_1 = __importDefault(require("boxen")); const chalk_1 = __importDefault(require("chalk")); const language_1 = require("../../services/language"); const storage_1 = require("../../services/storage"); const system_detector_1 = require("../../services/system-detector"); const utils_1 = require("../../utils"); const components_1 = require("../components"); class MainPage { constructor() { this.messages = []; this.chatState = { canSendMessage: true, isProcessing: false }; this.loadingController = null; this.isDestroyed = false; this.configChangeListener = null; this.currentMessages = language_1.languageService.getMessages(); this.commandManager = new components_1.CommandManager(this.currentMessages); this.helpManager = new components_1.HelpManager(this.currentMessages); this.responseManager = new components_1.ResponseManager(this.currentMessages); this.fileSearchManager = new components_1.FileSearchManager(); this.inputHandler = new components_1.InputHandler(this.commandManager, this.fileSearchManager, this.currentMessages); this.initHandler = new components_1.InitHandler(this.currentMessages); this.systemDetector = new system_detector_1.SystemDetector(); // 创建 MessageHandler 回调 const messageHandlerCallbacks = { onStateChange: (state) => { this.setChatState(state); }, onLoadingStart: (controller) => { this.loadingController = controller; }, onLoadingStop: () => { if (this.loadingController) { this.loadingController.stop(); this.loadingController = null; } }, getSelectedImageFiles: () => { return this.getSelectedImageFiles(); }, getSelectedTextFiles: () => { return this.getSelectedTextFiles(); }, addMessage: (message) => { this.messages.push(message); }, getRecentMessages: () => { return this.messages; }, getSystemDetector: () => { return this.systemDetector; } }; this.messageHandler = new components_1.MessageHandler(this.currentMessages, messageHandlerCallbacks); // 监听语言变化 language_1.languageService.onLanguageChange((language) => { this.updateLanguage(); }); // 监听配置变更 this.configChangeListener = () => { // 在非聊天状态下刷新欢迎框显示 if (this.chatState.canSendMessage && !this.chatState.isProcessing) { this.refreshWelcomeBox(); } }; storage_1.StorageService.onConfigChange(this.configChangeListener); } // 销毁方法,清理所有资源 destroy() { if (this.isDestroyed) return; this.isDestroyed = true; // 移除配置变更监听器 if (this.configChangeListener) { storage_1.StorageService.removeConfigChangeListener(this.configChangeListener); this.configChangeListener = null; } // 停止loading动画 if (this.loadingController) { this.loadingController.stop(); this.loadingController = null; } // 清理SystemDetector资源 this.systemDetector.cleanup().catch(error => { // 忽略清理错误 }); // 重置聊天状态 this.setChatState({ canSendMessage: false, isProcessing: false }); // 清空消息 this.messages = []; // 重置命令管理器状态 this.commandManager.resetStates(); // 清除选中文件列表 this.clearSelectedFiles(); // 清理stdin状态 if (process.stdin.isTTY) { try { process.stdin.setRawMode(false); } catch (error) { // 忽略错误 } } // 移除所有可能的事件监听器 process.stdin.removeAllListeners('data'); process.stdin.removeAllListeners('error'); process.stdin.removeAllListeners('end'); try { process.stdin.pause(); } catch (error) { // 忽略错误 } } updateLanguage() { this.currentMessages = language_1.languageService.getMessages(); this.commandManager.updateLanguage(this.currentMessages); this.helpManager.updateLanguage(this.currentMessages); this.responseManager.updateLanguage(this.currentMessages); this.inputHandler.updateLanguage(this.currentMessages); this.initHandler.updateLanguage(this.currentMessages); this.messageHandler.updateLanguage(this.currentMessages); } // 公开API:注入AI回复 injectAIReply(content) { // 停止loading动画 if (this.loadingController) { this.loadingController.stop(); this.loadingController = null; } this.messageHandler.injectAIReply(content); } // 公开API:设置聊天状态 setChatState(state) { this.chatState = { ...this.chatState, ...state }; } // 公开API:获取聊天状态 getChatState() { return { ...this.chatState }; } // 公开API:获取当前选中的图片文件列表 getSelectedImageFiles() { return this.inputHandler.getSelectedImageFiles(); } // 公开API:获取当前选中的文本文件列表 getSelectedTextFiles() { return this.inputHandler.getSelectedTextFiles(); } // 公开API:清除选中文件列表 clearSelectedFiles() { this.inputHandler.clearSelectedFiles(); } // 重新加载页面 async reload() { this.destroy(); await this.show(); } /** * 刷新欢迎框显示(配置变更时调用) */ refreshWelcomeBox() { if (this.isDestroyed || this.chatState.isProcessing) return; // 先清除屏幕上方的欢迎框区域(保留聊天历史) // 移动到顶部并清除前几行 process.stdout.write('\x1B[H'); // 移动到屏幕顶部 // 清除前15行(大致是欢迎框的高度) for (let i = 0; i < 15; i++) { process.stdout.write('\x1B[2K'); // 清除整行 if (i < 14) { process.stdout.write('\x1B[1B'); // 下移一行 } } // 返回到顶部重新显示欢迎框 process.stdout.write('\x1B[H'); this.showWelcomeBox(); // 移动到最底部(继续输入位置) process.stdout.write('\x1B[999B'); } /** * 显示欢迎框 */ showWelcomeBox() { const main = this.currentMessages.main; // 获取当前配置信息 const currentDir = process.cwd(); const apiConfig = storage_1.StorageService.getApiConfig(); // 简化配置信息显示 const configLines = [ `${chalk_1.default.gray('Directory:')} ${chalk_1.default.white(currentDir)}`, `${chalk_1.default.gray('API URL:')} ${chalk_1.default.white(apiConfig.baseUrl || 'Not configured')}`, `${chalk_1.default.gray('API Key:')} ${chalk_1.default.white(apiConfig.apiKey ? utils_1.StringUtils.maskApiKey(apiConfig.apiKey) : 'Not configured')}` ]; // 欢迎方框 - 更紧凑的设计 const welcomeBox = (0, boxen_1.default)(chalk_1.default.hex('#FF6B6B').bold(main.title) + '\n' + chalk_1.default.hex('#4ECDC4').italic(main.subtitle) + '\n\n' + configLines.join('\n'), { padding: { top: 1, bottom: 1, left: 2, right: 2 }, margin: { top: 1, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'magenta', title: chalk_1.default.white.bold('Welcome'), titleAlignment: 'center' }); process.stdout.write(welcomeBox + '\n'); } async show() { // 确保之前的状态已清理 if (this.isDestroyed) { // 重新初始化 this.isDestroyed = false; this.chatState = { canSendMessage: true, isProcessing: false }; this.messages = []; this.commandManager.resetStates(); this.loadingController = null; } // 强制清屏,确保完全清除欢迎页面内容 process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); process.stdout.write('\x1Bc'); // 确保stdin处于正确状态 try { if (process.stdin.isTTY) { process.stdin.setRawMode(false); } process.stdin.removeAllListeners('data'); process.stdin.removeAllListeners('error'); process.stdin.removeAllListeners('end'); process.stdin.pause(); } catch (error) { // 忽略清理错误 } // 显示欢迎框 this.showWelcomeBox(); // 在显示输入之前先进行系统检测 await this.performSystemDetection(); // 开始聊天循环 await this.startChatLoop(); } async performSystemDetection() { try { // 检测系统状态 const detectionResult = await this.systemDetector.detectSystem(); // 显示系统信息 await this.systemDetector.displaySystemInfo(detectionResult); // 添加空行分隔,直接进入输入状态 if (detectionResult.hasRole || detectionResult.hasMcpServices) { console.log(); // 添加空行分隔 } } catch (error) { console.error('系统检测失败:', error); // 检测失败不影响后续流程,继续进入聊天 } } async startChatLoop() { try { while (true) { // 检查是否已被销毁 if (this.isDestroyed) { break; } // 检查是否可以发送消息 if (!this.chatState.canSendMessage) { //process.stdout.write(chalk.red(this.currentMessages.main.status.cannotSendMessage + '\n')); await new Promise(resolve => setTimeout(resolve, 1000)); continue; } // 获取用户输入 const userInput = await this.getUserInput(); if (userInput === '/exit') { const exitAction = await this.commandManager.handleExitWithHistoryCheck(this.messages); if (exitAction !== 'cancel') { break; } else { continue; // 用户取消退出,继续聊天循环 } } // 使用 CommandManager 处理用户输入 const commandResult = await this.commandManager.handleInput(userInput, this.messages); if (commandResult.handled) { // 命令已被处理 if (commandResult.shouldReload) { await this.reload(); continue; // 重新加载后继续循环 } if (commandResult.newMessages) { this.messages = commandResult.newMessages; } if (commandResult.shouldContinue) { continue; } if (commandResult.shouldExit) { break; } } // 如果 CommandManager 返回未处理,则有可能是普通消息或包含文件引用的消息 if (!commandResult.handled) { // 检查是否是普通消息 if (!userInput.startsWith('/')) { // 添加用户消息并直接显示 this.messageHandler.addUserMessage(userInput); // 处理AI请求 await this.messageHandler.processAIRequest(); continue; // 继续下一次循环 } // 处理未被 commandManager.handleInput 捕获的其他命令 switch (userInput) { case '/help': this.helpManager.showHelp(this.commandManager.getCommands()); break; case '/config': process.stdout.write(chalk_1.default.yellow(this.currentMessages.main.messages.configInDevelopment + '\n')); break; case '/history': this.commandManager.showHistory(this.messages); break; case '/init': await this.handleInitCommand(); break; default: // 未知命令 process.stdout.write(chalk_1.default.red(this.currentMessages.main.messages.unknownCommand.replace('{command}', userInput) + '\n')); break; } continue; } } } catch (error) { console.error('聊天循环出现错误:', error); } finally { // 确保在退出时清理所有资源 this.destroy(); } } async getUserInput() { // 检查是否已被销毁 if (this.isDestroyed) { return '/exit'; } return new Promise(async (resolve, reject) => { let currentInput = ''; let cursorPosition = 0; let currentState = null; let isDestroyed = false; let lastDisplayLines = 1; let lastSuggestionLines = 0; // 显示初始提示符 const promptText = chalk_1.default.cyan(this.currentMessages.main.prompt); process.stdout.write(promptText); // 单行输入框渲染 const redrawInputLine = () => { if (isDestroyed) return; const terminalWidth = process.stdout.columns || 80; // 首先回到第一行的开头 process.stdout.write('\r'); if (lastDisplayLines > 1) { // 向上移动到起始行 process.stdout.write(`\x1B[${lastDisplayLines - 1}A`); } // 清除所有相关行 for (let i = 0; i < lastDisplayLines; i++) { process.stdout.write('\x1B[2K'); // 清除当前行 if (i < lastDisplayLines - 1) { process.stdout.write('\x1B[1B'); // 向下移动到下一行 } } // 回到第一行开头 if (lastDisplayLines > 1) { process.stdout.write(`\x1B[${lastDisplayLines - 1}A`); } process.stdout.write('\r'); // 计算新内容的显示信息 const displayText = promptText + currentInput; const displayWidth = utils_1.StringUtils.getDisplayWidth(displayText); const newDisplayLines = Math.ceil(displayWidth / terminalWidth) || 1; // 输出新内容 process.stdout.write(displayText); // 更新行数记录 lastDisplayLines = newDisplayLines; // 计算并设置光标位置 const promptLength = utils_1.StringUtils.getDisplayWidth(this.currentMessages.main.prompt); const inputToCursor = currentInput.substring(0, cursorPosition); const cursorOffset = utils_1.StringUtils.getDisplayWidth(inputToCursor); const totalCursorPos = promptLength + cursorOffset; const targetLine = Math.floor(totalCursorPos / terminalWidth); const targetCol = totalCursorPos % terminalWidth; // 从当前位置(内容末尾)移动到目标位置 const currentEndLine = newDisplayLines - 1; if (targetLine < currentEndLine) { // 需要向上移动 process.stdout.write(`\x1B[${currentEndLine - targetLine}A`); } // 设置到正确的列位置 process.stdout.write(`\x1B[${targetCol + 1}G`); }; // 显示建议列表 const showSuggestions = (state) => { if (!state.showingSuggestions || state.suggestions.length === 0 || isDestroyed) return; const title = this.inputHandler.getSuggestionTitle(state.suggestionsType); const renderedSuggestions = this.inputHandler.renderSuggestions(state.suggestions, state.selectedIndex); // 计算建议列表需要的行数 let suggestionLines = renderedSuggestions.length; if (title) suggestionLines += 1; // 输出建议列表 process.stdout.write('\n'); if (title) { process.stdout.write(title + '\n'); } renderedSuggestions.forEach(suggestion => { process.stdout.write(suggestion + '\n'); }); // 更新建议行数追踪 lastSuggestionLines = suggestionLines + 1; // +1 是因为开头的 \n // 向上移动回输入行 process.stdout.write(`\x1B[${lastSuggestionLines}A`); // 重绘输入行确保显示正确 redrawInputLine(); }; // 隐藏建议列表 const hideSuggestions = () => { if (isDestroyed || lastSuggestionLines === 0) return; // 向下移动到建议列表位置并清除 process.stdout.write(`\x1B[${lastSuggestionLines}B`); // 向上清除每一行 for (let i = 0; i < lastSuggestionLines; i++) { process.stdout.write('\x1B[1A\x1B[2K'); // 向上一行并清除 } // 重置建议行数 lastSuggestionLines = 0; // 重绘输入行 redrawInputLine(); }; // 更新显示 const updateDisplay = async () => { if (isDestroyed) return; // 更新选中文件列表 this.inputHandler.updateSelectedFiles(currentInput); // 总是先清除下方内容,避免残留(hideSuggestions 会重绘输入行) hideSuggestions(); // 获取新状态 const newState = await this.inputHandler.analyzInput(currentInput); currentState = newState; // 显示新建议(如果有) if (newState.showingSuggestions) { showSuggestions(newState); } // 注意:不需要 else 分支,因为 hideSuggestions 已经重绘了输入行 }; // 高效更新建议列表选中状态(避免完全重绘) const updateSuggestionSelection = (newIndex) => { if (!currentState?.showingSuggestions || isDestroyed) return; currentState.selectedIndex = newIndex; // 先隐藏当前建议,再显示新的 if (lastSuggestionLines > 0) { hideSuggestions(); } showSuggestions(currentState); }; // 清理函数 const cleanup = () => { if (isDestroyed) return; isDestroyed = true; if (currentState?.showingSuggestions) { hideSuggestions(); } // 移除所有事件监听器 process.stdin.removeAllListeners('data'); process.stdin.removeAllListeners('error'); process.stdin.removeAllListeners('end'); // 重置stdin状态 if (process.stdin.isTTY) { try { process.stdin.setRawMode(false); } catch (error) { // 忽略错误,可能已经被重置 } } // 暂停stdin try { process.stdin.pause(); } catch (error) { // 忽略错误 } // 移除进程退出监听器 process.removeListener('SIGINT', handleExit); process.removeListener('SIGTERM', handleExit); }; // 键盘事件处理 const onKeyPress = async (key) => { if (isDestroyed) return; try { const keyCode = key.charCodeAt(0); // Ctrl+C - 优雅退出 if (keyCode === 3) { cleanup(); process.stdout.write('\n'); const exitAction = await this.commandManager.handleExitWithHistoryCheck(this.messages); if (exitAction === 'cancel') { return; } resolve('/exit'); return; } // Enter 键 if (keyCode === 13) { if (currentState?.showingSuggestions && currentState.suggestions.length > 0) { // 选择当前高亮的建议 const selectedSuggestion = currentState.suggestions[currentState.selectedIndex]; const newInput = this.inputHandler.handleSuggestionSelection(currentInput, selectedSuggestion); if (selectedSuggestion.type === 'command') { // 命令类型直接执行 if (currentState.showingSuggestions) { hideSuggestions(); } process.stdout.write('\x1B[2K\x1B[0G'); process.stdout.write(chalk_1.default.cyan(this.currentMessages.main.prompt) + newInput + '\n'); cleanup(); resolve(newInput); return; } else { // 文件类型,更新输入内容 if (currentState.showingSuggestions) { hideSuggestions(); } currentInput = newInput; cursorPosition = currentInput.length; redrawInputLine(); await updateDisplay(); return; } } else if (currentInput.trim()) { // 发送内容 const finalInput = currentInput.trim(); cleanup(); process.stdout.write('\n'); resolve(finalInput); return; } else { return; } } // Backspace 键 if (keyCode === 127 || keyCode === 8) { if (currentInput.length > 0 && cursorPosition > 0) { if (currentState?.showingSuggestions) { hideSuggestions(); } currentInput = currentInput.slice(0, cursorPosition - 1) + currentInput.slice(cursorPosition); cursorPosition--; redrawInputLine(); await updateDisplay(); } return; } // 上下左右箭头键处理 if (key.length >= 3 && key.startsWith('\x1B[')) { if (key === '\x1B[A') { // 上箭头 if (currentState?.showingSuggestions && currentState.suggestions.length > 0) { const newIndex = currentState.selectedIndex > 0 ? currentState.selectedIndex - 1 : currentState.suggestions.length - 1; updateSuggestionSelection(newIndex); } return; } else if (key === '\x1B[B') { // 下箭头 if (currentState?.showingSuggestions && currentState.suggestions.length > 0) { const newIndex = currentState.selectedIndex < currentState.suggestions.length - 1 ? currentState.selectedIndex + 1 : 0; updateSuggestionSelection(newIndex); } return; } else if (key === '\x1B[D') { // 左箭头 if (cursorPosition > 0) { if (currentState?.showingSuggestions) { hideSuggestions(); currentState.showingSuggestions = false; } cursorPosition--; if (!currentState?.showingSuggestions) { redrawInputLine(); } } return; } else if (key === '\x1B[C') { // 右箭头 if (cursorPosition < currentInput.length) { if (currentState?.showingSuggestions) { hideSuggestions(); currentState.showingSuggestions = false; } cursorPosition++; if (!currentState?.showingSuggestions) { redrawInputLine(); } } return; } } // ESC 键处理 if (keyCode === 27 && key.length === 1) { if (currentState?.showingSuggestions) { hideSuggestions(); currentState.showingSuggestions = false; currentState.suggestions = []; } return; } // 普通字符输入 const isPrintable = (key.length === 1 && keyCode >= 32) || (key.length > 1 && !key.startsWith('\x1B')); if (isPrintable) { let textToInsert = key; // 处理粘贴内容 - 统一转换为单行 if (key.includes('\n') || key.includes('\r') || key.length > 10) { const hasFileReference = currentInput.includes('@'); if (hasFileReference) { textToInsert = utils_1.StringUtils.processFileContentPaste(key); } else { // 所有多行内容都转换为单行 textToInsert = key.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim(); } } if (textToInsert) { if (currentState?.showingSuggestions) { hideSuggestions(); } currentInput = currentInput.slice(0, cursorPosition) + textToInsert + currentInput.slice(cursorPosition); cursorPosition += textToInsert.length; redrawInputLine(); await updateDisplay(); } } } catch (error) { // 捕获任何异常,避免程序崩溃 cleanup(); reject(error); } }; // 错误处理 const onError = (error) => { if (!isDestroyed) { cleanup(); reject(error); } }; // 进程退出处理 const handleExit = () => { if (!isDestroyed) { cleanup(); resolve('/exit'); } }; // 设置原始模式和事件监听 try { // 确保stdin处于正确的状态 if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); process.stdin.setEncoding('utf8'); // 添加事件监听器 process.stdin.on('data', onKeyPress); process.stdin.on('error', onError); // 添加进程退出监听,确保清理 process.on('SIGINT', handleExit); process.on('SIGTERM', handleExit); } catch (error) { cleanup(); reject(error); } }); } /** * 处理 /init 命令 */ async handleInitCommand() { await this.initHandler.execute(); } } exports.MainPage = MainPage; //# sourceMappingURL=main.js.map