@code4bug/jarvis-agent 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/dist/agents/code-reviewer.md +69 -0
  4. package/dist/agents/dba.md +68 -0
  5. package/dist/agents/finance-advisor.md +81 -0
  6. package/dist/agents/index.d.ts +31 -0
  7. package/dist/agents/index.js +86 -0
  8. package/dist/agents/jarvis.md +95 -0
  9. package/dist/agents/stock-trader.md +81 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +9 -0
  12. package/dist/commands/index.d.ts +19 -0
  13. package/dist/commands/index.js +79 -0
  14. package/dist/commands/init.d.ts +15 -0
  15. package/dist/commands/init.js +283 -0
  16. package/dist/components/DangerConfirm.d.ts +17 -0
  17. package/dist/components/DangerConfirm.js +50 -0
  18. package/dist/components/MarkdownText.d.ts +12 -0
  19. package/dist/components/MarkdownText.js +166 -0
  20. package/dist/components/MessageItem.d.ts +8 -0
  21. package/dist/components/MessageItem.js +78 -0
  22. package/dist/components/MultilineInput.d.ts +34 -0
  23. package/dist/components/MultilineInput.js +437 -0
  24. package/dist/components/SlashCommandMenu.d.ts +16 -0
  25. package/dist/components/SlashCommandMenu.js +43 -0
  26. package/dist/components/StatusBar.d.ts +8 -0
  27. package/dist/components/StatusBar.js +26 -0
  28. package/dist/components/StreamingText.d.ts +6 -0
  29. package/dist/components/StreamingText.js +10 -0
  30. package/dist/components/WelcomeHeader.d.ts +6 -0
  31. package/dist/components/WelcomeHeader.js +25 -0
  32. package/dist/config/agentState.d.ts +16 -0
  33. package/dist/config/agentState.js +65 -0
  34. package/dist/config/constants.d.ts +25 -0
  35. package/dist/config/constants.js +67 -0
  36. package/dist/config/loader.d.ts +30 -0
  37. package/dist/config/loader.js +64 -0
  38. package/dist/config/systemInfo.d.ts +12 -0
  39. package/dist/config/systemInfo.js +95 -0
  40. package/dist/core/QueryEngine.d.ts +52 -0
  41. package/dist/core/QueryEngine.js +246 -0
  42. package/dist/core/hint.d.ts +14 -0
  43. package/dist/core/hint.js +279 -0
  44. package/dist/core/query.d.ts +24 -0
  45. package/dist/core/query.js +245 -0
  46. package/dist/core/safeguard.d.ts +96 -0
  47. package/dist/core/safeguard.js +236 -0
  48. package/dist/hooks/useFocus.d.ts +12 -0
  49. package/dist/hooks/useFocus.js +35 -0
  50. package/dist/hooks/useInputHistory.d.ts +14 -0
  51. package/dist/hooks/useInputHistory.js +102 -0
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.js +6 -0
  54. package/dist/screens/repl.d.ts +1 -0
  55. package/dist/screens/repl.js +842 -0
  56. package/dist/services/api/llm.d.ts +27 -0
  57. package/dist/services/api/llm.js +314 -0
  58. package/dist/services/api/mock.d.ts +9 -0
  59. package/dist/services/api/mock.js +102 -0
  60. package/dist/skills/index.d.ts +23 -0
  61. package/dist/skills/index.js +232 -0
  62. package/dist/skills/loader.d.ts +45 -0
  63. package/dist/skills/loader.js +108 -0
  64. package/dist/tools/createSkill.d.ts +8 -0
  65. package/dist/tools/createSkill.js +255 -0
  66. package/dist/tools/index.d.ts +16 -0
  67. package/dist/tools/index.js +23 -0
  68. package/dist/tools/listDirectory.d.ts +2 -0
  69. package/dist/tools/listDirectory.js +20 -0
  70. package/dist/tools/readFile.d.ts +2 -0
  71. package/dist/tools/readFile.js +17 -0
  72. package/dist/tools/runCommand.d.ts +2 -0
  73. package/dist/tools/runCommand.js +69 -0
  74. package/dist/tools/searchFiles.d.ts +2 -0
  75. package/dist/tools/searchFiles.js +45 -0
  76. package/dist/tools/writeFile.d.ts +2 -0
  77. package/dist/tools/writeFile.js +42 -0
  78. package/dist/types/index.d.ts +86 -0
  79. package/dist/types/index.js +2 -0
  80. package/package.json +55 -0
