thincoder 0.10.0 → 0.11.0

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.
@@ -1,157 +1,241 @@
1
1
  import { existsSync, readFileSync } from "node:fs"
2
2
  import { ansi, C } from "./ansi.mjs"
3
3
 
4
- /** /config command: view and set agent/embedding config.
5
- * Extracted from slash-commands.mjs.
6
- * ctx: { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey, ansi, C } */
7
- export async function handleConfigCommand(ctx) {
8
- const { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, maskKey } = ctx
4
+ /** /config command: view and set agent/embedding/proxy config. */
5
+ export async function handleConfigCommand(ctx, args = []) {
6
+ const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, maskKey } = ctx
9
7
  const { configPath } = await import("../config.mjs")
10
8
  const ac = agent.config?.agent ?? {}
11
9
  const ec = agent.config?.embedding ?? {}
12
10
 
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"}`
11
+ // agent.config.proxy 已被 loadConfig 归一化为 { uri, web, model } | undefined
12
+ function proxySummary() {
13
+ const pc = agent.config?.proxy
14
+ if (!pc) return "not configured"
15
+ return `${pc.uri} web:${pc.web ? "on" : "off"} model:${pc.model ? "on" : "off"}`
17
16
  }
18
17
 
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" },
28
- ]
29
-
30
- openPicker({
31
- title: "Config",
32
- entries: mainEntries,
33
- onSelect: async (e) => {
34
- if (e.action === "view") {
35
- const cp = configPath
36
- pushLabel(`❯ Config`, ansi.bold + C.tool)
37
- pushLine(`Active: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
38
- pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, 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)
44
- pushLine(`Config file: ${cp}`, C.dim)
45
- return
46
- }
47
- if (e.action === "embedkey") {
48
- const embKey = await askQuestion("Enter embedding API key (default: SiliconFlow bge-m3):")
49
- if (!embKey) return
50
- agent.config.embedding ??= {}
51
- agent.config.embedding.apiKey = embKey
52
- await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: embKey } })
53
- if (agent.memory) {
54
- const { createEmbedder } = await import("../embedding.mjs")
55
- agent.memory.embedder = createEmbedder(agent.config.embedding)
18
+ async function setEmbedKey() {
19
+ const embKey = await askQuestion("Enter embedding API key (default: SiliconFlow bge-m3):")
20
+ if (!embKey) return false
21
+ agent.config.embedding ??= {}
22
+ agent.config.embedding.apiKey = embKey
23
+ await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: embKey } })
24
+ if (agent.memory) {
25
+ const { createEmbedder } = await import("../embedding.mjs")
26
+ agent.memory.embedder = createEmbedder(agent.config.embedding)
27
+ }
28
+ pushLabel("❯ Config", ansi.bold + C.tool)
29
+ pushLine("Embedding key saved, vector search enabled", C.tool)
30
+ return true
31
+ }
32
+
33
+ /** 保存后的公共重载:loadConfig injectProxy → 恢复 provider 选择。
34
+ * 运行时 /model 切过 provider(未落盘)时保持它,不回滚到磁盘值。 */
35
+ async function reloadConfig() {
36
+ const { loadConfig } = await import("../config.mjs")
37
+ const { injectProxy } = await import("../proxy.mjs")
38
+ const cfg = loadConfig()
39
+ injectProxy(cfg.providersList, cfg)
40
+ const runtimeName = agent.activeProvider
41
+ agent.providers = cfg.providersList
42
+ agent.config = cfg
43
+ agent.config.agent ??= {}
44
+ const keep = cfg.providersList.find((p) => p.name === runtimeName)
45
+ if (runtimeName && runtimeName !== cfg.activeProvider && keep) {
46
+ // 运行时选择在新配置里仍存在 保持(provider 为注入 proxyUri 后的新对象)
47
+ agent.activeProvider = runtimeName
48
+ agent.provider = { ...keep }
49
+ } else {
50
+ agent.activeProvider = cfg.activeProvider
51
+ agent.provider = cfg.provider
52
+ agent.provider.proxyUri = cfg.providersList.find((p) => p.name === cfg.activeProvider)?.proxyUri
53
+ }
54
+ }
55
+
56
+ /** 保存 config(mutate 改 raw)→ reloadConfig(provider 代理无需重启即生效) */
57
+ async function saveProxy(mutate) {
58
+ const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
59
+ mutate(raw)
60
+ const { saveConfig } = await import("../config.mjs")
61
+ saveConfig(raw)
62
+ await reloadConfig()
63
+ }
64
+
65
+ // ── Proxy sub-menu loop:每轮重建 entries 显示最新状态,defaultIndex 记住上次位置 ──
66
+ async function proxyMenu() {
67
+ let proxyIdx = 0
68
+ for (;;) {
69
+ const pc = agent.config?.proxy // 已归一化 { uri, web, model } | undefined
70
+ const entries = [
71
+ { type: "header", text: `Proxy: ${pc?.uri || "(not set)"}` },
72
+ { type: "item", text: "Set proxy URI…", action: "seturi" },
73
+ { type: "item", text: `Web tools (fetch/websearch): ${!pc || pc.web ? "ON" : "OFF"}`, action: "toggleweb" },
74
+ { type: "item", text: `Model requests (providers with proxy:true): ${pc?.model ? "ON" : "OFF"}`, action: "togglemodel" },
75
+ { type: "item", text: "Test connection", action: "test" },
76
+ { type: "item", text: "Clear proxy", action: "clear" },
77
+ ]
78
+ const c = await showPicker("Proxy", entries, { defaultIndex: proxyIdx })
79
+ if (!c) return // Esc 返回主菜单
80
+ proxyIdx = Math.max(0, entries.filter((e) => e.type === "item").indexOf(c))
81
+
82
+ try {
83
+ if (c.action === "seturi") {
84
+ const newUri = await askQuestion("Proxy URI (e.g. http://127.0.0.1:7890):")
85
+ if (!newUri) continue // 空输入不改动
86
+ // web 默认 true、保留原 model 值(对象形态);旧 string 形态升级为规范对象
87
+ await saveProxy((raw) => {
88
+ raw.proxy = raw.proxy && typeof raw.proxy === "object" && !Array.isArray(raw.proxy)
89
+ ? { ...raw.proxy, uri: newUri }
90
+ : { uri: newUri, web: true, model: false }
91
+ })
92
+ pushLabel("❯ Config", ansi.bold + C.tool)
93
+ pushLine(`proxy.uri = ${newUri}`, C.tool)
94
+ } else if (c.action === "toggleweb" || c.action === "togglemodel") {
95
+ if (!pc) { pushLine("Proxy URI not set — use Set proxy URI… first", C.error); continue }
96
+ const key = c.action === "toggleweb" ? "web" : "model"
97
+ await saveProxy((raw) => { raw.proxy = { ...pc, [key]: !pc[key] } })
98
+ pushLabel("❯ Config", ansi.bold + C.tool)
99
+ pushLine(`proxy.${key} = ${!pc[key] ? "on" : "off"}`, C.tool)
100
+ } else if (c.action === "test") {
101
+ const { proxyFetch, resolveWebProxy } = await import("../proxy.mjs")
102
+ const { UA } = await import("../tools/web.mjs")
103
+ const uri = resolveWebProxy({ agent })
104
+ pushLabel("❯ Config", ansi.bold + C.tool)
105
+ pushLine(`Testing ${uri ? `via proxy ${uri}` : "direct (no proxy)"}...`, C.dim)
106
+ try {
107
+ const res = await Promise.race([
108
+ proxyFetch("https://www.gstatic.com/generate_204", { headers: { "User-Agent": UA } }, uri),
109
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 5s")), 5000)),
110
+ ])
111
+ if (res.ok) pushLine(`✓ OK (HTTP ${res.status})`, C.tool)
112
+ else pushLine(`✗ HTTP ${res.status}`, C.error)
113
+ } catch (error) {
114
+ pushLine(`✗ ${error.message}`, C.error)
115
+ }
116
+ } else if (c.action === "clear") {
117
+ await saveProxy((raw) => { delete raw.proxy })
118
+ pushLabel("❯ Config", ansi.bold + C.tool)
119
+ pushLine("Proxy cleared", C.tool)
56
120
  }
57
- pushLabel(`❯ Config`, ansi.bold + C.tool)
58
- pushLine(`Embedding key saved, vector search enabled`, C.tool)
59
- return
60
- }
61
- // Boolean toggle: agent.verifyGuard
62
- if (e.action === "agent.verifyGuard") {
63
- const newVal = ac.verifyGuard !== true // toggle: undefined/false → true, true → false
64
- try {
65
- const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
121
+ } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
122
+ }
123
+ }
124
+
125
+ // Direct args: /config embedkey
126
+ const sub = args[0]?.toLowerCase()
127
+ if (sub === "embedkey") {
128
+ await setEmbedKey()
129
+ return
130
+ }
131
+ if (sub) { pushLine("Usage: /config [embedkey]", C.error); return }
132
+
133
+ // ── Main config loop ──
134
+ let running = true
135
+ let mainIdx = 0 // 记住上次选中位置,改完一项回主菜单时恢复
136
+ while (running) {
137
+ const mainEntries = [
138
+ { type: "header", text: `proxy=${proxySummary()} | maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${ac.compactThreshold ?? 100000} | verifyGuard=${ac.verifyGuard === true ? "on" : "off"} | embedding=${agent.memory?.embedder ? "on" : "off"}` },
139
+ { type: "item", text: `agent.maxTurns = ${ac.maxTurns ?? 100}`, action: "agent.maxTurns" },
140
+ { type: "item", text: `agent.subagentTurns = ${ac.subagentTurns ?? 100}`, action: "agent.subagentTurns" },
141
+ { type: "item", text: `agent.compactThreshold = ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, action: "agent.compactThreshold" },
142
+ { type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
143
+ { type: "item", text: "Set embedding API key", action: "embedkey" },
144
+ { type: "item", text: `embedding.model = ${ec.model ?? "BAAI/bge-m3"}`, action: "embedding.model" },
145
+ { type: "item", text: `proxy = ${proxySummary()}`, action: "proxy" },
146
+ { type: "item", text: "View full config", action: "view" },
147
+ ]
148
+
149
+ const choice = await showPicker("Config", mainEntries, { defaultIndex: mainIdx })
150
+ if (!choice) { running = false; continue } // Esc
151
+ mainIdx = Math.max(0, mainEntries.filter((e) => e.type === "item").indexOf(choice))
152
+
153
+ if (choice.action === "view") {
154
+ pushLabel("❯ Config", ansi.bold + C.tool)
155
+ pushLine(`Active: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
156
+ pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
157
+ pushLine(`agent.maxTurns: ${ac.maxTurns ?? 100}`, C.dim)
158
+ pushLine(`agent.subagentTurns: ${ac.subagentTurns ?? 100}`, C.dim)
159
+ pushLine(`agent.compactThreshold: ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, C.dim)
160
+ pushLine(`agent.verifyGuard: ${ac.verifyGuard === true ? "on" : "off"}`, C.dim)
161
+ pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${ec.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
162
+ pushLine(`proxy: ${proxySummary()}`, C.dim)
163
+ pushLine(`Config file: ${configPath}`, C.dim)
164
+ running = false
165
+ continue
166
+ }
167
+
168
+ if (choice.action === "proxy") {
169
+ await proxyMenu()
170
+ continue
171
+ }
172
+
173
+ if (choice.action === "embedkey") {
174
+ if (await setEmbedKey()) running = false
175
+ continue
176
+ }
177
+
178
+ if (choice.action === "agent.verifyGuard") {
179
+ const newVal = ac.verifyGuard !== true
180
+ try {
181
+ await saveProxy((raw) => {
66
182
  raw.agent ??= {}
67
183
  raw.agent.verifyGuard = newVal
68
- const { saveConfig, loadConfig } = await import("../config.mjs")
69
- saveConfig(raw)
70
- const cfg = loadConfig()
71
- agent.provider = cfg.provider
72
- agent.providers = cfg.providersList
73
- agent.activeProvider = cfg.activeProvider
74
- agent.config = cfg
75
- agent.config.agent ??= {}
76
- pushLabel(`❯ Config`, ansi.bold + C.tool)
77
- pushLine(`agent.verifyGuard = ${newVal ? "on" : "off"}`, C.tool)
78
- } catch (error) {
79
- pushLine(`Save failed: ${error.message}`, C.error)
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
184
  })
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
185
+ pushLabel("❯ Config", ansi.bold + C.tool)
186
+ pushLine(`agent.verifyGuard = ${newVal ? "on" : "off"}`, C.tool)
187
+ running = false
188
+ } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
189
+ continue
190
+ }
191
+
192
+ if (choice.action === "embedding.model") {
193
+ const models = [
194
+ { label: "BAAI/bge-m3 (multilingual, 1024d)", value: "BAAI/bge-m3" },
195
+ { label: "BAAI/bge-large-zh-v1.5 (Chinese, 1024d)", value: "BAAI/bge-large-zh-v1.5" },
196
+ { label: "BAAI/bge-large-en-v1.5 (English, 1024d)", value: "BAAI/bge-large-en-v1.5" },
197
+ { label: "text-embedding-3-small (OpenAI, 1536d)", value: "text-embedding-3-small" },
198
+ { label: "text-embedding-3-large (OpenAI, 3072d)", value: "text-embedding-3-large" },
199
+ ]
200
+ const currentVal = ec.model ?? "BAAI/bge-m3"
201
+ const modelChoice = await showPicker("Embedding Model", [
202
+ { type: "header", text: `Current: ${currentVal}` },
203
+ ...models.map(m => ({ type: "item", text: m.label, action: m.value })),
204
+ ])
205
+ if (!modelChoice) continue
131
206
  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)
154
- }
155
- },
156
- })
207
+ await saveProxy((raw) => {
208
+ raw.embedding ??= {}
209
+ raw.embedding.model = modelChoice.action
210
+ })
211
+ pushLabel("❯ Config", ansi.bold + C.tool)
212
+ pushLine(`embedding.model = ${modelChoice.action}`, C.tool)
213
+ running = false
214
+ } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
215
+ continue
216
+ }
217
+
218
+ // Numeric config items
219
+ const label = choice.action
220
+ const current = label === "agent.maxTurns" ? (ac.maxTurns ?? 100)
221
+ : label === "agent.subagentTurns" ? (ac.subagentTurns ?? 100)
222
+ : label === "agent.compactThreshold" ? (ac.compactThreshold ?? 100000)
223
+ : ""
224
+ const val = await askQuestion(`${label} (current: ${current}):`)
225
+ if (!val) continue
226
+ try {
227
+ const num = Number(val)
228
+ if (isNaN(num)) { pushLine("Value must be a number", C.error); continue }
229
+ await saveProxy((raw) => {
230
+ const keys = label.split(".")
231
+ let obj = raw
232
+ for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
233
+ obj[keys[keys.length - 1]] = num
234
+ })
235
+ pushLabel("❯ Config", ansi.bold + C.tool)
236
+ pushLine(`${label} = ${val}`, C.tool)
237
+ pushLine("(restart to apply)", C.dim)
238
+ running = false
239
+ } catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
240
+ }
157
241
  }
