thincoder 0.7.2 → 0.7.4

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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * tui.mjs — 裸 ANSI 终端 UI
3
3
  * 零依赖:raw mode 键盘输入、ANSI 转义渲染、自研宽字符换行。
4
- * 布局:header / 对话区(可滚动)/ todo 面板(有任务时)/ 输入框 / 状态栏。
4
+ * 布局:header / 对话区 (可滚动)/ todo 面板 (有任务时)/ 输入框 / 状态栏。
5
5
  */
6
6
 
7
7
  import { emitKeypressEvents } from "node:readline"
@@ -11,7 +11,7 @@ import { existsSync, readFileSync } from "node:fs"
11
11
  import { runAgent, ContinueError } from "./agent.mjs"
12
12
  import { estimateTokens } from "./context.mjs"
13
13
  import { saveSession, clearSession, archiveCurrent, listSlots, switchToSlot, sessionPath } from "./session.mjs"
14
- import { PROVIDER_PRESETS as PRESETS } from "./config.mjs"
14
+ import { PROVIDER_PRESETS as PRESETS, specForModel } from "./config.mjs"
15
15
  import { closeAllMcp } from "./mcp.mjs"
16
16
 
17
17
  // ---------------------------------------------------------------- ANSI 工具
@@ -22,7 +22,7 @@ const ansi = {
22
22
  showCursor: `${ESC}[?25h`,
23
23
  altBuffer: `${ESC}[?1049h`,
24
24
  mainBuffer: `${ESC}[?1049l`,
25
- mouseOn: `${ESC}[?1000h${ESC}[?1006h`, // 基本鼠标 + SGR 扩展坐标(滚轮上报)
25
+ mouseOn: `${ESC}[?1000h${ESC}[?1006h`, // 基本鼠标 + SGR 扩展坐标 (滚轮上报)
26
26
  mouseOff: `${ESC}[?1000l${ESC}[?1006l`,
27
27
  home: `${ESC}[H`,
28
28
  clearLine: `${ESC}[K`,
@@ -34,10 +34,10 @@ const ansi = {
34
34
  }
35
35
 
36
36
  const C = {
37
- user: ansi.fg(4), // blue(标签)
38
- assistant: ansi.fg(2), // green(标签)
39
- text: ansi.fg(7), // white(对话正文)
40
- reason: `${ESC}[2m${ESC}[3m`, // dim + italic(思考流)
37
+ user: ansi.fg(4), // blue (标签)
38
+ assistant: ansi.fg(2), // green (标签)
39
+ text: ansi.fg(7), // white (对话正文)
40
+ reason: `${ESC}[2m${ESC}[3m`, // dim + italic (思考流)
41
41
  tool: ansi.fg(6), // cyan
42
42
  error: ansi.fg(1), // red
43
43
  dim: ansi.gray,
@@ -100,7 +100,7 @@ const isTableRow = (line) => (line.match(/\|/g) ?? []).length >= 2
100
100
  const isTableSeparator = (line) => /^\s*\|?[\s:|-]+\|[\s:|-]*$/.test(line) && line.includes("-")
101
101
 
102
102
  /**
103
- * 识别文本中的 markdown 表格块,按显示宽度重排(修 CJK 错位)。
103
+ * 识别文本中的 markdown 表格块,按显示宽度重排 (修 CJK 错位)。
104
104
  * width 为可用显示宽度;过宽的表格按列收缩。非表格行原样保留。
105
105
  */
106
106
  export function formatTables(text, width) {
@@ -135,7 +135,7 @@ function renderTable(block, width) {
135
135
  const colCount = Math.max(...rows.map((r) => r.length))
136
136
  for (const r of rows) while (r.length < colCount) r.push("")
137
137
 
138
- // 列宽:先按内容,超宽则从最宽列开始收缩(收缩到至少 3)
138
+ // 列宽:先按内容,超宽则从最宽列开始收缩 (收缩到至少 3)
139
139
  const widths = Array.from({ length: colCount }, (_, c) =>
140
140
  Math.max(3, ...rows.map((r) => stringWidth(r[c] ?? ""))),
141
141
  )
@@ -145,7 +145,7 @@ function renderTable(block, width) {
145
145
  widths[widest]--
146
146
  }
147
147
 
148
- // 单元格渲染:sliceByWidth 截断(表头单行),padByWidth 补齐
148
+ // 单元格渲染:sliceByWidth 截断 (表头单行),padByWidth 补齐
149
149
  const fmtCell = (text, ci) => padByWidth(sliceByWidth(text, widths[ci]), widths[ci])
150
150
  const fmtRow = (cells) => "│ " + cells.map((c, i) => fmtCell(c, i)).join(" │ ") + " │"
151
151
 
@@ -153,7 +153,7 @@ function renderTable(block, width) {
153
153
  const separator = "├" + widths.map((w) => "─".repeat(w + 2)).join("┼") + "┤"
154
154
 
155
155
  const out = []
156
- // 表头:单行截断(表头通常是短标签,折行不如截断直观)
156
+ // 表头:单行截断 (表头通常是短标签,折行不如截断直观)
157
157
  out.push(fmtRow(rows[0]))
158
158
  out.push(separator)
159
159
 
@@ -170,7 +170,7 @@ function renderTable(block, width) {
170
170
  return out
171
171
  }
172
172
 
173
- /** 输入区布局:把输入缓冲折行,同时算出光标的 (行, 列) 位置(显示宽度) */
173
+ /** 输入区布局:把输入缓冲折行,同时算出光标的 (行, 列) 位置 (显示宽度) */
174
174
  export function layoutInput(chars, cursor, width) {
175
175
  const PROMPT = "▸ "
176
176
  const lines = []
@@ -207,7 +207,7 @@ export function layoutInput(chars, cursor, width) {
207
207
  }
208
208
 
209
209
  /**
210
- * 显示净化:控制字符会破坏终端网格数学(\r 回车覆盖、\t 宽度误判致整帧错位、ANSI/响铃冲屏)。
210
+ * 显示净化:控制字符会破坏终端网格数学 (\r 回车覆盖、\t 宽度误判致整帧错位、ANSI/响铃冲屏)。
211
211
  * 只动显示层——模型看到的工具结果原文不变;session 里已存的脏 display 回放时也经此净化。
212
212
  */
213
213
  const ANSI_SEQUENCE_RE = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][0-9A-B]|\x1b[=>#][0-9]?/g
@@ -221,7 +221,7 @@ export function sanitizeDisplay(s) {
221
221
  .replace(/\n+$/, "")
222
222
  }
223
223
 
224
- /** 文本按宽度折行(保留 \n),返回行数组 */
224
+ /** 文本按宽度折行 (保留 \n),返回行数组 */
225
225
  export function wrapText(text, width) {
226
226
  const lines = []
227
227
  for (const rawLine of text.split("\n")) {
@@ -256,8 +256,8 @@ export async function startTUI(agent, opts = {}) {
256
256
 
257
257
  const state = {
258
258
  lines: [], // 对话区行:{ text, color }
259
- streaming: "", // 当前流式缓冲
260
- input: [], // 输入缓冲区(码点数组)
259
+ streaming: "", // current流式缓冲
260
+ input: [], // 输入缓冲区 (码点数组)
261
261
  cursor: 0,
262
262
  history: [],
263
263
  historyIndex: -1,
@@ -265,30 +265,29 @@ 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
+ permissionPreview: [], // 权限审批的内容预览行 (渲染在输入框上方,不分隔)
269
269
  question: null, // { text, options, resolve } — agent 的 question 工具回调
270
270
  picker: null, // 模型选择器 { entries, lines, index, scroll, selectedLine }
271
- wizard: null, // 首次配置向导 { step, index, scroll, selectedLine, fields, error, lines }
272
- tasks: agent.tasks ?? [], // task 工具的任务列表(状态栏显示进度);会话恢复时直接带上,全完成自动收起
273
- tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }, // 累计 token 用量(状态栏显示)
274
- ctxCache: { len: -1, tokens: 0 }, // 上下文占用估算缓存(estimateTokens 是 O(n),history 变长才重算)
275
- reasoning: "", // 思考流缓冲(暗色展示)
271
+ wizard: null, // 首次Config向导 { step, index, scroll, selectedLine, fields, error, lines }
272
+ tasks: agent.tasks ?? [], // task 工具的任务列表 (状态栏显示进度);会话恢复时直接带上,全完成自动收起
273
+ tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }, // 累计 token 用量 (状态栏显示)
274
+ ctxCache: { len: -1, tokens: 0 }, // 上下文占用估算缓存 (estimateTokens 是 O(n),history 变长才重算)
275
+ reasoning: "", // 思考流缓冲 (暗色展示)
276
276
  completion: null, // Tab 补全状态 { candidates, index }
277
- toolStreams: {}, // 各工具的实时输出(按工具名隔离,并行工具互不串扰)
278
- subOutput: "", // 子 agent 流式输出(滚动显示,最长保留末尾 300 字符)
279
- currentSub: null, // 当前活跃的子 agent 角色名
280
- currentTool: null, // 正在执行的工具名(状态栏显示)
281
- processingStarted: 0, // 本轮处理开始时间(状态栏计时)
277
+ toolStreams: {}, // 各工具的实时输出 (按工具名隔离,并行工具互不串扰)
278
+ subTasks: {}, // 子 agent 面板:{ roleName: { role, text, done } },每 role 一行,完成后标记 done 停留片刻
279
+ currentTool: null, // 正在执行的工具名 (状态栏显示)
280
+ processingStarted: 0, // 本轮处理开始时间 (状态栏计时)
282
281
  status: "Ready",
283
282
  }
284
283
 
285
- // 恢复的会话如果所有任务已完成,自动收起 todo 面板(对齐运行时行为)
284
+ // 恢复的会话如果所有任务completed,自动收起 todo 面板 (对齐运行时行为)
286
285
  if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
287
286
  state.tasks = []
288
287
  }
289
288
 
290
- // 输入流先过一道滤网:鼠标序列(滚轮)在这里拦截处理,剥净后才交给 keypress 解析,
291
- // 防止序列残片(如 "64;72;42M")漏进输入框
289
+ // 输入流先过一道滤网:鼠标序列 (滚轮)在这里拦截处理,剥净后才交给 keypress 解析,
290
+ // 防止序列残片 (如 "64;72;42M")漏进输入框
292
291
  const keyStream = new PassThrough()
293
292
  let mousePending = "" // 跨 chunk 的不完整鼠标序列尾部
294
293
  let lastRenderedScroll = 0
@@ -300,7 +299,7 @@ export async function startTUI(agent, opts = {}) {
300
299
  let text = mousePending + chunk.toString("utf8")
301
300
  mousePending = ""
302
301
 
303
- // 滚轮:\x1b[<64;…M 上滚,\x1b[<65;…M 下滚(每次 3 行)
302
+ // 滚轮:\x1b[<64;…M 上滚,\x1b[<65;…M 下滚 (每次 3 行)
304
303
  for (const m of text.matchAll(/\x1b\[<(\d+);\d+;\d+([Mm])/g)) {
305
304
  if (Number(m[1]) === 64) {
306
305
  state.scroll += 3
@@ -328,14 +327,14 @@ export async function startTUI(agent, opts = {}) {
328
327
  const cleanup = () => {
329
328
  if (cleanedUp) return
330
329
  cleanedUp = true
331
- // 退出前保存会话(同步写);先归档当前到槽位,再落新——不丢
330
+ // 退出前保存会话 (同步写);先归档current到槽位,再落新——不丢
332
331
  try {
333
332
  archiveCurrent(agent.cwd)
334
333
  saveSession(agent, state.lines)
335
334
  } catch {
336
335
  // 存失败不耽误退出
337
336
  }
338
- // 关闭 MCP stdio 子进程,不留孤儿
337
+ // Off MCP stdio 子进程,不留孤儿
339
338
  try {
340
339
  closeAllMcp(agent)
341
340
  } catch {
@@ -348,7 +347,7 @@ export async function startTUI(agent, opts = {}) {
348
347
 
349
348
  const pushLine = (text, color) => {
350
349
  state.lines.push({ text, color })
351
- if (state.lines.length > 5000) state.lines.splice(0, 1000) // 防无限增长
350
+ if (state.lines.length > 5000) state.lines.splice(0, 1000) // 防none限增长
352
351
  render()
353
352
  }
354
353
 
@@ -359,7 +358,7 @@ export async function startTUI(agent, opts = {}) {
359
358
  render()
360
359
  }
361
360
 
362
- // 每轮对话只打一次助手标签(首个 token 或首个工具调用时)
361
+ // 每轮对话只打一次助手标签 (首个 token 或首个工具调用时)
363
362
  let assistantLabeled = false
364
363
  const ensureAssistantLabel = () => {
365
364
  if (!assistantLabeled) {
@@ -370,11 +369,11 @@ export async function startTUI(agent, opts = {}) {
370
369
 
371
370
  // ---------------------------------------------------------- 渲染
372
371
 
373
- // 帧去重 + 流式限流:内容没变的帧不重写(防闪屏);token 洪流合并到 ~25fps
372
+ // 帧去重 + 流式限流:内容没变的帧不重写 (防闪屏);token 洪流合并到 ~25fps
374
373
  let lastFrame = ""
375
374
  let renderTimer = null
376
375
 
377
- /** 流式期间的限流渲染(trailing edge:最后一次变化一定渲染到) */
376
+ /** 流式期间的限流渲染 (trailing edge:最后一次变化一定渲染到) */
378
377
  function scheduleRender() {
379
378
  if (renderTimer) return
380
379
  renderTimer = setTimeout(() => {
@@ -389,10 +388,11 @@ export async function startTUI(agent, opts = {}) {
389
388
  const model = agent.provider.model
390
389
  const thinking = agent.provider.thinking
391
390
  const effort = agent.provider.reasoningEffort
391
+ const isMultimodal = specForModel(model).multimodal
392
392
  const thinkBadge = thinking?.type === "disabled" ? "│ think: off"
393
393
  : effort ? `│ think: ${effort}` : thinking?.type === "enabled" ? "│ think: on" : ""
394
394
 
395
- // 输入区:全边框盒,宽度 W(所有输出行严格 ≤ cols-1,防自动折行错位)
395
+ // 输入区:全边框盒,宽度 W (所有输出行严格 ≤ cols-1,防自动折行错位)
396
396
  const W = Math.max(20, cols - 1)
397
397
  const layout = layoutInput(state.input, state.cursor, W - 4)
398
398
  // 最多显示 5 行;超出时以光标所在行为中心滚动
@@ -402,12 +402,12 @@ export async function startTUI(agent, opts = {}) {
402
402
  inputOffset = Math.min(layout.cursorLine, layout.lines.length - MAX_INPUT_LINES)
403
403
  }
404
404
  const inputLines = layout.lines.slice(inputOffset, inputOffset + MAX_INPUT_LINES)
405
- // question 模式下输入框显示选项/答案草稿,而不是普通输入(高度也要跟着走)
405
+ // question 模式下输入框显示选项/答案草稿,而不是普通输入 (高度也要跟着走)
406
406
  let boxLines = inputLines
407
407
  if (state.question) {
408
408
  const q = state.question
409
409
  if (q.options.length > 0) {
410
- // 选项窗口:只显示选中项 ±2,选项过多时防输入框无限增高撑破锚定布局
410
+ // 选项窗口:只显示选中项 ±2,选项过多时防输入框none限增高撑破锚定布局
411
411
  const sel = q.selected ?? 0
412
412
  const QWIN = 5
413
413
  const start = Math.max(0, Math.min(sel - 2, q.options.length - QWIN))
@@ -422,7 +422,7 @@ export async function startTUI(agent, opts = {}) {
422
422
 
423
423
  const headerH = 1
424
424
  const statusH = 1
425
- // 浮层(模型选择器 / 初始配置向导)打开时,在对话区下方预留一块(标题 + 列表窗口)
425
+ // 浮层 (模型选择器 / 初始Config向导)打开时,在对话区下方预留一块 (标题 + 列表窗口)
426
426
  const overlay = state.picker ?? state.wizard
427
427
  const pickerH = overlay
428
428
  ? Math.min(overlay.lines.length + 1, Math.max(6, rows - 12))
@@ -440,9 +440,11 @@ export async function startTUI(agent, opts = {}) {
440
440
  visibleTasks = [...inProgress, ...pending, ...done].slice(0, MAX_TASK_LINES)
441
441
  }
442
442
  const taskPanelH = visibleTasks.length
443
- // 子 agent 流式输出占位(显示时占最多 2 行)
444
- const subOutLen = (state.subOutput && state.processing) ? wrapText(state.subOutput, W - 8).slice(-2).length : 0
445
- // 权限预览占位:字符数之外再封顶显示行数(rows-8),多行短行也能把帧撑过终端高度,破坏锚定布局
443
+ // 子 agent 面板 (subTasks):每活跃子 agent 一行,上方对话区下方,最多 4 行折叠
444
+ const activeSubs = Object.values(state.subTasks).filter((s) => !s.done)
445
+ const subPanelH = Math.min(activeSubs.length, 4)
446
+ const subOutLen = subPanelH
447
+ // 权限预览占位:字符数之外再封顶显示行数 (rows-8),多行短行也能把帧撑过终端高度,破坏锚定布局
446
448
  let permPreviewLines = []
447
449
  if (state.permission) {
448
450
  const maxLines = Math.max(1, rows - 8)
@@ -456,7 +458,7 @@ export async function startTUI(agent, opts = {}) {
456
458
  const permPreviewLen = state.permission ? 1 + permPreviewLines.length : 0
457
459
  const convH = Math.max(1, rows - headerH - inputBoxH - statusH - pickerH - taskPanelH - subOutLen - permPreviewLen)
458
460
 
459
- // 对话区内容行(含流式缓冲);markdown 表格先按显示宽度重排
461
+ // 对话区内容行 (含流式缓冲);markdown 表格先按显示宽度重排
460
462
  const convLines = []
461
463
  for (const l of state.lines) {
462
464
  for (const line of formatTables(sanitizeDisplay(l.text), cols - 1)) {
@@ -465,7 +467,7 @@ export async function startTUI(agent, opts = {}) {
465
467
  }
466
468
  }
467
469
  }
468
- // 思考流(暗色)在正文流之前
470
+ // 思考流 (暗色)在正文流之前
469
471
  if (state.reasoning) {
470
472
  for (const wrapped of wrapText(sanitizeDisplay(state.reasoning), cols - 1)) {
471
473
  convLines.push({ text: wrapped, color: C.reason })
@@ -478,7 +480,7 @@ export async function startTUI(agent, opts = {}) {
478
480
  }
479
481
  }
480
482
  }
481
- // 工具实时输出(暗色,只保留末尾防刷屏;按工具名隔离防止并行工具串扰)
483
+ // 工具实时输出 (暗色,只保留末尾防刷屏;按工具名隔离防止并行工具串扰)
482
484
  const allStreams = Object.values(state.toolStreams).join("")
483
485
  if (allStreams) {
484
486
  const tail = sanitizeDisplay(allStreams.slice(-4000))
@@ -494,26 +496,26 @@ export async function startTUI(agent, opts = {}) {
494
496
 
495
497
  const out = [ansi.home]
496
498
 
497
- // header(超宽截断,防终端折行)
499
+ // header (超宽截断,防终端折行)
498
500
  out.push(
499
501
  `${ansi.bold}${C.tool} ThinCoder ${ansi.reset}${ansi.dim}│ ${sliceByWidth(model, 30)}${thinkBadge ? " " + thinkBadge : ""} │ ${sliceByWidth(basename(agent.cwd), Math.max(10, cols - 60))}${ansi.reset}${ansi.clearLine}`,
500
502
  )
501
503
 
502
- // 对话区(不足部分补空行,把输入框钉在底部)
504
+ // 对话区 (不足部分补空行,把输入框钉在底部)
503
505
  const pad = convH - visible.length
504
506
  for (let i = 0; i < pad; i++) out.push(ansi.clearLine)
505
507
  for (const l of visible) {
506
508
  out.push(`${l.color}${l.text}${ansi.reset}${ansi.clearLine}`)
507
509
  }
508
510
 
509
- // 浮层(模型选择器 / 初始配置向导):列表滚动跟随选中行
511
+ // 浮层 (模型选择器 / 初始Config向导):列表滚动跟随选中行
510
512
  if (overlay) {
511
513
  const winH = pickerH - 1
512
514
  if (overlay.selectedLine < overlay.scroll) overlay.scroll = overlay.selectedLine
513
515
  if (overlay.selectedLine >= overlay.scroll + winH) overlay.scroll = overlay.selectedLine - winH + 1
514
516
  const start = Math.max(0, Math.min(overlay.scroll, Math.max(0, overlay.lines.length - winH)))
515
517
  const shown = overlay.lines.slice(start, start + winH)
516
- const overlayTitle = state.picker ? ` ❯ ${state.picker.title} ` : " ❯ 初始配置 "
518
+ const overlayTitle = state.picker ? ` ❯ ${state.picker.title} ` : " ❯ 初始Config "
517
519
  out.push(`${ansi.bold}${C.tool}${overlayTitle}${ansi.reset}${ansi.dim}${state.picker ? "(↑↓ 移动, Enter 确认, Esc 取消)" : ""}${ansi.reset}${ansi.clearLine}`)
518
520
  for (const l of shown) {
519
521
  out.push(`${l.color}${sliceByWidth(l.text, cols - 1)}${ansi.reset}${ansi.clearLine}`)
@@ -521,23 +523,29 @@ export async function startTUI(agent, opts = {}) {
521
523
  for (let i = shown.length; i < winH; i++) out.push(ansi.clearLine)
522
524
  }
523
525
 
524
- // todo 面板(对话区与输入框之间):▶ in_progress / ✓ done(删除线) / ○ pending
526
+ // todo 面板 (对话区与输入框之间):▶ in_progress / ✓ done(删除线) / ○ pending
525
527
  for (const t of visibleTasks) {
526
528
  const mark = t.status === "done" ? "✓" : t.status === "in_progress" ? "▶" : "○"
527
529
  const color = t.status === "done" ? `${C.dim}${ESC}[9m` : t.status === "in_progress" ? C.tool : C.text
528
530
  out.push(`${color} ${mark} ${sliceByWidth(t.title, cols - 4)}${ansi.reset}${ansi.clearLine}`)
529
531
  }
530
532
 
531
- // 子 agent 流式输出(最多 2 行,滚动显示最新内容)
532
- if (state.subOutput && state.processing) {
533
- const lines = wrapText(state.subOutput, W - 8)
534
- const tail = lines.slice(-2)
535
- for (const l of tail) {
536
- out.push(`${C.dim}[${state.currentSub}] ${l}${ansi.reset}${ansi.clearLine}`)
533
+ // 子 agent 面板:每活跃子 agent 一行,done 的灰色显示后 3 秒自动清除
534
+ const subs = Object.values(state.subTasks)
535
+ if (subs.length > 0 && state.processing) {
536
+ for (const s of subs.slice(0, 4)) {
537
+ const icon = s.done ? "✓" : "…"
538
+ const color = s.done ? C.dim : C.tool
539
+ const label = `[${s.role}]`.padEnd(10)
540
+ const text = s.text ? sliceByWidth(s.text, W - 14) : (s.done ? "done" : "running...")
541
+ out.push(`${color} ${icon} ${label} ${text}${ansi.reset}${ansi.clearLine}`)
542
+ }
543
+ if (subs.length > 4) {
544
+ out.push(`${C.dim} ... +${subs.length - 4} more subagents${ansi.reset}${ansi.clearLine}`)
537
545
  }
538
546
  }
539
547
 
540
- // 权限审批内容预览(黄色,紧挨输入框上方);用上方已封顶的 permPreviewLines,渲染行数与占位一致
548
+ // 权限审批内容预览 (黄色,紧挨输入框上方);用上方已封顶的 permPreviewLines,渲染行数与占位一致
541
549
  if (state.permission) {
542
550
  out.push(`${ansi.bold}${C.warn}❯ 权限请求${ansi.reset}${ansi.clearLine}`)
543
551
  for (const wrapped of permPreviewLines) {
@@ -545,7 +553,7 @@ export async function startTUI(agent, opts = {}) {
545
553
  }
546
554
  }
547
555
 
548
- // 输入框(全边框,宽 W)
556
+ // 输入框 (全边框,宽 W)
549
557
  let borderColor = C.tool
550
558
  let title
551
559
  if (state.question) {
@@ -567,7 +575,13 @@ export async function startTUI(agent, opts = {}) {
567
575
  } else {
568
576
  title = " Input "
569
577
  }
570
- const topBorder = `╭─${title}${"─".repeat(Math.max(0, W - 3 - stringWidth(title)))}╮`
578
+ let topBorder
579
+ if (title === " Input " && isMultimodal) {
580
+ const hint = process.platform === "win32" ? " Alt+V paste " : " Ctrl+V paste "
581
+ topBorder = `╭─${title}${"─".repeat(Math.max(0, W - 4 - stringWidth(title) - stringWidth(hint)))}${hint}─╮`
582
+ } else {
583
+ topBorder = `╭─${title}${"─".repeat(Math.max(0, W - 3 - stringWidth(title)))}╮`
584
+ }
571
585
  out.push(`${borderColor}${topBorder}${ansi.reset}${ansi.clearLine}`)
572
586
  for (const l of boxLines) {
573
587
  const content = sliceByWidth(l, W - 4)
@@ -576,59 +590,59 @@ export async function startTUI(agent, opts = {}) {
576
590
  }
577
591
  out.push(`${borderColor}╰${"─".repeat(Math.max(0, W - 2))}╯${ansi.reset}${ansi.clearLine}`)
578
592
 
579
- // 状态栏(输入 / 开头时变为命令提示)
593
+ // 状态栏 (输入 / 开头时变为Commands提示)
580
594
  const scrollHint = state.scroll > 0 ? ` │ scrolled ${state.scroll}` : ""
581
595
  const rawInput = state.input.join("")
582
596
  let statusLine
583
597
  if (state.question) {
584
598
  const q = state.question
585
599
  statusLine = q.options.length > 0
586
- ? " ↑↓: 选择 │ Enter: 确认 │ Esc: 取消"
587
- : " 输入回答后 Enter 提交 │ Esc: 取消"
600
+ ? " ↑↓: select │ Enter: confirm │ Esc: cancel"
601
+ : " Type answer then Enter │ Esc: cancel"
588
602
  } else if (state.permission) {
589
603
  statusLine = state.permission.name === "continue"
590
- ? " y: 继续 │ n: 停止"
591
- : " y: 批准 │ n: 拒绝 │ a: 批准并全部放行(AUTO"
604
+ ? " y: continue │ n: stop"
605
+ : " y: approve │ n: deny │ a: approve all (AUTO)"
592
606
  } else if (state.picker) {
593
- statusLine = " ↑↓: 选择 │ Enter: 确认 │ Esc: 取消"
607
+ statusLine = " ↑↓: select │ Enter: confirm │ Esc: cancel"
594
608
  } else if (state.wizard) {
595
609
  statusLine = state.wizard.step === "provider"
596
- ? " ↑↓: 选择 │ Enter: 确认 │ Esc: 跳过"
597
- : " 输入后 Enter 确认 │ Esc: 取消"
610
+ ? " ↑↓: select │ Enter: confirm │ Esc: skip"
611
+ : " Type then Enter │ Esc: cancel"
598
612
  } else if (rawInput.startsWith("/") && !state.processing && !state.permission) {
599
613
  const [cmd, sub] = rawInput.split(/\s+/)
600
614
  const cmds = SLASH_COMMANDS.filter((c) => c.name.startsWith(cmd))
601
615
  const match = cmds.length === 1 ? cmds[0] : null
602
616
  if (match?.name === "/config" && cmd === "/config") {
603
- statusLine = " /config 打开配置菜单"
617
+ statusLine = " /config open config menu"
604
618
  } else if (match?.name === "/provider" && cmd === "/provider") {
605
- statusLine = " /provider 打开 Provider 管理菜单"
619
+ statusLine = " /provider open provider management menu"
606
620
  } else if (match?.name === "/model" && cmd === "/model" && !sub) {
607
- statusLine = " /model 打开模型选择器"
621
+ statusLine = " /model open model picker"
608
622
  } else if (match?.name === "/think" && cmd === "/think") {
609
- statusLine = " /think 打开思维模式菜单"
623
+ statusLine = " /think open thinking mode menu"
610
624
  } else if (match?.name === "/mcp" && cmd === "/mcp") {
611
- statusLine = " /mcp 打开 MCP 管理菜单"
625
+ statusLine = " /mcp open MCP management menu"
612
626
  } else if (match?.name === "/goal" && cmd === "/goal") {
613
- statusLine = " /goal 打开目标管理菜单"
627
+ statusLine = " /goal open goal management menu"
614
628
  } else if (match?.name === "/session" && cmd === "/session") {
615
- statusLine = " /session 选择归档会话"
629
+ statusLine = " /session select archived session"
616
630
  } else if (match?.name === "/rewind" && cmd === "/rewind") {
617
- statusLine = " /rewind 选择存档点回滚"
631
+ statusLine = " /rewind select checkpoint to restore"
618
632
  } else if (cmds.length > 0) {
619
633
  if (cmds.length <= 4) {
620
634
  statusLine = ` ${cmds.map((c) => `${c.name} ${c.desc}`).join(" │ ")}`
621
635
  } else {
622
- statusLine = ` ${cmds.map((c) => c.name).join(" ")} │ Tab 补全`
636
+ statusLine = ` ${cmds.map((c) => c.name).join(" ")} │ Tab complete`
623
637
  }
624
638
  } else {
625
- statusLine = ` 未知命令(/help 查看可用命令)`
639
+ statusLine = ` unknown command (/help for available commands)`
626
640
  }
627
641
  } else {
628
642
  const taskHint = state.tasks.length > 0
629
643
  ? ` │ ✓${state.tasks.filter((t) => t.status === "done").length}/${state.tasks.length}`
630
644
  : ""
631
- // token 用量:↑输入 ↓输出 + 缓存命中率(DeepSeek usage 带 prompt_cache_hit/miss_tokens)
645
+ // token 用量:↑输入 ↓输出 + 缓存命中率 (DeepSeek usage 带 prompt_cache_hit/miss_tokens)
632
646
  const tk = state.tokens
633
647
  const fmtK = (n) => (n >= 10000 ? `${Math.round(n / 1000)}k` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`)
634
648
  const cacheTotal = tk.cacheHit + tk.cacheMiss
@@ -638,7 +652,7 @@ export async function startTUI(agent, opts = {}) {
638
652
  const elapsed = state.processing ? ` ${Math.floor((Date.now() - state.processingStarted) / 1000)}s` : ""
639
653
  const toolHint = state.currentTool ? ` ${state.currentTool}…` : ""
640
654
  const statusText = state.processing ? `${state.status}${toolHint}${elapsed}` : state.status
641
- // 上下文利用率:占压缩阈值百分比(到 100% 触发压缩;≥80% 变黄提醒该收尾或 /new)
655
+ // 上下文利用率:占压缩阈值百分比 (到 100% 触发压缩;≥80% 变黄提醒该收尾或 /new)
642
656
  if (state.ctxCache.len !== agent.history.length) {
643
657
  state.ctxCache = { len: agent.history.length, tokens: estimateTokens(agent.history) }
644
658
  }
@@ -665,12 +679,12 @@ export async function startTUI(agent, opts = {}) {
665
679
  process.stdout.write(frame)
666
680
  }
667
681
 
668
- // 光标:输入态定位到输入框内(IME 候选框跟随真实光标);处理中/权限确认/菜单态时隐藏
682
+ // 光标:输入态定位到输入框内 (IME 候选框跟随真实光标);处理中/权限确认/菜单态时隐藏
669
683
  if (state.processing || state.permission || state.question || state.picker || state.wizard?.step === "provider") {
670
684
  process.stdout.write(ansi.hideCursor)
671
685
  } else {
672
686
  const cursorRow = 1 + convH + pickerH + taskPanelH + 2 + (layout.cursorLine - inputOffset) // header + 对话区 + todo 面板 + 上边框 + 行偏移
673
- const cursorCol = 3 + layout.cursorCol // 左边框 + 空格 + 文本偏移(1 基)
687
+ const cursorCol = 3 + layout.cursorCol // 左边框 + 空格 + 文本偏移 (1 基)
674
688
  process.stdout.write(`${ESC}[${cursorRow};${cursorCol}H${ansi.showCursor}`)
675
689
  }
676
690
  }
@@ -688,7 +702,7 @@ export async function startTUI(agent, opts = {}) {
688
702
  state.historyIndex = -1
689
703
  state.scroll = 0
690
704
 
691
- // 斜杠命令:本地处理,不进入 agent
705
+ // 斜杠Commands:本地处理,不进入 agent
692
706
  if (text.startsWith("/")) {
693
707
  await handleSlash(text)
694
708
  return
@@ -697,7 +711,7 @@ export async function startTUI(agent, opts = {}) {
697
711
  pushLabel(`❯ You:`, ansi.bold + C.user)
698
712
  pushLine(text, C.text)
699
713
 
700
- // 任务开始前自动打存档点(git 仓库内;失败静默,不挡任务)
714
+ // 任务开始前自动打存档点 (git 仓库内;失败静默,不挡任务)
701
715
  try {
702
716
  const { createCheckpoint } = await import("./checkpoint.mjs")
703
717
  await createCheckpoint(agent.cwd)
@@ -710,10 +724,11 @@ export async function startTUI(agent, opts = {}) {
710
724
  state.status = "Processing..."
711
725
  state.streaming = ""
712
726
  state.reasoning = ""
727
+ state.subTasks = {}
713
728
  state.currentTool = null
714
729
  state.processingStarted = Date.now()
715
730
  state.controller = new AbortController()
716
- // 处理中每秒刷新一次状态栏(运行计时)
731
+ // 处理中每秒刷新一次状态栏 (运行计时)
717
732
  const ticker = setInterval(() => {
718
733
  if (state.processing) render()
719
734
  }, 1000)
@@ -721,11 +736,12 @@ export async function startTUI(agent, opts = {}) {
721
736
 
722
737
  const callbacks = {
723
738
  onToken: (t) => {
724
- // 子 agent 流式输出:前缀匹配 explore/coder/plan/sub(无角色子 agent 用 sub/)的 token 进 subOutput
739
+ // 子 agent 流式输出:前缀匹配 explore/coder/plan/sub token 进 subTasks 面板
725
740
  const subMatch = t.match(/^(explore|coder|plan|sub)\//)
726
741
  if (subMatch) {
727
- state.currentSub = subMatch[1]
728
- state.subOutput = (state.subOutput + t.slice(subMatch[0].length)).slice(-300)
742
+ const role = subMatch[1]
743
+ if (!state.subTasks[role]) state.subTasks[role] = { role, text: "", done: false }
744
+ state.subTasks[role].text = (state.subTasks[role].text + t.slice(subMatch[0].length)).slice(-200)
729
745
  scheduleRender()
730
746
  return
731
747
  }
@@ -734,11 +750,11 @@ export async function startTUI(agent, opts = {}) {
734
750
  scheduleRender()
735
751
  },
736
752
  onReasoning: (t) => {
737
- // 子 agent 的思考 token 同样带 role/ 前缀,进 subOutput 滚动区,不污染主思考流
753
+ // 子 agent 的思考 token 同样带 role/ 前缀,进 subTasks 面板,不污染主思考流
738
754
  const subMatch = t.match(/^(explore|coder|plan|sub)\//)
739
755
  if (subMatch) {
740
- state.currentSub = subMatch[1]
741
- state.subOutput = (state.subOutput + t.slice(subMatch[0].length)).slice(-300)
756
+ const role = subMatch[1]
757
+ if (!state.subTasks[role]) state.subTasks[role] = { role, text: "", done: false }
742
758
  scheduleRender()
743
759
  return
744
760
  }
@@ -754,17 +770,25 @@ export async function startTUI(agent, opts = {}) {
754
770
  },
755
771
  onToolResult: (name, result) => {
756
772
  state.currentTool = null
757
- // 子 agent 结束(父 agent 侧的 subagent 工具结果带着最终报告):清空流式缓冲,报告进对话区。
758
- // 注意只能用精确匹配——子 agent 内部工具调用不 relay 到 TUI(刷了满屏的教训)
773
+ // 子 agent 结束:标记 done,面板保留片刻后清除
759
774
  const isSubagent = name === "subagent"
760
775
  if (isSubagent) {
761
- state.subOutput = ""
762
- state.currentSub = null
763
- // agent 报告摘要(最多 8 行)直接展示在对话区
776
+ // 所有活跃子 agent 标记 done
777
+ for (const key of Object.keys(state.subTasks)) {
778
+ state.subTasks[key].done = true
779
+ }
780
+ // 子 agent 报告摘要 (最多 8 行)直接展示在对话区
764
781
  const lines = result.split("\n")
765
782
  const preview = lines.slice(0, 8).map((l) => l.slice(0, 120)).join("\n")
766
783
  if (preview) pushLine(preview, C.dim)
767
784
  if (lines.length > 8) pushLine(` ... (${lines.length - 8} more lines)`, C.dim)
785
+ // 3 秒后清除面板中 done 的条目
786
+ setTimeout(() => {
787
+ for (const key of Object.keys(state.subTasks)) {
788
+ if (state.subTasks[key].done) delete state.subTasks[key]
789
+ }
790
+ if (state.processing) render()
791
+ }, 3000)
768
792
  }
769
793
  const stream = state.toolStreams[name]
770
794
  if (stream) {
@@ -784,7 +808,7 @@ export async function startTUI(agent, opts = {}) {
784
808
  onPermissionRequest: (name, args) => askPermission(name, args),
785
809
  onQuestion: (text, options) => askQuestion(text, options),
786
810
  onCompress: () => {
787
- pushLine(" [context] 上下文过长,已自动压缩(早期对话由 LLM 摘要,任务状态保留)", C.warn)
811
+ pushLine(" [context] Context too long, auto-compacted (early conversation summarized by LLM, task state preserved)", C.warn)
788
812
  },
789
813
  onUsage: (usage) => {
790
814
  state.tokens.prompt += usage.prompt_tokens ?? 0
@@ -792,7 +816,7 @@ export async function startTUI(agent, opts = {}) {
792
816
  state.tokens.cacheHit += usage.prompt_cache_hit_tokens ?? 0
793
817
  state.tokens.cacheMiss += usage.prompt_cache_miss_tokens ?? 0
794
818
  },
795
- // 节流等待(主动闸门 / 429 退避):状态栏明示,防用户以为卡死
819
+ // 节流等待 (主动闸门 / 429 退避):状态栏明示,防用户以为卡死
796
820
  onWait: ({ phase, seconds }) => {
797
821
  state.status = phase === "gate" ? `TPM 节流等待 ~${seconds}s` : `限流 429,${seconds}s 后重试`
798
822
  render()
@@ -800,7 +824,7 @@ export async function startTUI(agent, opts = {}) {
800
824
  onTaskUpdate: (items) => {
801
825
  state.tasks = items
802
826
  const done = items.filter((i) => i.status === "done").length
803
- // 留痕带上当前任务标题:回看历史时知道进行到哪一项
827
+ // 留痕带上current任务标题:回看历史时知道进行到哪一项
804
828
  const current = items.find((i) => i.status === "in_progress")
805
829
  pushLine(` [task] ${done}/${items.length}${current ? ` ▶ ${current.title}` : ""}`, C.dim)
806
830
  render()
@@ -823,12 +847,12 @@ export async function startTUI(agent, opts = {}) {
823
847
  } catch (error) {
824
848
  flushStream()
825
849
  if (error.name === "AbortError" || state.controller?.signal.aborted) {
826
- pushLine("[已中止]", C.warn)
850
+ pushLine("[stopped]", C.warn)
827
851
  break
828
852
  }
829
853
  if (error instanceof ContinueError) {
830
854
  pushLabel(`❯ Continue`, ansi.bold + C.warn)
831
- pushLine(`已执行 ${error.turn} 轮(上限 ${error.turn}),要继续吗?`, C.warn)
855
+ pushLine(`Ran ${error.turn} turns (limit ${error.turn}). Continue?`, C.warn)
832
856
  // 暂停询问:复用 permission 机制
833
857
  const willContinue = await new Promise((resolve) => {
834
858
  state.permission = {
@@ -841,11 +865,11 @@ export async function startTUI(agent, opts = {}) {
841
865
  })
842
866
  state.permission = null
843
867
  if (!willContinue) {
844
- pushLine("[已取消继续]", C.warn)
868
+ pushLine("[continue cancelled]", C.warn)
845
869
  break
846
870
  }
847
- pushLine("[继续执行…]", C.tool)
848
- // 重创新 AbortController:旧 signal 一旦 abort 过,resume 会立即失败(防御性,当前路径不可达但耦合紧)
871
+ pushLine("[continuing…]", C.tool)
872
+ // 重创新 AbortController:旧 signal 一旦 abort 过,resume 会立即失败 (防御性,current路径不可达但耦合紧)
849
873
  state.controller = new AbortController()
850
874
  continue
851
875
  }
@@ -856,13 +880,14 @@ export async function startTUI(agent, opts = {}) {
856
880
 
857
881
  clearInterval(ticker)
858
882
  state.processing = false
883
+ state.subTasks = {}
859
884
  state.controller = null
860
885
  state.status = "Ready"
861
- // 全部完成时自动收起 todo 面板(对齐 kimi-code TUI;agent.tasks 本身保留)
886
+ // 全部完成时自动收起 todo 面板 (对齐 kimi-code TUI;agent.tasks 本身保留)
862
887
  if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
863
888
  state.tasks = []
864
889
  }
865
- // 每轮结束后保存会话(崩溃也不丢)
890
+ // 每轮结束后保存会话 (崩溃也不丢)
866
891
  try {
867
892
  saveSession(agent, state.lines)
868
893
  } catch {
@@ -897,14 +922,14 @@ export async function startTUI(agent, opts = {}) {
897
922
  })
898
923
  }
899
924
 
900
- /** 权限请求的关键信息(按工具定制),返回行数组。name 可能带子 agent 前缀("coder/bash"),取基名匹配 */
925
+ /** 权限请求的关键信息 (按工具定制),返回行数组。name 可能带子 agent 前缀 ("coder/bash"),取基名匹配 */
901
926
  function formatPermission(name, args) {
902
- const cap = (s, n = 1000) => (s.length > n ? `${s.slice(0, n)}…(${s.length} 字符)` : s)
927
+ const cap = (s, n = 1000) => (s.length > n ? `${s.slice(0, n)}…(${s.length} chars total)` : s)
903
928
  const base = name.includes("/") ? name.split("/").pop() : name
904
929
  if (base === "bash") return cap(args.command ?? "").split("\n")
905
930
  if (base === "write") {
906
931
  // 批准写文件必须看得到要写什么:路径 + 内容预览
907
- return [`${args.path}(写入 ${(args.content ?? "").length} 字符)`, ...cap(args.content ?? "", 1000).split("\n")]
932
+ return [`${args.path} (write ${(args.content ?? "").length} chars)`, ...cap(args.content ?? "", 1000).split("\n")]
908
933
  }
909
934
  if (base === "edit") {
910
935
  // 简易 diff:- 旧内容 / + 新内容
@@ -919,7 +944,7 @@ export async function startTUI(agent, opts = {}) {
919
944
  // 补丁本身就是可读的 diff,直接预览
920
945
  return cap(args.patch ?? "", 1500).split("\n")
921
946
  }
922
- if (base === "delete") return [`${args.path}${args.force ? "force:跟踪文件也删)" : ""}`]
947
+ if (base === "delete") return [`${args.path}${args.force ? " (force: also delete tracked files)" : ""}`]
923
948
  if (base === "subagent") return cap(args.task ?? "", 500).split("\n")
924
949
  if (base === "memory_put") return [`[${args.type ?? ""}] ${args.title ?? ""}`, ...cap(args.content ?? "", 500).split("\n")]
925
950
  return [cap(summarize(args), 300)]
@@ -927,9 +952,9 @@ export async function startTUI(agent, opts = {}) {
927
952
 
928
953
  function askQuestion(text, options = []) {
929
954
  // 一次只能问一个:question 是只读工具走并行通道,同批第二个直接驳回,
930
- // 否则后到的会覆盖 state.question,先到的 Promise 永远悬挂(agent 死等)
955
+ // 否则后到的会覆盖 state.question,先到的 Promise 永远悬挂 (agent 死等)
931
956
  if (state.question) {
932
- return Promise.resolve("(error: 已有问题在等待回答;请一次只问一个,得到答复后再问下一个)")
957
+ return Promise.resolve("(error: another question is pending; ask one at a time and wait for the answer)")
933
958
  }
934
959
  if (!options.length) {
935
960
  // 自由文本:打开输入态让用户打字,Enter 提交
@@ -951,27 +976,71 @@ export async function startTUI(agent, opts = {}) {
951
976
  })
952
977
  }
953
978
 
954
- // ---------------------------------------------------------- 斜杠命令
979
+ /** Ctrl+V / Alt+V:读取剪贴板图片 → 写入工作目录临时文件 → 输入框插入 read_image 命令 */
980
+ async function pasteClipboardImage(agent) {
981
+ const { execFile } = await import("node:child_process")
982
+ const { mkdir, stat, unlink } = await import("node:fs/promises")
983
+ const { join } = await import("node:path")
984
+
985
+ const run = (cmd, args) => new Promise((resolve, reject) => {
986
+ execFile(cmd, args, { timeout: 10000 }, (err, stdout) => { if (err) reject(err); else resolve(stdout) })
987
+ })
988
+
989
+ const dest = join(agent.cwd, `.thincoder-paste-${Date.now()}.png`)
990
+ const isWin = process.platform === "win32"
991
+ const isMac = process.platform === "darwin"
992
+
993
+ try {
994
+ if (isWin) {
995
+ const psScript = `Add-Type -AssemblyName System.Windows.Forms; if ([System.Windows.Forms.Clipboard]::ContainsImage()) { [System.Windows.Forms.Clipboard]::GetImage().Save('${dest.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png); exit 0 } else { exit 1 }`
996
+ await run("powershell", ["-NoProfile", "-Command", psScript])
997
+ } else if (isMac) {
998
+ const script = `try; set f to (POSIX file "${dest}"); set img to the clipboard as «class PNGf»; set fd to open for access f with write permission; write img to fd; close access fd; end try`
999
+ await run("osascript", ["-e", script])
1000
+ } else {
1001
+ await run("bash", ["-c", `xclip -selection clipboard -t image/png -o > "${dest}" 2>/dev/null || { which wl-paste >/dev/null 2>&1 && wl-paste -t image/png > "${dest}" 2>/dev/null; } || exit 1`])
1002
+ }
1003
+ } catch {
1004
+ pushLine("Clipboard does not contain an image, or clipboard access failed", C.dim)
1005
+ try { await unlink(dest) } catch {}
1006
+ return
1007
+ }
1008
+
1009
+ const st = await stat(dest).catch(() => null)
1010
+ if (!st || st.size === 0) {
1011
+ pushLine("Clipboard does not contain an image, or clipboard access failed", C.dim)
1012
+ try { await unlink(dest) } catch {}
1013
+ return
1014
+ }
1015
+
1016
+ const cmd = `read_image ${dest}`
1017
+ state.input.splice(state.cursor, 0, ...[...cmd])
1018
+ state.cursor += cmd.length
1019
+ pushLine(`[image pasted → ${dest}]`, C.tool)
1020
+ render()
1021
+ }
1022
+
1023
+ // ---------------------------------------------------------- 斜杠Commands
955
1024
 
956
1025
  const SLASH_COMMANDS = [
957
- { name: "/plan", group: "Agent", desc: "规划模式(先设计、再实现)" },
958
- { name: "/auto", group: "Agent", desc: "自动授权开关" },
959
- { name: "/model", group: "Agent", desc: "选择模型" },
960
- { name: "/goal", group: "Agent", desc: "设置/查看/取消长期目标" },
961
- { name: "/think", group: "Agent", desc: "思维模式与推理强度" },
962
- { name: "/init", group: "Tools", desc: "生成项目 AGENTS.md 骨架" },
963
- { name: "/skills", group: "Tools", desc: "列出项目技能" },
964
- { name: "/mcp", group: "Tools", desc: "管理 MCP server" },
965
- { name: "/provider", group: "Config", desc: "管理 provider(增/删/配 key" },
966
- { name: "/config", group: "Config", desc: "配置管理(embedding / agent" },
967
- { name: "/reindex", group: "Config", desc: "重建记忆索引" },
968
- { name: "/new", group: "Session", desc: "新会话(旧会话归档到槽位)" },
969
- { name: "/session", group: "Session", desc: "列出/切换归档会话" },
970
- { name: "/clear", group: "Session", desc: "清屏" },
971
- { name: "/distill", group: "Session", desc: "从会话提取知识" },
972
- { name: "/rewind", group: "Session", desc: "回滚到存档点" },
973
- { name: "/exit", group: "Session", desc: "退出" },
974
- { name: "/help", group: "", desc: "此列表" },
1026
+ { name: "/plan", group: "Agent", desc: "toggle plan mode (design first, then implement)" },
1027
+ { name: "/auto", group: "Agent", desc: "toggle auto-approve" },
1028
+ { name: "/model", group: "Agent", desc: "select model" },
1029
+ { name: "/goal", group: "Agent", desc: "set/view/cancel long-term goal" },
1030
+ { name: "/think", group: "Agent", desc: "thinking mode & reasoning effort" },
1031
+ { name: "/init", group: "Tools", desc: "generate project AGENTS.md skeleton" },
1032
+ { name: "/skills", group: "Tools", desc: "list project skills" },
1033
+ { name: "/mcp", group: "Tools", desc: "manage MCP servers" },
1034
+ { name: "/provider", group: "Config", desc: "manage providers (add/remove/set key)" },
1035
+ { name: "/config", group: "Config", desc: "config management (embedding / agent)" },
1036
+ { name: "/reindex", group: "Config", desc: "rebuild memory index" },
1037
+ { name: "/new", group: "Session", desc: "new session (old one archived to slot)" },
1038
+ { name: "/session", group: "Session", desc: "list/switch archived sessions" },
1039
+ { name: "/clear", group: "Session", desc: "clear screen" },
1040
+ { name: "/distill", group: "Session", desc: "extract knowledge from session" },
1041
+ { name: "/rewind", group: "Session", desc: "restore checkpoint" },
1042
+ { name: "/exit", group: "Session", desc: "exit" },
1043
+ { name: "/help", group: "", desc: "this list" },
975
1044
  ]
976
1045
 
977
1046
  async function handleSlash(text) {
@@ -992,7 +1061,7 @@ export async function startTUI(agent, opts = {}) {
992
1061
  state.lines = []
993
1062
  state.streaming = ""
994
1063
  clearSession(agent.cwd)
995
- pushLine("已开始新会话(旧会话已归档到槽位;/session 可查看)", C.dim)
1064
+ pushLine("New session started (old session archived to slot; /session to view)", C.dim)
996
1065
  return
997
1066
  case "/exit":
998
1067
  cleanup()
@@ -1001,19 +1070,19 @@ export async function startTUI(agent, opts = {}) {
1001
1070
  case "/session": {
1002
1071
  const slots = listSlots(agent.cwd)
1003
1072
  if (slots.length === 0) {
1004
- pushLine("没有归档会话(用 /new 后旧会话会自动归档)", C.dim)
1073
+ pushLine("No archived sessions (use /new and old sessions auto-archive to slots)", C.dim)
1005
1074
  } else {
1006
1075
  const entries = [
1007
- { type: "header", text: "归档会话(↑↓ 选择, Enter 切换, Esc 取消)" },
1008
- ...slots.map((s) => ({ type: "item", text: `槽位 ${s.slot} — ${s.date}`, slot: s.slot })),
1076
+ { type: "header", text: "Archived sessions (↑↓ select, Enter switch, Esc cancel)" },
1077
+ ...slots.map((s) => ({ type: "item", text: `Slot ${s.slot} — ${s.date}`, slot: s.slot })),
1009
1078
  ]
1010
1079
  openPicker({
1011
- title: "切换会话",
1080
+ title: "Switch Session",
1012
1081
  entries,
1013
1082
  onSelect: (e) => {
1014
1083
  const data = switchToSlot(agent.cwd, e.slot)
1015
1084
  if (!data) {
1016
- pushLine(`槽位 ${e.slot} 不存在`, C.dim)
1085
+ pushLine(`Slot ${e.slot} not found`, C.dim)
1017
1086
  return
1018
1087
  }
1019
1088
  applySession(agent, data)
@@ -1024,7 +1093,7 @@ export async function startTUI(agent, opts = {}) {
1024
1093
  if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
1025
1094
  state.tasks = []
1026
1095
  }
1027
- pushLabel(`── 已切换到槽位 ${e.slot}(${data.history.length} 条消息)──`, C.warn)
1096
+ pushLabel(`── Switched to slot ${e.slot} (${data.history.length} messages) ──`, C.warn)
1028
1097
  render()
1029
1098
  },
1030
1099
  })
@@ -1033,7 +1102,7 @@ export async function startTUI(agent, opts = {}) {
1033
1102
  }
1034
1103
  case "/reindex": {
1035
1104
  const { syncDir, codeSync, docSync } = await import("./memory.mjs")
1036
- pushLine("[reindex] 重建索引...", C.tool)
1105
+ pushLine("[reindex] Rebuilding index...", C.tool)
1037
1106
  agent.memory.db.prepare("DELETE FROM files").run()
1038
1107
  agent.memory.db.prepare("DELETE FROM code_chunks").run()
1039
1108
  agent.memory.db.prepare("DELETE FROM doc_chunks").run()
@@ -1049,26 +1118,26 @@ export async function startTUI(agent, opts = {}) {
1049
1118
  pushLine(` team: +${s.added} ~${s.updated} -${s.removed}`, C.dim)
1050
1119
  }
1051
1120
  // 重建代码索引
1052
- pushLine(` [code] 重建代码索引...`, C.tool)
1121
+ pushLine(` [code] Rebuilding code index...`, C.tool)
1053
1122
  const cr = await codeSync(agent.memory, agent.cwd, {
1054
1123
  onProgress: (p) => {
1055
1124
  if (p.phase === "index" && p.current % 20 === 0) {
1056
- pushLine(` 索引中... ${p.current}/${p.total}`, C.dim)
1125
+ pushLine(` Indexing... ${p.current}/${p.total}`, C.dim)
1057
1126
  }
1058
1127
  }
1059
1128
  })
1060
- pushLine(` code: ${cr.total} 文件,+${cr.updated} ~${cr.skipped} -${cr.removed}`, C.dim)
1129
+ pushLine(` code: ${cr.total} files, +${cr.updated} ~${cr.skipped} -${cr.removed}`, C.dim)
1061
1130
  // 重建文档索引
1062
- pushLine(` [doc] 重建文档索引...`, C.tool)
1131
+ pushLine(` [doc] Rebuilding doc index...`, C.tool)
1063
1132
  const dr = await docSync(agent.memory, agent.cwd, {
1064
1133
  onProgress: (p) => {
1065
1134
  if (p.phase === "index" && p.current % 5 === 0) {
1066
- pushLine(` 索引中... ${p.current}/${p.total}`, C.dim)
1135
+ pushLine(` Indexing... ${p.current}/${p.total}`, C.dim)
1067
1136
  }
1068
1137
  }
1069
1138
  })
1070
- pushLine(` doc: ${dr.total} 文件,+${dr.updated} ~${dr.skipped} -${dr.removed}`, C.dim)
1071
- pushLine(`[reindex] 完成,共 ${total} 条目。向量将在下次搜索时惰性生成。`, C.tool)
1139
+ pushLine(` doc: ${dr.total} files, +${dr.updated} ~${dr.skipped} -${dr.removed}`, C.dim)
1140
+ pushLine(`[reindex] Done, ${total} entries total. Vectors will be lazily generated on next search.`, C.tool)
1072
1141
  return
1073
1142
  }
1074
1143
  case "/distill":
@@ -1080,7 +1149,7 @@ export async function startTUI(agent, opts = {}) {
1080
1149
  const { join, basename } = await import("node:path")
1081
1150
  const agPath = join(agent.cwd, "AGENTS.md")
1082
1151
  if (existsSync(agPath)) {
1083
- pushLine(`AGENTS.md 已存在: ${agPath}`, C.warn)
1152
+ pushLine(`AGENTS.md already exists: ${agPath}`, C.warn)
1084
1153
  return
1085
1154
  }
1086
1155
 
@@ -1131,45 +1200,45 @@ export async function startTUI(agent, opts = {}) {
1131
1200
 
1132
1201
  const lines = [`# ${name}`, ""]
1133
1202
  if (lang) {
1134
- lines.push(`## 技术栈`, "", lang, "")
1135
- if (cmds) lines.push(`## 命令`, "", cmds, "")
1203
+ lines.push(`## Tech Stack`, "", lang, "")
1204
+ if (cmds) lines.push(`## Commands`, "", cmds, "")
1136
1205
  }
1137
1206
 
1138
1207
  const template = lines.join("\n")
1139
1208
  await writeFile(agPath, template, "utf8")
1140
1209
  pushLabel(`❯ Init`, ansi.bold + C.tool)
1141
- pushLine(`已生成 AGENTS.md → ${agPath}${lang ? ` (${lang})` : ""}`, C.tool)
1142
- if (lang) pushLine("可继续告诉我项目信息,我来补充约定和结构", C.dim)
1210
+ pushLine(`Generated AGENTS.md → ${agPath}${lang ? ` (${lang})` : ""}`, C.tool)
1211
+ if (lang) pushLine("Tell me more about the project and I will fill in conventions and structure", C.dim)
1143
1212
  return
1144
1213
  }
1145
1214
  case "/rewind": {
1146
1215
  const { listCheckpoints, rewind, isGitRepo } = await import("./checkpoint.mjs")
1147
1216
  if (!isGitRepo(agent.cwd)) {
1148
- pushLine("[rewind] 当前目录不是 git 仓库,无法使用存档点", C.error)
1217
+ pushLine("[rewind] not a git repository, checkpoints unavailable", C.error)
1149
1218
  return
1150
1219
  }
1151
1220
  const cps = await listCheckpoints(agent.cwd)
1152
1221
  if (cps.length === 0) {
1153
- pushLine("(暂无存档点——每次提交任务前自动创建)", C.dim)
1222
+ pushLine("(no checkpoints — created automatically before each task)", C.dim)
1154
1223
  return
1155
1224
  }
1156
1225
  const entries = [
1157
- { type: "header", text: "存档点(↑↓ 选择, Enter 回滚, Esc 取消)" },
1226
+ { type: "header", text: "Checkpoints (↑↓ select, Enter restore, Esc cancel)" },
1158
1227
  ...cps.slice(0, 12).map((cp) => ({
1159
1228
  type: "item",
1160
- text: `${cp.id} ${new Date(cp.time).toLocaleString()} (+${cp.untracked} 个未跟踪文件)`,
1229
+ text: `${cp.id} ${new Date(cp.time).toLocaleString()} (+${cp.untracked} untracked files)`,
1161
1230
  id: cp.id,
1162
1231
  })),
1163
1232
  ]
1164
1233
  openPicker({
1165
- title: "回滚存档点",
1234
+ title: "Restore Checkpoint",
1166
1235
  entries,
1167
1236
  onSelect: async (e) => {
1168
1237
  try {
1169
1238
  const summary = await rewind(agent.cwd, e.id)
1170
1239
  pushLabel(`❯ Rewind`, ansi.bold + C.warn)
1171
- pushLine(`已回滚到 ${e.id}:补丁${summary.patchApplied ? "已应用" : ""},删除新建文件 ${summary.deleted} 个,还原文件 ${summary.restored} 个`, C.tool)
1172
- pushLine("(当前状态已先存为新存档点,可再次 /rewind 回到刚才)", C.dim)
1240
+ pushLine(`Restored to ${e.id}: patch ${summary.patchApplied ? "applied" : "none"},deleted ${summary.deleted} new files, restored ${summary.restored} 个`, C.tool)
1241
+ pushLine("(current state saved as new checkpoint; /rewind again to go back)", C.dim)
1173
1242
  } catch (error) {
1174
1243
  pushLine(`[rewind] ${error.message}`, C.error)
1175
1244
  }
@@ -1188,50 +1257,50 @@ export async function startTUI(agent, opts = {}) {
1188
1257
  pushLabel(`❯ Plan`, ansi.bold + (agent.planMode ? C.tool : C.dim))
1189
1258
  pushLine(
1190
1259
  agent.planMode
1191
- ? `规划模式已开启:只读工具受限,先设计方案再实现。再次 /plan 退出。`
1192
- : `规划模式已关闭:可以编辑文件和执行命令了。`,
1260
+ ? `Plan mode ON: read-only tools only. Design first, then implement. /plan again to exit.`
1261
+ : `Plan mode OFF: you may now edit files and run commands.`,
1193
1262
  agent.planMode ? C.tool : C.dim,
1194
1263
  )
1195
1264
  return
1196
1265
  }
1197
1266
  case "/goal": {
1198
1267
  const entries = [
1199
- { type: "header", text: agent.goal ? `当前目标: ${agent.goal.objective.slice(0, 60)}` : "操作" },
1200
- { type: "item", text: "设置新目标", action: "set" },
1268
+ { type: "header", text: agent.goal ? `Current goal: ${agent.goal.objective.slice(0, 60)}` : "Actions" },
1269
+ { type: "item", text: "Set new goal", action: "set" },
1201
1270
  ]
1202
1271
  if (agent.goal) {
1203
- entries.push({ type: "item", text: "取消目标", action: "cancel" })
1204
- entries.push({ type: "item", text: "查看详情", action: "view" })
1272
+ entries.push({ type: "item", text: "Cancel goal", action: "cancel" })
1273
+ entries.push({ type: "item", text: "View details", action: "view" })
1205
1274
  }
1206
1275
  openPicker({
1207
- title: "目标管理",
1276
+ title: "Goal",
1208
1277
  entries,
1209
1278
  onSelect: (e) => {
1210
1279
  if (e.action === "view") {
1211
- const statusText = { active: "进行中", complete: "已完成", blocked: "已阻塞" }[agent.goal.status] ?? agent.goal.status
1280
+ const statusText = { active: "active", complete: "completed", blocked: "blocked" }[agent.goal.status] ?? agent.goal.status
1212
1281
  pushLabel(`❯ Goal`, ansi.bold + C.warn)
1213
- pushLine(`目标: ${agent.goal.objective}`, C.tool)
1214
- if (agent.goal.criteria) pushLine(` 完成条件: ${agent.goal.criteria}`, C.dim)
1215
- pushLine(` 状态: ${statusText} │ 已用轮数: ${agent.goal.turnsUsed ?? 0} │ 设置于: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
1282
+ pushLine(`Goal: ${agent.goal.objective}`, C.tool)
1283
+ if (agent.goal.criteria) pushLine(` Criteria: ${agent.goal.criteria}`, C.dim)
1284
+ pushLine(` Status: ${statusText} │ Turns used: ${agent.goal.turnsUsed ?? 0} │ Set at: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
1216
1285
  return
1217
1286
  }
1218
1287
  if (e.action === "cancel") {
1219
1288
  agent.goal = null
1220
1289
  pushLabel(`❯ Goal`, ansi.bold + C.dim)
1221
- pushLine(`目标已取消。`, C.dim)
1290
+ pushLine(`Goal cancelled.`, C.dim)
1222
1291
  return
1223
1292
  }
1224
1293
  // set — 需要输入目标文本
1225
- askQuestion("请输入目标描述(; 分隔完成条件)").then((text) => {
1294
+ askQuestion("Enter goal description (; separates criteria)").then((text) => {
1226
1295
  if (!text) return
1227
1296
  const semi = text.indexOf(";") >= 0 ? ";" : text.indexOf(";") >= 0 ? ";" : null
1228
1297
  const objective = semi ? text.slice(0, semi).trim() : text.trim()
1229
1298
  const criteria = semi ? text.slice(semi + 1).trim() : ""
1230
1299
  agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
1231
1300
  pushLabel(`❯ Goal`, ansi.bold + C.warn)
1232
- pushLine(`目标已设置: ${objective}`, C.tool)
1233
- if (criteria) pushLine(` 完成条件: ${criteria}`, C.dim)
1234
- else pushLine(` ⚠ 未完成条件——agent goal set 设立时会被要求补上可验证的完成条件`, C.warn)
1301
+ pushLine(`Goal set: ${objective}`, C.tool)
1302
+ if (criteria) pushLine(` Criteria: ${criteria}`, C.dim)
1303
+ else pushLine(` ⚠ No criteria — agent will be asked to provide verifiable criteria when using goal set`, C.warn)
1235
1304
  })
1236
1305
  },
1237
1306
  })
@@ -1242,7 +1311,7 @@ export async function startTUI(agent, opts = {}) {
1242
1311
  const skills = await loadSkills(agent.cwd)
1243
1312
  pushLabel(`❯ Skills`, ansi.bold + C.tool)
1244
1313
  if (skills.length === 0) {
1245
- pushLine("(无项目技能——在 .thincoder/skills/ 下创建 .md 文件即可添加)", C.dim)
1314
+ pushLine(" (none项目技能——在 .thincoder/skills/ 下创建 .md 文件即可添加)", C.dim)
1246
1315
  }
1247
1316
  for (const s of skills) {
1248
1317
  pushLine(` ${s.name}: ${s.description.slice(0, 100)}`, C.dim)
@@ -1253,24 +1322,24 @@ export async function startTUI(agent, opts = {}) {
1253
1322
  case "/mcp": {
1254
1323
  const servers = agent.config?.mcp?.servers ?? []
1255
1324
  const entries = [
1256
- { type: "header", text: `已配置 ${servers.length} MCP server` },
1257
- { type: "item", text: "查看列表", action: "list" },
1258
- { type: "item", text: "添加服务器", action: "add" },
1325
+ { type: "header", text: `${servers.length} MCP servers configured` },
1326
+ { type: "item", text: "View list", action: "list" },
1327
+ { type: "item", text: "Add server", action: "add" },
1259
1328
  ]
1260
1329
  if (servers.length > 0) {
1261
1330
  entries.push(
1262
- { type: "item", text: "移除服务器", action: "remove" },
1263
- { type: "item", text: "重连服务器", action: "connect" },
1331
+ { type: "item", text: "Remove server", action: "remove" },
1332
+ { type: "item", text: "Reconnect server", action: "connect" },
1264
1333
  )
1265
1334
  }
1266
1335
  openPicker({
1267
- title: "MCP 管理",
1336
+ title: "MCP",
1268
1337
  entries,
1269
1338
  onSelect: async (e) => {
1270
1339
  if (e.action === "list") {
1271
1340
  pushLabel(`❯ MCP Servers`, ansi.bold + C.tool)
1272
1341
  if (servers.length === 0) {
1273
- pushLine("(无 MCP server)", C.dim)
1342
+ pushLine(" (none MCP server)", C.dim)
1274
1343
  }
1275
1344
  for (const srv of servers) {
1276
1345
  const connected = agent.tools.some((t) => t._mcpName === srv.name)
@@ -1284,11 +1353,11 @@ export async function startTUI(agent, opts = {}) {
1284
1353
  }
1285
1354
  if (e.action === "remove") {
1286
1355
  const removeEntries = [
1287
- { type: "header", text: "选择要移除的服务器" },
1356
+ { type: "header", text: "Select server to remove" },
1288
1357
  ...servers.map((s) => ({ type: "item", text: s.name, name: s.name })),
1289
1358
  ]
1290
1359
  openPicker({
1291
- title: "移除 MCP",
1360
+ title: "Remove MCP",
1292
1361
  entries: removeEntries,
1293
1362
  onSelect: async (se) => {
1294
1363
  const { removeMcpTools } = await import("./mcp.mjs")
@@ -1296,18 +1365,18 @@ export async function startTUI(agent, opts = {}) {
1296
1365
  await persistRaw((raw) => { raw.mcp ??= { servers: [] }; raw.mcp.servers = raw.mcp.servers.filter((s) => s.name !== se.name) })
1297
1366
  if (agent.config?.mcp?.servers) agent.config.mcp.servers = agent.config.mcp.servers.filter((s) => s.name !== se.name)
1298
1367
  pushLabel(`❯ MCP`, ansi.bold + C.tool)
1299
- pushLine(`${se.name} 已断开并从配置移除。`, C.tool)
1368
+ pushLine(`${se.name} disconnected and removed from config.`, C.tool)
1300
1369
  },
1301
1370
  })
1302
1371
  return
1303
1372
  }
1304
1373
  if (e.action === "connect") {
1305
1374
  const connEntries = [
1306
- { type: "header", text: "选择要重连的服务器" },
1375
+ { type: "header", text: "Select server to reconnect" },
1307
1376
  ...servers.map((s) => ({ type: "item", text: s.name, name: s.name })),
1308
1377
  ]
1309
1378
  openPicker({
1310
- title: "重连 MCP",
1379
+ title: "Reconnect MCP",
1311
1380
  entries: connEntries,
1312
1381
  onSelect: async (se) => {
1313
1382
  const srv = servers.find((s) => s.name === se.name)
@@ -1315,11 +1384,11 @@ export async function startTUI(agent, opts = {}) {
1315
1384
  const { removeMcpTools, connectMcpServer } = await import("./mcp.mjs")
1316
1385
  removeMcpTools(agent, se.name)
1317
1386
  try {
1318
- pushLine(`[mcp] 重连 ${se.name}...`, C.dim)
1387
+ pushLine(`[mcp] Reconnecting ${se.name}...`, C.dim)
1319
1388
  const tools = await connectMcpServer(srv)
1320
1389
  agent.tools.push(...tools)
1321
1390
  pushLabel(`❯ MCP`, ansi.bold + C.tool)
1322
- pushLine(`${se.name} 已重连,${tools.length} 个工具可用。`, C.tool)
1391
+ pushLine(`${se.name} reconnected, ${tools.length} tools available.`, C.tool)
1323
1392
  } catch (error) {
1324
1393
  pushLine(`[mcp] ${se.name}: ${error.message}`, C.error)
1325
1394
  }
@@ -1328,13 +1397,13 @@ export async function startTUI(agent, opts = {}) {
1328
1397
  return
1329
1398
  }
1330
1399
  if (e.action === "add") {
1331
- askQuestion("输入: <名称> <URL|命令> [参数...]\nURL 自动识别: https://… → HTTP, ws://… → WebSocket, 其他 → stdio 命令").then(async (text) => {
1400
+ askQuestion("输入: <名称> <URL|Commands> [参数...]\nURL 自动识别: https://… → HTTP, ws://… → WebSocket, 其他 → stdio Commands").then(async (text) => {
1332
1401
  if (!text) return
1333
1402
  const parts = text.split(/\s+/)
1334
- if (parts.length < 2) { pushLine("用法: <名称> <URL|命令> [参数...]", C.error); return }
1403
+ if (parts.length < 2) { pushLine("用法: <名称> <URL|Commands> [参数...]", C.error); return }
1335
1404
  const [name, second, ...extras] = parts
1336
1405
  const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
1337
- if (existing) { pushLine(`[mcp] "${name}" 已存在`, C.error); return }
1406
+ if (existing) { pushLine(`[mcp] "${name}" already exists`, C.error); return }
1338
1407
  const isWS = /^wss?:\/\//.test(second)
1339
1408
  const isHTTP = /^https?:\/\//.test(second)
1340
1409
  let srv
@@ -1355,7 +1424,7 @@ export async function startTUI(agent, opts = {}) {
1355
1424
  return
1356
1425
  }
1357
1426
 
1358
- // ---- header 解析(/mcp add 共享)----
1427
+ // ---- header 解析 (/mcp add 共享)----
1359
1428
  function parseHeaders(pairs) {
1360
1429
  const headers = {}
1361
1430
  for (const pair of pairs) {
@@ -1365,7 +1434,7 @@ export async function startTUI(agent, opts = {}) {
1365
1434
  return headers
1366
1435
  }
1367
1436
 
1368
- // ---- /mcp 共享 helper: 保存配置 + 连接 ----
1437
+ // ---- /mcp 共享 helper: 保存Config + Connecting ----
1369
1438
  async function addAndConnect(srv) {
1370
1439
  await persistRaw((raw) => {
1371
1440
  raw.mcp ??= { servers: [] }
@@ -1379,16 +1448,16 @@ export async function startTUI(agent, opts = {}) {
1379
1448
  agent.config.mcp ??= { servers: [] }
1380
1449
  agent.config.mcp.servers.push(srv)
1381
1450
  try {
1382
- pushLine(`[mcp] 连接 ${srv.name}...`, C.dim)
1451
+ pushLine(`[mcp] Connecting ${srv.name}...`, C.dim)
1383
1452
  const { connectMcpServer } = await import("./mcp.mjs")
1384
1453
  const tools = await connectMcpServer(srv)
1385
1454
  agent.tools.push(...tools)
1386
1455
  pushLabel(`❯ MCP`, ansi.bold + C.tool)
1387
1456
  const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
1388
- pushLine(`${srv.name} (${desc}) 已连接,${tools.length} 个工具:`, C.tool)
1457
+ pushLine(`${srv.name} (${desc}) connected, ${tools.length} tools:`, C.tool)
1389
1458
  for (const t of tools) pushLine(` ${t.name}: ${t.description.slice(0, 100)}`, C.dim)
1390
1459
  } catch (error) {
1391
- pushLine(`[mcp] ${srv.name}: ${error.message}(配置已保存,重启后重试)`, C.error)
1460
+ pushLine(`[mcp] ${srv.name}: ${error.message} (config saved, retry after restart)`, C.error)
1392
1461
  }
1393
1462
  }
1394
1463
  case "/auto":
@@ -1402,8 +1471,8 @@ export async function startTUI(agent, opts = {}) {
1402
1471
  pushLabel(`❯ Auto`, ansi.bold + (agent.autoApprove ? C.warn : C.tool))
1403
1472
  pushLine(
1404
1473
  agent.autoApprove
1405
- ? `AUTO 已开启:所有工具调用(含写文件/bash/子 agent)不再询问,自动执行。长任务专用,/auto 关闭。`
1406
- : `AUTO 已关闭:有副作用的工具调用恢复逐个确认。`,
1474
+ ? `AUTO ON: all tool calls (write/bash/subagent) auto-approved. For long tasks. /auto to disable.`
1475
+ : `AUTO OFF: destructive tool calls require per-use approval again.`,
1407
1476
  agent.autoApprove ? C.warn : C.dim,
1408
1477
  )
1409
1478
  return
@@ -1415,19 +1484,19 @@ export async function startTUI(agent, opts = {}) {
1415
1484
  const isEffortOnly = spec.thinkApi === "effort"
1416
1485
 
1417
1486
  const entries = [
1418
- { type: "header", text: "思维模式" },
1419
- { type: "item", text: `开启${thinkingEnabled ? " ← 当前" : ""}`, action: "on" },
1420
- { type: "item", text: `关闭${!thinkingEnabled ? " ← 当前" : ""}`, action: "off" },
1421
- { type: "header", text: "推理强度" },
1487
+ { type: "header", text: "Thinking mode" },
1488
+ { type: "item", text: `On${thinkingEnabled ? " ← current" : ""}`, action: "on" },
1489
+ { type: "item", text: `Off${!thinkingEnabled ? " ← current" : ""}`, action: "off" },
1490
+ { type: "header", text: "Reasoning effort" },
1422
1491
  ...["low", "high", "max"].map((l) => ({
1423
1492
  type: "item",
1424
- text: `${l}${cur.reasoningEffort === l ? " ← 当前" : ""}`,
1493
+ text: `${l}${cur.reasoningEffort === l ? " ← current" : ""}`,
1425
1494
  action: "effort",
1426
1495
  level: l,
1427
1496
  })),
1428
1497
  ]
1429
1498
  openPicker({
1430
- title: "思维模式",
1499
+ title: "Thinking mode",
1431
1500
  entries,
1432
1501
  defaultIndex: thinkingEnabled ? 0 : 1,
1433
1502
  onSelect: async (e) => {
@@ -1435,7 +1504,7 @@ export async function startTUI(agent, opts = {}) {
1435
1504
  cur.reasoningEffort = e.level
1436
1505
  await syncProviderField("reasoningEffort", e.level)
1437
1506
  pushLabel(`❯ Think`, ansi.bold + C.tool)
1438
- pushLine(`推理强度已设为 ${e.level}`, C.tool)
1507
+ pushLine(`Reasoning effort set to ${e.level}`, C.tool)
1439
1508
  } else {
1440
1509
  const enable = e.action === "on"
1441
1510
  if (isEffortOnly) {
@@ -1452,8 +1521,8 @@ export async function startTUI(agent, opts = {}) {
1452
1521
  else await syncProviderField("reasoningEffort", cur.reasoningEffort)
1453
1522
  }
1454
1523
  pushLabel(`❯ Think`, ansi.bold + C.tool)
1455
- pushLine(`思维模式已${enable ? "开启" : "关闭"}`, C.tool)
1456
- if (enable) pushLine(`推理强度: ${cur.reasoningEffort}`, C.dim)
1524
+ pushLine(`Thinking mode已${enable ? "On" : "Off"}`, C.tool)
1525
+ if (enable) pushLine(`Reasoning effort: ${cur.reasoningEffort}`, C.dim)
1457
1526
  }
1458
1527
  },
1459
1528
  })
@@ -1465,22 +1534,22 @@ export async function startTUI(agent, opts = {}) {
1465
1534
  }
1466
1535
  case "/provider": {
1467
1536
  const entries = [
1468
- { type: "header", text: `已配置 ${agent.providers.length} 个 provider` },
1469
- { type: "item", text: "查看列表", action: "list" },
1470
- { type: "item", text: "添加 provider", action: "add" },
1537
+ { type: "header", text: `${agent.providers.length} providers` },
1538
+ { type: "item", text: "View list", action: "list" },
1539
+ { type: "item", text: "Add provider", action: "add" },
1471
1540
  ]
1472
1541
  if (agent.providers.length > 0) {
1473
1542
  entries.push(
1474
- { type: "item", text: "移除 provider", action: "remove" },
1543
+ { type: "item", text: "Remove provider", action: "remove" },
1475
1544
  )
1476
1545
  }
1477
1546
  if (!agent.provider.apiKey) {
1478
- entries.push({ type: "item", text: "设置 API Key", action: "key" })
1547
+ entries.push({ type: "item", text: "Set API Key", action: "key" })
1479
1548
  } else {
1480
- entries.push({ type: "item", text: "更换 API Key", action: "key" })
1549
+ entries.push({ type: "item", text: "Change API Key", action: "key" })
1481
1550
  }
1482
1551
  openPicker({
1483
- title: "Provider 管理",
1552
+ title: "Providers",
1484
1553
  entries,
1485
1554
  onSelect: async (e) => {
1486
1555
  if (e.action === "list") {
@@ -1488,7 +1557,7 @@ export async function startTUI(agent, opts = {}) {
1488
1557
  for (const p of agent.providers) {
1489
1558
  const active = p.name === agent.activeProvider
1490
1559
  pushLine(
1491
- `${active ? " ▸" : " "} ${p.name.padEnd(12)} ${p.model.padEnd(20)} ${p.baseURL}${p.apiKey ? " ●key" : " ○无key"}${active ? " ← 当前" : ""}`,
1560
+ `${active ? " ▸" : " "} ${p.name.padEnd(12)} ${p.model.padEnd(20)} ${p.baseURL}${p.apiKey ? " ●key" : " ○nonekey"}${active ? " ← current" : ""}`,
1492
1561
  active ? C.tool : C.dim,
1493
1562
  )
1494
1563
  }
@@ -1497,22 +1566,22 @@ export async function startTUI(agent, opts = {}) {
1497
1566
  if (e.action === "remove") {
1498
1567
  const candidates = agent.providers.filter((p) => p.name !== agent.activeProvider)
1499
1568
  if (candidates.length === 0) {
1500
- pushLine("只有当前 provider,无法移除(先用 /model 切换到别的 provider)", C.warn)
1569
+ pushLine("Cannot remove current provider (switch to another with /model first)", C.warn)
1501
1570
  return
1502
1571
  }
1503
1572
  const removeEntries = [
1504
- { type: "header", text: "选择要移除的 provider(当前使用的不可移除)" },
1573
+ { type: "header", text: "选择要移除的 provider (current使用的不可移除)" },
1505
1574
  ...candidates.map((p) => ({ type: "item", text: `${p.name} (${p.model})`, name: p.name })),
1506
1575
  ]
1507
1576
  openPicker({
1508
- title: "移除 Provider",
1577
+ title: "Remove Provider",
1509
1578
  entries: removeEntries,
1510
1579
  onSelect: async (se) => {
1511
1580
  const at = agent.providers.findIndex((p) => p.name === se.name)
1512
1581
  agent.providers.splice(at, 1)
1513
1582
  await persistRaw((raw) => { raw.providers = agent.providers })
1514
1583
  pushLabel(`❯ Provider`, ansi.bold + C.tool)
1515
- pushLine(`已删除 ${se.name}`, C.tool)
1584
+ pushLine(`Removed ${se.name}`, C.tool)
1516
1585
  },
1517
1586
  })
1518
1587
  return
@@ -1520,47 +1589,47 @@ export async function startTUI(agent, opts = {}) {
1520
1589
  if (e.action === "add") {
1521
1590
  // Add needs text input: name baseURL model
1522
1591
  askQuestion(
1523
- `输入: <名称> <baseURL> <模型>\n预设可用: ${Object.keys(PRESETS).join(", ")}\n或只输预设名(如 deepseek)自动补全`,
1592
+ `输入: <名称> <baseURL> <model>\n预设可用: ${Object.keys(PRESETS).join(", ")}\nor just a preset name (e.g. deepseek) for auto-fill`,
1524
1593
  ).then(async (text) => {
1525
1594
  if (!text) return
1526
1595
  const parts = text.split(/\s+/)
1527
1596
  const name = parts[0]
1528
1597
  if (!name) return
1529
1598
  if (agent.providers.some((p) => p.name === name)) {
1530
- pushLine(`"${name}" 已存在;先 /provider → 移除`, C.warn)
1599
+ pushLine(`"${name}" already exists;先 /provider → 移除`, C.warn)
1531
1600
  return
1532
1601
  }
1533
1602
  const preset = PRESETS[name]
1534
1603
  const baseURL = (parts[1] ?? preset?.baseURL)?.replace(/\/+$/, "")
1535
1604
  const model = parts[2] ?? preset?.model
1536
1605
  if (!baseURL || !model) {
1537
- pushLine(`缺少参数: ${name} <baseURL> <模型>`, C.error)
1606
+ pushLine(`Missing args: ${name} <baseURL> <model>`, C.error)
1538
1607
  return
1539
1608
  }
1540
- if (!/^https?:\/\//.test(baseURL)) { pushLine(`baseURL 应以 http(s):// 开头`, C.error); return }
1609
+ if (!/^https?:\/\//.test(baseURL)) { pushLine(`baseURL must start with http(s)://`, C.error); return }
1541
1610
  agent.providers.push({ name, baseURL, model, ...(preset?.desc ? { desc: preset.desc } : {}) })
1542
1611
  await persistRaw((raw) => { raw.providers = agent.providers })
1543
1612
  pushLabel(`❯ Provider`, ansi.bold + C.tool)
1544
- pushLine(`已添加 ${name}(${baseURL} / ${model})`, C.tool)
1545
- pushLine(`下一步: /provider → 设置 Key`, C.dim)
1613
+ pushLine(`Added ${name} (${baseURL} / ${model})`, C.tool)
1614
+ pushLine(`Next: /provider → Set Key`, C.dim)
1546
1615
  })
1547
1616
  return
1548
1617
  }
1549
1618
  if (e.action === "key") {
1550
1619
  // Key: pick which provider, then prompt for key
1551
1620
  const keyEntries = [
1552
- { type: "header", text: "选择要配 key provider" },
1621
+ { type: "header", text: "Select provider to configure key" },
1553
1622
  ...agent.providers.map((p) => ({
1554
1623
  type: "item",
1555
- text: `${p.name}${p.name === agent.activeProvider ? " ← 当前" : ""}${p.apiKey ? " ●已有key" : " ○无key"}`,
1624
+ text: `${p.name}${p.name === agent.activeProvider ? " ← current" : ""}${p.apiKey ? " ●has key" : " ○nonekey"}`,
1556
1625
  name: p.name,
1557
1626
  })),
1558
1627
  ]
1559
1628
  openPicker({
1560
- title: "配置 API Key",
1629
+ title: "Configure API Key",
1561
1630
  entries: keyEntries,
1562
1631
  onSelect: (se) => {
1563
- askQuestion(`为 ${se.name} 输入 API Key:`).then(async (key) => {
1632
+ askQuestion(`Enter API key for ${se.name}:`).then(async (key) => {
1564
1633
  if (!key) return
1565
1634
  await setProviderKey(se.name, key)
1566
1635
  })
@@ -1573,29 +1642,29 @@ export async function startTUI(agent, opts = {}) {
1573
1642
  }
1574
1643
  case "/config": {
1575
1644
  const entries = [
1576
- { type: "header", text: "配置管理" },
1577
- { type: "item", text: "查看当前配置", action: "view" },
1578
- { type: "item", text: "设置 embedding key(向量检索)", action: "embedkey" },
1579
- { type: "item", text: "高级设置(set path value", action: "set" },
1645
+ { type: "header", text: "Config" },
1646
+ { type: "item", text: "View current config", action: "view" },
1647
+ { type: "item", text: "Set embedding key (vector search)", action: "embedkey" },
1648
+ { type: "item", text: "Advanced (set path value)", action: "set" },
1580
1649
  ]
1581
1650
  openPicker({
1582
- title: "配置管理",
1651
+ title: "Config",
1583
1652
  entries,
1584
1653
  onSelect: async (e) => {
1585
1654
  if (e.action === "view") {
1586
1655
  const { configPath: cp } = await import("./config.mjs")
1587
- pushLabel(`❯ 配置`, ansi.bold + C.tool)
1588
- pushLine(`激活: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
1656
+ pushLabel(`❯ Config`, ansi.bold + C.tool)
1657
+ pushLine(`Active: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
1589
1658
  pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
1590
1659
  const ac = agent.config?.agent ?? {}
1591
1660
  const tn = `${ac.compactThreshold ?? 100000}${ac.compactThresholdAuto ? " (auto)" : ""}`
1592
1661
  pushLine(`agent: maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${tn}`, C.dim)
1593
- pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${agent.config?.embedding?.model ?? ""})` : "disabled(纯 FTS 检索)"}`, C.dim)
1594
- pushLine(`配置文件: ${cp}`, C.dim)
1662
+ pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${agent.config?.embedding?.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
1663
+ pushLine(`Config文件: ${cp}`, C.dim)
1595
1664
  return
1596
1665
  }
1597
1666
  if (e.action === "embedkey") {
1598
- askQuestion("输入 embedding 服务的 API Key(默认 SiliconFlow bge-m3):").then(async (key) => {
1667
+ askQuestion("Enter embedding API key (default: SiliconFlow bge-m3):").then(async (key) => {
1599
1668
  if (!key) return
1600
1669
  agent.config.embedding ??= {}
1601
1670
  agent.config.embedding.apiKey = key
@@ -1605,16 +1674,16 @@ export async function startTUI(agent, opts = {}) {
1605
1674
  agent.memory.embedder = createEmbedder(agent.config.embedding)
1606
1675
  }
1607
1676
  pushLabel(`❯ Config`, ansi.bold + C.tool)
1608
- pushLine(`embedding key 已保存,向量检索已启用`, C.tool)
1677
+ pushLine(`Embedding key saved, vector search enabled`, C.tool)
1609
1678
  })
1610
1679
  return
1611
1680
  }
1612
1681
  if (e.action === "set") {
1613
- askQuestion("输入: <path> <value>(如 agent.maxTurns 80,支持 a.b 嵌套):").then(async (text) => {
1682
+ askQuestion("Enter: <path> <value> (e.g. agent.maxTurns 80, supports a.b nesting):").then(async (text) => {
1614
1683
  if (!text) return
1615
1684
  const parts = text.split(/\s+/)
1616
1685
  const [path, value] = [parts[0], parts.slice(1).join(" ")]
1617
- if (!path || !value) { pushLine("用法: <path> <value> agent.maxTurns 80", C.error); return }
1686
+ if (!path || !value) { pushLine("Usage: <path> <value> e.g. agent.maxTurns 80", C.error); return }
1618
1687
  try {
1619
1688
  const { configPath, loadConfig, saveConfig } = await import("./config.mjs")
1620
1689
  const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
@@ -1629,9 +1698,9 @@ export async function startTUI(agent, opts = {}) {
1629
1698
  agent.activeProvider = cfg.activeProvider
1630
1699
  agent.config = cfg
1631
1700
  pushLabel(`❯ Config`, ansi.bold + C.tool)
1632
- pushLine(`已保存: ${path} = ${value}`, C.tool)
1701
+ pushLine(`Saved: ${path} = ${value}`, C.tool)
1633
1702
  } catch (error) {
1634
- pushLine(`保存失败: ${error.message}`, C.error)
1703
+ pushLine(`Save failed: ${error.message}`, C.error)
1635
1704
  }
1636
1705
  })
1637
1706
  }
@@ -1662,7 +1731,7 @@ export async function startTUI(agent, opts = {}) {
1662
1731
  return
1663
1732
  }
1664
1733
  default:
1665
- pushLine(`Unknown command: ${cmd}(/help 查看可用命令)`, C.error)
1734
+ pushLine(`Unknown command: ${cmd} (/help 查看可用Commands)`, C.error)
1666
1735
  return
1667
1736
  }
1668
1737
  }
@@ -1673,18 +1742,18 @@ export async function startTUI(agent, opts = {}) {
1673
1742
  return `${key.slice(0, 5)}…${key.slice(-4)}`
1674
1743
  }
1675
1744
 
1676
- /** Tab 补全候选:命令名 / 子命令 / provider 名 / 预设名 / think 参数 */
1745
+ /** Tab 补全候选:Commands名 / 子Commands / provider 名 / 预设名 / think 参数 */
1677
1746
  function completions(input) {
1678
1747
  if (!input.startsWith("/")) return []
1679
1748
  const parts = input.split(/\s+/)
1680
- // 还在敲第一个 token:补命令名
1749
+ // 还在敲第一个 token:补Commands名
1681
1750
  if (parts.length === 1) {
1682
1751
  return SLASH_COMMANDS.filter((c) => c.name.startsWith(parts[0])).map((c) => c.name)
1683
1752
  }
1684
1753
  const cmd = parts[0]
1685
- const last = parts.at(-1) // 结尾是空格时为 "",即列出全部候选
1754
+ const last = parts.at(-1) // 结尾是空格时Enter API key for "",即列出全部候选
1686
1755
  const head = parts.slice(0, -1).join(" ")
1687
- const argIndex = parts.length - 2 // 正在敲第几个参数(0 基)
1756
+ const argIndex = parts.length - 2 // 正在敲第几个参数 (0 基)
1688
1757
  const match = (cands) => cands.filter((c) => c.startsWith(last)).map((c) => `${head} ${c}`)
1689
1758
  if (cmd === "/model" && argIndex === 0) return match(agent.providers.map((p) => p.name))
1690
1759
  if (cmd === "/provider") {
@@ -1722,7 +1791,7 @@ export async function startTUI(agent, opts = {}) {
1722
1791
  render()
1723
1792
  }
1724
1793
 
1725
- /** 读配置文件 → 修改 → 写回;文件不存在时从空对象开始 */
1794
+ /** 读Config文件 → 修改 → 写回;文件not found时从空对象开始 */
1726
1795
  async function persistRaw(mutate) {
1727
1796
  const { saveConfig, configPath } = await import("./config.mjs")
1728
1797
  const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
@@ -1730,7 +1799,7 @@ export async function startTUI(agent, opts = {}) {
1730
1799
  saveConfig(raw)
1731
1800
  }
1732
1801
 
1733
- /** 把当前激活 provider 的某个字段同步到 providers 列表并持久化 */
1802
+ /** 把current激活 provider 的某个字段同步到 providers 列表并持久化 */
1734
1803
  async function syncProviderField(field, value) {
1735
1804
  const target = agent.providers.find((p) => p.name === agent.activeProvider)
1736
1805
  if (!target) return
@@ -1742,12 +1811,12 @@ export async function startTUI(agent, opts = {}) {
1742
1811
  })
1743
1812
  }
1744
1813
 
1745
- // ---------------------------------------------------------- 模型选择器(/model)
1814
+ // ---------------------------------------------------------- 模型选择器 (/model)
1746
1815
 
1747
1816
  const pickerItems = () => state.picker?.entries.filter((e) => e.type === "item") ?? []
1748
1817
 
1749
1818
  /** 打开通用列表选择器。entries 含 { type: "header"|"item", text, note?, ...extra },
1750
- * onSelect 拿到选中条目(含 extra 字段透传),onCancel 在 Esc 时调。 */
1819
+ * onSelect 拿到选中条目 (含 extra 字段透传),onCancel 在 Esc 时调。 */
1751
1820
  function openPicker({ title, entries, onSelect, onCancel, defaultIndex = 0 }) {
1752
1821
  state.picker = { title, entries, lines: [], index: defaultIndex, scroll: 0, selectedLine: 0, onSelect, onCancel }
1753
1822
  renderPickerLines()
@@ -1785,17 +1854,17 @@ export async function startTUI(agent, opts = {}) {
1785
1854
  render()
1786
1855
  }
1787
1856
 
1788
- // ========== 模型选择器(基于通用 picker,异步拉取远端模型列表) ==========
1857
+ // ========== 模型选择器 (基于通用 picker,异步拉取远端模型列表) ==========
1789
1858
 
1790
1859
  async function openModelPicker() {
1791
1860
  const entries = []
1792
1861
  for (const p of agent.providers) {
1793
- entries.push({ type: "header", text: p.name, note: `${p.baseURL}${p.apiKey ? "" : "(未配 key"} 加载中...` })
1862
+ entries.push({ type: "header", text: p.name, note: `${p.baseURL}${p.apiKey ? "" : " (no key)"} loading...` })
1794
1863
  entries.push({ type: "item", text: p.model, provider: p.name, model: p.model })
1795
1864
  }
1796
1865
  const onSelect = (e) => selectModel(e).catch((err) => pushLine(`[error] ${err.message}`, C.error))
1797
- openPicker({ title: "选择模型", entries, onSelect })
1798
- // 默认选中当前在用的模型
1866
+ openPicker({ title: "Select Model", entries, onSelect })
1867
+ // 默认选中current在用的模型
1799
1868
  const current = pickerItems().findIndex(
1800
1869
  (e) => e.provider === agent.activeProvider && e.model === agent.provider.model,
1801
1870
  )
@@ -1806,7 +1875,7 @@ export async function startTUI(agent, opts = {}) {
1806
1875
  await Promise.all(
1807
1876
  agent.providers.map(async (p) => {
1808
1877
  const header = entries.find((e) => e.type === "header" && e.provider === undefined && e.text === p.name)
1809
- const noteBase = `${p.baseURL}${p.apiKey ? "" : "(未配 key"}`
1878
+ const noteBase = `${p.baseURL}${p.apiKey ? "" : " (no key)"}`
1810
1879
  try {
1811
1880
  const envKey = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }[p.name]
1812
1881
  let apiKey = p.apiKey
@@ -1824,65 +1893,65 @@ export async function startTUI(agent, opts = {}) {
1824
1893
  )
1825
1894
  if (header) header.note = noteBase
1826
1895
  } catch (error) {
1827
- if (header) header.note = `${noteBase} (拉取失败: ${sliceByWidth(error.message, 60)})`
1896
+ if (header) header.note = `${noteBase} (fetch failed: ${sliceByWidth(error.message, 60)})`
1828
1897
  }
1829
1898
  if (state.picker?.entries === entries) renderPickerLines()
1830
1899
  }),
1831
1900
  )
1832
1901
  }
1833
1902
 
1834
- /** 给指定 provider 写 key(内存 + 配置文件);若它是当前激活的,同步运行时 */
1903
+ /** 给指定 provider 写 key (内存 + Config文件);若它是current激活的,同步运行时 */
1835
1904
  async function setProviderKey(name, key) {
1836
1905
  const target = agent.providers.find((p) => p.name === name)
1837
1906
  if (!target) {
1838
- pushLine(`未找到 provider "${name}"`, C.error)
1907
+ pushLine(`Provider "${name}"`, C.error)
1839
1908
  return
1840
1909
  }
1841
1910
  target.apiKey = key
1842
1911
  if (name === agent.activeProvider) agent.provider.apiKey = key
1843
1912
  await persistRaw((raw) => { raw.providers = agent.providers })
1844
1913
  pushLabel(`❯ Provider`, ansi.bold + C.tool)
1845
- pushLine(`apiKey 已保存到 ${name}`, C.tool)
1914
+ pushLine(`API key saved to ${name}`, C.tool)
1846
1915
  }
1847
1916
 
1848
- // ---------------------------------------------------------- 初始配置向导(首次启动)
1917
+ // ---------------------------------------------------------- 初始Config向导 (首次启动)
1849
1918
 
1850
- /** 菜单步的候选项:已有 provider(未配 key 的标注)+ 未添加的预设 + 自定义 */
1919
+ /** 菜单步的候选项:已有 provider (no key 的标注)+ 未添加的预设 + 自定义 */
1851
1920
  function wizardProviderItems() {
1852
1921
  const items = []
1853
1922
  for (const p of agent.providers) {
1854
- items.push({ kind: "existing", name: p.name, baseURL: p.baseURL, model: p.model, label: `${p.name}(已添加${p.apiKey ? "" : ",未配 key"})` })
1923
+ items.push({ kind: "existing", name: p.name, baseURL: p.baseURL, model: p.model, label: `${p.name} (added${p.apiKey ? "" : ",no key"})` })
1855
1924
  }
1856
1925
  for (const [name, p] of Object.entries(PRESETS)) {
1857
1926
  if (!agent.providers.some((x) => x.name === name)) {
1858
- items.push({ kind: "preset", name, baseURL: p.baseURL, model: p.model, label: `${name}(${p.desc})` })
1927
+ items.push({ kind: "preset", name, baseURL: p.baseURL, model: p.model, label: `${name} (${p.desc})` })
1859
1928
  }
1860
1929
  }
1861
- items.push({ kind: "custom", name: null, label: "自定义端点…" })
1930
+ items.push({ kind: "custom", name: null, label: "Custom endpoint…" })
1862
1931
  return items
1863
1932
  }
1864
1933
 
1865
- /** 文本步骤定义:提示语 + 校验(通过返回 true,否则返回错误文案) */
1934
+ /** 文本步骤定义:提示语 + 校验 (通过返回 true,否则返回错误文案) */
1866
1935
  const WIZARD_STEPS = {
1867
1936
  name: {
1868
- prompt: "给这个 provider 起个名字(字母/数字/-/_,如 my-openai)",
1937
+ prompt: "给这个 provider 起个名字 (字母/数字/-/_,如 my-openai)",
1869
1938
  validate: (v) =>
1870
- (/^[\w-]+$/.test(v) && !agent.providers.some((p) => p.name === v)) || "名字需为字母/数字/-/_,且不与已有 provider 重名",
1939
+ (/^[\w-]+$/.test(v) && !agent.providers.some((p) => p.name === v)) || "Name must be alphanumeric/-/_ and unique",
1871
1940
  },
1872
1941
  baseURL: {
1873
- prompt: "输入 baseURL(如 https://api.openai.com/v1)",
1874
- validate: (v) => /^https?:\/\/.+/.test(v) || "baseURL 应以 http(s):// 开头",
1942
+ prompt: "输入 baseURL (如 https://api.openai.com/v1)",
1943
+ validate: (v) => /^https?:\/\/.+/.test(v) || "baseURL must start with http(s)://",
1875
1944
  },
1876
1945
  model: {
1877
- prompt: "输入模型名(如 gpt-4o)",
1878
- validate: (v) => v.length > 0 || "模型名不能为空",
1946
+ prompt: "输入模型名 (如 gpt-4o)",
1947
+ validate: (v) => v.length > 0 || "Model name required",
1879
1948
  },
1880
1949
  key: {
1881
1950
  prompt: "输入 API key",
1882
1951
  validate: (v) => v.length > 0 || "key 不能为空",
1883
1952
  },
1884
1953
  embedkey: {
1885
- prompt: "可选:embedding API keySiliconFlow,记忆向量检索用;直接回车跳过)",
1954
+ prompt: "可选:embedding API key (SiliconFlow,记忆向量检索用;直接回车跳过)",
1886
1955
  validate: () => true, // 可跳过
1887
1956
  },
1888
1957
  }
@@ -1898,7 +1967,7 @@ export async function startTUI(agent, opts = {}) {
1898
1967
  if (!w) return
1899
1968
  const lines = []
1900
1969
  if (w.step === "provider") {
1901
- lines.push({ text: " 选择一个模型提供商:", color: C.text })
1970
+ lines.push({ text: " Choose a model provider:", color: C.text })
1902
1971
  wizardProviderItems().forEach((it, i) => {
1903
1972
  if (i === w.index) w.selectedLine = lines.length
1904
1973
  lines.push({
@@ -1908,11 +1977,11 @@ export async function startTUI(agent, opts = {}) {
1908
1977
  })
1909
1978
  } else {
1910
1979
  const f = w.fields
1911
- if (f.name) lines.push({ text: ` 提供商: ${f.name}`, color: C.dim })
1980
+ if (f.name) lines.push({ text: ` Provider: ${f.name}`, color: C.dim })
1912
1981
  if (f.baseURL) lines.push({ text: ` baseURL: ${f.baseURL}`, color: C.dim })
1913
1982
  if (f.model) lines.push({ text: ` 模型: ${f.model}`, color: C.dim })
1914
1983
  lines.push({ text: ` ❯ ${WIZARD_STEPS[w.step].prompt}`, color: ansi.bold + C.text })
1915
- lines.push({ text: " (在下方输入框输入)", color: C.dim })
1984
+ lines.push({ text: " (type in input box below)", color: C.dim })
1916
1985
  w.selectedLine = 0
1917
1986
  }
1918
1987
  if (w.error) lines.push({ text: ` ${w.error}`, color: C.error })
@@ -1955,11 +2024,11 @@ export async function startTUI(agent, opts = {}) {
1955
2024
 
1956
2025
  function cancelWizard() {
1957
2026
  state.wizard = null
1958
- pushLine("已跳过初始配置。之后随时可用 /provider add 添加提供商、/provider key 配 key。", C.dim)
2027
+ pushLine("已跳过初始Config。之后随时可用 /provider add 添加Provider、/provider key 配 key。", C.dim)
1959
2028
  render()
1960
2029
  }
1961
2030
 
1962
- /** 向导完成:写入 provider(有则更新)、设为激活、持久化,然后接模型选择器 */
2031
+ /** 向导完成:写入 provider (有则更新)、设为激活、持久化,然后接模型选择器 */
1963
2032
  async function finishWizard() {
1964
2033
  const f = state.wizard.fields
1965
2034
  state.wizard = null
@@ -1978,7 +2047,7 @@ export async function startTUI(agent, opts = {}) {
1978
2047
  })
1979
2048
  agent.config.activeProvider = f.name
1980
2049
  pushLabel(`❯ Setup`, ansi.bold + C.tool)
1981
- pushLine(`配置完成:${f.name} / ${f.model}(已写入配置文件)`, C.tool)
2050
+ pushLine(`Setup complete: ${f.name} / ${f.model} (saved to config)`, C.tool)
1982
2051
  // embedding key:配了就启用向量检索,没配提示事后通道
1983
2052
  if (f.embedkey) {
1984
2053
  agent.config.embedding ??= {}
@@ -1988,11 +2057,11 @@ export async function startTUI(agent, opts = {}) {
1988
2057
  const { createEmbedder } = await import("./embedding.mjs")
1989
2058
  agent.memory.embedder = createEmbedder(agent.config.embedding)
1990
2059
  }
1991
- pushLine(`向量检索已启用(${agent.config.embedding.model ?? "BAAI/bge-m3"})`, C.tool)
2060
+ pushLine(`Vector search enabled (${agent.config.embedding.model ?? "BAAI/bge-m3"})`, C.tool)
1992
2061
  } else {
1993
- pushLine(`向量检索未启用(记忆退化为纯文本检索);之后可 /config embedkey <key> 开启`, C.dim)
2062
+ pushLine(`向量检索未启用 (记忆退化为纯文本检索);之后可 /config embedkey <key> On`, C.dim)
1994
2063
  }
1995
- pushLine(`选择要用的模型(Esc 保持 ${f.model})`, C.dim)
2064
+ pushLine(`Select model (Esc to keep ${f.model})`, C.dim)
1996
2065
  openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
1997
2066
  }
1998
2067
 
@@ -2014,7 +2083,7 @@ export async function startTUI(agent, opts = {}) {
2014
2083
  const { resolveCompactThreshold } = await import("./config.mjs")
2015
2084
  const { value } = resolveCompactThreshold(null, item.model)
2016
2085
  agent.config.agent.compactThreshold = value
2017
- thresholdNote = `,压缩阈值随模型调整为 ${value}`
2086
+ thresholdNote = `, compact threshold adjusted to ${value}`
2018
2087
  }
2019
2088
  await persistRaw((raw) => {
2020
2089
  raw.providers = agent.providers
@@ -2022,14 +2091,14 @@ export async function startTUI(agent, opts = {}) {
2022
2091
  })
2023
2092
  agent.config.activeProvider = item.provider
2024
2093
  pushLabel(`❯ Model`, ansi.bold + C.tool)
2025
- pushLine(`已切换到 ${item.provider} / ${item.model}${thresholdNote}(已持久化)`, C.tool)
2026
- if (!agent.provider.apiKey) pushLine(`该 provider 还没配 key: /config key <apikey>`, C.warn)
2094
+ pushLine(`Switched to ${item.provider} / ${item.model}${thresholdNote} (persisted)`, C.tool)
2095
+ if (!agent.provider.apiKey) pushLine(`Provider has no key: /provider Set Key`, C.warn)
2027
2096
  }
2028
2097
 
2029
- /** /distill:从当前会话提取候选,逐条 y/n 确认后入库 */
2098
+ /** /distill:从current会话提取候选,逐条 y/n 确认后入库 */
2030
2099
  async function runDistill() {
2031
2100
  if (agent.history.length === 0) {
2032
- pushLine("[distill] 当前会话为空,没有可提取的内容", C.dim)
2101
+ pushLine("[distill] current会话为空,没有可提取的内容", C.dim)
2033
2102
  return
2034
2103
  }
2035
2104
  state.processing = true
@@ -2037,17 +2106,17 @@ export async function startTUI(agent, opts = {}) {
2037
2106
  render()
2038
2107
  try {
2039
2108
  const { extractCandidates, historyToTranscript, saveCandidate } = await import("./distill.mjs")
2040
- pushLine("[distill] 正在分析会话...", C.tool)
2109
+ pushLine("[distill] Analyzing session...", C.tool)
2041
2110
  const candidates = await extractCandidates(agent.provider, historyToTranscript(agent.history))
2042
2111
  if (candidates.length === 0) {
2043
- pushLine("[distill] 本次会话没有值得沉淀的知识", C.dim)
2112
+ pushLine("[distill] No knowledge worth saving from this session", C.dim)
2044
2113
  return
2045
2114
  }
2046
2115
  let saved = 0
2047
2116
  for (const c of candidates) {
2048
- pushLine(`── 候选 [${c.type}] ${c.title} (scope: ${c.scope ?? "personal"})`, C.warn)
2117
+ pushLine(`── Candidate [${c.type}] ${c.title} (scope: ${c.scope ?? "personal"})`, C.warn)
2049
2118
  for (const line of c.content.split("\n").slice(0, 6)) pushLine(` ${line}`, C.dim)
2050
- if (c.type === "rule") pushLine(" (rule 类建议手动撰写;确认提取请按 y)", C.warn)
2119
+ if (c.type === "rule") pushLine(" (rule type consider writing manually; press y to extract)", C.warn)
2051
2120
  const accept = await askPermission("distill-save", { title: c.title })
2052
2121
  if (!accept) {
2053
2122
  pushLine(" skipped", C.dim)
@@ -2057,7 +2126,7 @@ export async function startTUI(agent, opts = {}) {
2057
2126
  pushLine(` saved -> ${where}`, C.tool)
2058
2127
  saved++
2059
2128
  }
2060
- pushLine(`[distill] 完成:入库 ${saved}/${candidates.length} 条`, C.tool)
2129
+ pushLine(`[distill] Done: saved ${saved}/${candidates.length} 条`, C.tool)
2061
2130
  } catch (error) {
2062
2131
  pushLine(`[distill] error: ${error.message}`, C.error)
2063
2132
  } finally {
@@ -2071,7 +2140,7 @@ export async function startTUI(agent, opts = {}) {
2071
2140
 
2072
2141
  // keypress 挂在过滤后的 keyStream 上:鼠标序列已在上游滤网中处理并剥除
2073
2142
  keyStream.on("keypress", (str, key = {}) => {
2074
- // 权限确认态:y 批准 / n 拒绝 / a 批准并开启 AUTO(后续不再询问)
2143
+ // 权限确认态:y 批准 / n 拒绝 / a 批准并On AUTO (后续不再询问)
2075
2144
  if (state.permission) {
2076
2145
  const answer = (str || "").toLowerCase()
2077
2146
  const isContinue = state.permission.name === "continue"
@@ -2085,10 +2154,10 @@ export async function startTUI(agent, opts = {}) {
2085
2154
  agent.autoApprove = true
2086
2155
  agent._pendingReminders = agent._pendingReminders ?? []
2087
2156
  agent._pendingReminders.push("[System reminder: AUTO mode is now ON. All tool calls are automatically approved. Use /auto to disable.]")
2088
- pushLine(` [auto] AUTO 已开启:后续工具调用不再询问(/auto 关闭)`, C.warn)
2157
+ pushLine(` [auto] AUTO 已On:后续工具调用不再询问 (/auto Off)`, C.warn)
2089
2158
  }
2090
2159
  const approved = answer === "y" || (answer === "a" && !isContinue)
2091
- // 决定落痕:对话区留下批准/拒绝记录(continue 询问有自己的输出,不重复记)
2160
+ // 决定落痕:对话区留下批准/拒绝记录 (continue 询问有自己的输出,不重复记)
2092
2161
  if (!isContinue) {
2093
2162
  pushLine(` [${approved ? "approved" : "denied"}] ${name}`, approved ? C.dim : C.error)
2094
2163
  }
@@ -2150,7 +2219,7 @@ export async function startTUI(agent, opts = {}) {
2150
2219
  if (key.ctrl && key.name === "c") {
2151
2220
  if (state.processing && state.controller) {
2152
2221
  state.controller.abort()
2153
- pushLine("[中止中…]", C.warn)
2222
+ pushLine("[Aborting…]", C.warn)
2154
2223
  render()
2155
2224
  return
2156
2225
  }
@@ -2177,7 +2246,7 @@ export async function startTUI(agent, opts = {}) {
2177
2246
  return
2178
2247
  }
2179
2248
 
2180
- // 初始配置向导:菜单步 ↑↓/Enter/Esc;文本步 Enter 提交、Esc 取消,编辑键落到正常输入
2249
+ // 初始Config向导:菜单步 ↑↓/Enter/Esc;文本步 Enter 提交、Esc 取消,编辑键落到正常输入
2181
2250
  if (state.wizard) {
2182
2251
  const w = state.wizard
2183
2252
  if (key.name === "escape") {
@@ -2219,7 +2288,7 @@ export async function startTUI(agent, opts = {}) {
2219
2288
 
2220
2289
  if (state.processing) return // 处理中锁定输入
2221
2290
 
2222
- // Tab:斜杠命令补全(循环候选);其余输入忽略(\t 会顶破输入框,永不直接插入)
2291
+ // Tab:斜杠Commands补全 (循环候选);其余输入忽略 (\t 会顶破输入框,永不直接插入)
2223
2292
  if (key.name === "tab") {
2224
2293
  handleTab()
2225
2294
  return
@@ -2293,7 +2362,14 @@ export async function startTUI(agent, opts = {}) {
2293
2362
  return
2294
2363
  }
2295
2364
 
2296
- // 可打印字符 / 粘贴(str 可能一次多个字符);Tab 一律转成两个空格(\t 显示宽度不定,会顶破输入框)
2365
+ // Ctrl+V (Unix) / Alt+V (Windows):粘贴剪贴板图片 存临时文件 → 输入框插入 read_image
2366
+ const isPasteImage = (key.name === "v" && (key.ctrl || key.meta)) || (key.name === "v" && key.alt)
2367
+ if (isPasteImage) {
2368
+ pasteClipboardImage(agent).catch((e) => pushLine(`[error] ${e.message}`, C.error))
2369
+ return
2370
+ }
2371
+
2372
+ // 可打印字符 / 粘贴 (str 可能一次多个字符);Tab 一律转成两个空格 (\t 显示宽度不定,会顶破输入框)
2297
2373
  // \r\n 在 Windows raw mode 下可能漏进来冲乱页面
2298
2374
  if (str && !key.ctrl && !key.meta) {
2299
2375
  const chars = [...str.replace(/[\r\n]+/g, "").replace(/\t/g, " ")]
@@ -2305,18 +2381,18 @@ export async function startTUI(agent, opts = {}) {
2305
2381
 
2306
2382
  // 启动画面
2307
2383
  if (!agent.provider.apiKey) {
2308
- pushLabel(`欢迎使用 ThinCoder!`, ansi.bold + C.tool)
2309
- pushLine("检测到还没配置 API key,进入初始配置(Esc 可随时跳过)", C.text)
2384
+ pushLabel(`Welcome to ThinCoder!`, ansi.bold + C.tool)
2385
+ pushLine("检测到还没Config API key,进入初始Config (Esc 可随时跳过)", C.text)
2310
2386
  startWizard()
2311
2387
  } else {
2312
2388
  pushLine(`Welcome to ThinCoder. Provider: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
2313
2389
  }
2314
2390
  pushLine(`Tools: ${agent.tools.map((t) => t.name).join(", ")}`, C.dim)
2315
- // 恢复上次会话:重建对话区显示(tool 结果行省略,保持清爽)
2391
+ // 恢复上次会话:重建对话区显示 (tool 结果行省略,保持清爽)
2316
2392
  if (opts.restored?.display?.length) {
2317
2393
  // 用户视角的恢复:display 是退出前对话区的原样快照,所见即所得
2318
2394
  state.lines = [...opts.restored.display.map((l) => ({ text: l.text, color: l.color })), ...state.lines]
2319
- pushLabel(`── 已恢复上次会话(退出前原样回放);/new 开始新会话 ──`, C.warn)
2395
+ pushLabel(`── Restored previous session; /new for a fresh session ──`, C.warn)
2320
2396
  } else if (opts.restored?.history?.length) {
2321
2397
  // 重建对话区:user/assistant 消息逐条展示,tool 结果行只保留首行摘要
2322
2398
  for (let i = 0; i < opts.restored.history.length; i++) {
@@ -2338,15 +2414,15 @@ export async function startTUI(agent, opts = {}) {
2338
2414
  }
2339
2415
  // tool 消息本身不单独渲染——已在 assistant 的 tool_calls 后以摘要形式展示
2340
2416
  }
2341
- pushLabel(`── 已恢复上次会话(${opts.restored.history.length} 条消息);/new 开始新会话 ──`, C.warn)
2417
+ pushLabel(`── Restored previous session (${opts.restored.history.length} messages); /new for a fresh session ──`, C.warn)
2342
2418
  }
2343
2419
  // 有归档槽位时给个提示
2344
2420
  if (listSlots(agent.cwd).length > 0) {
2345
- pushLine("提示:存在归档会话,/session 可查看/切换", C.dim)
2421
+ pushLine("Tip: archived sessions available — /session to view/switch", C.dim)
2346
2422
  }
2347
2423
  render()
2348
2424
 
2349
- // 后台索引(进界面后再跑,不阻塞启动);进度走底部状态栏,不往对话区塞行
2425
+ // 后台索引 (进界面后再跑,不阻塞启动);进度走底部状态栏,不往对话区塞行
2350
2426
  ;(async () => {
2351
2427
  const { codeSync, docSync } = await import("./memory.mjs")
2352
2428
  const cwd = agent.cwd