thincoder 0.8.1 → 0.8.3

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.
Files changed (98) hide show
  1. package/README.md +2 -1
  2. package/bin/thincoder.cjs +4 -0
  3. package/bin/thincoder.mjs +25 -25
  4. package/package.json +2 -2
  5. package/src/agent/dispatch.mjs +17 -12
  6. package/src/agent/helpers.mjs +22 -11
  7. package/src/agent/setup.mjs +32 -10
  8. package/src/agent-tools/goal.mjs +7 -6
  9. package/src/agent-tools/plan.mjs +3 -3
  10. package/src/agent-tools/recent-changes.mjs +3 -3
  11. package/src/agent-tools/skill.mjs +6 -6
  12. package/src/agent-tools/subagent.mjs +19 -18
  13. package/src/agent-tools/task.mjs +4 -4
  14. package/src/agent-tools/verify.mjs +21 -19
  15. package/src/agent-tools.mjs +3 -3
  16. package/src/agent.mjs +58 -37
  17. package/src/cli/distill-command.mjs +2 -2
  18. package/src/cli/make-agent.mjs +9 -9
  19. package/src/cli/memory-command.mjs +1 -1
  20. package/src/cli/permission.mjs +3 -3
  21. package/src/cli/setup-wizard.mjs +15 -15
  22. package/src/config.mjs +55 -53
  23. package/src/context.mjs +59 -56
  24. package/src/distill.mjs +35 -35
  25. package/src/embedding.mjs +17 -17
  26. package/src/git/checkpoint.mjs +211 -38
  27. package/src/git/gitmem.mjs +21 -20
  28. package/src/markdown.mjs +13 -13
  29. package/src/mcp/helpers.mjs +6 -1
  30. package/src/mcp/transport-http.mjs +2 -1
  31. package/src/mcp/transport-stdio.mjs +3 -2
  32. package/src/mcp/transport-ws.mjs +3 -2
  33. package/src/mcp.mjs +5 -2
  34. package/src/memory/code-index.mjs +16 -14
  35. package/src/memory/code-sync.mjs +36 -26
  36. package/src/memory/core.mjs +42 -35
  37. package/src/memory/docs.mjs +24 -15
  38. package/src/memory/schema.mjs +28 -27
  39. package/src/memory.mjs +2 -2
  40. package/src/prompts/coder.md +1 -1
  41. package/src/prompts/discipline.md +3 -0
  42. package/src/prompts/main.md +1 -2
  43. package/src/prompts/system.md +21 -16
  44. package/src/provider/core.mjs +11 -6
  45. package/src/provider/index.mjs +2 -2
  46. package/src/provider/rate.mjs +11 -11
  47. package/src/session.mjs +73 -40
  48. package/src/skills.mjs +17 -17
  49. package/src/tools/checkpoint.md +6 -2
  50. package/src/tools/file.mjs +16 -12
  51. package/src/tools/git.mjs +83 -12
  52. package/src/tools/index.mjs +1 -1
  53. package/src/tools/patch.mjs +20 -18
  54. package/src/tools/repomap-parse.mjs +17 -17
  55. package/src/tools/repomap.mjs +29 -29
  56. package/src/tools/shared.mjs +49 -28
  57. package/src/tools/system.mjs +163 -118
  58. package/src/tools/web.mjs +6 -6
  59. package/src/tui/agent-turn.mjs +82 -26
  60. package/src/tui/ansi.mjs +4 -2
  61. package/src/tui/clipboard.mjs +2 -2
  62. package/src/tui/cmd-auto.mjs +1 -1
  63. package/src/tui/cmd-clear.mjs +1 -1
  64. package/src/tui/cmd-config.mjs +2 -2
  65. package/src/tui/cmd-exit.mjs +2 -2
  66. package/src/tui/cmd-extract.mjs +1 -1
  67. package/src/tui/cmd-goal.mjs +4 -4
  68. package/src/tui/cmd-help.mjs +1 -1
  69. package/src/tui/cmd-init.mjs +3 -3
  70. package/src/tui/cmd-mcp.mjs +5 -5
  71. package/src/tui/cmd-model.mjs +1 -1
  72. package/src/tui/cmd-new.mjs +1 -1
  73. package/src/tui/cmd-plan.mjs +1 -1
  74. package/src/tui/cmd-reindex.mjs +2 -2
  75. package/src/tui/cmd-restore.mjs +1 -1
  76. package/src/tui/cmd-session.mjs +1 -1
  77. package/src/tui/cmd-skills.mjs +1 -1
  78. package/src/tui/cmd-think.mjs +2 -2
  79. package/src/tui/config-helpers.mjs +6 -6
  80. package/src/tui/distill-cmd.mjs +3 -3
  81. package/src/tui/index.mjs +131 -72
  82. package/src/tui/interaction.mjs +13 -12
  83. package/src/tui/key-handler.mjs +31 -23
  84. package/src/tui/layout.mjs +22 -17
  85. package/src/tui/pickers.mjs +19 -19
  86. package/src/tui/render-frame.mjs +37 -11
  87. package/src/tui/render.mjs +25 -24
  88. package/src/tui/slash-commands.mjs +14 -14
  89. package/src/tui/startup.mjs +15 -14
  90. package/src/tui/wizard.mjs +11 -11
  91. package/src/tui.mjs +2 -2
  92. package/bin/thincoder.js +0 -4
  93. package/src/tools/bash.mjs +0 -144
  94. package/src/tools/glob.mjs +0 -51
  95. package/src/tools/grep.mjs +0 -100
  96. package/src/tools/ls.mjs +0 -36
  97. package/src/tools.mjs +0 -2
  98. package/src/tui-render.mjs +0 -4
