@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.
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/agents/code-reviewer.md +69 -0
- package/dist/agents/dba.md +68 -0
- package/dist/agents/finance-advisor.md +81 -0
- package/dist/agents/index.d.ts +31 -0
- package/dist/agents/index.js +86 -0
- package/dist/agents/jarvis.md +95 -0
- package/dist/agents/stock-trader.md +81 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +9 -0
- package/dist/commands/index.d.ts +19 -0
- package/dist/commands/index.js +79 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +283 -0
- package/dist/components/DangerConfirm.d.ts +17 -0
- package/dist/components/DangerConfirm.js +50 -0
- package/dist/components/MarkdownText.d.ts +12 -0
- package/dist/components/MarkdownText.js +166 -0
- package/dist/components/MessageItem.d.ts +8 -0
- package/dist/components/MessageItem.js +78 -0
- package/dist/components/MultilineInput.d.ts +34 -0
- package/dist/components/MultilineInput.js +437 -0
- package/dist/components/SlashCommandMenu.d.ts +16 -0
- package/dist/components/SlashCommandMenu.js +43 -0
- package/dist/components/StatusBar.d.ts +8 -0
- package/dist/components/StatusBar.js +26 -0
- package/dist/components/StreamingText.d.ts +6 -0
- package/dist/components/StreamingText.js +10 -0
- package/dist/components/WelcomeHeader.d.ts +6 -0
- package/dist/components/WelcomeHeader.js +25 -0
- package/dist/config/agentState.d.ts +16 -0
- package/dist/config/agentState.js +65 -0
- package/dist/config/constants.d.ts +25 -0
- package/dist/config/constants.js +67 -0
- package/dist/config/loader.d.ts +30 -0
- package/dist/config/loader.js +64 -0
- package/dist/config/systemInfo.d.ts +12 -0
- package/dist/config/systemInfo.js +95 -0
- package/dist/core/QueryEngine.d.ts +52 -0
- package/dist/core/QueryEngine.js +246 -0
- package/dist/core/hint.d.ts +14 -0
- package/dist/core/hint.js +279 -0
- package/dist/core/query.d.ts +24 -0
- package/dist/core/query.js +245 -0
- package/dist/core/safeguard.d.ts +96 -0
- package/dist/core/safeguard.js +236 -0
- package/dist/hooks/useFocus.d.ts +12 -0
- package/dist/hooks/useFocus.js +35 -0
- package/dist/hooks/useInputHistory.d.ts +14 -0
- package/dist/hooks/useInputHistory.js +102 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/screens/repl.d.ts +1 -0
- package/dist/screens/repl.js +842 -0
- package/dist/services/api/llm.d.ts +27 -0
- package/dist/services/api/llm.js +314 -0
- package/dist/services/api/mock.d.ts +9 -0
- package/dist/services/api/mock.js +102 -0
- package/dist/skills/index.d.ts +23 -0
- package/dist/skills/index.js +232 -0
- package/dist/skills/loader.d.ts +45 -0
- package/dist/skills/loader.js +108 -0
- package/dist/tools/createSkill.d.ts +8 -0
- package/dist/tools/createSkill.js +255 -0
- package/dist/tools/index.d.ts +16 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/listDirectory.d.ts +2 -0
- package/dist/tools/listDirectory.js +20 -0
- package/dist/tools/readFile.d.ts +2 -0
- package/dist/tools/readFile.js +17 -0
- package/dist/tools/runCommand.d.ts +2 -0
- package/dist/tools/runCommand.js +69 -0
- package/dist/tools/searchFiles.d.ts +2 -0
- package/dist/tools/searchFiles.js +45 -0
- package/dist/tools/writeFile.d.ts +2 -0
- package/dist/tools/writeFile.js +42 -0
- package/dist/types/index.d.ts +86 -0
- package/dist/types/index.js +2 -0
- package/package.json +55 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdin } from 'ink';
|
|
4
|
+
/** 粘贴占位符的正则,匹配 [Pasted text #N +X lines] */
|
|
5
|
+
const PASTE_PLACEHOLDER_RE = /\[Pasted text #(\d+) \+(\d+) lines\]/g;
|
|
6
|
+
/** 粘贴检测的时间窗口(ms),在此窗口内连续到达的数据视为一次粘贴 */
|
|
7
|
+
const PASTE_DETECT_WINDOW_MS = 8;
|
|
8
|
+
/**
|
|
9
|
+
* 多行文本输入组件,支持光标移动和粘贴折叠。
|
|
10
|
+
*
|
|
11
|
+
* - Enter: 提交(占位符会被展开为真实内容)
|
|
12
|
+
* - Alt/Option+Enter: 换行
|
|
13
|
+
* - ←→: 左右移动光标
|
|
14
|
+
* - ↑↓: 上下行移动光标(单行时触发 onUpArrow/onDownArrow)
|
|
15
|
+
* - Backspace: 删除光标前一个字符(占位符整体删除)
|
|
16
|
+
* - 粘贴多行内容: 折叠为 [Pasted text #N +X lines] 占位符
|
|
17
|
+
*/
|
|
18
|
+
export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, onDownArrow, placeholder = '', isActive = true, showCursor = true, slashMenuActive = false, onSlashMenuUp, onSlashMenuDown, onSlashMenuSelect, onSlashMenuClose, onTabFillPlaceholder, }) {
|
|
19
|
+
const { stdin } = useStdin();
|
|
20
|
+
const [cursor, setCursor] = useState(value.length);
|
|
21
|
+
// 粘贴内容存储
|
|
22
|
+
const pasteCountRef = useRef(0);
|
|
23
|
+
const pastedChunksRef = useRef(new Map());
|
|
24
|
+
// bracketed paste 缓冲
|
|
25
|
+
const pasteBufferRef = useRef(null);
|
|
26
|
+
// 时间窗口粘贴检测:收集短时间内连续到达的数据块
|
|
27
|
+
const batchBufferRef = useRef('');
|
|
28
|
+
const batchTimerRef = useRef(null);
|
|
29
|
+
// 启用 bracketed paste mode
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!stdin || !isActive)
|
|
32
|
+
return;
|
|
33
|
+
process.stdout.write('\x1B[?2004h');
|
|
34
|
+
return () => {
|
|
35
|
+
process.stdout.write('\x1B[?2004l');
|
|
36
|
+
};
|
|
37
|
+
}, [stdin, isActive]);
|
|
38
|
+
// 标记内部触发的 value 变更,避免 useEffect 覆盖光标位置
|
|
39
|
+
const internalChangeRef = useRef(false);
|
|
40
|
+
// 外部 value 变化时,将光标移到末尾(仅外部变更时)
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (internalChangeRef.current) {
|
|
43
|
+
internalChangeRef.current = false;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
setCursor(value.length);
|
|
47
|
+
}, [value]);
|
|
48
|
+
const valueRef = useRef(value);
|
|
49
|
+
valueRef.current = value;
|
|
50
|
+
const cursorRef = useRef(cursor);
|
|
51
|
+
cursorRef.current = cursor;
|
|
52
|
+
const onChangeRef = useRef(onChange);
|
|
53
|
+
onChangeRef.current = onChange;
|
|
54
|
+
/** 内部触发 onChange,标记为内部变更避免光标跳转 */
|
|
55
|
+
const emitChange = (val) => {
|
|
56
|
+
internalChangeRef.current = true;
|
|
57
|
+
onChangeRef.current(val);
|
|
58
|
+
};
|
|
59
|
+
const onSubmitRef = useRef(onSubmit);
|
|
60
|
+
onSubmitRef.current = onSubmit;
|
|
61
|
+
const onUpArrowRef = useRef(onUpArrow);
|
|
62
|
+
onUpArrowRef.current = onUpArrow;
|
|
63
|
+
const onDownArrowRef = useRef(onDownArrow);
|
|
64
|
+
onDownArrowRef.current = onDownArrow;
|
|
65
|
+
// 斜杠菜单相关 ref
|
|
66
|
+
const slashMenuActiveRef = useRef(slashMenuActive);
|
|
67
|
+
slashMenuActiveRef.current = slashMenuActive;
|
|
68
|
+
const onSlashMenuUpRef = useRef(onSlashMenuUp);
|
|
69
|
+
onSlashMenuUpRef.current = onSlashMenuUp;
|
|
70
|
+
const onSlashMenuDownRef = useRef(onSlashMenuDown);
|
|
71
|
+
onSlashMenuDownRef.current = onSlashMenuDown;
|
|
72
|
+
const onSlashMenuSelectRef = useRef(onSlashMenuSelect);
|
|
73
|
+
onSlashMenuSelectRef.current = onSlashMenuSelect;
|
|
74
|
+
const onSlashMenuCloseRef = useRef(onSlashMenuClose);
|
|
75
|
+
onSlashMenuCloseRef.current = onSlashMenuClose;
|
|
76
|
+
const onTabFillPlaceholderRef = useRef(onTabFillPlaceholder);
|
|
77
|
+
onTabFillPlaceholderRef.current = onTabFillPlaceholder;
|
|
78
|
+
// 辅助:根据光标偏移计算所在行号和行内列号
|
|
79
|
+
const getCursorRowCol = (text, pos) => {
|
|
80
|
+
const before = text.slice(0, pos);
|
|
81
|
+
const row = (before.match(/\n/g) || []).length;
|
|
82
|
+
const lastNewline = before.lastIndexOf('\n');
|
|
83
|
+
const col = lastNewline === -1 ? pos : pos - lastNewline - 1;
|
|
84
|
+
return { row, col };
|
|
85
|
+
};
|
|
86
|
+
// 辅助:根据行号和列号计算偏移
|
|
87
|
+
const rowColToOffset = (text, row, col) => {
|
|
88
|
+
const lines = text.split('\n');
|
|
89
|
+
let offset = 0;
|
|
90
|
+
for (let i = 0; i < row && i < lines.length; i++) {
|
|
91
|
+
offset += lines[i].length + 1;
|
|
92
|
+
}
|
|
93
|
+
const targetLine = lines[Math.min(row, lines.length - 1)] ?? '';
|
|
94
|
+
return offset + Math.min(col, targetLine.length);
|
|
95
|
+
};
|
|
96
|
+
/** 将粘贴的多行内容折叠为占位符,插入到当前光标位置 */
|
|
97
|
+
const insertPaste = (pastedText) => {
|
|
98
|
+
const v = valueRef.current;
|
|
99
|
+
const c = cursorRef.current;
|
|
100
|
+
const cleaned = pastedText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
101
|
+
const lineCount = cleaned.split('\n').length;
|
|
102
|
+
// 单行粘贴直接插入,不折叠
|
|
103
|
+
if (lineCount <= 1) {
|
|
104
|
+
const newVal = v.slice(0, c) + cleaned + v.slice(c);
|
|
105
|
+
emitChange(newVal);
|
|
106
|
+
setCursor(c + cleaned.length);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// 多行 → 生成占位符
|
|
110
|
+
const id = ++pasteCountRef.current;
|
|
111
|
+
pastedChunksRef.current.set(id, { id, content: cleaned, lineCount });
|
|
112
|
+
const tag = `[Pasted text #${id} +${lineCount} lines]`;
|
|
113
|
+
const newVal = v.slice(0, c) + tag + v.slice(c);
|
|
114
|
+
emitChange(newVal);
|
|
115
|
+
setCursor(c + tag.length);
|
|
116
|
+
};
|
|
117
|
+
/** 展开 value 中所有占位符为真实内容 */
|
|
118
|
+
const expandPlaceholders = (text) => {
|
|
119
|
+
return text.replace(PASTE_PLACEHOLDER_RE, (match, idStr) => {
|
|
120
|
+
const id = parseInt(idStr, 10);
|
|
121
|
+
const chunk = pastedChunksRef.current.get(id);
|
|
122
|
+
return chunk ? chunk.content : match;
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
/** 检测光标前是否紧邻一个占位符 */
|
|
126
|
+
const findPlaceholderBeforeCursor = (text, pos) => {
|
|
127
|
+
if (pos === 0 || text[pos - 1] !== ']')
|
|
128
|
+
return null;
|
|
129
|
+
const before = text.slice(0, pos);
|
|
130
|
+
const idx = before.lastIndexOf('[Pasted text #');
|
|
131
|
+
if (idx === -1)
|
|
132
|
+
return null;
|
|
133
|
+
const sub = before.slice(idx);
|
|
134
|
+
const m = sub.match(/^\[Pasted text #\d+ \+\d+ lines\]$/);
|
|
135
|
+
if (m)
|
|
136
|
+
return { start: idx, end: pos };
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
/** 处理单个普通输入字符/按键(非粘贴) */
|
|
140
|
+
const handleNormalInput = (raw) => {
|
|
141
|
+
const v = valueRef.current;
|
|
142
|
+
const c = cursorRef.current;
|
|
143
|
+
// Alt+Enter → 插入换行
|
|
144
|
+
if (raw === '\x1B\r' || raw === '\x1B\n') {
|
|
145
|
+
const newVal = v.slice(0, c) + '\n' + v.slice(c);
|
|
146
|
+
emitChange(newVal);
|
|
147
|
+
setCursor(c + 1);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Enter → 提交(展开占位符)
|
|
151
|
+
if (raw === '\r' || raw === '\n') {
|
|
152
|
+
// 斜杠菜单激活时,回车 = 选中当前项
|
|
153
|
+
if (slashMenuActiveRef.current) {
|
|
154
|
+
onSlashMenuSelectRef.current?.();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const expanded = expandPlaceholders(v);
|
|
158
|
+
onSubmitRef.current(expanded);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Backspace
|
|
162
|
+
if (raw === '\x7F' || raw === '\x08') {
|
|
163
|
+
if (c > 0) {
|
|
164
|
+
const ph = findPlaceholderBeforeCursor(v, c);
|
|
165
|
+
if (ph) {
|
|
166
|
+
const match = v.slice(ph.start, ph.end).match(/\[Pasted text #(\d+)/);
|
|
167
|
+
if (match)
|
|
168
|
+
pastedChunksRef.current.delete(parseInt(match[1], 10));
|
|
169
|
+
const newVal = v.slice(0, ph.start) + v.slice(ph.end);
|
|
170
|
+
emitChange(newVal);
|
|
171
|
+
setCursor(ph.start);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const newVal = v.slice(0, c - 1) + v.slice(c);
|
|
175
|
+
emitChange(newVal);
|
|
176
|
+
setCursor(c - 1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// ← 左移光标
|
|
182
|
+
if (raw === '\x1B[D') {
|
|
183
|
+
setCursor(prev => Math.max(0, prev - 1));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// → 右移光标
|
|
187
|
+
if (raw === '\x1B[C') {
|
|
188
|
+
setCursor(prev => Math.min(v.length, prev + 1));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// ↑ 上移光标
|
|
192
|
+
if (raw === '\x1B[A') {
|
|
193
|
+
// 斜杠菜单激活时,上箭头 = 选中上一项
|
|
194
|
+
if (slashMenuActiveRef.current) {
|
|
195
|
+
onSlashMenuUpRef.current?.();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const lines = v.split('\n');
|
|
199
|
+
if (lines.length <= 1) {
|
|
200
|
+
onUpArrowRef.current?.();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const { row, col } = getCursorRowCol(v, c);
|
|
204
|
+
if (row === 0) {
|
|
205
|
+
onUpArrowRef.current?.();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
setCursor(rowColToOffset(v, row - 1, col));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// ↓ 下移光标
|
|
212
|
+
if (raw === '\x1B[B') {
|
|
213
|
+
// 斜杠菜单激活时,下箭头 = 选中下一项
|
|
214
|
+
if (slashMenuActiveRef.current) {
|
|
215
|
+
onSlashMenuDownRef.current?.();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const lines = v.split('\n');
|
|
219
|
+
if (lines.length <= 1) {
|
|
220
|
+
onDownArrowRef.current?.();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const { row, col } = getCursorRowCol(v, c);
|
|
224
|
+
if (row >= lines.length - 1) {
|
|
225
|
+
onDownArrowRef.current?.();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
setCursor(rowColToOffset(v, row + 1, col));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Ctrl+C → 不在此处处理,交给上层 useInput 的双击退出逻辑
|
|
232
|
+
if (raw === '\x03')
|
|
233
|
+
return;
|
|
234
|
+
// 忽略控制字符和转义序列
|
|
235
|
+
if (raw === '\t') {
|
|
236
|
+
// 斜杠菜单激活时,Tab = 选中当前项
|
|
237
|
+
if (slashMenuActiveRef.current) {
|
|
238
|
+
onSlashMenuSelectRef.current?.();
|
|
239
|
+
}
|
|
240
|
+
else if (valueRef.current.length === 0) {
|
|
241
|
+
// 输入为空时,Tab = 填入 placeholder
|
|
242
|
+
onTabFillPlaceholderRef.current?.();
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (raw === '\x1B') {
|
|
247
|
+
// 斜杠菜单激活时,ESC = 关闭菜单
|
|
248
|
+
if (slashMenuActiveRef.current) {
|
|
249
|
+
onSlashMenuCloseRef.current?.();
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (raw.length === 1 && raw.charCodeAt(0) < 32)
|
|
254
|
+
return;
|
|
255
|
+
if (raw.startsWith('\x1B[') || raw.startsWith('\x1B]') || raw.startsWith('\x1BO'))
|
|
256
|
+
return;
|
|
257
|
+
// 可打印字符 → 在光标位置插入
|
|
258
|
+
const newVal = v.slice(0, c) + raw + v.slice(c);
|
|
259
|
+
emitChange(newVal);
|
|
260
|
+
setCursor(c + raw.length);
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* 处理批量缓冲区中积累的数据。
|
|
264
|
+
* 如果缓冲区包含换行(多行),视为粘贴;否则逐字符处理。
|
|
265
|
+
*/
|
|
266
|
+
const flushBatchBuffer = () => {
|
|
267
|
+
const buf = batchBufferRef.current;
|
|
268
|
+
batchBufferRef.current = '';
|
|
269
|
+
batchTimerRef.current = null;
|
|
270
|
+
if (!buf)
|
|
271
|
+
return;
|
|
272
|
+
const cleaned = buf.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
273
|
+
// 多行 → 粘贴
|
|
274
|
+
if (cleaned.includes('\n') && cleaned.length > 1) {
|
|
275
|
+
insertPaste(cleaned);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// 单字符或单行短文本,按普通输入处理
|
|
279
|
+
handleNormalInput(buf);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
// Alt+Enter 组合键检测:ESC 可能单独到达,需要等待后续字符
|
|
283
|
+
const escTimerRef = useRef(null);
|
|
284
|
+
const ESC_WAIT_MS = 50; // 等待后续字符的时间窗口
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (!stdin || !isActive)
|
|
287
|
+
return;
|
|
288
|
+
const onData = (data) => {
|
|
289
|
+
const raw = data.toString('utf-8');
|
|
290
|
+
// === Bracketed paste 模式处理(优先级最高) ===
|
|
291
|
+
if (raw.includes('\x1B[200~')) {
|
|
292
|
+
const startIdx = raw.indexOf('\x1B[200~') + 6;
|
|
293
|
+
const endIdx = raw.indexOf('\x1B[201~');
|
|
294
|
+
if (endIdx !== -1) {
|
|
295
|
+
insertPaste(raw.slice(startIdx, endIdx));
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
pasteBufferRef.current = raw.slice(startIdx);
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (pasteBufferRef.current !== null) {
|
|
303
|
+
const endIdx = raw.indexOf('\x1B[201~');
|
|
304
|
+
if (endIdx !== -1) {
|
|
305
|
+
pasteBufferRef.current += raw.slice(0, endIdx);
|
|
306
|
+
insertPaste(pasteBufferRef.current);
|
|
307
|
+
pasteBufferRef.current = null;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
pasteBufferRef.current += raw;
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// === 处理 ESC 等待状态:上一次收到了单独的 ESC,现在看后续字符 ===
|
|
315
|
+
if (escTimerRef.current !== null) {
|
|
316
|
+
clearTimeout(escTimerRef.current);
|
|
317
|
+
escTimerRef.current = null;
|
|
318
|
+
if (raw === '\r' || raw === '\n') {
|
|
319
|
+
// ESC + Enter = Alt+Enter → 插入换行
|
|
320
|
+
handleNormalInput('\x1B\r');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// ESC + 其他字符:先处理 ESC 本身,再处理当前字符
|
|
324
|
+
handleNormalInput('\x1B');
|
|
325
|
+
// 继续往下处理当前 raw
|
|
326
|
+
}
|
|
327
|
+
// === 非 bracketed paste:用时间窗口检测 ===
|
|
328
|
+
// 控制字符和转义序列不参与批量缓冲,直接处理
|
|
329
|
+
const isSingleControl = raw === '\r' || raw === '\n' ||
|
|
330
|
+
raw === '\x7F' || raw === '\x08' ||
|
|
331
|
+
raw === '\t' || raw === '\x1B' ||
|
|
332
|
+
raw === '\x1B\r' || raw === '\x1B\n' ||
|
|
333
|
+
raw.startsWith('\x1B[') || raw.startsWith('\x1B]') || raw.startsWith('\x1BO');
|
|
334
|
+
// Ctrl+C → 不拦截,让上层 useInput 处理双击退出
|
|
335
|
+
if (raw === '\x03')
|
|
336
|
+
return;
|
|
337
|
+
// Ctrl+O → 不拦截,让上层 useInput 处理详情展开/折叠
|
|
338
|
+
if (raw === '\x0F')
|
|
339
|
+
return;
|
|
340
|
+
// Ctrl+L → 拦截,不穿透到终端(避免清屏)
|
|
341
|
+
if (raw === '\x0C')
|
|
342
|
+
return;
|
|
343
|
+
// 单独的 ESC:进入等待状态,看后续是否跟着 Enter(Alt+Enter 组合)
|
|
344
|
+
if (raw === '\x1B') {
|
|
345
|
+
escTimerRef.current = setTimeout(() => {
|
|
346
|
+
escTimerRef.current = null;
|
|
347
|
+
handleNormalInput('\x1B');
|
|
348
|
+
}, ESC_WAIT_MS);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (isSingleControl && batchBufferRef.current === '') {
|
|
352
|
+
// 没有正在积累的缓冲,直接处理控制字符
|
|
353
|
+
handleNormalInput(raw);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (isSingleControl && batchBufferRef.current !== '') {
|
|
357
|
+
// 有缓冲在积累中,控制字符也追加进去(可能是粘贴内容中的 \r)
|
|
358
|
+
batchBufferRef.current += raw;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
// 可打印字符:追加到批量缓冲,重置定时器
|
|
362
|
+
batchBufferRef.current += raw;
|
|
363
|
+
if (batchTimerRef.current !== null) {
|
|
364
|
+
clearTimeout(batchTimerRef.current);
|
|
365
|
+
}
|
|
366
|
+
batchTimerRef.current = setTimeout(flushBatchBuffer, PASTE_DETECT_WINDOW_MS);
|
|
367
|
+
};
|
|
368
|
+
stdin.prependListener('data', onData);
|
|
369
|
+
return () => {
|
|
370
|
+
stdin.off('data', onData);
|
|
371
|
+
if (batchTimerRef.current !== null) {
|
|
372
|
+
clearTimeout(batchTimerRef.current);
|
|
373
|
+
batchTimerRef.current = null;
|
|
374
|
+
}
|
|
375
|
+
if (escTimerRef.current !== null) {
|
|
376
|
+
clearTimeout(escTimerRef.current);
|
|
377
|
+
escTimerRef.current = null;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}, [stdin, isActive]);
|
|
381
|
+
// 组件卸载时清理
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
return () => {
|
|
384
|
+
if (batchTimerRef.current !== null) {
|
|
385
|
+
clearTimeout(batchTimerRef.current);
|
|
386
|
+
}
|
|
387
|
+
if (escTimerRef.current !== null) {
|
|
388
|
+
clearTimeout(escTimerRef.current);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}, []);
|
|
392
|
+
useInput(() => { }, { isActive });
|
|
393
|
+
// --- 渲染 ---
|
|
394
|
+
const isEmpty = value.length === 0;
|
|
395
|
+
if (isEmpty) {
|
|
396
|
+
// value 清空时重置粘贴状态
|
|
397
|
+
if (pasteCountRef.current > 0) {
|
|
398
|
+
pasteCountRef.current = 0;
|
|
399
|
+
pastedChunksRef.current.clear();
|
|
400
|
+
}
|
|
401
|
+
if (showCursor && placeholder.length > 0) {
|
|
402
|
+
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
|
+
}
|
|
404
|
+
return (_jsxs(Box, { children: [showCursor && _jsx(Text, { inverse: true, children: " " }), _jsx(Text, { color: "gray", children: placeholder })] }));
|
|
405
|
+
}
|
|
406
|
+
const lines = value.split('\n');
|
|
407
|
+
const { row: cursorRow, col: cursorCol } = getCursorRowCol(value, cursor);
|
|
408
|
+
/** 渲染一段文本,将其中的占位符高亮 */
|
|
409
|
+
const renderWithPlaceholders = (text, keyPrefix) => {
|
|
410
|
+
const parts = [];
|
|
411
|
+
let lastIndex = 0;
|
|
412
|
+
const re = new RegExp(PASTE_PLACEHOLDER_RE.source, 'g');
|
|
413
|
+
let m;
|
|
414
|
+
while ((m = re.exec(text)) !== null) {
|
|
415
|
+
if (m.index > lastIndex) {
|
|
416
|
+
parts.push(_jsx(Text, { children: text.slice(lastIndex, m.index) }, `${keyPrefix}-t-${lastIndex}`));
|
|
417
|
+
}
|
|
418
|
+
parts.push(_jsx(Text, { color: "cyan", dimColor: true, children: m[0] }, `${keyPrefix}-p-${m.index}`));
|
|
419
|
+
lastIndex = m.index + m[0].length;
|
|
420
|
+
}
|
|
421
|
+
if (lastIndex < text.length) {
|
|
422
|
+
parts.push(_jsx(Text, { children: text.slice(lastIndex) }, `${keyPrefix}-t-${lastIndex}`));
|
|
423
|
+
}
|
|
424
|
+
return parts;
|
|
425
|
+
};
|
|
426
|
+
return (_jsx(Box, { flexDirection: "column", children: lines.map((line, i) => {
|
|
427
|
+
if (!showCursor || i !== cursorRow) {
|
|
428
|
+
const parts = renderWithPlaceholders(line, `l${i}`);
|
|
429
|
+
return _jsx(Box, { children: parts.length > 0 ? parts : _jsx(Text, { children: " " }) }, i);
|
|
430
|
+
}
|
|
431
|
+
// 光标所在行
|
|
432
|
+
const before = line.slice(0, cursorCol);
|
|
433
|
+
const cursorChar = line[cursorCol] ?? ' ';
|
|
434
|
+
const after = line.slice(cursorCol + 1);
|
|
435
|
+
return (_jsxs(Box, { children: [renderWithPlaceholders(before, 'b'), _jsx(Text, { inverse: true, children: cursorChar }), renderWithPlaceholders(after, 'a')] }, i));
|
|
436
|
+
}) }));
|
|
437
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SlashCommand } from '../commands/index.js';
|
|
3
|
+
interface SlashCommandMenuProps {
|
|
4
|
+
commands: SlashCommand[];
|
|
5
|
+
selectedIndex: number;
|
|
6
|
+
/** 菜单最大可见行数 */
|
|
7
|
+
maxVisible?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 斜杠命令下拉菜单组件
|
|
11
|
+
*
|
|
12
|
+
* 显示在输入框上方,支持滚动窗口。
|
|
13
|
+
*/
|
|
14
|
+
declare function SlashCommandMenu({ commands, selectedIndex, maxVisible, }: SlashCommandMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
declare const _default: React.MemoExoticComponent<typeof SlashCommandMenu>;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
/** 类别标签颜色 */
|
|
5
|
+
const categoryColor = {
|
|
6
|
+
agent: 'magenta',
|
|
7
|
+
tool: 'cyan',
|
|
8
|
+
builtin: 'gray',
|
|
9
|
+
};
|
|
10
|
+
const categoryLabel = {
|
|
11
|
+
agent: '智能体',
|
|
12
|
+
tool: '工具',
|
|
13
|
+
builtin: '内置',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* 斜杠命令下拉菜单组件
|
|
17
|
+
*
|
|
18
|
+
* 显示在输入框上方,支持滚动窗口。
|
|
19
|
+
*/
|
|
20
|
+
function SlashCommandMenu({ commands, selectedIndex, maxVisible = 6, }) {
|
|
21
|
+
if (commands.length === 0) {
|
|
22
|
+
return (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: "gray", italic: true, children: "\u65E0\u5339\u914D\u547D\u4EE4" }) }));
|
|
23
|
+
}
|
|
24
|
+
// 计算滚动窗口
|
|
25
|
+
const total = commands.length;
|
|
26
|
+
const visible = Math.min(total, maxVisible);
|
|
27
|
+
let start = 0;
|
|
28
|
+
if (selectedIndex >= visible) {
|
|
29
|
+
start = selectedIndex - visible + 1;
|
|
30
|
+
}
|
|
31
|
+
if (start + visible > total) {
|
|
32
|
+
start = Math.max(0, total - visible);
|
|
33
|
+
}
|
|
34
|
+
const visibleItems = commands.slice(start, start + visible);
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [start > 0 && (_jsxs(Text, { color: "gray", dimColor: true, children: [" \u2191 \u8FD8\u6709 ", start, " \u9879"] })), visibleItems.map((cmd, i) => {
|
|
36
|
+
const realIndex = start + i;
|
|
37
|
+
const isSelected = realIndex === selectedIndex;
|
|
38
|
+
const catColor = categoryColor[cmd.category] ?? 'gray';
|
|
39
|
+
const catText = categoryLabel[cmd.category] ?? cmd.category;
|
|
40
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : 'white', children: [' ', "/", cmd.name, ' '] }), _jsxs(Text, { color: isSelected ? 'cyan' : 'gray', children: ["- ", cmd.description, ' '] }), _jsxs(Text, { color: catColor, dimColor: true, children: ["[", catText, "]"] })] }, cmd.name));
|
|
41
|
+
}), start + visible < total && (_jsxs(Text, { color: "gray", dimColor: true, children: [" \u2193 \u8FD8\u6709 ", total - start - visible, " \u9879"] }))] }));
|
|
42
|
+
}
|
|
43
|
+
export default React.memo(SlashCommandMenu);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface StatusBarProps {
|
|
3
|
+
width: number;
|
|
4
|
+
totalTokens: number;
|
|
5
|
+
}
|
|
6
|
+
declare function StatusBar({ width, totalTokens }: StatusBarProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
declare const _default: React.MemoExoticComponent<typeof StatusBar>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { MODEL_NAME, PROJECT_NAME, ENABLE_THINKING_MODE_TOGGLE, CONTEXT_TOKEN_LIMIT } from '../config/constants.js';
|
|
5
|
+
/** 生成 token 用量进度条 */
|
|
6
|
+
function tokenProgressBar(used, limit, barWidth) {
|
|
7
|
+
const ratio = Math.min(used / limit, 1);
|
|
8
|
+
const filled = Math.round(ratio * barWidth);
|
|
9
|
+
const empty = barWidth - filled;
|
|
10
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
11
|
+
const color = ratio >= 0.9 ? 'red' : ratio >= 0.7 ? 'yellow' : 'green';
|
|
12
|
+
return { bar, color };
|
|
13
|
+
}
|
|
14
|
+
function StatusBar({ width, totalTokens }) {
|
|
15
|
+
const left = ` ${MODEL_NAME} │ ${PROJECT_NAME}`;
|
|
16
|
+
// 右侧:token 进度条 + 思考模式切换(可选)
|
|
17
|
+
const tokenLabel = `${totalTokens}/${CONTEXT_TOKEN_LIMIT}`;
|
|
18
|
+
const barWidth = 10;
|
|
19
|
+
const { bar, color } = tokenProgressBar(totalTokens, CONTEXT_TOKEN_LIMIT, barWidth);
|
|
20
|
+
const effortPart = ENABLE_THINKING_MODE_TOGGLE ? ' │ ● medium · /effort' : '';
|
|
21
|
+
// 右侧完整文本长度(用于计算间距)
|
|
22
|
+
const rightLen = tokenLabel.length + 1 + barWidth + effortPart.length + 1;
|
|
23
|
+
const gap = Math.max(width - left.length - rightLen, 1);
|
|
24
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: left }), _jsx(Text, { children: ' '.repeat(gap) }), _jsxs(Text, { color: "gray", children: [tokenLabel, " "] }), _jsx(Text, { color: color, children: bar }), ENABLE_THINKING_MODE_TOGGLE && _jsx(Text, { color: "gray", children: effortPart }), _jsx(Text, { children: " " })] }));
|
|
25
|
+
}
|
|
26
|
+
export default React.memo(StatusBar);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import MarkdownText from './MarkdownText.js';
|
|
5
|
+
function StreamingText({ text }) {
|
|
6
|
+
if (!text)
|
|
7
|
+
return null;
|
|
8
|
+
return (_jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "\u25CF" }), _jsx(Box, { marginLeft: 1, children: _jsx(MarkdownText, { text: text }) })] }) }));
|
|
9
|
+
}
|
|
10
|
+
export default React.memo(StreamingText);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { APP_NAME, APP_VERSION, MODEL_NAME } from '../config/constants.js';
|
|
5
|
+
function truncatePath(p, max) {
|
|
6
|
+
if (p.length <= max)
|
|
7
|
+
return p;
|
|
8
|
+
return '…' + p.slice(p.length - max + 1);
|
|
9
|
+
}
|
|
10
|
+
/** ASCII art logo */
|
|
11
|
+
const LOGO_LINES = [
|
|
12
|
+
' ██╗ █████╗ ██████╗ ██╗ ██╗██╗███████╗',
|
|
13
|
+
' ██║██╔══██╗██╔══██╗██║ ██║██║██╔════╝',
|
|
14
|
+
' ██║███████║██████╔╝██║ ██║██║███████╗',
|
|
15
|
+
'██ ██║██╔══██║██╔══██╗╚██╗ ██╔╝██║╚════██║',
|
|
16
|
+
'╚█████╔╝██║ ██║██║ ██║ ╚████╔╝ ██║███████║',
|
|
17
|
+
' ╚════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝╚══════╝',
|
|
18
|
+
];
|
|
19
|
+
const LOGO_COLORS = ['cyan', 'cyan', 'blueBright', 'blueBright', 'magenta', 'magenta'];
|
|
20
|
+
function WelcomeHeader({ width }) {
|
|
21
|
+
const maxPath = Math.max(width - 10, 20);
|
|
22
|
+
const showLogo = width >= 52;
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, width: width, children: [showLogo && (_jsx(Box, { flexDirection: "column", children: LOGO_LINES.map((line, i) => (_jsx(Text, { color: LOGO_COLORS[i], children: line }, i))) })), _jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: "gray" }), _jsx(Text, { color: "white", bold: true, children: "Your AI-Powered Dev Companion" }), _jsx(Text, { color: "gray" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: '─'.repeat(Math.min(width - 4, 48)) }) }), _jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: "gray", children: "model " }), _jsx(Text, { color: "cyan", children: MODEL_NAME }), _jsxs(Text, { color: "gray", children: [" ", APP_NAME, " "] }), _jsx(Text, { color: "magenta", children: APP_VERSION })] }), _jsx(Box, { children: _jsx(Text, { color: "gray", children: truncatePath(process.cwd(), maxPath) }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: '─'.repeat(Math.min(width - 4, 48)) }) }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "init" }), _jsx(Text, { color: "gray", children: " \u521D\u59CB\u5316 " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "help" }), _jsx(Text, { color: "gray", children: " \u5E2E\u52A9 " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "new" }), _jsx(Text, { color: "gray", children: " \u65B0\u4F1A\u8BDD " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "agent" }), _jsx(Text, { color: "gray", children: " \u5207\u6362" })] }), _jsx(Text, { children: ' ' })] }));
|
|
24
|
+
}
|
|
25
|
+
export default React.memo(WelcomeHeader);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 智能体选择状态管理
|
|
3
|
+
*
|
|
4
|
+
* 持久化当前激活的智能体到 ~/.jarvis/agent.json
|
|
5
|
+
* 启动时自动读取,运行时可动态切换。
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 获取当前激活的智能体名称
|
|
9
|
+
*
|
|
10
|
+
* 优先级:运行时设置 > 持久化文件 > fallback
|
|
11
|
+
*/
|
|
12
|
+
export declare function getActiveAgent(fallback: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* 切换当前激活的智能体(运行时 + 持久化)
|
|
15
|
+
*/
|
|
16
|
+
export declare function setActiveAgent(name: string): void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 智能体选择状态管理
|
|
3
|
+
*
|
|
4
|
+
* 持久化当前激活的智能体到 ~/.jarvis/agent.json
|
|
5
|
+
* 启动时自动读取,运行时可动态切换。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
const JARVIS_HOME = path.join(os.homedir(), '.jarvis');
|
|
11
|
+
const AGENT_STATE_FILE = path.join(JARVIS_HOME, 'agent.json');
|
|
12
|
+
/** 运行时当前激活的智能体名称 */
|
|
13
|
+
let _currentAgent = null;
|
|
14
|
+
/** 确保 ~/.jarvis 目录存在 */
|
|
15
|
+
function ensureDir() {
|
|
16
|
+
if (!fs.existsSync(JARVIS_HOME)) {
|
|
17
|
+
fs.mkdirSync(JARVIS_HOME, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** 从 ~/.jarvis/agent.json 读取持久化的智能体选择 */
|
|
21
|
+
function loadPersistedAgent() {
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(AGENT_STATE_FILE))
|
|
24
|
+
return null;
|
|
25
|
+
const raw = fs.readFileSync(AGENT_STATE_FILE, 'utf-8');
|
|
26
|
+
const state = JSON.parse(raw);
|
|
27
|
+
return state.activeAgent || null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** 持久化智能体选择到 ~/.jarvis/agent.json */
|
|
34
|
+
function persistAgent(name) {
|
|
35
|
+
try {
|
|
36
|
+
ensureDir();
|
|
37
|
+
const state = { activeAgent: name };
|
|
38
|
+
fs.writeFileSync(AGENT_STATE_FILE, JSON.stringify(state, null, 2), 'utf-8');
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error('[agentState] 持久化失败:', err instanceof Error ? err.message : err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 获取当前激活的智能体名称
|
|
46
|
+
*
|
|
47
|
+
* 优先级:运行时设置 > 持久化文件 > fallback
|
|
48
|
+
*/
|
|
49
|
+
export function getActiveAgent(fallback) {
|
|
50
|
+
if (_currentAgent)
|
|
51
|
+
return _currentAgent;
|
|
52
|
+
const persisted = loadPersistedAgent();
|
|
53
|
+
if (persisted) {
|
|
54
|
+
_currentAgent = persisted;
|
|
55
|
+
return persisted;
|
|
56
|
+
}
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 切换当前激活的智能体(运行时 + 持久化)
|
|
61
|
+
*/
|
|
62
|
+
export function setActiveAgent(name) {
|
|
63
|
+
_currentAgent = name;
|
|
64
|
+
persistAgent(name);
|
|
65
|
+
}
|