tianxincode 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/api/filesystem.routes.js +13 -4
  2. package/dist/api/filesystem.routes.js.map +1 -1
  3. package/dist/components/App.backup.d.ts +34 -0
  4. package/dist/components/App.backup.d.ts.map +1 -0
  5. package/dist/components/App.backup.js +692 -0
  6. package/dist/components/App.backup.js.map +1 -0
  7. package/dist/components/App.d.ts +33 -24
  8. package/dist/components/App.d.ts.map +1 -1
  9. package/dist/components/App.js +182 -718
  10. package/dist/components/App.js.map +1 -1
  11. package/dist/components/FileSelector.d.ts +47 -0
  12. package/dist/components/FileSelector.d.ts.map +1 -0
  13. package/dist/components/FileSelector.js +40 -0
  14. package/dist/components/FileSelector.js.map +1 -0
  15. package/dist/components/Header.d.ts +3 -0
  16. package/dist/components/Header.d.ts.map +1 -0
  17. package/dist/components/Header.js +17 -0
  18. package/dist/components/Header.js.map +1 -0
  19. package/dist/components/InputArea.d.ts +50 -0
  20. package/dist/components/InputArea.d.ts.map +1 -0
  21. package/dist/components/InputArea.js +52 -0
  22. package/dist/components/InputArea.js.map +1 -0
  23. package/dist/components/MainContent.d.ts +11 -0
  24. package/dist/components/MainContent.d.ts.map +1 -0
  25. package/dist/components/MainContent.js +118 -0
  26. package/dist/components/MainContent.js.map +1 -0
  27. package/dist/components/MessageList.d.ts +11 -0
  28. package/dist/components/MessageList.d.ts.map +1 -0
  29. package/dist/components/MessageList.js +129 -0
  30. package/dist/components/MessageList.js.map +1 -0
  31. package/dist/components/ModelSelector.d.ts +42 -0
  32. package/dist/components/ModelSelector.d.ts.map +1 -0
  33. package/dist/components/ModelSelector.js +36 -0
  34. package/dist/components/ModelSelector.js.map +1 -0
  35. package/dist/components/StatusBar.d.ts +14 -0
  36. package/dist/components/StatusBar.d.ts.map +1 -0
  37. package/dist/components/StatusBar.js +21 -0
  38. package/dist/components/StatusBar.js.map +1 -0
  39. package/dist/components/hooks/useChat.d.ts +69 -0
  40. package/dist/components/hooks/useChat.d.ts.map +1 -0
  41. package/dist/components/hooks/useChat.js +270 -0
  42. package/dist/components/hooks/useChat.js.map +1 -0
  43. package/dist/components/hooks/useFileSelect.d.ts +58 -0
  44. package/dist/components/hooks/useFileSelect.d.ts.map +1 -0
  45. package/dist/components/hooks/useFileSelect.js +182 -0
  46. package/dist/components/hooks/useFileSelect.js.map +1 -0
  47. package/dist/components/hooks/useModelSelect.d.ts +54 -0
  48. package/dist/components/hooks/useModelSelect.d.ts.map +1 -0
  49. package/dist/components/hooks/useModelSelect.js +137 -0
  50. package/dist/components/hooks/useModelSelect.js.map +1 -0
  51. package/dist/components/useInputHandler.d.ts +33 -0
  52. package/dist/components/useInputHandler.d.ts.map +1 -0
  53. package/dist/components/useInputHandler.js +130 -0
  54. package/dist/components/useInputHandler.js.map +1 -0
  55. package/dist/hooks/useAlternateBuffer.d.ts +14 -0
  56. package/dist/hooks/useAlternateBuffer.d.ts.map +1 -0
  57. package/dist/hooks/useAlternateBuffer.js +20 -0
  58. package/dist/hooks/useAlternateBuffer.js.map +1 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +0 -1
  61. package/dist/index.js.map +1 -1
  62. package/package.json +7 -5
  63. package/scripts/ink-scroll-demo.tsx +147 -0
  64. package/web/dist/assets/{AiLogsView-D_SQ6qo4.js → AiLogsView-rdisKFSa.js} +1 -1
  65. package/web/dist/assets/{DevWorkflowView-iHAv9LC0.js → DevWorkflowView-CV7LEFE2.js} +5 -5
  66. package/web/dist/assets/DevWorkflowView-D_iXemzz.css +1 -0
  67. package/web/dist/assets/{Layout-CQhbIJxB.js → Layout-WWXhjQI7.js} +1 -1
  68. package/web/dist/assets/{TasksView-B9B2H3-S.js → TasksView-vUyC5Nlz.js} +1 -1
  69. package/web/dist/assets/{TerminalView-BKhdi5UF.js → TerminalView-qS_Sznel.js} +1 -1
  70. package/web/dist/assets/{cssMode-C_gwipb0.js → cssMode-CJFHsxhF.js} +1 -1
  71. package/web/dist/assets/{freemarker2-WCA_pR0J.js → freemarker2-GCZND4I7.js} +1 -1
  72. package/web/dist/assets/{handlebars-fzVGzXHj.js → handlebars-IPke9hyC.js} +1 -1
  73. package/web/dist/assets/{html-C4Rga6DD.js → html-Dl_nDKmC.js} +1 -1
  74. package/web/dist/assets/{htmlMode-BvrJ57rW.js → htmlMode-DpdrVNft.js} +1 -1
  75. package/web/dist/assets/{index-9RP8PAfk.js → index-TrZtH1m_.js} +13 -13
  76. package/web/dist/assets/{javascript-cXKyuP98.js → javascript-DHT2J-uS.js} +1 -1
  77. package/web/dist/assets/{jsonMode-Q4CMzwiX.js → jsonMode-BeJp-o3r.js} +1 -1
  78. package/web/dist/assets/{liquid-CrbU0gmK.js → liquid-C5FLdOvv.js} +1 -1
  79. package/web/dist/assets/{mdx-D72Dcs66.js → mdx-CZwiqZoo.js} +1 -1
  80. package/web/dist/assets/{python-DPqkZ_Xm.js → python-D5pMpjh7.js} +1 -1
  81. package/web/dist/assets/{razor-DknXaqXN.js → razor-Ba49D4hB.js} +1 -1
  82. package/web/dist/assets/{tsMode-DZAV3Wgo.js → tsMode-vBfGu0Qc.js} +1 -1
  83. package/web/dist/assets/{typescript-DenKTmW3.js → typescript-NOD0JyOl.js} +1 -1
  84. package/web/dist/assets/{xml-C_uqJta0.js → xml-CJKugy72.js} +1 -1
  85. package/web/dist/assets/{yaml-vwb3U7d7.js → yaml-CEjcQv6-.js} +1 -1
  86. package/web/dist/index.html +1 -1
  87. package/web/dist/assets/DevWorkflowView-C5VnPngb.css +0 -1
