@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/LICENSE
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { APP_VERSION } from './config/constants
|
|
3
|
-
import { startJarvis } from './index
|
|
2
|
+
import { APP_VERSION } from './config/constants';
|
|
3
|
+
import { startJarvis } from './index';
|
|
4
4
|
const arg = process.argv[2];
|
|
5
5
|
if (arg === '--version' || arg === '-v' || arg === 'version') {
|
|
6
6
|
console.log(APP_VERSION);
|
package/dist/commands/index.js
CHANGED
|
@@ -26,8 +26,8 @@ const toolCommands = [
|
|
|
26
26
|
{ name: 'create_skill', description: '创建新的 Skill 到 ~/.jarvis/skills/', category: 'builtin' },
|
|
27
27
|
];
|
|
28
28
|
/** 智能体子命令:从 agents 目录动态加载(二级菜单) */
|
|
29
|
-
import { loadAllAgents } from '../agents/index
|
|
30
|
-
import { listSkills } from '../skills/index
|
|
29
|
+
import { loadAllAgents } from '../agents/index';
|
|
30
|
+
import { listSkills } from '../skills/index';
|
|
31
31
|
let _agentSubCommands = null;
|
|
32
32
|
export function getAgentSubCommands() {
|
|
33
33
|
if (_agentSubCommands)
|
package/dist/commands/init.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { execSync } from 'child_process';
|
|
10
|
-
import { APP_NAME, APP_VERSION } from '../config/constants
|
|
10
|
+
import { APP_NAME, APP_VERSION } from '../config/constants';
|
|
11
11
|
// ===== 辅助函数 =====
|
|
12
12
|
/** 安全执行命令 */
|
|
13
13
|
function safeExec(cmd) {
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import Spinner from 'ink-spinner';
|
|
5
|
-
import MarkdownText from './MarkdownText
|
|
5
|
+
import MarkdownText from './MarkdownText';
|
|
6
6
|
// 状态圆点 icon,根据消息类型 + 状态决定颜色
|
|
7
7
|
// reasoning 完成 → 白色 / tool_exec 成功 → 绿色 / error → 红色 / aborted → 黄色 / pending → 黄色
|
|
8
8
|
function statusDot(status, type) {
|
|
@@ -58,8 +58,16 @@ function MessageItem({ msg, showDetails = false }) {
|
|
|
58
58
|
// Bash 工具直接显示命令内容
|
|
59
59
|
const isBash = msg.toolName === 'Bash';
|
|
60
60
|
const bashCmd = isBash && msg.toolArgs?.command ? String(msg.toolArgs.command) : '';
|
|
61
|
+
// Skill 工具:skill_xxx → 名称(参数摘要) 格式
|
|
62
|
+
const isSkill = msg.toolName?.startsWith('skill_');
|
|
63
|
+
const skillName = isSkill ? msg.toolName.replace(/^skill_/, '') : '';
|
|
64
|
+
const skillArgsSummary = isSkill && msg.toolArgs
|
|
65
|
+
? Object.values(msg.toolArgs).map((v) => String(v)).filter(Boolean).join(', ')
|
|
66
|
+
: '';
|
|
61
67
|
const toolLabel = isBash && bashCmd ? `Bash(${bashCmd})` : (msg.toolName || 'tool');
|
|
62
|
-
|
|
68
|
+
// 并行组标识
|
|
69
|
+
const isParallel = !!msg.parallelGroupId;
|
|
70
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [isParallel && _jsx(Text, { color: "cyan", dimColor: true, children: "\u21C9 " }), _jsxs(Text, { color: dotColor, children: [dot, " "] }), isBash && bashCmd ? (_jsxs(Text, { children: [_jsx(Text, { color: "white", bold: true, children: "Bash" }), _jsxs(Text, { color: "gray", children: ["(", bashCmd, ")"] })] })) : isSkill ? (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: skillName }), _jsxs(Text, { color: "gray", children: ["(", skillArgsSummary, ")"] })] })) : (_jsx(Text, { color: "magenta", bold: true, children: toolLabel }))] }), showDetails && msg.toolArgs && !isBash && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "gray", dimColor: true, wrap: "wrap", children: JSON.stringify(msg.toolArgs) }) })), showDetails && msg.toolResult && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "gray", wrap: "wrap", children: msg.toolResult.length > 300 ? msg.toolResult.slice(0, 300) + '…' : msg.toolResult }) })), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
63
71
|
}
|
|
64
72
|
if (msg.type === 'error') {
|
|
65
73
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: "red", children: [_jsx(Text, { color: dotColor, children: dot }), " ", msg.content] }), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
@@ -19,6 +19,12 @@ interface MultilineInputProps {
|
|
|
19
19
|
onSlashMenuClose?: () => void;
|
|
20
20
|
/** 输入为空时按 Tab 的回调(用于填入 placeholder) */
|
|
21
21
|
onTabFillPlaceholder?: () => void;
|
|
22
|
+
/**
|
|
23
|
+
* 输入框下方的行数(分隔线 + StatusBar 等),用于将终端物理光标
|
|
24
|
+
* 上移到输入框行,使 IME composing 显示在正确位置而非状态栏上。
|
|
25
|
+
* 默认值 2(底部分隔线 1 行 + StatusBar 1 行)。
|
|
26
|
+
*/
|
|
27
|
+
rowsBelow?: number;
|
|
22
28
|
}
|
|
23
29
|
/**
|
|
24
30
|
* 多行文本输入组件,支持光标移动和粘贴折叠。
|
|
@@ -30,5 +36,5 @@ interface MultilineInputProps {
|
|
|
30
36
|
* - Backspace: 删除光标前一个字符(占位符整体删除)
|
|
31
37
|
* - 粘贴多行内容: 折叠为 [Pasted text #N +X lines] 占位符
|
|
32
38
|
*/
|
|
33
|
-
export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, onDownArrow, placeholder, isActive, showCursor, slashMenuActive, onSlashMenuUp, onSlashMenuDown, onSlashMenuSelect, onSlashMenuClose, onTabFillPlaceholder, }: MultilineInputProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, onDownArrow, placeholder, isActive, showCursor, slashMenuActive, onSlashMenuUp, onSlashMenuDown, onSlashMenuSelect, onSlashMenuClose, onTabFillPlaceholder, rowsBelow, }: MultilineInputProps): import("react/jsx-runtime").JSX.Element;
|
|
34
40
|
export {};
|
|
@@ -5,6 +5,16 @@ import { Box, Text, useInput, useStdin } from 'ink';
|
|
|
5
5
|
const PASTE_PLACEHOLDER_RE = /\[Pasted text #(\d+) \+(\d+) lines\]/g;
|
|
6
6
|
/** 粘贴检测的时间窗口(ms),在此窗口内连续到达的数据视为一次粘贴 */
|
|
7
7
|
const PASTE_DETECT_WINDOW_MS = 8;
|
|
8
|
+
/**
|
|
9
|
+
* IME composing 检测时间窗口(ms)。
|
|
10
|
+
* 在此窗口内连续到达的可打印字符视为 IME composing 输入,
|
|
11
|
+
* 延迟渲染以避免拼音字母逐个触发 re-render 导致 TUI 偏移。
|
|
12
|
+
*/
|
|
13
|
+
const IME_COMPOSE_WINDOW_MS = 80;
|
|
14
|
+
/** 判断字符串是否全部为 ASCII 小写字母(拼音输入的典型特征) */
|
|
15
|
+
const isAsciiLowerAlpha = (s) => /^[a-z]+$/.test(s);
|
|
16
|
+
/** 判断字符串是否包含非 ASCII 字符(中文、日文等 CJK 字符) */
|
|
17
|
+
const hasNonAscii = (s) => /[^\x00-\x7F]/.test(s);
|
|
8
18
|
/**
|
|
9
19
|
* 多行文本输入组件,支持光标移动和粘贴折叠。
|
|
10
20
|
*
|
|
@@ -15,7 +25,7 @@ const PASTE_DETECT_WINDOW_MS = 8;
|
|
|
15
25
|
* - Backspace: 删除光标前一个字符(占位符整体删除)
|
|
16
26
|
* - 粘贴多行内容: 折叠为 [Pasted text #N +X lines] 占位符
|
|
17
27
|
*/
|
|
18
|
-
export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, onDownArrow, placeholder = '', isActive = true, showCursor = true, slashMenuActive = false, onSlashMenuUp, onSlashMenuDown, onSlashMenuSelect, onSlashMenuClose, onTabFillPlaceholder, }) {
|
|
28
|
+
export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, onDownArrow, placeholder = '', isActive = true, showCursor = true, slashMenuActive = false, onSlashMenuUp, onSlashMenuDown, onSlashMenuSelect, onSlashMenuClose, onTabFillPlaceholder, rowsBelow = 2, }) {
|
|
19
29
|
const { stdin } = useStdin();
|
|
20
30
|
const [cursor, setCursor] = useState(value.length);
|
|
21
31
|
// 粘贴内容存储
|
|
@@ -26,10 +36,23 @@ export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, o
|
|
|
26
36
|
// 时间窗口粘贴检测:收集短时间内连续到达的数据块
|
|
27
37
|
const batchBufferRef = useRef('');
|
|
28
38
|
const batchTimerRef = useRef(null);
|
|
29
|
-
//
|
|
39
|
+
// IME composing 缓冲:积累拼音字母,等 composing 结束后再一次性更新
|
|
40
|
+
// imeBufferRef: 当前正在 composing 的拼音字母
|
|
41
|
+
// imeTimerRef: composing 超时定时器,超时后将拼音作为普通文本提交
|
|
42
|
+
// imeInsertedLen: 已经临时插入到 value 中的 composing 文本长度(用于替换)
|
|
43
|
+
const imeBufferRef = useRef('');
|
|
44
|
+
const imeTimerRef = useRef(null);
|
|
45
|
+
const imeInsertedLenRef = useRef(0);
|
|
46
|
+
// 终端光标重定位定时器(用于 IME composing 位置修正)
|
|
47
|
+
const cursorRelocTimerRef = useRef(null);
|
|
48
|
+
// 启用 bracketed paste mode,并在激活时清空 stdin 缓冲区
|
|
30
49
|
useEffect(() => {
|
|
31
50
|
if (!stdin || !isActive)
|
|
32
51
|
return;
|
|
52
|
+
// 清空 stdin 缓冲区,丢弃 processing 期间积累的输入
|
|
53
|
+
if (typeof stdin.read === 'function') {
|
|
54
|
+
while (stdin.read() !== null) { /* drain */ }
|
|
55
|
+
}
|
|
33
56
|
process.stdout.write('\x1B[?2004h');
|
|
34
57
|
return () => {
|
|
35
58
|
process.stdout.write('\x1B[?2004l');
|
|
@@ -273,12 +296,72 @@ export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, o
|
|
|
273
296
|
// 多行 → 粘贴
|
|
274
297
|
if (cleaned.includes('\n') && cleaned.length > 1) {
|
|
275
298
|
insertPaste(cleaned);
|
|
299
|
+
return;
|
|
276
300
|
}
|
|
277
|
-
|
|
278
|
-
|
|
301
|
+
// 检测是否为 IME composing 输入(连续小写字母 = 拼音)
|
|
302
|
+
if (isAsciiLowerAlpha(buf)) {
|
|
303
|
+
// 可能是拼音 composing,进入 IME 缓冲模式
|
|
304
|
+
imeBufferRef.current += buf;
|
|
305
|
+
// 不更新 value/不触发 re-render,只重置 IME 超时
|
|
306
|
+
if (imeTimerRef.current !== null) {
|
|
307
|
+
clearTimeout(imeTimerRef.current);
|
|
308
|
+
}
|
|
309
|
+
imeTimerRef.current = setTimeout(flushImeBuffer, IME_COMPOSE_WINDOW_MS);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// 收到非 ASCII 字符(中文等):如果有 IME 缓冲,说明 composing 结束
|
|
313
|
+
if (hasNonAscii(buf) && imeBufferRef.current.length > 0) {
|
|
314
|
+
// 丢弃之前积累的拼音字母(它们只是 composing 中间态),
|
|
315
|
+
// 回退已临时插入的 composing 文本
|
|
316
|
+
if (imeTimerRef.current !== null) {
|
|
317
|
+
clearTimeout(imeTimerRef.current);
|
|
318
|
+
imeTimerRef.current = null;
|
|
319
|
+
}
|
|
320
|
+
const insertedLen = imeInsertedLenRef.current;
|
|
321
|
+
imeBufferRef.current = '';
|
|
322
|
+
imeInsertedLenRef.current = 0;
|
|
323
|
+
if (insertedLen > 0) {
|
|
324
|
+
// 回退之前临时插入的拼音
|
|
325
|
+
const v = valueRef.current;
|
|
326
|
+
const c = cursorRef.current;
|
|
327
|
+
const newVal = v.slice(0, c - insertedLen) + v.slice(c);
|
|
328
|
+
emitChange(newVal);
|
|
329
|
+
setCursor(c - insertedLen);
|
|
330
|
+
}
|
|
331
|
+
// 插入最终的中文字符
|
|
279
332
|
handleNormalInput(buf);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// 收到非拼音的 ASCII 可打印字符,且有 IME 缓冲:先提交 IME 缓冲
|
|
336
|
+
if (imeBufferRef.current.length > 0) {
|
|
337
|
+
commitImeBuffer();
|
|
338
|
+
}
|
|
339
|
+
// 单字符或单行短文本,按普通输入处理
|
|
340
|
+
handleNormalInput(buf);
|
|
341
|
+
};
|
|
342
|
+
/** 提交 IME 缓冲区中的拼音为普通文本(composing 超时或被打断时调用) */
|
|
343
|
+
const commitImeBuffer = () => {
|
|
344
|
+
if (imeTimerRef.current !== null) {
|
|
345
|
+
clearTimeout(imeTimerRef.current);
|
|
346
|
+
imeTimerRef.current = null;
|
|
347
|
+
}
|
|
348
|
+
const buf = imeBufferRef.current;
|
|
349
|
+
const insertedLen = imeInsertedLenRef.current;
|
|
350
|
+
imeBufferRef.current = '';
|
|
351
|
+
imeInsertedLenRef.current = 0;
|
|
352
|
+
if (!buf)
|
|
353
|
+
return;
|
|
354
|
+
// 需要插入的是 buf 中尚未被临时插入的部分
|
|
355
|
+
const remaining = buf.slice(insertedLen);
|
|
356
|
+
if (remaining.length > 0) {
|
|
357
|
+
handleNormalInput(remaining);
|
|
280
358
|
}
|
|
281
359
|
};
|
|
360
|
+
/** IME composing 超时:将积累的拼音作为普通文本提交 */
|
|
361
|
+
const flushImeBuffer = () => {
|
|
362
|
+
imeTimerRef.current = null;
|
|
363
|
+
commitImeBuffer();
|
|
364
|
+
};
|
|
282
365
|
// Alt+Enter 组合键检测:ESC 可能单独到达,需要等待后续字符
|
|
283
366
|
const escTimerRef = useRef(null);
|
|
284
367
|
const ESC_WAIT_MS = 50; // 等待后续字符的时间窗口
|
|
@@ -350,6 +433,10 @@ export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, o
|
|
|
350
433
|
}
|
|
351
434
|
if (isSingleControl && batchBufferRef.current === '') {
|
|
352
435
|
// 没有正在积累的缓冲,直接处理控制字符
|
|
436
|
+
// 如果有 IME 缓冲,先提交
|
|
437
|
+
if (imeBufferRef.current.length > 0) {
|
|
438
|
+
commitImeBuffer();
|
|
439
|
+
}
|
|
353
440
|
handleNormalInput(raw);
|
|
354
441
|
return;
|
|
355
442
|
}
|
|
@@ -376,6 +463,10 @@ export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, o
|
|
|
376
463
|
clearTimeout(escTimerRef.current);
|
|
377
464
|
escTimerRef.current = null;
|
|
378
465
|
}
|
|
466
|
+
if (imeTimerRef.current !== null) {
|
|
467
|
+
clearTimeout(imeTimerRef.current);
|
|
468
|
+
imeTimerRef.current = null;
|
|
469
|
+
}
|
|
379
470
|
};
|
|
380
471
|
}, [stdin, isActive]);
|
|
381
472
|
// 组件卸载时清理
|
|
@@ -387,9 +478,61 @@ export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, o
|
|
|
387
478
|
if (escTimerRef.current !== null) {
|
|
388
479
|
clearTimeout(escTimerRef.current);
|
|
389
480
|
}
|
|
481
|
+
if (imeTimerRef.current !== null) {
|
|
482
|
+
clearTimeout(imeTimerRef.current);
|
|
483
|
+
}
|
|
484
|
+
if (cursorRelocTimerRef.current !== null) {
|
|
485
|
+
clearTimeout(cursorRelocTimerRef.current);
|
|
486
|
+
}
|
|
390
487
|
};
|
|
391
488
|
}, []);
|
|
392
489
|
useInput(() => { }, { isActive });
|
|
490
|
+
/**
|
|
491
|
+
* 将终端物理光标固定到当前行第 3 列,避免 IME composing 从行末溢出。
|
|
492
|
+
*
|
|
493
|
+
* 背景:ink 渲染完成后,终端光标停留在最后一行(StatusBar)末尾。
|
|
494
|
+
* macOS 终端模拟器的内联 IME 会在物理光标位置显示 composing 拼音,
|
|
495
|
+
* 若光标在行末会导致行宽溢出、TUI 布局被挤压偏移。
|
|
496
|
+
*
|
|
497
|
+
* 注意:只能移列,绝对不能移行(\x1B[1A 等)。
|
|
498
|
+
* ink 增量渲染以当前光标行为起点,移行后每次 re-render 都会导致
|
|
499
|
+
* TUI 整体上偏移一行(每次输入字符都触发 re-render)。
|
|
500
|
+
*
|
|
501
|
+
* 解决方案:仅将列定位到 3(❯ 占 2 列,输入框第一个字符从第 3 列开始),
|
|
502
|
+
* IME composing 文本从该列开始,不会从行末溢出。
|
|
503
|
+
*/
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
// 取消之前的定时器
|
|
506
|
+
if (cursorRelocTimerRef.current !== null) {
|
|
507
|
+
clearTimeout(cursorRelocTimerRef.current);
|
|
508
|
+
}
|
|
509
|
+
if (!showCursor || !isActive) {
|
|
510
|
+
// 非输入状态:将物理光标移到列 1(行首),避免 IME 候选框在可见区域弹出
|
|
511
|
+
cursorRelocTimerRef.current = setTimeout(() => {
|
|
512
|
+
cursorRelocTimerRef.current = null;
|
|
513
|
+
process.stdout.write('\x1B[1G');
|
|
514
|
+
}, 80);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
// 输入激活状态:
|
|
518
|
+
// 1. 上移 rowsBelow 行,到达输入框所在行
|
|
519
|
+
// 2. 将列定位到 3(❯ 占 2 列,输入框第一个字符从第 3 列开始)
|
|
520
|
+
// 3. 下移 rowsBelow 行,回到原来的光标行(ink 渲染起点不变)
|
|
521
|
+
// 这样 IME composing 显示在输入框行而非 StatusBar 行
|
|
522
|
+
cursorRelocTimerRef.current = setTimeout(() => {
|
|
523
|
+
cursorRelocTimerRef.current = null;
|
|
524
|
+
const up = rowsBelow > 0 ? `\x1B[${rowsBelow}A` : '';
|
|
525
|
+
const down = rowsBelow > 0 ? `\x1B[${rowsBelow}B` : '';
|
|
526
|
+
process.stdout.write(`${up}\x1B[3G${down}`);
|
|
527
|
+
}, 80);
|
|
528
|
+
}
|
|
529
|
+
return () => {
|
|
530
|
+
if (cursorRelocTimerRef.current !== null) {
|
|
531
|
+
clearTimeout(cursorRelocTimerRef.current);
|
|
532
|
+
cursorRelocTimerRef.current = null;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
});
|
|
393
536
|
// --- 渲染 ---
|
|
394
537
|
const isEmpty = value.length === 0;
|
|
395
538
|
if (isEmpty) {
|
|
@@ -398,6 +541,7 @@ export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, o
|
|
|
398
541
|
pasteCountRef.current = 0;
|
|
399
542
|
pastedChunksRef.current.clear();
|
|
400
543
|
}
|
|
544
|
+
// 更新光标位置信息(空输入时光标在起始位置)
|
|
401
545
|
if (showCursor && placeholder.length > 0) {
|
|
402
546
|
return (_jsxs(Box, { children: [_jsx(Text, { inverse: true, color: "white", children: placeholder[0] }), _jsx(Text, { color: "gray", children: placeholder.slice(1) }), _jsx(Text, { color: "gray", dimColor: true, children: " [Tab]" })] }));
|
|
403
547
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import { MODEL_NAME, PROJECT_NAME, ENABLE_THINKING_MODE_TOGGLE, CONTEXT_TOKEN_LIMIT } from '../config/constants
|
|
4
|
+
import { MODEL_NAME, PROJECT_NAME, ENABLE_THINKING_MODE_TOGGLE, CONTEXT_TOKEN_LIMIT } from '../config/constants';
|
|
5
5
|
/** 生成 token 用量进度条 */
|
|
6
6
|
function tokenProgressBar(used, limit, barWidth) {
|
|
7
7
|
const ratio = Math.min(used / limit, 1);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import MarkdownText from './MarkdownText
|
|
4
|
+
import MarkdownText from './MarkdownText';
|
|
5
5
|
function StreamingText({ text }) {
|
|
6
6
|
if (!text)
|
|
7
7
|
return null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import { APP_NAME, APP_VERSION, MODEL_NAME } from '../config/constants
|
|
4
|
+
import { APP_NAME, APP_VERSION, MODEL_NAME } from '../config/constants';
|
|
5
5
|
function truncatePath(p, max) {
|
|
6
6
|
if (p.length <= max)
|
|
7
7
|
return p;
|
package/dist/config/constants.js
CHANGED
|
@@ -24,7 +24,7 @@ export const SESSIONS_DIR = path.join(os.homedir(), '.jarvis', 'sessions');
|
|
|
24
24
|
/** 输入后是否隐藏 WelcomeHeader,默认 false(不隐藏) */
|
|
25
25
|
export const HIDE_WELCOME_AFTER_INPUT = false;
|
|
26
26
|
/** 从配置文件获取当前模型名称 */
|
|
27
|
-
import { loadConfig, getActiveModel } from './loader
|
|
27
|
+
import { loadConfig, getActiveModel } from './loader';
|
|
28
28
|
const _cfg = loadConfig();
|
|
29
29
|
function resolveModelName() {
|
|
30
30
|
try {
|
|
@@ -50,8 +50,8 @@ export const DEFAULT_AGENT_COLOR = 'green';
|
|
|
50
50
|
/** 智能体默认标识符 */
|
|
51
51
|
export const DEFAULT_AGENT_EMOJI = '>';
|
|
52
52
|
// ===== 动态应用名称(跟随激活智能体) =====
|
|
53
|
-
import { getAgent } from '../agents/index
|
|
54
|
-
import { getActiveAgent } from './agentState
|
|
53
|
+
import { getAgent } from '../agents/index';
|
|
54
|
+
import { getActiveAgent } from './agentState';
|
|
55
55
|
/** 当前激活的智能体名称 — 启动时从 ~/.jarvis/agent.json 读取,运行时可切换 */
|
|
56
56
|
export const DEFAULT_AGENT = getActiveAgent(DEFAULT_AGENT_FALLBACK);
|
|
57
57
|
function resolveAppName() {
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface ModelConfig {
|
|
|
11
11
|
model: string;
|
|
12
12
|
temperature?: number;
|
|
13
13
|
max_tokens?: number;
|
|
14
|
+
/** 额外请求体参数,会直接合并到 API 请求 body 中(如 enable_thinking、chat_template_kwargs 等) */
|
|
15
|
+
extra_body?: Record<string, unknown>;
|
|
14
16
|
}
|
|
15
17
|
export interface SystemConfig {
|
|
16
18
|
context_token_limit?: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Message, Session, LoopState } from '../types/index.js';
|
|
2
|
-
import { DangerConfirmResult } from './query
|
|
2
|
+
import { DangerConfirmResult } from './query';
|
|
3
3
|
export interface EngineCallbacks {
|
|
4
4
|
onMessage: (msg: Message) => void;
|
|
5
5
|
onUpdateMessage: (id: string, updates: Partial<Message>) => void;
|
|
@@ -15,13 +15,13 @@ export declare class QueryEngine {
|
|
|
15
15
|
private service;
|
|
16
16
|
private session;
|
|
17
17
|
private transcript;
|
|
18
|
-
private
|
|
18
|
+
private workerBridge;
|
|
19
19
|
constructor();
|
|
20
20
|
private createSession;
|
|
21
21
|
private ensureSessionDir;
|
|
22
|
-
/**
|
|
22
|
+
/** 处理用户输入(在独立 Worker 线程中执行) */
|
|
23
23
|
handleQuery(userInput: string, callbacks: EngineCallbacks): Promise<void>;
|
|
24
|
-
/**
|
|
24
|
+
/** 终止当前任务(通知 Worker 中断) */
|
|
25
25
|
abort(): void;
|
|
26
26
|
/** 重置会话 */
|
|
27
27
|
reset(): void;
|
package/dist/core/QueryEngine.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { v4 as uuid } from 'uuid';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { clearAuthorizations } from './safeguard.js';
|
|
4
|
+
import { WorkerBridge } from './WorkerBridge';
|
|
5
|
+
import { MockService } from '../services/api/mock';
|
|
6
|
+
import { LLMServiceImpl } from '../services/api/llm';
|
|
7
|
+
import { loadConfig, getActiveModel } from '../config/loader';
|
|
8
|
+
import { SESSIONS_DIR } from '../config/constants';
|
|
9
|
+
import { setActiveAgent } from '../config/agentState';
|
|
10
|
+
import { clearAuthorizations } from './safeguard';
|
|
12
11
|
export class QueryEngine {
|
|
13
12
|
service;
|
|
14
13
|
session;
|
|
15
14
|
transcript = [];
|
|
16
|
-
|
|
15
|
+
workerBridge = new WorkerBridge();
|
|
17
16
|
constructor() {
|
|
18
17
|
// 尝试从配置文件加载 LLM,失败则回退 MockService
|
|
19
18
|
const config = loadConfig();
|
|
@@ -47,9 +46,8 @@ export class QueryEngine {
|
|
|
47
46
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
48
47
|
}
|
|
49
48
|
}
|
|
50
|
-
/**
|
|
49
|
+
/** 处理用户输入(在独立 Worker 线程中执行) */
|
|
51
50
|
async handleQuery(userInput, callbacks) {
|
|
52
|
-
this.abortSignal = { aborted: false };
|
|
53
51
|
const userMsg = {
|
|
54
52
|
id: uuid(),
|
|
55
53
|
type: 'user',
|
|
@@ -59,24 +57,25 @@ export class QueryEngine {
|
|
|
59
57
|
};
|
|
60
58
|
callbacks.onMessage(userMsg);
|
|
61
59
|
this.session.messages.push(userMsg);
|
|
62
|
-
|
|
60
|
+
// 将回调包装后传给 WorkerBridge,Worker 事件会映射回这里
|
|
61
|
+
const bridgeCallbacks = {
|
|
63
62
|
onMessage: (msg) => {
|
|
64
63
|
callbacks.onMessage(msg);
|
|
65
64
|
this.session.messages.push(msg);
|
|
66
65
|
},
|
|
67
66
|
onUpdateMessage: callbacks.onUpdateMessage,
|
|
68
67
|
onStreamText: (text) => {
|
|
69
|
-
// 每收到一个 chunk 视为一个 token,实时递增并通知 UI
|
|
70
68
|
this.session.totalTokens++;
|
|
71
69
|
callbacks.onStreamText(text);
|
|
72
70
|
callbacks.onSessionUpdate(this.session);
|
|
73
71
|
},
|
|
74
72
|
onClearStreamText: callbacks.onClearStreamText,
|
|
75
73
|
onLoopStateChange: callbacks.onLoopStateChange,
|
|
74
|
+
onSessionUpdate: callbacks.onSessionUpdate,
|
|
76
75
|
onConfirmDangerousCommand: callbacks.onConfirmDangerousCommand,
|
|
77
76
|
};
|
|
78
77
|
try {
|
|
79
|
-
this.transcript = await
|
|
78
|
+
this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks);
|
|
80
79
|
}
|
|
81
80
|
catch (err) {
|
|
82
81
|
const errMsg = {
|
|
@@ -92,9 +91,9 @@ export class QueryEngine {
|
|
|
92
91
|
callbacks.onSessionUpdate(this.session);
|
|
93
92
|
this.saveSession();
|
|
94
93
|
}
|
|
95
|
-
/**
|
|
94
|
+
/** 终止当前任务(通知 Worker 中断) */
|
|
96
95
|
abort() {
|
|
97
|
-
this.
|
|
96
|
+
this.workerBridge.abort();
|
|
98
97
|
}
|
|
99
98
|
/** 重置会话 */
|
|
100
99
|
reset() {
|
|
@@ -182,6 +181,9 @@ export class QueryEngine {
|
|
|
182
181
|
this.saveSession();
|
|
183
182
|
// 恢复会话状态
|
|
184
183
|
this.session = loaded;
|
|
184
|
+
// 过滤掉 pending 状态的 thinking 消息(上次会话 abort 时可能残留)
|
|
185
|
+
const cleanedMessages = loaded.messages.filter((m) => !(m.type === 'thinking' && m.status === 'pending'));
|
|
186
|
+
this.session.messages = cleanedMessages;
|
|
185
187
|
// 从历史消息重建 transcript
|
|
186
188
|
this.transcript = [];
|
|
187
189
|
for (const msg of loaded.messages) {
|
|
@@ -208,7 +210,7 @@ export class QueryEngine {
|
|
|
208
210
|
});
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
|
-
return { session: this.session, messages:
|
|
213
|
+
return { session: this.session, messages: cleanedMessages };
|
|
212
214
|
}
|
|
213
215
|
catch {
|
|
214
216
|
return null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TranscriptMessage } from '../types/index';
|
|
2
|
+
import { EngineCallbacks } from './QueryEngine';
|
|
3
|
+
export declare class WorkerBridge {
|
|
4
|
+
private worker;
|
|
5
|
+
/** 在独立 Worker 线程中执行查询,返回更新后的 transcript */
|
|
6
|
+
run(userInput: string, transcript: TranscriptMessage[], callbacks: EngineCallbacks): Promise<TranscriptMessage[]>;
|
|
7
|
+
/** 向 Worker 发送中断信号 */
|
|
8
|
+
abort(): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkerBridge — 主线程侧封装
|
|
3
|
+
* 负责创建/复用 Worker、转发回调事件、处理危险命令确认的双向通信
|
|
4
|
+
*/
|
|
5
|
+
import { Worker } from 'worker_threads';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
// 兼容 ESM __dirname
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
/**
|
|
12
|
+
* 构造 Worker 实例:
|
|
13
|
+
* - tsx 开发模式(__filename 以 .ts 结尾):用内联脚本通过 tsImport 加载 .ts
|
|
14
|
+
* - tsc 编译后(__filename 以 .js 结尾):直接加载 .js
|
|
15
|
+
*/
|
|
16
|
+
function createWorker(workerTsPath) {
|
|
17
|
+
const isTsx = __filename.endsWith('.ts');
|
|
18
|
+
if (isTsx) {
|
|
19
|
+
// tsImport 第二个参数是 parent URL,用 file:// 绝对路径代替 import.meta.url
|
|
20
|
+
const inlineScript = `
|
|
21
|
+
import { tsImport } from 'tsx/esm/api';
|
|
22
|
+
import { workerData } from 'worker_threads';
|
|
23
|
+
import { pathToFileURL } from 'url';
|
|
24
|
+
await tsImport(workerData.__workerFile, pathToFileURL(workerData.__workerFile).href);
|
|
25
|
+
`;
|
|
26
|
+
return new Worker(inlineScript, {
|
|
27
|
+
eval: true,
|
|
28
|
+
workerData: { __workerFile: workerTsPath },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const jsPath = workerTsPath.replace(/\.ts$/, '.js');
|
|
32
|
+
return new Worker(jsPath);
|
|
33
|
+
}
|
|
34
|
+
export class WorkerBridge {
|
|
35
|
+
worker = null;
|
|
36
|
+
/** 在独立 Worker 线程中执行查询,返回更新后的 transcript */
|
|
37
|
+
run(userInput, transcript, callbacks) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const workerTsPath = path.join(__dirname, 'queryWorker.ts');
|
|
40
|
+
const worker = createWorker(workerTsPath);
|
|
41
|
+
this.worker = worker;
|
|
42
|
+
worker.on('message', async (msg) => {
|
|
43
|
+
switch (msg.type) {
|
|
44
|
+
case 'message':
|
|
45
|
+
callbacks.onMessage(msg.msg);
|
|
46
|
+
break;
|
|
47
|
+
case 'update_message':
|
|
48
|
+
callbacks.onUpdateMessage(msg.id, msg.updates);
|
|
49
|
+
break;
|
|
50
|
+
case 'stream_text':
|
|
51
|
+
callbacks.onStreamText(msg.text);
|
|
52
|
+
break;
|
|
53
|
+
case 'clear_stream_text':
|
|
54
|
+
callbacks.onClearStreamText?.();
|
|
55
|
+
break;
|
|
56
|
+
case 'loop_state':
|
|
57
|
+
callbacks.onLoopStateChange(msg.state);
|
|
58
|
+
break;
|
|
59
|
+
case 'session_update':
|
|
60
|
+
callbacks.onSessionUpdate(msg.session);
|
|
61
|
+
break;
|
|
62
|
+
case 'danger_confirm_request': {
|
|
63
|
+
// 主线程弹出交互式确认,结果回传 Worker
|
|
64
|
+
const choice = callbacks.onConfirmDangerousCommand
|
|
65
|
+
? await callbacks.onConfirmDangerousCommand(msg.command, msg.reason, msg.ruleName)
|
|
66
|
+
: 'cancel';
|
|
67
|
+
const reply = {
|
|
68
|
+
type: 'danger_confirm_result',
|
|
69
|
+
requestId: msg.requestId,
|
|
70
|
+
choice,
|
|
71
|
+
};
|
|
72
|
+
worker.postMessage(reply);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'done':
|
|
76
|
+
this.worker = null;
|
|
77
|
+
worker.terminate();
|
|
78
|
+
resolve(msg.transcript);
|
|
79
|
+
break;
|
|
80
|
+
case 'error':
|
|
81
|
+
this.worker = null;
|
|
82
|
+
worker.terminate();
|
|
83
|
+
reject(new Error(msg.message));
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
worker.on('error', (err) => {
|
|
88
|
+
this.worker = null;
|
|
89
|
+
reject(err);
|
|
90
|
+
});
|
|
91
|
+
worker.on('exit', (code) => {
|
|
92
|
+
if (code !== 0 && this.worker) {
|
|
93
|
+
this.worker = null;
|
|
94
|
+
reject(new Error(`Worker 异常退出,code=${code}`));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// 启动执行
|
|
98
|
+
const runMsg = { type: 'run', userInput, transcript };
|
|
99
|
+
worker.postMessage(runMsg);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** 向 Worker 发送中断信号 */
|
|
103
|
+
abort() {
|
|
104
|
+
if (this.worker) {
|
|
105
|
+
const msg = { type: 'abort' };
|
|
106
|
+
this.worker.postMessage(msg);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/core/hint.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
|
-
import { getAgent } from '../agents/index
|
|
10
|
-
import { DEFAULT_AGENT } from '../config/constants
|
|
11
|
-
import { loadConfig, getActiveModel } from '../config/loader
|
|
12
|
-
import { getDefaultConfig } from '../services/api/llm
|
|
9
|
+
import { getAgent } from '../agents/index';
|
|
10
|
+
import { DEFAULT_AGENT } from '../config/constants';
|
|
11
|
+
import { loadConfig, getActiveModel } from '../config/loader';
|
|
12
|
+
import { getDefaultConfig } from '../services/api/llm';
|
|
13
13
|
/** 安全读取 JSON 文件 */
|
|
14
14
|
function readJsonSafe(filePath) {
|
|
15
15
|
try {
|