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.
- 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-rdisKFSa.js} +1 -1
- package/web/dist/assets/{DevWorkflowView-iHAv9LC0.js → DevWorkflowView-CV7LEFE2.js} +5 -5
- package/web/dist/assets/DevWorkflowView-D_iXemzz.css +1 -0
- package/web/dist/assets/{Layout-CQhbIJxB.js → Layout-WWXhjQI7.js} +1 -1
- package/web/dist/assets/{TasksView-B9B2H3-S.js → TasksView-vUyC5Nlz.js} +1 -1
- package/web/dist/assets/{TerminalView-BKhdi5UF.js → TerminalView-qS_Sznel.js} +1 -1
- package/web/dist/assets/{cssMode-C_gwipb0.js → cssMode-CJFHsxhF.js} +1 -1
- package/web/dist/assets/{freemarker2-WCA_pR0J.js → freemarker2-GCZND4I7.js} +1 -1
- package/web/dist/assets/{handlebars-fzVGzXHj.js → handlebars-IPke9hyC.js} +1 -1
- package/web/dist/assets/{html-C4Rga6DD.js → html-Dl_nDKmC.js} +1 -1
- package/web/dist/assets/{htmlMode-BvrJ57rW.js → htmlMode-DpdrVNft.js} +1 -1
- package/web/dist/assets/{index-9RP8PAfk.js → index-TrZtH1m_.js} +13 -13
- package/web/dist/assets/{javascript-cXKyuP98.js → javascript-DHT2J-uS.js} +1 -1
- package/web/dist/assets/{jsonMode-Q4CMzwiX.js → jsonMode-BeJp-o3r.js} +1 -1
- package/web/dist/assets/{liquid-CrbU0gmK.js → liquid-C5FLdOvv.js} +1 -1
- package/web/dist/assets/{mdx-D72Dcs66.js → mdx-CZwiqZoo.js} +1 -1
- package/web/dist/assets/{python-DPqkZ_Xm.js → python-D5pMpjh7.js} +1 -1
- package/web/dist/assets/{razor-DknXaqXN.js → razor-Ba49D4hB.js} +1 -1
- package/web/dist/assets/{tsMode-DZAV3Wgo.js → tsMode-vBfGu0Qc.js} +1 -1
- package/web/dist/assets/{typescript-DenKTmW3.js → typescript-NOD0JyOl.js} +1 -1
- package/web/dist/assets/{xml-C_uqJta0.js → xml-CJKugy72.js} +1 -1
- package/web/dist/assets/{yaml-vwb3U7d7.js → yaml-CEjcQv6-.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/assets/DevWorkflowView-C5VnPngb.css +0 -1
package/dist/components/App.js
CHANGED
|
@@ -1,755 +1,219 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* App
|
|
2
|
+
* App 组件 - 主应用组件
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 组合所有子组件和 Hooks
|
|
6
|
+
* - 管理应用级别的全局状态(session、model)
|
|
7
|
+
* - 处理初始化逻辑(加载会话、模型配置)
|
|
8
|
+
* - 处理键盘事件(ESC 退出)
|
|
5
9
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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,
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
19
|
-
import
|
|
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 {
|
|
25
|
-
|
|
39
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
40
|
+
/**
|
|
41
|
+
* Header 静态项的唯一标识
|
|
42
|
+
* 用于消息列表中标识 Header 位置
|
|
43
|
+
*/
|
|
44
|
+
const HEADER_KEY = 'header-static-key';
|
|
26
45
|
/**
|
|
27
|
-
*
|
|
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
|
-
//
|
|
45
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
*
|
|
132
|
-
*
|
|
90
|
+
* 文件选择回调
|
|
91
|
+
* 当用户选择文件后,将文件路径插入到输入框
|
|
92
|
+
*
|
|
93
|
+
* 处理逻辑:
|
|
94
|
+
* 1. 如果输入中有 @ 符号,替换 @ 后的内容
|
|
95
|
+
* 2. 否则在末尾追加文件路径
|
|
96
|
+
* 3. 更新光标位置到末尾
|
|
133
97
|
*/
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
*
|
|
108
|
+
* 模型选择回调
|
|
109
|
+
* 当用户选择模型后更新状态
|
|
179
110
|
*/
|
|
180
|
-
const
|
|
181
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
448
|
-
abortControllerRef.current = null;
|
|
449
|
-
setStatus('idle');
|
|
450
|
-
}
|
|
451
|
-
}, [input, status, currentSession, addMessage]);
|
|
452
|
-
// ========== 键盘输入处理 ==========
|
|
182
|
+
}, []);
|
|
453
183
|
/**
|
|
454
|
-
*
|
|
455
|
-
*
|
|
184
|
+
* ESC 键处理
|
|
185
|
+
* - 如果正在思考中,则停止
|
|
186
|
+
* - 如果处于空闲状态,则退出应用
|
|
456
187
|
*/
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|