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