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.
- package/CHANGELOG.md +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +565 -128
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +3 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +11 -32
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/tools/exec-prelude.mjs +0 -84
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/tui/cmd-eng.mjs
CHANGED
|
@@ -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.)
|
package/src/tui/cmd-extract.mjs
CHANGED
|
@@ -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
|
|
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
|
+
}
|