@code4bug/jarvis-agent 1.0.2 → 1.0.3
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/LICENSE +1 -1
- package/dist/cli.js +2 -2
- package/dist/commands/index.js +2 -2
- package/dist/commands/init.js +1 -1
- package/dist/components/MessageItem.d.ts +1 -1
- package/dist/components/MessageItem.js +10 -2
- package/dist/components/MultilineInput.d.ts +7 -1
- package/dist/components/MultilineInput.js +148 -4
- package/dist/components/SlashCommandMenu.d.ts +1 -1
- package/dist/components/StatusBar.js +1 -1
- package/dist/components/StreamingText.js +1 -1
- package/dist/components/WelcomeHeader.js +1 -1
- package/dist/config/constants.js +3 -3
- package/dist/config/loader.d.ts +2 -0
- package/dist/core/QueryEngine.d.ts +4 -4
- package/dist/core/QueryEngine.js +19 -17
- package/dist/core/WorkerBridge.d.ts +9 -0
- package/dist/core/WorkerBridge.js +109 -0
- package/dist/core/hint.js +4 -4
- package/dist/core/query.d.ts +8 -1
- package/dist/core/query.js +279 -57
- package/dist/core/queryWorker.d.ts +44 -0
- package/dist/core/queryWorker.js +66 -0
- package/dist/core/safeguard.js +1 -1
- package/dist/hooks/useDoubleCtrlCExit.d.ts +5 -0
- package/dist/hooks/useDoubleCtrlCExit.js +34 -0
- package/dist/hooks/useInputHistory.js +35 -3
- package/dist/hooks/useSlashMenu.d.ts +36 -0
- package/dist/hooks/useSlashMenu.js +216 -0
- package/dist/hooks/useStreamThrottle.d.ts +20 -0
- package/dist/hooks/useStreamThrottle.js +120 -0
- package/dist/hooks/useTerminalWidth.d.ts +2 -0
- package/dist/hooks/useTerminalWidth.js +13 -0
- package/dist/hooks/useTokenDisplay.d.ts +13 -0
- package/dist/hooks/useTokenDisplay.js +45 -0
- package/dist/index.js +1 -1
- package/dist/screens/repl.js +164 -636
- package/dist/screens/slashCommands.d.ts +7 -0
- package/dist/screens/slashCommands.js +134 -0
- package/dist/services/api/llm.d.ts +4 -2
- package/dist/services/api/llm.js +70 -16
- package/dist/services/api/mock.d.ts +1 -1
- package/dist/skills/index.d.ts +2 -2
- package/dist/skills/index.js +3 -3
- package/dist/tools/createSkill.d.ts +1 -1
- package/dist/tools/createSkill.js +3 -3
- package/dist/tools/index.d.ts +9 -8
- package/dist/tools/index.js +10 -9
- package/dist/tools/listDirectory.d.ts +1 -1
- package/dist/tools/readFile.d.ts +1 -1
- package/dist/tools/runCommand.d.ts +1 -1
- package/dist/tools/runCommand.js +38 -7
- package/dist/tools/searchFiles.d.ts +1 -1
- package/dist/tools/semanticSearch.d.ts +9 -0
- package/dist/tools/semanticSearch.js +159 -0
- package/dist/tools/writeFile.d.ts +1 -1
- package/dist/tools/writeFile.js +125 -25
- package/dist/types/index.d.ts +10 -1
- package/package.json +1 -1
package/dist/screens/repl.js
CHANGED
|
@@ -2,72 +2,24 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useApp } from 'ink';
|
|
4
4
|
import Spinner from 'ink-spinner';
|
|
5
|
-
import MultilineInput from '../components/MultilineInput
|
|
6
|
-
import WelcomeHeader from '../components/WelcomeHeader
|
|
7
|
-
import MessageItem from '../components/MessageItem
|
|
8
|
-
import StreamingText from '../components/StreamingText
|
|
9
|
-
import StatusBar from '../components/StatusBar
|
|
10
|
-
import SlashCommandMenu from '../components/SlashCommandMenu
|
|
11
|
-
import DangerConfirm from '../components/DangerConfirm
|
|
12
|
-
import { useWindowFocus } from '../hooks/useFocus
|
|
13
|
-
import { useInputHistory } from '../hooks/useInputHistory
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { executeInit } from '../commands/init.js';
|
|
24
|
-
/** 双击 Ctrl+C 退出:第一次按下后显示倒计时,3 秒内再按一次退出,否则取消 */
|
|
25
|
-
function useDoubleCtrlCExit(exit) {
|
|
26
|
-
const [countdown, setCountdown] = useState(null);
|
|
27
|
-
const timerRef = useRef(null);
|
|
28
|
-
const clearTimer = useCallback(() => {
|
|
29
|
-
if (timerRef.current) {
|
|
30
|
-
clearInterval(timerRef.current);
|
|
31
|
-
timerRef.current = null;
|
|
32
|
-
}
|
|
33
|
-
setCountdown(null);
|
|
34
|
-
}, []);
|
|
35
|
-
const handleCtrlC = useCallback(() => {
|
|
36
|
-
if (countdown !== null) {
|
|
37
|
-
// 第二次按下,立即退出
|
|
38
|
-
clearTimer();
|
|
39
|
-
exit();
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
// 第一次按下,启动 1 秒倒计时
|
|
43
|
-
setCountdown(1);
|
|
44
|
-
timerRef.current = setInterval(() => {
|
|
45
|
-
setCountdown((prev) => {
|
|
46
|
-
if (prev === null || prev <= 1) {
|
|
47
|
-
clearTimer();
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
return prev - 1;
|
|
51
|
-
});
|
|
52
|
-
}, 1000);
|
|
53
|
-
}, [countdown, clearTimer, exit]);
|
|
54
|
-
// 组件卸载时清理
|
|
55
|
-
useEffect(() => () => { if (timerRef.current)
|
|
56
|
-
clearInterval(timerRef.current); }, []);
|
|
57
|
-
return { countdown, handleCtrlC };
|
|
58
|
-
}
|
|
59
|
-
/** 响应式终端宽度 */
|
|
60
|
-
function useTerminalWidth() {
|
|
61
|
-
const [width, setWidth] = useState(() => process.stdout.columns || 80);
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
const onResize = () => {
|
|
64
|
-
setWidth(process.stdout.columns || 80);
|
|
65
|
-
};
|
|
66
|
-
process.stdout.on('resize', onResize);
|
|
67
|
-
return () => { process.stdout.off('resize', onResize); };
|
|
68
|
-
}, []);
|
|
69
|
-
return width;
|
|
70
|
-
}
|
|
5
|
+
import MultilineInput from '../components/MultilineInput';
|
|
6
|
+
import WelcomeHeader from '../components/WelcomeHeader';
|
|
7
|
+
import MessageItem from '../components/MessageItem';
|
|
8
|
+
import StreamingText from '../components/StreamingText';
|
|
9
|
+
import StatusBar from '../components/StatusBar';
|
|
10
|
+
import SlashCommandMenu from '../components/SlashCommandMenu';
|
|
11
|
+
import DangerConfirm from '../components/DangerConfirm';
|
|
12
|
+
import { useWindowFocus } from '../hooks/useFocus';
|
|
13
|
+
import { useInputHistory } from '../hooks/useInputHistory';
|
|
14
|
+
import { useDoubleCtrlCExit } from '../hooks/useDoubleCtrlCExit';
|
|
15
|
+
import { useTerminalWidth } from '../hooks/useTerminalWidth';
|
|
16
|
+
import { useStreamThrottle } from '../hooks/useStreamThrottle';
|
|
17
|
+
import { useTokenDisplay } from '../hooks/useTokenDisplay';
|
|
18
|
+
import { useSlashMenu } from '../hooks/useSlashMenu';
|
|
19
|
+
import { executeSlashCommand } from './slashCommands';
|
|
20
|
+
import { QueryEngine } from '../core/QueryEngine';
|
|
21
|
+
import { HIDE_WELCOME_AFTER_INPUT } from '../config/constants';
|
|
22
|
+
import { generateAgentHint } from '../core/hint';
|
|
71
23
|
export default function REPL() {
|
|
72
24
|
const { exit } = useApp();
|
|
73
25
|
const width = useTerminalWidth();
|
|
@@ -77,156 +29,61 @@ export default function REPL() {
|
|
|
77
29
|
const [messages, setMessages] = useState([]);
|
|
78
30
|
const [input, setInput] = useState('');
|
|
79
31
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
80
|
-
const [streamText, setStreamText] = useState('');
|
|
81
|
-
// 流式文本节流:用 ref 积累 chunk,定时刷新到 state,减少 re-render 频率
|
|
82
|
-
const streamBufferRef = useRef('');
|
|
83
|
-
const streamTimerRef = useRef(null);
|
|
84
|
-
const STREAM_FLUSH_INTERVAL = 80; // ms
|
|
85
|
-
// thinking 文本节流:同样用 ref 积累,定时刷新到 messages state
|
|
86
|
-
const thinkingBufferRef = useRef('');
|
|
87
|
-
const thinkingIdRef = useRef(null);
|
|
88
|
-
const thinkingTimerRef = useRef(null);
|
|
89
|
-
const THINKING_FLUSH_INTERVAL = 100; // ms
|
|
90
32
|
const [loopState, setLoopState] = useState(null);
|
|
91
33
|
const [showWelcome, setShowWelcome] = useState(true);
|
|
34
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
35
|
+
const [placeholder, setPlaceholder] = useState('');
|
|
36
|
+
const lastEscRef = useRef(0);
|
|
92
37
|
const sessionRef = useRef({
|
|
93
38
|
id: '', messages: [], createdAt: 0, updatedAt: 0, totalTokens: 0, totalCost: 0,
|
|
94
39
|
});
|
|
95
|
-
const [showDetails, setShowDetails] = useState(false);
|
|
96
|
-
// 双击 ESC 清空输入:记录首次 ESC 时间戳
|
|
97
|
-
const lastEscRef = useRef(0);
|
|
98
|
-
// 动态 placeholder:启动时通过 LLM 生成符合角色的提示
|
|
99
|
-
const [placeholder, setPlaceholder] = useState('');
|
|
100
|
-
// ===== 斜杠命令菜单状态 =====
|
|
101
|
-
const [slashMenuVisible, setSlashMenuVisible] = useState(false);
|
|
102
|
-
const [slashMenuItems, setSlashMenuItems] = useState([]);
|
|
103
|
-
const [slashMenuIndex, setSlashMenuIndex] = useState(0);
|
|
104
|
-
// 是否处于 /agent 二级菜单
|
|
105
|
-
const [agentMenuMode, setAgentMenuMode] = useState(false);
|
|
106
|
-
// 是否处于 /resume 二级菜单
|
|
107
|
-
const [resumeMenuMode, setResumeMenuMode] = useState(false);
|
|
108
|
-
// ===== 危险命令确认状态 =====
|
|
109
|
-
const [dangerConfirm, setDangerConfirm] = useState(null);
|
|
110
40
|
const engineRef = useRef(null);
|
|
111
|
-
//
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
41
|
+
// 节流 hooks
|
|
42
|
+
const { streamText, streamBufferRef, startStreamTimer, stopStreamTimer, appendStreamChunk, clearStream, handleThinkingUpdate, finishThinking, thinkingIdRef, stopAll, } = useStreamThrottle(setMessages);
|
|
43
|
+
const { displayTokens, tokenCountRef, startTokenTimer, stopTokenTimer, updateTokenCount, resetTokens, } = useTokenDisplay();
|
|
44
|
+
// 斜杠菜单
|
|
45
|
+
const slashMenu = useSlashMenu({
|
|
46
|
+
engineRef, sessionRef, tokenCountRef,
|
|
47
|
+
setMessages,
|
|
48
|
+
setDisplayTokens: (n) => updateTokenCount(n),
|
|
49
|
+
setLoopState, setIsProcessing, setShowWelcome, setInput,
|
|
50
|
+
stopAll,
|
|
51
|
+
});
|
|
52
|
+
// 危险命令确认
|
|
53
|
+
const [dangerConfirm, setDangerConfirm] = useState(null);
|
|
115
54
|
useEffect(() => {
|
|
116
55
|
engineRef.current = new QueryEngine();
|
|
117
56
|
sessionRef.current = engineRef.current.getSession();
|
|
118
|
-
// 异步生成符合当前角色的输入提示
|
|
119
57
|
generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
|
|
120
58
|
console.error('[hint] 初始化提示失败:', err);
|
|
121
59
|
});
|
|
122
60
|
}, []);
|
|
123
|
-
//
|
|
124
|
-
const startTokenTimer = useCallback(() => {
|
|
125
|
-
if (tokenTimerRef.current)
|
|
126
|
-
return;
|
|
127
|
-
tokenTimerRef.current = setInterval(() => {
|
|
128
|
-
setDisplayTokens(tokenCountRef.current);
|
|
129
|
-
}, 100);
|
|
130
|
-
}, []);
|
|
131
|
-
const stopTokenTimer = useCallback(() => {
|
|
132
|
-
if (tokenTimerRef.current) {
|
|
133
|
-
clearInterval(tokenTimerRef.current);
|
|
134
|
-
tokenTimerRef.current = null;
|
|
135
|
-
}
|
|
136
|
-
// 最终同步一次,确保最终值准确
|
|
137
|
-
setDisplayTokens(tokenCountRef.current);
|
|
138
|
-
}, []);
|
|
139
|
-
// 启动流式文本节流定时器
|
|
140
|
-
const startStreamTimer = useCallback(() => {
|
|
141
|
-
if (streamTimerRef.current)
|
|
142
|
-
return;
|
|
143
|
-
streamTimerRef.current = setInterval(() => {
|
|
144
|
-
if (streamBufferRef.current) {
|
|
145
|
-
const buf = streamBufferRef.current;
|
|
146
|
-
streamBufferRef.current = '';
|
|
147
|
-
setStreamText((prev) => prev + buf);
|
|
148
|
-
}
|
|
149
|
-
}, STREAM_FLUSH_INTERVAL);
|
|
150
|
-
}, []);
|
|
151
|
-
const stopStreamTimer = useCallback(() => {
|
|
152
|
-
if (streamTimerRef.current) {
|
|
153
|
-
clearInterval(streamTimerRef.current);
|
|
154
|
-
streamTimerRef.current = null;
|
|
155
|
-
}
|
|
156
|
-
// 最终刷新残余
|
|
157
|
-
if (streamBufferRef.current) {
|
|
158
|
-
const buf = streamBufferRef.current;
|
|
159
|
-
streamBufferRef.current = '';
|
|
160
|
-
setStreamText((prev) => prev + buf);
|
|
161
|
-
}
|
|
162
|
-
}, []);
|
|
163
|
-
// 启动 thinking 文本节流定时器
|
|
164
|
-
const startThinkingTimer = useCallback(() => {
|
|
165
|
-
if (thinkingTimerRef.current)
|
|
166
|
-
return;
|
|
167
|
-
thinkingTimerRef.current = setInterval(() => {
|
|
168
|
-
const id = thinkingIdRef.current;
|
|
169
|
-
const buf = thinkingBufferRef.current;
|
|
170
|
-
if (id && buf) {
|
|
171
|
-
setMessages((prev) => prev.map((msg) => (msg.id === id ? { ...msg, content: buf } : msg)));
|
|
172
|
-
}
|
|
173
|
-
}, THINKING_FLUSH_INTERVAL);
|
|
174
|
-
}, []);
|
|
175
|
-
const stopThinkingTimer = useCallback(() => {
|
|
176
|
-
if (thinkingTimerRef.current) {
|
|
177
|
-
clearInterval(thinkingTimerRef.current);
|
|
178
|
-
thinkingTimerRef.current = null;
|
|
179
|
-
}
|
|
180
|
-
// 最终刷新残余
|
|
181
|
-
const id = thinkingIdRef.current;
|
|
182
|
-
const buf = thinkingBufferRef.current;
|
|
183
|
-
if (id && buf) {
|
|
184
|
-
setMessages((prev) => prev.map((msg) => (msg.id === id ? { ...msg, content: buf } : msg)));
|
|
185
|
-
}
|
|
186
|
-
}, []);
|
|
187
|
-
// 组件卸载时清理定时器
|
|
188
|
-
useEffect(() => () => {
|
|
189
|
-
if (tokenTimerRef.current)
|
|
190
|
-
clearInterval(tokenTimerRef.current);
|
|
191
|
-
if (streamTimerRef.current)
|
|
192
|
-
clearInterval(streamTimerRef.current);
|
|
193
|
-
if (thinkingTimerRef.current)
|
|
194
|
-
clearInterval(thinkingTimerRef.current);
|
|
195
|
-
}, []);
|
|
61
|
+
// ===== Engine Callbacks =====
|
|
196
62
|
const callbacks = {
|
|
197
63
|
onMessage: (msg) => {
|
|
198
64
|
setMessages((prev) => [...prev, msg]);
|
|
199
65
|
},
|
|
200
66
|
onUpdateMessage: (id, updates) => {
|
|
201
|
-
// thinking
|
|
67
|
+
// thinking 消息流式内容更新(无 status 字段)
|
|
202
68
|
if (updates.content !== undefined && !updates.status && thinkingIdRef.current === null) {
|
|
203
|
-
|
|
204
|
-
thinkingIdRef.current = id;
|
|
205
|
-
thinkingBufferRef.current = updates.content;
|
|
206
|
-
startThinkingTimer();
|
|
69
|
+
handleThinkingUpdate(id, updates.content);
|
|
207
70
|
return;
|
|
208
71
|
}
|
|
209
72
|
if (thinkingIdRef.current === id && updates.content !== undefined && !updates.status) {
|
|
210
|
-
|
|
211
|
-
thinkingBufferRef.current = updates.content;
|
|
73
|
+
handleThinkingUpdate(id, updates.content);
|
|
212
74
|
return;
|
|
213
75
|
}
|
|
214
|
-
// thinking
|
|
76
|
+
// thinking 消息完成(带 status)
|
|
215
77
|
if (thinkingIdRef.current === id && updates.status) {
|
|
216
|
-
|
|
217
|
-
thinkingIdRef.current = null;
|
|
218
|
-
thinkingBufferRef.current = '';
|
|
78
|
+
finishThinking();
|
|
219
79
|
}
|
|
220
80
|
setMessages((prev) => prev.map((msg) => (msg.id === id ? { ...msg, ...updates } : msg)));
|
|
221
81
|
},
|
|
222
82
|
onStreamText: (text) => {
|
|
223
|
-
|
|
224
|
-
streamBufferRef.current += text;
|
|
83
|
+
appendStreamChunk(text);
|
|
225
84
|
},
|
|
226
85
|
onClearStreamText: () => {
|
|
227
|
-
|
|
228
|
-
streamBufferRef.current = '';
|
|
229
|
-
setStreamText('');
|
|
86
|
+
clearStream();
|
|
230
87
|
},
|
|
231
88
|
onLoopStateChange: (state) => {
|
|
232
89
|
setLoopState(state);
|
|
@@ -237,18 +94,12 @@ export default function REPL() {
|
|
|
237
94
|
}
|
|
238
95
|
else {
|
|
239
96
|
stopTokenTimer();
|
|
240
|
-
|
|
241
|
-
stopThinkingTimer();
|
|
242
|
-
thinkingIdRef.current = null;
|
|
243
|
-
thinkingBufferRef.current = '';
|
|
244
|
-
setStreamText('');
|
|
245
|
-
streamBufferRef.current = '';
|
|
97
|
+
stopAll();
|
|
246
98
|
}
|
|
247
99
|
},
|
|
248
100
|
onSessionUpdate: (s) => {
|
|
249
|
-
// 会话对象本身不参与当前界面渲染,保留在 ref 中避免每个 chunk 触发整页重绘
|
|
250
101
|
sessionRef.current = s;
|
|
251
|
-
|
|
102
|
+
updateTokenCount(s.totalTokens);
|
|
252
103
|
},
|
|
253
104
|
onConfirmDangerousCommand: (command, reason, ruleName) => {
|
|
254
105
|
return new Promise((resolve) => {
|
|
@@ -256,192 +107,59 @@ export default function REPL() {
|
|
|
256
107
|
});
|
|
257
108
|
},
|
|
258
109
|
};
|
|
259
|
-
// =====
|
|
260
|
-
const executeSlashCommand = useCallback((cmdName) => {
|
|
261
|
-
switch (cmdName) {
|
|
262
|
-
case 'init': {
|
|
263
|
-
const result = executeInit();
|
|
264
|
-
const initMsg = {
|
|
265
|
-
id: `init-${Date.now()}`,
|
|
266
|
-
type: 'system',
|
|
267
|
-
status: 'success',
|
|
268
|
-
content: result.displayText,
|
|
269
|
-
timestamp: Date.now(),
|
|
270
|
-
};
|
|
271
|
-
setMessages((prev) => [...prev, initMsg]);
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
case 'new':
|
|
275
|
-
// 新会话:重置引擎 + 清空所有状态
|
|
276
|
-
if (engineRef.current) {
|
|
277
|
-
engineRef.current.reset();
|
|
278
|
-
sessionRef.current = engineRef.current.getSession();
|
|
279
|
-
}
|
|
280
|
-
setMessages([]);
|
|
281
|
-
setStreamText('');
|
|
282
|
-
streamBufferRef.current = '';
|
|
283
|
-
setLoopState(null);
|
|
284
|
-
setIsProcessing(false);
|
|
285
|
-
setShowWelcome(true);
|
|
286
|
-
tokenCountRef.current = 0;
|
|
287
|
-
setDisplayTokens(0);
|
|
288
|
-
generateAgentHint().then((hint) => setPlaceholder(hint)).catch(() => { });
|
|
289
|
-
break;
|
|
290
|
-
case 'help': {
|
|
291
|
-
const helpText = [
|
|
292
|
-
'可用命令:',
|
|
293
|
-
' /init 初始化项目信息,生成 JARVIS.md',
|
|
294
|
-
' /new 开启新会话,重新初始化上下文',
|
|
295
|
-
' /resume 恢复历史会话(支持二级菜单选择)',
|
|
296
|
-
' /resume <ID> 直接恢复指定会话',
|
|
297
|
-
' /help 显示此帮助信息',
|
|
298
|
-
' /session_clear 清理所有非当前会话的历史记录',
|
|
299
|
-
' /skills 查看当前所有 tools 和 skills',
|
|
300
|
-
' /permissions 查看所有持久化授权列表',
|
|
301
|
-
' /create_skill <描述> 根据需求创建新 skill',
|
|
302
|
-
' /agent <名称> 切换智能体(需重启生效)',
|
|
303
|
-
' /read <路径> 读取文件内容',
|
|
304
|
-
' /write <路径> 写入文件',
|
|
305
|
-
' /bash <命令> 执行 Bash 命令',
|
|
306
|
-
' /ls <路径> 列出目录',
|
|
307
|
-
' /search <词> 搜索文件内容',
|
|
308
|
-
' /version 显示当前版本号',
|
|
309
|
-
'',
|
|
310
|
-
'快捷键:',
|
|
311
|
-
' Ctrl+L 清屏重置',
|
|
312
|
-
' Ctrl+O 切换详情显示',
|
|
313
|
-
' ESC 中断推理 / 双击清空输入',
|
|
314
|
-
' Ctrl+C ×2 退出',
|
|
315
|
-
].join('\n');
|
|
316
|
-
const helpMsg = {
|
|
317
|
-
id: `help-${Date.now()}`,
|
|
318
|
-
type: 'system',
|
|
319
|
-
status: 'success',
|
|
320
|
-
content: helpText,
|
|
321
|
-
timestamp: Date.now(),
|
|
322
|
-
};
|
|
323
|
-
setMessages((prev) => [...prev, helpMsg]);
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
326
|
-
case 'session_clear': {
|
|
327
|
-
if (engineRef.current) {
|
|
328
|
-
const count = engineRef.current.clearOtherSessions();
|
|
329
|
-
const resultMsg = {
|
|
330
|
-
id: `session-clear-${Date.now()}`,
|
|
331
|
-
type: 'system',
|
|
332
|
-
status: 'success',
|
|
333
|
-
content: count > 0
|
|
334
|
-
? `已清理 ${count} 个历史会话,当前会话保留。`
|
|
335
|
-
: '当前没有需要清理的历史会话。',
|
|
336
|
-
timestamp: Date.now(),
|
|
337
|
-
};
|
|
338
|
-
setMessages((prev) => [...prev, resultMsg]);
|
|
339
|
-
}
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
case 'permissions': {
|
|
343
|
-
const perms = listPermanentAuthorizations();
|
|
344
|
-
const lines = ['持久化授权列表 (~/.jarvis/.permissions.json)', ''];
|
|
345
|
-
if (perms.rules.length > 0) {
|
|
346
|
-
lines.push('按规则授权:');
|
|
347
|
-
for (const r of perms.rules) {
|
|
348
|
-
const rule = DANGER_RULES.find((d) => d.name === r);
|
|
349
|
-
lines.push(` [v] ${r}${rule ? ` — ${rule.reason}` : ''}`);
|
|
350
|
-
}
|
|
351
|
-
lines.push('');
|
|
352
|
-
}
|
|
353
|
-
if (perms.commands.length > 0) {
|
|
354
|
-
lines.push('按命令授权:');
|
|
355
|
-
for (const c of perms.commands) {
|
|
356
|
-
lines.push(` [v] [${c.ruleName}] ${c.command} (${c.grantedAt})`);
|
|
357
|
-
}
|
|
358
|
-
lines.push('');
|
|
359
|
-
}
|
|
360
|
-
if (perms.rules.length === 0 && perms.commands.length === 0) {
|
|
361
|
-
lines.push('(空) 暂无持久化授权记录');
|
|
362
|
-
}
|
|
363
|
-
const permMsg = {
|
|
364
|
-
id: `perms-${Date.now()}`,
|
|
365
|
-
type: 'system',
|
|
366
|
-
status: 'success',
|
|
367
|
-
content: lines.join('\n'),
|
|
368
|
-
timestamp: Date.now(),
|
|
369
|
-
};
|
|
370
|
-
setMessages((prev) => [...prev, permMsg]);
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
|
-
case 'skills': {
|
|
374
|
-
const skills = listSkills();
|
|
375
|
-
const parts = [];
|
|
376
|
-
parts.push('### Built-in Tools\n');
|
|
377
|
-
allTools.forEach((t, i) => {
|
|
378
|
-
parts.push(`${i + 1}. \`${t.name}\` - ${t.description.slice(0, 60)}`);
|
|
379
|
-
});
|
|
380
|
-
parts.push('');
|
|
381
|
-
parts.push(`### External Skills\n`);
|
|
382
|
-
parts.push(`> ${getExternalSkillsDir()}\n`);
|
|
383
|
-
if (skills.length === 0) {
|
|
384
|
-
parts.push('_(empty)_');
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
skills.forEach((s, i) => {
|
|
388
|
-
const hint = s.meta.argumentHint ? ` \`${s.meta.argumentHint}\`` : '';
|
|
389
|
-
const flags = [];
|
|
390
|
-
if (s.meta.disableModelInvocation)
|
|
391
|
-
flags.push('manual-only');
|
|
392
|
-
if (s.meta.userInvocable === false)
|
|
393
|
-
flags.push('hidden');
|
|
394
|
-
const flagStr = flags.length > 0 ? ` _(${flags.join(', ')})_` : '';
|
|
395
|
-
parts.push(`${i + 1}. \`${s.meta.name}\`${hint} - ${s.meta.description}${flagStr}`);
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
parts.push('');
|
|
399
|
-
parts.push(`**Total:** ${allTools.length} tools + ${skills.length} skills = ${allTools.length + skills.length}`);
|
|
400
|
-
const skillsMsg = {
|
|
401
|
-
id: `skills-${Date.now()}`,
|
|
402
|
-
type: 'system',
|
|
403
|
-
status: 'success',
|
|
404
|
-
content: parts.join('\n'),
|
|
405
|
-
timestamp: Date.now(),
|
|
406
|
-
};
|
|
407
|
-
setMessages((prev) => [...prev, skillsMsg]);
|
|
408
|
-
break;
|
|
409
|
-
}
|
|
410
|
-
case 'version': {
|
|
411
|
-
const versionMsg = {
|
|
412
|
-
id: `version-${Date.now()}`,
|
|
413
|
-
type: 'system',
|
|
414
|
-
status: 'success',
|
|
415
|
-
content: `当前版本: ${APP_VERSION}`,
|
|
416
|
-
timestamp: Date.now(),
|
|
417
|
-
};
|
|
418
|
-
setMessages((prev) => [...prev, versionMsg]);
|
|
419
|
-
break;
|
|
420
|
-
}
|
|
421
|
-
default:
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
}, []);
|
|
110
|
+
// ===== 提交处理 =====
|
|
425
111
|
const handleSubmit = useCallback(async (value) => {
|
|
426
112
|
const trimmed = value.trim();
|
|
427
113
|
if (!trimmed || isProcessing || !engineRef.current)
|
|
428
114
|
return;
|
|
429
|
-
// 斜杠命令拦截
|
|
430
115
|
if (trimmed.startsWith('/')) {
|
|
431
116
|
const parts = trimmed.slice(1).split(/\s+/);
|
|
432
117
|
const cmdName = parts[0].toLowerCase();
|
|
433
118
|
const hasArgs = parts.length > 1 && parts.slice(1).join('').length > 0;
|
|
434
|
-
//
|
|
119
|
+
// 内置命令
|
|
435
120
|
if (['new', 'help', 'init', 'session_clear', 'permissions', 'skills', 'version'].includes(cmdName)) {
|
|
436
121
|
setInput('');
|
|
437
|
-
setSlashMenuVisible(false);
|
|
438
|
-
|
|
122
|
+
slashMenu.setSlashMenuVisible(false);
|
|
123
|
+
if (cmdName === 'new') {
|
|
124
|
+
// 新会话:重置引擎 + 清空所有状态
|
|
125
|
+
if (engineRef.current) {
|
|
126
|
+
engineRef.current.reset();
|
|
127
|
+
sessionRef.current = engineRef.current.getSession();
|
|
128
|
+
}
|
|
129
|
+
setMessages([]);
|
|
130
|
+
clearStream();
|
|
131
|
+
setLoopState(null);
|
|
132
|
+
setIsProcessing(false);
|
|
133
|
+
setShowWelcome(true);
|
|
134
|
+
resetTokens();
|
|
135
|
+
generateAgentHint().then((hint) => setPlaceholder(hint)).catch(() => { });
|
|
136
|
+
}
|
|
137
|
+
else if (cmdName === 'session_clear') {
|
|
138
|
+
if (engineRef.current) {
|
|
139
|
+
const count = engineRef.current.clearOtherSessions();
|
|
140
|
+
const resultMsg = {
|
|
141
|
+
id: `session-clear-${Date.now()}`,
|
|
142
|
+
type: 'system',
|
|
143
|
+
status: 'success',
|
|
144
|
+
content: count > 0
|
|
145
|
+
? `已清理 ${count} 个历史会话,当前会话保留。`
|
|
146
|
+
: '当前没有需要清理的历史会话。',
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
};
|
|
149
|
+
setMessages((prev) => [...prev, resultMsg]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const msg = executeSlashCommand(cmdName);
|
|
154
|
+
if (msg)
|
|
155
|
+
setMessages((prev) => [...prev, msg]);
|
|
156
|
+
}
|
|
439
157
|
return;
|
|
440
158
|
}
|
|
441
|
-
// /create_skill
|
|
159
|
+
// /create_skill
|
|
442
160
|
if (cmdName === 'create_skill') {
|
|
443
161
|
setInput('');
|
|
444
|
-
setSlashMenuVisible(false);
|
|
162
|
+
slashMenu.setSlashMenuVisible(false);
|
|
445
163
|
const skillArgs = parts.slice(1).join(' ').trim();
|
|
446
164
|
if (!skillArgs) {
|
|
447
165
|
const hintMsg = {
|
|
@@ -454,56 +172,23 @@ export default function REPL() {
|
|
|
454
172
|
setMessages((prev) => [...prev, hintMsg]);
|
|
455
173
|
return;
|
|
456
174
|
}
|
|
457
|
-
// 构造提示,让 LLM 使用 create_skill 工具
|
|
458
175
|
const prompt = `请使用 create_skill 工具帮我创建一个新的 skill。需求如下:${skillArgs}\n\n请根据需求自动生成合适的 name、description、instruction,如果需要 Python 实现也请生成 skill.py 代码。`;
|
|
459
176
|
if (HIDE_WELCOME_AFTER_INPUT)
|
|
460
177
|
setShowWelcome(false);
|
|
461
178
|
pushHistory(trimmed);
|
|
462
|
-
|
|
179
|
+
clearStream();
|
|
463
180
|
await engineRef.current.handleQuery(prompt, callbacks);
|
|
464
181
|
return;
|
|
465
182
|
}
|
|
466
|
-
// /resume
|
|
183
|
+
// /resume
|
|
467
184
|
if (cmdName === 'resume') {
|
|
468
185
|
setInput('');
|
|
469
|
-
setSlashMenuVisible(false);
|
|
470
|
-
setResumeMenuMode(false);
|
|
186
|
+
slashMenu.setSlashMenuVisible(false);
|
|
471
187
|
if (hasArgs && engineRef.current) {
|
|
472
|
-
// /resume <id> 直接恢复
|
|
473
188
|
const sessionId = parts.slice(1).join(' ').trim();
|
|
474
|
-
|
|
475
|
-
if (result) {
|
|
476
|
-
setMessages(result.messages);
|
|
477
|
-
sessionRef.current = result.session;
|
|
478
|
-
tokenCountRef.current = result.session.totalTokens;
|
|
479
|
-
setDisplayTokens(result.session.totalTokens);
|
|
480
|
-
setStreamText('');
|
|
481
|
-
streamBufferRef.current = '';
|
|
482
|
-
setLoopState(null);
|
|
483
|
-
setIsProcessing(false);
|
|
484
|
-
setShowWelcome(false);
|
|
485
|
-
const resumeMsg = {
|
|
486
|
-
id: `resume-${Date.now()}`,
|
|
487
|
-
type: 'system',
|
|
488
|
-
status: 'success',
|
|
489
|
-
content: `已恢复会话 ${sessionId.slice(0, 8)}...(${result.messages.length} 条消息)`,
|
|
490
|
-
timestamp: Date.now(),
|
|
491
|
-
};
|
|
492
|
-
setMessages((prev) => [...prev, resumeMsg]);
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
const errMsg = {
|
|
496
|
-
id: `resume-err-${Date.now()}`,
|
|
497
|
-
type: 'error',
|
|
498
|
-
status: 'error',
|
|
499
|
-
content: `会话 ${sessionId} 不存在或已损坏`,
|
|
500
|
-
timestamp: Date.now(),
|
|
501
|
-
};
|
|
502
|
-
setMessages((prev) => [...prev, errMsg]);
|
|
503
|
-
}
|
|
189
|
+
slashMenu.resumeSession(sessionId);
|
|
504
190
|
}
|
|
505
191
|
else {
|
|
506
|
-
// /resume 无参数:显示会话列表
|
|
507
192
|
const sessions = QueryEngine.listSessions().slice(0, 20);
|
|
508
193
|
if (sessions.length === 0) {
|
|
509
194
|
const noMsg = {
|
|
@@ -533,56 +218,18 @@ export default function REPL() {
|
|
|
533
218
|
}
|
|
534
219
|
return;
|
|
535
220
|
}
|
|
536
|
-
//
|
|
537
|
-
if (
|
|
538
|
-
setInput('');
|
|
539
|
-
setSlashMenuVisible(false);
|
|
540
|
-
const perms = listPermanentAuthorizations();
|
|
541
|
-
const lines = ['持久化授权列表 (~/.jarvis/.permissions.json)', ''];
|
|
542
|
-
if (perms.rules.length > 0) {
|
|
543
|
-
lines.push('按规则授权:');
|
|
544
|
-
for (const r of perms.rules) {
|
|
545
|
-
const rule = DANGER_RULES.find((d) => d.name === r);
|
|
546
|
-
lines.push(` [v] ${r}${rule ? ` — ${rule.reason}` : ''}`);
|
|
547
|
-
}
|
|
548
|
-
lines.push('');
|
|
549
|
-
}
|
|
550
|
-
if (perms.commands.length > 0) {
|
|
551
|
-
lines.push('按命令授权:');
|
|
552
|
-
for (const c of perms.commands) {
|
|
553
|
-
lines.push(` [v] [${c.ruleName}] ${c.command} (${c.grantedAt})`);
|
|
554
|
-
}
|
|
555
|
-
lines.push('');
|
|
556
|
-
}
|
|
557
|
-
if (perms.rules.length === 0 && perms.commands.length === 0) {
|
|
558
|
-
lines.push('(空) 暂无持久化授权记录');
|
|
559
|
-
}
|
|
560
|
-
const listMsg = {
|
|
561
|
-
id: `perms-${Date.now()}`,
|
|
562
|
-
type: 'system',
|
|
563
|
-
status: 'success',
|
|
564
|
-
content: lines.join('\n'),
|
|
565
|
-
timestamp: Date.now(),
|
|
566
|
-
};
|
|
567
|
-
setMessages((prev) => [...prev, listMsg]);
|
|
221
|
+
// 工具命令带参数:继续走正常提交流程;无参数则忽略
|
|
222
|
+
if (!hasArgs)
|
|
568
223
|
return;
|
|
569
|
-
}
|
|
570
|
-
// 工具命令带参数:去掉 / 前缀,作为消息发给 LLM
|
|
571
|
-
if (hasArgs) {
|
|
572
|
-
// 继续走正常提交流程
|
|
573
|
-
}
|
|
574
|
-
else {
|
|
575
|
-
// 无参数的 / 命令,忽略
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
224
|
}
|
|
579
225
|
if (HIDE_WELCOME_AFTER_INPUT)
|
|
580
226
|
setShowWelcome(false);
|
|
581
227
|
pushHistory(trimmed);
|
|
582
228
|
setInput('');
|
|
583
|
-
|
|
229
|
+
clearStream();
|
|
584
230
|
await engineRef.current.handleQuery(trimmed, callbacks);
|
|
585
|
-
}, [isProcessing, pushHistory,
|
|
231
|
+
}, [isProcessing, pushHistory, clearStream, resetTokens, slashMenu]);
|
|
232
|
+
// ===== 输入处理 =====
|
|
586
233
|
const handleUpArrow = useCallback(() => {
|
|
587
234
|
const result = navigateUp(input);
|
|
588
235
|
if (result !== null)
|
|
@@ -596,197 +243,82 @@ export default function REPL() {
|
|
|
596
243
|
const handleInputChange = useCallback((val) => {
|
|
597
244
|
resetNavigation();
|
|
598
245
|
setInput(val);
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
// /agent 二级菜单:输入 "/agent " 或 "/agent xxx" 时显示智能体列表
|
|
603
|
-
if (/^agent\s/i.test(query)) {
|
|
604
|
-
const subQuery = query.replace(/^agent\s*/i, '');
|
|
605
|
-
const matched = filterAgentCommands(subQuery);
|
|
606
|
-
setSlashMenuItems(matched);
|
|
607
|
-
setSlashMenuIndex(0);
|
|
608
|
-
setSlashMenuVisible(matched.length > 0);
|
|
609
|
-
setAgentMenuMode(true);
|
|
610
|
-
setResumeMenuMode(false);
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
// /resume 二级菜单:输入 "/resume " 或 "/resume xxx" 时显示历史会话列表
|
|
614
|
-
if (/^resume\s/i.test(query)) {
|
|
615
|
-
const subQuery = query.replace(/^resume\s*/i, '').toLowerCase();
|
|
616
|
-
const sessions = QueryEngine.listSessions().slice(0, 20);
|
|
617
|
-
const items = sessions.map((s) => {
|
|
618
|
-
const date = new Date(s.updatedAt);
|
|
619
|
-
const dateStr = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
620
|
-
return {
|
|
621
|
-
name: s.id,
|
|
622
|
-
description: `[${dateStr}] ${s.summary}`,
|
|
623
|
-
category: 'builtin',
|
|
624
|
-
};
|
|
625
|
-
});
|
|
626
|
-
const matched = subQuery
|
|
627
|
-
? items.filter((i) => i.name.includes(subQuery) || i.description.toLowerCase().includes(subQuery))
|
|
628
|
-
: items;
|
|
629
|
-
setSlashMenuItems(matched);
|
|
630
|
-
setSlashMenuIndex(0);
|
|
631
|
-
setSlashMenuVisible(matched.length > 0);
|
|
632
|
-
setResumeMenuMode(true);
|
|
633
|
-
setAgentMenuMode(false);
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
// 一级菜单
|
|
637
|
-
const matched = filterCommands(query);
|
|
638
|
-
setSlashMenuItems(matched);
|
|
639
|
-
setSlashMenuIndex(0);
|
|
640
|
-
setSlashMenuVisible(matched.length > 0);
|
|
641
|
-
setAgentMenuMode(false);
|
|
642
|
-
setResumeMenuMode(false);
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
setSlashMenuVisible(false);
|
|
646
|
-
setAgentMenuMode(false);
|
|
647
|
-
setResumeMenuMode(false);
|
|
648
|
-
}
|
|
649
|
-
}, [resetNavigation]);
|
|
650
|
-
// 斜杠菜单:上移
|
|
651
|
-
const handleSlashMenuUp = useCallback(() => {
|
|
652
|
-
setSlashMenuIndex((prev) => (prev > 0 ? prev - 1 : slashMenuItems.length - 1));
|
|
653
|
-
}, [slashMenuItems.length]);
|
|
654
|
-
// 斜杠菜单:下移
|
|
655
|
-
const handleSlashMenuDown = useCallback(() => {
|
|
656
|
-
setSlashMenuIndex((prev) => (prev < slashMenuItems.length - 1 ? prev + 1 : 0));
|
|
657
|
-
}, [slashMenuItems.length]);
|
|
658
|
-
// 斜杠菜单:选中
|
|
659
|
-
const handleSlashMenuSelect = useCallback(() => {
|
|
660
|
-
if (slashMenuItems.length === 0)
|
|
661
|
-
return;
|
|
662
|
-
const cmd = slashMenuItems[slashMenuIndex];
|
|
663
|
-
if (!cmd)
|
|
664
|
-
return;
|
|
665
|
-
// 二级 agent 菜单:执行切换
|
|
666
|
-
if (agentMenuMode) {
|
|
667
|
-
setActiveAgent(cmd.name);
|
|
668
|
-
setInput('');
|
|
669
|
-
setSlashMenuVisible(false);
|
|
670
|
-
setAgentMenuMode(false);
|
|
671
|
-
const switchMsg = {
|
|
672
|
-
id: `switch-${Date.now()}`,
|
|
673
|
-
type: 'system',
|
|
674
|
-
status: 'success',
|
|
675
|
-
content: `已切换智能体为 ${cmd.name},请重启以生效(Ctrl+C 两次退出后重新启动)`,
|
|
676
|
-
timestamp: Date.now(),
|
|
677
|
-
};
|
|
678
|
-
setMessages((prev) => [...prev, switchMsg]);
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
// 二级 resume 菜单:恢复会话
|
|
682
|
-
if (resumeMenuMode) {
|
|
683
|
-
setInput('');
|
|
684
|
-
setSlashMenuVisible(false);
|
|
685
|
-
setResumeMenuMode(false);
|
|
686
|
-
if (engineRef.current) {
|
|
687
|
-
const result = engineRef.current.loadSession(cmd.name);
|
|
688
|
-
if (result) {
|
|
689
|
-
setMessages(result.messages);
|
|
690
|
-
sessionRef.current = result.session;
|
|
691
|
-
tokenCountRef.current = result.session.totalTokens;
|
|
692
|
-
setDisplayTokens(result.session.totalTokens);
|
|
693
|
-
setStreamText('');
|
|
694
|
-
streamBufferRef.current = '';
|
|
695
|
-
setLoopState(null);
|
|
696
|
-
setIsProcessing(false);
|
|
697
|
-
setShowWelcome(false);
|
|
698
|
-
const resumeMsg = {
|
|
699
|
-
id: `resume-${Date.now()}`,
|
|
700
|
-
type: 'system',
|
|
701
|
-
status: 'success',
|
|
702
|
-
content: `已恢复会话 ${cmd.name.slice(0, 8)}...(${result.messages.length} 条消息)`,
|
|
703
|
-
timestamp: Date.now(),
|
|
704
|
-
};
|
|
705
|
-
setMessages((prev) => [...prev, resumeMsg]);
|
|
706
|
-
}
|
|
707
|
-
else {
|
|
708
|
-
const errMsg = {
|
|
709
|
-
id: `resume-err-${Date.now()}`,
|
|
710
|
-
type: 'error',
|
|
711
|
-
status: 'error',
|
|
712
|
-
content: `会话 ${cmd.name} 不存在或已损坏`,
|
|
713
|
-
timestamp: Date.now(),
|
|
714
|
-
};
|
|
715
|
-
setMessages((prev) => [...prev, errMsg]);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
// 一级菜单选中 /agent → 进入二级菜单
|
|
721
|
-
if (cmd.name === 'agent') {
|
|
722
|
-
setInput('/agent ');
|
|
723
|
-
const matched = filterAgentCommands('');
|
|
724
|
-
setSlashMenuItems(matched);
|
|
725
|
-
setSlashMenuIndex(0);
|
|
726
|
-
setAgentMenuMode(true);
|
|
727
|
-
setResumeMenuMode(false);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
// 一级菜单选中 /resume → 进入二级菜单
|
|
731
|
-
if (cmd.name === 'resume') {
|
|
732
|
-
setInput('/resume ');
|
|
733
|
-
const sessions = QueryEngine.listSessions().slice(0, 20);
|
|
734
|
-
const items = sessions.map((s) => {
|
|
735
|
-
const date = new Date(s.updatedAt);
|
|
736
|
-
const dateStr = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
737
|
-
return {
|
|
738
|
-
name: s.id,
|
|
739
|
-
description: `[${dateStr}] ${s.summary}`,
|
|
740
|
-
category: 'builtin',
|
|
741
|
-
};
|
|
742
|
-
});
|
|
743
|
-
setSlashMenuItems(items);
|
|
744
|
-
setSlashMenuIndex(0);
|
|
745
|
-
setSlashMenuVisible(items.length > 0);
|
|
746
|
-
setResumeMenuMode(true);
|
|
747
|
-
setAgentMenuMode(false);
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
// 内置命令:直接执行
|
|
751
|
-
if (cmd.category === 'builtin') {
|
|
752
|
-
setInput('');
|
|
753
|
-
setSlashMenuVisible(false);
|
|
754
|
-
executeSlashCommand(cmd.name);
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
// 工具命令:填入输入框,等用户补充参数
|
|
758
|
-
setInput(`/${cmd.name} `);
|
|
759
|
-
setSlashMenuVisible(false);
|
|
760
|
-
}, [slashMenuItems, slashMenuIndex, agentMenuMode, resumeMenuMode, executeSlashCommand]);
|
|
761
|
-
// 斜杠菜单:关闭
|
|
762
|
-
const handleSlashMenuClose = useCallback(() => {
|
|
763
|
-
if (agentMenuMode || resumeMenuMode) {
|
|
764
|
-
// 二级菜单 ESC → 回到一级菜单
|
|
765
|
-
setAgentMenuMode(false);
|
|
766
|
-
setResumeMenuMode(false);
|
|
767
|
-
setInput('/');
|
|
768
|
-
const matched = filterCommands('');
|
|
769
|
-
setSlashMenuItems(matched);
|
|
770
|
-
setSlashMenuIndex(0);
|
|
771
|
-
setSlashMenuVisible(matched.length > 0);
|
|
772
|
-
}
|
|
773
|
-
else {
|
|
774
|
-
setSlashMenuVisible(false);
|
|
775
|
-
}
|
|
776
|
-
}, [agentMenuMode, resumeMenuMode]);
|
|
777
|
-
// Tab 填入 placeholder 推荐问题
|
|
246
|
+
slashMenu.updateSlashMenu(val);
|
|
247
|
+
}, [resetNavigation, slashMenu]);
|
|
248
|
+
// Tab 填入 placeholder
|
|
778
249
|
const handleTabFillPlaceholder = useCallback(() => {
|
|
779
250
|
if (placeholder) {
|
|
780
|
-
// placeholder 格式为 Try "xxx...",提取引号内的内容
|
|
781
251
|
const match = placeholder.match(/^Try\s+"(.+)"$/);
|
|
782
252
|
const text = match ? match[1] : placeholder;
|
|
783
253
|
setInput(text);
|
|
784
254
|
}
|
|
785
255
|
}, [placeholder]);
|
|
786
|
-
//
|
|
256
|
+
// ===== 隐藏系统默认终端光标,使用 ink 渲染的反色光标代替 =====
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
process.stdout.write('\x1B[?25l');
|
|
259
|
+
return () => {
|
|
260
|
+
process.stdout.write('\x1B[?25h');
|
|
261
|
+
};
|
|
262
|
+
}, []);
|
|
263
|
+
// ===== processing/streaming 期间,每次 re-render 后将物理光标移到行首 =====
|
|
264
|
+
// ink 每次渲染后光标停在 StatusBar 行末,macOS IME 会在该位置显示 composing 候选框,
|
|
265
|
+
// 导致候选框与进度条重叠。将光标移到列 1(行首)让候选框出现在屏幕左侧空白区域。
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (!isProcessing)
|
|
268
|
+
return;
|
|
269
|
+
process.stdout.write('\x1B[1G');
|
|
270
|
+
});
|
|
271
|
+
// ===== processing 期间隐藏终端光标,阻止 IME composing 显示 =====
|
|
272
|
+
const isProcessingRef = useRef(isProcessing);
|
|
273
|
+
isProcessingRef.current = isProcessing;
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (!isProcessing)
|
|
276
|
+
return;
|
|
277
|
+
// 隐藏终端光标 — 终端 IME 的 inline composing 仅在光标可见时渲染
|
|
278
|
+
process.stdout.write('\x1B[?25l');
|
|
279
|
+
// 高优先级 stdin 拦截:吞掉 processing 期间的所有输入(Ctrl+C / ESC 除外)
|
|
280
|
+
// 防止字符积累在缓冲区,processing 结束后污染输入框
|
|
281
|
+
const drain = (data) => {
|
|
282
|
+
if (!isProcessingRef.current)
|
|
283
|
+
return;
|
|
284
|
+
const raw = typeof data === 'string' ? data : data.toString('utf-8');
|
|
285
|
+
// 放行 Ctrl+C、ESC 和 Ctrl+O,其余全部吞掉
|
|
286
|
+
if (raw === '\x03' || raw === '\x1B' || raw === '\x0F')
|
|
287
|
+
return;
|
|
288
|
+
// 清空 Buffer 内容(仅当确实是 Buffer 时),减少后续处理的干扰
|
|
289
|
+
if (Buffer.isBuffer(data))
|
|
290
|
+
data.fill(0);
|
|
291
|
+
};
|
|
292
|
+
process.stdin.prependListener('data', drain);
|
|
293
|
+
return () => {
|
|
294
|
+
process.stdin.removeListener('data', drain);
|
|
295
|
+
// 注意:不在此处恢复终端光标,由 repl.tsx 顶层 useEffect 统一管理
|
|
296
|
+
// drain stdin 缓冲区,丢弃 processing 期间积累的数据
|
|
297
|
+
if (typeof process.stdin.read === 'function') {
|
|
298
|
+
while (process.stdin.read() !== null) { /* drain */ }
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}, [isProcessing]);
|
|
302
|
+
// ===== 快捷键 =====
|
|
787
303
|
useInput((ch, key) => {
|
|
788
|
-
|
|
789
|
-
|
|
304
|
+
// processing 状态下,仅允许 Ctrl+C(退出)和 ESC(中断)
|
|
305
|
+
if (isProcessing) {
|
|
306
|
+
if (key.ctrl && ch === 'c') {
|
|
307
|
+
handleCtrlC();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (key.escape && engineRef.current) {
|
|
311
|
+
engineRef.current.abort();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (key.ctrl && ch === 'o') {
|
|
315
|
+
setShowDetails((prev) => !prev);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
return; // 丢弃其他所有按键
|
|
319
|
+
}
|
|
320
|
+
if (key.tab && slashMenu.slashMenuVisible) {
|
|
321
|
+
slashMenu.handleSlashMenuSelect();
|
|
790
322
|
return;
|
|
791
323
|
}
|
|
792
324
|
if (key.ctrl && ch === 'c') {
|
|
@@ -803,14 +335,11 @@ export default function REPL() {
|
|
|
803
335
|
sessionRef.current = engineRef.current.getSession();
|
|
804
336
|
}
|
|
805
337
|
setMessages([]);
|
|
806
|
-
|
|
807
|
-
streamBufferRef.current = '';
|
|
338
|
+
clearStream();
|
|
808
339
|
setLoopState(null);
|
|
809
340
|
setIsProcessing(false);
|
|
810
341
|
setShowWelcome(true);
|
|
811
|
-
|
|
812
|
-
setDisplayTokens(0);
|
|
813
|
-
// 重新生成角色提示
|
|
342
|
+
resetTokens();
|
|
814
343
|
generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
|
|
815
344
|
console.error('[hint] 重新生成提示失败:', err);
|
|
816
345
|
});
|
|
@@ -818,11 +347,9 @@ export default function REPL() {
|
|
|
818
347
|
}
|
|
819
348
|
if (key.escape) {
|
|
820
349
|
if (isProcessing && engineRef.current) {
|
|
821
|
-
// 推理中按 ESC:中断推理,流式文本停止后由 query 层标记最后一条消息为 aborted
|
|
822
350
|
engineRef.current.abort();
|
|
823
351
|
}
|
|
824
352
|
else if (input.length > 0) {
|
|
825
|
-
// 输入框有内容:双击 ESC(500ms 内)清空
|
|
826
353
|
const now = Date.now();
|
|
827
354
|
if (now - lastEscRef.current < 500) {
|
|
828
355
|
setInput('');
|
|
@@ -835,8 +362,9 @@ export default function REPL() {
|
|
|
835
362
|
return;
|
|
836
363
|
}
|
|
837
364
|
});
|
|
365
|
+
// ===== 渲染 =====
|
|
838
366
|
return (_jsxs(Box, { flexDirection: "column", width: width, children: [showWelcome && _jsx(WelcomeHeader, { width: width }), _jsxs(Box, { flexDirection: "column", paddingX: 1, marginTop: showWelcome ? 0 : 1, children: [messages.map((msg) => (_jsx(MessageItem, { msg: msg, showDetails: showDetails }, msg.id))), streamText && _jsx(StreamingText, { text: streamText }), dangerConfirm && (_jsx(DangerConfirm, { command: dangerConfirm.command, reason: dangerConfirm.reason, ruleName: dangerConfirm.ruleName, onSelect: (choice) => {
|
|
839
367
|
dangerConfirm.resolve(choice);
|
|
840
368
|
setDangerConfirm(null);
|
|
841
|
-
} })), loopState?.isRunning && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" iteration ", loopState.iteration, "/", loopState.maxIterations] })] }))] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), slashMenuVisible && !isProcessing && (_jsx(SlashCommandMenu, { commands: slashMenuItems, selectedIndex: slashMenuIndex })), _jsx(Box, { children: countdown !== null ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: "Press " }), _jsx(Text, { color: "yellow", bold: true, children: "Ctrl+C" }), _jsx(Text, { color: "yellow", children: " again to exit " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["(", countdown, "s)"] })] })) : isProcessing ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "gray", italic: true, children: " processing..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(MultilineInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, placeholder: placeholder, isActive: !isProcessing, showCursor: windowFocused && !isProcessing, slashMenuActive: slashMenuVisible, onSlashMenuUp: handleSlashMenuUp, onSlashMenuDown: handleSlashMenuDown, onSlashMenuSelect: handleSlashMenuSelect, onSlashMenuClose: handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder })] })) }), _jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), _jsx(StatusBar, { width: width - 2, totalTokens: displayTokens })] })] }));
|
|
369
|
+
} })), loopState?.isRunning && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" iteration ", loopState.iteration, "/", loopState.maxIterations] })] }))] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), slashMenu.slashMenuVisible && !isProcessing && (_jsx(SlashCommandMenu, { commands: slashMenu.slashMenuItems, selectedIndex: slashMenu.slashMenuIndex })), _jsx(Box, { children: countdown !== null ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: "Press " }), _jsx(Text, { color: "yellow", bold: true, children: "Ctrl+C" }), _jsx(Text, { color: "yellow", children: " again to exit " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["(", countdown, "s)"] })] })) : isProcessing ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "gray", italic: true, children: " processing..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(MultilineInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, placeholder: placeholder, isActive: !isProcessing, showCursor: windowFocused && !isProcessing, slashMenuActive: slashMenu.slashMenuVisible, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: slashMenu.handleSlashMenuSelect, onSlashMenuClose: slashMenu.handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder })] })) }), _jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), _jsx(StatusBar, { width: width - 2, totalTokens: displayTokens })] })] }));
|
|
842
370
|
}
|