@@ -0,0 +1,842 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback, useRef, useEffect } from 'react';
3
+ import { Box, Text, useInput, useApp } from 'ink';
4
+ import Spinner from 'ink-spinner';
5
+ import MultilineInput from '../components/MultilineInput.js';
6
+ import WelcomeHeader from '../components/WelcomeHeader.js';
7
+ import MessageItem from '../components/MessageItem.js';
8
+ import StreamingText from '../components/StreamingText.js';
9
+ import StatusBar from '../components/StatusBar.js';
10
+ import SlashCommandMenu from '../components/SlashCommandMenu.js';
11
+ import DangerConfirm from '../components/DangerConfirm.js';
12
+ import { useWindowFocus } from '../hooks/useFocus.js';
13
+ import { useInputHistory } from '../hooks/useInputHistory.js';
14
+ import { QueryEngine } from '../core/QueryEngine.js';
15
+ import { HIDE_WELCOME_AFTER_INPUT, APP_VERSION } from '../config/constants.js';
16
+ import { generateAgentHint } from '../core/hint.js';
17
+ import { filterCommands, filterAgentCommands } from '../commands/index.js';
18
+ import { setActiveAgent } from '../config/agentState.js';
19
+ import { allTools } from '../tools/index.js';
20
+ import { listSkills } from '../skills/index.js';
21
+ import { getExternalSkillsDir } from '../skills/loader.js';
22
+ import { listPermanentAuthorizations, DANGER_RULES, } from '../core/safeguard.js';
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
+ }
71
+ export default function REPL() {
72
+ const { exit } = useApp();
73
+ const width = useTerminalWidth();
74
+ const windowFocused = useWindowFocus();
75
+ const { countdown, handleCtrlC } = useDoubleCtrlCExit(exit);
76
+ const { pushHistory, navigateUp, navigateDown, resetNavigation } = useInputHistory();
77
+ const [messages, setMessages] = useState([]);
78
+ const [input, setInput] = useState('');
79
+ 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
+ const [loopState, setLoopState] = useState(null);
91
+ const [showWelcome, setShowWelcome] = useState(true);
92
+ const sessionRef = useRef({
93
+ id: '', messages: [], createdAt: 0, updatedAt: 0, totalTokens: 0, totalCost: 0,
94
+ });
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
+ const engineRef = useRef(null);
111
+ // 用 ref 缓存 token 计数,定时器驱动 UI 刷新,避免每个 chunk 都触发 re-render
112
+ const tokenCountRef = useRef(0);
113
+ const tokenTimerRef = useRef(null);
114
+ const [displayTokens, setDisplayTokens] = useState(0);
115
+ useEffect(() => {
116
+ engineRef.current = new QueryEngine();
117
+ sessionRef.current = engineRef.current.getSession();
118
+ // 异步生成符合当前角色的输入提示
119
+ generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
120
+ console.error('[hint] 初始化提示失败:', err);
121
+ });
122
+ }, []);
123
+ // 启动 token 进度条定时刷新(每 100ms 同步一次 ref → state)
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
+ }, []);
196
+ const callbacks = {
197
+ onMessage: (msg) => {
198
+ setMessages((prev) => [...prev, msg]);
199
+ },
200
+ onUpdateMessage: (id, updates) => {
201
+ // thinking 消息的 content 更新走节流通道,避免高频 re-render
202
+ if (updates.content !== undefined && !updates.status && thinkingIdRef.current === null) {
203
+ // 首次 thinking 更新,记录 id 并启动节流
204
+ thinkingIdRef.current = id;
205
+ thinkingBufferRef.current = updates.content;
206
+ startThinkingTimer();
207
+ return;
208
+ }
209
+ if (thinkingIdRef.current === id && updates.content !== undefined && !updates.status) {
210
+ // 后续 thinking chunk,只更新 buffer
211
+ thinkingBufferRef.current = updates.content;
212
+ return;
213
+ }
214
+ // thinking 完成(带 status 更新)或其他消息:停止节流,直接更新
215
+ if (thinkingIdRef.current === id && updates.status) {
216
+ stopThinkingTimer();
217
+ thinkingIdRef.current = null;
218
+ thinkingBufferRef.current = '';
219
+ }
220
+ setMessages((prev) => prev.map((msg) => (msg.id === id ? { ...msg, ...updates } : msg)));
221
+ },
222
+ onStreamText: (text) => {
223
+ // 不直接 setState,而是积累到 buffer,由定时器批量刷新
224
+ streamBufferRef.current += text;
225
+ },
226
+ onClearStreamText: () => {
227
+ // 每轮迭代结束时清空流式文本,避免 streamText 被后续工具消息挤到底部
228
+ streamBufferRef.current = '';
229
+ setStreamText('');
230
+ },
231
+ onLoopStateChange: (state) => {
232
+ setLoopState(state);
233
+ setIsProcessing(state.isRunning);
234
+ if (state.isRunning) {
235
+ startTokenTimer();
236
+ startStreamTimer();
237
+ }
238
+ else {
239
+ stopTokenTimer();
240
+ stopStreamTimer();
241
+ stopThinkingTimer();
242
+ thinkingIdRef.current = null;
243
+ thinkingBufferRef.current = '';
244
+ setStreamText('');
245
+ streamBufferRef.current = '';
246
+ }
247
+ },
248
+ onSessionUpdate: (s) => {
249
+ // 会话对象本身不参与当前界面渲染,保留在 ref 中避免每个 chunk 触发整页重绘
250
+ sessionRef.current = s;
251
+ tokenCountRef.current = s.totalTokens;
252
+ },
253
+ onConfirmDangerousCommand: (command, reason, ruleName) => {
254
+ return new Promise((resolve) => {
255
+ setDangerConfirm({ command, reason, ruleName, resolve });
256
+ });
257
+ },
258
+ };
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
+ }, []);
425
+ const handleSubmit = useCallback(async (value) => {
426
+ const trimmed = value.trim();
427
+ if (!trimmed || isProcessing || !engineRef.current)
428
+ return;
429
+ // 斜杠命令拦截
430
+ if (trimmed.startsWith('/')) {
431
+ const parts = trimmed.slice(1).split(/\s+/);
432
+ const cmdName = parts[0].toLowerCase();
433
+ const hasArgs = parts.length > 1 && parts.slice(1).join('').length > 0;
434
+ // 内置命令:直接执行
435
+ if (['new', 'help', 'init', 'session_clear', 'permissions', 'skills', 'version'].includes(cmdName)) {
436
+ setInput('');
437
+ setSlashMenuVisible(false);
438
+ executeSlashCommand(cmdName);
439
+ return;
440
+ }
441
+ // /create_skill 命令:将需求转发给 LLM,由 LLM 调用 create_skill 工具完成
442
+ if (cmdName === 'create_skill') {
443
+ setInput('');
444
+ setSlashMenuVisible(false);
445
+ const skillArgs = parts.slice(1).join(' ').trim();
446
+ if (!skillArgs) {
447
+ const hintMsg = {
448
+ id: `create-skill-hint-${Date.now()}`,
449
+ type: 'system',
450
+ status: 'success',
451
+ content: '用法: /create_skill <描述你想创建的 skill>\n\n例如:\n /create_skill 一个可以查询天气的工具\n /create_skill 代码格式化工具,支持 Python 和 JS',
452
+ timestamp: Date.now(),
453
+ };
454
+ setMessages((prev) => [...prev, hintMsg]);
455
+ return;
456
+ }
457
+ // 构造提示,让 LLM 使用 create_skill 工具
458
+ const prompt = `请使用 create_skill 工具帮我创建一个新的 skill。需求如下:${skillArgs}\n\n请根据需求自动生成合适的 name、description、instruction,如果需要 Python 实现也请生成 skill.py 代码。`;
459
+ if (HIDE_WELCOME_AFTER_INPUT)
460
+ setShowWelcome(false);
461
+ pushHistory(trimmed);
462
+ setStreamText('');
463
+ await engineRef.current.handleQuery(prompt, callbacks);
464
+ return;
465
+ }
466
+ // /resume 命令处理
467
+ if (cmdName === 'resume') {
468
+ setInput('');
469
+ setSlashMenuVisible(false);
470
+ setResumeMenuMode(false);
471
+ if (hasArgs && engineRef.current) {
472
+ // /resume <id> 直接恢复
473
+ const sessionId = parts.slice(1).join(' ').trim();
474
+ const result = engineRef.current.loadSession(sessionId);
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
+ }
504
+ }
505
+ else {
506
+ // /resume 无参数:显示会话列表
507
+ const sessions = QueryEngine.listSessions().slice(0, 20);
508
+ if (sessions.length === 0) {
509
+ const noMsg = {
510
+ id: `resume-empty-${Date.now()}`,
511
+ type: 'system',
512
+ status: 'success',
513
+ content: '暂无历史会话',
514
+ timestamp: Date.now(),
515
+ };
516
+ setMessages((prev) => [...prev, noMsg]);
517
+ }
518
+ else {
519
+ const lines = sessions.map((s, i) => {
520
+ const date = new Date(s.updatedAt);
521
+ const dateStr = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
522
+ return ` ${i + 1}. [${dateStr}] ${s.summary}\n ID: ${s.id}`;
523
+ });
524
+ const listMsg = {
525
+ id: `resume-list-${Date.now()}`,
526
+ type: 'system',
527
+ status: 'success',
528
+ content: `历史会话(最近 ${sessions.length} 条):\n\n${lines.join('\n\n')}\n\n使用 /resume <ID> 恢复指定会话`,
529
+ timestamp: Date.now(),
530
+ };
531
+ setMessages((prev) => [...prev, listMsg]);
532
+ }
533
+ }
534
+ return;
535
+ }
536
+ // /permissions 命令:查看持久化授权列表
537
+ if (cmdName === 'permissions') {
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]);
568
+ return;
569
+ }
570
+ // 工具命令带参数:去掉 / 前缀,作为消息发给 LLM
571
+ if (hasArgs) {
572
+ // 继续走正常提交流程
573
+ }
574
+ else {
575
+ // 无参数的 / 命令,忽略
576
+ return;
577
+ }
578
+ }
579
+ if (HIDE_WELCOME_AFTER_INPUT)
580
+ setShowWelcome(false);
581
+ pushHistory(trimmed);
582
+ setInput('');
583
+ setStreamText('');
584
+ await engineRef.current.handleQuery(trimmed, callbacks);
585
+ }, [isProcessing, pushHistory, executeSlashCommand]);
586
+ const handleUpArrow = useCallback(() => {
587
+ const result = navigateUp(input);
588
+ if (result !== null)
589
+ setInput(result);
590
+ }, [navigateUp, input]);
591
+ const handleDownArrow = useCallback(() => {
592
+ const result = navigateDown();
593
+ if (result !== null)
594
+ setInput(result);
595
+ }, [navigateDown]);
596
+ const handleInputChange = useCallback((val) => {
597
+ resetNavigation();
598
+ setInput(val);
599
+ // 检测斜杠命令:输入以 / 开头且为单行
600
+ if (val.startsWith('/') && !val.includes('\n')) {
601
+ const query = val.slice(1); // 去掉 /
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 推荐问题
778
+ const handleTabFillPlaceholder = useCallback(() => {
779
+ if (placeholder) {
780
+ // placeholder 格式为 Try "xxx...",提取引号内的内容
781
+ const match = placeholder.match(/^Try\s+"(.+)"$/);
782
+ const text = match ? match[1] : placeholder;
783
+ setInput(text);
784
+ }
785
+ }, [placeholder]);
786
+ // 快捷键
787
+ useInput((ch, key) => {
788
+ if (key.tab && slashMenuVisible) {
789
+ handleSlashMenuSelect();
790
+ return;
791
+ }
792
+ if (key.ctrl && ch === 'c') {
793
+ handleCtrlC();
794
+ return;
795
+ }
796
+ if (key.ctrl && ch === 'o') {
797
+ setShowDetails((prev) => !prev);
798
+ return;
799
+ }
800
+ if (key.ctrl && ch === 'l') {
801
+ if (engineRef.current) {
802
+ engineRef.current.reset();
803
+ sessionRef.current = engineRef.current.getSession();
804
+ }
805
+ setMessages([]);
806
+ setStreamText('');
807
+ streamBufferRef.current = '';
808
+ setLoopState(null);
809
+ setIsProcessing(false);
810
+ setShowWelcome(true);
811
+ tokenCountRef.current = 0;
812
+ setDisplayTokens(0);
813
+ // 重新生成角色提示
814
+ generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
815
+ console.error('[hint] 重新生成提示失败:', err);
816
+ });
817
+ return;
818
+ }
819
+ if (key.escape) {
820
+ if (isProcessing && engineRef.current) {
821
+ // 推理中按 ESC:中断推理,流式文本停止后由 query 层标记最后一条消息为 aborted
822
+ engineRef.current.abort();
823
+ }
824
+ else if (input.length > 0) {
825
+ // 输入框有内容:双击 ESC(500ms 内)清空
826
+ const now = Date.now();
827
+ if (now - lastEscRef.current < 500) {
828
+ setInput('');
829
+ lastEscRef.current = 0;
830
+ }
831
+ else {
832
+ lastEscRef.current = now;
833
+ }
834
+ }
835
+ return;
836
+ }
837
+ });
838
+ 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
+ dangerConfirm.resolve(choice);
840
+ 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 })] })] }));
842
+ }