thincoder 0.12.54 → 0.12.59

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 (129) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +1 -1
  3. package/bin/thincoder.mjs +25 -3
  4. package/package.json +3 -7
  5. package/src/acp/bridge.mjs +132 -26
  6. package/src/advisor/messages.mjs +38 -3
  7. package/src/advisor/run.mjs +91 -53
  8. package/src/advisor.mjs +15 -7
  9. package/src/agent/dispatch.mjs +156 -39
  10. package/src/agent/helpers.mjs +46 -4
  11. package/src/agent/setup.mjs +102 -19
  12. package/src/agent/spawn-child.mjs +28 -1
  13. package/src/agent-tools/advisor.mjs +43 -11
  14. package/src/agent-tools/consult.mjs +37 -6
  15. package/src/agent-tools/eng.mjs +4 -1
  16. package/src/agent-tools/goal.mjs +11 -1
  17. package/src/agent-tools/read-history.mjs +160 -0
  18. package/src/agent-tools/settings.mjs +162 -0
  19. package/src/agent-tools/skill.mjs +2 -1
  20. package/src/agent-tools/subagent-actions.mjs +432 -0
  21. package/src/agent-tools/subagent-async.mjs +427 -0
  22. package/src/agent-tools/subagent-scheduler.mjs +319 -0
  23. package/src/agent-tools/subagent.mjs +565 -128
  24. package/src/agent-tools/task.mjs +4 -3
  25. package/src/agent-tools/timer.mjs +9 -4
  26. package/src/agent-tools/verify.mjs +161 -49
  27. package/src/agent-tools.mjs +1 -0
  28. package/src/agent.mjs +182 -81
  29. package/src/auto-think.mjs +14 -0
  30. package/src/cli/make-agent.mjs +27 -1
  31. package/src/cli/memory-command.mjs +28 -7
  32. package/src/cli/permission.mjs +8 -1
  33. package/src/config.mjs +125 -8
  34. package/src/context.mjs +115 -34
  35. package/src/distill.mjs +19 -1
  36. package/src/escape.mjs +82 -27
  37. package/src/log.mjs +195 -0
  38. package/src/mcp/transport-http.mjs +13 -1
  39. package/src/mcp.mjs +52 -7
  40. package/src/memory/code-sync.mjs +1 -1
  41. package/src/memory/core.mjs +204 -10
  42. package/src/memory/docs.mjs +197 -62
  43. package/src/memory.mjs +1 -1
  44. package/src/model-specs.mjs +38 -1
  45. package/src/prompts/advisor-design.md +46 -0
  46. package/src/prompts/advisor-round1.md +49 -2
  47. package/src/prompts/advisor-round2.md +47 -0
  48. package/src/prompts/advisor-round3.md +47 -0
  49. package/src/prompts/coder.md +22 -0
  50. package/src/prompts/consult-base.md +13 -0
  51. package/src/prompts/discipline.md +25 -6
  52. package/src/prompts/eng-coder.md +2 -2
  53. package/src/prompts/engineering-sub.md +23 -1
  54. package/src/prompts/engineering.md +157 -50
  55. package/src/prompts/explore.md +1 -2
  56. package/src/prompts/main.md +11 -5
  57. package/src/prompts/methodology-template.md +14 -0
  58. package/src/prompts/system.md +5 -2
  59. package/src/provider/anthropic.mjs +7 -5
  60. package/src/provider/core.mjs +104 -28
  61. package/src/provider/google.mjs +57 -24
  62. package/src/provider/normalize.mjs +1 -1
  63. package/src/provider/rate.mjs +0 -2
  64. package/src/provider/responses.mjs +8 -13
  65. package/src/provider/sse.mjs +20 -0
  66. package/src/session.mjs +15 -0
  67. package/src/tools/apply_patch.md +5 -1
  68. package/src/tools/bash.md +3 -3
  69. package/src/tools/delete.md +1 -0
  70. package/src/tools/edit-batch.mjs +92 -0
  71. package/src/tools/edit-diff.mjs +265 -0
  72. package/src/tools/edit.md +11 -6
  73. package/src/tools/execute.md +8 -8
  74. package/src/tools/execute.mjs +31 -35
  75. package/src/tools/file.mjs +26 -114
  76. package/src/tools/file_ops.md +3 -2
  77. package/src/tools/get_current_time.md +3 -1
  78. package/src/tools/git.md +1 -1
  79. package/src/tools/git.mjs +8 -16
  80. package/src/tools/hashline_edit.md +2 -0
  81. package/src/tools/index.mjs +3 -2
  82. package/src/tools/insert_after.md +2 -1
  83. package/src/tools/lint.md +3 -1
  84. package/src/tools/linter.mjs +9 -37
  85. package/src/tools/lsp.md +4 -1
  86. package/src/tools/patch.mjs +84 -13
  87. package/src/tools/pdf-parse-text.mjs +497 -0
  88. package/src/tools/pdf-parse-xref.mjs +499 -0
  89. package/src/tools/pdf.mjs +155 -0
  90. package/src/tools/question.md +2 -1
  91. package/src/tools/read.md +1 -0
  92. package/src/tools/read_pdf.md +21 -0
  93. package/src/tools/repomap.mjs +1 -1
  94. package/src/tools/shared.mjs +11 -32
  95. package/src/tools/system.mjs +6 -21
  96. package/src/tools/tree.md +2 -1
  97. package/src/tools/web.mjs +5 -3
  98. package/src/tools/websearch.md +2 -1
  99. package/src/tools/write.md +2 -0
  100. package/src/traces/trace-store.mjs +224 -0
  101. package/src/tui/agent-turn.mjs +387 -24
  102. package/src/tui/clipboard.mjs +17 -6
  103. package/src/tui/cmd-config.mjs +29 -9
  104. package/src/tui/cmd-eng.mjs +1 -0
  105. package/src/tui/cmd-extract.mjs +1 -1
  106. package/src/tui/cmd-mcp-form.mjs +197 -0
  107. package/src/tui/cmd-mcp.mjs +264 -114
  108. package/src/tui/cmd-think.mjs +1 -1
  109. package/src/tui/index.mjs +49 -95
  110. package/src/tui/interaction.mjs +41 -3
  111. package/src/tui/key-handler.mjs +105 -143
  112. package/src/tui/key-modes.mjs +215 -0
  113. package/src/tui/layout.mjs +22 -1
  114. package/src/tui/mouse.mjs +41 -1
  115. package/src/tui/pickers.mjs +73 -7
  116. package/src/tui/render-conversation.mjs +13 -161
  117. package/src/tui/render-frame.mjs +45 -20
  118. package/src/tui/render-loop.mjs +4 -1
  119. package/src/tui/render-segments.mjs +165 -0
  120. package/src/tui/render.mjs +4 -4
  121. package/src/tui/startup.mjs +40 -2
  122. package/src/tui/subagent-blocks.mjs +404 -111
  123. package/src/tui/subagent-panel.mjs +88 -13
  124. package/src/tui/tool-args.mjs +10 -2
  125. package/src/tui/tool-events.mjs +172 -95
  126. package/src/tui/update-notice.mjs +72 -0
  127. package/src/tui/wizard.mjs +36 -6
  128. package/src/agent-tools/escalate.mjs +0 -179
  129. package/src/tools/exec-prelude.mjs +0 -84
