thincoder 0.11.0 → 0.12.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 +8 -0
- package/package.json +1 -1
- package/src/advisor.mjs +535 -72
- package/src/agent/helpers.mjs +18 -5
- package/src/agent/setup.mjs +2 -2
- package/src/agent-tools/advisor.mjs +36 -0
- package/src/agent-tools/plan.mjs +53 -2
- package/src/agent-tools/subagent.mjs +7 -1
- package/src/agent-tools/timer.mjs +1 -1
- package/src/agent-tools/verify.mjs +1 -0
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +80 -21
- package/src/auto-think.mjs +23 -5
- package/src/cli/make-agent.mjs +20 -0
- package/src/config.mjs +1 -1
- package/src/mcp/transport-stdio.mjs +4 -3
- package/src/mcp.mjs +1 -1
- package/src/prompts/advisor-round1.md +23 -0
- package/src/prompts/advisor-round2.md +26 -0
- package/src/prompts/advisor-round3.md +24 -0
- package/src/prompts/coder.md +1 -0
- package/src/prompts/discipline.md +15 -1
- package/src/prompts/explore.md +2 -0
- package/src/prompts/plan.md +2 -0
- package/src/prompts/system.md +5 -1
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +6 -126
- package/src/provider/google.mjs +4 -2
- package/src/provider/sse.mjs +112 -0
- package/src/skills.mjs +67 -31
- package/src/tools/bash.md +8 -0
- package/src/tools/codemode.mjs +5 -16
- package/src/tools/edit.md +8 -0
- package/src/tools/git.mjs +9 -6
- package/src/tools/read.md +7 -0
- package/src/tools/shared.mjs +43 -2
- package/src/tools/system.mjs +14 -10
- package/src/tools/web.mjs +21 -16
- package/src/tui/agent-turn.mjs +130 -73
- package/src/tui/cmd-advisor.mjs +237 -41
- package/src/tui/cmd-auto.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +4 -2
- package/src/tui/cmd-plan.mjs +6 -8
- package/src/tui/cmd-think.mjs +93 -70
- package/src/tui/index.mjs +9 -188
- package/src/tui/interaction.mjs +2 -1
- package/src/tui/key-handler.mjs +8 -4
- package/src/tui/layout.mjs +3 -2
- package/src/tui/render-conversation.mjs +92 -0
- package/src/tui/render-frame.mjs +94 -168
- package/src/tui/render-loop.mjs +110 -0
package/src/tui/cmd-advisor.mjs
CHANGED
|
@@ -1,56 +1,252 @@
|
|
|
1
|
-
/** /advisor command: toggle advisor on/off, select model.
|
|
2
|
-
*
|
|
3
|
-
|
|
1
|
+
/** /advisor command: toggle advisor on/off, select model, configure thinking.
|
|
2
|
+
* Interactive loop UX — stays in menu after each action, Esc to exit.
|
|
3
|
+
* ctx: { agent, showPicker, pushLine, pushLabel, persistRaw } */
|
|
4
|
+
import { ansi, C } from "./ansi.mjs"
|
|
4
5
|
|
|
5
6
|
export async function handleAdvisorCommand(ctx) {
|
|
6
|
-
const { agent, showPicker, pushLine } = ctx
|
|
7
|
+
const { agent, showPicker, pushLine, pushLabel } = ctx
|
|
7
8
|
const cfg = agent.config.advisor ??= {}
|
|
8
|
-
const enabled = cfg.enabled === true
|
|
9
|
-
const curProvider = cfg.provider || agent.activeProvider
|
|
10
|
-
const curModel = cfg.model || agent.provider.model
|
|
11
9
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const persist = async () => {
|
|
11
|
+
if (ctx.persistRaw) {
|
|
12
|
+
await ctx.persistRaw((raw) => {
|
|
13
|
+
raw.agent ??= {}
|
|
14
|
+
raw.agent.advisor = cfg
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Lazy model cache — fetched once per /advisor session
|
|
20
|
+
let modelCache = null
|
|
21
|
+
|
|
22
|
+
// ── State helpers ──
|
|
23
|
+
function advisorStatus() {
|
|
24
|
+
const enabled = cfg.enabled === true
|
|
25
|
+
const curModel = cfg.model || agent.provider.model
|
|
26
|
+
const thinkInfo = cfg.thinking === null ? "off"
|
|
27
|
+
: cfg.thinking?.type === "disabled" ? "off"
|
|
28
|
+
: cfg.reasoningEffort ? `on (${cfg.reasoningEffort})`
|
|
29
|
+
: cfg.thinking ? `on (${cfg.thinking.type})` : "(main)"
|
|
30
|
+
return `Advisor: ${enabled ? "ON" : "OFF"} | Model: ${curModel} | Think: ${thinkInfo}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function headerLine() {
|
|
34
|
+
return ` ${advisorStatus()}`.replace(/\|/g, ansi.dim + "|" + ansi.reset)
|
|
35
|
+
}
|
|
16
36
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
// ── Model picker sub-loop ──
|
|
38
|
+
async function modelPicker() {
|
|
39
|
+
if (!modelCache) {
|
|
40
|
+
modelCache = await fetchAdvisorModels(agent)
|
|
41
|
+
}
|
|
42
|
+
let modelIdx = 0
|
|
43
|
+
for (;;) {
|
|
44
|
+
const entries = buildModelEntries(agent, cfg, modelCache)
|
|
45
|
+
const c = await showPicker("Advisor Model", entries, { defaultIndex: modelIdx })
|
|
46
|
+
if (!c) return
|
|
47
|
+
modelIdx = Math.max(0, entries.filter((e) => e.type === "item").indexOf(c))
|
|
48
|
+
|
|
49
|
+
if (c.action === "inherit") {
|
|
50
|
+
delete cfg.provider
|
|
51
|
+
delete cfg.model
|
|
52
|
+
await persist()
|
|
53
|
+
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
54
|
+
pushLine("Model: using main model", C.tool)
|
|
55
|
+
} else if (c.action === "switch") {
|
|
56
|
+
cfg.provider = c.provider
|
|
57
|
+
cfg.model = c.model
|
|
58
|
+
await persist()
|
|
59
|
+
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
60
|
+
pushLine(`Model: ${c.provider}/${c.model}`, C.tool)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Thinking picker sub-loop ──
|
|
66
|
+
async function thinkingPicker() {
|
|
67
|
+
let thinkIdx = 0
|
|
68
|
+
for (;;) {
|
|
69
|
+
const entries = buildThinkingEntries(agent, cfg)
|
|
70
|
+
const c = await showPicker("Advisor Thinking", entries, { defaultIndex: thinkIdx })
|
|
71
|
+
if (!c) return
|
|
72
|
+
thinkIdx = Math.max(0, entries.filter((e) => e.type === "item").indexOf(c))
|
|
73
|
+
|
|
74
|
+
if (c.action === "inherit") {
|
|
75
|
+
delete cfg.thinking
|
|
76
|
+
delete cfg.reasoningEffort
|
|
77
|
+
await persist()
|
|
78
|
+
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
79
|
+
pushLine("Thinking: using main model settings", C.tool)
|
|
80
|
+
} else if (c.action === "think_on") {
|
|
81
|
+
const { specForModel } = await import("../config.mjs")
|
|
82
|
+
const spec = specForModel(getEffectiveModel(agent, cfg))
|
|
83
|
+
cfg.thinking = { type: spec.thinkEnabledValue ?? "enabled" }
|
|
84
|
+
if (spec.thinkApi === "effort") delete cfg.thinking
|
|
85
|
+
await persist()
|
|
86
|
+
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
87
|
+
pushLine(`Thinking: ON`, C.tool)
|
|
88
|
+
} else if (c.action === "think_off") {
|
|
89
|
+
const { specForModel } = await import("../config.mjs")
|
|
90
|
+
const spec = specForModel(getEffectiveModel(agent, cfg))
|
|
91
|
+
const isCustomThink = (spec.thinkEnabledValue ?? "enabled") !== "enabled"
|
|
92
|
+
cfg.thinking = isCustomThink ? null : { type: "disabled" }
|
|
93
|
+
await persist()
|
|
94
|
+
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
95
|
+
pushLine("Thinking: OFF", C.tool)
|
|
96
|
+
} else if (c.action.startsWith("effort_")) {
|
|
97
|
+
cfg.reasoningEffort = c.action.slice(7)
|
|
98
|
+
await persist()
|
|
99
|
+
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
100
|
+
pushLine(`Reasoning effort: ${cfg.reasoningEffort}`, C.tool)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Main loop ──
|
|
106
|
+
let mainIdx = 0
|
|
107
|
+
for (;;) {
|
|
108
|
+
const enabled = cfg.enabled === true
|
|
109
|
+
const curProvider = cfg.provider || "(main)"
|
|
110
|
+
const curModel = cfg.model || agent.provider.model
|
|
111
|
+
const guardInfo = cfg.guard === true ? "on" : "off"
|
|
112
|
+
|
|
113
|
+
const entries = [
|
|
114
|
+
{ type: "header", text: headerLine() },
|
|
115
|
+
{ type: "item", text: `Advisor: ${enabled ? "ON" : "OFF"}`, action: "toggle" },
|
|
116
|
+
{ type: "item", text: `Model: ${curModel}`, action: "model", note: `Provider: ${curProvider}` },
|
|
117
|
+
{ type: "item", text: `Thinking: ${advisorStatus().split("|")[2]?.trim() || "(main)"}`, action: "thinking" },
|
|
118
|
+
{ type: "item", text: `Guard: ${guardInfo}`, action: "guard" },
|
|
119
|
+
{ type: "item", text: "View full config", action: "view" },
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
const choice = await showPicker("Advisor", entries, { defaultIndex: mainIdx })
|
|
123
|
+
if (!choice) return // Esc
|
|
124
|
+
mainIdx = Math.max(0, entries.filter((e) => e.type === "item").indexOf(choice))
|
|
125
|
+
|
|
126
|
+
if (choice.action === "view") {
|
|
127
|
+
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
128
|
+
pushLine(`Status: ${enabled ? "ON" : "OFF"}`, C.dim)
|
|
129
|
+
pushLine(`Model: ${curModel} (provider: ${curProvider})`, C.dim)
|
|
130
|
+
pushLine(`Guard: ${guardInfo}`, C.dim)
|
|
131
|
+
pushLine(`Thinking: ${advisorStatus().split("|")[2]?.trim() || "(main)"}`, C.dim)
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (choice.action === "toggle") {
|
|
136
|
+
cfg.enabled = !cfg.enabled
|
|
137
|
+
await persist().catch(err => pushLine(`[error] Advisor toggle: ${err.message}`, C.error))
|
|
138
|
+
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
139
|
+
pushLine(`Advisor: ${cfg.enabled ? "ON" : "OFF"}`, C.tool)
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (choice.action === "guard") {
|
|
144
|
+
cfg.guard = !(cfg.guard === true)
|
|
145
|
+
await persist().catch(err => pushLine(`[error] ${err.message}`, C.error))
|
|
146
|
+
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
147
|
+
pushLine(`Guard: ${cfg.guard === true ? "on" : "off"}`, C.tool)
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (choice.action === "model") {
|
|
152
|
+
await modelPicker()
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (choice.action === "thinking") {
|
|
157
|
+
await thinkingPicker()
|
|
158
|
+
continue
|
|
26
159
|
}
|
|
27
|
-
} else if (e.action === "model") {
|
|
28
|
-
await openAdvisorModelPicker(ctx).catch(err => pushLine(`[error] ${err.message}`, C.error))
|
|
29
160
|
}
|
|
30
161
|
}
|
|
31
162
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
163
|
+
// ── Model helpers ──
|
|
164
|
+
|
|
165
|
+
function getEffectiveModel(agent, cfg) {
|
|
166
|
+
const providerForDefaults = cfg.provider
|
|
167
|
+
? agent.providers?.find(p => p.name === cfg.provider) || agent.provider
|
|
168
|
+
: agent.provider
|
|
169
|
+
return cfg.model || providerForDefaults.model
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function fetchAdvisorModels(agent) {
|
|
173
|
+
const { listModels } = await import("../provider/index.mjs")
|
|
174
|
+
const result = new Map()
|
|
175
|
+
await Promise.all((agent.providers || []).map(async (p) => {
|
|
176
|
+
try {
|
|
177
|
+
const envKey = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }[p.name]
|
|
178
|
+
let apiKey = p.apiKey
|
|
179
|
+
if (!apiKey && envKey && process.env[envKey]) apiKey = process.env[envKey]
|
|
180
|
+
if (!apiKey) apiKey = process.env.THINCODER_API_KEY
|
|
181
|
+
const models = await listModels({ baseURL: p.baseURL, apiKey: apiKey ?? "" }, { signal: AbortSignal.timeout(10000) })
|
|
182
|
+
result.set(p.name, { models, error: null })
|
|
183
|
+
} catch (err) {
|
|
184
|
+
result.set(p.name, { models: [], error: err.message.slice(0, 40) })
|
|
185
|
+
}
|
|
186
|
+
}))
|
|
187
|
+
return result
|
|
188
|
+
}
|
|
35
189
|
|
|
36
|
-
|
|
190
|
+
function buildModelEntries(agent, cfg, cache) {
|
|
37
191
|
const entries = []
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
192
|
+
entries.push({ type: "item", text: (!cfg.provider ? "● " : " ") + "Use main model", action: "inherit" })
|
|
193
|
+
|
|
194
|
+
for (const p of agent.providers || []) {
|
|
195
|
+
const cached = cache.get(p.name)
|
|
196
|
+
const hasKey = !!(p.apiKey
|
|
197
|
+
|| (p.name === "deepseek" && process.env.DEEPSEEK_API_KEY)
|
|
198
|
+
|| (p.name === "openai" && process.env.OPENAI_API_KEY)
|
|
199
|
+
|| process.env.THINCODER_API_KEY)
|
|
200
|
+
const noteParts = [p.baseURL]
|
|
201
|
+
if (!hasKey) noteParts.push("(no key)")
|
|
202
|
+
if (agent.activeProvider === p.name) noteParts.push("← active")
|
|
203
|
+
if (cached?.error) noteParts.push(`(fetch failed: ${cached.error})`)
|
|
204
|
+
entries.push({ type: "header", text: p.name, note: noteParts.join(" ") })
|
|
205
|
+
|
|
206
|
+
// Default model
|
|
207
|
+
const isDefault = cfg.provider === p.name && cfg.model === p.model
|
|
208
|
+
entries.push({ type: "item", text: `${isDefault ? "● " : " "}${p.model}`, action: "switch", provider: p.name, model: p.model })
|
|
209
|
+
|
|
210
|
+
// Additional models from API, excluding the default model
|
|
211
|
+
if (cached?.models) {
|
|
212
|
+
for (const m of cached.models) {
|
|
213
|
+
if (m === p.model) continue
|
|
214
|
+
const isSelected = cfg.provider === p.name && cfg.model === m
|
|
215
|
+
entries.push({ type: "item", text: `${isSelected ? "● " : " "}${m}`, action: "switch", provider: p.name, model: m })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
41
218
|
}
|
|
219
|
+
return entries
|
|
220
|
+
}
|
|
42
221
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
222
|
+
async function buildThinkingEntries(agent, cfg) {
|
|
223
|
+
const { specForModel } = await import("../config.mjs")
|
|
224
|
+
const providerForDefaults = cfg.provider
|
|
225
|
+
? agent.providers?.find(p => p.name === cfg.provider) || agent.provider
|
|
226
|
+
: agent.provider
|
|
227
|
+
const effectiveModel = cfg.model || providerForDefaults.model
|
|
228
|
+
const spec = specForModel(effectiveModel)
|
|
229
|
+
const thinkOnValue = spec.thinkEnabledValue ?? "enabled"
|
|
230
|
+
const isCustomThink = thinkOnValue !== "enabled"
|
|
231
|
+
const isEffortOnly = spec.thinkApi === "effort"
|
|
232
|
+
const effortLevels = spec.reasoningEffortEnum ?? ["high", "max"]
|
|
233
|
+
|
|
234
|
+
const curEffort = cfg.reasoningEffort ?? providerForDefaults.reasoningEffort
|
|
235
|
+
const curThinking = cfg.thinking ?? providerForDefaults.thinking
|
|
236
|
+
const thinkingEnabled = curThinking?.type === thinkOnValue
|
|
237
|
+
|| (curThinking?.type === undefined && !isCustomThink)
|
|
238
|
+
|
|
239
|
+
const entries = [
|
|
240
|
+
{ type: "item", text: "Use main model settings", action: "inherit" },
|
|
241
|
+
]
|
|
242
|
+
if (!isEffortOnly) {
|
|
243
|
+
entries.push({ type: "header", text: "Thinking mode" })
|
|
244
|
+
entries.push({ type: "item", text: `Enabled ${thinkingEnabled ? "← current" : ""}`, action: "think_on" })
|
|
245
|
+
entries.push({ type: "item", text: `Disabled ${(curThinking?.type === "disabled" || curThinking === null) ? "← current" : ""}`, action: "think_off" })
|
|
246
|
+
}
|
|
247
|
+
entries.push({ type: "header", text: "Reasoning effort" })
|
|
248
|
+
for (const level of effortLevels) {
|
|
249
|
+
entries.push({ type: "item", text: `${level} ${curEffort === level ? "← current" : ""}`, action: `effort_${level}` })
|
|
55
250
|
}
|
|
251
|
+
return entries
|
|
56
252
|
}
|
package/src/tui/cmd-auto.mjs
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/** /auto command: toggle auto-approve mode.
|
|
2
|
-
* ctx: { agent } */
|
|
2
|
+
* ctx: { agent, pushLine, pushLabel } */
|
|
3
|
+
import { ansi, C } from "./ansi.mjs"
|
|
4
|
+
|
|
3
5
|
export async function handleAutoCommand(ctx) {
|
|
4
|
-
const { agent } = ctx
|
|
6
|
+
const { agent, pushLine, pushLabel } = ctx
|
|
5
7
|
agent.autoApprove = !agent.autoApprove
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
agent._pendingReminders.push("[System reminder: AUTO mode is now ON. All tool calls are automatically approved — you may write, edit, and run commands without asking. Use this for long autonomous tasks. The user can still interrupt.]")
|
|
9
|
-
} else {
|
|
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.]")
|
|
11
|
-
}
|
|
8
|
+
pushLabel("❯ Auto", ansi.bold + C.tool)
|
|
9
|
+
pushLine(`Auto-approve: ${agent.autoApprove ? "ON" : "OFF"}`, C.tool)
|
|
12
10
|
}
|
package/src/tui/cmd-mcp.mjs
CHANGED
|
@@ -21,7 +21,7 @@ async function addAndConnect(ctx, srv) {
|
|
|
21
21
|
const entry = { name: srv.name }
|
|
22
22
|
if (srv.url) { entry.url = srv.url; if (srv.headers) entry.headers = srv.headers }
|
|
23
23
|
else if (srv.wsUrl) { entry.wsUrl = srv.wsUrl; if (srv.headers) entry.headers = srv.headers }
|
|
24
|
-
else { entry.command = srv.command; if (srv.args) entry.args = srv.args }
|
|
24
|
+
else { entry.command = srv.command; if (srv.args) entry.args = srv.args; if (srv.env) entry.env = srv.env }
|
|
25
25
|
raw.mcp.servers.push(entry)
|
|
26
26
|
})
|
|
27
27
|
agent.config ??= {}
|
|
@@ -136,7 +136,9 @@ Return ONLY the JSON object:`,
|
|
|
136
136
|
if (!cmd) return
|
|
137
137
|
const argsInput = await askQuestion("Arguments (space-separated, or leave empty):")
|
|
138
138
|
const cmdArgs = argsInput ? argsInput.split(/\s+/) : undefined
|
|
139
|
-
await
|
|
139
|
+
const envInput = await askQuestion("Environment variables (KEY=value, space-separated, or leave empty):")
|
|
140
|
+
const env = envInput ? parseHeaders(envInput.split(/\s+/)) : undefined
|
|
141
|
+
await addAndConnect(ctx, { name, command: cmd, args: cmdArgs, env })
|
|
140
142
|
} else {
|
|
141
143
|
const urlPrompt = transport === "ws" ? "WebSocket URL (ws://…):" : "HTTP URL (https://…):"
|
|
142
144
|
const url = await askQuestion(urlPrompt)
|
package/src/tui/cmd-plan.mjs
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/** /plan command: toggle plan mode (read-only explore → design → implement).
|
|
2
|
-
* ctx: { agent } */
|
|
2
|
+
* ctx: { agent, pushLine, pushLabel } */
|
|
3
|
+
import { ansi, C } from "./ansi.mjs"
|
|
4
|
+
|
|
3
5
|
export async function handlePlanCommand(ctx) {
|
|
4
|
-
const { agent } = ctx
|
|
6
|
+
const { agent, pushLine, pushLabel } = ctx
|
|
5
7
|
agent.planMode = !agent.planMode
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
agent._pendingReminders.push("[System reminder: plan mode is now ON. You are restricted to READ-ONLY tools — explore, search, read, analyze. DO NOT write, edit, or run mutation commands. Present your design to the user first.]")
|
|
9
|
-
} else {
|
|
10
|
-
agent._pendingReminders.push("[System reminder: plan mode is now OFF. You may edit files, run commands, and implement changes.]")
|
|
11
|
-
}
|
|
8
|
+
pushLabel("❯ Plan", ansi.bold + C.tool)
|
|
9
|
+
pushLine(`Plan mode: ${agent.planMode ? "ON" : "OFF"}`, C.tool)
|
|
12
10
|
}
|
package/src/tui/cmd-think.mjs
CHANGED
|
@@ -1,68 +1,27 @@
|
|
|
1
|
-
import { C } from "./ansi.mjs"
|
|
2
|
-
|
|
3
1
|
/** /think command: toggle thinking mode, set reasoning effort.
|
|
4
|
-
*
|
|
5
|
-
* ctx: { agent, showPicker, syncProviderField, pushLine } */
|
|
2
|
+
* Interactive loop UX — stays in menu after each action, Esc to exit.
|
|
3
|
+
* ctx: { agent, showPicker, syncProviderField, pushLine, pushLabel } */
|
|
4
|
+
import { ansi, C } from "./ansi.mjs"
|
|
5
|
+
|
|
6
6
|
export async function handleThinkCommand(ctx, args = []) {
|
|
7
|
-
const { agent, showPicker, syncProviderField, pushLine } = ctx
|
|
8
|
-
const cur = agent.provider
|
|
7
|
+
const { agent, showPicker, syncProviderField, pushLine, pushLabel } = ctx
|
|
9
8
|
const { specForModel } = await import("../config.mjs")
|
|
9
|
+
|
|
10
|
+
// Fast path: direct args — exit immediately
|
|
11
|
+
const cur = agent.provider
|
|
10
12
|
const spec = specForModel(cur.model)
|
|
11
13
|
const isEffortOnly = spec.thinkApi === "effort"
|
|
12
14
|
const thinkOnValue = spec.thinkEnabledValue ?? "enabled"
|
|
13
15
|
const isCustomThink = thinkOnValue !== "enabled"
|
|
14
16
|
const effortLevels = spec.reasoningEffortEnum ?? ["high", "max"]
|
|
15
17
|
|
|
16
|
-
async function apply(e) {
|
|
17
|
-
if (e.action === "auto") {
|
|
18
|
-
const cfg = agent.config.agent ??= {}
|
|
19
|
-
cfg.autoThink = !cfg.autoThink
|
|
20
|
-
agent._pendingReminders = agent._pendingReminders ?? []
|
|
21
|
-
if (cfg.autoThink) {
|
|
22
|
-
// Turn off manual effort — auto will set it per-turn
|
|
23
|
-
delete cur.reasoningEffort
|
|
24
|
-
await syncProviderField("reasoningEffort", undefined)
|
|
25
|
-
agent._pendingReminders.push("[System reminder: Auto-think is now ON. Reasoning effort will be automatically set per-task based on difficulty classification.]")
|
|
26
|
-
} else {
|
|
27
|
-
agent._pendingReminders.push("[System reminder: Auto-think is now OFF. Reasoning effort will remain at its current manual setting.]")
|
|
28
|
-
}
|
|
29
|
-
} else if (e.action === "effort") {
|
|
30
|
-
cur.reasoningEffort = e.level
|
|
31
|
-
await syncProviderField("reasoningEffort", e.level)
|
|
32
|
-
} else {
|
|
33
|
-
const enable = e.action === "on"
|
|
34
|
-
if (isEffortOnly) {
|
|
35
|
-
if (!enable) delete cur.reasoningEffort
|
|
36
|
-
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
37
|
-
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
38
|
-
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
39
|
-
} else {
|
|
40
|
-
if (enable) {
|
|
41
|
-
cur.thinking = { type: thinkOnValue }
|
|
42
|
-
if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
43
|
-
} else {
|
|
44
|
-
// Custom-think models (MiniMax "adaptive") don't support "disabled" — remove the field instead
|
|
45
|
-
cur.thinking = isCustomThink ? undefined : { type: "disabled" }
|
|
46
|
-
delete cur.reasoningEffort
|
|
47
|
-
}
|
|
48
|
-
await syncProviderField("thinking", cur.thinking)
|
|
49
|
-
if (enable) {
|
|
50
|
-
await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
51
|
-
} else {
|
|
52
|
-
await syncProviderField("reasoningEffort", undefined)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Direct args: /think on|off │ /think effort <level>
|
|
59
|
-
// autoThink 开启时手动值每轮被覆盖(picker 里也隐藏了开关/effort 项),直参同样拒绝
|
|
60
18
|
const autoThinkEnabled = agent.config?.agent?.autoThink === true
|
|
61
19
|
const sub = args[0]?.toLowerCase()
|
|
62
20
|
if (sub === "on" || sub === "off") {
|
|
63
21
|
if (autoThinkEnabled) { pushLine("Auto-think is ON — manual settings are overridden each turn; turn Auto off first via /think", C.error); return }
|
|
64
|
-
await
|
|
65
|
-
|
|
22
|
+
await applyThink({ action: sub }, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue)
|
|
23
|
+
pushLabel("❯ Think", ansi.bold + C.tool)
|
|
24
|
+
pushLine(`Thinking: ${sub}`, C.tool)
|
|
66
25
|
return
|
|
67
26
|
}
|
|
68
27
|
if (sub === "effort") {
|
|
@@ -72,29 +31,93 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
72
31
|
return
|
|
73
32
|
}
|
|
74
33
|
if (autoThinkEnabled) { pushLine("Auto-think is ON — manual settings are overridden each turn; turn Auto off first via /think", C.error); return }
|
|
75
|
-
await
|
|
76
|
-
|
|
34
|
+
await applyThink({ action: "effort", level }, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue)
|
|
35
|
+
pushLabel("❯ Think", ansi.bold + C.tool)
|
|
36
|
+
pushLine(`Thinking effort: ${level}`, C.tool)
|
|
77
37
|
return
|
|
78
38
|
}
|
|
79
39
|
if (sub) { pushLine("Usage: /think [on|off|effort <level>]", C.error); return }
|
|
80
40
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
41
|
+
// ── Interactive loop ──
|
|
42
|
+
let mainIdx = 0
|
|
43
|
+
for (;;) {
|
|
44
|
+
const autoOn = agent.config?.agent?.autoThink === true
|
|
45
|
+
const thinkingEnabled = cur.thinking?.type === thinkOnValue
|
|
46
|
+
|| (cur.thinking?.type === undefined && !isCustomThink)
|
|
47
|
+
|
|
48
|
+
const entries = [
|
|
49
|
+
{ type: "header", text: `Auto: ${autoOn ? "ON" : "OFF"} | Thinking: ${thinkingEnabled ? "ON" : "OFF"} | Effort: ${cur.reasoningEffort || "—"}` },
|
|
50
|
+
{ type: "item", text: `Auto: ${autoOn ? "ON" : "OFF"}`, action: "auto" },
|
|
51
|
+
]
|
|
52
|
+
if (!isEffortOnly && !autoOn) {
|
|
53
|
+
entries.push({ type: "item", text: `Thinking: ${thinkingEnabled ? "ON" : "OFF"}`, action: thinkingEnabled ? "off" : "on" })
|
|
54
|
+
}
|
|
55
|
+
if (!autoOn) {
|
|
56
|
+
for (const level of effortLevels) {
|
|
57
|
+
const mark = cur.reasoningEffort === level ? "▸ " : " "
|
|
58
|
+
entries.push({ type: "item", text: `${mark}effort: ${level}`, action: "effort", level })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const e = await showPicker("Think", entries, { defaultIndex: mainIdx })
|
|
63
|
+
if (!e) return // Esc
|
|
64
|
+
mainIdx = Math.max(0, entries.filter((en) => en.type === "item").indexOf(e))
|
|
65
|
+
|
|
66
|
+
const prevAuto = autoOn
|
|
67
|
+
const prevThinking = thinkingEnabled
|
|
68
|
+
const prevEffort = cur.reasoningEffort
|
|
69
|
+
|
|
70
|
+
await applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue)
|
|
71
|
+
|
|
72
|
+
// Feedback
|
|
73
|
+
pushLabel("❯ Think", ansi.bold + C.tool)
|
|
74
|
+
if (e.action === "auto") {
|
|
75
|
+
const newAuto = agent.config?.agent?.autoThink === true
|
|
76
|
+
pushLine(`Auto-think: ${newAuto ? "ON" : "OFF"}`, C.tool)
|
|
77
|
+
} else if (e.action === "effort") {
|
|
78
|
+
pushLine(`Reasoning effort: ${e.level}`, C.tool)
|
|
79
|
+
} else {
|
|
80
|
+
const nowEnabled = cur.thinking?.type === thinkOnValue
|
|
81
|
+
|| (cur.thinking?.type === undefined && !isCustomThink)
|
|
82
|
+
pushLine(`Thinking: ${nowEnabled ? "ON" : "OFF"}`, C.tool)
|
|
83
|
+
}
|
|
88
84
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Shared apply logic — extracted from handleThinkCommand for reuse in both fast path and loop */
|
|
88
|
+
async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue) {
|
|
89
|
+
const cur = agent.provider
|
|
90
|
+
if (e.action === "auto") {
|
|
91
|
+
const cfg = agent.config.agent ??= {}
|
|
92
|
+
cfg.autoThink = !cfg.autoThink
|
|
93
|
+
if (cfg.autoThink) {
|
|
94
|
+
delete cur.reasoningEffort
|
|
95
|
+
await syncProviderField("reasoningEffort", undefined)
|
|
96
|
+
}
|
|
97
|
+
} else if (e.action === "effort") {
|
|
98
|
+
cur.reasoningEffort = e.level
|
|
99
|
+
await syncProviderField("reasoningEffort", e.level)
|
|
100
|
+
} else {
|
|
101
|
+
const enable = e.action === "on"
|
|
102
|
+
if (isEffortOnly) {
|
|
103
|
+
if (!enable) delete cur.reasoningEffort
|
|
104
|
+
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
105
|
+
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
106
|
+
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
107
|
+
} else {
|
|
108
|
+
if (enable) {
|
|
109
|
+
cur.thinking = { type: thinkOnValue }
|
|
110
|
+
if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
111
|
+
} else {
|
|
112
|
+
cur.thinking = isCustomThink ? undefined : { type: "disabled" }
|
|
113
|
+
delete cur.reasoningEffort
|
|
114
|
+
}
|
|
115
|
+
await syncProviderField("thinking", cur.thinking)
|
|
116
|
+
if (enable) {
|
|
117
|
+
await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
118
|
+
} else {
|
|
119
|
+
await syncProviderField("reasoningEffort", undefined)
|
|
120
|
+
}
|
|
93
121
|
}
|
|
94
|
-
} else if (!autoThinkEnabled) {
|
|
95
|
-
entries.push({ type: "item", text: "effort: high", action: "effort", level: "high" })
|
|
96
|
-
entries.push({ type: "item", text: "effort: max", action: "effort", level: "max" })
|
|
97
122
|
}
|
|
98
|
-
const e = await showPicker("Think", entries)
|
|
99
|
-
if (e) await apply(e)
|
|
100
123
|
}
|