@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.
Files changed (59) hide show
  1. package/LICENSE +1 -1
  2. package/dist/cli.js +2 -2
  3. package/dist/commands/index.js +2 -2
  4. package/dist/commands/init.js +1 -1
  5. package/dist/components/MessageItem.d.ts +1 -1
  6. package/dist/components/MessageItem.js +10 -2
  7. package/dist/components/MultilineInput.d.ts +7 -1
  8. package/dist/components/MultilineInput.js +148 -4
  9. package/dist/components/SlashCommandMenu.d.ts +1 -1
  10. package/dist/components/StatusBar.js +1 -1
  11. package/dist/components/StreamingText.js +1 -1
  12. package/dist/components/WelcomeHeader.js +1 -1
  13. package/dist/config/constants.js +3 -3
  14. package/dist/config/loader.d.ts +2 -0
  15. package/dist/core/QueryEngine.d.ts +4 -4
  16. package/dist/core/QueryEngine.js +19 -17
  17. package/dist/core/WorkerBridge.d.ts +9 -0
  18. package/dist/core/WorkerBridge.js +109 -0
  19. package/dist/core/hint.js +4 -4
  20. package/dist/core/query.d.ts +8 -1
  21. package/dist/core/query.js +279 -57
  22. package/dist/core/queryWorker.d.ts +44 -0
  23. package/dist/core/queryWorker.js +66 -0
  24. package/dist/core/safeguard.js +1 -1
  25. package/dist/hooks/useDoubleCtrlCExit.d.ts +5 -0
  26. package/dist/hooks/useDoubleCtrlCExit.js +34 -0
  27. package/dist/hooks/useInputHistory.js +35 -3
  28. package/dist/hooks/useSlashMenu.d.ts +36 -0
  29. package/dist/hooks/useSlashMenu.js +216 -0
  30. package/dist/hooks/useStreamThrottle.d.ts +20 -0
  31. package/dist/hooks/useStreamThrottle.js +120 -0
  32. package/dist/hooks/useTerminalWidth.d.ts +2 -0
  33. package/dist/hooks/useTerminalWidth.js +13 -0
  34. package/dist/hooks/useTokenDisplay.d.ts +13 -0
  35. package/dist/hooks/useTokenDisplay.js +45 -0
  36. package/dist/index.js +1 -1
  37. package/dist/screens/repl.js +164 -636
  38. package/dist/screens/slashCommands.d.ts +7 -0
  39. package/dist/screens/slashCommands.js +134 -0
  40. package/dist/services/api/llm.d.ts +4 -2
  41. package/dist/services/api/llm.js +70 -16
  42. package/dist/services/api/mock.d.ts +1 -1
  43. package/dist/skills/index.d.ts +2 -2
  44. package/dist/skills/index.js +3 -3
  45. package/dist/tools/createSkill.d.ts +1 -1
  46. package/dist/tools/createSkill.js +3 -3
  47. package/dist/tools/index.d.ts +9 -8
  48. package/dist/tools/index.js +10 -9
  49. package/dist/tools/listDirectory.d.ts +1 -1
  50. package/dist/tools/readFile.d.ts +1 -1
  51. package/dist/tools/runCommand.d.ts +1 -1
  52. package/dist/tools/runCommand.js +38 -7
  53. package/dist/tools/searchFiles.d.ts +1 -1
  54. package/dist/tools/semanticSearch.d.ts +9 -0
  55. package/dist/tools/semanticSearch.js +159 -0
  56. package/dist/tools/writeFile.d.ts +1 -1
  57. package/dist/tools/writeFile.js +125 -25
  58. package/dist/types/index.d.ts +10 -1
  59. package/package.json +1 -1
@@ -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.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
- }
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
- // ref 缓存 token 计数,定时器驱动 UI 刷新,避免每个 chunk 都触发 re-render
112
- const tokenCountRef = useRef(0);
113
- const tokenTimerRef = useRef(null);
114
- const [displayTokens, setDisplayTokens] = useState(0);
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
- // 启动 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
- }, []);
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 消息的 content 更新走节流通道,避免高频 re-render
67
+ // thinking 消息流式内容更新(无 status 字段)
202
68
  if (updates.content !== undefined && !updates.status && thinkingIdRef.current === null) {
203
- // 首次 thinking 更新,记录 id 并启动节流
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
- // 后续 thinking chunk,只更新 buffer
211
- thinkingBufferRef.current = updates.content;
73
+ handleThinkingUpdate(id, updates.content);
212
74
  return;
213
75
  }
214
- // thinking 完成(带 status 更新)或其他消息:停止节流,直接更新
76
+ // thinking 消息完成(带 status
215
77
  if (thinkingIdRef.current === id && updates.status) {
216
- stopThinkingTimer();
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
- // 不直接 setState,而是积累到 buffer,由定时器批量刷新
224
- streamBufferRef.current += text;
83
+ appendStreamChunk(text);
225
84
  },
226
85
  onClearStreamText: () => {
227
- // 每轮迭代结束时清空流式文本,避免 streamText 被后续工具消息挤到底部
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
- stopStreamTimer();
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
- tokenCountRef.current = s.totalTokens;
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
- executeSlashCommand(cmdName);
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 命令:将需求转发给 LLM,由 LLM 调用 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
- setStreamText('');
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
- 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
- }
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
- // /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]);
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
- setStreamText('');
229
+ clearStream();
584
230
  await engineRef.current.handleQuery(trimmed, callbacks);
585
- }, [isProcessing, pushHistory, executeSlashCommand]);
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
- 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 推荐问题
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
- if (key.tab && slashMenuVisible) {
789
- handleSlashMenuSelect();
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
- setStreamText('');
807
- streamBufferRef.current = '';
338
+ clearStream();
808
339
  setLoopState(null);
809
340
  setIsProcessing(false);
810
341
  setShowWelcome(true);
811
- tokenCountRef.current = 0;
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
  }