@ghenya/clinn 0.8.7 → 0.9.0
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/Mem/history.js +326 -24
- package/Mem/index.js +123 -14
- package/Src/agent.cjs +74 -11
- package/Src/index.js +81 -8
- package/Src/index.jsx +366 -108
- package/Tools/extended_tools.js +83 -8
- package/Tools/index.js +1 -1
- package/Tools/search_tools.js +55 -1
- package/config.json +1 -1
- package/package.json +8 -8
package/Src/index.jsx
CHANGED
|
@@ -11,7 +11,7 @@ import os from "os";
|
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
|
|
13
13
|
const Tools = require("../Tools");
|
|
14
|
-
const { listRecentTurns, searchHistory, getFileList, loadFileTurns } = require("../Mem/history");
|
|
14
|
+
const { listRecentTurns, searchHistory, getFileList, loadFileTurns, listSessions, getSession, loadSessionTurns, globalSearch, endSession, startSession } = require("../Mem/history");
|
|
15
15
|
const kao = require("../Tools/kaomoji");
|
|
16
16
|
|
|
17
17
|
const __filename = new URL(import.meta.url).pathname;
|
|
@@ -22,7 +22,7 @@ const CLINN_CONFIG = path.join(CLINN_DIR, "config.json");
|
|
|
22
22
|
const PKG_CONFIG = path.join(__dirname, "..", "config.json");
|
|
23
23
|
const LOGO_PATH = path.join(__dirname, "..", "Logos", "StartLogo.txt");
|
|
24
24
|
|
|
25
|
-
const VER = "0.
|
|
25
|
+
const VER = "0.9.0";
|
|
26
26
|
|
|
27
27
|
function ensureDir() { if (!fs.existsSync(CLINN_DIR)) fs.mkdirSync(CLINN_DIR, { recursive: true }); }
|
|
28
28
|
function loadConfig() {
|
|
@@ -133,7 +133,6 @@ function formatMd(text, maxW) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
function Msg({ role, content }) {
|
|
136
|
-
const lines = content.split("\n");
|
|
137
136
|
const C = {
|
|
138
137
|
user: "greenBright",
|
|
139
138
|
system: "yellow",
|
|
@@ -143,12 +142,23 @@ function Msg({ role, content }) {
|
|
|
143
142
|
const face = content._kao || kao.matchFace(content, role);
|
|
144
143
|
const displayLabel = `${face} ${label}`;
|
|
145
144
|
const bodyColor = role === "user" ? "white" : role === "system" ? "yellow" : undefined;
|
|
145
|
+
|
|
146
|
+
const MAX = role === "user" ? 300 : 99999;
|
|
147
|
+
const folded = content.length > MAX;
|
|
148
|
+
const lines = (folded ? content.slice(0, 200) : content).split("\n");
|
|
149
|
+
const hidden = folded ? content.length - 200 : 0;
|
|
150
|
+
|
|
146
151
|
return (
|
|
147
152
|
<Box flexDirection="column">
|
|
148
153
|
<Text color={C[role]} bold>{displayLabel}</Text>
|
|
149
154
|
<Text color={bodyColor}>
|
|
150
155
|
{lines.map((l, i) => (i === 0 ? "" : "\n") + " " + (l || " ")).join("")}
|
|
151
156
|
</Text>
|
|
157
|
+
{folded && (
|
|
158
|
+
<Text color="gray" dimColor>
|
|
159
|
+
{" [+" + Math.ceil(hidden / 100) * 100 + " MORE]"}
|
|
160
|
+
</Text>
|
|
161
|
+
)}
|
|
152
162
|
</Box>
|
|
153
163
|
);
|
|
154
164
|
}
|
|
@@ -169,24 +179,82 @@ function Streaming({ content }) {
|
|
|
169
179
|
);
|
|
170
180
|
}
|
|
171
181
|
|
|
172
|
-
function ToolLog({ tools }) {
|
|
182
|
+
function ToolLog({ tools, collapsed }) {
|
|
173
183
|
if (!tools.length) return null;
|
|
184
|
+
|
|
185
|
+
function colorResult(text) {
|
|
186
|
+
if (!text) return null;
|
|
187
|
+
const lower = text.toLowerCase();
|
|
188
|
+
let color = "gray";
|
|
189
|
+
let sym = "";
|
|
190
|
+
if (/error|失败|错误|eacces|eperm|enoent/i.test(lower)) { color = "red"; sym = "✗ "; }
|
|
191
|
+
else if (/\[ok\]|完成|成功|created|wrote|写入|创建|已保存|passed/i.test(lower)) { color = "green"; sym = " "; }
|
|
192
|
+
else if (/deleted|删除|移除|removed/i.test(lower)) { color = "red"; sym = "✗ "; }
|
|
193
|
+
else if (/modified|修改|更新|patched|changed/i.test(lower)) { color = "yellow"; sym = "~ "; }
|
|
194
|
+
else if (/not found|未找到|不存在|no match/i.test(lower)) { color = "red"; sym = "? "; }
|
|
195
|
+
return { text: sym + text.slice(0, 100), color };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (collapsed) {
|
|
199
|
+
return (
|
|
200
|
+
<Box flexDirection="column" paddingLeft={2}>
|
|
201
|
+
<Text color="gray" dimColor>{"─".repeat(50)} 工具调用 ({tools.length})</Text>
|
|
202
|
+
{tools.map((t, i) => {
|
|
203
|
+
const cr = colorResult(t.result);
|
|
204
|
+
return (
|
|
205
|
+
<Box key={i} flexDirection="row">
|
|
206
|
+
{t.status === "done"
|
|
207
|
+
? <Text color="green"> ✓ </Text>
|
|
208
|
+
: <Text color="yellow"><Spinner /></Text>}
|
|
209
|
+
<Text color="cyan">{t.name}</Text>
|
|
210
|
+
{cr ? <Text color={cr.color}> {cr.text}</Text> : null}
|
|
211
|
+
</Box>
|
|
212
|
+
);
|
|
213
|
+
})}
|
|
214
|
+
</Box>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
174
218
|
return (
|
|
175
219
|
<Box flexDirection="column" paddingLeft={2}>
|
|
220
|
+
<Text color="gray" dimColor>{"─".repeat(50)} 工具调用 ({tools.length})</Text>
|
|
176
221
|
{tools.map((t, i) => (
|
|
177
|
-
<Box key={i} flexDirection="column">
|
|
178
|
-
<Box>
|
|
222
|
+
<Box key={i} flexDirection="column" marginBottom={1}>
|
|
223
|
+
<Box flexDirection="row">
|
|
179
224
|
{t.status === "done"
|
|
180
225
|
? <Text color="green">✓ </Text>
|
|
181
|
-
: <Text color="yellow"><Spinner /> </Text>}
|
|
182
|
-
<Text
|
|
183
|
-
{t.args ? <Text color="gray"> {t.args}</Text> : null}
|
|
226
|
+
: <Text color="yellow"><Spinner type="dots" /> </Text>}
|
|
227
|
+
<Text color="cyan" bold>{t.name}</Text>
|
|
184
228
|
</Box>
|
|
185
|
-
{t.
|
|
186
|
-
<Box paddingLeft={
|
|
187
|
-
<Text color="gray">{t.
|
|
229
|
+
{t.args ? (
|
|
230
|
+
<Box paddingLeft={3}>
|
|
231
|
+
<Text color="gray">{t.args}</Text>
|
|
188
232
|
</Box>
|
|
189
233
|
) : null}
|
|
234
|
+
{t.result ? (() => {
|
|
235
|
+
const lines = t.result.split("\n").filter(l => l.trim());
|
|
236
|
+
if (lines.length <= 1) {
|
|
237
|
+
const cr = colorResult(t.result);
|
|
238
|
+
return (
|
|
239
|
+
<Box paddingLeft={3}>
|
|
240
|
+
<Text color={cr?.color || "gray"}>{t.result.slice(0, 200)}</Text>
|
|
241
|
+
</Box>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return (
|
|
245
|
+
<Box flexDirection="column" paddingLeft={3}>
|
|
246
|
+
{lines.map((l, j) => {
|
|
247
|
+
const cr = colorResult(l);
|
|
248
|
+
return (
|
|
249
|
+
<Text key={j} color={cr?.color || "gray"}>
|
|
250
|
+
{cr?.text || l.slice(0, 200)}
|
|
251
|
+
</Text>
|
|
252
|
+
);
|
|
253
|
+
}).slice(0, 6)}
|
|
254
|
+
{lines.length > 6 ? <Text color="gray" dimColor>... (+{lines.length - 6} 行)</Text> : null}
|
|
255
|
+
</Box>
|
|
256
|
+
);
|
|
257
|
+
})() : null}
|
|
190
258
|
</Box>
|
|
191
259
|
))}
|
|
192
260
|
</Box>
|
|
@@ -236,6 +304,9 @@ const COMMANDS = [
|
|
|
236
304
|
{ cmd: "/history files", desc: "历史文件列表" },
|
|
237
305
|
{ cmd: "/history search", desc: "搜索历史" },
|
|
238
306
|
{ cmd: "/history read", desc: "读取历史文件" },
|
|
307
|
+
{ cmd: "/sessions", desc: "列出对话 session" },
|
|
308
|
+
{ cmd: "/session", desc: "查看某个 session" },
|
|
309
|
+
{ cmd: "/session_search", desc: "全局搜索 session" },
|
|
239
310
|
{ cmd: "/trusted", desc: "查看受信任工具" },
|
|
240
311
|
{ cmd: "/trust", desc: "永久信任工具" },
|
|
241
312
|
{ cmd: "/untrust", desc: "取消信任" },
|
|
@@ -253,29 +324,27 @@ function App() {
|
|
|
253
324
|
const [thinking, setThinking] = useState(false);
|
|
254
325
|
const [streaming, setStreaming] = useState("");
|
|
255
326
|
const [tools, setTools] = useState([]);
|
|
327
|
+
const [toolsCollapsed, setToolsCollapsed] = useState(false);
|
|
256
328
|
const [ctxPct, setCtxPct] = useState(0);
|
|
257
329
|
const [blocked, setBlocked] = useState(false);
|
|
258
330
|
const [slashIdx, setSlashIdx] = useState(0);
|
|
259
331
|
const [mascotFace, setMascotFace] = useState(kao.mascotForFrame());
|
|
260
332
|
const [promptFace, setPromptFace] = useState(kao.promptFace(""));
|
|
333
|
+
const [queue, setQueue] = useState([]);
|
|
334
|
+
const abortRef = useRef(null);
|
|
261
335
|
|
|
262
|
-
// 状态栏每 8 秒切换表情
|
|
263
336
|
useEffect(() => {
|
|
264
337
|
const timer = setInterval(() => setMascotFace(kao.mascotForFrame()), 1000);
|
|
265
338
|
return () => clearInterval(timer);
|
|
266
339
|
}, []);
|
|
267
340
|
|
|
268
|
-
// 输入框表情: 启动动画 + 根据输入内容实时匹配
|
|
269
341
|
useEffect(() => {
|
|
270
342
|
const timer = setInterval(() => setPromptFace(kao.promptFace(input)), 150);
|
|
271
343
|
return () => clearInterval(timer);
|
|
272
344
|
}, [input]);
|
|
273
|
-
const [msgs, setMsgs] = useState([
|
|
274
|
-
|
|
275
|
-
{ role: "system", content: "按 / 查看命令菜单 ↑↓选择 回车选取" },
|
|
276
|
-
]);
|
|
345
|
+
const [msgs, setMsgs] = useState([]);
|
|
346
|
+
const [started, setStarted] = useState(false);
|
|
277
347
|
|
|
278
|
-
// 启动时从 agent 获取真实上下文占比
|
|
279
348
|
useEffect(() => {
|
|
280
349
|
setCtxPct(getAgent().estimateContextPct());
|
|
281
350
|
}, []);
|
|
@@ -293,50 +362,80 @@ function App() {
|
|
|
293
362
|
setThinking(true);
|
|
294
363
|
setStreaming("");
|
|
295
364
|
setTools([]);
|
|
365
|
+
setToolsCollapsed(false);
|
|
366
|
+
|
|
367
|
+
const controller = new AbortController();
|
|
368
|
+
abortRef.current = controller;
|
|
296
369
|
|
|
297
370
|
let buf = "";
|
|
298
371
|
const toolMap = new Map();
|
|
299
372
|
|
|
300
373
|
try {
|
|
301
|
-
await
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
t
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
374
|
+
await Promise.race([
|
|
375
|
+
getAgent().run(query, {
|
|
376
|
+
onContent: (tok) => {
|
|
377
|
+
buf += tok;
|
|
378
|
+
setStreaming(buf.replace(/\*\*(.+?)\*\*/g, "$1"));
|
|
379
|
+
},
|
|
380
|
+
onToolCall: (name, args = {}) => {
|
|
381
|
+
const short = Object.entries(args).map(([k, v]) => {
|
|
382
|
+
const s = String(v);
|
|
383
|
+
return k + "=" + (s.length > 50 ? s.slice(0, 50) + "\u2026" : s);
|
|
384
|
+
}).join(" ");
|
|
385
|
+
toolMap.set(name, { name, args: short, status: "running", result: "" });
|
|
386
|
+
setTools([...toolMap.values()]);
|
|
387
|
+
},
|
|
388
|
+
onToolResult: (name, result) => {
|
|
389
|
+
const t = toolMap.get(name);
|
|
390
|
+
if (t) {
|
|
391
|
+
t.status = "done";
|
|
392
|
+
t.result = String(result || "").slice(0, 500);
|
|
393
|
+
}
|
|
394
|
+
setTools([...toolMap.values()]);
|
|
395
|
+
},
|
|
396
|
+
onContextPct: (pct) => setCtxPct(pct),
|
|
397
|
+
}),
|
|
398
|
+
new Promise((_, reject) => {
|
|
399
|
+
controller.signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")));
|
|
400
|
+
}),
|
|
401
|
+
]);
|
|
324
402
|
} catch (e) {
|
|
325
|
-
|
|
403
|
+
if (e.name === "AbortError" || e.message === "Aborted") {
|
|
404
|
+
addMsg("system", "\u23f9 已中断");
|
|
405
|
+
buf = "";
|
|
406
|
+
} else {
|
|
407
|
+
addMsg("system", "\u2717 错误: " + e.message);
|
|
408
|
+
}
|
|
326
409
|
}
|
|
327
410
|
|
|
328
411
|
if (buf) addMsg("assistant", formatMd(buf, cols - 4));
|
|
329
412
|
setStreaming("");
|
|
330
413
|
setThinking(false);
|
|
331
|
-
|
|
332
|
-
|
|
414
|
+
setToolsCollapsed(true);
|
|
415
|
+
abortRef.current = null;
|
|
416
|
+
|
|
417
|
+
// 处理队列中的下一条
|
|
418
|
+
setQueue(prev => {
|
|
419
|
+
if (prev.length <= 1) {
|
|
420
|
+
setBlocked(false);
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
// delay next to allow React to update
|
|
424
|
+
const [done, ...rest] = prev;
|
|
425
|
+
setTimeout(() => {
|
|
426
|
+
const next = rest[0];
|
|
427
|
+
addMsg("user", next);
|
|
428
|
+
run(next);
|
|
429
|
+
}, 50);
|
|
430
|
+
return rest;
|
|
431
|
+
});
|
|
333
432
|
}, [blocked, addMsg, cols]);
|
|
334
433
|
|
|
335
434
|
const slashRef = useRef({ filtered: [], clamped: 0 });
|
|
336
435
|
|
|
337
436
|
const handleSubmit = useCallback((val) => {
|
|
338
437
|
const v = val.trim();
|
|
339
|
-
if (!v
|
|
438
|
+
if (!v) return;
|
|
340
439
|
const { filtered, clamped } = slashRef.current;
|
|
341
440
|
if (filtered.length > 0 && clamped < filtered.length) {
|
|
342
441
|
const sel = filtered[clamped].cmd;
|
|
@@ -346,8 +445,21 @@ function App() {
|
|
|
346
445
|
return;
|
|
347
446
|
}
|
|
348
447
|
}
|
|
448
|
+
|
|
449
|
+
// 如果正在执行中,加入队列而非阻塞
|
|
450
|
+
if (blocked || thinking) {
|
|
451
|
+
if (v === "/reset" || v === "/clear" || v === "/exit" || v === "/help" || v === "/version" || v === "/status" || v === "/ctx") {
|
|
452
|
+
// 这些命令仍可直接执行(不阻塞)
|
|
453
|
+
} else {
|
|
454
|
+
setQueue(prev => [...prev, v]);
|
|
455
|
+
setInput("");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
349
460
|
addMsg("user", v);
|
|
350
461
|
setInput("");
|
|
462
|
+
setStarted(true);
|
|
351
463
|
|
|
352
464
|
const parts = v.slice(1).trim().split(/\s+/);
|
|
353
465
|
const cmd = parts[0].toLowerCase();
|
|
@@ -357,48 +469,54 @@ function App() {
|
|
|
357
469
|
|
|
358
470
|
if (v === "/help") {
|
|
359
471
|
say([
|
|
360
|
-
"
|
|
361
|
-
"
|
|
362
|
-
"
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"
|
|
367
|
-
"
|
|
368
|
-
"
|
|
369
|
-
"
|
|
370
|
-
"
|
|
371
|
-
"
|
|
372
|
-
"
|
|
373
|
-
"
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
"
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
"
|
|
380
|
-
"
|
|
381
|
-
"
|
|
382
|
-
"
|
|
383
|
-
"
|
|
384
|
-
"
|
|
385
|
-
"
|
|
386
|
-
"
|
|
387
|
-
"
|
|
388
|
-
"
|
|
389
|
-
"
|
|
390
|
-
"
|
|
391
|
-
"
|
|
392
|
-
"
|
|
393
|
-
"
|
|
472
|
+
"\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
473
|
+
"\u2502 /help 查看帮助 \u2502",
|
|
474
|
+
"\u2502 /exit 退出程序 \u2502",
|
|
475
|
+
"\u2502 /reset 重置对话 \u2502",
|
|
476
|
+
"\u2502 /clear 清除对话 \u2502",
|
|
477
|
+
"\u2502 /version 查看版本 \u2502",
|
|
478
|
+
"\u2502 /status 当前状态 \u2502",
|
|
479
|
+
"\u2502 /ctx 上下文使用量 \u2502",
|
|
480
|
+
"\u2502 /tools 列出工具 \u2502",
|
|
481
|
+
"\u2502 /tools_more 全部工具 \u2502",
|
|
482
|
+
"\u2502 /tool_search <q> 搜索工具 \u2502",
|
|
483
|
+
"\u2502 /tool_list_saved 列出持久化工具 \u2502",
|
|
484
|
+
"\u2502 /tool_save <name> 持久化保存工具 \u2502",
|
|
485
|
+
"\u2502 /tool_del <name> 删除持久化工具 \u2502",
|
|
486
|
+
"\u2502 /temp <0-2> 设置温度 \u2502",
|
|
487
|
+
"\u2502 /token <n> 设置maxTokens \u2502",
|
|
488
|
+
"\u2502 /memory 记忆统计 \u2502",
|
|
489
|
+
"\u2502 /memory_list [n] 列出记忆条目 \u2502",
|
|
490
|
+
"\u2502 /memory_search <q> 搜索记忆 \u2502",
|
|
491
|
+
"\u2502 /memory_del <id> 删除记忆 \u2502",
|
|
492
|
+
"\u2502 /memory_clear 清空记忆 \u2502",
|
|
493
|
+
"\u2502 /compress 手动压缩上下文 \u2502",
|
|
494
|
+
"\u2502 /history [n] 最近历史 \u2502",
|
|
495
|
+
"\u2502 /history files 历史文件列表 \u2502",
|
|
496
|
+
"\u2502 /history search <q>搜索历史 \u2502",
|
|
497
|
+
"\u2502 /history read <f> 读取历史文件 \u2502",
|
|
498
|
+
"\u2502 /sessions [n] 列出对话session\u2502",
|
|
499
|
+
"\u2502 /session <id> 查看session \u2502",
|
|
500
|
+
"\u2502 /session_search <q>全局搜索session\u2502",
|
|
501
|
+
"\u2502 /trusted 受信任工具 \u2502",
|
|
502
|
+
"\u2502 /trust <name> 永久信任工具 \u2502",
|
|
503
|
+
"\u2502 /untrust <name> 取消信任 \u2502",
|
|
504
|
+
"\u2502 /api 查看API配置 \u2502",
|
|
505
|
+
"\u2502 /api key <K> 设置Key \u2502",
|
|
506
|
+
"\u2502 /api url <U> 设置地址 \u2502",
|
|
507
|
+
"\u2502 /api model <M> 设置模型 \u2502",
|
|
508
|
+
"\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
394
509
|
].join("\n"));
|
|
395
510
|
}
|
|
396
511
|
else if (cmd === "exit") process.exit(0);
|
|
397
512
|
else if (cmd === "reset") {
|
|
398
|
-
getAgent()
|
|
399
|
-
|
|
513
|
+
const a = getAgent();
|
|
514
|
+
endSession(a.sessionId);
|
|
515
|
+
a.reset();
|
|
516
|
+
a.sessionId = startSession();
|
|
517
|
+
setMsgs([{ role: "system", content: "对话已重置,新 session 已创建。" }]);
|
|
400
518
|
setCtxPct(0);
|
|
401
|
-
say("
|
|
519
|
+
say("对话已重置, 新 session 已创建");
|
|
402
520
|
}
|
|
403
521
|
else if (cmd === "clear") {
|
|
404
522
|
setMsgs([{ role: "system", content: "对话已清除。" }]);
|
|
@@ -414,13 +532,13 @@ function App() {
|
|
|
414
532
|
const pct = a.estimateContextPct();
|
|
415
533
|
const names = Tools.listToolNames();
|
|
416
534
|
say([
|
|
417
|
-
"
|
|
535
|
+
"\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
418
536
|
" 模型: " + CONFIG.llm.model + " 温度: " + CONFIG.llm.temperature,
|
|
419
537
|
" Tokens: 输入" + usage.prompt + " · 输出" + usage.completion,
|
|
420
538
|
" 上下文: " + pct + "% (上限 " + a.getMaxContextTokens() + " tokens)",
|
|
421
|
-
" 记忆: " + mem.entries + "
|
|
539
|
+
" 记忆: ROM" + (mem.romEntries || mem.entries) + " RAM" + (mem.ramEntries || 0) + " · 历史 " + mem.historyMessages + " 轮",
|
|
422
540
|
" 工具: " + names.length + " 个 — " + names.slice(0, 8).join(", ") + (names.length > 8 ? " ..." : ""),
|
|
423
|
-
"
|
|
541
|
+
"\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
424
542
|
].join("\n"));
|
|
425
543
|
}
|
|
426
544
|
else if (cmd === "ctx") {
|
|
@@ -430,7 +548,7 @@ function App() {
|
|
|
430
548
|
const barW = 30;
|
|
431
549
|
const filled = Math.max(1, Math.round(pct / 100 * barW));
|
|
432
550
|
const empty = barW - filled;
|
|
433
|
-
const bar = "
|
|
551
|
+
const bar = "\u2588".repeat(filled) + "\u2500".repeat(empty);
|
|
434
552
|
say([
|
|
435
553
|
"上下文: [" + bar + "] " + pct + "%",
|
|
436
554
|
"上限: " + a.getMaxContextTokens() + " tokens",
|
|
@@ -440,13 +558,16 @@ function App() {
|
|
|
440
558
|
}
|
|
441
559
|
else if (cmd === "memory") {
|
|
442
560
|
const s = getAgent().memory.stats();
|
|
443
|
-
say("记忆: " + s.entries + "
|
|
561
|
+
say("记忆: ROM" + (s.romEntries || s.entries) + " RAM" + (s.ramEntries || 0) + " · 历史消息 " + s.historyMessages + " 轮");
|
|
444
562
|
}
|
|
445
563
|
else if (cmd === "memory_list") {
|
|
446
564
|
const n = parseInt(rest) || 20;
|
|
447
565
|
const all = getAgent().memory.getAllEntries().slice(-n);
|
|
448
566
|
if (!all.length) { say("(无记忆条目)"); return; }
|
|
449
|
-
say(all.map((e) =>
|
|
567
|
+
say(all.map((e) => {
|
|
568
|
+
const tag = e._type === "ram" ? "[RAM]" : "[ROM]";
|
|
569
|
+
return tag + " #" + e.id + " [" + (e.tags?.join(",") || "-") + "] " + e.text;
|
|
570
|
+
}).join("\n"));
|
|
450
571
|
}
|
|
451
572
|
else if (cmd === "memory_search") {
|
|
452
573
|
if (!rest) { say("用法: /memory_search <关键词>"); return; }
|
|
@@ -461,15 +582,15 @@ function App() {
|
|
|
461
582
|
say(ok ? "已删除 #" + id : "不存在 #" + id);
|
|
462
583
|
}
|
|
463
584
|
else if (cmd === "memory_clear") {
|
|
464
|
-
getAgent().memory.
|
|
465
|
-
say("记忆已全部清空");
|
|
585
|
+
getAgent().memory.clearAllEntries();
|
|
586
|
+
say("记忆已全部清空 (ROM+RAM)");
|
|
466
587
|
}
|
|
467
588
|
else if (cmd === "compress") {
|
|
468
589
|
const a = getAgent();
|
|
469
590
|
const compressed = a.memory.compressHistory();
|
|
470
591
|
if (!compressed) { say("对话太短, 无需压缩"); return; }
|
|
471
592
|
a.memory.clear();
|
|
472
|
-
a.memory.
|
|
593
|
+
a.memory.addRamEntry("手动压缩: " + compressed.slice(0, 180), ["manual-summary"]);
|
|
473
594
|
say("上下文已压缩, 历史清空, 摘要已存入记忆");
|
|
474
595
|
}
|
|
475
596
|
else if (cmd === "tools") {
|
|
@@ -542,6 +663,59 @@ function App() {
|
|
|
542
663
|
say(turns.map((t, i) => (i + 1) + ". [" + (t.time?.slice(0, 16) || "?") + "] " + (t.user || "").slice(0, 120)).join("\n"));
|
|
543
664
|
}
|
|
544
665
|
}
|
|
666
|
+
else if (cmd === "sessions") {
|
|
667
|
+
const n = parseInt(rest) || 20;
|
|
668
|
+
const sessions = listSessions(n);
|
|
669
|
+
if (!sessions.length) { say("(暂无对话 session)"); return; }
|
|
670
|
+
const lines = sessions.map((s, i) => {
|
|
671
|
+
const marker = s.status === "active" ? "\u25cf" : "\u25cb";
|
|
672
|
+
return (i + 1) + ". " + marker + " [" + (s.started || "?").slice(0, 16) + "] " +
|
|
673
|
+
(s.title || "(无标题)") + " | " + s.turnCount + "轮" +
|
|
674
|
+
(s.status === "active" ? " \u25c0 当前" : "");
|
|
675
|
+
});
|
|
676
|
+
say("对话 Sessions (" + sessions.length + "):\n" + lines.join("\n") +
|
|
677
|
+
"\n\n用 /session <编号> 查看详情, /session_search <关键词> 搜索");
|
|
678
|
+
}
|
|
679
|
+
else if (cmd === "session") {
|
|
680
|
+
if (!rest) { say("用法: /session <session_id 或列表序号>\n请先用 /sessions 查看列表"); return; }
|
|
681
|
+
const sessions = listSessions(999);
|
|
682
|
+
let sid = rest.trim();
|
|
683
|
+
if (/^\d+$/.test(sid)) {
|
|
684
|
+
const idx = parseInt(sid) - 1;
|
|
685
|
+
if (idx < 0 || idx >= sessions.length) { say("序号超出范围 (1-" + sessions.length + ")"); return; }
|
|
686
|
+
sid = sessions[idx].id;
|
|
687
|
+
}
|
|
688
|
+
const session = getSession(sid);
|
|
689
|
+
if (!session) { say("未找到 session: " + sid); return; }
|
|
690
|
+
const turns = loadSessionTurns(sid, 30);
|
|
691
|
+
const header = "=== " + (session.title || "(无标题)") + " ===\n" +
|
|
692
|
+
"ID: " + session.id + " | " + (session.started || "?").slice(0, 16) + " | " + session.turnCount + "轮\n";
|
|
693
|
+
if (!turns.length) { say(header + "\n(这个 session 暂无对话轮次)"); return; }
|
|
694
|
+
const body = turns.map((t, i) => {
|
|
695
|
+
return "[" + (i + 1) + "] " + (t.time || "?").slice(0, 16) + "\n" +
|
|
696
|
+
" 问: " + (t.user || "").slice(0, 300) + "\n" +
|
|
697
|
+
" 答: " + (t.assistant || "").slice(0, 300);
|
|
698
|
+
}).join("\n\n");
|
|
699
|
+
say(header + "\n" + body + "\n\n(共 " + turns.length + " 轮, 显示最近 30 轮)");
|
|
700
|
+
}
|
|
701
|
+
else if (cmd === "session_search") {
|
|
702
|
+
if (!rest) { say("用法: /session_search <关键词> [数量]\n在整个历史中搜索相关 session"); return; }
|
|
703
|
+
const parts = rest.trim().split(/\s+/);
|
|
704
|
+
const limit = parseInt(parts[parts.length - 1]) || 10;
|
|
705
|
+
const query = isNaN(parseInt(parts[parts.length - 1])) ? rest.trim() : parts.slice(0, -1).join(" ");
|
|
706
|
+
const results = globalSearch(query, limit);
|
|
707
|
+
if (!results.length) { say("全史搜索未找到 \"" + query + "\""); return; }
|
|
708
|
+
const lines = [];
|
|
709
|
+
for (const [i, r] of results.entries()) {
|
|
710
|
+
lines.push((i + 1) + ". [" + (r.sessionStarted || "?").slice(0, 16) + "] " +
|
|
711
|
+
r.sessionTitle + " | " + r.matchCount + "处匹配 | 得分" + r.totalScore);
|
|
712
|
+
for (const m of r.topMatches) {
|
|
713
|
+
lines.push(" · " + (m.user || "").slice(0, 100));
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
say("搜索 \"" + query + "\" — " + results.length + " 个 session:\n" + lines.join("\n") +
|
|
717
|
+
"\n\n用 /session <编号> 查看详情");
|
|
718
|
+
}
|
|
545
719
|
else if (cmd === "trusted") {
|
|
546
720
|
const trusted = CONFIG.tools?.trustedTools || [];
|
|
547
721
|
if (!trusted.length) { say("(暂无受信任工具)"); return; }
|
|
@@ -590,7 +764,7 @@ function App() {
|
|
|
590
764
|
}
|
|
591
765
|
}
|
|
592
766
|
else { run(v); }
|
|
593
|
-
}, [thinking, addMsg, run]);
|
|
767
|
+
}, [thinking, blocked, addMsg, run]);
|
|
594
768
|
|
|
595
769
|
const slashInput = input.startsWith("/");
|
|
596
770
|
const slashFiltered = slashInput
|
|
@@ -599,7 +773,17 @@ function App() {
|
|
|
599
773
|
const slashClamped = Math.max(0, Math.min(slashIdx, slashFiltered.length - 1));
|
|
600
774
|
slashRef.current = { filtered: slashFiltered, clamped: slashClamped };
|
|
601
775
|
|
|
602
|
-
useInput((
|
|
776
|
+
useInput((inputVal, key) => {
|
|
777
|
+
if (key.ctrl && inputVal === "c") {
|
|
778
|
+
if (abortRef.current) {
|
|
779
|
+
abortRef.current.abort();
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (input.length > 0) {
|
|
783
|
+
setInput("");
|
|
784
|
+
}
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
603
787
|
if (key.escape) process.exit(0);
|
|
604
788
|
if (!slashInput || !slashFiltered.length) return;
|
|
605
789
|
if (key.upArrow) setSlashIdx((slashClamped - 1 + slashFiltered.length) % slashFiltered.length);
|
|
@@ -607,7 +791,34 @@ function App() {
|
|
|
607
791
|
});
|
|
608
792
|
|
|
609
793
|
return (
|
|
610
|
-
<Box
|
|
794
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
795
|
+
{!started ? (
|
|
796
|
+
<Box flexDirection="column" flexShrink={0}>
|
|
797
|
+
{/* 启动页: LOGO + 欢迎框 */}
|
|
798
|
+
<Box flexDirection="row">
|
|
799
|
+
{/* 左边 LOGO */}
|
|
800
|
+
<Box flexDirection="column" paddingRight={2}>
|
|
801
|
+
{LOGO_LINES.map((line, i) => (
|
|
802
|
+
<Text key={i} color="cyanBright">{line}</Text>
|
|
803
|
+
))}
|
|
804
|
+
<Text color="green">{CONFIG.llm.model} · {CONFIG.llm.contextWindow || 131072} tokens</Text>
|
|
805
|
+
</Box>
|
|
806
|
+
{/* 右边欢迎框 */}
|
|
807
|
+
<Box borderStyle="round" borderColor="cyan" flexDirection="column" paddingX={2} paddingY={1}>
|
|
808
|
+
<Text color="cyanBright" bold>Clinn v{VER}</Text>
|
|
809
|
+
<Text color="magenta" dimColor>Self-Evolving AI · Terminal Agent · Ink</Text>
|
|
810
|
+
<Text> </Text>
|
|
811
|
+
<Text dimColor>Terminal-native AI coding assistant</Text>
|
|
812
|
+
<Text> </Text>
|
|
813
|
+
<Text dimColor>/ command menu · ↑↓ select · Enter confirm</Text>
|
|
814
|
+
</Box>
|
|
815
|
+
</Box>
|
|
816
|
+
<Box marginTop={1}>
|
|
817
|
+
<Text color="gray" dimColor>{"─".repeat(cols - 2)}</Text>
|
|
818
|
+
</Box>
|
|
819
|
+
</Box>
|
|
820
|
+
) : (
|
|
821
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
611
822
|
<Box flexDirection="column" flexShrink={0}>
|
|
612
823
|
{LOGO_LINES.map((line, i) => (
|
|
613
824
|
<Text key={i} color="cyanBright">{line}</Text>
|
|
@@ -615,16 +826,21 @@ function App() {
|
|
|
615
826
|
<Box marginTop={1}>
|
|
616
827
|
<Text color="magenta" dimColor>Self-Evolving AI · Terminal Agent · Ink</Text>
|
|
617
828
|
</Box>
|
|
618
|
-
<Text color="gray">{"─".repeat(cols -
|
|
829
|
+
<Text color="gray" dimColor>{"─".repeat(cols - 2)}</Text>
|
|
619
830
|
</Box>
|
|
620
831
|
|
|
621
|
-
<Box flexDirection="column"
|
|
832
|
+
<Box flexDirection="column">
|
|
622
833
|
{msgs.map((m, i) => (
|
|
623
834
|
<Msg key={i} role={m.role} content={m.content} />
|
|
624
835
|
))}
|
|
625
836
|
{streaming ? <Streaming content={streaming} /> : null}
|
|
626
837
|
{thinking && !streaming ? <Thinking /> : null}
|
|
627
|
-
|
|
838
|
+
{tools.length > 0 && (
|
|
839
|
+
<Box flexDirection="column">
|
|
840
|
+
<Text color="gray" dimColor>{"\u2500\u2500\u2500".repeat(8)}</Text>
|
|
841
|
+
<ToolLog tools={tools} collapsed={toolsCollapsed} />
|
|
842
|
+
</Box>
|
|
843
|
+
)}
|
|
628
844
|
</Box>
|
|
629
845
|
|
|
630
846
|
{slashInput && slashFiltered.length > 0 && (
|
|
@@ -633,20 +849,22 @@ function App() {
|
|
|
633
849
|
const sel = i === slashClamped;
|
|
634
850
|
return (
|
|
635
851
|
<Box key={c.cmd} paddingLeft={1}>
|
|
636
|
-
<Text color={sel ? "cyan" : "gray"}>{sel ? "
|
|
852
|
+
<Text color={sel ? "cyan" : "gray"}>{sel ? "\u25b8 " : " "}</Text>
|
|
637
853
|
<Text color={sel ? "cyanBright" : undefined} bold={sel}>{c.cmd}</Text>
|
|
638
|
-
<Text color="gray">{"
|
|
854
|
+
<Text color="gray">{" \u00b7 " + c.desc}</Text>
|
|
639
855
|
</Box>
|
|
640
856
|
);
|
|
641
857
|
})}
|
|
642
858
|
<Box paddingLeft={1}>
|
|
643
|
-
<Text color="gray">
|
|
859
|
+
<Text color="gray"> \u2191\u2193 切换 \u00b7 回车 选取</Text>
|
|
644
860
|
</Box>
|
|
645
861
|
</Box>
|
|
646
862
|
)}
|
|
647
863
|
|
|
864
|
+
</Box>
|
|
865
|
+
)}
|
|
648
866
|
<Box flexDirection="column" flexShrink={0}>
|
|
649
|
-
<Text color="gray">{"─".repeat(cols -
|
|
867
|
+
<Text color="gray" dimColor>{"─".repeat(cols - 2)}</Text>
|
|
650
868
|
<Box paddingX={1}>
|
|
651
869
|
<Text dimColor>
|
|
652
870
|
<Text color="cyan">{mascotFace}</Text> {CONFIG.llm.model}
|
|
@@ -659,20 +877,60 @@ function App() {
|
|
|
659
877
|
</Text>
|
|
660
878
|
</Box>
|
|
661
879
|
|
|
662
|
-
|
|
663
|
-
<Box
|
|
664
|
-
<Text color="
|
|
880
|
+
{queue.length > 0 && (
|
|
881
|
+
<Box paddingX={1} paddingBottom={1}>
|
|
882
|
+
<Text color="yellow" dimColor>
|
|
883
|
+
{"│ "}队列: {queue.length} 条待执行
|
|
884
|
+
{queue.slice(0, 3).map((q, i) => (
|
|
885
|
+
<Text key={i} color="gray">{" · "}{q.slice(0, 30)}{q.length > 30 ? "…" : ""}</Text>
|
|
886
|
+
))}
|
|
887
|
+
{queue.length > 3 ? <Text color="gray"> ...</Text> : null}
|
|
888
|
+
</Text>
|
|
889
|
+
</Box>
|
|
890
|
+
)}
|
|
891
|
+
|
|
892
|
+
<Text color="cyan">{"─".repeat(cols - 2)}</Text>
|
|
893
|
+
|
|
894
|
+
{input.length > 200 && (
|
|
895
|
+
<Box paddingX={1}>
|
|
896
|
+
<Text color="gray" dimColor> [{input.length} chars]</Text>
|
|
897
|
+
</Box>
|
|
898
|
+
)}
|
|
899
|
+
|
|
900
|
+
<Box paddingY={0} flexDirection="row">
|
|
901
|
+
<Text color="greenBright" bold>{promptFace} </Text>
|
|
902
|
+
<Box flexGrow={1}>
|
|
665
903
|
<TextInput
|
|
666
|
-
value={input}
|
|
667
|
-
onChange={v => {
|
|
904
|
+
value={input.length > 500 ? "…" + input.slice(-300) : input}
|
|
905
|
+
onChange={v => {
|
|
906
|
+
setSlashIdx(0);
|
|
907
|
+
if (v.startsWith("…")) {
|
|
908
|
+
setInput(input.slice(0, Math.max(0, input.length - 300)) + v.slice(1));
|
|
909
|
+
} else {
|
|
910
|
+
setInput(v);
|
|
911
|
+
}
|
|
912
|
+
}}
|
|
668
913
|
onSubmit={handleSubmit}
|
|
669
|
-
placeholder={thinking ? "
|
|
914
|
+
placeholder={thinking ? "thinking..." : "> "}
|
|
670
915
|
/>
|
|
671
916
|
</Box>
|
|
672
917
|
</Box>
|
|
918
|
+
|
|
919
|
+
<Text color="cyan">{"─".repeat(cols - 2)}</Text>
|
|
673
920
|
</Box>
|
|
674
921
|
</Box>
|
|
675
922
|
);
|
|
676
923
|
}
|
|
677
924
|
|
|
678
925
|
const { waitUntilExit } = render(<App />);
|
|
926
|
+
|
|
927
|
+
// 拦截 SIGINT:Ctrl+C 由 Ink useInput 处理,不直接退出
|
|
928
|
+
let sigintCount = 0;
|
|
929
|
+
process.on("SIGINT", () => {
|
|
930
|
+
sigintCount++;
|
|
931
|
+
if (sigintCount >= 3) {
|
|
932
|
+
process.exit(0);
|
|
933
|
+
}
|
|
934
|
+
// 单次/双次 Ctrl+C 不退出,交给 Ink 的 useInput 处理
|
|
935
|
+
setTimeout(() => { sigintCount = 0; }, 1000);
|
|
936
|
+
});
|