@@ -1,755 +1,219 @@
1
1
  /**
2
- * App 主组件
2
+ * App 组件 - 主应用组件
3
3
  *
4
- * 这是 TXCode CLI 模式的主界面组件,使用 Ink (类 React) 库渲染到终端
4
+ * 职责:
5
+ * - 组合所有子组件和 Hooks
6
+ * - 管理应用级别的全局状态(session、model)
7
+ * - 处理初始化逻辑(加载会话、模型配置)
8
+ * - 处理键盘事件(ESC 退出)
5
9
  *
6
- * 核心功能:
7
- * 1. 用户输入处理 - 接收键盘输入,支持命令和聊天
8
- * 2. 消息展示 - 显示用户/AI 的聊天消息
9
- * 3. AI 调用 - 调用 AI 服务处理用户问题
10
- * 4. 工具调用展示 - 实时显示 AI 的思考和工具调用过程
11
- * 5. 文件选择 - 支持 @ 符号触发文件选择器
12
- * 6. 模型选择 - 支持 /model 命令切换 AI 模型
13
- * 7. 状态显示 - 显示 Token 使用量、压缩比例等
10
+ * 架构设计:
11
+ * - 使用 useChat hook 管理聊天逻辑
12
+ * - 使用 useFileSelect hook 管理文件选择
13
+ * - 使用 useModelSelect hook 管理模型选择
14
+ * - 子组件:Header、MessageList、InputArea
15
+ *
16
+ * 组件结构:
17
+ * ┌─────────────────────────┐
18
+ * │ Header (Logo) │
19
+ * ├─────────────────────────┤
20
+ * │ MessageList (消息) │
21
+ * │ ... │
22
+ * ├─────────────────────────┤
23
+ * │ InputArea (输入区域) │
24
+ * │ - 输入框 │
25
+ * │ - 文件/模型选择器 │
26
+ * │ - 状态栏 │
27
+ * └─────────────────────────┘
14
28
  */
15
- import React, { useState, useEffect, useCallback } from 'react';
16
- import { Box, Text, useInput, useApp, Static } from 'ink';
17
- import { v4 as uuidv4 } from 'uuid';
18
- import * as fs from 'fs';
19
- import * as path from 'path';
29
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
30
+ import { Box, useStdout, useApp } from 'ink';
31
+ import { InputArea } from './InputArea.js';
32
+ import { MessageList } from './MessageList.js';
33
+ import { useChat } from './hooks/useChat.js';
34
+ import { useFileSelect } from './hooks/useFileSelect.js';
35
+ import { useModelSelect } from './hooks/useModelSelect.js';
20
36
  import { sessionService } from '../modules/session/index.js';
21
37
  import { memoryService } from '../modules/memory/index.js';
22
- import { dbService } from '../modules/db/index.js';
23
38
  import { configService } from '../modules/config/index.js';
24
- import { codeChatService } from '../services/codeChat/index.js';
25
- import { commandChatService } from '../services/commandChat/index.js';
39
+ import { v4 as uuidv4 } from 'uuid';
40
+ /**
41
+ * Header 静态项的唯一标识
42
+ * 用于消息列表中标识 Header 位置
43
+ */
44
+ const HEADER_KEY = 'header-static-key';
26
45
  /**
27
- * App 主组件函数
46
+ * 主应用组件
47
+ *
48
+ * 功能:
49
+ * 1. 初始化 - 加载会话和配置
50
+ * 2. 聊天功能 - 通过 useChat 处理
51
+ * 3. 文件选择 - 通过 useFileSelect 处理
52
+ * 4. 模型选择 - 通过 useModelSelect 处理
53
+ * 5. 窗口自适应 - 监听 resize 事件
28
54
  *
29
- * 组件状态说明:
30
- * - input: 用户输入的文本
31
- * - cursorPosition: 光标位置 (支持光标左右移动)
32
- * - messages: 聊天消息列表
33
- * - status: 当前状态 (idle/thinking)
34
- * - currentSession: 当前会话 ID
35
- * - error: 错误信息
36
- * - tokenStats: Token 统计
37
- * - compressionPercent: 压缩百分比
38
- * - currentModelName: 当前模型名称
39
- * - fileSelectMode: 文件选择模式
40
- * - modelSelectMode: 模型选择模式
41
- * - inputHistory: 输入历史 (上下键遍历)
55
+ * @returns 主应用 UI
42
56
  */
