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/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
- const convH = Math.max(1, rows - headerH - inputBoxH - statusH - pickerH - taskPanelH)
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 = ` ${sliceByWidth(state.question.text, W - 6)} `
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
- ? ` │ ▶${state.tasks.filter((t) => t.status === "done").length}/${state.tasks.length}`
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
- const first = result.split("\n")[0]
693
- pushLine(` [done] ${name} → ${sliceByWidth(first, 100)}`, C.dim)
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> [args] 添加)", C.dim)
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(`/mcp add <name> <command> [args...] 添加 stdio server`, C.dim)
1093
- pushLine(`/mcp url <name> <url> [headers...] 添加 HTTP server`, C.dim)
1094
- pushLine(`/mcp remove <name> 断开并移除 server`, C.dim)
1095
- pushLine(`/mcp connect <name> 重连已配置的 server`, C.dim)
1096
- pushLine("配置持久化到 config.json mcp.servers[]", C.dim)
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...] (stdio) ----
1100
- if (sub === "add") {
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(" 例: /mcp add github npx -y @modelcontextprotocol/server-github", C.dim)
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 command = args[1]
1109
- const cmdArgs = args.slice(2)
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
- const srv = { name, command, args: cmdArgs.length > 0 ? cmdArgs : undefined }
1113
- await addAndConnect(srv)
1114
- return
1115
- }
1116
- // ---- /mcp url <name> <url> [key=value...] (HTTP) ----
1117
- if (sub === "url") {
1118
- const args = rest.slice(1)
1119
- if (args.length < 2) {
1120
- pushLine("用法: /mcp url <name> <url> [header=value...]", C.error)
1121
- pushLine(" 例: /mcp url myapi https://api.example.com/mcp Authorization=\"Bearer token123\"", C.dim)
1122
- return
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 /mcp url)`, C.error); return }
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 | url | remove | connect)`, C.error)
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
- cur.thinking = enable ? { type: "enabled" } : { type: "disabled" }
1230
- if (!enable) delete cur.reasoningEffort
1231
- else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
1232
- await syncProviderField("thinking", cur.thinking)
1233
- if (!enable) await syncProviderField("reasoningEffort", undefined)
1234
- else await syncProviderField("reasoningEffort", cur.reasoningEffort)
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(/\r/g, "").replace(/\t/g, " ")]
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
- for (const m of opts.restored.history) {
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
- pushLine(` [tool] ${tc.function?.name ?? "?"}`, C.tool)
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
  }