@@ -17,8 +17,9 @@ export function embeddingPatch(raw, embKey, defaults) {
17
17
  export async function handleConfigCommand(ctx, args = []) {
18
18
  const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, maskKey, pickModelForSlot } = ctx
19
19
  const { configPath, DEFAULTS } = await import("../config.mjs")
20
- const ac = agent.config?.agent ?? {}
21
- const ec = agent.config?.embedding ?? {}
20
+ let ac = agent.config?.agent ?? {}
21
+ let ec = agent.config?.embedding ?? {}
22
+ let tc = agent.config?.traces ?? {}
22
23
 
23
24
  // agent.config.proxy 已被 loadConfig 归一化为 { uri, web, model } | undefined
24
25
  function proxySummary() {
@@ -232,13 +233,19 @@ export async function handleConfigCommand(ctx, args = []) {
232
233
  let running = true
233
234
  let mainIdx = 0 // 记住上次选中位置,改完一项回主菜单时恢复
234
235
  while (running) {
236
+ // 每轮刷新——保存分支经 reloadConfig 替换了 agent.config 对象——回菜单显示新值
237
+ ac = agent.config?.agent ?? {}
238
+ ec = agent.config?.embedding ?? {}
239
+ tc = agent.config?.traces ?? {}
235
240
  const consultCount = (ac.consultModels ?? []).length
236
241
  const mainEntries = [
237
- { type: "header", text: `proxy=${proxySummary()} | maxTurns=${ac.maxTurns ?? 200} | compactThreshold=${ac.compactThreshold ?? 100000} | verifyGuard=${ac.verifyGuard === true ? "on" : "off"} | consult=${consultCount} model(s) | embedding=${agent.memory?.embedder ? "on" : "off"}` },
242
+ { type: "header", text: `proxy=${proxySummary()} | maxTurns=${ac.maxTurns ?? 200} | compactThreshold=${ac.compactThreshold ?? 100000} | verifyGuard=${ac.verifyGuard === true ? "on" : "off"} | consult=${consultCount} model(s) | embedding=${agent.memory?.embedder ? "on" : "off"} | traces=${tc.enabled === false ? "off" : "on"}` },
238
243
  { type: "item", text: `agent.maxTurns = ${ac.maxTurns ?? 200}`, action: "agent.maxTurns" },
239
244
  { type: "item", text: `agent.subagentTurns = ${ac.subagentTurns ?? 100}`, action: "agent.subagentTurns" },
240
245
  { type: "item", text: `agent.compactThreshold = ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, action: "agent.compactThreshold" },
241
246
  { type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
247
+ { type: "item", text: `traces.enabled = ${tc.enabled === false ? "off" : "on"}(轨迹存档——发布默认关——隐私)`, action: "traces.enabled" },
248
+ { type: "item", text: `traces.retentionHours = ${tc.retentionHours ?? 24} h(超期文件启动时清理)`, action: "traces.retentionHours" },
242
249
  { type: "item", text: `agent.consultModels = ${consultCount} model(s)${consultCount ? ` (${(ac.consultModels ?? []).map((m) => m.provider + ":" + m.model).join(", ")})` : ""}`, action: "consult" },
243
250
  { type: "item", text: `agent.consultTurns = ${ac.consultTurns ?? 40}`, action: "agent.consultTurns" },
244
251
  { type: "item", text: `agent.consultTimeoutMs = ${Math.round((ac.consultTimeoutMs ?? 600000) / 60000)} min`, action: "agent.consultTimeoutMs" },
@@ -259,14 +266,15 @@ export async function handleConfigCommand(ctx, args = []) {
259
266
  pushLine(`agent.subagentTurns: ${ac.subagentTurns ?? 100}`, C.dim)
260
267
  pushLine(`agent.compactThreshold: ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, C.dim)
261
268
  pushLine(`agent.verifyGuard: ${ac.verifyGuard === true ? "on" : "off"}`, C.dim)
269
+ pushLine(`traces.enabled: ${tc.enabled === false ? "off" : "on"}(默认 off——发布隐私——本地分析可开)`, C.dim)
270
+ pushLine(`traces.retentionHours: ${tc.retentionHours ?? 24}(超期文件启动清理——D-TR10)`, C.dim)
262
271
  pushLine(`agent.consultModels: ${(ac.consultModels ?? []).map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ") || "(none)"}`, C.dim)
263
272
  pushLine(`agent.consultTurns: ${ac.consultTurns ?? 40}`, C.dim)
264
273
  pushLine(`agent.consultTimeoutMs: ${Math.round((ac.consultTimeoutMs ?? 600000) / 60000)} min`, C.dim)
265
274
  pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${ec.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
266
275
  pushLine(`proxy: ${proxySummary()}`, C.dim)
267
276
  pushLine(`Config file: ${configPath}`, C.dim)
268
- running = false
269
- continue
277
+ continue // 回主菜单(Esc 退出)
270
278
  }
271
279
 
272
280
  if (choice.action === "proxy") {
@@ -280,7 +288,7 @@ export async function handleConfigCommand(ctx, args = []) {
280
288
  }
281
289
 
282
290
  if (choice.action === "embedkey") {
283
- if (await setEmbedKey()) running = false
291
+ await setEmbedKey() // 保存成功/取消都回主菜单(Esc 退出)
284
292
  continue
285
293
  }
286
294
 
@@ -293,9 +301,20 @@ export async function handleConfigCommand(ctx, args = []) {
293
301
  })
294
302
  pushLabel("❯ Config", ansi.bold + C.tool)
295
303
  pushLine(`agent.verifyGuard = ${newVal ? "on" : "off"}`, C.tool)
296
- running = false
297
304
  } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
298
- continue
305
+ continue // 回主菜单(Esc 退出)
306
+ }
307
+
308
+ if (choice.action === "traces.enabled") {
309
+ const newVal = tc.enabled === false // 显式写布尔(默认 off——DEFAULTS 合并后必有值)
310
+ try {
311
+ await saveProxy((raw) => {
312
+ raw.traces = { ...(raw.traces ?? {}), enabled: newVal }
313
+ })
314
+ pushLabel("❯ Config", ansi.bold + C.tool)
315
+ pushLine(`traces.enabled = ${newVal ? "on" : "off"}${newVal ? "(轨迹落盘 ~/.thincoder/traces/——保留 " + (tc.retentionHours ?? 24) + "h)" : ""}`, C.tool)
316
+ } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
317
+ continue // 回主菜单(Esc 退出)
299
318
  }
300
319
 
301
320
  // Embedding model is fixed (BAAI/bge-m3, SiliconFlow) — no picker; it's over-engineering
@@ -308,6 +327,7 @@ export async function handleConfigCommand(ctx, args = []) {
308
327
  : label === "agent.subagentTurns" ? (ac.subagentTurns ?? 100)
309
328
  : label === "agent.compactThreshold" ? (ac.compactThreshold ?? 100000)
310
329
  : label === "agent.consultTurns" ? (ac.consultTurns ?? 40)
330
+ : label === "traces.retentionHours" ? (tc.retentionHours ?? 24)
311
331
  : isTimeout ? Math.round((ac.consultTimeoutMs ?? 600000) / 60000)
312
332
  : ""
313
333
  const val = await askQuestion(`${label} (current: ${current}${isTimeout ? " min" : ""}):`)
@@ -325,7 +345,7 @@ export async function handleConfigCommand(ctx, args = []) {
325
345
  pushLabel("❯ Config", ansi.bold + C.tool)
326
346
  pushLine(`${label} = ${isTimeout ? `${val} min (${stored} ms)` : val}`, C.tool)
327
347
  pushLine("(restart to apply)", C.dim)
328
- running = false
329
348
  } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
349
+ // 回主菜单(Esc 退出)——数值输入 Enter 空值也已 continue 于此
330
350
  }
331
351
  }
@@ -49,6 +49,7 @@ export async function handleEngCommand(ctx) {
49
49
  agent.config.agent.engineering = !agent.config.agent.engineering
50
50
  if (!agent.config.agent.engineering) {
51
51
  agent._engDesignToken = null // invalidate stale token
52
+ agent._engDesignTokens = new Map() // multi-design slots die with the mode (2026-09-01 fix #2)
52
53
  // OFF must reach the model too (2026-08-25): /auto pushes a reminder on toggle — the
53
54
  // mode flip is invisible to the agent otherwise. (ON needs none here: the injector
54
55
  // in agent.mjs already announces ON transitions on the next turn.)
@@ -7,7 +7,7 @@ export async function handleExtractCommand(ctx) {
7
7
  pushLine("[extract] Analyzing session...", C.dim)
8
8
  const count = await runDistill()
9
9
  const msg = count > 0
10
- ? `Knowledge extracted: ${count} candidate(s) saved to memory (agent will recall them via memory_search)`
10
+ ? `Knowledge extracted: ${count} candidate(s) saved to memory (agent will recall them via the memory tool)`
11
11
  : "No new knowledge found in this session."
12
12
  pushLine(msg, count > 0 ? C.tool : C.dim)
13
13
  }
@@ -0,0 +1,197 @@
1
+ import { C } from "./ansi.mjs"
2
+
3
+ /** MCP.md §5 v2(D-1):edit/add 统一字段 picker 表单机制。
4
+ * fieldPicker 循环:picker 列字段行(label + 当前值打码)+ `✓ Save & test` 末行;
5
+ * 选中字段 → askQuestion 只输入该字段新值——空=不变、`-`=删除可选字段、`k=`=删除
6
+ * header/env 项、required 字段拒绝 `-`(不许删空)→ 回 picker(已改值保留——T18b
7
+ * 中间 Esc 回 picker 不丢);选 `✓ Save & test` → 必填校验(add 含 name 重复检查)
8
+ * → 调用方走 F2 预览+探活确认环;探活失败回同一 picker(AC2——独立 retry 路径废除)。
9
+ * 本文件为 v1 cmd-mcp.mjs 的 mergeKeyValuePairs/maskToken 迁移落点(逗号分隔 kv 解析
10
+ * 并入 mergeKeyValuePairs——v2 的 headers/env 输入统一按"键值对合并/删除"语义处理,
11
+ * 原 parseHeaders 整段替换语义不再需要);拆分前置——cmd-mcp.mjs 499 行压 500 硬限,评审 #1)。 */
12
+
13
+ /** F3 评审 #3 清除语义(T12):`k=`(空 value)= 从 merged 中删除该项;`k=v` = 设置。
14
+ * 返回更新后的对象(无项时 null——调用方据此 delete 字段)。 */
15
+ function mergeKeyValuePairs(merged, input) {
16
+ for (const pair of String(input).split(",")) {
17
+ const eq = pair.indexOf("=")
18
+ if (eq > 0) {
19
+ const key = pair.slice(0, eq).trim()
20
+ const value = pair.slice(eq + 1).trim().replace(/^["']|["']$/g, "")
21
+ if (!key) continue
22
+ if (value) merged[key] = value
23
+ else delete merged[key]
24
+ }
25
+ }
26
+ return Object.keys(merged).length > 0 ? merged : null
27
+ }
28
+
29
+ /** F2(评审 #6):预览 token 遮蔽——len > 12 显示前 4 字符 + "…",否则全遮。 */
30
+ export function maskToken(token) {
31
+ const t = String(token ?? "")
32
+ if (!t) return ""
33
+ return t.length > 12 ? `${t.slice(0, 4)}…` : "•".repeat(t.length)
34
+ }
35
+
36
+ /** edit 工作副本——headers/env/args 嵌套独立;fieldPicker 原地改副本,取消/失败不
37
+ * 污染原配置(零副作用)。 */
38
+ export function cloneEntry(srv) {
39
+ return {
40
+ ...srv,
41
+ headers: srv.headers ? { ...srv.headers } : undefined,
42
+ env: srv.env ? { ...srv.env } : undefined,
43
+ args: srv.args ? [...srv.args] : undefined,
44
+ }
45
+ }
46
+
47
+ const FIELD_LABELS = {
48
+ name: "Name",
49
+ url: "HTTP URL",
50
+ wsUrl: "WebSocket URL",
51
+ token: "Token",
52
+ headers: "Headers",
53
+ command: "Command",
54
+ args: "Args",
55
+ env: "Env",
56
+ }
57
+
58
+ /** F3/F3b:字段行顺序——add 含 name(可编辑、`(required)` 标注);edit 无 name 行
59
+ * (name 不可改)。HTTP: url/token/headers;WS: wsUrl/token/headers;stdio:
60
+ * command/args/env。 */
61
+ function fieldsFor(transport, mode) {
62
+ const base = transport === "ws" ? ["wsUrl", "token", "headers"]
63
+ : transport === "stdio" ? ["command", "args", "env"]
64
+ : ["url", "token", "headers"]
65
+ return mode === "add" ? ["name", ...base] : base
66
+ }
67
+
68
+ /** 必填字段(F3b 校验口径):name + transport 的端点/命令字段。 */
69
+ function requiredFieldsFor(transport) {
70
+ return transport === "ws" ? ["name", "wsUrl"]
71
+ : transport === "stdio" ? ["name", "command"]
72
+ : ["name", "url"]
73
+ }
74
+
75
+ /** edit 模式的 transport 由 entry 推导(AI 生成的 entry 同理——url/wsUrl/command 判定)。 */
76
+ function deriveTransport(entry) {
77
+ return entry.wsUrl ? "ws" : entry.url ? "http" : "stdio"
78
+ }
79
+
80
+ /** 单行显示值截断——picker 行宽控制(URL/command 可能很长)。 */
81
+ function shortVal(value, max = 32) {
82
+ const v = String(value ?? "")
83
+ return v.length > max ? `${v.slice(0, max - 1)}…` : v
84
+ }
85
+
86
+ /** F3 字段行显示:label 右侧打码/摘要(`Name (required)`、`Token d90c26bb…`、
87
+ * `Headers 2 items`)。必填空 → (required)(add 初始标注——F3b);可选空 → (none)。 */
88
+ function fieldDisplay(entry, field, { add, required }) {
89
+ const v = entry[field]
90
+ if (field === "headers" || field === "env") return `${Object.keys(v ?? {}).length} items`
91
+ if (field === "args") return (v ?? []).length ? shortVal(v.join(" ")) : "(none)"
92
+ if (field === "token") return v ? maskToken(v) : "(none)"
93
+ // name/url/wsUrl/command:端点与命令非机密——截断显示
94
+ if (v) return shortVal(v)
95
+ return add && required ? "(required)" : "(none)"
96
+ }
97
+
98
+ /** picker 行集:字段行(action `field:<name>`)+ 末行 `✓ Save & test`(action "save")。
99
+ * label 补齐对齐(最小 10 列——F3 示例形态 `Token d90c26bb…`)。 */
100
+ function formEntries(entry, transport, mode) {
101
+ const fields = fieldsFor(transport, mode)
102
+ const required = requiredFieldsFor(transport)
103
+ const pad = Math.max(10, Math.max(...fields.map((f) => FIELD_LABELS[f].length)) + 1)
104
+ return [
105
+ ...fields.map((f) => ({
106
+ type: "item",
107
+ text: `${FIELD_LABELS[f].padEnd(pad)} ${fieldDisplay(entry, f, { add: mode === "add", required: required.includes(f) })}`,
108
+ action: `field:${f}`,
109
+ })),
110
+ { type: "item", text: "✓ Save & test", action: "save" },
111
+ ]
112
+ }
113
+
114
+ /** 字段输入提示 `(current: …)`——token 打码(maskToken)、headers/env 列键值对、
115
+ * args 空格串接;空值 → "none"。 */
116
+ function currentText(entry, field) {
117
+ const v = entry[field]
118
+ if (field === "token") return v ? maskToken(v) : "none"
119
+ if (field === "headers" || field === "env") {
120
+ const pairs = Object.entries(v ?? {}).map(([k, val]) => `${k}=${val}`)
121
+ return pairs.length ? pairs.join(", ") : "none"
122
+ }
123
+ if (field === "args") return (v ?? []).length ? v.join(" ") : "none"
124
+ return String(v ?? "") || "none"
125
+ }
126
+
127
+ const PROMPT_BASES = {
128
+ name: "Server name",
129
+ url: "HTTP URL",
130
+ wsUrl: "WebSocket URL",
131
+ token: "Auth token (Bearer, optional; '-' clears, empty keeps)",
132
+ headers: "Headers (key=value, comma-separated; key= removes; empty keeps; '-' clears all)",
133
+ command: "Command",
134
+ args: "Arguments (space-separated; '-' clears, empty keeps)",
135
+ env: "Environment variables (key=value, comma-separated; key= removes; empty keeps; '-' clears all)",
136
+ }
137
+
138
+ function fieldPrompt(entry, field) {
139
+ return `${PROMPT_BASES[field]} (current: ${currentText(entry, field)}):`
140
+ }
141
+
142
+ /** 字段输入应用(UI 决策 #3):空=不变;`-`=删除可选字段(token/headers/env/args);
143
+ * `k=`=删 header/env 项;required 字段(name/url/wsUrl/command)拒绝 `-`——必填不许删空。
144
+ * 返回错误文案(调用方 pushLine)或 null。 */
145
+ function applyFieldInput(entry, field, input, required) {
146
+ if (required) {
147
+ if (input === "-") return `${FIELD_LABELS[field]} is required — cannot be cleared`
148
+ if (input) entry[field] = input
149
+ return null
150
+ }
151
+ if (field === "token") {
152
+ if (input === "-") delete entry.token
153
+ else if (input) entry.token = input
154
+ } else if (field === "headers" || field === "env") {
155
+ if (input === "-") delete entry[field]
156
+ else if (input) {
157
+ const updated = mergeKeyValuePairs(entry[field] ? { ...entry[field] } : {}, input)
158
+ if (updated) entry[field] = updated
159
+ else delete entry[field]
160
+ }
161
+ } else if (field === "args") {
162
+ if (input === "-") delete entry.args
163
+ else if (input) entry.args = input.split(/\s+/)
164
+ }
165
+ return null
166
+ }
167
+
168
+ /** D-1 表单循环(F3/F3b——一处实现两处复用;F2 字段级重试 = 复用同一 picker)。
169
+ * entry 为工作副本:已改值在循环间保留(Esc/空输入不丢——T18b)。
170
+ * 返回 { action: "save" | "cancel", entry };cancel = picker 层 Esc(放弃整个表单)。 */
171
+ export async function fieldPicker(ctx, { title, mode, entry, transport, existingNames = [] }) {
172
+ const { showPicker, askQuestion, pushLine } = ctx
173
+ const tr = transport ?? deriveTransport(entry)
174
+ const required = requiredFieldsFor(tr)
175
+ for (;;) {
176
+ const sel = await showPicker(title, formEntries(entry, tr, mode))
177
+ if (!sel) return { action: "cancel", entry }
178
+ if (sel.action === "save") {
179
+ // F3b:Save 校验必填非空——未满足提示并留在 picker(不落盘)
180
+ const missing = fieldsFor(tr, mode).filter((f) => required.includes(f) && !String(entry[f] ?? "").trim())
181
+ if (missing.length) {
182
+ pushLine(`[mcp] Missing required: ${missing.map((f) => FIELD_LABELS[f]).join(", ")} — fill before saving`, C.error)
183
+ continue
184
+ }
185
+ if (mode === "add" && entry.name && existingNames.includes(entry.name)) {
186
+ pushLine(`[mcp] "${entry.name}" already exists`, C.error)
187
+ continue
188
+ }
189
+ return { action: "save", entry }
190
+ }
191
+ const field = sel.action.slice("field:".length)
192
+ const raw = ((await askQuestion(fieldPrompt(entry, field))) ?? "").trim()
193
+ const err = applyFieldInput(entry, field, raw, required.includes(field))
194
+ if (err) pushLine(`[mcp] ${err}`, C.error)
195
+ // 循环回 picker——已改值保留(T18b:中间 Esc 回 picker 不丢)
196
+ }
197
+ }