43
57
  export function App() {
44
- // ========== Hooks ==========
45
- // useApp() 提供 Ink 应用的上下文,包含 exit() 方法用于退出程序
58
+ // ==================== Hooks ====================
59
+ /** stdout 用于获取终端尺寸 */
60
+ const { stdout } = useStdout();
61
+ /** useApp 提供应用退出功能 */
46
62
  const { exit } = useApp();
47
- // ========== 输入相关状态 ==========
48
- /** 用户输入的文本 */
49
- const [input, setInput] = useState('');
50
- /** 光标在输入框中的位置 */
51
- const [cursorPosition, setCursorPosition] = useState(0);
52
- // ========== 消息相关状态 ==========
53
- /** 聊天消息列表 */
54
- const [messages, setMessages] = useState([]);
55
- /** 当前状态:idle(空闲) 或 thinking(AI 思考中) 或 stopping(正在停止) */
56
- const [status, setStatus] = useState('idle');
63
+ // ==================== 全局状态 ====================
57
64
  /** 当前会话 ID */
58
65
  const [currentSession, setCurrentSession] = useState(null);
59
- /** 错误信息 */
60
- const [error, setError] = useState(null);
61
- // ========== 统计相关状态 ==========
62
- /** Token 统计 */
63
- const [tokenStats, setTokenStats] = useState({
64
- promptTokens: 0,
65
- completionTokens: 0,
66
- totalTokens: 0,
67
- });
68
- /** 压缩百分比 */
69
- const [compressionPercent, setCompressionPercent] = useState(0);
70
- /** 当前模型名称 */
66
+ /** 当前使用的模型名称 */
71
67
  const [currentModelName, setCurrentModelName] = useState('deepseek-chat');
72
- // ========== 文件选择相关状态 ==========
73
- /** 是否处于文件选择模式 */
74
- const [fileSelectMode, setFileSelectMode] = useState(false);
75
- /** 当前目录路径 */
76
- const [currentDir, setCurrentDir] = useState('');
77
- /** 基础目录路径 */
78
- const [baseDir, setBaseDir] = useState('');
79
- /** 文件列表 */
80
- const [fileList, setFileList] = useState([]);
81
- /** 当前选中项索引 */
82
- const [selectedIndex, setSelectedIndex] = useState(0);
83
- // ========== 模型选择相关状态 ==========
84
- /** 是否处于模型选择模式 */
85
- const [modelSelectMode, setModelSelectMode] = useState(false);
86
- /** 模型列表 */
87
- const [modelList, setModelList] = useState([]);
88
- /** 当前选中模型索引 */
89
- const [modelSelectedIndex, setModelSelectedIndex] = useState(0);
90
- // ========== 输入历史相关状态 ==========
91
- /** 输入历史记录 */
92
- const [inputHistory, setInputHistory] = useState([]);
93
- /** 当前历史记录索引 */
94
- const [historyIndex, setHistoryIndex] = useState(-1);
95
- // ========== 动画相关状态 ==========
96
- /** 三点动画字符 */
97
- const [dotAnimation, setDotAnimation] = useState('');
98
- // ========== AI 停止控制 ==========
99
- /** AbortController 用于停止 AI 调用 */
100
- const abortControllerRef = React.useRef(null);
101
- // ========== 程序退出时保存数据库 ==========
102
- useEffect(() => {
103
- const handleExit = () => {
104
- dbService.close();
105
- };
106
- process.on('exit', handleExit);
107
- process.on('SIGINT', handleExit);
108
- return () => {
109
- process.off('exit', handleExit);
110
- process.off('SIGINT', handleExit);
111
- };
68
+ /** 重新挂载 key,用于触发 MessageList 刷新 */
69
+ const [remountKey, setRemountKey] = useState(0);
70
+ /**
71
+ * Input 组件的引用
72
+ * 用于在文件选择后向输入框插入内容
73
+ * @property input - 当前输入内容
74
+ * @property cursorPosition - 光标位置
75
+ * @property setInput - 设置输入内容的方法
76
+ * @property setCursorPosition - 设置光标位置的方法
77
+ */
78
+ const inputRef = useRef({
79
+ input: '', cursorPosition: 0, setInput: () => { }, setCursorPosition: () => { }
80
+ });
81
+ // ==================== 回调处理 ====================
82
+ /**
83
+ * 会话变化回调
84
+ * 当聊天中创建新会话或切换会话时触发
85
+ */
86
+ const handleSessionChange = useCallback((sessionId) => {
87
+ setCurrentSession(sessionId);
112
88
  }, []);
113
- // ========== 三点动画效果 ==========
114
- useEffect(() => {
115
- if (status === 'thinking') {
116
- const dots = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧'];
117
- let i = 0;
118
- const interval = setInterval(() => {
119
- setDotAnimation(dots[i % dots.length]);
120
- i++;
121
- }, 150);
122
- return () => clearInterval(interval);
123
- }
124
- else {
125
- setDotAnimation('');
126
- }
127
- }, [status]);
128
- // ========== 辅助函数 ==========
129
89
  /**
130
- * 加载目录内容 - 用于文件选择器
131
- * @param dir - 目录路径
132
- * @param resetBaseDir - 是否重置基础目录
90
+ * 文件选择回调
91
+ * 当用户选择文件后,将文件路径插入到输入框
92
+ *
93
+ * 处理逻辑:
94
+ * 1. 如果输入中有 @ 符号,替换 @ 后的内容
95
+ * 2. 否则在末尾追加文件路径
96
+ * 3. 更新光标位置到末尾
133
97
  */
134
- const loadDirectory = useCallback((dir, resetBaseDir = false) => {
135
- try {
136
- // 设置基础目录 (如果是首次或需要重置)
137
- if (!baseDir || resetBaseDir) {
138
- setBaseDir(dir);
139
- }
140
- const files = [];
141
- // 添加父目录选项 (..)
142
- const parentDir = path.dirname(dir);
143
- if (parentDir !== dir) {
144
- files.push({ name: '..', path: parentDir, isDirectory: true });
145
- }
146
- // 读取目录内容
147
- const entries = fs.readdirSync(dir, { withFileTypes: true });
148
- for (const entry of entries) {
149
- if (entry.name === '.' || entry.name === '..')
150
- continue;
151
- const fullPath = path.join(dir, entry.name);
152
- files.push({
153
- name: entry.name,
154
- path: fullPath,
155
- isDirectory: entry.isDirectory()
156
- });
157
- }
158
- files.sort((a, b) => {
159
- if (a.name === '..')
160
- return -1;
161
- if (b.name === '..')
162
- return 1;
163
- if (a.isDirectory && !b.isDirectory)
164
- return -1;
165
- if (!a.isDirectory && b.isDirectory)
166
- return 1;
167
- return a.name.localeCompare(b.name);
168
- });
169
- setFileList(files.slice(0, 50));
170
- setCurrentDir(dir);
171
- setSelectedIndex(0);
172
- }
173
- catch (err) {
174
- setFileList([]);
175
- }
176
- }, [baseDir]);
98
+ const handleFileSelected = useCallback((relativePath) => {
99
+ const currentInput = inputRef.current.input || '';
100
+ const atIndex = currentInput.lastIndexOf('@');
101
+ const newInput = atIndex !== -1
102
+ ? currentInput.slice(0, atIndex + 1) + relativePath + ' '
103
+ : currentInput + relativePath + ' ';
104
+ inputRef.current.setInput(newInput);
105
+ inputRef.current.setCursorPosition(newInput.length);
106
+ }, []);
177
107
  /**
178
- * 加载可用模型列表 - 从配置服务获取所有可用的 AI 模型
108
+ * 模型选择回调
109
+ * 当用户选择模型后更新状态
179
110
  */
180
- const loadModels = useCallback(async () => {
181
- try {
182
- const { configService } = await import('../modules/config/index.js');
183
- const providers = configService.getProviders();
184
- const models = [];
185
- // 遍历所有提供商,获取其模型列表
186
- for (const provider of providers) {
187
- const providerModels = configService.getModels(provider.id);
188
- for (const m of providerModels) {
189
- models.push({ id: m.id, name: m.name });
190
- }
191
- }
192
- // 如果没有模型,添加默认模型
193
- if (models.length === 0) {
194
- models.push({ id: 'deepseek-chat', name: 'deepseek-chat' });
195
- }
196
- setModelList(models);
197
- setModelSelectedIndex(0);
198
- }
199
- catch (err) {
200
- setModelList([]);
201
- }
111
+ const handleModelSelected = useCallback((modelName) => {
112
+ setCurrentModelName(modelName);
202
113
  }, []);
203
- // ========== 初始化 ==========
114
+ // ==================== Hook 初始化 ====================
115
+ /** 聊天功能 Hook */
116
+ const chat = useChat({
117
+ sessionId: currentSession,
118
+ onSessionChange: handleSessionChange,
119
+ });
120
+ /** 文件选择 Hook */
121
+ const fileSelect = useFileSelect({
122
+ onFileSelected: handleFileSelected,
123
+ });
124
+ /** 模型选择 Hook */
125
+ const modelSelect = useModelSelect({
126
+ onModelSelected: handleModelSelected,
127
+ });
128
+ // ==================== 布局计算 ====================
129
+ /** 终端高度 */
130
+ const terminalHeight = stdout.rows || 24;
131
+ /** Header 区域高度 */
132
+ const headerHeight = 10;
133
+ /** 输入区域高度 */
134
+ const inputHeight = 3;
135
+ /** 消息列表区域高度 = 终端高度 - Header - 输入区域 */
136
+ const contentHeight = terminalHeight - headerHeight - inputHeight;
137
+ // ==================== 副作用 ====================
138
+ /**
139
+ * 窗口大小变化监听
140
+ * 当终端窗口大小改变时,触发重新渲染
141
+ */
142
+ useEffect(() => {
143
+ const onResize = () => {
144
+ setRemountKey(prev => prev + 1);
145
+ };
146
+ stdout.on('resize', onResize);
147
+ return () => { stdout.off('resize', onResize); };
148
+ }, [stdout]);
149
+ /**
150
+ * 消息列表项
151
+ * 包含 Header 和所有消息,用于 MessageList 渲染
152
+ */
153
+ const items = [
154
+ { id: HEADER_KEY, isHeader: true },
155
+ ...chat.messages.map(m => ({ id: m.id, isHeader: false, content: m.content }))
156
+ ];
157
+ /**
158
+ * 初始化加载
159
+ * - 从 sessionService 获取当前会话
160
+ * - 从 memoryService 加载历史消息
161
+ * - 从 configService 加载默认模型
162
+ */
204
163
  useEffect(() => {
164
+ // 加载当前会话
205
165
  const session = sessionService.getCurrent();
206
166
  if (session) {
207
167
  setCurrentSession(session.id);
168
+ // 加载会话的永久消息
208
169
  const msgs = memoryService.getPermanentMessages(session.id);
209
- setMessages(msgs.map(m => ({
170
+ chat.setMessages(msgs.map(m => ({
210
171
  id: uuidv4(),
211
172
  role: m.role,
212
173
  content: m.content,
213
174
  timestamp: new Date(m.createdAt),
214
175
  })));
215
176
  }
216
- const loadCurrentModel = () => {
217
- const defaultModel = configService.get('defaultModel');
218
- //console.log('[DEBUG] loadCurrentModel - defaultModel from config:', defaultModel);
219
- if (defaultModel) {
220
- setCurrentModelName(defaultModel);
221
- // console.log('[DEBUG] setCurrentModelName to:', defaultModel);
222
- }
223
- };
224
- loadCurrentModel();
225
- }, []);
226
- // ========== 消息管理 ==========
227
- /**
228
- * 添加消息到列表 - 防止重复添加相同的消息
229
- * @param role - 消息角色
230
- * @param content - 消息内容
231
- */
232
- const addMessage = useCallback((role, content) => {
233
- const contentKey = `${role}:${content.slice(0, 100)}`;
234
- setMessages(prev => {
235
- // 检查最后一条消息是否相同 (防重复)
236
- const lastMsg = prev[prev.length - 1];
237
- if (lastMsg && lastMsg.role === role && lastMsg.content === content) {
238
- return prev;
239
- }
240
- const msg = {
241
- id: uuidv4(),
242
- role,
243
- content,
244
- timestamp: new Date(),
245
- };
246
- return [...prev, msg];
247
- });
248
- }, []);
249
- // ========== 核心业务逻辑 ==========
250
- /**
251
- * 提交用户输入 - 处理流程:
252
- * 1. 检查输入是否有效
253
- * 2. 如果是命令 (/开头),执行命令
254
- * 3. 否则调用 AI 服务处理
255
- * 4. 更新 UI 和状态
256
- *
257
- * 🔍 排查 /help 命令无反应的思路:
258
- * - 确认 handleSubmit 是否被调用 (检查 useInput 中 key.return 分支)
259
- * - 确认 trimmedInput.startsWith('/') 返回 true
260
- * - 确认 executeCommand 返回了正确的 result
261
- * - 确认 addMessage 被调用并成功添加了消息
262
- */
263
- const handleSubmit = useCallback(async () => {
264
- // 验证输入:不能为空,且当前不在思考中
265
- if (!input.trim() || status === 'thinking') {
266
- // 🔍 如果在这里 return,说明 input 为空或正在思考中
267
- return;
268
- }
269
- // ========== 准备输入 ==========
270
- const trimmedInput = input.trim();
271
- setInput(''); // 清空输入框
272
- setCursorPosition(0); // 重置光标位置
273
- setError(null); // 清空错误
274
- // ========== 命令处理 ==========
275
- // 以 / 开头的输入作为命令处理
276
- if (trimmedInput.startsWith('/')) {
277
- try {
278
- const result = await commandChatService.handleCommand({
279
- message: trimmedInput,
280
- sessionId: currentSession || undefined,
281
- });
282
- if (result.answer) {
283
- addMessage('system', result.answer);
284
- }
285
- if (result.sessionId && result.success) {
286
- setCurrentSession(result.sessionId);
287
- }
288
- }
289
- catch (err) {
290
- console.error('[DEBUG] 命令执行异常:', err);
291
- addMessage('system', `命令执行异常: ${err instanceof Error ? err.message : String(err)}`);
292
- return;
293
- }
294
- return;
295
- }
296
- addMessage('user', trimmedInput);
297
- setStatus('thinking');
298
- abortControllerRef.current = new AbortController();
299
- try {
300
- const result = await codeChatService.handleChat({
301
- message: trimmedInput,
302
- sessionId: currentSession || undefined,
303
- abortSignal: abortControllerRef.current.signal,
304
- onStep: (step, iteration, usage) => {
305
- if (usage?.promptTokens) {
306
- setTokenStats(prev => ({ ...prev, promptTokens: usage.promptTokens }));
307
- }
308
- if (step.thought) {
309
- const thoughtPreview = step.thought.length > 150
310
- ? step.thought.slice(0, 150) + '...'
311
- : step.thought;
312
- addMessage('system', `💭 ${thoughtPreview}`);
313
- }
314
- const actionNames = {
315
- 'read_file': '读取文件',
316
- 'write_file': '创建文件',
317
- 'edit_file': '编辑文件',
318
- 'execute_bash': '执行命令',
319
- 'bash': '执行命令',
320
- 'find_files': '搜索文件',
321
- 'grep': '搜索内容',
322
- 'glob': '文件匹配',
323
- 'loadSkill': '加载技能',
324
- 'todowrite': '任务列表',
325
- 'task': '任务代理',
326
- 'webfetch': '网页获取',
327
- 'question': '提问',
328
- };
329
- const toolCalls = step.toolCalls || [];
330
- for (const tc of toolCalls) {
331
- const name = tc.function.name;
332
- const actionName = actionNames[name] || name;
333
- let stepInfo = `🔧 ${actionName}`;
334
- if (tc.function.arguments) {
335
- try {
336
- const inputObj = typeof tc.function.arguments === 'string'
337
- ? JSON.parse(tc.function.arguments)
338
- : tc.function.arguments;
339
- if (name === 'read_file') {
340
- const offset = inputObj.offset ?? 1;
341
- const limit = inputObj.limit ?? 300;
342
- stepInfo += `: ${inputObj.file_path} offset:${offset} limit:${limit}`;
343
- }
344
- else if (name === 'edit_file') {
345
- stepInfo += `: ${inputObj.file_path}`;
346
- }
347
- else if (name === 'write_file') {
348
- stepInfo += `: ${inputObj.file_path}`;
349
- }
350
- else if (name === 'execute_bash' || name === 'bash') {
351
- stepInfo += `: ${inputObj.command?.slice(0, 50)}`;
352
- }
353
- else if (name === 'find_files' || name === 'glob') {
354
- stepInfo += `: ${inputObj.pattern}`;
355
- }
356
- else if (name === 'grep') {
357
- stepInfo += `: ${inputObj.pattern}`;
358
- }
359
- else if (name === 'loadSkill') {
360
- stepInfo += `: ${inputObj.skillPath}`;
361
- }
362
- else if (name === 'webfetch') {
363
- stepInfo += `: ${inputObj.url?.slice(0, 50)}`;
364
- }
365
- else if (name === 'todowrite') {
366
- if (inputObj.todos && Array.isArray(inputObj.todos)) {
367
- stepInfo += `: ${inputObj.todos.length} 个任务`;
368
- }
369
- }
370
- else if (name === 'question') {
371
- stepInfo += `: ${inputObj.questions?.length || 0} 个问题`;
372
- }
373
- else {
374
- const keys = Object.keys(inputObj);
375
- if (keys.length > 0) {
376
- const firstKey = keys[0];
377
- const val = inputObj[firstKey];
378
- if (typeof val === 'string') {
379
- stepInfo += `: ${val.slice(0, 50)}`;
380
- }
381
- else if (typeof val === 'number') {
382
- stepInfo += `: ${val}`;
383
- }
384
- }
385
- }
386
- }
387
- catch { }
388
- }
389
- if (step.success === false) {
390
- addMessage('system', `${stepInfo} ❌`);
391
- }
392
- else {
393
- addMessage('system', `${stepInfo} ✓`);
394
- }
395
- }
396
- if (toolCalls.length === 0 && step.success !== undefined) {
397
- if (step.success === false) {
398
- addMessage('system', `🔧 工具执行 ❌`);
399
- }
400
- else {
401
- addMessage('system', `🔧 工具执行 ✓`);
402
- }
403
- }
404
- },
405
- onCompact: (info) => {
406
- addMessage('system', `【压缩完成】${info.summary || ''}`);
407
- if (currentSession) {
408
- const msgs = memoryService.getPermanentMessages(currentSession);
409
- setMessages(msgs.map(m => ({
410
- id: uuidv4(),
411
- role: m.role,
412
- content: m.content,
413
- timestamp: new Date(m.createdAt),
414
- })));
415
- }
416
- },
417
- });
418
- if (result.answer) {
419
- addMessage('assistant', result.answer);
420
- }
421
- if (result.usage) {
422
- setTokenStats({
423
- promptTokens: result.usage.promptTokens || 0,
424
- completionTokens: result.usage.completionTokens || 0,
425
- totalTokens: result.usage.totalTokens || 0,
426
- });
427
- }
428
- if (result.sessionId) {
429
- setCurrentSession(result.sessionId);
430
- const stats = memoryService.getSessionStats(result.sessionId);
431
- if (stats.totalMessages > 0) {
432
- const compressPercent = Math.round((stats.compressedCount / stats.totalMessages) * 100);
433
- setCompressionPercent(compressPercent);
434
- }
435
- }
436
- }
437
- catch (err) {
438
- const errorMsg = err instanceof Error ? err.message : 'Unknown error';
439
- if (errorMsg === 'ABORTED') {
440
- addMessage('system', '已停止');
441
- }
442
- else {
443
- setError(errorMsg);
444
- addMessage('system', `错误: ${errorMsg}`);
445
- }
177
+ // 加载默认模型配置
178
+ const defaultModel = configService.get('defaultModel');
179
+ if (defaultModel) {
180
+ setCurrentModelName(defaultModel);
446
181
  }
447
- finally {
448
- abortControllerRef.current = null;
449
- setStatus('idle');
450
- }
451
- }, [input, status, currentSession, addMessage]);
452
- // ========== 键盘输入处理 ==========
182
+ }, []);
453
183
  /**
454
- * useInput Hook - 处理所有键盘输入事件
455
- * 根据当前模式 (普通/文件选择/模型选择) 执行不同逻辑
184
+ * ESC 键处理
185
+ * - 如果正在思考中,则停止
186
+ * - 如果处于空闲状态,则退出应用
456
187
  */
457
- useInput((char, key) => {
458
- // ========== 文件选择模式 ==========
459
- if (fileSelectMode) {
460
- const keyAny = key;
461
- if (keyAny.upArrow) {
462
- setSelectedIndex(prev => Math.max(0, prev - 1));
463
- }
464
- else if (keyAny.downArrow) {
465
- setSelectedIndex(prev => Math.min(fileList.length - 1, prev + 1));
466
- }
467
- else if (key.return) {
468
- if (fileList[selectedIndex]) {
469
- const selected = fileList[selectedIndex];
470
- if (selected.isDirectory || selected.name === '..') {
471
- const newInput = input + selected.name + '/';
472
- setInput(newInput);
473
- setCursorPosition(newInput.length);
474
- loadDirectory(selected.path);
475
- }
476
- else {
477
- const fullPath = selected.path;
478
- const effectiveBaseDir = baseDir || process.cwd();
479
- let relativePath = path.relative(effectiveBaseDir, fullPath);
480
- relativePath = relativePath.split(path.sep).join('/');
481
- const atIndex = input.lastIndexOf('@');
482
- const newInput = atIndex !== -1
483
- ? input.slice(0, atIndex + 1) + relativePath + ' '
484
- : input + relativePath + ' ';
485
- setInput(newInput);
486
- setCursorPosition(newInput.length);
487
- setFileSelectMode(false);
488
- setFileList([]);
489
- setBaseDir('');
490
- }
491
- }
492
- }
493
- else if (key.escape) {
494
- setFileSelectMode(false);
495
- setFileList([]);
496
- setBaseDir('');
497
- }
498
- return;
499
- }
500
- if (modelSelectMode) {
501
- const keyAny = key;
502
- if (keyAny.upArrow) {
503
- setModelSelectedIndex(prev => Math.max(0, prev - 1));
504
- }
505
- else if (keyAny.downArrow) {
506
- setModelSelectedIndex(prev => Math.min(modelList.length - 1, prev + 1));
507
- }
508
- else if (key.return) {
509
- if (modelList[modelSelectedIndex]) {
510
- const modelName = modelList[modelSelectedIndex].name;
511
- console.log('[模型切换] 准备保存模型:', modelName);
512
- setCurrentModelName(modelName);
513
- configService.set('defaultModel', modelName);
514
- console.log('[模型切换] 已保存到 config 表');
515
- }
516
- setModelSelectMode(false);
517
- setModelList([]);
518
- }
519
- else if (key.escape) {
520
- setModelSelectMode(false);
521
- setModelList([]);
522
- }
523
- return;
524
- }
525
- const keyAny = key;
526
- if (keyAny.upArrow) {
527
- if (inputHistory.length > 0) {
528
- const newIndex = historyIndex < inputHistory.length - 1 ? historyIndex + 1 : historyIndex;
529
- setHistoryIndex(newIndex);
530
- const historyInput = inputHistory[inputHistory.length - 1 - newIndex];
531
- setInput(historyInput);
532
- setCursorPosition(historyInput.length);
533
- }
534
- return;
535
- }
536
- if (keyAny.downArrow) {
537
- if (historyIndex > 0) {
538
- const newIndex = historyIndex - 1;
539
- setHistoryIndex(newIndex);
540
- const historyInput = inputHistory[inputHistory.length - 1 - newIndex];
541
- setInput(historyInput);
542
- setCursorPosition(historyInput.length);
543
- }
544
- else if (historyIndex === 0) {
545
- setHistoryIndex(-1);
546
- setInput('');
547
- setCursorPosition(0);
548
- }
549
- return;
550
- }
551
- if (keyAny.leftArrow) {
552
- setCursorPosition(prev => Math.max(0, prev - 1));
553
- return;
554
- }
555
- if (keyAny.rightArrow) {
556
- setCursorPosition(prev => Math.min(input.length, prev + 1));
557
- return;
558
- }
559
- // 🔍 排查 /help 无反应:确认回车键是否触发 handleSubmit
560
- if (key.return) {
561
- // 🔍 断点0: 检查 input 的值
562
- // console.log('[DEBUG] key.return, input:', JSON.stringify(input));
563
- if (input === '/model') {
564
- setModelSelectMode(true);
565
- loadModels();
566
- return;
567
- }
568
- if (input.trim()) {
569
- setInputHistory(prev => [...prev, input]);
570
- setHistoryIndex(-1);
571
- }
572
- // 🔍 断点1: 确认 handleSubmit 被调用
573
- // console.log('[DEBUG] 调用 handleSubmit, input:', JSON.stringify(input));
574
- handleSubmit();
575
- }
576
- else if (key.backspace || key.delete) {
577
- if (cursorPosition > 0) {
578
- const newInput = input.slice(0, cursorPosition - 1) + input.slice(cursorPosition);
579
- setInput(newInput);
580
- setCursorPosition(prev => prev - 1);
581
- setHistoryIndex(-1);
582
- if ((newInput.endsWith('/') || newInput.endsWith(path.sep)) && newInput.length > 1) {
583
- const inputPath = newInput.endsWith('//') || newInput.endsWith(path.sep + path.sep)
584
- ? newInput.slice(0, -1)
585
- : newInput;
586
- const dirPath = path.isAbsolute(inputPath) ? inputPath : path.resolve(process.cwd(), inputPath);
587
- setFileSelectMode(true);
588
- loadDirectory(dirPath);
589
- }
590
- }
591
- }
592
- else if (key.escape) {
593
- if (status === 'thinking' && abortControllerRef.current) {
594
- setStatus('stopping');
595
- abortControllerRef.current.abort();
596
- }
597
- else if (status === 'idle') {
598
- dbService.close();
599
- exit();
600
- }
601
- }
602
- else if (char && !key.ctrl && !key.meta) {
603
- const newInput = input.slice(0, cursorPosition) + char + input.slice(cursorPosition);
604
- setInput(newInput);
605
- setCursorPosition(prev => prev + char.length);
606
- setHistoryIndex(-1);
607
- if (char === '@') {
608
- setFileSelectMode(true);
609
- loadDirectory(process.cwd(), true);
610
- }
611
- if (newInput === '/model') {
612
- setModelSelectMode(true);
613
- loadModels();
614
- }
615
- }
616
- });
617
- // ========== UI 渲染 ==========
618
- const sessionText = currentSession ? `${currentSession.slice(0, 8)}` : '无会话';
619
- const isToolCall = (content) => {
620
- return content.startsWith('🔧') ||
621
- content.startsWith('✓') ||
622
- content.startsWith('* ');
623
- };
624
- const isThought = (content) => {
625
- return content.startsWith('💭') || content.startsWith('~ ');
626
- };
627
- const formatToolCall = (content) => {
628
- if (content.startsWith('🔧 ')) {
629
- return content.replace('🔧 ', '* ').replace(' ✓', '').replace(' ❌', '');
630
- }
631
- return content;
632
- };
633
- const formatThought = (content) => {
634
- if (content.startsWith('💭 ')) {
635
- return content.replace('💭 ', '~ ');
636
- }
637
- return content;
638
- };
639
- return (React.createElement(Box, { flexDirection: "column", padding: 1 },
640
- React.createElement(Box, { flexDirection: "column" },
641
- React.createElement(Static, { items: messages }, (msg) => {
642
- const isError = msg.role === 'system' && msg.content.startsWith('错误:');
643
- const isTool = msg.role === 'system' && isToolCall(msg.content);
644
- const isThoughtMsg = msg.role === 'system' && isThought(msg.content);
645
- if (msg.role === 'user') {
646
- return (React.createElement(Box, { key: msg.id, backgroundColor: "#121212", paddingX: 2, paddingY: 1, borderLeft: "2", borderColor: "#27272a", marginBottom: 1 },
647
- React.createElement(Text, { bold: true, color: "#f4f4f5" },
648
- "# ",
649
- msg.content)));
650
- }
651
- if (msg.role === 'assistant') {
652
- let thought = '';
653
- let toolCalls = [];
654
- let success = true;
655
- try {
656
- const parsed = JSON.parse(msg.content);
657
- if (parsed.type === 'assistant_with_tools') {
658
- thought = parsed.thought || '';
659
- toolCalls = parsed.toolCalls || [];
660
- success = parsed.success !== false;
661
- }
662
- else {
663
- thought = msg.content;
664
- }
665
- }
666
- catch {
667
- thought = msg.content;
668
- }
669
- const actionNames = {
670
- 'read': '读取文件',
671
- 'read_file': '读取文件',
672
- 'edit_file': '编辑文件',
673
- 'write_file': '写入文件',
674
- 'bash': '执行命令',
675
- 'execute_bash': '执行命令',
676
- 'find_files': '搜索文件',
677
- 'grep': '搜索内容',
678
- 'glob': '文件匹配',
679
- };
680
- return (React.createElement(Box, { key: msg.id, marginBottom: 1, flexDirection: "column" },
681
- thought && (React.createElement(Box, { paddingLeft: 2 },
682
- React.createElement(Text, { bold: true, color: "cyan" },
683
- "~ ",
684
- thought.slice(0, 150),
685
- thought.length > 150 ? '...' : ''))),
686
- toolCalls.map((tc, idx) => {
687
- const name = tc.function?.name || 'unknown';
688
- const actionName = actionNames[name] || name;
689
- return (React.createElement(Box, { key: idx, paddingLeft: 2 },
690
- React.createElement(Text, { dimColor: true },
691
- "* ",
692
- actionName,
693
- " ",
694
- success ? '✓' : '✗')));
695
- }),
696
- !thought && toolCalls.length === 0 && (React.createElement(Box, { paddingLeft: 2 },
697
- React.createElement(Text, { color: "#d4d4d8" }, msg.content)))));
698
- }
699
- if (isThoughtMsg) {
700
- return (React.createElement(Box, { key: msg.id, marginBottom: 1, paddingLeft: 2 },
701
- React.createElement(Text, { bold: true, color: "cyan" }, formatThought(msg.content))));
702
- }
703
- if (isTool) {
704
- return (React.createElement(Box, { key: msg.id, marginBottom: 1, paddingLeft: 2 },
705
- React.createElement(Text, { dimColor: true }, formatToolCall(msg.content))));
706
- }
707
- if (isError) {
708
- return (React.createElement(Box, { key: msg.id, marginBottom: 1, paddingLeft: 2 },
709
- React.createElement(Text, { color: "red" }, msg.content)));
710
- }
711
- // 显示其他 system 消息(如命令结果)
712
- if (msg.role === 'system') {
713
- return (React.createElement(Box, { key: msg.id, marginBottom: 1, paddingLeft: 2 },
714
- React.createElement(Text, { dimColor: true }, msg.content)));
715
- }
716
- return null;
717
- }),
718
- status === 'thinking' && (React.createElement(Box, { marginBottom: 1, paddingLeft: 2 },
719
- React.createElement(Text, { bold: true, color: "cyan" }, "\u25A3 Build"),
720
- React.createElement(Text, { dimColor: true },
721
- " \u00B7 ",
722
- currentModelName),
723
- React.createElement(Text, { dimColor: true }, " \u00B7 \u6309 ESC \u505C\u6B62..."))),
724
- status === 'stopping' && (React.createElement(Box, { marginBottom: 1, paddingLeft: 2 },
725
- React.createElement(Text, { bold: true, color: "yellow" }, "\u25A0 \u6B63\u5728\u505C\u6B62...")))),
726
- React.createElement(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, marginBottom: 1 },
727
- React.createElement(Text, { dimColor: true }, '> '),
728
- React.createElement(Text, null, status === 'stopping' ? '等待停止...' : input.slice(0, cursorPosition)),
729
- React.createElement(Text, { color: "cyan" }, "\u258B"),
730
- React.createElement(Text, null, status === 'stopping' ? '' : input.slice(cursorPosition))),
731
- fileSelectMode && (React.createElement(Box, { flexDirection: "column", marginBottom: 1, paddingX: 1 },
732
- React.createElement(Text, { dimColor: true },
733
- currentDir || '/',
734
- " (\u2191\u2193 \u9009\u62E9, Enter \u786E\u8BA4, ESC \u53D6\u6D88)"),
735
- fileList.slice(0, 15).map((file, index) => (React.createElement(Box, { key: file.path },
736
- React.createElement(Text, null, index === selectedIndex ? (React.createElement(Text, { bold: true, color: "green" }, `▶ ${file.name}${file.isDirectory ? '/' : ''}`)) : (React.createElement(Text, { dimColor: true }, ` ${file.name}${file.isDirectory ? '/' : ''}`)))))),
737
- fileList.length === 0 && (React.createElement(Text, { dimColor: true }, "\u7A7A\u76EE\u5F55...")))),
738
- modelSelectMode && (React.createElement(Box, { flexDirection: "column", marginBottom: 1, paddingX: 1 },
739
- React.createElement(Text, { dimColor: true }, "\u9009\u62E9\u6A21\u578B (\u2191\u2193 \u9009\u62E9, Enter \u786E\u8BA4, ESC \u53D6\u6D88):"),
740
- modelList.map((model, index) => (React.createElement(Box, { key: model.id },
741
- React.createElement(Text, null, index === modelSelectedIndex ? (React.createElement(Text, { bold: true, color: "green" }, `▶ ${model.name}`)) : (React.createElement(Text, { dimColor: true }, ` ${model.name}`)))))),
742
- modelList.length === 0 && (React.createElement(Text, { dimColor: true }, "\u672A\u627E\u5230\u6A21\u578B...")))),
743
- React.createElement(Box, { paddingX: 2 },
744
- React.createElement(Text, { dimColor: true },
745
- status === 'thinking' ? `${dotAnimation} 思考中` : '✓ 就绪',
746
- ` | 模型:${currentModelName}`,
747
- ` | 会话:${sessionText}`,
748
- ` | token:(${tokenStats.promptTokens}${tokenStats.promptTokens > 50000 ? ' 会话太大推荐用/compact压缩会话' : ''})`,
749
- ` | 帮助 /help | 退出 ctrl+c`)),
750
- error && (React.createElement(Box, { marginTop: 1 },
751
- React.createElement(Text, { color: "red" },
752
- "\u9519\u8BEF: ",
753
- error)))));
188
+ const handleEscape = useCallback(() => {
189
+ if (chat.status === 'thinking') {
190
+ chat.stop();
191
+ }
192
+ else if (chat.status === 'idle') {
193
+ exit();
194
+ }
195
+ }, [chat.status, chat.stop, exit]);
196
+ /** 打开文件选择器 */
197
+ const handleFileSelect = useCallback(() => {
198
+ fileSelect.open();
199
+ }, [fileSelect]);
200
+ /** 打开模型选择器 */
201
+ const handleModelSelect = useCallback(() => {
202
+ modelSelect.open();
203
+ }, [modelSelect]);
204
+ // ==================== 渲染 ====================
205
+ return (React.createElement(Box, { flexDirection: "column", width: stdout.columns || 80 },
206
+ React.createElement(MessageList, { messages: chat.messages, status: chat.status, currentModelName: currentModelName, historyRemountKey: remountKey }),
207
+ React.createElement(InputArea
208
+ // 使用消息数量作为 key,强制重新挂载以重置输入状态
209
+ , {
210
+ // 使用消息数量作为 key,强制重新挂载以重置输入状态
211
+ key: `input-${chat.messages.length}`, messages: chat.messages, setMessages: chat.setMessages, contentHeight: contentHeight, status: chat.status, currentModelName: currentModelName, tokenStats: chat.tokenStats, onSubmit: chat.handleSubmit, onEscape: handleEscape, onFileSelect: handleFileSelect, onModelSelect: handleModelSelect,
212
+ // 文件选择相关 props
213
+ fileSelectMode: fileSelect.fileSelectMode, fileList: fileSelect.fileList, selectedIndex: fileSelect.selectedIndex, currentDir: fileSelect.currentDir, onFileSelectConfirm: fileSelect.confirm, onFileSelectCancel: fileSelect.cancel, onFileSelectUp: fileSelect.moveUp, onFileSelectDown: fileSelect.moveDown,
214
+ // 模型选择相关 props
215
+ modelSelectMode: modelSelect.modelSelectMode, modelList: modelSelect.modelList, modelSelectedIndex: modelSelect.modelSelectedIndex, onModelSelectConfirm: modelSelect.confirm, onModelSelectCancel: modelSelect.cancel, onModelSelectUp: modelSelect.moveUp, onModelSelectDown: modelSelect.moveDown,
216
+ // 其他 props
217
+ sessionId: currentSession, inputRef: inputRef })));
754
218
  }
755
219
  //# sourceMappingURL=App.js.map