package/src/tui/index.mjs CHANGED
@@ -1,19 +1,19 @@
1
1
  /**
2
- * tui.mjs — ANSI 终端 UI
3
- * 零依赖:raw mode 键盘输入、ANSI 转义渲染、自研宽字符换行。
4
- * 布局:header / 对话区 (可滚动)/ todo 面板 (有任务时)/ 输入框 / 状态栏。
2
+ * tui.mjs — bare ANSI terminal UI
3
+ * Zero dependencies: raw mode keyboard input, ANSI escape rendering, custom wide-char wrapping.
4
+ * Layout: header / conversation (scrollable) / todo panel (when tasks exist) / input box / status bar.
5
5
  *
6
- * 大型逻辑块已拆到独立模块:
7
- * agent-turn.mjs — agent 循环 + 回调构建
8
- * key-handler.mjs — 键盘事件分发
9
- * startup.mjs — 启动画面 + 会话恢复 + 后台索引
10
- * interaction.mjs — 权限审批 + 问答
11
- * pickers.mjs — 通用列表选择器 + 模型选择器
12
- * wizard.mjs — 首次配置向导
13
- * slash-commands.mjs — 斜杠命令分发
6
+ * Large logic blocks extracted to independent modules:
7
+ * agent-turn.mjs — agent loop + callback construction
8
+ * key-handler.mjs — keyboard event dispatch
9
+ * startup.mjs — startup screen + session restore + background indexing
10
+ * interaction.mjs — permission approval + Q&A
11
+ * pickers.mjs — generic list picker + model picker
12
+ * wizard.mjs — first-launch config wizard
13
+ * slash-commands.mjs — slash command dispatch
14
14
  * config-helpers.mjs — persistRaw / syncProviderField / maskKey
15
- * clipboard.mjs — 剪贴板图片粘贴
16
- * distill-cmd.mjs — /distill 命令
15
+ * clipboard.mjs — clipboard image paste
16
+ * distill-cmd.mjs — /distill command
17
17
  */
18
18
 
19
19
  import { emitKeypressEvents } from "node:readline"
@@ -36,9 +36,9 @@ import { showStartup, backgroundIndex } from "./startup.mjs"
36
36
  import { createConfigHelpers } from "./config-helpers.mjs"
37
37
 
38
38
  /**
39
- * 启动 TUI,接管终端直到退出。
40
- * agent: createAgent 的返回值
41
- * opts: { projectDir?, team?, author? } —— /distill 写入 project/team 层时用
39
+ * Start the TUI, taking over the terminal until exit.
40
+ * agent: return value of createAgent
41
+ * opts: { projectDir?, team?, author? } used by /distill when writing to project/team layers
42
42
  */
