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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/make-agent.mjs +7 -0
- package/src/config.mjs +46 -19
- package/src/prompts/coder.md +5 -2
- package/src/prompts/discipline.md +8 -5
- package/src/prompts/system.md +8 -5
- package/src/provider/anthropic.mjs +190 -0
- package/src/provider/core.mjs +36 -4
- package/src/provider/google.mjs +197 -0
- package/src/proxy.mjs +236 -0
- package/src/tools/fetch.md +2 -1
- package/src/tools/git.mjs +120 -154
- package/src/tools/index.mjs +9 -9
- package/src/tools/linter.mjs +46 -32
- package/src/tools/web.mjs +103 -82
- package/src/tools/websearch.md +5 -3
- package/src/tui/agent-turn.mjs +12 -13
- package/src/tui/cmd-advisor.mjs +29 -41
- package/src/tui/cmd-clear.mjs +11 -17
- package/src/tui/cmd-config.mjs +226 -142
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-fold.mjs +2 -3
- package/src/tui/cmd-goal.mjs +58 -27
- package/src/tui/cmd-help.mjs +3 -1
- package/src/tui/cmd-mcp.mjs +178 -142
- package/src/tui/cmd-model.mjs +15 -4
- package/src/tui/cmd-new.mjs +7 -13
- package/src/tui/cmd-restore.mjs +12 -16
- package/src/tui/cmd-session.mjs +28 -32
- package/src/tui/cmd-think.mjs +75 -50
- package/src/tui/cmd-undo.mjs +19 -23
- package/src/tui/cmd-upgrade.mjs +22 -26
- package/src/tui/index.mjs +64 -38
- package/src/tui/key-handler.mjs +48 -18
- package/src/tui/layout.mjs +12 -2
- package/src/tui/pickers.mjs +151 -182
- package/src/tui/render-frame.mjs +29 -11
- package/src/tui/slash-commands.mjs +26 -16
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
}
|
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 (
|
|
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
|
}
|
package/src/tui/cmd-fold.mjs
CHANGED
|
@@ -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
|
|
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)
|
package/src/tui/cmd-goal.mjs
CHANGED
|
@@ -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,
|
|
4
|
-
export async function handleGoalCommand(ctx) {
|
|
5
|
-
const { agent, pushLine, pushLabel,
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
}
|
package/src/tui/cmd-help.mjs
CHANGED
|
@@ -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
|
-
|
|
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) {
|