thincoder 0.8.2 → 0.8.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.
Files changed (98) hide show
  1. package/README.md +2 -1
  2. package/bin/thincoder.mjs +25 -25
  3. package/package.json +1 -1
  4. package/src/agent/dispatch.mjs +17 -12
  5. package/src/agent/helpers.mjs +22 -11
  6. package/src/agent/setup.mjs +32 -10
  7. package/src/agent-tools/goal.mjs +12 -11
  8. package/src/agent-tools/plan.mjs +5 -6
  9. package/src/agent-tools/recent-changes.mjs +3 -3
  10. package/src/agent-tools/skill.mjs +6 -6
  11. package/src/agent-tools/subagent.mjs +19 -18
  12. package/src/agent-tools/task.mjs +7 -26
  13. package/src/agent-tools/verify.mjs +21 -19
  14. package/src/agent-tools.mjs +3 -3
  15. package/src/agent.mjs +82 -92
  16. package/src/cli/distill-command.mjs +2 -2
  17. package/src/cli/make-agent.mjs +9 -9
  18. package/src/cli/memory-command.mjs +1 -1
  19. package/src/cli/permission.mjs +3 -3
  20. package/src/cli/setup-wizard.mjs +15 -15
  21. package/src/config.mjs +56 -53
  22. package/src/context.mjs +59 -61
  23. package/src/distill.mjs +35 -35
  24. package/src/embedding.mjs +17 -17
  25. package/src/git/checkpoint.mjs +211 -38
  26. package/src/git/gitmem.mjs +21 -20
  27. package/src/markdown.mjs +13 -13
  28. package/src/mcp/helpers.mjs +6 -1
  29. package/src/mcp/transport-http.mjs +2 -1
  30. package/src/mcp/transport-stdio.mjs +3 -2
  31. package/src/mcp/transport-ws.mjs +3 -2
  32. package/src/mcp.mjs +5 -2
  33. package/src/memory/code-index.mjs +16 -14
  34. package/src/memory/code-sync.mjs +36 -26
  35. package/src/memory/core.mjs +42 -35
  36. package/src/memory/docs.mjs +24 -15
  37. package/src/memory/schema.mjs +28 -27
  38. package/src/memory.mjs +2 -2
  39. package/src/prompts/coder.md +0 -6
  40. package/src/prompts/discipline.md +9 -0
  41. package/src/prompts/main.md +24 -24
  42. package/src/prompts/system.md +21 -16
  43. package/src/provider/core.mjs +11 -6
  44. package/src/provider/index.mjs +2 -2
  45. package/src/provider/rate.mjs +11 -11
  46. package/src/session.mjs +73 -42
  47. package/src/skills.mjs +17 -17
  48. package/src/tools/checkpoint.md +6 -2
  49. package/src/tools/file.mjs +20 -14
  50. package/src/tools/git.mjs +83 -12
  51. package/src/tools/index.mjs +1 -1
  52. package/src/tools/ls.md +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 +55 -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 +5 -3
  61. package/src/tui/clipboard.mjs +2 -2
  62. package/src/tui/cmd-auto.mjs +3 -12
  63. package/src/tui/cmd-clear.mjs +1 -1
  64. package/src/tui/cmd-config.mjs +108 -23
  65. package/src/tui/cmd-exit.mjs +2 -2
  66. package/src/tui/cmd-extract.mjs +11 -3
  67. package/src/tui/cmd-goal.mjs +3 -12
  68. package/src/tui/cmd-help.mjs +2 -2
  69. package/src/tui/cmd-init.mjs +3 -3
  70. package/src/tui/cmd-mcp.mjs +38 -25
  71. package/src/tui/cmd-model.mjs +1 -1
  72. package/src/tui/cmd-new.mjs +33 -13
  73. package/src/tui/cmd-plan.mjs +3 -12
  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 +4 -11
  79. package/src/tui/config-helpers.mjs +6 -6
  80. package/src/tui/distill-cmd.mjs +4 -3
  81. package/src/tui/index.mjs +131 -72
  82. package/src/tui/interaction.mjs +13 -12
  83. package/src/tui/key-handler.mjs +34 -26
  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 +22 -22
  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
@@ -1,7 +1,7 @@
1
1
  import { C } from "./ansi.mjs"
2
2
 
3
- /** Ctrl+V / Alt+V:读取剪贴板图片写入工作目录临时文件输入框插入 read_image 命令。
4
- * index.mjs 抽出。
3
+ /** Ctrl+V / Alt+V: read clipboard image write temp file in working directory insert read_image command into input box.
4
+ * Extracted from index.mjs.
5
5
  * ctx: { agent, state, pushLine, render } */