43
43
  export async function startTUI(agent, opts = {}) {
44
44
  if (!process.stdin.isTTY) {
@@ -48,52 +48,106 @@ export async function startTUI(agent, opts = {}) {
48
48
  const distillOpts = opts
49
49
 
50
50
  const state = {
51
- lines: [], // 对话区行:{ text, color }
52
- streaming: "", // current流式缓冲
53
- input: [], // 输入缓冲区 (码点数组)
51
+ lines: [], // conversation lines: { text, color }
52
+ streaming: "", // current streaming buffer
53
+ input: [], // input buffer (codepoint array)
54
54
  cursor: 0,
55
55
  history: [],
56
56
  historyIndex: -1,
57
- scroll: 0, // 从底部向上的滚动行数
57
+ scroll: 0, // scroll lines from bottom upward
58
58
  processing: false,
59
59
  controller: null, // AbortController for current agent run
60
60
  permission: null, // { name, args, resolve }
61
- permissionPreview: [], // 权限审批的内容预览行 (渲染在输入框上方,不分隔)
62
- question: null, // { text, options, resolve } — agent question 工具回调
63
- picker: null, // 模型选择器 { entries, lines, index, scroll, selectedLine }
64
- wizard: null, // 首次Config向导 { step, index, scroll, selectedLine, fields, error, lines }
65
- tasks: agent.tasks ?? [], // task 工具的任务列表 (状态栏显示进度);会话恢复时直接带上,全完成自动收起
66
- tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }, // 累计 token 用量 (状态栏显示)
67
- ctxCache: { len: -1, tokens: 0 }, // 上下文占用估算缓存 (estimateTokens O(n)history 变长才重算)
68
- reasoning: "", // 思考流缓冲 (暗色展示)
69
- completion: null, // Tab 补全状态 { candidates, index }
70
- toolStreams: {}, // 各工具的实时输出 (按工具名隔离,并行工具互不串扰)
71
- subTasks: {}, // agent 面板:{ roleName: { role, text, done } },每 role 一行,完成后标记 done 停留片刻
72
- currentTool: null, // 正在执行的工具名 (状态栏显示)
73
- processingStarted: 0, // 本轮处理开始时间 (状态栏计时)
61
+ permissionPreview: [], // content preview lines for permission approval (rendered above input box, without separation)
62
+ question: null, // { text, options, resolve } — agent question tool callback
63
+ picker: null, // model picker { entries, lines, index, scroll, selectedLine }
64
+ wizard: null, // first-launch config wizard { step, index, scroll, selectedLine, fields, error, lines }
65
+ tasks: agent.tasks ?? [], // task list from task tool (progress shown in status bar); carried over on session restore, auto-collapsed when all done
66
+ tokens: { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }, // cumulative token usage (shown in status bar)
67
+ ctxCache: { len: -1, tokens: 0 }, // context utilization estimate cache (estimateTokens is O(n), only recompute when history grows)
68
+ reasoning: "", // thinking stream buffer (dimmed display)
69
+ completion: null, // Tab completion state { candidates, index }
70
+ toolStreams: {}, // per-tool live output (isolated by tool name, parallel tools don't interleave)
71
+ subTasks: {}, // sub-agent panel: { roleName: { role, text, done } }, one line per role, marked done briefly after completion
72
+ outputPanels: {}, // generic tool output panel: { toolName: { text, done } } — streamed live during execution, collapsed to summary on completion
73
+ currentTool: null, // currently executing tool name (shown in status bar)
74
+ processingStarted: 0, // current turn start time (status bar timer)
74
75
  status: "Ready",
75
- queue: [], // 处理中排队的待执行消息:[{ text }],处理完自动取下一条
76
+ queue: [], // queued messages while processing: [{ text }], auto-dequeued when current turn finishes
76
77
  }
77
78
 
78
- // 恢复的会话如果所有任务completed,自动收起 todo 面板 (对齐运行时行为)
79
+ // On session restore, if all tasks are completed, auto-collapse the todo panel (match runtime behavior)
79
80
  if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
80
81
  state.tasks = []
81
82
  }
82
83
 
83
- // 输入流先过一道滤网:鼠标序列 (滚轮)在这里拦截处理,剥净后才交给 keypress 解析,
84
- // 防止序列残片 ( "64;72;42M")漏进输入框
84
+ // Input stream goes through a filter: mouse sequences (scroll wheel) are intercepted and handled here,
85
+ // stripped clean before passing to keypress parsing, preventing sequence fragments (e.g. "64;72;42M")
86
+ // from leaking into the input box
85
87
  const keyStream = new PassThrough()
86
- let mousePending = "" // chunk 的不完整鼠标序列尾部
88
+ let mousePending = "" // incomplete mouse sequence tail spanning chunks
87
89
  let lastRenderedScroll = 0
88
90
  emitKeypressEvents(keyStream)
89
91
  process.stdin.setRawMode(true)
90
- process.stdout.write(ansi.altBuffer + ansi.hideCursor + ansi.mouseOn)
92
+ process.stdout.write(ansi.altBuffer + ansi.hideCursor + ansi.mouseOn + ansi.bracketedPasteOn)
93
+
94
+ const utf8Decoder = new TextDecoder("utf-8", { fatal: false })
95
+
96
+ let pasteMode = false
97
+ let pasteAccum = ""
91
98
 
92
99
  process.stdin.on("data", (chunk) => {
93
- let text = mousePending + chunk.toString("utf8")
100
+ let text = mousePending + utf8Decoder.decode(chunk, { stream: true })
94
101
  mousePending = ""
95
102
 
96
- // 滚轮:\x1b[<64;…M 上滚,\x1b[<65;…M 下滚 (每次 3 行)
103
+ // Bracketed paste: terminal wraps pasted text in \x1b[200~ ... \x1b[201~
104
+ // Insert pasted content directly into state.input to avoid slow char-by-char keypress render
105
+ if (pasteMode) {
106
+ const endIdx = text.indexOf("\x1b[201~")
107
+ if (endIdx >= 0) {
108
+ pasteAccum += text.slice(0, endIdx)
109
+ pasteMode = false
110
+ const pasted = pasteAccum.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ")
111
+ pasteAccum = ""
112
+ if (pasted) {
113
+ const chars = [...pasted]
114
+ state.input.splice(state.cursor, 0, ...chars)
115
+ state.cursor += chars.length
116
+ render()
117
+ }
118
+ text = text.slice(endIdx + 6)
119
+ } else {
120
+ pasteAccum += text
121
+ return
122
+ }
123
+ }
124
+
125
+ // Check for paste start (may appear mid-chunk alongside other input)
126
+ const pasteStartIdx = text.indexOf("\x1b[200~")
127
+ if (pasteStartIdx >= 0) {
128
+ const before = text.slice(0, pasteStartIdx)
129
+ const after = text.slice(pasteStartIdx + 6)
130
+ const endIdx = after.indexOf("\x1b[201~")
131
+ if (endIdx >= 0) {
132
+ // Paste begin and end in the same chunk: insert pasted content directly
133
+ const pasted = after.slice(0, endIdx).replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ")
134
+ if (pasted) {
135
+ const chars = [...pasted]
136
+ state.input.splice(state.cursor, 0, ...chars)
137
+ state.cursor += chars.length
138
+ render()
139
+ }
140
+ text = before + after.slice(endIdx + 6)
141
+ } else {
142
+ // Paste spans multiple chunks: write prefix, enter paste mode
143
+ if (before) keyStream.write(before)
144
+ pasteMode = true
145
+ pasteAccum = after
146
+ return
147
+ }
148
+ }
149
+
150
+ // Scroll wheel: \x1b[<64;…M = scroll up, \x1b[<65;…M = scroll down (3 lines each)
97
151
  for (const m of text.matchAll(/\x1b\[<(\d+);\d+;\d+([Mm])/g)) {
98
152
  if (Number(m[1]) === 64) {
99
153
  state.scroll += 3
@@ -102,7 +156,7 @@ export async function startTUI(agent, opts = {}) {
102
156
  }
103
157
  }
104
158
 
105
- // 剥掉完整鼠标序列;不完整的尾部留到下一块数据再拼
159
+ // Strip complete mouse sequences; keep incomplete tail for reassembly with next chunk
106
160
  text = text.replace(/\x1b\[<\d+;\d+;\d+[Mm]/g, "")
107
161
  const tail = text.match(/\x1b\[<[\d;]*$/)
108
162
  if (tail) {
@@ -121,38 +175,41 @@ export async function startTUI(agent, opts = {}) {
121
175
  const cleanup = () => {
122
176
  if (cleanedUp) return
123
177
  cleanedUp = true
124
- // 退出前保存会话 (同步写);先归档current到槽位,再落新——不丢
178
+ // Save session before exit (synchronous write); archive current to a slot first, then save new — never lose data
125
179
  try {
126
180
  archiveCurrent(agent.cwd)
127
181
  saveSession(agent, state.lines)
128
182
  } catch {
129
- // 存失败不耽误退出
183
+ // Save failure shouldn't block exit
130
184
  }
131
- // Off MCP stdio 子进程,不留孤儿
185
+ // Kill MCP stdio subprocesses, don't leave orphans
132
186
  try {
133
187
  closeAllMcp(agent)
134
188
  } catch {
135
- // 关不掉就算了,进程马上退出
189
+ // Can't close? fine, process is exiting anyway
136
190
  }
137
191
  process.stdin.setRawMode(false)
138
- process.stdout.write(ansi.mouseOff + ansi.mainBuffer + ansi.showCursor + ansi.reset)
192
+ process.stdout.write(ansi.mouseOff + ansi.bracketedPasteOff + ansi.mainBuffer + ansi.showCursor + ansi.reset)
139
193
  }
140
194
  process.on("exit", cleanup)
141
195
 
142
196
  const pushLine = (text, color) => {
143
197
  state.lines.push({ text, color })
144
- if (state.lines.length > 5000) state.lines.splice(0, 1000) // 防none限增长
198
+ if (state.lines.length > 5000) {
199
+ state.lines.splice(0, 1000)
200
+ state.lines.unshift({ text: `... [earlier messages trimmed — ${state.lines.length} lines remaining]`, color: C.dim })
201
+ }
145
202
  render()
146
203
  }
147
204
 
148
- /** 消息块标签:空行 + 标签行。用户/助手消息之间留出呼吸空间 */
205
+ /** Message block label: blank line + label line. Breathing space between user/assistant messages */
149
206
  const pushLabel = (text, color) => {
150
207
  if (state.lines.length > 0) state.lines.push({ text: "", color: C.dim })
151
208
  state.lines.push({ text, color })
152
209
  render()
153
210
  }
154
211
 
155
- // 每轮对话只打一次助手标签 (首个 token 或首个工具调用时)
212
+ // Only emit the assistant label once per turn (on first token or first tool call)
156
213
  let assistantLabeled = false
157
214
  const ensureAssistantLabel = () => {
158
215
  if (!assistantLabeled) {
@@ -161,9 +218,10 @@ export async function startTUI(agent, opts = {}) {
161
218
  }
162
219
  }
163
220
 
164
- // ---------------------------------------------------------- 渲染
221
+ // ---------------------------------------------------------- Render
165
222
 
166
- // 帧去重 + 流式限流:内容没变的帧不重写 (防闪屏);token 洪流合并到 ~25fps
223
+ // Frame dedup + streaming rate limit: skip re-rendering unchanged frames (prevents flicker);
224
+ // merge token flood to ~25fps
167
225
  let lastFrame = ""
168
226
  let renderTimer = null
169
227
 
@@ -176,8 +234,8 @@ export async function startTUI(agent, opts = {}) {
176
234
  }
177
235
 
178
236
  function render() {
179
- // 副作用:scroll 归位 + ctxCache 更新 + overlay scroll 归位
180
- // (renderFrame 是纯函数,副作用集中在此)
237
+ // Side effects: reset scroll + update ctxCache + clamp overlay scroll
238
+ // (renderFrame is pure, side effects concentrated here)
181
239
  const dims = { cols: process.stdout.columns || 80, rows: process.stdout.rows || 24 }
182
240
  const layout = computeLayout(state, dims)
183
241
  // clamp conversation scroll
@@ -202,9 +260,9 @@ export async function startTUI(agent, opts = {}) {
202
260
  })
203
261
  if (frame !== lastFrame) {
204
262
  lastFrame = frame
205
- process.stdout.write(frame)
263
+ process.stdout.write(ansi.hideCursor + frame)
206
264
  }
207
- // 光标:输入态定位到输入框内;权限确认/菜单态时隐藏
265
+ // Cursor: position inside input box when editing; hide during permission/menu mode
208
266
  if (state.permission || state.question || state.picker || state.wizard?.step === "provider") {
209
267
  process.stdout.write(ansi.hideCursor)
210
268
  } else {
@@ -214,7 +272,7 @@ export async function startTUI(agent, opts = {}) {
214
272
 
215
273
  process.stdout.on("resize", render)
216
274
 
217
- // ---------------------------------------------------------- 提交
275
+ // ---------------------------------------------------------- Submit
218
276
 
219
277
  async function submit() {
220
278
  const text = state.input.join("").trim()
@@ -225,10 +283,11 @@ export async function startTUI(agent, opts = {}) {
225
283
  state.historyIndex = -1
226
284
  state.scroll = 0
227
285
 
228
- // 斜杠Commands:本地处理,不进入 agent
286
+ // Slash commands: handled locally, don't enter agent loop
229
287
  if (text.startsWith("/")) {
230
288
  if (state.processing) {
231
- // 处理中:只读命令(切换/查看/帮助)直接执行,有副作用的(清屏/新建/重建索引/提取)排队
289
+ // While processing: read-only commands (switch/view/help) execute directly;
290
+ // side-effect commands (clear/new/reindex/extract) are queued
232
291
  const cmd0 = text.split(/\s+/)[0]
233
292
  const ALIASES = { "/h": "/help", "/x": "/exit", "/m": "/model", "/p": "/plan", "/t": "/think", "/c": "/clear", "/n": "/new" }
234
293
  const resolved0 = ALIASES[cmd0] ?? cmd0
@@ -245,7 +304,7 @@ export async function startTUI(agent, opts = {}) {
245
304
  return
246
305
  }
247
306
 
248
- // 处理中:入队等待,不立即执行
307
+ // While processing: queue for later, don't execute immediately
249
308
  if (state.processing) {
250
309
  state.queue.push({ text })
251
310
  pushLabel(`❯ You: (queued #${state.queue.length})`, ansi.bold + C.user)
@@ -257,15 +316,15 @@ export async function startTUI(agent, opts = {}) {
257
316
  await turn(text)
258
317
  }
259
318
 
260
- // 交互原语:权限审批 + 问答输入,实现在 interaction.mjs
319
+ // Interaction primitives: permission approval + Q&A input, implemented in interaction.mjs
261
320
  const { askPermission, askQuestion } = createInteraction({
262
321
  agent, state, pushLine, pushLabel, render, summarize,
263
322
  })
264
323
 
265
- // 剪贴板图片粘贴:实现在 clipboard.mjs
324
+ // Clipboard image paste: implemented in clipboard.mjs
266
325
  const pasteClipboardImage = () => pasteClipboardImageImpl({ agent, state, pushLine, render })
267
326
 
268
- // agent 循环:实现在 agent-turn.mjs
327
+ // Agent loop: implemented in agent-turn.mjs
269
328
  const turnCtx = {
270
329
  agent, state, pushLine, pushLabel, render, scheduleRender, ensureAssistantLabel,
271
330
  askPermission, askQuestion, handleSlash: null, summarize,
@@ -274,17 +333,17 @@ export async function startTUI(agent, opts = {}) {
274
333
  }
275
334
  const turn = (text) => runAgentTurn(turnCtx, text)
276
335
 
277
- // ---------------------------------------------------------- 斜杠Commands
336
+ // ---------------------------------------------------------- Slash Commands
278
337
 
279
- // 配置辅助:实现在 config-helpers.mjs
338
+ // Config helpers: implemented in config-helpers.mjs
280
339
  const { persistRaw, syncProviderField, maskKey } = createConfigHelpers(agent)
281
340
 
282
- // 模型选择器 + 通用 picker:实现在 pickers.mjs
341
+ // Model picker + generic picker: implemented in pickers.mjs
283
342
  const { openPicker, closePicker, renderPickerLines, openModelPicker, setProviderKey } = createPickers({
284
343
  agent, state, render, ansi, C, pushLine, pushLabel, persistRaw, askQuestion, maskKey,
285
344
  })
286
345
 
287
- // 初始 Config 向导:实现在 wizard.mjs,通过 ctx 传入闭包依赖
346
+ // First-launch config wizard: implemented in wizard.mjs, closure deps passed via ctx
288
347
  const { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, wizardProviderItems } = createWizard({
289
348
  agent, state, pushLine, pushLabel, render, persistRaw,
290
349
  openModelPicker: () => openModelPicker(),
@@ -293,7 +352,7 @@ export async function startTUI(agent, opts = {}) {
293
352
  // /distill: impl in distill-cmd.mjs, ctx-passed
294
353
  const runDistill = () => runDistillImpl({ agent, state, pushLine, render, askPermission, distillOpts })
295
354
 
296
- // 斜杠命令分发、Tab 补全:实现在 slash-commands.mjs,通过 ctx 传入闭包依赖
355
+ // Slash command dispatch + Tab completion: implemented in slash-commands.mjs, closure deps passed via ctx
297
356
  const { handleSlash, completions, handleTab } = createSlashCommands({
298
357
  agent, state, distillOpts,
299
358
  pushLine, pushLabel, render,
@@ -304,12 +363,12 @@ export async function startTUI(agent, opts = {}) {
304
363
  runDistill,
305
364
  exit: () => { cleanup(); setTimeout(() => process.exit(0), 100) },
306
365
  })
307
- // handleSlash turnCtx 引用(循环依赖:submit → turn → handleSlash),在此回填
366
+ // handleSlash is referenced by turnCtx (circular dep: submit → turn → handleSlash), backfilled here
308
367
  turnCtx.handleSlash = handleSlash
309
368
 
310
- // ---------------------------------------------------------- 键盘 / 鼠标
369
+ // ---------------------------------------------------------- Keyboard / Mouse
311
370
 
312
- // keypress 挂在过滤后的 keyStream 上:鼠标序列已在上游滤网中处理并剥除
371
+ // keypress is attached to filtered keyStream: mouse sequences already intercepted and stripped upstream
313
372
  const onKeypress = createKeyHandler({
314
373
  agent, state, render, closePicker, renderPickerLines,
315
374
  handleSlash, handleTab, submit, pasteClipboardImage,
@@ -318,7 +377,7 @@ export async function startTUI(agent, opts = {}) {
318
377
  })
319
378
  keyStream.on("keypress", onKeypress)
320
379
 
321
- // ---------------------------------------------------------- 启动画面 + 后台索引
380
+ // ---------------------------------------------------------- Startup screen + background indexing
322
381
 
323
382
  showStartup({ agent, state, opts, pushLine, pushLabel, render, startWizard })
324
383
  backgroundIndex({ agent, state, render })
@@ -1,22 +1,22 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
2
 
3
- /** 交互原语:权限审批 + 问答输入。
4
- * index.mjs 抽出,通过 createInteraction(ctx) 接收闭包依赖。
3
+ /** Interaction primitives: permission approval + question input.
4
+ * Extracted from index.mjs, receives closure dependencies via createInteraction(ctx).
5
5
  * ctx: { agent, state, pushLine, pushLabel, render, summarize } */
6
6
  export function createInteraction(ctx) {
7
7
  const { agent, state, pushLine, pushLabel, render, summarize } = ctx
8
8
 
9
- /** 权限请求的关键信息 (按工具定制),返回行数组。name 可能带子 agent 前缀 ("coder/bash"),取基名匹配 */
9
+ /** Key info for permission request (customized by tool), returns array of lines. name may have subagent prefix ("coder/bash"), use base name to match */
10
10
  function formatPermission(name, args) {
11
11
  const cap = (s, n = 1000) => (s.length > n ? `${s.slice(0, n)}…(${s.length} chars total)` : s)
12
12
  const base = name.includes("/") ? name.split("/").pop() : name
13
13
  if (base === "bash") return cap(args.command ?? "").split("\n")
14
14
  if (base === "write") {
15
- // 批准写文件必须看得到要写什么:路径 + 内容预览
15
+ // approving file writes must show what's being written: path + content preview
16
16
  return [`${args.path} (write ${(args.content ?? "").length} chars)`, ...cap(args.content ?? "", 1000).split("\n")]
17
17
  }
18
18
  if (base === "edit") {
19
- // 简易 diff:- 旧内容 / + 新内容
19
+ // simple diff: - old content / + new content
20
20
  return [
21
21
  `${args.path}`,
22
22
  ...cap(args.old_string ?? "", 500).split("\n").map((l) => `- ${l}`),
@@ -25,7 +25,7 @@ export function createInteraction(ctx) {
25
25
  ]
26
26
  }
27
27
  if (base === "apply_patch") {
28
- // 补丁本身就是可读的 diff,直接预览
28
+ // patches are readable diffs themselves, preview directly
29
29
  return cap(args.patch ?? "", 1500).split("\n")
30
30
  }
31
31
  if (base === "delete") return [`${args.path}${args.force ? " (force: also delete tracked files)" : ""}`]
@@ -35,12 +35,12 @@ export function createInteraction(ctx) {
35
35
  }
36
36
 
37
37
  function askPermission(name, args) {
38
- // auto 模式:完全授权,不再询问
38
+ // auto mode: fully authorized, no more prompts
39
39
  if (agent.autoApprove) {
40
40
  pushLine(` [auto] ${name} ${summarize(args)}`, C.warn)
41
41
  return Promise.resolve(true)
42
42
  }
43
- // 预览内容存到 permissionPreview,渲染在输入框上方紧挨"Allow?"提示
43
+ // store preview content in permissionPreview, rendered above input box next to "Allow?" prompt
44
44
  state.permissionPreview = formatPermission(name, args)
45
45
  return new Promise((resolve) => {
46
46
  state.permission = { name, args, resolve }
@@ -50,13 +50,14 @@ export function createInteraction(ctx) {
50
50
  }
51
51
 
52
52
  function askQuestion(text, options = []) {
53
- // 一次只能问一个:question 是只读工具走并行通道,同批第二个直接驳回,
54
- // 否则后到的会覆盖 state.question,先到的 Promise 永远悬挂 (agent 死等)
53
+ // only one question at a time: question is a read-only tool on a parallel channel,
54
+ // a second concurrent one is rejected directly; otherwise the later one overwrites
55
+ // state.question and the first Promise hangs forever (agent deadlock)
55
56
  if (state.question) {
56
57
  return Promise.resolve("(error: another question is pending; ask one at a time and wait for the answer)")
57
58
  }
58
59
  if (!options.length) {
59
- // 自由文本:打开输入态让用户打字,Enter 提交
60
+ // free text: open input mode for user typing, Enter to submit
60
61
  pushLabel(`❯ Question`, ansi.bold + C.tool)
61
62
  for (const line of text.split("\n")) pushLine(` ${line}`, C.text)
62
63
  return new Promise((resolve) => {
@@ -65,7 +66,7 @@ export function createInteraction(ctx) {
65
66
  render()
66
67
  })
67
68
  }
68
- // 选项模式:输入框内显示列表,方向键选,Enter 确认
69
+ // options mode: show list in input box, arrow keys to select, Enter to confirm
69
70
  pushLabel(`❯ Question`, ansi.bold + C.tool)
70
71
  for (const line of text.split("\n")) pushLine(` ${line}`, C.text)
71
72
  return new Promise((resolve) => {
@@ -1,7 +1,7 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
2
 
3
- /** 键盘事件分发:权限确认 / 问答 / picker / wizard / 编辑 / 翻页 / 历史 / 粘贴。
4
- * index.mjs 抽出。
3
+ /** Keyboard event dispatch: permission confirm / question / picker / wizard / edit / scroll / history / paste.
4
+ * Extracted from index.mjs.
5
5
  * ctx: { agent, state, render, renderPickerLines, closePicker,
6
6
  * handleSlash, handleTab, submit, pasteClipboardImage,
7
7
  * wizardChooseProvider, wizardSubmitText, cancelWizard, wizardProviderItems,
@@ -10,7 +10,7 @@ export function createKeyHandler(ctx) {
10
10
  const { agent, state, render, closePicker, renderPickerLines, handleSlash, handleTab, submit, pasteClipboardImage, wizardChooseProvider, wizardSubmitText, cancelWizard, wizardProviderItems, renderWizard, pushLine, cleanup } = ctx
11
11
 
12
12
  return function onKeypress(str, key = {}) {
13
- // 权限确认态:y 批准 / n 拒绝 / a 批准并On AUTO (后续不再询问)
13
+ // permission confirm state: y approve / n deny / a approve + turn ON AUTO (no further prompts)
14
14
  if (state.permission) {
15
15
  const answer = (str || "").toLowerCase()
16
16
  const isContinue = state.permission.name === "continue"
@@ -27,7 +27,7 @@ export function createKeyHandler(ctx) {
27
27
  pushLine(` [auto] AUTO ON: tool calls no longer prompt for approval (/auto to disable)`, C.warn)
28
28
  }
29
29
  const approved = answer === "y" || (answer === "a" && !isContinue)
30
- // 决定落痕:对话区留下批准/拒绝记录 (continue 询问有自己的输出,不重复记)
30
+ // leave trail: record approval/denial in conversation (continue prompt has its own output, don't duplicate)
31
31
  if (!isContinue) {
32
32
  pushLine(` [${approved ? "approved" : "denied"}] ${name}`, approved ? C.dim : C.error)
33
33
  }
@@ -37,11 +37,11 @@ export function createKeyHandler(ctx) {
37
37
  return
38
38
  }
39
39
 
40
- // question 工具回调:自由文本 / 选项选择
40
+ // question tool callback: free text / option selection
41
41
  if (state.question) {
42
42
  const q = state.question
43
43
  if (q.options.length > 0) {
44
- // 选项模式:↑↓ 选择,Enter 确认,Esc 取消
44
+ // options mode: ↑↓ select, Enter confirm, Esc cancel
45
45
  if (key.name === "escape") {
46
46
  q.resolve("(cancelled)")
47
47
  state.question = null
@@ -62,7 +62,7 @@ export function createKeyHandler(ctx) {
62
62
  render()
63
63
  }
64
64
  } else {
65
- // 自由文本:键入答案,Enter 提交,Esc 取消
65
+ // free text: type answer, Enter submit, Esc cancel
66
66
  if (key.name === "escape") {
67
67
  q.resolve("(cancelled)")
68
68
  state.question = null
@@ -97,7 +97,7 @@ export function createKeyHandler(ctx) {
97
97
  setTimeout(() => process.exit(0), 100)
98
98
  }
99
99
 
100
- // 通用列表选择器:↑↓ 移动,Enter 确认,Esc 取消
100
+ // generic list picker: ↑↓ move, Enter confirm, Esc cancel
101
101
  if (state.picker) {
102
102
  const items = state.picker?.entries.filter((e) => e.type === "item") ?? []
103
103
  if (key.name === "escape") {
@@ -111,8 +111,8 @@ export function createKeyHandler(ctx) {
111
111
  } else if (key.name === "return" && items.length) {
112
112
  const selected = items[state.picker.index]
113
113
  const handler = state.picker.onSelect
114
- state.picker = null // 先关 picker,避免 onSelect 内部 render picker 还在
115
- // onSelect async(如删 provider 要写文件),用 catch 兜住错误不被吞
114
+ state.picker = null // close picker first, avoid picker still being present during onSelect render
115
+ // onSelect is async (e.g. removing a provider writes file), catch errors so they aren't swallowed
116
116
  Promise.resolve(handler?.(selected)).catch((err) => {
117
117
  pushLine(`[error] ${err.message}`, C.error)
118
118
  }).finally(() => render())
@@ -120,7 +120,7 @@ export function createKeyHandler(ctx) {
120
120
  return
121
121
  }
122
122
 
123
- // 初始Config向导:菜单步 ↑↓/Enter/Esc;文本步 Enter 提交、Esc 取消,编辑键落到正常输入
123
+ // initial config wizard: menu step ↑↓/Enter/Esc; text step Enter submit, Esc cancel, edit keys fall through to normal input
124
124
  if (state.wizard) {
125
125
  const w = state.wizard
126
126
  if (key.name === "escape") {
@@ -144,11 +144,11 @@ export function createKeyHandler(ctx) {
144
144
  wizardSubmitText()
145
145
  return
146
146
  }
147
- // 文本步骤屏蔽翻页/历史,其余编辑键放行到下面的普通输入逻辑
147
+ // text steps: block scroll/history, remaining edit keys fall through to normal input logic below
148
148
  if (key.name === "up" || key.name === "down" || key.name === "pageup" || key.name === "pagedown") return
149
149
  }
150
150
 
151
- // 翻页
151
+ // page scroll
152
152
  if (key.name === "pageup") {
153
153
  state.scroll += Math.max(1, (process.stdout.rows || 24) - 8)
154
154
  render()
@@ -161,9 +161,9 @@ export function createKeyHandler(ctx) {
161
161
  }
162
162
 
163
163
  if (state.processing) {
164
- // 处理中允许输入(排队),但屏蔽方向键历史和 Tab 补全
164
+ // during processing, allow input (queued), but block arrow-key history and Tab completion
165
165
  if (key.name === "tab" || key.name === "up" || key.name === "down") return
166
- // Ctrl+D:删除队列中最后一条
166
+ // Ctrl+D: remove last item from queue
167
167
  if (key.ctrl && key.name === "d") {
168
168
  if (state.queue.length > 0) {
169
169
  state.queue.pop()
@@ -171,16 +171,24 @@ export function createKeyHandler(ctx) {
171
171
  }
172
172
  return
173
173
  }
174
- // 其余可打印字符正常进入输入框
174
+ // remaining printable characters go into input box normally
175
175
  }
176
176
 
177
- // Tab:斜杠Commands补全 (循环候选);其余输入忽略 (\t 会顶破输入框,永不直接插入)
177
+ // Tab: slash-command completion (cycle candidates); other input ignored (\t would blow up input box, never inserted directly)
178
178
  if (key.name === "tab") {
179
179
  handleTab()
180
180
  return
181
181
  }
182
182
 
183
- // 输入历史
183
+ // Ctrl+U: clear entire input box
184
+ if ((key.name === "u" && key.ctrl) || str === "\x15") {
185
+ state.input = []
186
+ state.cursor = 0
187
+ render()
188
+ return
189
+ }
190
+
191
+ // input history
184
192
  if (key.name === "up") {
185
193
  if (state.history.length) {
186
194
  state.historyIndex = state.historyIndex === -1 ? state.history.length - 1 : Math.max(0, state.historyIndex - 1)
@@ -205,7 +213,7 @@ export function createKeyHandler(ctx) {
205
213
  return
206
214
  }
207
215
 
208
- // 光标移动
216
+ // cursor movement
209
217
  if (key.name === "left") {
210
218
  state.cursor = Math.max(0, state.cursor - 1)
211
219
  render()
@@ -227,7 +235,7 @@ export function createKeyHandler(ctx) {
227
235
  return
228
236
  }
229
237
 
230
- // 编辑
238
+ // editing
231
239
  if (key.name === "backspace") {
232
240
  if (state.cursor > 0) {
233
241
  state.input.splice(state.cursor - 1, 1)
@@ -248,15 +256,15 @@ export function createKeyHandler(ctx) {
248
256
  return
249
257
  }
250
258
 
251
- // Ctrl+V (Unix) / Alt+V (Windows):粘贴剪贴板图片存临时文件输入框插入 read_image
259
+ // Ctrl+V (Unix) / Alt+V (Windows): paste clipboard image save temp file insert read_image into input box
252
260
  const isPasteImage = (key.name === "v" && (key.ctrl || key.meta)) || (key.name === "v" && key.alt)
253
261
  if (isPasteImage) {
254
262
  pasteClipboardImage(agent).catch((e) => pushLine(`[error] ${e.message}`, C.error))
255
263
  return
256
264
  }
257
265
 
258
- // 可打印字符 / 粘贴 (str 可能一次多个字符);Tab 一律转成两个空格 (\t 显示宽度不定,会顶破输入框)
259
- // \r\n Windows raw mode 下可能漏进来冲乱页面
266
+ // printable characters / paste (str may contain multiple chars at once); Tab always converted to two spaces (\t has variable display width, would blow up input box)
267
+ // \r\n may leak through in Windows raw mode and scramble the display
260
268
  if (str && !key.ctrl && !key.meta) {
261
269
  const chars = [...str.replace(/[\r\n]+/g, "").replace(/\t/g, " ")]
262
270
  state.input.splice(state.cursor, 0, ...chars)