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.
Files changed (51) hide show
  1. package/README.md +8 -0
  2. package/package.json +1 -1
  3. package/src/advisor.mjs +535 -72
  4. package/src/agent/helpers.mjs +18 -5
  5. package/src/agent/setup.mjs +2 -2
  6. package/src/agent-tools/advisor.mjs +36 -0
  7. package/src/agent-tools/plan.mjs +53 -2
  8. package/src/agent-tools/subagent.mjs +7 -1
  9. package/src/agent-tools/timer.mjs +1 -1
  10. package/src/agent-tools/verify.mjs +1 -0
  11. package/src/agent-tools.mjs +1 -0
  12. package/src/agent.mjs +80 -21
  13. package/src/auto-think.mjs +23 -5
  14. package/src/cli/make-agent.mjs +20 -0
  15. package/src/config.mjs +1 -1
  16. package/src/mcp/transport-stdio.mjs +4 -3
  17. package/src/mcp.mjs +1 -1
  18. package/src/prompts/advisor-round1.md +23 -0
  19. package/src/prompts/advisor-round2.md +26 -0
  20. package/src/prompts/advisor-round3.md +24 -0
  21. package/src/prompts/coder.md +1 -0
  22. package/src/prompts/discipline.md +15 -1
  23. package/src/prompts/explore.md +2 -0
  24. package/src/prompts/plan.md +2 -0
  25. package/src/prompts/system.md +5 -1
  26. package/src/provider/anthropic.mjs +4 -4
  27. package/src/provider/core.mjs +6 -126
  28. package/src/provider/google.mjs +4 -2
  29. package/src/provider/sse.mjs +112 -0
  30. package/src/skills.mjs +67 -31
  31. package/src/tools/bash.md +8 -0
  32. package/src/tools/codemode.mjs +5 -16
  33. package/src/tools/edit.md +8 -0
  34. package/src/tools/git.mjs +9 -6
  35. package/src/tools/read.md +7 -0
  36. package/src/tools/shared.mjs +43 -2
  37. package/src/tools/system.mjs +14 -10
  38. package/src/tools/web.mjs +21 -16
  39. package/src/tui/agent-turn.mjs +130 -73
  40. package/src/tui/cmd-advisor.mjs +237 -41
  41. package/src/tui/cmd-auto.mjs +6 -8
  42. package/src/tui/cmd-mcp.mjs +4 -2
  43. package/src/tui/cmd-plan.mjs +6 -8
  44. package/src/tui/cmd-think.mjs +93 -70
  45. package/src/tui/index.mjs +9 -188
  46. package/src/tui/interaction.mjs +2 -1
  47. package/src/tui/key-handler.mjs +8 -4
  48. package/src/tui/layout.mjs +3 -2
  49. package/src/tui/render-conversation.mjs +92 -0
  50. package/src/tui/render-frame.mjs +94 -168
  51. package/src/tui/render-loop.mjs +110 -0
@@ -1,56 +1,252 @@
1
- /** /advisor command: toggle advisor on/off, select model.
2
- * ctx: { agent, showPicker, pushLine } */
3
- import { C } from "./ansi.mjs"
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 entries = [
13
- { type: "item", text: `Advisor: ${enabled ? "ON" : "OFF"}`, action: "toggle" },
14
- { type: "item", text: `Model: ${curProvider}/${curModel}`, action: "model" },
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
- const e = await showPicker("Advisor", entries)
18
- if (!e) return
19
- if (e.action === "toggle") {
20
- cfg.enabled = !cfg.enabled
21
- agent._pendingReminders = agent._pendingReminders ?? []
22
- if (cfg.enabled) {
23
- agent._pendingReminders.push("[System reminder: Advisor review is now ON. After each turn your output will be reviewed, and observations may be injected as system reminders. Treat them critically — they are observations, not commands.]")
24
- } else {
25
- agent._pendingReminders.push("[System reminder: Advisor review is now OFF. Future turns will not be reviewed automatically.]")
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
- async function openAdvisorModelPicker(ctx) {
33
- const { agent, showPicker, pushLine } = ctx
34
- const providers = agent.providers || []
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
- // Build flat list: each provider's name + a "use current model" entry
190
+ function buildModelEntries(agent, cfg, cache) {
37
191
  const entries = []
38
- for (const p of providers) {
39
- const mark = p.name === agent.activeProvider ? "* " : " "
40
- entries.push({ type: "item", text: `${mark}${p.name} ${p.baseURL}`, action: "set_provider", provider: p.name, model: p.model })
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
- const e = await showPicker("Advisor Model", entries)
44
- if (e?.action !== "set_provider") return
45
- const cfg = agent.config.advisor ??= {}
46
- if (e.provider === agent.activeProvider && e.model === agent.provider.model) {
47
- // Same as main — clear override (use main pool)
48
- delete cfg.provider
49
- delete cfg.model
50
- pushLine(`Advisor: 使用主模型 (${agent.activeProvider}/${agent.provider.model})`, C.dim)
51
- } else {
52
- cfg.provider = e.provider
53
- cfg.model = e.model
54
- pushLine(`Advisor: ${e.provider}/${e.model}`, C.dim)
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
  }
@@ -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
- agent._pendingReminders = agent._pendingReminders ?? []
7
- if (agent.autoApprove) {
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
  }
@@ -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 addAndConnect(ctx, { name, command: cmd, args: cmdArgs })
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)
@@ -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
- agent._pendingReminders = agent._pendingReminders ?? []
7
- if (agent.planMode) {
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
  }
@@ -1,68 +1,27 @@
1
- import { C } from "./ansi.mjs"
2
-
3
1
  /** /think command: toggle thinking mode, set reasoning effort.
4
- * Extracted from slash-commands.mjs.
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 apply({ action: sub })
65
- pushLine(`Thinking: ${sub}`, C.dim)
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 apply({ action: "effort", level })
76
- pushLine(`Thinking effort: ${level}`, C.dim)
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
- // "enabled" when thinking.type matches the model's enabled value, or when thinking is absent and the model is NOT a custom-think model (defaults to on for standard models)
82
- const thinkingEnabled = cur.thinking?.type === thinkOnValue || (cur.thinking?.type === undefined && !isCustomThink)
83
- const entries = []
84
- // Auto-think: classify difficulty per-prompt and auto-set reasoning effort
85
- entries.push({ type: "item", text: `Auto: ${autoThinkEnabled ? "ON" : "OFF"}`, action: "auto" })
86
- if (!isEffortOnly) {
87
- if (!autoThinkEnabled) entries.push({ type: "item", text: `Thinking: ${thinkingEnabled ? "ON" : "OFF"}`, action: thinkingEnabled ? "off" : "on" })
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
- if (spec.reasoningEffortEnum && !autoThinkEnabled) {
90
- for (const level of spec.reasoningEffortEnum) {
91
- const mark = cur.reasoningEffort === level ? "▸ " : " "
92
- entries.push({ type: "item", text: `${mark}effort: ${level}`, action: "effort", level })
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
  }