thincoder 0.9.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.
Files changed (42) hide show
  1. package/README.md +6 -1
  2. package/package.json +1 -1
  3. package/src/cli/make-agent.mjs +7 -0
  4. package/src/config.mjs +46 -19
  5. package/src/context.mjs +18 -0
  6. package/src/prompts/coder.md +5 -2
  7. package/src/prompts/discipline.md +8 -5
  8. package/src/prompts/system.md +8 -5
  9. package/src/provider/anthropic.mjs +190 -0
  10. package/src/provider/core.mjs +36 -4
  11. package/src/provider/google.mjs +197 -0
  12. package/src/proxy.mjs +236 -0
  13. package/src/tools/codemode.mjs +178 -0
  14. package/src/tools/fetch.md +2 -1
  15. package/src/tools/git.mjs +120 -154
  16. package/src/tools/index.mjs +11 -9
  17. package/src/tools/linter.mjs +46 -32
  18. package/src/tools/lsp.mjs +317 -0
  19. package/src/tools/web.mjs +103 -82
  20. package/src/tools/websearch.md +5 -3
  21. package/src/tui/agent-turn.mjs +12 -13
  22. package/src/tui/cmd-advisor.mjs +29 -41
  23. package/src/tui/cmd-clear.mjs +11 -17
  24. package/src/tui/cmd-config.mjs +226 -142
  25. package/src/tui/cmd-extract.mjs +1 -1
  26. package/src/tui/cmd-fold.mjs +2 -3
  27. package/src/tui/cmd-goal.mjs +58 -27
  28. package/src/tui/cmd-help.mjs +3 -1
  29. package/src/tui/cmd-mcp.mjs +178 -142
  30. package/src/tui/cmd-model.mjs +15 -4
  31. package/src/tui/cmd-new.mjs +7 -13
  32. package/src/tui/cmd-restore.mjs +12 -16
  33. package/src/tui/cmd-session.mjs +28 -32
  34. package/src/tui/cmd-think.mjs +75 -50
  35. package/src/tui/cmd-undo.mjs +19 -23
  36. package/src/tui/cmd-upgrade.mjs +22 -26
  37. package/src/tui/index.mjs +64 -38
  38. package/src/tui/key-handler.mjs +48 -18
  39. package/src/tui/layout.mjs +12 -2
  40. package/src/tui/pickers.mjs +151 -182
  41. package/src/tui/render-frame.mjs +29 -11
  42. package/src/tui/slash-commands.mjs +26 -16
@@ -1,10 +1,12 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
+ import { SLASH_ALIASES } from "./slash-commands.mjs"
2
3
 
3
4
  /** /help command: list all slash commands and aliases.
4
5
  * ctx: { pushLine, pushLabel, SLASH_COMMANDS } */
5
6
  export async function handleHelpCommand(ctx) {
6
7
  const { pushLine, pushLabel, SLASH_COMMANDS } = ctx
7
- const aliasList = { "/help": "/h", "/exit": "/x", "/model": "/m", "/plan": "/p", "/think": "/t", "/clear": "/c", "/new": "/n" }
8
+ // reverse the shared alias table: command alias
9
+ const aliasList = Object.fromEntries(Object.entries(SLASH_ALIASES).map(([alias, cmd]) => [cmd, alias]))
8
10
  const order = ["Agent", "Session", "Project", "System"]
9
11
  const byGroup = new Map()
10
12
  for (const c of SLASH_COMMANDS) {
@@ -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
  }