@@ -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 (use /skills to list, agent will recall via memory_search)`
10
+ ? `Knowledge extracted: ${count} candidate(s) saved to memory (agent will recall them via memory_search)`
11
11
  : "No new knowledge found in this session."
12
12
  pushLine(msg, count > 0 ? C.tool : C.dim)
13
13
  }
@@ -3,10 +3,9 @@
3
3
  */
4
4
  import { C } from "./ansi.mjs"
5
5
 
6
- export async function handleFoldCommand(ctx) {
6
+ export async function handleFoldCommand(ctx, args = []) {
7
7
  const { state } = ctx
8
- const text = state.input.join("").trim()
9
- const arg = text.split(/\s+/)[1]
8
+ const arg = args[0]?.toLowerCase()
10
9
  if (arg === "on") {
11
10
  state.foldEnabled = true
12
11
  ctx.pushLine("Folding: on (long tool results are collapsed)", C.dim)
@@ -1,8 +1,47 @@
1
+ import { C } from "./ansi.mjs"
2
+
1
3
  /** /goal command: set/view/cancel long-term goal.
2
4
  * Extracted from slash-commands.mjs.
3
- * ctx: { agent, pushLine, pushLabel, openPicker, askQuestion } */
4
- export async function handleGoalCommand(ctx) {
5
- const { agent, pushLine, pushLabel, openPicker, askQuestion } = ctx
5
+ * ctx: { agent, pushLine, pushLabel, showPicker, askQuestion } */
6
+ export async function handleGoalCommand(ctx, args = []) {
7
+ const { agent, pushLine, pushLabel, showPicker, askQuestion } = ctx
8
+
9
+ function setGoal(goalText) {
10
+ const semiIdx = goalText.indexOf(";") >= 0 ? goalText.indexOf(";") : goalText.indexOf(";")
11
+ const objective = semiIdx >= 0 ? goalText.slice(0, semiIdx).trim() : goalText.trim()
12
+ const criteria = semiIdx >= 0 ? goalText.slice(semiIdx + 1).trim() : ""
13
+ agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
14
+ }
15
+
16
+ function viewGoal() {
17
+ const statusText = { active: "active", complete: "completed", blocked: "blocked" }[agent.goal.status] ?? agent.goal.status
18
+ pushLine(`Goal: ${agent.goal.objective}`, C.tool)
19
+ if (agent.goal.criteria) pushLine(` Criteria: ${agent.goal.criteria}`, C.dim)
20
+ pushLine(` Status: ${statusText} │ Turns used: ${agent.goal.turnsUsed ?? 0} │ Set at: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
21
+ }
22
+
23
+ // Direct args: /goal set <text> │ /goal cancel │ /goal view
24
+ const sub = args[0]?.toLowerCase()
25
+ if (sub === "set") {
26
+ const goalText = args.slice(1).join(" ")
27
+ if (!goalText) { pushLine("Usage: /goal set <objective>[; criteria]", C.error); return }
28
+ setGoal(goalText)
29
+ pushLine(`Goal set: ${agent.goal.objective}`, C.tool)
30
+ return
31
+ }
32
+ if (sub === "cancel") {
33
+ if (!agent.goal) { pushLine("No goal set", C.dim); return }
34
+ agent.goal = null
35
+ pushLine("Goal cancelled", C.tool)
36
+ return
37
+ }
38
+ if (sub === "view") {
39
+ if (!agent.goal) { pushLine("No goal set", C.dim); return }
40
+ viewGoal()
41
+ return
42
+ }
43
+ if (sub) { pushLine("Usage: /goal [set <text>|cancel|view]", C.error); return }
44
+
6
45
  const entries = [
7
46
  { type: "header", text: agent.goal ? `Current goal: ${agent.goal.objective.slice(0, 60)}` : "Actions" },
8
47
  { type: "item", text: "Set new goal", action: "set" },
@@ -11,28 +50,20 @@ export async function handleGoalCommand(ctx) {
11
50
  entries.push({ type: "item", text: "Cancel goal", action: "cancel" })
12
51
  entries.push({ type: "item", text: "View details", action: "view" })
13
52
  }
14
- openPicker({
15
- title: "Goal",
16
- entries,
17
- onSelect: async (e) => {
18
- if (e.action === "view") {
19
- const statusText = { active: "active", complete: "completed", blocked: "blocked" }[agent.goal.status] ?? agent.goal.status
20
- pushLine(`Goal: ${agent.goal.objective}`, C.tool)
21
- if (agent.goal.criteria) pushLine(` Criteria: ${agent.goal.criteria}`, C.dim)
22
- pushLine(` Status: ${statusText} │ Turns used: ${agent.goal.turnsUsed ?? 0} │ Set at: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
23
- return
24
- }
25
- if (e.action === "cancel") {
26
- agent.goal = null
27
- return
28
- }
29
- // set requires entering goal text
30
- const goalText = await askQuestion("Enter goal description (; separates criteria)")
31
- if (!goalText) return
32
- const semi = goalText.indexOf(";") >= 0 ? ";" : goalText.indexOf(";") >= 0 ? ";" : null
33
- const objective = semi ? goalText.slice(0, semi).trim() : goalText.trim()
34
- const criteria = semi ? goalText.slice(semi + 1).trim() : ""
35
- agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
36
- },
37
- })
53
+ // 先 await picker 返回(选中即关闭),再 askQuestion —— 两者不共存
54
+ const e = await showPicker("Goal", entries)
55
+ if (!e) return
56
+ if (e.action === "view") {
57
+ viewGoal()
58
+ return
59
+ }
60
+ if (e.action === "cancel") {
61
+ agent.goal = null
62
+ return
63
+ }
64
+ // set requires entering goal text
65
+ const goalText = await askQuestion("Enter goal description (; separates criteria)")
66
+ if (!goalText) return
67
+ setGoal(goalText)
68
+ pushLine(`Goal set: ${agent.goal.objective}`, C.tool) // 与直参路径口径一致
38
69
  }
@@ -1,10 +1,12 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
+ import { SLASH_ALIASES } from "./slash-commands.mjs"
2
3
 
3
4
  /** /help command: list all slash commands and aliases.
4
5
  * ctx: { pushLine, pushLabel, SLASH_COMMANDS } */
5
6
  export async function handleHelpCommand(ctx) {
6
7
  const { pushLine, pushLabel, SLASH_COMMANDS } = ctx
7
- const aliasList = { "/help": "/h", "/exit": "/x", "/model": "/m", "/plan": "/p", "/think": "/t", "/clear": "/c", "/new": "/n" }
8
+ // reverse the shared alias table: command alias
9
+ const aliasList = Object.fromEntries(Object.entries(SLASH_ALIASES).map(([alias, cmd]) => [cmd, alias]))
8
10
  const order = ["Agent", "Session", "Project", "System"]
9
11
  const byGroup = new Map()
10
12
  for (const c of SLASH_COMMANDS) {