6
6
  export async function pasteClipboardImage(ctx) {
7
7
  const { agent, state, pushLine, render } = ctx
@@ -1,9 +1,7 @@
1
- import { ansi, C } from "./ansi.mjs"
2
-
3
- /** /auto 命令:切换 auto-approve 模式。
4
- * ctx: { agent, pushLine, pushLabel } */
1
+ /** /auto command: toggle auto-approve mode.
2
+ * ctx: { agent } */
5
3
  export async function handleAutoCommand(ctx) {
6
- const { agent, pushLine, pushLabel } = ctx
4
+ const { agent } = ctx
7
5
  agent.autoApprove = !agent.autoApprove
8
6
  agent._pendingReminders = agent._pendingReminders ?? []
9
7
  if (agent.autoApprove) {
@@ -11,11 +9,4 @@ export async function handleAutoCommand(ctx) {
11
9
  } else {
12
10
  agent._pendingReminders.push("[System reminder: AUTO mode is now OFF. Destructive tool calls now require user approval again. Confirm before writing files, running commands, or spawning subagents.]")
13
11
  }
14
- pushLabel(`❯ Auto`, ansi.bold + (agent.autoApprove ? C.warn : C.tool))
15
- pushLine(
16
- agent.autoApprove
17
- ? `AUTO ON: all tool calls (write/bash/subagent) auto-approved. For long tasks. /auto to disable.`
18
- : `AUTO OFF: destructive tool calls require per-use approval again.`,
19
- agent.autoApprove ? C.warn : C.dim,
20
- )
21
12
  }
@@ -1,4 +1,4 @@
1
- /** /clear 命令:清屏(二次确认防误触)。
1
+ /** /clear command: clear screen (confirm to prevent accidental trigger).
2
2
  * ctx: { state, openPicker, render } */
3
3
  export async function handleClearCommand(ctx) {
4
4
  const { state, openPicker, render } = ctx
@@ -1,30 +1,46 @@
1
1
  import { existsSync, readFileSync } from "node:fs"
2
2
  import { ansi, C } from "./ansi.mjs"
3
3
 
4
- /** /config 命令:设置 embedding key / 高级 path=value 配置。
5
- * slash-commands.mjs 抽出。
4
+ /** /config command: view and set agent/embedding config.
5
+ * Extracted from slash-commands.mjs.
6
6
  * ctx: { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey, ansi, C } */
7
7
  export async function handleConfigCommand(ctx) {
8
8
  const { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey } = ctx
9
9
  const { configPath } = await import("../config.mjs")
10
- const cp = configPath
11
10
  const ac = agent.config?.agent ?? {}
12
- const entries = [
13
- { type: "header", text: "Current config" },
14
- { type: "item", text: "Set embedding key (vector search)", action: "embedkey" },
15
- { type: "item", text: "Advanced (set path value)", action: "set" },
11
+ const ec = agent.config?.embedding ?? {}
12
+
13
+ function cfgSummary() {
14
+ const tn = `${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`
15
+ const vg = ac.verifyGuard === true ? "on" : "off"
16
+ return `agent.maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${tn} | verifyGuard=${vg} | embedding=${agent.memory?.embedder ? "on" : "off"}`
17
+ }
18
+
19
+ const mainEntries = [
20
+ { type: "header", text: `Config: ${cfgSummary()}` },
21
+ { type: "item", text: `agent.maxTurns = ${ac.maxTurns ?? 100}`, action: "agent.maxTurns" },
22
+ { type: "item", text: `agent.subagentTurns = ${ac.subagentTurns ?? 100}`, action: "agent.subagentTurns" },
23
+ { type: "item", text: `agent.compactThreshold = ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, action: "agent.compactThreshold" },
24
+ { type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
25
+ { type: "item", text: "Set embedding API key", action: "embedkey" },
26
+ { type: "item", text: `embedding.model = ${ec.model ?? "BAAI/bge-m3"}`, action: "embedding.model" },
27
+ { type: "item", text: "View full config", action: "view" },
16
28
  ]
29
+
17
30
  openPicker({
18
31
  title: "Config",
19
- entries,
32
+ entries: mainEntries,
20
33
  onSelect: async (e) => {
21
34
  if (e.action === "view") {
35
+ const cp = configPath
22
36
  pushLabel(`❯ Config`, ansi.bold + C.tool)
23
37
  pushLine(`Active: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
24
38
  pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
25
- const tn = `${ac.compactThreshold ?? 100000}${ac.compactThresholdAuto ? " (auto)" : ""}`
26
- pushLine(`agent: maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${tn}`, C.dim)
27
- pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${agent.config?.embedding?.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
39
+ pushLine(`agent.maxTurns: ${ac.maxTurns ?? 100}`, C.dim)
40
+ pushLine(`agent.subagentTurns: ${ac.subagentTurns ?? 100}`, C.dim)
41
+ pushLine(`agent.compactThreshold: ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, C.dim)
42
+ pushLine(`agent.verifyGuard: ${ac.verifyGuard === true ? "on" : "off"}`, C.dim)
43
+ pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${ec.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
28
44
  pushLine(`Config file: ${cp}`, C.dim)
29
45
  return
30
46
  }
@@ -42,30 +58,99 @@ export async function handleConfigCommand(ctx) {
42
58
  pushLine(`Embedding key saved, vector search enabled`, C.tool)
43
59
  return
44
60
  }
45
- if (e.action === "set") {
46
- const settext = await askQuestion("Enter: <path> <value> (e.g. agent.maxTurns 80, supports a.b nesting):")
47
- if (!settext) return
48
- const parts = settext.split(/\s+/)
49
- const [path, value] = [parts[0], parts.slice(1).join(" ")]
50
- if (!path || !value) { pushLine("Usage: <path> <value> e.g. agent.maxTurns 80", C.error); return }
61
+ // Boolean toggle: agent.verifyGuard
62
+ if (e.action === "agent.verifyGuard") {
63
+ const newVal = ac.verifyGuard !== true // toggle: undefined/false → true, true → false
51
64
  try {
52
- const { configPath, loadConfig, saveConfig } = await import("../config.mjs")
53
65
  const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
54
- const keys = path.split(".")
55
- let obj = raw
56
- for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
57
- obj[keys[keys.length - 1]] = isNaN(value) ? value : Number(value)
66
+ raw.agent ??= {}
67
+ raw.agent.verifyGuard = newVal
68
+ const { saveConfig, loadConfig } = await import("../config.mjs")
58
69
  saveConfig(raw)
59
70
  const cfg = loadConfig()
60
71
  agent.provider = cfg.provider
61
72
  agent.providers = cfg.providersList
62
73
  agent.activeProvider = cfg.activeProvider
63
74
  agent.config = cfg
75
+ agent.config.agent ??= {}
64
76
  pushLabel(`❯ Config`, ansi.bold + C.tool)
65
- pushLine(`Saved: ${path} = ${value}`, C.tool)
77
+ pushLine(`agent.verifyGuard = ${newVal ? "on" : "off"}`, C.tool)
66
78
  } catch (error) {
67
79
  pushLine(`Save failed: ${error.message}`, C.error)
68
80
  }
81
+ return
82
+ }
83
+ // embedding.model picker
84
+ if (e.action === "embedding.model") {
85
+ const models = [
86
+ { label: "BAAI/bge-m3 (multilingual, 1024d)", value: "BAAI/bge-m3" },
87
+ { label: "BAAI/bge-large-zh-v1.5 (Chinese, 1024d)", value: "BAAI/bge-large-zh-v1.5" },
88
+ { label: "BAAI/bge-large-en-v1.5 (English, 1024d)", value: "BAAI/bge-large-en-v1.5" },
89
+ { label: "text-embedding-3-small (OpenAI, 1536d)", value: "text-embedding-3-small" },
90
+ { label: "text-embedding-3-large (OpenAI, 3072d)", value: "text-embedding-3-large" },
91
+ ]
92
+ const currentVal = ec.model ?? "BAAI/bge-m3"
93
+ openPicker({
94
+ title: `Embedding model (current: ${currentVal})`,
95
+ entries: [
96
+ { type: "header", text: `Current: ${currentVal}` },
97
+ ...models.map((m) => ({ type: "item", text: m.label, action: m.value })),
98
+ ],
99
+ onSelect: async (sel) => {
100
+ try {
101
+ const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
102
+ raw.embedding ??= {}
103
+ raw.embedding.model = sel.action
104
+ const { saveConfig, loadConfig } = await import("../config.mjs")
105
+ saveConfig(raw)
106
+ const cfg = loadConfig()
107
+ agent.provider = cfg.provider
108
+ agent.providers = cfg.providersList
109
+ agent.activeProvider = cfg.activeProvider
110
+ agent.config = cfg
111
+ agent.config.agent ??= {}
112
+ pushLabel(`❯ Config`, ansi.bold + C.tool)
113
+ pushLine(`embedding.model = ${sel.action}`, C.tool)
114
+ } catch (error) {
115
+ pushLine(`Save failed: ${error.message}`, C.error)
116
+ }
117
+ },
118
+ })
119
+ return
120
+ }
121
+ // Numeric config items: ask for value, parse as number
122
+ const isNumeric = e.action.startsWith("agent.")
123
+ const label = e.action
124
+ const current = e.action === "agent.maxTurns" ? (ac.maxTurns ?? 100)
125
+ : e.action === "agent.subagentTurns" ? (ac.subagentTurns ?? 100)
126
+ : e.action === "agent.compactThreshold" ? (ac.compactThreshold ?? 100000)
127
+ : ""
128
+ const prompt = `${label} (current: ${current}):`
129
+ const val = await askQuestion(prompt)
130
+ if (!val) return
131
+ try {
132
+ const { configPath, loadConfig, saveConfig } = await import("../config.mjs")
133
+ const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
134
+ if (isNumeric) {
135
+ const keys = label.split(".")
136
+ let obj = raw
137
+ for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
138
+ const num = Number(val)
139
+ if (isNaN(num)) { pushLine("Value must be a number", C.error); return }
140
+ obj[keys[keys.length - 1]] = num
141
+ }
142
+ saveConfig(raw)
143
+ const cfg = loadConfig()
144
+ agent.provider = cfg.provider
145
+ agent.providers = cfg.providersList
146
+ agent.activeProvider = cfg.activeProvider
147
+ agent.config = cfg
148
+ agent.config.agent ??= {}
149
+ pushLabel(`❯ Config`, ansi.bold + C.tool)
150
+ pushLine(`${label} = ${val}`, C.tool)
151
+ pushLine("(restart to apply to existing agent state)", C.dim)
152
+ } catch (error) {
153
+ pushLine(`Save failed: ${error.message}`, C.error)
69
154
  }
70
155
  },
71
156
  })
@@ -1,5 +1,5 @@
1
- /** /exit 命令:退出 TUI
2
- * ctx: { exit } — exit index.mjs 注入的 cleanup + process.exit 回调 */
1
+ /** /exit command: exit TUI.
2
+ * ctx: { exit } — exit is the cleanup + process.exit callback injected by index.mjs */
3
3
  export async function handleExitCommand(ctx) {
4
4
  ctx.exit()
5
5
  }
@@ -1,5 +1,13 @@
1
- /** /extract 命令:从当前会话提取知识。
2
- * ctx: { runDistill } */
1
+ import { C } from "./ansi.mjs"
2
+
3
+ /** /extract command: extract knowledge from current session.
4
+ * ctx: { runDistill, state, pushLine } */
3
5
  export async function handleExtractCommand(ctx) {
4
- await ctx.runDistill()
6
+ const { runDistill, state, pushLine } = ctx
7
+ pushLine("[extract] Analyzing session...", C.dim)
8
+ const count = await runDistill()
9
+ const msg = count > 0
10
+ ? `Knowledge extracted: ${count} candidate(s) saved to memory (use /skills to list, agent will recall via memory_search)`
11
+ : "No new knowledge found in this session."
12
+ pushLine(msg, count > 0 ? C.tool : C.dim)
5
13
  }
@@ -1,7 +1,5 @@
1
- import { ansi, C } from "./ansi.mjs"
2
-
3
- /** /goal 命令:设置/查看/取消长期目标。
4
- * 从 slash-commands.mjs 抽出。
1
+ /** /goal command: set/view/cancel long-term goal.
2
+ * Extracted from slash-commands.mjs.
5
3
  * ctx: { agent, pushLine, pushLabel, openPicker, askQuestion } */
6
4
  export async function handleGoalCommand(ctx) {
7
5
  const { agent, pushLine, pushLabel, openPicker, askQuestion } = ctx
@@ -19,7 +17,6 @@ export async function handleGoalCommand(ctx) {
19
17
  onSelect: async (e) => {
20
18
  if (e.action === "view") {
21
19
  const statusText = { active: "active", complete: "completed", blocked: "blocked" }[agent.goal.status] ?? agent.goal.status
22
- pushLabel(`❯ Goal`, ansi.bold + C.warn)
23
20
  pushLine(`Goal: ${agent.goal.objective}`, C.tool)
24
21
  if (agent.goal.criteria) pushLine(` Criteria: ${agent.goal.criteria}`, C.dim)
25
22
  pushLine(` Status: ${statusText} │ Turns used: ${agent.goal.turnsUsed ?? 0} │ Set at: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
@@ -27,21 +24,15 @@ export async function handleGoalCommand(ctx) {
27
24
  }
28
25
  if (e.action === "cancel") {
29
26
  agent.goal = null
30
- pushLabel(`❯ Goal`, ansi.bold + C.dim)
31
- pushLine(`Goal cancelled.`, C.dim)
32
27
  return
33
28
  }
34
- // set — 需要输入目标文本
29
+ // set — requires entering goal text
35
30
  const goalText = await askQuestion("Enter goal description (; separates criteria)")
36
31
  if (!goalText) return
37
32
  const semi = goalText.indexOf(";") >= 0 ? ";" : goalText.indexOf(";") >= 0 ? ";" : null
38
33
  const objective = semi ? goalText.slice(0, semi).trim() : goalText.trim()
39
34
  const criteria = semi ? goalText.slice(semi + 1).trim() : ""
40
35
  agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
41
- pushLabel(`❯ Goal`, ansi.bold + C.warn)
42
- pushLine(`Goal set: ${objective}`, C.tool)
43
- if (criteria) pushLine(` Criteria: ${criteria}`, C.dim)
44
- else pushLine(` ⚠ No criteria — agent will be asked to provide verifiable criteria when using goal set`, C.warn)
45
36
  },
46
37
  })
47
38
  }
@@ -1,11 +1,11 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
2
 
3
- /** /help 命令:列出所有斜杠命令及别名。
3
+ /** /help command: list all slash commands and aliases.
4
4
  * ctx: { pushLine, pushLabel, SLASH_COMMANDS } */
5
5
  export async function handleHelpCommand(ctx) {
6
6
  const { pushLine, pushLabel, SLASH_COMMANDS } = ctx
7
7
  const aliasList = { "/help": "/h", "/exit": "/x", "/model": "/m", "/plan": "/p", "/think": "/t", "/clear": "/c", "/new": "/n" }
8
- const order = ["Agent", "Session", "Tools", "Config"]
8
+ const order = ["Agent", "Session", "Project", "System"]
9
9
  const byGroup = new Map()
10
10
  for (const c of SLASH_COMMANDS) {
11
11
  if (!c.group) continue
@@ -2,8 +2,8 @@ import { existsSync } from "node:fs"
2
2
  import { basename } from "node:path"
3
3
  import { ansi, C } from "./ansi.mjs"
4
4
 
5
- /** /init 命令:探测项目类型,生成 AGENTS.md 骨架。
6
- * slash-commands.mjs 抽出。
5
+ /** /init command: detect project type, generate AGENTS.md skeleton.
6
+ * Extracted from slash-commands.mjs.
7
7
  * ctx: { agent, pushLine, pushLabel } */
8
8
  export async function handleInitCommand(ctx) {
9
9
  const { agent, pushLine, pushLabel } = ctx
@@ -15,7 +15,7 @@ export async function handleInitCommand(ctx) {
15
15
  return
16
16
  }
17
17
 
18
- // 探测项目类型与关键信息
18
+ // Detect project type and key information
19
19
  let name = basename(agent.cwd)
20
20
  let lang = "", cmds = ""
21
21
 
@@ -1,7 +1,7 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
2
 
3
- /** /mcp 命令处理器:查看/添加/删除/重连 MCP server
4
- * slash-commands.mjs 抽出,含 /mcp 专用的 parseHeaders / addAndConnect helper。
3
+ /** /mcp command handler: view/add/remove/reconnect MCP server.
4
+ * Extracted from slash-commands.mjs, includes /mcp-specific parseHeaders / addAndConnect helpers.
5
5
  * ctx: { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, ansi, C } */
6
6
 
7
7
  function parseHeaders(pairs) {
@@ -13,7 +13,7 @@ function parseHeaders(pairs) {
13
13
  return headers
14
14
  }
15
15
 
16
- /** /mcp 共享 helper: 保存 Config + Connecting (persistRaw ctx 获取) */
16
+ /** /mcp shared helper: save config + connect (persistRaw obtained from ctx) */
17
17
  async function addAndConnect(ctx, srv) {
18
18
  const { agent, pushLine, pushLabel, persistRaw } = ctx
19
19
  await persistRaw((raw) => {
@@ -83,10 +83,10 @@ export async function handleMcpCommand(ctx) {
83
83
  title: "Remove MCP Server",
84
84
  entries: removeEntries,
85
85
  onSelect: async (se) => {
86
- // 从配置删除
86
+ // Remove from config
87
87
  agent.config.mcp.servers = servers.filter((s) => s.name !== se.name)
88
88
  await persistRaw((raw) => { raw.mcp.servers = agent.config.mcp.servers })
89
- // 从工具列表删除
89
+ // Remove from tool list
90
90
  const { removeMcpTools } = await import("../mcp.mjs")
91
91
  removeMcpTools(agent, se.name)
92
92
  pushLine(`[mcp] ${se.name} removed`, C.tool)
@@ -120,26 +120,39 @@ export async function handleMcpCommand(ctx) {
120
120
  return
121
121
  }
122
122
  if (e.action === "add") {
123
- const mcpInput = await askQuestion("Enter: <name> <URL|command> [args...]\nURL auto-detect: https://… → HTTP, ws://… WebSocket, other → stdio command")
124
- if (!mcpInput) return
125
- const parts = mcpInput.split(/\s+/)
126
- if (parts.length < 2) { pushLine("Usage: <name> <URL|command> [args...]", C.error); return }
127
- const [name, second, ...extras] = parts
128
- const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
129
- if (existing) { pushLine(`[mcp] "${name}" already exists`, C.error); return }
130
- const isWS = /^wss?:\/\//.test(second)
131
- const isHTTP = /^https?:\/\//.test(second)
132
- let srv
133
- if (isWS) {
134
- const headers = parseHeaders(extras)
135
- srv = { name, wsUrl: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
136
- } else if (isHTTP) {
137
- const headers = parseHeaders(extras)
138
- srv = { name, url: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
139
- } else {
140
- srv = { name, command: second, args: extras.length > 0 ? extras : undefined }
141
- }
142
- await addAndConnect(ctx, srv)
123
+ // Pick transport type first, then ask name + URL/command
124
+ openPicker({
125
+ title: "MCP Transport",
126
+ entries: [
127
+ { type: "header", text: "Select server transport" },
128
+ { type: "item", text: "HTTP (https://…)", action: "http" },
129
+ { type: "item", text: "WebSocket (ws://…)", action: "ws" },
130
+ { type: "item", text: "stdio (local command)", action: "stdio" },
131
+ ],
132
+ onSelect: async (te) => {
133
+ const name = await askQuestion("Server name:")
134
+ if (!name) return
135
+ const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
136
+ if (existing) { pushLine(`[mcp] "${name}" already exists`, C.error); return }
137
+ if (te.action === "stdio") {
138
+ const cmd = await askQuestion("Command (e.g. npx, python):")
139
+ if (!cmd) return
140
+ const argsInput = await askQuestion("Arguments (space-separated, or leave empty):")
141
+ const args = argsInput ? argsInput.split(/\s+/) : undefined
142
+ await addAndConnect(ctx, { name, command: cmd, args })
143
+ } else {
144
+ const urlPrompt = te.action === "ws" ? "WebSocket URL (ws://…):" : "HTTP URL (https://…):"
145
+ const url = await askQuestion(urlPrompt)
146
+ if (!url) return
147
+ const headersInput = await askQuestion("Headers (key=value, space-separated, or leave empty):")
148
+ const headers = headersInput ? parseHeaders(headersInput.split(/\s+/)) : undefined
149
+ const srv = te.action === "ws"
150
+ ? { name, wsUrl: url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
151
+ : { name, url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
152
+ await addAndConnect(ctx, srv)
153
+ }
154
+ },
155
+ })
143
156
  }
144
157
  },
145
158
  })
@@ -1,6 +1,6 @@
1
1
  import { C } from "./ansi.mjs"
2
2
 
3
- /** /model 命令:打开模型选择器。
3
+ /** /model command: open model picker.
4
4
  * ctx: { openModelPicker, pushLine } */
5
5
  export async function handleModelCommand(ctx) {
6
6
  ctx.openModelPicker().catch((e) => ctx.pushLine(`[error] ${e.message}`, C.error))
@@ -1,18 +1,38 @@
1
1
  import { clearSession } from "../session.mjs"
2
2
  import { C } from "./ansi.mjs"
3
3
 
4
- /** /new 命令:新建会话(旧会话归档到槽位)。
5
- * ctx: { agent, state, pushLine } */
4
+ /** /new command: start new session (old session archived to slot).
5
+ * ctx: { agent, state, pushLine, openPicker, render } */
6
6
  export async function handleNewCommand(ctx) {
7
- const { agent, state, pushLine } = ctx
8
- agent.history = []
9
- agent.tasks = []
10
- agent.planMode = false
11
- agent.goal = null
12
- agent._pendingReminders = []
13
- state.tasks = []
14
- state.lines = []
15
- state.streaming = ""
16
- clearSession(agent.cwd)
17
- pushLine("New session started (old session archived to slot; /session to view)", C.dim)
7
+ const { agent, state, pushLine, openPicker, render } = ctx
8
+
9
+ const doNewSession = () => {
10
+ agent.history = []
11
+ agent.tasks = []
12
+ agent.planMode = false
13
+ agent.goal = null
14
+ agent._pendingReminders = []
15
+ state.tasks = []
16
+ state.lines = []
17
+ state.streaming = ""
18
+ clearSession(agent.cwd)
19
+ render()
20
+ pushLine("New session started (old session archived to slot; /session to view)", C.dim)
21
+ }
22
+
23
+ if (agent.history.length > 0) {
24
+ openPicker({
25
+ title: "Start new session?",
26
+ entries: [
27
+ { type: "item", text: "Yes, archive current and start new", action: "yes" },
28
+ { type: "item", text: "Cancel", action: "no" },
29
+ ],
30
+ defaultIndex: 1,
31
+ onSelect: (e) => {
32
+ if (e.action === "yes") doNewSession()
33
+ },
34
+ })
35
+ return
36
+ }
37
+ doNewSession()
18
38
  }
@@ -1,9 +1,7 @@
1
- import { ansi, C } from "./ansi.mjs"
2
-
3
- /** /plan 命令:切换 plan mode(只读探索 → 设计 → 实现)。
4
- * ctx: { agent, pushLine, pushLabel } */
1
+ /** /plan command: toggle plan mode (read-only explore → design → implement).
2
+ * ctx: { agent } */
5
3
  export async function handlePlanCommand(ctx) {
6
- const { agent, pushLine, pushLabel } = ctx
4
+ const { agent } = ctx
7
5
  agent.planMode = !agent.planMode
8
6
  agent._pendingReminders = agent._pendingReminders ?? []
9
7
  if (agent.planMode) {
@@ -11,11 +9,4 @@ export async function handlePlanCommand(ctx) {
11
9
  } else {
12
10
  agent._pendingReminders.push("[System reminder: plan mode is now OFF. You may edit files, run commands, and implement changes.]")
13
11
  }
14
- pushLabel(`❯ Plan`, ansi.bold + (agent.planMode ? C.tool : C.dim))
15
- pushLine(
16
- agent.planMode
17
- ? `Plan mode ON: read-only tools only. Design first, then implement. /plan again to exit.`
18
- : `Plan mode OFF: you may now edit files and run commands.`,
19
- agent.planMode ? C.tool : C.dim,
20
- )
21
12
  }
@@ -1,6 +1,6 @@
1
1
  import { C } from "./ansi.mjs"
2
2
 
3
- /** /reindex 命令:重建记忆索引(files + code_chunks + doc_chunks)。
3
+ /** /reindex command: rebuild memory index (files + code_chunks + doc_chunks).
4
4
  * ctx: { agent, distillOpts, pushLine } */
5
5
  export async function handleReindexCommand(ctx) {
6
6
  const { agent, distillOpts, pushLine } = ctx
@@ -20,7 +20,7 @@ export async function handleReindexCommand(ctx) {
20
20
  total += s.added
21
21
  pushLine(` team: +${s.added} ~${s.updated} -${s.removed}`, C.dim)
22
22
  }
23
- // 重建代码索引和文档索引并行(读写不同表,WAL 支持)
23
+ // Rebuild code index and doc index in parallel (read/write different tables, WAL supports)
24
24
  pushLine(` [code+doc] Rebuilding indexes...`, C.tool)
25
25
  const [cr, dr] = await Promise.all([
26
26
  codeSync(agent.memory, agent.cwd, {
@@ -1,6 +1,6 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
2
 
3
- /** /restore 命令:列出 git checkpoint 并回滚到选中快照。
3
+ /** /restore command: list git checkpoints and roll back to selected snapshot.
4
4
  * ctx: { agent, openPicker, pushLine, pushLabel } */
5
5
  export async function handleRestoreCommand(ctx) {
6
6
  const { agent, openPicker, pushLine, pushLabel } = ctx
@@ -1,7 +1,7 @@
1
1
  import { listSlots, switchToSlot, applySession } from "../session.mjs"
2
2
  import { ansi, C } from "./ansi.mjs"
3
3
 
4
- /** /session 命令:列出/切换归档会话槽位。
4
+ /** /session command: list/switch archived session slots.
5
5
  * ctx: { agent, state, openPicker, pushLine, pushLabel, render } */
6
6
  export async function handleSessionCommand(ctx) {
7
7
  const { agent, state, openPicker, pushLine, pushLabel, render } = ctx
@@ -1,6 +1,6 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
2
 
3
- /** /skills 命令:列出项目技能。
3
+ /** /skills command: list project skills.
4
4
  * ctx: { agent, pushLine, pushLabel } */
5
5
  export async function handleSkillsCommand(ctx) {
6
6
  const { agent, pushLine, pushLabel } = ctx
@@ -1,10 +1,8 @@
1
- import { ansi, C } from "./ansi.mjs"
2
-
3
- /** /think 命令:切换思考模式、设置 reasoning effort。
4
- * 从 slash-commands.mjs 抽出。
5
- * ctx: { agent, pushLine, pushLabel, openPicker, syncProviderField } */
1
+ /** /think command: toggle thinking mode, set reasoning effort.
2
+ * Extracted from slash-commands.mjs.
3
+ * ctx: { agent, openPicker, syncProviderField } */
6
4
  export async function handleThinkCommand(ctx) {
7
- const { agent, pushLine, pushLabel, openPicker, syncProviderField } = ctx
5
+ const { agent, openPicker, syncProviderField } = ctx
8
6
  const cur = agent.provider
9
7
  const thinkingEnabled = cur.thinking?.type === "enabled" || cur.thinking?.type === undefined
10
8
  const { specForModel } = await import("../config.mjs")
@@ -30,8 +28,6 @@ export async function handleThinkCommand(ctx) {
30
28
  if (e.action === "effort") {
31
29
  cur.reasoningEffort = e.level
32
30
  await syncProviderField("reasoningEffort", e.level)
33
- pushLabel(`❯ Think`, ansi.bold + C.tool)
34
- pushLine(`Reasoning effort set to ${e.level}`, C.tool)
35
31
  } else {
36
32
  const enable = e.action === "on"
37
33
  if (isEffortOnly) {
@@ -47,9 +43,6 @@ export async function handleThinkCommand(ctx) {
47
43
  if (!enable) await syncProviderField("reasoningEffort", undefined)
48
44
  else await syncProviderField("reasoningEffort", cur.reasoningEffort)
49
45
  }
50
- pushLabel(`❯ Think`, ansi.bold + C.tool)
51
- pushLine(`Thinking mode ${enable ? "On" : "Off"}`, C.tool)
52
- if (enable) pushLine(`Reasoning effort: ${cur.reasoningEffort}`, C.dim)
53
46
  }
54
47
  },
55
48
  })
@@ -1,10 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs"
2
2
 
3
- /** persistRaw / syncProviderField / maskKey:配置读写辅助。
4
- * index.mjs 抽出,供 slash-commands / wizard / pickers 共用。
5
- * createConfigHelpers(agent) 返回 { persistRaw, syncProviderField, maskKey } */
3
+ /** persistRaw / syncProviderField / maskKey: config read/write helpers.
4
+ * Extracted from index.mjs for shared use by slash-commands, wizard, and pickers.
5
+ * createConfigHelpers(agent) returns { persistRaw, syncProviderField, maskKey } */
6
6
  export function createConfigHelpers(agent) {
7
- /** 读取 config.json → mutate → 落盘 */
7
+ /** Read config.json → mutate → persist to disk */
8
8
  async function persistRaw(mutate) {
9
9
  const { saveConfig, configPath } = await import("../config.mjs")
10
10
  const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
@@ -12,7 +12,7 @@ export function createConfigHelpers(agent) {
12
12
  saveConfig(raw)
13
13
  }
14
14
 
15
- /** 同步当前 provider 的某个字段到 providers 数组并落盘 */
15
+ /** Sync a field of the current provider to the providers array and persist to disk */
16
16
  async function syncProviderField(field, value) {
17
17
  const target = agent.providers.find((p) => p.name === agent.activeProvider)
18
18
  if (!target) return
@@ -23,7 +23,7 @@ export function createConfigHelpers(agent) {
23
23
  })
24
24
  }
25
25
 
26
- /** API key 脱敏显示 */
26
+ /** Mask API key for display */
27
27
  function maskKey(key) {
28
28
  if (!key) return "(none)"
29
29
  if (key.length <= 8) return "***"