thincoder 0.4.0 → 0.6.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/README.md +90 -46
- package/bin/thincoder.mjs +20 -16
- package/package.json +2 -2
- package/src/SYSTEM_PROMPT.md +7 -6
- package/src/agent.mjs +356 -59
- package/src/coder-overlay.md +2 -1
- package/src/config.mjs +54 -25
- package/src/context.mjs +128 -48
- package/src/explore-overlay.md +6 -2
- package/src/main-overlay.md +8 -0
- package/src/memory.mjs +653 -4
- package/src/plan-overlay.md +13 -0
- package/src/provider.mjs +74 -7
- package/src/repomap.mjs +204 -0
- package/src/session.mjs +142 -15
- package/src/skills.mjs +2 -1
- package/src/tools/bash.md +2 -0
- package/src/tools/glob.md +1 -1
- package/src/tools.mjs +63 -13
- package/src/tui.mjs +245 -43
package/src/tui.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { basename } from "node:path"
|
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs"
|
|
11
11
|
import { runAgent, ContinueError } from "./agent.mjs"
|
|
12
12
|
import { estimateTokens } from "./context.mjs"
|
|
13
|
-
import { saveSession, clearSession } from "./session.mjs"
|
|
13
|
+
import { saveSession, clearSession, archiveCurrent, listSlots, switchToSlot, sessionPath } from "./session.mjs"
|
|
14
14
|
import { PROVIDER_PRESETS as PRESETS } from "./config.mjs"
|
|
15
15
|
import { closeAllMcp } from "./mcp.mjs"
|
|
16
16
|
|
|
@@ -207,6 +207,21 @@ export function layoutInput(chars, cursor, width) {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
/** 文本按宽度折行(保留 \n),返回行数组 */
|
|
210
|
+
/**
|
|
211
|
+
* 显示净化:控制字符会破坏终端网格数学(\r 回车覆盖、\t 宽度误判致整帧错位、ANSI/响铃冲屏)。
|
|
212
|
+
* 只动显示层——模型看到的工具结果原文不变;session 里已存的脏 display 回放时也经此净化。
|
|
213
|
+
*/
|
|
214
|
+
const ANSI_SEQUENCE_RE = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][0-9A-B]|\x1b[=>#][0-9]?/g
|
|
215
|
+
export function sanitizeDisplay(s) {
|
|
216
|
+
return s
|
|
217
|
+
.replace(ANSI_SEQUENCE_RE, "")
|
|
218
|
+
.replace(/\r\n/g, "\n")
|
|
219
|
+
.replace(/\r/g, "\n")
|
|
220
|
+
.replace(/\t/g, " ")
|
|
221
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
|
222
|
+
.replace(/\n+$/, "")
|
|
223
|
+
}
|
|
224
|
+
|
|
210
225
|
export function wrapText(text, width) {
|
|
211
226
|
const lines = []
|
|
212
227
|
for (const rawLine of text.split("\n")) {
|
|
@@ -250,20 +265,28 @@ export async function startTUI(agent, opts = {}) {
|
|
|
250
265
|
processing: false,
|
|
251
266
|
controller: null, // AbortController for current agent run
|
|
252
267
|
permission: null, // { name, args, resolve }
|
|
268
|
+
permissionPreview: [], // 权限审批的内容预览行(渲染在输入框上方,不分隔)
|
|
253
269
|
question: null, // { text, options, resolve } — agent 的 question 工具回调
|
|
254
270
|
picker: null, // 模型选择器 { entries, lines, index, scroll, selectedLine }
|
|
255
271
|
wizard: null, // 首次配置向导 { step, index, scroll, selectedLine, fields, error, lines }
|
|
256
|
-
tasks: agent.tasks ?? [], // task
|
|
272
|
+
tasks: agent.tasks ?? [], // task 工具的任务列表(状态栏显示进度);会话恢复时直接带上,全完成自动收起
|
|
257
273
|
tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }, // 累计 token 用量(状态栏显示)
|
|
258
274
|
ctxCache: { len: -1, tokens: 0 }, // 上下文占用估算缓存(estimateTokens 是 O(n),history 变长才重算)
|
|
259
275
|
reasoning: "", // 思考流缓冲(暗色展示)
|
|
260
276
|
completion: null, // Tab 补全状态 { candidates, index }
|
|
261
277
|
toolStreams: {}, // 各工具的实时输出(按工具名隔离,并行工具互不串扰)
|
|
278
|
+
subOutput: "", // 子 agent 流式输出(滚动显示,最长保留末尾 300 字符)
|
|
279
|
+
currentSub: null, // 当前活跃的子 agent 角色名
|
|
262
280
|
currentTool: null, // 正在执行的工具名(状态栏显示)
|
|
263
281
|
processingStarted: 0, // 本轮处理开始时间(状态栏计时)
|
|
264
282
|
status: "Ready",
|
|
265
283
|
}
|
|
266
284
|
|
|
285
|
+
// 恢复的会话如果所有任务已完成,自动收起 todo 面板(对齐运行时行为)
|
|
286
|
+
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
287
|
+
state.tasks = []
|
|
288
|
+
}
|
|
289
|
+
|
|
267
290
|
// 输入流先过一道滤网:鼠标序列(滚轮)在这里拦截处理,剥净后才交给 keypress 解析,
|
|
268
291
|
// 防止序列残片(如 "64;72;42M")漏进输入框
|
|
269
292
|
const keyStream = new PassThrough()
|
|
@@ -301,10 +324,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
301
324
|
if (text) keyStream.write(text)
|
|
302
325
|
})
|
|
303
326
|
|
|
327
|
+
let cleanedUp = false
|
|
304
328
|
const cleanup = () => {
|
|
305
|
-
|
|
329
|
+
if (cleanedUp) return
|
|
330
|
+
cleanedUp = true
|
|
331
|
+
// 退出前保存会话(同步写);先归档当前到槽位,再落新——不丢
|
|
306
332
|
try {
|
|
307
|
-
|
|
333
|
+
archiveCurrent(agent.cwd)
|
|
334
|
+
saveSession(agent, state.lines)
|
|
308
335
|
} catch {
|
|
309
336
|
// 存失败不耽误退出
|
|
310
337
|
}
|
|
@@ -405,12 +432,16 @@ export async function startTUI(agent, opts = {}) {
|
|
|
405
432
|
visibleTasks = [...inProgress, ...pending, ...done].slice(0, MAX_TASK_LINES)
|
|
406
433
|
}
|
|
407
434
|
const taskPanelH = visibleTasks.length
|
|
408
|
-
|
|
435
|
+
// 子 agent 流式输出占位(显示时占最多 2 行)
|
|
436
|
+
const subOutLen = (state.subOutput && state.processing) ? wrapText(state.subOutput, W - 8).slice(-2).length : 0
|
|
437
|
+
// 权限预览占位
|
|
438
|
+
const permPreviewLen = state.permission ? 1 + state.permissionPreview.reduce((s, l) => s + wrapText(` ${l}`, W - 1).length, 0) : 0
|
|
439
|
+
const convH = Math.max(1, rows - headerH - inputBoxH - statusH - pickerH - taskPanelH - subOutLen - permPreviewLen)
|
|
409
440
|
|
|
410
441
|
// 对话区内容行(含流式缓冲);markdown 表格先按显示宽度重排
|
|
411
442
|
const convLines = []
|
|
412
443
|
for (const l of state.lines) {
|
|
413
|
-
for (const line of formatTables(l.text, cols - 1)) {
|
|
444
|
+
for (const line of formatTables(sanitizeDisplay(l.text), cols - 1)) {
|
|
414
445
|
for (const wrapped of wrapText(line, cols - 1)) {
|
|
415
446
|
convLines.push({ text: wrapped, color: l.color })
|
|
416
447
|
}
|
|
@@ -418,12 +449,12 @@ export async function startTUI(agent, opts = {}) {
|
|
|
418
449
|
}
|
|
419
450
|
// 思考流(暗色)在正文流之前
|
|
420
451
|
if (state.reasoning) {
|
|
421
|
-
for (const wrapped of wrapText(state.reasoning, cols - 1)) {
|
|
452
|
+
for (const wrapped of wrapText(sanitizeDisplay(state.reasoning), cols - 1)) {
|
|
422
453
|
convLines.push({ text: wrapped, color: C.reason })
|
|
423
454
|
}
|
|
424
455
|
}
|
|
425
456
|
if (state.streaming) {
|
|
426
|
-
for (const line of formatTables(state.streaming, cols - 1)) {
|
|
457
|
+
for (const line of formatTables(sanitizeDisplay(state.streaming), cols - 1)) {
|
|
427
458
|
for (const wrapped of wrapText(line, cols - 1)) {
|
|
428
459
|
convLines.push({ text: wrapped, color: C.text })
|
|
429
460
|
}
|
|
@@ -432,7 +463,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
432
463
|
// 工具实时输出(暗色,只保留末尾防刷屏;按工具名隔离防止并行工具串扰)
|
|
433
464
|
const allStreams = Object.values(state.toolStreams).join("")
|
|
434
465
|
if (allStreams) {
|
|
435
|
-
const tail = allStreams.slice(-4000)
|
|
466
|
+
const tail = sanitizeDisplay(allStreams.slice(-4000))
|
|
436
467
|
for (const wrapped of wrapText(tail, cols - 1)) {
|
|
437
468
|
convLines.push({ text: wrapped, color: C.dim })
|
|
438
469
|
}
|
|
@@ -479,6 +510,25 @@ export async function startTUI(agent, opts = {}) {
|
|
|
479
510
|
out.push(`${color} ${mark} ${sliceByWidth(t.title, cols - 4)}${ansi.reset}${ansi.clearLine}`)
|
|
480
511
|
}
|
|
481
512
|
|
|
513
|
+
// 子 agent 流式输出(最多 2 行,滚动显示最新内容)
|
|
514
|
+
if (state.subOutput && state.processing) {
|
|
515
|
+
const lines = wrapText(state.subOutput, W - 8)
|
|
516
|
+
const tail = lines.slice(-2)
|
|
517
|
+
for (const l of tail) {
|
|
518
|
+
out.push(`${C.dim}[${state.currentSub}] ${l}${ansi.reset}${ansi.clearLine}`)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 权限审批内容预览(黄色,紧挨输入框上方)
|
|
523
|
+
if (state.permission) {
|
|
524
|
+
out.push(`${ansi.bold}${C.warn}❯ 权限请求${ansi.reset}${ansi.clearLine}`)
|
|
525
|
+
for (const line of state.permissionPreview) {
|
|
526
|
+
for (const wrapped of wrapText(` ${line}`, W - 1)) {
|
|
527
|
+
out.push(`${C.warn}${wrapped}${ansi.reset}${ansi.clearLine}`)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
482
532
|
// 输入框(全边框,宽 W)
|
|
483
533
|
let borderColor = C.tool
|
|
484
534
|
let title
|
|
@@ -552,7 +602,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
552
602
|
}
|
|
553
603
|
} else {
|
|
554
604
|
const taskHint = state.tasks.length > 0
|
|
555
|
-
? ` │
|
|
605
|
+
? ` │ ✓${state.tasks.filter((t) => t.status === "done").length}/${state.tasks.length}`
|
|
556
606
|
: ""
|
|
557
607
|
// token 用量:↑输入 ↓输出 + 缓存命中率(DeepSeek usage 带 prompt_cache_hit/miss_tokens)
|
|
558
608
|
const tk = state.tokens
|
|
@@ -595,7 +645,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
595
645
|
if (state.processing || state.permission || state.question || state.picker || state.wizard?.step === "provider") {
|
|
596
646
|
process.stdout.write(ansi.hideCursor)
|
|
597
647
|
} else {
|
|
598
|
-
const cursorRow = 1 + convH + taskPanelH + 2 + (layout.cursorLine - inputOffset) // header + 对话区 + todo 面板 + 上边框 + 行偏移
|
|
648
|
+
const cursorRow = 1 + convH + pickerH + taskPanelH + 2 + (layout.cursorLine - inputOffset) // header + 对话区 + todo 面板 + 上边框 + 行偏移
|
|
599
649
|
const cursorCol = 3 + layout.cursorCol // 左边框 + 空格 + 文本偏移(1 基)
|
|
600
650
|
process.stdout.write(`${ESC}[${cursorRow};${cursorCol}H${ansi.showCursor}`)
|
|
601
651
|
}
|
|
@@ -647,6 +697,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
647
697
|
|
|
648
698
|
const callbacks = {
|
|
649
699
|
onToken: (t) => {
|
|
700
|
+
// 子 agent 流式输出:前缀匹配 explore/coder/plan 的 token 进 subOutput
|
|
701
|
+
const subMatch = t.match(/^(explore|coder|plan)\//)
|
|
702
|
+
if (subMatch) {
|
|
703
|
+
state.currentSub = subMatch[1]
|
|
704
|
+
state.subOutput = (state.subOutput + t.slice(subMatch[0].length)).slice(-300)
|
|
705
|
+
scheduleRender()
|
|
706
|
+
return
|
|
707
|
+
}
|
|
650
708
|
ensureAssistantLabel()
|
|
651
709
|
state.streaming += t
|
|
652
710
|
scheduleRender()
|
|
@@ -664,14 +722,27 @@ export async function startTUI(agent, opts = {}) {
|
|
|
664
722
|
},
|
|
665
723
|
onToolResult: (name, result) => {
|
|
666
724
|
state.currentTool = null
|
|
725
|
+
// 子 agent 结束:清空流式缓冲,报告进对话区
|
|
726
|
+
const isSubagent = name.startsWith("explore/") || name.startsWith("coder/") || name.startsWith("plan/")
|
|
727
|
+
if (isSubagent) {
|
|
728
|
+
state.subOutput = ""
|
|
729
|
+
state.currentSub = null
|
|
730
|
+
// 子 agent 报告摘要(最多 8 行)直接展示在对话区
|
|
731
|
+
const lines = result.split("\n")
|
|
732
|
+
const preview = lines.slice(0, 8).map((l) => l.slice(0, 120)).join("\n")
|
|
733
|
+
if (preview) pushLine(preview, C.dim)
|
|
734
|
+
if (lines.length > 8) pushLine(` ... (${lines.length - 8} more lines)`, C.dim)
|
|
735
|
+
}
|
|
667
736
|
const stream = state.toolStreams[name]
|
|
668
737
|
if (stream) {
|
|
669
738
|
const tail = stream.trimEnd().slice(-4000)
|
|
670
739
|
if (tail) pushLine(tail, C.dim)
|
|
671
740
|
delete state.toolStreams[name]
|
|
672
741
|
}
|
|
673
|
-
|
|
674
|
-
|
|
742
|
+
if (!isSubagent) {
|
|
743
|
+
const first = result.split("\n")[0]
|
|
744
|
+
pushLine(` [done] ${name} → ${sliceByWidth(first, 100)}`, C.dim)
|
|
745
|
+
}
|
|
675
746
|
},
|
|
676
747
|
onToolOutput: (name, chunk) => {
|
|
677
748
|
state.toolStreams[name] = (state.toolStreams[name] ?? "") + chunk
|
|
@@ -696,6 +767,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
696
767
|
pushLine(` [task] ${done}/${items.length}${current ? ` ▶ ${current.title}` : ""}`, C.dim)
|
|
697
768
|
render()
|
|
698
769
|
},
|
|
770
|
+
// 增量保存:每 5 个工具 turn 落一次盘,中途崩溃丢失窗口从一整轮缩到几轮
|
|
771
|
+
onTurnEnd: (() => {
|
|
772
|
+
let n = 0
|
|
773
|
+
return () => {
|
|
774
|
+
if (++n % 5 !== 0) return
|
|
775
|
+
try { saveSession(agent, state.lines) } catch {}
|
|
776
|
+
}
|
|
777
|
+
})(),
|
|
699
778
|
}
|
|
700
779
|
|
|
701
780
|
for (let resume = false; ; resume = true) {
|
|
@@ -747,7 +826,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
747
826
|
}
|
|
748
827
|
// 每轮结束后保存会话(崩溃也不丢)
|
|
749
828
|
try {
|
|
750
|
-
saveSession(agent)
|
|
829
|
+
saveSession(agent, state.lines)
|
|
751
830
|
} catch {
|
|
752
831
|
// 存失败不打断使用
|
|
753
832
|
}
|
|
@@ -771,10 +850,8 @@ export async function startTUI(agent, opts = {}) {
|
|
|
771
850
|
pushLine(` [auto] ${name} ${summarize(args)}`, C.warn)
|
|
772
851
|
return Promise.resolve(true)
|
|
773
852
|
}
|
|
774
|
-
//
|
|
775
|
-
|
|
776
|
-
pushLabel(`❯ 权限请求`, ansi.bold + C.warn)
|
|
777
|
-
for (const line of formatPermission(name, args)) pushLine(` ${line}`, C.warn)
|
|
853
|
+
// 预览内容存到 permissionPreview,渲染在输入框上方紧挨"Allow?"提示
|
|
854
|
+
state.permissionPreview = formatPermission(name, args)
|
|
778
855
|
return new Promise((resolve) => {
|
|
779
856
|
state.permission = { name, args, resolve }
|
|
780
857
|
state.status = `Waiting: ${name}`
|
|
@@ -782,15 +859,16 @@ export async function startTUI(agent, opts = {}) {
|
|
|
782
859
|
})
|
|
783
860
|
}
|
|
784
861
|
|
|
785
|
-
/**
|
|
862
|
+
/** 权限请求的关键信息(按工具定制),返回行数组。name 可能带子 agent 前缀("coder/bash"),取基名匹配 */
|
|
786
863
|
function formatPermission(name, args) {
|
|
787
864
|
const cap = (s, n = 1000) => (s.length > n ? `${s.slice(0, n)}…(共 ${s.length} 字符)` : s)
|
|
788
|
-
|
|
789
|
-
if (
|
|
865
|
+
const base = name.includes("/") ? name.split("/").pop() : name
|
|
866
|
+
if (base === "bash") return cap(args.command ?? "").split("\n")
|
|
867
|
+
if (base === "write") {
|
|
790
868
|
// 批准写文件必须看得到要写什么:路径 + 内容预览
|
|
791
869
|
return [`${args.path}(写入 ${(args.content ?? "").length} 字符)`, ...cap(args.content ?? "", 1000).split("\n")]
|
|
792
870
|
}
|
|
793
|
-
if (
|
|
871
|
+
if (base === "edit") {
|
|
794
872
|
// 简易 diff:- 旧内容 / + 新内容
|
|
795
873
|
return [
|
|
796
874
|
`${args.path}`,
|
|
@@ -799,9 +877,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
799
877
|
...cap(args.new_string ?? "", 500).split("\n").map((l) => `+ ${l}`),
|
|
800
878
|
]
|
|
801
879
|
}
|
|
802
|
-
if (
|
|
803
|
-
if (
|
|
804
|
-
if (
|
|
880
|
+
if (base === "delete") return [`${args.path}${args.force ? "(force:跟踪文件也删)" : ""}`]
|
|
881
|
+
if (base === "subagent") return cap(args.task ?? "", 500).split("\n")
|
|
882
|
+
if (base === "memory_put") return [`[${args.type ?? ""}] ${args.title ?? ""}`, ...cap(args.content ?? "", 500).split("\n")]
|
|
805
883
|
return [cap(summarize(args), 300)]
|
|
806
884
|
}
|
|
807
885
|
|
|
@@ -844,7 +922,8 @@ export async function startTUI(agent, opts = {}) {
|
|
|
844
922
|
{ name: "/provider", group: "Config", desc: "管理 provider(增/删/配 key)" },
|
|
845
923
|
{ name: "/config", group: "Config", desc: "配置管理(embedding / agent)" },
|
|
846
924
|
{ name: "/reindex", group: "Config", desc: "重建记忆索引" },
|
|
847
|
-
{ name: "/new", group: "Session", desc: "
|
|
925
|
+
{ name: "/new", group: "Session", desc: "新会话(旧会话归档到槽位)" },
|
|
926
|
+
{ name: "/session", group: "Session", desc: "列出/切换归档会话" },
|
|
848
927
|
{ name: "/clear", group: "Session", desc: "清屏" },
|
|
849
928
|
{ name: "/distill", group: "Session", desc: "从会话提取知识" },
|
|
850
929
|
{ name: "/rewind", group: "Session", desc: "回滚到存档点" },
|
|
@@ -870,16 +949,52 @@ export async function startTUI(agent, opts = {}) {
|
|
|
870
949
|
state.lines = []
|
|
871
950
|
state.streaming = ""
|
|
872
951
|
clearSession(agent.cwd)
|
|
873
|
-
pushLine("
|
|
952
|
+
pushLine("已开始新会话(旧会话已归档到槽位;/session 可查看)", C.dim)
|
|
874
953
|
return
|
|
875
954
|
case "/exit":
|
|
876
955
|
cleanup()
|
|
877
956
|
setTimeout(() => process.exit(0), 100) // 延迟一拍:fetch 后立刻 exit 在 Windows/Node 24 会触发 libuv 断言
|
|
878
957
|
return
|
|
958
|
+
case "/session": {
|
|
959
|
+
const slotNum = Number(rest[0])
|
|
960
|
+
if (rest.length > 0 && !isNaN(slotNum)) {
|
|
961
|
+
// 切换到指定槽位
|
|
962
|
+
const data = switchToSlot(agent.cwd, slotNum)
|
|
963
|
+
if (!data) {
|
|
964
|
+
pushLine(`槽位 ${slotNum} 不存在`, C.dim)
|
|
965
|
+
} else {
|
|
966
|
+
applySession(agent, data)
|
|
967
|
+
state.lines = data.display.length
|
|
968
|
+
? data.display.map((l) => ({ text: l.text, color: l.color }))
|
|
969
|
+
: []
|
|
970
|
+
state.tasks = agent.tasks ?? []
|
|
971
|
+
// 切换过来的会话如果任务全完成,自动收起面板
|
|
972
|
+
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
973
|
+
state.tasks = []
|
|
974
|
+
}
|
|
975
|
+
pushLabel(`── 已切换到槽位 ${slotNum}(${data.history.length} 条消息)──`, C.warn)
|
|
976
|
+
render()
|
|
977
|
+
}
|
|
978
|
+
} else {
|
|
979
|
+
// 列出所有槽位
|
|
980
|
+
const slots = listSlots(agent.cwd)
|
|
981
|
+
if (slots.length === 0) {
|
|
982
|
+
pushLine("没有归档会话(用 /new 后旧会话会自动归档)", C.dim)
|
|
983
|
+
} else {
|
|
984
|
+
pushLabel(`归档会话(/session <n> 切换):`, ansi.bold + C.tool)
|
|
985
|
+
for (const s of slots) {
|
|
986
|
+
pushLine(` 槽位 ${s.slot} — ${s.date}`, C.text)
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return
|
|
991
|
+
}
|
|
879
992
|
case "/reindex": {
|
|
880
|
-
const { syncDir } = await import("./memory.mjs")
|
|
993
|
+
const { syncDir, codeSync, docSync } = await import("./memory.mjs")
|
|
881
994
|
pushLine("[reindex] 重建索引...", C.tool)
|
|
882
995
|
agent.memory.db.prepare("DELETE FROM files").run()
|
|
996
|
+
agent.memory.db.prepare("DELETE FROM code_chunks").run()
|
|
997
|
+
agent.memory.db.prepare("DELETE FROM doc_chunks").run()
|
|
883
998
|
let total = 0
|
|
884
999
|
if (distillOpts.projectDir) {
|
|
885
1000
|
const s = await syncDir(agent.memory, { layer: "project", dir: distillOpts.projectDir })
|
|
@@ -891,7 +1006,27 @@ export async function startTUI(agent, opts = {}) {
|
|
|
891
1006
|
total += s.added
|
|
892
1007
|
pushLine(` team: +${s.added} ~${s.updated} -${s.removed}`, C.dim)
|
|
893
1008
|
}
|
|
894
|
-
|
|
1009
|
+
// 重建代码索引
|
|
1010
|
+
pushLine(` [code] 重建代码索引...`, C.tool)
|
|
1011
|
+
const cr = await codeSync(agent.memory, agent.cwd, {
|
|
1012
|
+
onProgress: (p) => {
|
|
1013
|
+
if (p.phase === "index" && p.current % 20 === 0) {
|
|
1014
|
+
pushLine(` 索引中... ${p.current}/${p.total}`, C.dim)
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
})
|
|
1018
|
+
pushLine(` code: ${cr.total} 文件,+${cr.updated} ~${cr.skipped} -${cr.removed}`, C.dim)
|
|
1019
|
+
// 重建文档索引
|
|
1020
|
+
pushLine(` [doc] 重建文档索引...`, C.tool)
|
|
1021
|
+
const dr = await docSync(agent.memory, agent.cwd, {
|
|
1022
|
+
onProgress: (p) => {
|
|
1023
|
+
if (p.phase === "index" && p.current % 5 === 0) {
|
|
1024
|
+
pushLine(` 索引中... ${p.current}/${p.total}`, C.dim)
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
})
|
|
1028
|
+
pushLine(` doc: ${dr.total} 文件,+${dr.updated} ~${dr.skipped} -${dr.removed}`, C.dim)
|
|
1029
|
+
pushLine(`[reindex] 完成,共 ${total} 条目。向量将在下次搜索时惰性生成。`, C.tool)
|
|
895
1030
|
return
|
|
896
1031
|
}
|
|
897
1032
|
case "/distill":
|
|
@@ -947,14 +1082,15 @@ export async function startTUI(agent, opts = {}) {
|
|
|
947
1082
|
const sub = rest[0]
|
|
948
1083
|
if (sub === "set") {
|
|
949
1084
|
const text = rest.slice(1).join(" ")
|
|
950
|
-
if (!text) { pushLine("用法: /goal set <目标描述>(;
|
|
1085
|
+
if (!text) { pushLine("用法: /goal set <目标描述>(; 分隔完成条件,必须是可机器检查的验证手段)", C.error); return }
|
|
951
1086
|
const semi = text.indexOf(";") >= 0 ? ";" : text.indexOf(";") >= 0 ? ";" : null
|
|
952
1087
|
const objective = semi ? text.slice(0, semi).trim() : text.trim()
|
|
953
1088
|
const criteria = semi ? text.slice(semi + 1).trim() : ""
|
|
954
|
-
agent.goal = { objective, criteria, setAt: Date.now() }
|
|
1089
|
+
agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
|
|
955
1090
|
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
956
1091
|
pushLine(`目标已设置: ${objective}`, C.tool)
|
|
957
1092
|
if (criteria) pushLine(` 完成条件: ${criteria}`, C.dim)
|
|
1093
|
+
else pushLine(` ⚠ 未完成条件——agent 用 goal set 设立时会被要求补上可验证的完成条件`, C.warn)
|
|
958
1094
|
return
|
|
959
1095
|
}
|
|
960
1096
|
if (sub === "cancel") {
|
|
@@ -964,10 +1100,11 @@ export async function startTUI(agent, opts = {}) {
|
|
|
964
1100
|
return
|
|
965
1101
|
}
|
|
966
1102
|
if (agent.goal) {
|
|
1103
|
+
const statusText = { active: "进行中", complete: "已完成", blocked: "已阻塞" }[agent.goal.status] ?? agent.goal.status
|
|
967
1104
|
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
968
1105
|
pushLine(`目标: ${agent.goal.objective}`, C.tool)
|
|
969
1106
|
if (agent.goal.criteria) pushLine(` 完成条件: ${agent.goal.criteria}`, C.dim)
|
|
970
|
-
pushLine(` 设置于: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
|
|
1107
|
+
pushLine(` 状态: ${statusText ?? "进行中"} │ 已用轮数: ${agent.goal.turnsUsed ?? 0} │ 设置于: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
|
|
971
1108
|
pushLine("操作: /goal set <描述> 覆盖 | /goal cancel 取消", C.dim)
|
|
972
1109
|
} else {
|
|
973
1110
|
pushLabel(`❯ Goal`, ansi.bold + C.dim)
|
|
@@ -1143,12 +1280,23 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1143
1280
|
// ---- /think on / off ----
|
|
1144
1281
|
if (sub === "on" || sub === "off") {
|
|
1145
1282
|
const enable = sub === "on"
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1283
|
+
// 仅用 reasoning_effort 的模型(K3):不碰 thinking 字段,只设/删 reasoningEffort
|
|
1284
|
+
const { specForModel } = await import("./config.mjs")
|
|
1285
|
+
const spec = specForModel(cur.model)
|
|
1286
|
+
if (spec.thinkApi === "effort") {
|
|
1287
|
+
// 仅用 reasoning_effort 的模型(K3 / Qwen):不碰 thinking 字段
|
|
1288
|
+
if (!enable) delete cur.reasoningEffort
|
|
1289
|
+
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
1290
|
+
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
1291
|
+
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
1292
|
+
} else {
|
|
1293
|
+
cur.thinking = enable ? { type: "enabled" } : { type: "disabled" }
|
|
1294
|
+
if (!enable) delete cur.reasoningEffort
|
|
1295
|
+
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
1296
|
+
await syncProviderField("thinking", cur.thinking)
|
|
1297
|
+
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
1298
|
+
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
1299
|
+
}
|
|
1152
1300
|
pushLabel(`❯ Think`, ansi.bold + C.tool)
|
|
1153
1301
|
pushLine(`思维模式已${enable ? "开启" : "关闭"}`, C.tool)
|
|
1154
1302
|
if (enable) pushLine(`推理强度: ${cur.reasoningEffort}`, C.dim)
|
|
@@ -1781,6 +1929,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1781
1929
|
if (validKeys.includes(answer) || key.name === "escape") {
|
|
1782
1930
|
const { resolve, name } = state.permission
|
|
1783
1931
|
state.permission = null
|
|
1932
|
+
state.permissionPreview = []
|
|
1784
1933
|
state.status = "Processing..."
|
|
1785
1934
|
if (answer === "a" && !isContinue) {
|
|
1786
1935
|
agent.autoApprove = true
|
|
@@ -1993,8 +2142,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1993
2142
|
}
|
|
1994
2143
|
|
|
1995
2144
|
// 可打印字符 / 粘贴(str 可能一次多个字符);Tab 一律转成两个空格(\t 显示宽度不定,会顶破输入框)
|
|
2145
|
+
// \r\n 在 Windows raw mode 下可能漏进来冲乱页面
|
|
1996
2146
|
if (str && !key.ctrl && !key.meta) {
|
|
1997
|
-
const chars = [...str.replace(
|
|
2147
|
+
const chars = [...str.replace(/[\r\n]+/g, "").replace(/\t/g, " ")]
|
|
1998
2148
|
state.input.splice(state.cursor, 0, ...chars)
|
|
1999
2149
|
state.cursor += chars.length
|
|
2000
2150
|
render()
|
|
@@ -2011,23 +2161,75 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2011
2161
|
}
|
|
2012
2162
|
pushLine(`Tools: ${agent.tools.map((t) => t.name).join(", ")}`, C.dim)
|
|
2013
2163
|
// 恢复上次会话:重建对话区显示(tool 结果行省略,保持清爽)
|
|
2014
|
-
if (opts.restored?.
|
|
2015
|
-
|
|
2164
|
+
if (opts.restored?.display?.length) {
|
|
2165
|
+
// 用户视角的恢复:display 是退出前对话区的原样快照,所见即所得
|
|
2166
|
+
state.lines = [...opts.restored.display.map((l) => ({ text: l.text, color: l.color })), ...state.lines]
|
|
2167
|
+
pushLabel(`── 已恢复上次会话(退出前原样回放);/new 开始新会话 ──`, C.warn)
|
|
2168
|
+
} else if (opts.restored?.history?.length) {
|
|
2169
|
+
// 重建对话区:user/assistant 消息逐条展示,tool 结果行只保留首行摘要
|
|
2170
|
+
for (let i = 0; i < opts.restored.history.length; i++) {
|
|
2171
|
+
const m = opts.restored.history[i]
|
|
2016
2172
|
if (m.role === "user") {
|
|
2173
|
+
if (typeof m.content === "string" && m.content.startsWith("[System reminder:")) continue
|
|
2017
2174
|
pushLabel(`❯ You:`, ansi.bold + C.user)
|
|
2018
2175
|
if (typeof m.content === "string" && m.content) pushLine(m.content, C.text)
|
|
2019
2176
|
} else if (m.role === "assistant") {
|
|
2020
2177
|
pushLabel(`❯ ThinCoder:`, ansi.bold + C.assistant)
|
|
2021
2178
|
if (typeof m.content === "string" && m.content) pushLine(m.content, C.text)
|
|
2022
2179
|
for (const tc of m.tool_calls ?? []) {
|
|
2023
|
-
|
|
2180
|
+
// 找到下一条对应的 tool 结果,显示首行摘要
|
|
2181
|
+
const toolResult = opts.restored.history[i + 1]
|
|
2182
|
+
const hasResult = toolResult?.role === "tool" && toolResult?.tool_call_id === tc.id
|
|
2183
|
+
const summary = hasResult ? " → " + sliceByWidth(String(toolResult.content).split("\n")[0], 80) : ""
|
|
2184
|
+
pushLine(` [tool] ${tc.function?.name ?? "?"}${summary}`, C.tool)
|
|
2024
2185
|
}
|
|
2025
2186
|
}
|
|
2026
|
-
// tool
|
|
2187
|
+
// tool 消息本身不单独渲染——已在 assistant 的 tool_calls 后以摘要形式展示
|
|
2027
2188
|
}
|
|
2028
2189
|
pushLabel(`── 已恢复上次会话(${opts.restored.history.length} 条消息);/new 开始新会话 ──`, C.warn)
|
|
2029
2190
|
}
|
|
2191
|
+
// 有归档槽位时给个提示
|
|
2192
|
+
if (listSlots(agent.cwd).length > 0) {
|
|
2193
|
+
pushLine("提示:存在归档会话,/session 可查看/切换", C.dim)
|
|
2194
|
+
}
|
|
2030
2195
|
render()
|
|
2196
|
+
|
|
2197
|
+
// 后台索引(进界面后再跑,不阻塞启动);进度走底部状态栏,不往对话区塞行
|
|
2198
|
+
;(async () => {
|
|
2199
|
+
const { codeSync, docSync } = await import("./memory.mjs")
|
|
2200
|
+
const cwd = agent.cwd
|
|
2201
|
+
let codeFiles = 0, docFiles = 0
|
|
2202
|
+
try {
|
|
2203
|
+
state.status = "Indexing code..."
|
|
2204
|
+
render()
|
|
2205
|
+
await codeSync(agent.memory, cwd, {
|
|
2206
|
+
onProgress: (p) => {
|
|
2207
|
+
if (p.phase === "index" && p.current % 30 === 0) {
|
|
2208
|
+
state.status = `Indexing code... ${p.current}/${p.total}`
|
|
2209
|
+
render()
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
})
|
|
2213
|
+
codeFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM code_chunks`).get()?.n ?? 0
|
|
2214
|
+
} catch { /* 不阻塞 */ }
|
|
2215
|
+
try {
|
|
2216
|
+
state.status = "Indexing docs..."
|
|
2217
|
+
render()
|
|
2218
|
+
await docSync(agent.memory, cwd, {
|
|
2219
|
+
onProgress: (p) => {
|
|
2220
|
+
if (p.phase === "index" && p.current % 10 === 0) {
|
|
2221
|
+
state.status = `Indexing docs... ${p.current}/${p.total}`
|
|
2222
|
+
render()
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
})
|
|
2226
|
+
docFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM doc_chunks`).get()?.n ?? 0
|
|
2227
|
+
} catch { /* 不阻塞 */ }
|
|
2228
|
+
state.status = codeFiles || docFiles
|
|
2229
|
+
? `Ready — idx code ${codeFiles} doc ${docFiles}`
|
|
2230
|
+
: "Ready"
|
|
2231
|
+
render()
|
|
2232
|
+
})()
|
|
2031
2233
|
}
|
|
2032
2234
|
|
|
2033
2235
|
function summarize(obj) {
|