thincoder 0.3.0 → 0.5.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 +9 -3
- package/bin/thincoder.mjs +57 -10
- package/package.json +11 -2
- package/src/SYSTEM_PROMPT.md +7 -6
- package/src/agent.mjs +333 -60
- package/src/coder-overlay.md +2 -1
- package/src/config.mjs +29 -20
- package/src/context.mjs +128 -48
- package/src/explore-overlay.md +6 -2
- package/src/main-overlay.md +8 -0
- package/src/memory.mjs +677 -3
- package/src/plan-overlay.md +13 -0
- package/src/provider.mjs +73 -6
- package/src/repomap.mjs +204 -0
- package/src/session.mjs +142 -15
- package/src/skills.mjs +2 -1
- package/src/tools/bash.md +1 -0
- package/src/tools.mjs +33 -7
- package/src/tui.mjs +228 -34
package/src/tui.mjs
CHANGED
|
@@ -9,7 +9,8 @@ import { PassThrough } from "node:stream"
|
|
|
9
9
|
import { basename } from "node:path"
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs"
|
|
11
11
|
import { runAgent, ContinueError } from "./agent.mjs"
|
|
12
|
-
import {
|
|
12
|
+
import { estimateTokens } from "./context.mjs"
|
|
13
|
+
import { saveSession, clearSession, archiveCurrent, listSlots, switchToSlot, sessionPath } from "./session.mjs"
|
|
13
14
|
import { PROVIDER_PRESETS as PRESETS } from "./config.mjs"
|
|
14
15
|
import { closeAllMcp } from "./mcp.mjs"
|
|
15
16
|
|
|
@@ -144,11 +145,29 @@ function renderTable(block, width) {
|
|
|
144
145
|
widths[widest]--
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
// 单元格渲染:sliceByWidth 截断(表头单行),padByWidth 补齐
|
|
149
|
+
const fmtCell = (text, ci) => padByWidth(sliceByWidth(text, widths[ci]), widths[ci])
|
|
150
|
+
const fmtRow = (cells) => "│ " + cells.map((c, i) => fmtCell(c, i)).join(" │ ") + " │"
|
|
151
|
+
|
|
152
|
+
// 分隔线
|
|
149
153
|
const separator = "├" + widths.map((w) => "─".repeat(w + 2)).join("┼") + "┤"
|
|
150
154
|
|
|
151
|
-
|
|
155
|
+
const out = []
|
|
156
|
+
// 表头:单行截断(表头通常是短标签,折行不如截断直观)
|
|
157
|
+
out.push(fmtRow(rows[0]))
|
|
158
|
+
out.push(separator)
|
|
159
|
+
|
|
160
|
+
// 数据行:过长单元格按列宽折行,一个逻辑行可能对应多条显示行
|
|
161
|
+
for (let r = 2; r < rows.length; r++) {
|
|
162
|
+
// wrapText 返回按 width 折行后的行数组,保留内部 \n
|
|
163
|
+
const wrapped = rows[r].map((cell, ci) => wrapText(cell, widths[ci]))
|
|
164
|
+
const height = Math.max(...wrapped.map((lines) => lines.length))
|
|
165
|
+
for (let lineIdx = 0; lineIdx < height; lineIdx++) {
|
|
166
|
+
out.push(fmtRow(wrapped.map((lines) => lines[lineIdx] ?? "")))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return out
|
|
152
171
|
}
|
|
153
172
|
|
|
154
173
|
/** 输入区布局:把输入缓冲折行,同时算出光标的 (行, 列) 位置(显示宽度) */
|
|
@@ -188,6 +207,21 @@ export function layoutInput(chars, cursor, width) {
|
|
|
188
207
|
}
|
|
189
208
|
|
|
190
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
|
+
|
|
191
225
|
export function wrapText(text, width) {
|
|
192
226
|
const lines = []
|
|
193
227
|
for (const rawLine of text.split("\n")) {
|
|
@@ -234,7 +268,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
234
268
|
question: null, // { text, options, resolve } — agent 的 question 工具回调
|
|
235
269
|
picker: null, // 模型选择器 { entries, lines, index, scroll, selectedLine }
|
|
236
270
|
wizard: null, // 首次配置向导 { step, index, scroll, selectedLine, fields, error, lines }
|
|
237
|
-
tasks: [], // task
|
|
271
|
+
tasks: agent.tasks ?? [], // task 工具的任务列表(状态栏显示进度);会话恢复时直接带上
|
|
272
|
+
tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }, // 累计 token 用量(状态栏显示)
|
|
273
|
+
ctxCache: { len: -1, tokens: 0 }, // 上下文占用估算缓存(estimateTokens 是 O(n),history 变长才重算)
|
|
238
274
|
reasoning: "", // 思考流缓冲(暗色展示)
|
|
239
275
|
completion: null, // Tab 补全状态 { candidates, index }
|
|
240
276
|
toolStreams: {}, // 各工具的实时输出(按工具名隔离,并行工具互不串扰)
|
|
@@ -280,10 +316,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
280
316
|
if (text) keyStream.write(text)
|
|
281
317
|
})
|
|
282
318
|
|
|
319
|
+
let cleanedUp = false
|
|
283
320
|
const cleanup = () => {
|
|
284
|
-
|
|
321
|
+
if (cleanedUp) return
|
|
322
|
+
cleanedUp = true
|
|
323
|
+
// 退出前保存会话(同步写);先归档当前到槽位,再落新——不丢
|
|
285
324
|
try {
|
|
286
|
-
|
|
325
|
+
archiveCurrent(agent.cwd)
|
|
326
|
+
saveSession(agent, state.lines)
|
|
287
327
|
} catch {
|
|
288
328
|
// 存失败不耽误退出
|
|
289
329
|
}
|
|
@@ -389,7 +429,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
389
429
|
// 对话区内容行(含流式缓冲);markdown 表格先按显示宽度重排
|
|
390
430
|
const convLines = []
|
|
391
431
|
for (const l of state.lines) {
|
|
392
|
-
for (const line of formatTables(l.text, cols - 1)) {
|
|
432
|
+
for (const line of formatTables(sanitizeDisplay(l.text), cols - 1)) {
|
|
393
433
|
for (const wrapped of wrapText(line, cols - 1)) {
|
|
394
434
|
convLines.push({ text: wrapped, color: l.color })
|
|
395
435
|
}
|
|
@@ -397,12 +437,12 @@ export async function startTUI(agent, opts = {}) {
|
|
|
397
437
|
}
|
|
398
438
|
// 思考流(暗色)在正文流之前
|
|
399
439
|
if (state.reasoning) {
|
|
400
|
-
for (const wrapped of wrapText(state.reasoning, cols - 1)) {
|
|
440
|
+
for (const wrapped of wrapText(sanitizeDisplay(state.reasoning), cols - 1)) {
|
|
401
441
|
convLines.push({ text: wrapped, color: C.reason })
|
|
402
442
|
}
|
|
403
443
|
}
|
|
404
444
|
if (state.streaming) {
|
|
405
|
-
for (const line of formatTables(state.streaming, cols - 1)) {
|
|
445
|
+
for (const line of formatTables(sanitizeDisplay(state.streaming), cols - 1)) {
|
|
406
446
|
for (const wrapped of wrapText(line, cols - 1)) {
|
|
407
447
|
convLines.push({ text: wrapped, color: C.text })
|
|
408
448
|
}
|
|
@@ -411,7 +451,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
411
451
|
// 工具实时输出(暗色,只保留末尾防刷屏;按工具名隔离防止并行工具串扰)
|
|
412
452
|
const allStreams = Object.values(state.toolStreams).join("")
|
|
413
453
|
if (allStreams) {
|
|
414
|
-
const tail = allStreams.slice(-4000)
|
|
454
|
+
const tail = sanitizeDisplay(allStreams.slice(-4000))
|
|
415
455
|
for (const wrapped of wrapText(tail, cols - 1)) {
|
|
416
456
|
convLines.push({ text: wrapped, color: C.dim })
|
|
417
457
|
}
|
|
@@ -451,10 +491,10 @@ export async function startTUI(agent, opts = {}) {
|
|
|
451
491
|
for (let i = shown.length; i < winH; i++) out.push(ansi.clearLine)
|
|
452
492
|
}
|
|
453
493
|
|
|
454
|
-
// todo 面板(对话区与输入框之间):▶ in_progress / ✓ done / ○ pending
|
|
494
|
+
// todo 面板(对话区与输入框之间):▶ in_progress / ✓ done(删除线) / ○ pending
|
|
455
495
|
for (const t of visibleTasks) {
|
|
456
496
|
const mark = t.status === "done" ? "✓" : t.status === "in_progress" ? "▶" : "○"
|
|
457
|
-
const color = t.status === "done" ? C.dim : t.status === "in_progress" ? C.tool : C.text
|
|
497
|
+
const color = t.status === "done" ? `${C.dim}${ESC}[9m` : t.status === "in_progress" ? C.tool : C.text
|
|
458
498
|
out.push(`${color} ${mark} ${sliceByWidth(t.title, cols - 4)}${ansi.reset}${ansi.clearLine}`)
|
|
459
499
|
}
|
|
460
500
|
|
|
@@ -533,10 +573,28 @@ export async function startTUI(agent, opts = {}) {
|
|
|
533
573
|
const taskHint = state.tasks.length > 0
|
|
534
574
|
? ` │ ▶${state.tasks.filter((t) => t.status === "done").length}/${state.tasks.length}`
|
|
535
575
|
: ""
|
|
576
|
+
// token 用量:↑输入 ↓输出 + 缓存命中率(DeepSeek usage 带 prompt_cache_hit/miss_tokens)
|
|
577
|
+
const tk = state.tokens
|
|
578
|
+
const fmtK = (n) => (n >= 10000 ? `${Math.round(n / 1000)}k` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`)
|
|
579
|
+
const cacheTotal = tk.cacheHit + tk.cacheMiss
|
|
580
|
+
const tokenHint = tk.prompt > 0
|
|
581
|
+
? ` │ ↑${fmtK(tk.prompt)} ↓${fmtK(tk.completion)}${cacheTotal > 0 ? ` hit${Math.round((tk.cacheHit / cacheTotal) * 100)}%` : ""}`
|
|
582
|
+
: ""
|
|
536
583
|
const elapsed = state.processing ? ` ${Math.floor((Date.now() - state.processingStarted) / 1000)}s` : ""
|
|
537
584
|
const toolHint = state.currentTool ? ` ${state.currentTool}…` : ""
|
|
538
585
|
const statusText = state.processing ? `${state.status}${toolHint}${elapsed}` : state.status
|
|
539
|
-
|
|
586
|
+
// 上下文利用率:占压缩阈值百分比(到 100% 触发压缩;≥80% 变黄提醒该收尾或 /new)
|
|
587
|
+
if (state.ctxCache.len !== agent.history.length) {
|
|
588
|
+
state.ctxCache = { len: agent.history.length, tokens: estimateTokens(agent.history) }
|
|
589
|
+
}
|
|
590
|
+
const ctxThreshold = agent.config?.agent?.compactThreshold ?? 100_000
|
|
591
|
+
const ctxPct = Math.round((state.ctxCache.tokens / ctxThreshold) * 100)
|
|
592
|
+
const ctxHint = ctxPct > 0
|
|
593
|
+
? ctxPct >= 80
|
|
594
|
+
? ` │ ${ansi.reset}${C.warn}ctx ${ctxPct}%${ansi.reset}${ansi.dim}`
|
|
595
|
+
: ` │ ctx ${ctxPct}%`
|
|
596
|
+
: ""
|
|
597
|
+
statusLine = ` ${statusText}${taskHint}${tokenHint}${ctxHint}${scrollHint} │ Enter: send │ /: commands │ wheel/PgUp/PgDn: scroll │ Ctrl+C: exit`
|
|
540
598
|
}
|
|
541
599
|
const autoBanner = agent.autoApprove ? `${C.warn} AUTO${ansi.reset}${ansi.dim}│` : ""
|
|
542
600
|
const planBanner = agent.planMode ? `${C.tool} PLAN${ansi.reset}${ansi.dim}│` : ""
|
|
@@ -556,7 +614,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
556
614
|
if (state.processing || state.permission || state.question || state.picker || state.wizard?.step === "provider") {
|
|
557
615
|
process.stdout.write(ansi.hideCursor)
|
|
558
616
|
} else {
|
|
559
|
-
const cursorRow = 1 + convH + taskPanelH + 2 + (layout.cursorLine - inputOffset) // header + 对话区 + todo 面板 + 上边框 + 行偏移
|
|
617
|
+
const cursorRow = 1 + convH + pickerH + taskPanelH + 2 + (layout.cursorLine - inputOffset) // header + 对话区 + todo 面板 + 上边框 + 行偏移
|
|
560
618
|
const cursorCol = 3 + layout.cursorCol // 左边框 + 空格 + 文本偏移(1 基)
|
|
561
619
|
process.stdout.write(`${ESC}[${cursorRow};${cursorCol}H${ansi.showCursor}`)
|
|
562
620
|
}
|
|
@@ -643,12 +701,28 @@ export async function startTUI(agent, opts = {}) {
|
|
|
643
701
|
onCompress: () => {
|
|
644
702
|
pushLine(" [context] 上下文过长,已自动压缩(早期对话由 LLM 摘要,任务状态保留)", C.warn)
|
|
645
703
|
},
|
|
704
|
+
onUsage: (usage) => {
|
|
705
|
+
state.tokens.prompt += usage.prompt_tokens ?? 0
|
|
706
|
+
state.tokens.completion += usage.completion_tokens ?? 0
|
|
707
|
+
state.tokens.cacheHit += usage.prompt_cache_hit_tokens ?? 0
|
|
708
|
+
state.tokens.cacheMiss += usage.prompt_cache_miss_tokens ?? 0
|
|
709
|
+
},
|
|
646
710
|
onTaskUpdate: (items) => {
|
|
647
711
|
state.tasks = items
|
|
648
712
|
const done = items.filter((i) => i.status === "done").length
|
|
649
|
-
|
|
713
|
+
// 留痕带上当前任务标题:回看历史时知道进行到哪一项
|
|
714
|
+
const current = items.find((i) => i.status === "in_progress")
|
|
715
|
+
pushLine(` [task] ${done}/${items.length}${current ? ` ▶ ${current.title}` : ""}`, C.dim)
|
|
650
716
|
render()
|
|
651
717
|
},
|
|
718
|
+
// 增量保存:每 5 个工具 turn 落一次盘,中途崩溃丢失窗口从一整轮缩到几轮
|
|
719
|
+
onTurnEnd: (() => {
|
|
720
|
+
let n = 0
|
|
721
|
+
return () => {
|
|
722
|
+
if (++n % 5 !== 0) return
|
|
723
|
+
try { saveSession(agent, state.lines) } catch {}
|
|
724
|
+
}
|
|
725
|
+
})(),
|
|
652
726
|
}
|
|
653
727
|
|
|
654
728
|
for (let resume = false; ; resume = true) {
|
|
@@ -700,7 +774,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
700
774
|
}
|
|
701
775
|
// 每轮结束后保存会话(崩溃也不丢)
|
|
702
776
|
try {
|
|
703
|
-
saveSession(agent)
|
|
777
|
+
saveSession(agent, state.lines)
|
|
704
778
|
} catch {
|
|
705
779
|
// 存失败不打断使用
|
|
706
780
|
}
|
|
@@ -725,8 +799,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
725
799
|
return Promise.resolve(true)
|
|
726
800
|
}
|
|
727
801
|
// 把关键参数摆出来:批什么要让人看明白
|
|
802
|
+
// 内容行用警告色——与正常输出(白)区分,滚动回看也能认出这是待审批内容
|
|
728
803
|
pushLabel(`❯ 权限请求`, ansi.bold + C.warn)
|
|
729
|
-
for (const line of formatPermission(name, args)) pushLine(` ${line}`, C.
|
|
804
|
+
for (const line of formatPermission(name, args)) pushLine(` ${line}`, C.warn)
|
|
730
805
|
return new Promise((resolve) => {
|
|
731
806
|
state.permission = { name, args, resolve }
|
|
732
807
|
state.status = `Waiting: ${name}`
|
|
@@ -734,14 +809,27 @@ export async function startTUI(agent, opts = {}) {
|
|
|
734
809
|
})
|
|
735
810
|
}
|
|
736
811
|
|
|
737
|
-
/**
|
|
812
|
+
/** 权限请求的关键信息(按工具定制),返回行数组。name 可能带子 agent 前缀("coder/bash"),取基名匹配 */
|
|
738
813
|
function formatPermission(name, args) {
|
|
739
814
|
const cap = (s, n = 1000) => (s.length > n ? `${s.slice(0, n)}…(共 ${s.length} 字符)` : s)
|
|
740
|
-
|
|
741
|
-
if (
|
|
742
|
-
if (
|
|
743
|
-
|
|
744
|
-
|
|
815
|
+
const base = name.includes("/") ? name.split("/").pop() : name
|
|
816
|
+
if (base === "bash") return cap(args.command ?? "").split("\n")
|
|
817
|
+
if (base === "write") {
|
|
818
|
+
// 批准写文件必须看得到要写什么:路径 + 内容预览
|
|
819
|
+
return [`${args.path}(写入 ${(args.content ?? "").length} 字符)`, ...cap(args.content ?? "", 1000).split("\n")]
|
|
820
|
+
}
|
|
821
|
+
if (base === "edit") {
|
|
822
|
+
// 简易 diff:- 旧内容 / + 新内容
|
|
823
|
+
return [
|
|
824
|
+
`${args.path}`,
|
|
825
|
+
...cap(args.old_string ?? "", 500).split("\n").map((l) => `- ${l}`),
|
|
826
|
+
" ↓",
|
|
827
|
+
...cap(args.new_string ?? "", 500).split("\n").map((l) => `+ ${l}`),
|
|
828
|
+
]
|
|
829
|
+
}
|
|
830
|
+
if (base === "delete") return [`${args.path}${args.force ? "(force:跟踪文件也删)" : ""}`]
|
|
831
|
+
if (base === "subagent") return cap(args.task ?? "", 500).split("\n")
|
|
832
|
+
if (base === "memory_put") return [`[${args.type ?? ""}] ${args.title ?? ""}`, ...cap(args.content ?? "", 500).split("\n")]
|
|
745
833
|
return [cap(summarize(args), 300)]
|
|
746
834
|
}
|
|
747
835
|
|
|
@@ -784,7 +872,8 @@ export async function startTUI(agent, opts = {}) {
|
|
|
784
872
|
{ name: "/provider", group: "Config", desc: "管理 provider(增/删/配 key)" },
|
|
785
873
|
{ name: "/config", group: "Config", desc: "配置管理(embedding / agent)" },
|
|
786
874
|
{ name: "/reindex", group: "Config", desc: "重建记忆索引" },
|
|
787
|
-
{ name: "/new", group: "Session", desc: "
|
|
875
|
+
{ name: "/new", group: "Session", desc: "新会话(旧会话归档到槽位)" },
|
|
876
|
+
{ name: "/session", group: "Session", desc: "列出/切换归档会话" },
|
|
788
877
|
{ name: "/clear", group: "Session", desc: "清屏" },
|
|
789
878
|
{ name: "/distill", group: "Session", desc: "从会话提取知识" },
|
|
790
879
|
{ name: "/rewind", group: "Session", desc: "回滚到存档点" },
|
|
@@ -810,16 +899,48 @@ export async function startTUI(agent, opts = {}) {
|
|
|
810
899
|
state.lines = []
|
|
811
900
|
state.streaming = ""
|
|
812
901
|
clearSession(agent.cwd)
|
|
813
|
-
pushLine("
|
|
902
|
+
pushLine("已开始新会话(旧会话已归档到槽位;/session 可查看)", C.dim)
|
|
814
903
|
return
|
|
815
904
|
case "/exit":
|
|
816
905
|
cleanup()
|
|
817
906
|
setTimeout(() => process.exit(0), 100) // 延迟一拍:fetch 后立刻 exit 在 Windows/Node 24 会触发 libuv 断言
|
|
818
907
|
return
|
|
908
|
+
case "/session": {
|
|
909
|
+
const slotNum = Number(rest[0])
|
|
910
|
+
if (rest.length > 0 && !isNaN(slotNum)) {
|
|
911
|
+
// 切换到指定槽位
|
|
912
|
+
const data = switchToSlot(agent.cwd, slotNum)
|
|
913
|
+
if (!data) {
|
|
914
|
+
pushLine(`槽位 ${slotNum} 不存在`, C.dim)
|
|
915
|
+
} else {
|
|
916
|
+
applySession(agent, data)
|
|
917
|
+
state.lines = data.display.length
|
|
918
|
+
? data.display.map((l) => ({ text: l.text, color: l.color }))
|
|
919
|
+
: []
|
|
920
|
+
state.tasks = agent.tasks ?? []
|
|
921
|
+
pushLabel(`── 已切换到槽位 ${slotNum}(${data.history.length} 条消息)──`, C.warn)
|
|
922
|
+
render()
|
|
923
|
+
}
|
|
924
|
+
} else {
|
|
925
|
+
// 列出所有槽位
|
|
926
|
+
const slots = listSlots(agent.cwd)
|
|
927
|
+
if (slots.length === 0) {
|
|
928
|
+
pushLine("没有归档会话(用 /new 后旧会话会自动归档)", C.dim)
|
|
929
|
+
} else {
|
|
930
|
+
pushLabel(`归档会话(/session <n> 切换):`, ansi.bold + C.tool)
|
|
931
|
+
for (const s of slots) {
|
|
932
|
+
pushLine(` 槽位 ${s.slot} — ${s.date}`, C.text)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return
|
|
937
|
+
}
|
|
819
938
|
case "/reindex": {
|
|
820
|
-
const { syncDir } = await import("./memory.mjs")
|
|
939
|
+
const { syncDir, codeSync, docSync } = await import("./memory.mjs")
|
|
821
940
|
pushLine("[reindex] 重建索引...", C.tool)
|
|
822
941
|
agent.memory.db.prepare("DELETE FROM files").run()
|
|
942
|
+
agent.memory.db.prepare("DELETE FROM code_chunks").run()
|
|
943
|
+
agent.memory.db.prepare("DELETE FROM doc_chunks").run()
|
|
823
944
|
let total = 0
|
|
824
945
|
if (distillOpts.projectDir) {
|
|
825
946
|
const s = await syncDir(agent.memory, { layer: "project", dir: distillOpts.projectDir })
|
|
@@ -831,7 +952,27 @@ export async function startTUI(agent, opts = {}) {
|
|
|
831
952
|
total += s.added
|
|
832
953
|
pushLine(` team: +${s.added} ~${s.updated} -${s.removed}`, C.dim)
|
|
833
954
|
}
|
|
834
|
-
|
|
955
|
+
// 重建代码索引
|
|
956
|
+
pushLine(` [code] 重建代码索引...`, C.tool)
|
|
957
|
+
const cr = await codeSync(agent.memory, agent.cwd, {
|
|
958
|
+
onProgress: (p) => {
|
|
959
|
+
if (p.phase === "index" && p.current % 20 === 0) {
|
|
960
|
+
pushLine(` 索引中... ${p.current}/${p.total}`, C.dim)
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
})
|
|
964
|
+
pushLine(` code: ${cr.total} 文件,+${cr.updated} ~${cr.skipped} -${cr.removed}`, C.dim)
|
|
965
|
+
// 重建文档索引
|
|
966
|
+
pushLine(` [doc] 重建文档索引...`, C.tool)
|
|
967
|
+
const dr = await docSync(agent.memory, agent.cwd, {
|
|
968
|
+
onProgress: (p) => {
|
|
969
|
+
if (p.phase === "index" && p.current % 5 === 0) {
|
|
970
|
+
pushLine(` 索引中... ${p.current}/${p.total}`, C.dim)
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
})
|
|
974
|
+
pushLine(` doc: ${dr.total} 文件,+${dr.updated} ~${dr.skipped} -${dr.removed}`, C.dim)
|
|
975
|
+
pushLine(`[reindex] 完成,共 ${total} 条目。向量将在下次搜索时惰性生成。`, C.tool)
|
|
835
976
|
return
|
|
836
977
|
}
|
|
837
978
|
case "/distill":
|
|
@@ -887,14 +1028,15 @@ export async function startTUI(agent, opts = {}) {
|
|
|
887
1028
|
const sub = rest[0]
|
|
888
1029
|
if (sub === "set") {
|
|
889
1030
|
const text = rest.slice(1).join(" ")
|
|
890
|
-
if (!text) { pushLine("用法: /goal set <目标描述>(;
|
|
1031
|
+
if (!text) { pushLine("用法: /goal set <目标描述>(; 分隔完成条件,必须是可机器检查的验证手段)", C.error); return }
|
|
891
1032
|
const semi = text.indexOf(";") >= 0 ? ";" : text.indexOf(";") >= 0 ? ";" : null
|
|
892
1033
|
const objective = semi ? text.slice(0, semi).trim() : text.trim()
|
|
893
1034
|
const criteria = semi ? text.slice(semi + 1).trim() : ""
|
|
894
|
-
agent.goal = { objective, criteria, setAt: Date.now() }
|
|
1035
|
+
agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
|
|
895
1036
|
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
896
1037
|
pushLine(`目标已设置: ${objective}`, C.tool)
|
|
897
1038
|
if (criteria) pushLine(` 完成条件: ${criteria}`, C.dim)
|
|
1039
|
+
else pushLine(` ⚠ 未完成条件——agent 用 goal set 设立时会被要求补上可验证的完成条件`, C.warn)
|
|
898
1040
|
return
|
|
899
1041
|
}
|
|
900
1042
|
if (sub === "cancel") {
|
|
@@ -904,10 +1046,11 @@ export async function startTUI(agent, opts = {}) {
|
|
|
904
1046
|
return
|
|
905
1047
|
}
|
|
906
1048
|
if (agent.goal) {
|
|
1049
|
+
const statusText = { active: "进行中", complete: "已完成", blocked: "已阻塞" }[agent.goal.status] ?? agent.goal.status
|
|
907
1050
|
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
908
1051
|
pushLine(`目标: ${agent.goal.objective}`, C.tool)
|
|
909
1052
|
if (agent.goal.criteria) pushLine(` 完成条件: ${agent.goal.criteria}`, C.dim)
|
|
910
|
-
pushLine(` 设置于: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
|
|
1053
|
+
pushLine(` 状态: ${statusText ?? "进行中"} │ 已用轮数: ${agent.goal.turnsUsed ?? 0} │ 设置于: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
|
|
911
1054
|
pushLine("操作: /goal set <描述> 覆盖 | /goal cancel 取消", C.dim)
|
|
912
1055
|
} else {
|
|
913
1056
|
pushLabel(`❯ Goal`, ansi.bold + C.dim)
|
|
@@ -1719,7 +1862,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1719
1862
|
const isContinue = state.permission.name === "continue"
|
|
1720
1863
|
const validKeys = isContinue ? ["y", "n"] : ["y", "n", "a"]
|
|
1721
1864
|
if (validKeys.includes(answer) || key.name === "escape") {
|
|
1722
|
-
const { resolve } = state.permission
|
|
1865
|
+
const { resolve, name } = state.permission
|
|
1723
1866
|
state.permission = null
|
|
1724
1867
|
state.status = "Processing..."
|
|
1725
1868
|
if (answer === "a" && !isContinue) {
|
|
@@ -1728,7 +1871,12 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1728
1871
|
agent._pendingReminders.push("[System reminder: AUTO mode is now ON. All tool calls are automatically approved. Use /auto to disable.]")
|
|
1729
1872
|
pushLine(` [auto] AUTO 已开启:后续工具调用不再询问(/auto 关闭)`, C.warn)
|
|
1730
1873
|
}
|
|
1731
|
-
|
|
1874
|
+
const approved = answer === "y" || (answer === "a" && !isContinue)
|
|
1875
|
+
// 决定落痕:对话区留下批准/拒绝记录(continue 询问有自己的输出,不重复记)
|
|
1876
|
+
if (!isContinue) {
|
|
1877
|
+
pushLine(` [${approved ? "approved" : "denied"}] ${name}`, approved ? C.dim : C.error)
|
|
1878
|
+
}
|
|
1879
|
+
resolve(approved)
|
|
1732
1880
|
render()
|
|
1733
1881
|
}
|
|
1734
1882
|
return
|
|
@@ -1946,9 +2094,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1946
2094
|
}
|
|
1947
2095
|
pushLine(`Tools: ${agent.tools.map((t) => t.name).join(", ")}`, C.dim)
|
|
1948
2096
|
// 恢复上次会话:重建对话区显示(tool 结果行省略,保持清爽)
|
|
1949
|
-
if (opts.restored?.
|
|
2097
|
+
if (opts.restored?.display?.length) {
|
|
2098
|
+
// 用户视角的恢复:display 是退出前对话区的原样快照,所见即所得
|
|
2099
|
+
state.lines = [...opts.restored.display.map((l) => ({ text: l.text, color: l.color })), ...state.lines]
|
|
2100
|
+
pushLabel(`── 已恢复上次会话(退出前原样回放);/new 开始新会话 ──`, C.warn)
|
|
2101
|
+
} else if (opts.restored?.history?.length) {
|
|
1950
2102
|
for (const m of opts.restored.history) {
|
|
1951
2103
|
if (m.role === "user") {
|
|
2104
|
+
if (typeof m.content === "string" && m.content.startsWith("[System reminder:")) continue
|
|
1952
2105
|
pushLabel(`❯ You:`, ansi.bold + C.user)
|
|
1953
2106
|
if (typeof m.content === "string" && m.content) pushLine(m.content, C.text)
|
|
1954
2107
|
} else if (m.role === "assistant") {
|
|
@@ -1962,7 +2115,48 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1962
2115
|
}
|
|
1963
2116
|
pushLabel(`── 已恢复上次会话(${opts.restored.history.length} 条消息);/new 开始新会话 ──`, C.warn)
|
|
1964
2117
|
}
|
|
2118
|
+
// 有归档槽位时给个提示
|
|
2119
|
+
if (listSlots(agent.cwd).length > 0) {
|
|
2120
|
+
pushLine("提示:存在归档会话,/session 可查看/切换", C.dim)
|
|
2121
|
+
}
|
|
1965
2122
|
render()
|
|
2123
|
+
|
|
2124
|
+
// 后台索引(进界面后再跑,不阻塞启动);进度走底部状态栏,不往对话区塞行
|
|
2125
|
+
;(async () => {
|
|
2126
|
+
const { codeSync, docSync } = await import("./memory.mjs")
|
|
2127
|
+
const cwd = agent.cwd
|
|
2128
|
+
let codeFiles = 0, docFiles = 0
|
|
2129
|
+
try {
|
|
2130
|
+
state.status = "Indexing code..."
|
|
2131
|
+
render()
|
|
2132
|
+
await codeSync(agent.memory, cwd, {
|
|
2133
|
+
onProgress: (p) => {
|
|
2134
|
+
if (p.phase === "index" && p.current % 30 === 0) {
|
|
2135
|
+
state.status = `Indexing code... ${p.current}/${p.total}`
|
|
2136
|
+
render()
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
})
|
|
2140
|
+
codeFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM code_chunks`).get()?.n ?? 0
|
|
2141
|
+
} catch { /* 不阻塞 */ }
|
|
2142
|
+
try {
|
|
2143
|
+
state.status = "Indexing docs..."
|
|
2144
|
+
render()
|
|
2145
|
+
await docSync(agent.memory, cwd, {
|
|
2146
|
+
onProgress: (p) => {
|
|
2147
|
+
if (p.phase === "index" && p.current % 10 === 0) {
|
|
2148
|
+
state.status = `Indexing docs... ${p.current}/${p.total}`
|
|
2149
|
+
render()
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
})
|
|
2153
|
+
docFiles = agent.memory.db.prepare(`SELECT COUNT(DISTINCT path) AS n FROM doc_chunks`).get()?.n ?? 0
|
|
2154
|
+
} catch { /* 不阻塞 */ }
|
|
2155
|
+
state.status = codeFiles || docFiles
|
|
2156
|
+
? `Ready — idx code ${codeFiles} doc ${docFiles}`
|
|
2157
|
+
: "Ready"
|
|
2158
|
+
render()
|
|
2159
|
+
})()
|
|
1966
2160
|
}
|
|
1967
2161
|
|
|
1968
2162
|
function summarize(obj) {
|