thincoder 0.5.0 → 0.7.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 -45
- package/package.json +2 -2
- package/src/SYSTEM_PROMPT.md +6 -3
- package/src/agent.mjs +87 -13
- package/src/coder-overlay.md +1 -1
- package/src/config.mjs +47 -23
- package/src/mcp.mjs +94 -3
- package/src/memory.mjs +64 -68
- package/src/provider.mjs +21 -4
- package/src/tools/bash.md +14 -1
- package/src/tools/glob.md +1 -1
- package/src/tools/grep.md +3 -0
- package/src/tools/insert_after.md +13 -0
- package/src/tools/question.md +1 -0
- package/src/tools/syntax_check.md +10 -0
- package/src/tools/websearch.md +1 -0
- package/src/tools.mjs +205 -68
- package/src/tui.mjs +204 -59
package/src/tui.mjs
CHANGED
|
@@ -265,20 +265,28 @@ export async function startTUI(agent, opts = {}) {
|
|
|
265
265
|
processing: false,
|
|
266
266
|
controller: null, // AbortController for current agent run
|
|
267
267
|
permission: null, // { name, args, resolve }
|
|
268
|
+
permissionPreview: [], // 权限审批的内容预览行(渲染在输入框上方,不分隔)
|
|
268
269
|
question: null, // { text, options, resolve } — agent 的 question 工具回调
|
|
269
270
|
picker: null, // 模型选择器 { entries, lines, index, scroll, selectedLine }
|
|
270
271
|
wizard: null, // 首次配置向导 { step, index, scroll, selectedLine, fields, error, lines }
|
|
271
|
-
tasks: agent.tasks ?? [], // task
|
|
272
|
+
tasks: agent.tasks ?? [], // task 工具的任务列表(状态栏显示进度);会话恢复时直接带上,全完成自动收起
|
|
272
273
|
tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }, // 累计 token 用量(状态栏显示)
|
|
273
274
|
ctxCache: { len: -1, tokens: 0 }, // 上下文占用估算缓存(estimateTokens 是 O(n),history 变长才重算)
|
|
274
275
|
reasoning: "", // 思考流缓冲(暗色展示)
|
|
275
276
|
completion: null, // Tab 补全状态 { candidates, index }
|
|
276
277
|
toolStreams: {}, // 各工具的实时输出(按工具名隔离,并行工具互不串扰)
|
|
278
|
+
subOutput: "", // 子 agent 流式输出(滚动显示,最长保留末尾 300 字符)
|
|
279
|
+
currentSub: null, // 当前活跃的子 agent 角色名
|
|
277
280
|
currentTool: null, // 正在执行的工具名(状态栏显示)
|
|
278
281
|
processingStarted: 0, // 本轮处理开始时间(状态栏计时)
|
|
279
282
|
status: "Ready",
|
|
280
283
|
}
|
|
281
284
|
|
|
285
|
+
// 恢复的会话如果所有任务已完成,自动收起 todo 面板(对齐运行时行为)
|
|
286
|
+
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
287
|
+
state.tasks = []
|
|
288
|
+
}
|
|
289
|
+
|
|
282
290
|
// 输入流先过一道滤网:鼠标序列(滚轮)在这里拦截处理,剥净后才交给 keypress 解析,
|
|
283
291
|
// 防止序列残片(如 "64;72;42M")漏进输入框
|
|
284
292
|
const keyStream = new PassThrough()
|
|
@@ -424,7 +432,11 @@ export async function startTUI(agent, opts = {}) {
|
|
|
424
432
|
visibleTasks = [...inProgress, ...pending, ...done].slice(0, MAX_TASK_LINES)
|
|
425
433
|
}
|
|
426
434
|
const taskPanelH = visibleTasks.length
|
|
427
|
-
|
|
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)
|
|
428
440
|
|
|
429
441
|
// 对话区内容行(含流式缓冲);markdown 表格先按显示宽度重排
|
|
430
442
|
const convLines = []
|
|
@@ -498,12 +510,31 @@ export async function startTUI(agent, opts = {}) {
|
|
|
498
510
|
out.push(`${color} ${mark} ${sliceByWidth(t.title, cols - 4)}${ansi.reset}${ansi.clearLine}`)
|
|
499
511
|
}
|
|
500
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
|
+
|
|
501
532
|
// 输入框(全边框,宽 W)
|
|
502
533
|
let borderColor = C.tool
|
|
503
534
|
let title
|
|
504
535
|
if (state.question) {
|
|
505
536
|
borderColor = C.tool
|
|
506
|
-
title =
|
|
537
|
+
title = " Question "
|
|
507
538
|
} else if (state.permission) {
|
|
508
539
|
borderColor = C.warn
|
|
509
540
|
if (state.permission.name === "continue") {
|
|
@@ -571,7 +602,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
571
602
|
}
|
|
572
603
|
} else {
|
|
573
604
|
const taskHint = state.tasks.length > 0
|
|
574
|
-
? ` │
|
|
605
|
+
? ` │ ✓${state.tasks.filter((t) => t.status === "done").length}/${state.tasks.length}`
|
|
575
606
|
: ""
|
|
576
607
|
// token 用量:↑输入 ↓输出 + 缓存命中率(DeepSeek usage 带 prompt_cache_hit/miss_tokens)
|
|
577
608
|
const tk = state.tokens
|
|
@@ -666,6 +697,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
666
697
|
|
|
667
698
|
const callbacks = {
|
|
668
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
|
+
}
|
|
669
708
|
ensureAssistantLabel()
|
|
670
709
|
state.streaming += t
|
|
671
710
|
scheduleRender()
|
|
@@ -683,14 +722,27 @@ export async function startTUI(agent, opts = {}) {
|
|
|
683
722
|
},
|
|
684
723
|
onToolResult: (name, result) => {
|
|
685
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
|
+
}
|
|
686
736
|
const stream = state.toolStreams[name]
|
|
687
737
|
if (stream) {
|
|
688
738
|
const tail = stream.trimEnd().slice(-4000)
|
|
689
739
|
if (tail) pushLine(tail, C.dim)
|
|
690
740
|
delete state.toolStreams[name]
|
|
691
741
|
}
|
|
692
|
-
|
|
693
|
-
|
|
742
|
+
if (!isSubagent) {
|
|
743
|
+
const first = result.split("\n")[0]
|
|
744
|
+
pushLine(` [done] ${name} → ${sliceByWidth(first, 100)}`, C.dim)
|
|
745
|
+
}
|
|
694
746
|
},
|
|
695
747
|
onToolOutput: (name, chunk) => {
|
|
696
748
|
state.toolStreams[name] = (state.toolStreams[name] ?? "") + chunk
|
|
@@ -798,10 +850,8 @@ export async function startTUI(agent, opts = {}) {
|
|
|
798
850
|
pushLine(` [auto] ${name} ${summarize(args)}`, C.warn)
|
|
799
851
|
return Promise.resolve(true)
|
|
800
852
|
}
|
|
801
|
-
//
|
|
802
|
-
|
|
803
|
-
pushLabel(`❯ 权限请求`, ansi.bold + C.warn)
|
|
804
|
-
for (const line of formatPermission(name, args)) pushLine(` ${line}`, C.warn)
|
|
853
|
+
// 预览内容存到 permissionPreview,渲染在输入框上方紧挨"Allow?"提示
|
|
854
|
+
state.permissionPreview = formatPermission(name, args)
|
|
805
855
|
return new Promise((resolve) => {
|
|
806
856
|
state.permission = { name, args, resolve }
|
|
807
857
|
state.status = `Waiting: ${name}`
|
|
@@ -867,6 +917,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
867
917
|
{ name: "/model", group: "Agent", desc: "选择模型" },
|
|
868
918
|
{ name: "/goal", group: "Agent", desc: "设置/查看/取消长期目标" },
|
|
869
919
|
{ name: "/think", group: "Agent", desc: "思维模式与推理强度" },
|
|
920
|
+
{ name: "/init", group: "Tools", desc: "生成项目 AGENTS.md 骨架" },
|
|
870
921
|
{ name: "/skills", group: "Tools", desc: "列出项目技能" },
|
|
871
922
|
{ name: "/mcp", group: "Tools", desc: "管理 MCP server" },
|
|
872
923
|
{ name: "/provider", group: "Config", desc: "管理 provider(增/删/配 key)" },
|
|
@@ -918,6 +969,10 @@ export async function startTUI(agent, opts = {}) {
|
|
|
918
969
|
? data.display.map((l) => ({ text: l.text, color: l.color }))
|
|
919
970
|
: []
|
|
920
971
|
state.tasks = agent.tasks ?? []
|
|
972
|
+
// 切换过来的会话如果任务全完成,自动收起面板
|
|
973
|
+
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
974
|
+
state.tasks = []
|
|
975
|
+
}
|
|
921
976
|
pushLabel(`── 已切换到槽位 ${slotNum}(${data.history.length} 条消息)──`, C.warn)
|
|
922
977
|
render()
|
|
923
978
|
}
|
|
@@ -978,6 +1033,74 @@ export async function startTUI(agent, opts = {}) {
|
|
|
978
1033
|
case "/distill":
|
|
979
1034
|
await runDistill()
|
|
980
1035
|
return
|
|
1036
|
+
case "/init": {
|
|
1037
|
+
const { existsSync } = await import("node:fs")
|
|
1038
|
+
const { writeFile, readFile } = await import("node:fs/promises")
|
|
1039
|
+
const { join, basename } = await import("node:path")
|
|
1040
|
+
const agPath = join(agent.cwd, "AGENTS.md")
|
|
1041
|
+
if (existsSync(agPath)) {
|
|
1042
|
+
pushLine(`AGENTS.md 已存在: ${agPath}`, C.warn)
|
|
1043
|
+
return
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// 探测项目类型与关键信息
|
|
1047
|
+
let name = basename(agent.cwd)
|
|
1048
|
+
let lang = "", cmds = ""
|
|
1049
|
+
|
|
1050
|
+
// Node.js
|
|
1051
|
+
try {
|
|
1052
|
+
const pkg = JSON.parse(await readFile(join(agent.cwd, "package.json"), "utf8"))
|
|
1053
|
+
if (pkg.name) name = pkg.name
|
|
1054
|
+
lang = "Node.js"
|
|
1055
|
+
const ks = Object.keys(pkg.scripts ?? {})
|
|
1056
|
+
if (ks.length) cmds = ks.slice(0, 5).map(k => `- \`npm run ${k}\``).join("\n")
|
|
1057
|
+
} catch {}
|
|
1058
|
+
|
|
1059
|
+
// Python
|
|
1060
|
+
if (!lang) {
|
|
1061
|
+
for (const f of ["requirements.txt", "pyproject.toml", "setup.py", "setup.cfg"]) {
|
|
1062
|
+
if (existsSync(join(agent.cwd, f))) { lang = "Python"; break }
|
|
1063
|
+
}
|
|
1064
|
+
if (lang) cmds = "- `pip install -r requirements.txt`\n- `python -m pytest`"
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Go
|
|
1068
|
+
if (!lang) {
|
|
1069
|
+
if (existsSync(join(agent.cwd, "go.mod"))) {
|
|
1070
|
+
lang = "Go"
|
|
1071
|
+
cmds = "- `go build ./...`\n- `go test ./...`"
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Rust
|
|
1076
|
+
if (!lang) {
|
|
1077
|
+
if (existsSync(join(agent.cwd, "Cargo.toml"))) {
|
|
1078
|
+
lang = "Rust"
|
|
1079
|
+
cmds = "- `cargo build`\n- `cargo test`"
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Java / Kotlin
|
|
1084
|
+
if (!lang) {
|
|
1085
|
+
if (existsSync(join(agent.cwd, "pom.xml"))) { lang = "Java (Maven)"; cmds = "- `mvn test`" }
|
|
1086
|
+
else if (existsSync(join(agent.cwd, "build.gradle")) || existsSync(join(agent.cwd, "build.gradle.kts"))) {
|
|
1087
|
+
lang = "Java/Kotlin (Gradle)"; cmds = "- `gradle test`"
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const lines = [`# ${name}`, ""]
|
|
1092
|
+
if (lang) {
|
|
1093
|
+
lines.push(`## 技术栈`, "", lang, "")
|
|
1094
|
+
if (cmds) lines.push(`## 命令`, "", cmds, "")
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const template = lines.join("\n")
|
|
1098
|
+
await writeFile(agPath, template, "utf8")
|
|
1099
|
+
pushLabel(`❯ Init`, ansi.bold + C.tool)
|
|
1100
|
+
pushLine(`已生成 AGENTS.md → ${agPath}${lang ? ` (${lang})` : ""}`, C.tool)
|
|
1101
|
+
if (lang) pushLine("可继续告诉我项目信息,我来补充约定和结构", C.dim)
|
|
1102
|
+
return
|
|
1103
|
+
}
|
|
981
1104
|
case "/rewind": {
|
|
982
1105
|
const { listCheckpoints, rewind, isGitRepo } = await import("./checkpoint.mjs")
|
|
983
1106
|
if (!isGitRepo(agent.cwd)) {
|
|
@@ -1078,60 +1201,52 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1078
1201
|
const servers = agent.config?.mcp?.servers ?? []
|
|
1079
1202
|
pushLabel(`❯ MCP Servers`, ansi.bold + C.tool)
|
|
1080
1203
|
if (servers.length === 0) {
|
|
1081
|
-
pushLine("(无 MCP server——使用 /mcp add <name> <command>
|
|
1204
|
+
pushLine("(无 MCP server——使用 /mcp add <name> <url|command> 添加)", C.dim)
|
|
1082
1205
|
}
|
|
1083
1206
|
for (const srv of servers) {
|
|
1084
1207
|
const connected = agent.tools.some((t) => t._mcpName === srv.name)
|
|
1085
1208
|
const mark = connected ? "●" : "○"
|
|
1086
1209
|
const color = connected ? C.tool : C.dim
|
|
1087
1210
|
const toolCount = agent.tools.filter((t) => t._mcpName === srv.name).length
|
|
1088
|
-
const desc = srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
1211
|
+
const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
1089
1212
|
pushLine(` ${mark} ${srv.name}: ${desc} (${toolCount} tools)`, color)
|
|
1090
1213
|
}
|
|
1091
1214
|
pushLabel(`❯ 操作`, ansi.bold + C.tool)
|
|
1092
|
-
pushLine(
|
|
1093
|
-
pushLine(
|
|
1094
|
-
pushLine(
|
|
1095
|
-
pushLine(
|
|
1096
|
-
pushLine(
|
|
1215
|
+
pushLine("/mcp add <name> <url|command> [args|headers...]", C.dim)
|
|
1216
|
+
pushLine(" URL 自动识别: https://… → HTTP, ws://… → WebSocket, 其他 → stdio 命令", C.dim)
|
|
1217
|
+
pushLine(" 例: /mcp add myapi https://api.example.com/mcp Authorization=\"Bearer x\"", C.dim)
|
|
1218
|
+
pushLine(" 例: /mcp add github npx -y @modelcontextprotocol/server-github", C.dim)
|
|
1219
|
+
pushLine(`/mcp remove <name> 断开并移除`, C.dim)
|
|
1220
|
+
pushLine(`/mcp connect <name> 重连已配置的 server`, C.dim)
|
|
1097
1221
|
return
|
|
1098
1222
|
}
|
|
1099
|
-
// ---- /mcp add <name> <command> [args...] (
|
|
1100
|
-
|
|
1223
|
+
// ---- /mcp add <name> <url|command> [args|headers...] (统一入口,自动识别传输类型) ----
|
|
1224
|
+
// url / ws 子命令作为别名保留(兼容旧配置)
|
|
1225
|
+
if (sub === "add" || sub === "url" || sub === "ws") {
|
|
1101
1226
|
const args = rest.slice(1)
|
|
1102
1227
|
if (args.length < 2) {
|
|
1103
|
-
pushLine("用法: /mcp add <name> <command> [args...]", C.error)
|
|
1104
|
-
pushLine("
|
|
1228
|
+
pushLine("用法: /mcp add <name> <url|command> [args|headers...]", C.error)
|
|
1229
|
+
pushLine(" URL 自动识别: https://… → HTTP, ws://… → WebSocket, 其他 → stdio 命令", C.dim)
|
|
1105
1230
|
return
|
|
1106
1231
|
}
|
|
1107
1232
|
const name = args[0]
|
|
1108
|
-
const
|
|
1109
|
-
const
|
|
1233
|
+
const second = args[1]
|
|
1234
|
+
const extras = args.slice(2)
|
|
1110
1235
|
const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
1111
1236
|
if (existing) { pushLine(`[mcp] "${name}" 已存在,用 /mcp remove ${name} 先移除`, C.error); return }
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
if (
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
const name = args[0]
|
|
1125
|
-
const url = args[1]
|
|
1126
|
-
const headerPairs = args.slice(2)
|
|
1127
|
-
const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
1128
|
-
if (existing) { pushLine(`[mcp] "${name}" 已存在,用 /mcp remove ${name} 先移除`, C.error); return }
|
|
1129
|
-
const headers = {}
|
|
1130
|
-
for (const pair of headerPairs) {
|
|
1131
|
-
const eq = pair.indexOf("=")
|
|
1132
|
-
if (eq > 0) headers[pair.slice(0, eq)] = pair.slice(eq + 1).replace(/^["']|["']$/g, "")
|
|
1237
|
+
|
|
1238
|
+
const isWS = /^wss?:\/\//.test(second)
|
|
1239
|
+
const isHTTP = /^https?:\/\//.test(second)
|
|
1240
|
+
let srv
|
|
1241
|
+
if (isWS || sub === "ws") {
|
|
1242
|
+
const headers = parseHeaders(extras)
|
|
1243
|
+
srv = { name, wsUrl: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
1244
|
+
} else if (isHTTP || sub === "url") {
|
|
1245
|
+
const headers = parseHeaders(extras)
|
|
1246
|
+
srv = { name, url: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
1247
|
+
} else {
|
|
1248
|
+
srv = { name, command: second, args: extras.length > 0 ? extras : undefined }
|
|
1133
1249
|
}
|
|
1134
|
-
const srv = { name, url, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
1135
1250
|
await addAndConnect(srv)
|
|
1136
1251
|
return
|
|
1137
1252
|
}
|
|
@@ -1152,7 +1267,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1152
1267
|
const name = rest[1]
|
|
1153
1268
|
if (!name) { pushLine("用法: /mcp connect <name>", C.error); return }
|
|
1154
1269
|
const srv = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
1155
|
-
if (!srv) { pushLine(`[mcp] "${name}" 未在配置中找到(先用 /mcp add
|
|
1270
|
+
if (!srv) { pushLine(`[mcp] "${name}" 未在配置中找到(先用 /mcp add 添加)`, C.error); return }
|
|
1156
1271
|
const { removeMcpTools, connectMcpServer } = await import("./mcp.mjs")
|
|
1157
1272
|
removeMcpTools(agent, name)
|
|
1158
1273
|
try {
|
|
@@ -1166,16 +1281,27 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1166
1281
|
}
|
|
1167
1282
|
return
|
|
1168
1283
|
}
|
|
1169
|
-
pushLine(`未知子命令: ${sub}(/mcp list | add |
|
|
1284
|
+
pushLine(`未知子命令: ${sub}(/mcp list | add | remove | connect)`, C.error)
|
|
1170
1285
|
return
|
|
1171
1286
|
}
|
|
1172
1287
|
|
|
1288
|
+
// ---- header 解析(/mcp add 共享)----
|
|
1289
|
+
function parseHeaders(pairs) {
|
|
1290
|
+
const headers = {}
|
|
1291
|
+
for (const pair of pairs) {
|
|
1292
|
+
const eq = pair.indexOf("=")
|
|
1293
|
+
if (eq > 0) headers[pair.slice(0, eq)] = pair.slice(eq + 1).replace(/^["']|["']$/g, "")
|
|
1294
|
+
}
|
|
1295
|
+
return headers
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1173
1298
|
// ---- /mcp 共享 helper: 保存配置 + 连接 ----
|
|
1174
1299
|
async function addAndConnect(srv) {
|
|
1175
1300
|
await persistRaw((raw) => {
|
|
1176
1301
|
raw.mcp ??= { servers: [] }
|
|
1177
1302
|
const entry = { name: srv.name }
|
|
1178
1303
|
if (srv.url) { entry.url = srv.url; if (srv.headers) entry.headers = srv.headers }
|
|
1304
|
+
else if (srv.wsUrl) { entry.wsUrl = srv.wsUrl; if (srv.headers) entry.headers = srv.headers }
|
|
1179
1305
|
else { entry.command = srv.command; if (srv.args) entry.args = srv.args }
|
|
1180
1306
|
raw.mcp.servers.push(entry)
|
|
1181
1307
|
})
|
|
@@ -1188,7 +1314,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1188
1314
|
const tools = await connectMcpServer(srv)
|
|
1189
1315
|
agent.tools.push(...tools)
|
|
1190
1316
|
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
1191
|
-
const desc = srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
1317
|
+
const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
1192
1318
|
pushLine(`${srv.name} (${desc}) 已连接,${tools.length} 个工具:`, C.tool)
|
|
1193
1319
|
for (const t of tools) pushLine(` ${t.name}: ${t.description.slice(0, 100)}`, C.dim)
|
|
1194
1320
|
} catch (error) {
|
|
@@ -1226,12 +1352,23 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1226
1352
|
// ---- /think on / off ----
|
|
1227
1353
|
if (sub === "on" || sub === "off") {
|
|
1228
1354
|
const enable = sub === "on"
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1355
|
+
// 仅用 reasoning_effort 的模型(K3):不碰 thinking 字段,只设/删 reasoningEffort
|
|
1356
|
+
const { specForModel } = await import("./config.mjs")
|
|
1357
|
+
const spec = specForModel(cur.model)
|
|
1358
|
+
if (spec.thinkApi === "effort") {
|
|
1359
|
+
// 仅用 reasoning_effort 的模型(K3 / Qwen):不碰 thinking 字段
|
|
1360
|
+
if (!enable) delete cur.reasoningEffort
|
|
1361
|
+
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
1362
|
+
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
1363
|
+
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
1364
|
+
} else {
|
|
1365
|
+
cur.thinking = enable ? { type: "enabled" } : { type: "disabled" }
|
|
1366
|
+
if (!enable) delete cur.reasoningEffort
|
|
1367
|
+
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
1368
|
+
await syncProviderField("thinking", cur.thinking)
|
|
1369
|
+
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
1370
|
+
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
1371
|
+
}
|
|
1235
1372
|
pushLabel(`❯ Think`, ansi.bold + C.tool)
|
|
1236
1373
|
pushLine(`思维模式已${enable ? "开启" : "关闭"}`, C.tool)
|
|
1237
1374
|
if (enable) pushLine(`推理强度: ${cur.reasoningEffort}`, C.dim)
|
|
@@ -1492,7 +1629,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1492
1629
|
if (cmd === "/config" && argIndex === 0) return match(["embedkey", "set"])
|
|
1493
1630
|
if (cmd === "/goal" && argIndex === 0) return match(["set", "cancel"])
|
|
1494
1631
|
if (cmd === "/mcp") {
|
|
1495
|
-
if (argIndex === 0) return match(["add", "url", "remove", "connect", "list"])
|
|
1632
|
+
if (argIndex === 0) return match(["add", "url", "ws", "remove", "connect", "list"])
|
|
1496
1633
|
if (argIndex === 1 && (parts[1] === "remove" || parts[1] === "connect")) return match((agent.config?.mcp?.servers ?? []).map((s) => s.name))
|
|
1497
1634
|
}
|
|
1498
1635
|
return []
|
|
@@ -1864,6 +2001,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1864
2001
|
if (validKeys.includes(answer) || key.name === "escape") {
|
|
1865
2002
|
const { resolve, name } = state.permission
|
|
1866
2003
|
state.permission = null
|
|
2004
|
+
state.permissionPreview = []
|
|
1867
2005
|
state.status = "Processing..."
|
|
1868
2006
|
if (answer === "a" && !isContinue) {
|
|
1869
2007
|
agent.autoApprove = true
|
|
@@ -2076,8 +2214,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2076
2214
|
}
|
|
2077
2215
|
|
|
2078
2216
|
// 可打印字符 / 粘贴(str 可能一次多个字符);Tab 一律转成两个空格(\t 显示宽度不定,会顶破输入框)
|
|
2217
|
+
// \r\n 在 Windows raw mode 下可能漏进来冲乱页面
|
|
2079
2218
|
if (str && !key.ctrl && !key.meta) {
|
|
2080
|
-
const chars = [...str.replace(
|
|
2219
|
+
const chars = [...str.replace(/[\r\n]+/g, "").replace(/\t/g, " ")]
|
|
2081
2220
|
state.input.splice(state.cursor, 0, ...chars)
|
|
2082
2221
|
state.cursor += chars.length
|
|
2083
2222
|
render()
|
|
@@ -2099,7 +2238,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2099
2238
|
state.lines = [...opts.restored.display.map((l) => ({ text: l.text, color: l.color })), ...state.lines]
|
|
2100
2239
|
pushLabel(`── 已恢复上次会话(退出前原样回放);/new 开始新会话 ──`, C.warn)
|
|
2101
2240
|
} else if (opts.restored?.history?.length) {
|
|
2102
|
-
|
|
2241
|
+
// 重建对话区:user/assistant 消息逐条展示,tool 结果行只保留首行摘要
|
|
2242
|
+
for (let i = 0; i < opts.restored.history.length; i++) {
|
|
2243
|
+
const m = opts.restored.history[i]
|
|
2103
2244
|
if (m.role === "user") {
|
|
2104
2245
|
if (typeof m.content === "string" && m.content.startsWith("[System reminder:")) continue
|
|
2105
2246
|
pushLabel(`❯ You:`, ansi.bold + C.user)
|
|
@@ -2108,10 +2249,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2108
2249
|
pushLabel(`❯ ThinCoder:`, ansi.bold + C.assistant)
|
|
2109
2250
|
if (typeof m.content === "string" && m.content) pushLine(m.content, C.text)
|
|
2110
2251
|
for (const tc of m.tool_calls ?? []) {
|
|
2111
|
-
|
|
2252
|
+
// 找到下一条对应的 tool 结果,显示首行摘要
|
|
2253
|
+
const toolResult = opts.restored.history[i + 1]
|
|
2254
|
+
const hasResult = toolResult?.role === "tool" && toolResult?.tool_call_id === tc.id
|
|
2255
|
+
const summary = hasResult ? " → " + sliceByWidth(String(toolResult.content).split("\n")[0], 80) : ""
|
|
2256
|
+
pushLine(` [tool] ${tc.function?.name ?? "?"}${summary}`, C.tool)
|
|
2112
2257
|
}
|
|
2113
2258
|
}
|
|
2114
|
-
// tool
|
|
2259
|
+
// tool 消息本身不单独渲染——已在 assistant 的 tool_calls 后以摘要形式展示
|
|
2115
2260
|
}
|
|
2116
2261
|
pushLabel(`── 已恢复上次会话(${opts.restored.history.length} 条消息);/new 开始新会话 ──`, C.warn)
|
|
2117
2262
|
}
|