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.
@@ -2,7 +2,7 @@ import { ansi, C } from "./ansi.mjs"
2
2
 
3
3
  /** /mcp command handler: view/add/remove/reconnect MCP server.
4
4
  * Extracted from slash-commands.mjs, includes /mcp-specific parseHeaders / addAndConnect helpers.
5
- * ctx: { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw, ansi, C } */
5
+ * ctx: { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, ansi, C } */
6
6
 
7
7
  function parseHeaders(pairs) {
8
8
  const headers = {}
@@ -41,106 +41,62 @@ async function addAndConnect(ctx, srv) {
41
41
  }
42
42
  }
43
43
 
44
- export async function handleMcpCommand(ctx) {
45
- const { agent, pushLine, pushLabel, openPicker, askQuestion, persistRaw } = ctx
46
- const servers = agent.config?.mcp?.servers ?? []
47
- const entries = [
48
- { type: "header", text: `${servers.length} MCP servers configured` },
49
- { type: "item", text: "View list", action: "list" },
50
- { type: "item", text: "Add server", action: "add" },
51
- ]
52
- if (servers.length > 0) {
53
- entries.push(
54
- { type: "item", text: "Remove server", action: "remove" },
55
- { type: "item", text: "Reconnect server", action: "connect" },
56
- )
44
+ export async function handleMcpCommand(ctx, args = []) {
45
+ const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw } = ctx
46
+ // 每轮重读:原本无 mcp 配置时 `?? []` 会拿到游离数组,Add server 后快照过期
47
+ const getServers = () => agent.config?.mcp?.servers ?? []
48
+
49
+ function listServers() {
50
+ const servers = getServers()
51
+ pushLabel(`❯ MCP Servers`, ansi.bold + C.tool)
52
+ if (servers.length === 0) {
53
+ pushLine(" (no MCP server configured)", C.dim)
54
+ }
55
+ for (const srv of servers) {
56
+ const connected = agent.tools.some((t) => t._mcpName === srv.name)
57
+ const mark = connected ? "●" : "○"
58
+ const color = connected ? C.tool : C.dim
59
+ const toolCount = agent.tools.filter((t) => t._mcpName === srv.name).length
60
+ const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
61
+ pushLine(` ${mark} ${srv.name} (${desc})${connected ? ` — ${toolCount} tools` : ""}`, color)
62
+ }
63
+ }
64
+
65
+ async function removeServer(name) {
66
+ agent.config.mcp.servers = getServers().filter((s) => s.name !== name)
67
+ await persistRaw((raw) => { raw.mcp.servers = agent.config.mcp.servers })
68
+ // Remove from tool list
69
+ const { removeMcpTools } = await import("../mcp.mjs")
70
+ removeMcpTools(agent, name)
71
+ pushLine(`[mcp] ${name} removed`, C.tool)
72
+ }
73
+
74
+ async function connectServer(name) {
75
+ const srv = getServers().find((s) => s.name === name)
76
+ const { removeMcpTools, connectMcpServer } = await import("../mcp.mjs")
77
+ removeMcpTools(agent, name)
78
+ try {
79
+ pushLine(`[mcp] Reconnecting ${name}...`, C.dim)
80
+ const tools = await connectMcpServer(srv)
81
+ agent.tools.push(...tools)
82
+ pushLabel(`❯ MCP`, ansi.bold + C.tool)
83
+ pushLine(`${name} reconnected, ${tools.length} tools available.`, C.tool)
84
+ } catch (error) {
85
+ pushLine(`[mcp] ${name}: ${error.message}`, C.error)
86
+ }
57
87
  }
58
- openPicker({
59
- title: "MCP",
60
- entries,
61
- onSelect: async (e) => {
62
- if (e.action === "list") {
63
- pushLabel(`❯ MCP Servers`, ansi.bold + C.tool)
64
- if (servers.length === 0) {
65
- pushLine(" (no MCP server configured)", C.dim)
66
- }
67
- for (const srv of servers) {
68
- const connected = agent.tools.some((t) => t._mcpName === srv.name)
69
- const mark = connected ? "●" : "○"
70
- const color = connected ? C.tool : C.dim
71
- const toolCount = agent.tools.filter((t) => t._mcpName === srv.name).length
72
- const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
73
- pushLine(` ${mark} ${srv.name} (${desc})${connected ? ` — ${toolCount} tools` : ""}`, color)
74
- }
75
- return
76
- }
77
- if (e.action === "remove") {
78
- const removeEntries = [
79
- { type: "header", text: "Select server to remove" },
80
- ...servers.map((s) => ({ type: "item", text: `${s.name} (${s.wsUrl ?? s.url ?? s.command})`, name: s.name })),
81
- ]
82
- openPicker({
83
- title: "Remove MCP Server",
84
- entries: removeEntries,
85
- onSelect: async (se) => {
86
- // Remove from config
87
- agent.config.mcp.servers = servers.filter((s) => s.name !== se.name)
88
- await persistRaw((raw) => { raw.mcp.servers = agent.config.mcp.servers })
89
- // Remove from tool list
90
- const { removeMcpTools } = await import("../mcp.mjs")
91
- removeMcpTools(agent, se.name)
92
- pushLine(`[mcp] ${se.name} removed`, C.tool)
93
- },
94
- })
95
- return
96
- }
97
- if (e.action === "connect") {
98
- const connectEntries = [
99
- { type: "header", text: "Select server to reconnect" },
100
- ...servers.map((s) => ({ type: "item", text: `${s.name} (${s.wsUrl ?? s.url ?? s.command})`, name: s.name })),
101
- ]
102
- openPicker({
103
- title: "Reconnect MCP",
104
- entries: connectEntries,
105
- onSelect: async (se) => {
106
- const srv = servers.find((s) => s.name === se.name)
107
- const { removeMcpTools, connectMcpServer } = await import("../mcp.mjs")
108
- removeMcpTools(agent, se.name)
109
- try {
110
- pushLine(`[mcp] Reconnecting ${se.name}...`, C.dim)
111
- const tools = await connectMcpServer(srv)
112
- agent.tools.push(...tools)
113
- pushLabel(`❯ MCP`, ansi.bold + C.tool)
114
- pushLine(`${se.name} reconnected, ${tools.length} tools available.`, C.tool)
115
- } catch (error) {
116
- pushLine(`[mcp] ${se.name}: ${error.message}`, C.error)
117
- }
118
- },
119
- })
120
- return
121
- }
122
- if (e.action === "add") {
123
- // Pick transport type first, then ask name + URL/command
124
- openPicker({
125
- title: "MCP Transport",
126
- entries: [
127
- { type: "header", text: "Select transport or use AI assist" },
128
- { type: "item", text: "🤖 Describe with AI — natural language → config", action: "ai" },
129
- { type: "item", text: "HTTP (https://…)", action: "http" },
130
- { type: "item", text: "WebSocket (ws://…)", action: "ws" },
131
- { type: "item", text: "stdio (local command)", action: "stdio" },
132
- ],
133
- onSelect: async (te) => {
134
- if (te.action === "ai") {
135
- const description = await askQuestion("Describe the MCP server you want to add (e.g. 'a filesystem server that gives access to /tmp'):")
136
- if (!description) return
137
- pushLine("[mcp] Generating config from description...", C.dim)
138
- try {
139
- const { chat } = await import("../provider/index.mjs")
140
- const res = await chat(agent.provider, {
141
- messages: [{
142
- role: "user",
143
- content: `Generate an MCP server configuration JSON from this description. Return ONLY the JSON object, no explanation.
88
+
89
+ async function addWithTransport(transport) {
90
+ if (transport === "ai") {
91
+ const description = await askQuestion("Describe the MCP server you want to add (e.g. 'a filesystem server that gives access to /tmp'):")
92
+ if (!description) return
93
+ pushLine("[mcp] Generating config from description...", C.dim)
94
+ try {
95
+ const { chat } = await import("../provider/index.mjs")
96
+ const res = await chat(agent.provider, {
97
+ messages: [{
98
+ role: "user",
99
+ content: `Generate an MCP server configuration JSON from this description. Return ONLY the JSON object, no explanation.
144
100
 
145
101
  Description: "${description}"
146
102
 
@@ -153,48 +109,128 @@ Example HTTP: {"name":"filesystem","url":"https://example.com/mcp","headers":{"A
153
109
  Example stdio: {"name":"filesystem","command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/tmp"]}
154
110
 
155
111
  Return ONLY the JSON object:`,
156
- }],
157
- tools: [],
158
- signal: AbortSignal.timeout(15_000),
159
- })
160
- const jsonMatch = (res.content ?? "").match(/\{[\s\S]*\}/)
161
- if (!jsonMatch) { pushLine("[mcp] AI response not valid JSON", C.error); return }
162
- const srv = JSON.parse(jsonMatch[0])
163
- if (!srv.name) { pushLine("[mcp] AI response missing 'name' field", C.error); return }
164
- // Show preview and confirm
165
- pushLine(`[mcp] Generated config: ${JSON.stringify(srv)}`, C.tool)
166
- const confirm = await askQuestion("Add this server? (y/n):")
167
- if (confirm?.toLowerCase() !== "y") { pushLine("[mcp] Cancelled", C.dim); return }
168
- await addAndConnect(ctx, srv)
169
- } catch (err) {
170
- pushLine(`[mcp] AI generation failed: ${err.message}`, C.error)
171
- }
172
- return
173
- }
174
- const name = await askQuestion("Server name:")
175
- if (!name) return
176
- const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
177
- if (existing) { pushLine(`[mcp] "${name}" already exists`, C.error); return }
178
- if (te.action === "stdio") {
179
- const cmd = await askQuestion("Command (e.g. npx, python):")
180
- if (!cmd) return
181
- const argsInput = await askQuestion("Arguments (space-separated, or leave empty):")
182
- const args = argsInput ? argsInput.split(/\s+/) : undefined
183
- await addAndConnect(ctx, { name, command: cmd, args })
184
- } else {
185
- const urlPrompt = te.action === "ws" ? "WebSocket URL (ws://…):" : "HTTP URL (https://…):"
186
- const url = await askQuestion(urlPrompt)
187
- if (!url) return
188
- const headersInput = await askQuestion("Headers (key=value, space-separated, or leave empty):")
189
- const headers = headersInput ? parseHeaders(headersInput.split(/\s+/)) : undefined
190
- const srv = te.action === "ws"
191
- ? { name, wsUrl: url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
192
- : { name, url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
193
- await addAndConnect(ctx, srv)
194
- }
195
- },
112
+ }],
113
+ tools: [],
114
+ signal: AbortSignal.timeout(15_000),
196
115
  })
116
+ const jsonMatch = (res.content ?? "").match(/\{[\s\S]*\}/)
117
+ if (!jsonMatch) { pushLine("[mcp] AI response not valid JSON", C.error); return }
118
+ const srv = JSON.parse(jsonMatch[0])
119
+ if (!srv.name) { pushLine("[mcp] AI response missing 'name' field", C.error); return }
120
+ // Show preview and confirm
121
+ pushLine(`[mcp] Generated config: ${JSON.stringify(srv)}`, C.tool)
122
+ const confirm = await askQuestion("Add this server? (y/n):")
123
+ if (confirm?.toLowerCase() !== "y") { pushLine("[mcp] Cancelled", C.dim); return }
124
+ await addAndConnect(ctx, srv)
125
+ } catch (err) {
126
+ pushLine(`[mcp] AI generation failed: ${err.message}`, C.error)
197
127
  }
198
- },
199
- })
128
+ return
129
+ }
130
+ const name = await askQuestion("Server name:")
131
+ if (!name) return
132
+ const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
133
+ if (existing) { pushLine(`[mcp] "${name}" already exists`, C.error); return }
134
+ if (transport === "stdio") {
135
+ const cmd = await askQuestion("Command (e.g. npx, python):")
136
+ if (!cmd) return
137
+ const argsInput = await askQuestion("Arguments (space-separated, or leave empty):")
138
+ const cmdArgs = argsInput ? argsInput.split(/\s+/) : undefined
139
+ await addAndConnect(ctx, { name, command: cmd, args: cmdArgs })
140
+ } else {
141
+ const urlPrompt = transport === "ws" ? "WebSocket URL (ws://…):" : "HTTP URL (https://…):"
142
+ const url = await askQuestion(urlPrompt)
143
+ if (!url) return
144
+ const headersInput = await askQuestion("Headers (key=value, space-separated, or leave empty):")
145
+ const headers = headersInput ? parseHeaders(headersInput.split(/\s+/)) : undefined
146
+ const srv = transport === "ws"
147
+ ? { name, wsUrl: url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
148
+ : { name, url, headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined }
149
+ await addAndConnect(ctx, srv)
150
+ }
151
+ }
152
+
153
+ async function addFlow() {
154
+ // Pick transport type first, then ask name + URL/command
155
+ const te = await showPicker("MCP Transport", [
156
+ { type: "header", text: "Select transport or use AI assist" },
157
+ { type: "item", text: "🤖 Describe with AI — natural language → config", action: "ai" },
158
+ { type: "item", text: "HTTP (https://…)", action: "http" },
159
+ { type: "item", text: "WebSocket (ws://…)", action: "ws" },
160
+ { type: "item", text: "stdio (local command)", action: "stdio" },
161
+ ])
162
+ if (te) await addWithTransport(te.action)
163
+ }
164
+
165
+ /** remove/connect 的服务器选择 picker + 执行。返回 true = 已执行;false = Esc 取消。 */
166
+ async function pickAndRun(action) {
167
+ const servers = getServers()
168
+ if (servers.length === 0) {
169
+ pushLine("[mcp] no MCP server configured", C.error)
170
+ return true
171
+ }
172
+ const subEntries = [
173
+ { type: "header", text: action === "remove" ? "Select server to remove" : "Select server to reconnect" },
174
+ ...servers.map((s) => ({ type: "item", text: `${s.name} (${s.wsUrl ?? s.url ?? s.command})`, name: s.name })),
175
+ ]
176
+ const se = await showPicker(action === "remove" ? "Remove MCP Server" : "Reconnect MCP", subEntries)
177
+ if (!se) return false // Esc 取消
178
+ if (action === "remove") await removeServer(se.name)
179
+ else await connectServer(se.name)
180
+ return true
181
+ }
182
+
183
+ // Direct args: /mcp list │ /mcp add │ /mcp http|ws|stdio|ai │ /mcp remove [name] │ /mcp connect [name]
184
+ const sub = args[0]?.toLowerCase()
185
+ if (sub === "list") { listServers(); return }
186
+ if (sub === "add") { await addFlow(); return }
187
+ if (sub === "http" || sub === "ws" || sub === "stdio" || sub === "ai") { await addWithTransport(sub); return }
188
+ if (sub === "remove" || sub === "connect") {
189
+ const name = args[1]
190
+ if (name) {
191
+ const servers = getServers()
192
+ if (!servers.some((s) => s.name === name)) {
193
+ pushLine(`[mcp] no server named "${name}" (${servers.map((s) => s.name).join(", ") || "none configured"})`, C.error)
194
+ return
195
+ }
196
+ if (sub === "remove") await removeServer(name)
197
+ else await connectServer(name)
198
+ return
199
+ }
200
+ // 已明确 remove/connect 意图但没带 name → 直接进服务器选择 picker,不落主菜单
201
+ await pickAndRun(sub)
202
+ return
203
+ } else if (sub) {
204
+ pushLine("Usage: /mcp [list|add|http|ws|stdio|ai|remove [name]|connect [name]]", C.error)
205
+ return
206
+ }
207
+
208
+ // 主菜单循环:选中即关闭,子菜单 Esc 返回主菜单,主菜单 Esc 退出
209
+ for (;;) {
210
+ const servers = getServers()
211
+ const entries = [
212
+ { type: "header", text: `${servers.length} MCP servers configured` },
213
+ { type: "item", text: "View list", action: "list" },
214
+ { type: "item", text: "Add server", action: "add" },
215
+ ]
216
+ if (servers.length > 0) {
217
+ entries.push(
218
+ { type: "item", text: "Remove server", action: "remove" },
219
+ { type: "item", text: "Reconnect server", action: "connect" },
220
+ )
221
+ }
222
+ const e = await showPicker("MCP", entries)
223
+ if (!e) return // Esc 退出
224
+ if (e.action === "list") {
225
+ listServers()
226
+ return
227
+ }
228
+ if (e.action === "add") {
229
+ await addFlow()
230
+ continue
231
+ }
232
+ const done = await pickAndRun(e.action)
233
+ if (!done) continue // 子菜单 Esc → 回主菜单
234
+ return
235
+ }
200
236
  }
@@ -1,7 +1,18 @@
1
1
  import { C } from "./ansi.mjs"
2
2
 
3
- /** /model command: open model picker.
4
- * ctx: { openModelPicker, pushLine } */
5
- export async function handleModelCommand(ctx) {
6
- ctx.openModelPicker().catch((e) => ctx.pushLine(`[error] ${e.message}`, C.error))
3
+ /** /model command: open model picker, or switch provider directly via `/model <provider>`.
4
+ * ctx: { agent, openModelPicker, selectModel, pushLine } */
5
+ export async function handleModelCommand(ctx, args = []) {
6
+ const name = args[0]?.toLowerCase()
7
+ if (!name) {
8
+ ctx.openModelPicker().catch((e) => ctx.pushLine(`[error] ${e.message}`, C.error))
9
+ return
10
+ }
11
+ const target = ctx.agent.providers.find((p) => p.name.toLowerCase() === name)
12
+ if (!target) {
13
+ const available = ctx.agent.providers.map((p) => p.name).join(", ")
14
+ ctx.pushLine(`Unknown provider: ${args[0]} (available: ${available})`, C.error)
15
+ return
16
+ }
17
+ await ctx.selectModel({ provider: target.name, model: target.model }).catch((e) => ctx.pushLine(`[error] ${e.message}`, C.error))
7
18
  }
@@ -2,9 +2,9 @@ import { clearSession } from "../session.mjs"
2
2
  import { C } from "./ansi.mjs"
3
3
 
4
4
  /** /new command: start new session (old session archived to slot).
5
- * ctx: { agent, state, pushLine, openPicker, render } */
5
+ * ctx: { agent, state, pushLine, showPicker, render } */
6
6
  export async function handleNewCommand(ctx) {
7
- const { agent, state, pushLine, openPicker, render } = ctx
7
+ const { agent, state, pushLine, showPicker, render } = ctx
8
8
 
9
9
  const doNewSession = () => {
10
10
  agent.history = []
@@ -21,17 +21,11 @@ export async function handleNewCommand(ctx) {
21
21
  }
22
22
 
23
23
  if (agent.history.length > 0) {
24
- openPicker({
25
- title: "Start new session?",
26
- entries: [
27
- { type: "item", text: "Yes, archive current and start new", action: "yes" },
28
- { type: "item", text: "Cancel", action: "no" },
29
- ],
30
- defaultIndex: 1,
31
- onSelect: (e) => {
32
- if (e.action === "yes") doNewSession()
33
- },
34
- })
24
+ const e = await showPicker("Start new session?", [
25
+ { type: "item", text: "Yes, archive current and start new", action: "yes" },
26
+ { type: "item", text: "Cancel", action: "no" },
27
+ ], { defaultIndex: 1 })
28
+ if (e?.action === "yes") doNewSession()
35
29
  return
36
30
  }
37
31
  doNewSession()
@@ -1,9 +1,9 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
2
 
3
3
  /** /restore command: list git checkpoints and roll back to selected snapshot.
4
- * ctx: { agent, openPicker, pushLine, pushLabel } */
4
+ * ctx: { agent, showPicker, pushLine, pushLabel } */
5
5
  export async function handleRestoreCommand(ctx) {
6
- const { agent, openPicker, pushLine, pushLabel } = ctx
6
+ const { agent, showPicker, pushLine, pushLabel } = ctx
7
7
  const { listCheckpoints, rewind, isGitRepo } = await import("../git/checkpoint.mjs")
8
8
  if (!isGitRepo(agent.cwd)) {
9
9
  pushLine("[rewind] not a git repository, checkpoints unavailable", C.error)
@@ -22,18 +22,14 @@ export async function handleRestoreCommand(ctx) {
22
22
  id: cp.id,
23
23
  })),
24
24
  ]
25
- openPicker({
26
- title: "Restore Checkpoint",
27
- entries,
28
- onSelect: async (e) => {
29
- try {
30
- const summary = await rewind(agent.cwd, e.id)
31
- pushLabel(`❯ Rewind`, ansi.bold + C.warn)
32
- pushLine(`Restored to ${e.id}: patch ${summary.patchApplied ? "applied" : "none"}, deleted ${summary.deleted} new files, restored ${summary.restored} file(s)`, C.tool)
33
- pushLine("(current state saved as new checkpoint; /restore again to go back)", C.dim)
34
- } catch (error) {
35
- pushLine(`[rewind] ${error.message}`, C.error)
36
- }
37
- },
38
- })
25
+ const e = await showPicker("Restore Checkpoint", entries)
26
+ if (!e) return
27
+ try {
28
+ const summary = await rewind(agent.cwd, e.id)
29
+ pushLabel(`❯ Rewind`, ansi.bold + C.warn)
30
+ pushLine(`Restored to ${e.id}: patch ${summary.patchApplied ? "applied" : "none"}, deleted ${summary.deleted} new files, restored ${summary.restored} file(s)`, C.tool)
31
+ pushLine("(current state saved as new checkpoint; /restore again to go back)", C.dim)
32
+ } catch (error) {
33
+ pushLine(`[rewind] ${error.message}`, C.error)
34
+ }
39
35
  }
@@ -2,41 +2,37 @@ import { listSlots, switchToSlot, applySession } from "../session.mjs"
2
2
  import { ansi, C } from "./ansi.mjs"
3
3
 
4
4
  /** /session command: list/switch archived session slots.
5
- * ctx: { agent, state, openPicker, pushLine, pushLabel, render } */
5
+ * ctx: { agent, state, showPicker, pushLine, pushLabel, render } */
6
6
  export async function handleSessionCommand(ctx) {
7
- const { agent, state, openPicker, pushLine, pushLabel, render } = ctx
7
+ const { agent, state, showPicker, pushLine, pushLabel, render } = ctx
8
8
  const slots = listSlots(agent.cwd)
9
9
  if (slots.length === 0) {
10
10
  pushLine("No archived sessions (use /new and old sessions auto-archive to slots)", C.dim)
11
- } else {
12
- const entries = [
13
- { type: "header", text: `Archived sessions (↑↓ select, Enter switch, Esc cancel)` },
14
- ...slots.map((s) => ({
15
- type: "item",
16
- text: `Slot ${s.slot} — ${s.date}`,
17
- slot: s.slot,
18
- })),
19
- ]
20
- openPicker({
21
- title: "Sessions",
22
- entries,
23
- onSelect: (e) => {
24
- const data = switchToSlot(agent.cwd, e.slot)
25
- if (!data) {
26
- pushLine(`Slot ${e.slot} not found`, C.dim)
27
- return
28
- }
29
- applySession(agent, data)
30
- state.lines = data.display.length
31
- ? data.display.map((l) => ({ text: l.text, color: l.color }))
32
- : []
33
- state.tasks = agent.tasks ?? []
34
- if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
35
- state.tasks = []
36
- }
37
- pushLabel(`── Switched to slot ${e.slot} (${data.history.length} messages) ──`, C.warn)
38
- render()
39
- },
40
- })
11
+ return
41
12
  }
13
+ const entries = [
14
+ { type: "header", text: `Archived sessions (↑↓ select, Enter switch, Esc cancel)` },
15
+ ...slots.map((s) => ({
16
+ type: "item",
17
+ text: `Slot ${s.slot} — ${s.date}`,
18
+ slot: s.slot,
19
+ })),
20
+ ]
21
+ const e = await showPicker("Sessions", entries)
22
+ if (!e) return
23
+ const data = switchToSlot(agent.cwd, e.slot)
24
+ if (!data) {
25
+ pushLine(`Slot ${e.slot} not found`, C.dim)
26
+ return
27
+ }
28
+ applySession(agent, data)
29
+ state.lines = data.display.length
30
+ ? data.display.map((l) => ({ text: l.text, color: l.color }))
31
+ : []
32
+ state.tasks = agent.tasks ?? []
33
+ if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
34
+ state.tasks = []
35
+ }
36
+ pushLabel(`── Switched to slot ${e.slot} (${data.history.length} messages) ──`, C.warn)
37
+ render()
42
38
  }
@@ -1,19 +1,87 @@
1
+ import { C } from "./ansi.mjs"
2
+
1
3
  /** /think command: toggle thinking mode, set reasoning effort.
2
4
  * Extracted from slash-commands.mjs.
3
- * ctx: { agent, openPicker, syncProviderField } */
4
- export async function handleThinkCommand(ctx) {
5
- const { agent, openPicker, syncProviderField } = ctx
5
+ * ctx: { agent, showPicker, syncProviderField, pushLine } */
6
+ export async function handleThinkCommand(ctx, args = []) {
7
+ const { agent, showPicker, syncProviderField, pushLine } = ctx
6
8
  const cur = agent.provider
7
9
  const { specForModel } = await import("../config.mjs")
8
10
  const spec = specForModel(cur.model)
9
11
  const isEffortOnly = spec.thinkApi === "effort"
10
- const thinkOnValue = spec.thinkOnValue ?? "enabled"
12
+ const thinkOnValue = spec.thinkEnabledValue ?? "enabled"
11
13
  const isCustomThink = thinkOnValue !== "enabled"
14
+ const effortLevels = spec.reasoningEffortEnum ?? ["high", "max"]
15
+
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
+ const autoThinkEnabled = agent.config?.agent?.autoThink === true
61
+ const sub = args[0]?.toLowerCase()
62
+ if (sub === "on" || sub === "off") {
63
+ 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)
66
+ return
67
+ }
68
+ if (sub === "effort") {
69
+ const level = args[1]?.toLowerCase()
70
+ if (!level || !effortLevels.includes(level)) {
71
+ pushLine(`Usage: /think effort <${effortLevels.join("|")}>`, C.error)
72
+ return
73
+ }
74
+ 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)
77
+ return
78
+ }
79
+ if (sub) { pushLine("Usage: /think [on|off|effort <level>]", C.error); return }
80
+
12
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)
13
82
  const thinkingEnabled = cur.thinking?.type === thinkOnValue || (cur.thinking?.type === undefined && !isCustomThink)
14
83
  const entries = []
15
84
  // Auto-think: classify difficulty per-prompt and auto-set reasoning effort
16
- const autoThinkEnabled = agent.config?.agent?.autoThink === true
17
85
  entries.push({ type: "item", text: `Auto: ${autoThinkEnabled ? "ON" : "OFF"}`, action: "auto" })
18
86
  if (!isEffortOnly) {
19
87
  if (!autoThinkEnabled) entries.push({ type: "item", text: `Thinking: ${thinkingEnabled ? "ON" : "OFF"}`, action: thinkingEnabled ? "off" : "on" })
@@ -27,49 +95,6 @@ export async function handleThinkCommand(ctx) {
27
95
  entries.push({ type: "item", text: "effort: high", action: "effort", level: "high" })
28
96
  entries.push({ type: "item", text: "effort: max", action: "effort", level: "max" })
29
97
  }
30
- openPicker({
31
- title: "Think",
32
- entries,
33
- onSelect: async (e) => {
34
- if (e.action === "auto") {
35
- const cfg = agent.config.agent ??= {}
36
- cfg.autoThink = !cfg.autoThink
37
- agent._pendingReminders = agent._pendingReminders ?? []
38
- if (cfg.autoThink) {
39
- // Turn off manual effort — auto will set it per-turn
40
- delete cur.reasoningEffort
41
- await syncProviderField("reasoningEffort", undefined)
42
- agent._pendingReminders.push("[System reminder: Auto-think is now ON. Reasoning effort will be automatically set per-task based on difficulty classification.]")
43
- } else {
44
- agent._pendingReminders.push("[System reminder: Auto-think is now OFF. Reasoning effort will remain at its current manual setting.]")
45
- }
46
- } else if (e.action === "effort") {
47
- cur.reasoningEffort = e.level
48
- await syncProviderField("reasoningEffort", e.level)
49
- } else {
50
- const enable = e.action === "on"
51
- if (isEffortOnly) {
52
- if (!enable) delete cur.reasoningEffort
53
- else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
54
- if (!enable) await syncProviderField("reasoningEffort", undefined)
55
- else await syncProviderField("reasoningEffort", cur.reasoningEffort)
56
- } else {
57
- if (enable) {
58
- cur.thinking = { type: thinkOnValue }
59
- if (!cur.reasoningEffort) cur.reasoningEffort = "high"
60
- } else {
61
- // Custom-think models (MiniMax "adaptive") don't support "disabled" — remove the field instead
62
- cur.thinking = isCustomThink ? undefined : { type: "disabled" }
63
- delete cur.reasoningEffort
64
- }
65
- await syncProviderField("thinking", cur.thinking)
66
- if (enable) {
67
- await syncProviderField("reasoningEffort", cur.reasoningEffort)
68
- } else {
69
- await syncProviderField("reasoningEffort", undefined)
70
- }
71
- }
72
- }
73
- },
74
- })
98
+ const e = await showPicker("Think", entries)
99
+ if (e) await apply(e)
75
100
  }