thincoder 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/advisor.mjs +360 -72
- package/src/agent/helpers.mjs +7 -3
- 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 +73 -21
- package/src/auto-think.mjs +23 -5
- package/src/cli/make-agent.mjs +7 -0
- package/src/config.mjs +47 -20
- 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 +6 -2
- package/src/prompts/discipline.md +23 -6
- package/src/prompts/explore.md +2 -0
- package/src/prompts/plan.md +2 -0
- package/src/prompts/system.md +13 -6
- package/src/provider/anthropic.mjs +190 -0
- package/src/provider/core.mjs +42 -130
- package/src/provider/google.mjs +199 -0
- package/src/provider/sse.mjs +112 -0
- package/src/proxy.mjs +236 -0
- package/src/tools/bash.md +8 -0
- package/src/tools/codemode.mjs +5 -16
- package/src/tools/edit.md +8 -0
- package/src/tools/fetch.md +2 -1
- package/src/tools/git.mjs +125 -156
- package/src/tools/index.mjs +9 -9
- package/src/tools/linter.mjs +46 -32
- package/src/tools/read.md +7 -0
- package/src/tools/shared.mjs +16 -0
- package/src/tools/system.mjs +14 -10
- package/src/tools/web.mjs +115 -89
- package/src/tools/websearch.md +5 -3
- package/src/tui/agent-turn.mjs +86 -75
- package/src/tui/cmd-advisor.mjs +138 -49
- package/src/tui/cmd-clear.mjs +11 -17
- package/src/tui/cmd-config.mjs +226 -142
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-fold.mjs +2 -3
- package/src/tui/cmd-goal.mjs +58 -27
- package/src/tui/cmd-help.mjs +3 -1
- package/src/tui/cmd-mcp.mjs +178 -142
- package/src/tui/cmd-model.mjs +15 -4
- package/src/tui/cmd-new.mjs +7 -13
- package/src/tui/cmd-restore.mjs +12 -16
- package/src/tui/cmd-session.mjs +28 -32
- package/src/tui/cmd-think.mjs +75 -50
- package/src/tui/cmd-undo.mjs +19 -23
- package/src/tui/cmd-upgrade.mjs +22 -26
- package/src/tui/index.mjs +61 -215
- package/src/tui/key-handler.mjs +56 -20
- package/src/tui/layout.mjs +13 -3
- package/src/tui/pickers.mjs +151 -182
- package/src/tui/render-conversation.mjs +92 -0
- package/src/tui/render-frame.mjs +56 -114
- package/src/tui/render-loop.mjs +181 -0
- package/src/tui/slash-commands.mjs +26 -16
package/src/tools/web.mjs
CHANGED
|
@@ -1,11 +1,60 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { DESC, truncate, stripTags, htmlToText, isPrivateHost } from "./shared.mjs";
|
|
2
|
+
import { URL } from "node:url";
|
|
3
|
+
import { resolveWebProxy, proxyFetch } from "../proxy.mjs";
|
|
4
|
+
|
|
5
|
+
export const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
|
6
|
+
const FETCH_TIMEOUT = 15_000
|
|
7
|
+
|
|
8
|
+
// ── Web search (Bing; direct by default, through proxy when configured and web toggle on) ──
|
|
9
|
+
|
|
10
|
+
function extractBing(html) {
|
|
11
|
+
const results = []
|
|
12
|
+
// RSS format: <item><title>..</title><link>..</link><description>..</description></item>
|
|
13
|
+
for (const m of html.matchAll(/<item>([\s\S]*?)<\/item>/gi)) {
|
|
14
|
+
const block = m[1]
|
|
15
|
+
const title = block.match(/<title>([\s\S]*?)<\/title>/)?.[1] ?? ""
|
|
16
|
+
const href = block.match(/<link>([\s\S]*?)<\/link>/)?.[1] ?? ""
|
|
17
|
+
const snippet = block.match(/<description>([\s\S]*?)<\/description>/)?.[1] ?? ""
|
|
18
|
+
if (!href) continue
|
|
19
|
+
results.push({ href, title: stripTags(title), snippet: stripTags(snippet) })
|
|
20
|
+
}
|
|
21
|
+
// Fallback: HTML b_algo blocks (older server-rendered pages)
|
|
22
|
+
if (results.length === 0) {
|
|
23
|
+
const blocks = html.split('<li class="b_algo"').slice(1)
|
|
24
|
+
for (const block of blocks) {
|
|
25
|
+
const link = block.match(/<h2[^>]*><a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/)
|
|
26
|
+
if (!link) continue
|
|
27
|
+
const snippet = block.match(/<p[^>]*>([\s\S]*?)<\/p>/)
|
|
28
|
+
results.push({ href: link[1], title: stripTags(link[2]), snippet: snippet ? stripTags(snippet[1]) : "" })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return results
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function bingUrl(query, page) {
|
|
35
|
+
let u = `https://www.bing.com/search?q=${encodeURIComponent(query)}&format=rss&setlang=en`
|
|
36
|
+
if (page > 1) u += `&first=${(page - 1) * 10 + 1}`
|
|
37
|
+
return u
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ENGINES = [{ name: "bing", label: "Bing", url: bingUrl, extract: extractBing, ua: UA }]
|
|
41
|
+
const ENGINE_NAMES = ENGINES.map(e => e.name)
|
|
42
|
+
|
|
43
|
+
async function fetchEngine(engine, query, page, ctx) {
|
|
44
|
+
const ctrl = new AbortController()
|
|
45
|
+
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT)
|
|
46
|
+
try {
|
|
47
|
+
const response = await proxyFetch(engine.url(query, page), {
|
|
48
|
+
headers: { "User-Agent": engine.ua, "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8" },
|
|
49
|
+
signal: ctrl.signal,
|
|
50
|
+
}, resolveWebProxy(ctx))
|
|
51
|
+
if (!response.ok) return null
|
|
52
|
+
const html = await response.text()
|
|
53
|
+
const results = engine.extract(html)
|
|
54
|
+
return { engine: engine.name, results }
|
|
55
|
+
} catch { return null }
|
|
56
|
+
finally { clearTimeout(timer) }
|
|
57
|
+
}
|
|
9
58
|
|
|
10
59
|
export const websearchTool = {
|
|
11
60
|
name: "websearch",
|
|
@@ -14,108 +63,85 @@ export const websearchTool = {
|
|
|
14
63
|
type: "object",
|
|
15
64
|
properties: {
|
|
16
65
|
query: { type: "string", description: "Search query" },
|
|
17
|
-
limit: { type: "number", description: "Max results (default 8)" },
|
|
66
|
+
limit: { type: "number", description: "Max results (default 8, max 20)" },
|
|
67
|
+
engine: { type: "string", enum: ENGINE_NAMES, description: "Specific engine — \"bing\" (Bing). Omit to search all engines concurrently." },
|
|
68
|
+
page: { type: "number", description: "Page number for pagination (1-based, default 1). Only used when engine is specified." },
|
|
18
69
|
},
|
|
19
70
|
required: ["query"],
|
|
20
71
|
},
|
|
21
72
|
readonly: true,
|
|
22
73
|
async execute(args, ctx) {
|
|
23
|
-
const limit = args.limit ?? 8
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
: AbortSignal.timeout(15_000),
|
|
32
|
-
})
|
|
33
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
34
|
-
html = await readBodyText(response)
|
|
35
|
-
} catch (error) {
|
|
36
|
-
throw new Error(`websearch request failed: ${error.cause?.code ?? error.message}`)
|
|
74
|
+
const limit = Math.min(args.limit ?? 8, 20)
|
|
75
|
+
const page = Math.max(1, args.page ?? 1)
|
|
76
|
+
if (args.engine) {
|
|
77
|
+
const engine = ENGINES.find(e => e.name === args.engine)
|
|
78
|
+
if (!engine) return `Unknown engine '${args.engine}'. Available: ${ENGINE_NAMES.join(", ")}`
|
|
79
|
+
const fetched = await fetchEngine(engine, args.query, page, ctx)
|
|
80
|
+
if (!fetched || fetched.results.length === 0) return `(no results from ${engine.label})`
|
|
81
|
+
return truncate(fetched.results.slice(0, limit).map((r, i) => `${i + 1}. ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
|
|
37
82
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
83
|
+
const promises = ENGINES.map(e => fetchEngine(e, args.query, 1, ctx))
|
|
84
|
+
const fetched = (await Promise.all(promises)).filter(Boolean)
|
|
85
|
+
if (fetched.length === 0) return "(no results — all search engines failed)"
|
|
86
|
+
const merged = [], indexes = fetched.map(() => 0)
|
|
87
|
+
let done = false
|
|
88
|
+
while (!done && merged.length < limit) {
|
|
89
|
+
done = true
|
|
90
|
+
for (let i = 0; i < fetched.length; i++) {
|
|
91
|
+
if (indexes[i] < fetched[i].results.length) {
|
|
92
|
+
merged.push({ ...fetched[i].results[indexes[i]], _engine: fetched[i].engine })
|
|
93
|
+
indexes[i]++; done = false
|
|
94
|
+
if (merged.length >= limit) break
|
|
95
|
+
}
|
|
96
|
+
}
|
|
52
97
|
}
|
|
53
|
-
|
|
54
|
-
return truncate(
|
|
55
|
-
results.map((r, i) => `${i + 1}. ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"),
|
|
56
|
-
)
|
|
98
|
+
return truncate(merged.slice(0, limit).map((r, i) => `${i + 1}. [${r._engine}] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
|
|
57
99
|
},
|
|
58
100
|
}
|
|
59
101
|
|
|
102
|
+
// ── Fetch tool (with proxy support) ──────
|
|
60
103
|
|
|
61
|
-
// ---------------------------------------------------------------- ls
|
|
62
|
-
|
|
63
|
-
/** SSRF protection: block internal private-network/metadata endpoints (localhost allowed — user's dev server, tests depend on it) */
|
|
64
104
|
function isPrivateUrl(urlStr) {
|
|
65
|
-
let u
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const [a, b] = [Number(m[1]), Number(m[2])]
|
|
77
|
-
if (a === 10) return true
|
|
78
|
-
if (a === 172 && b >= 16 && b <= 31) return true
|
|
79
|
-
if (a === 192 && b === 168) return true
|
|
80
|
-
if (a === 169 && b === 254) return true
|
|
81
|
-
if (a === 0) return true
|
|
82
|
-
}
|
|
83
|
-
// IPv6 loopback / link-local / unique local addresses
|
|
84
|
-
if (host === "::1" || host === "fe80::1" || host.startsWith("fc") || host.startsWith("fd")) return true
|
|
85
|
-
return false
|
|
105
|
+
let u; try { u = new URL(urlStr) } catch { return true }
|
|
106
|
+
return isPrivateHost(u.hostname)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// proxyFetch returns a native Response (Headers object, needs .get()) without proxy,
|
|
110
|
+
// but a Response-like with a plain lowercase-keyed Record through the CONNECT tunnel — handle both.
|
|
111
|
+
function headerOf(res, name) {
|
|
112
|
+
const h = res.headers
|
|
113
|
+
if (!h) return null
|
|
114
|
+
if (typeof h.get === "function") return h.get(name)
|
|
115
|
+
return h[name.toLowerCase()] ?? null
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
export const fetchTool = {
|
|
89
119
|
name: "fetch",
|
|
90
120
|
description: DESC("fetch"),
|
|
91
|
-
parameters: {
|
|
92
|
-
type: "object",
|
|
93
|
-
properties: {
|
|
94
|
-
url: { type: "string", description: "http/https URL" },
|
|
95
|
-
},
|
|
96
|
-
required: ["url"],
|
|
97
|
-
},
|
|
121
|
+
parameters: { type: "object", properties: { url: { type: "string", description: "http/https URL" } }, required: ["url"] },
|
|
98
122
|
readonly: true,
|
|
99
123
|
async execute(args, ctx) {
|
|
100
124
|
if (!/^https?:\/\//.test(args.url)) throw new Error("url must start with http:// or https://")
|
|
101
125
|
if (isPrivateUrl(args.url)) throw new Error("fetch blocked: internal/private/metadata addresses are not allowed")
|
|
102
|
-
let response
|
|
103
126
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
const proxyUri = resolveWebProxy(ctx)
|
|
128
|
+
const response = await proxyFetch(args.url, { headers: { "User-Agent": UA } }, proxyUri)
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
if ([301, 302, 307, 308].includes(response.status)) {
|
|
131
|
+
const loc = headerOf(response, "location")
|
|
132
|
+
if (loc) {
|
|
133
|
+
const r2 = await proxyFetch(loc, { headers: { "User-Agent": UA } }, proxyUri)
|
|
134
|
+
if (!r2.ok) throw new Error(`fetch failed: HTTP ${r2.status}`)
|
|
135
|
+
const ct2 = headerOf(r2, "content-type") ?? ""
|
|
136
|
+
const b2 = await r2.text()
|
|
137
|
+
return ct2.includes("text/html") ? truncate(htmlToText(b2)) : truncate(b2)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`fetch failed: HTTP ${response.status}`)
|
|
141
|
+
}
|
|
142
|
+
const ct = headerOf(response, "content-type") ?? ""
|
|
143
|
+
const body = await response.text()
|
|
144
|
+
return ct.includes("text/html") ? truncate(htmlToText(body)) : truncate(body)
|
|
145
|
+
} catch (e) { throw new Error(`fetch failed: ${e.cause?.code ?? e.message}`) }
|
|
120
146
|
},
|
|
121
147
|
}
|
package/src/tools/websearch.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
Search the web
|
|
1
|
+
Search the web via Bing. Returns result titles, URLs, and snippets. Use for looking up current information, docs, error messages.
|
|
2
2
|
|
|
3
3
|
Parameters:
|
|
4
4
|
- query (required): Search query
|
|
5
|
-
- limit: Max results (default 8)
|
|
5
|
+
- limit: Max results (default 8, max 20)
|
|
6
|
+
- engine: Specific engine to use — "bing" (Bing). Omit to search all engines concurrently.
|
|
7
|
+
- page: Page number for pagination (1-based, default 1). Only used when engine is specified.
|
|
6
8
|
|
|
7
9
|
Notes:
|
|
8
10
|
- Before searching the web, call `memory_search` first — you may already know the answer from a previous session. Only reach for websearch if memory comes up empty.
|
|
9
11
|
- Use this for information that is NOT in the local codebase — current docs, error messages, API references
|
|
10
12
|
- Follow up with `fetch` to read full pages from the results
|
|
11
|
-
-
|
|
13
|
+
- Proxy support: set `"proxy": {"uri": "http://host:port", "web": true}` in config.json
|
package/src/tui/agent-turn.mjs
CHANGED
|
@@ -10,6 +10,9 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
10
10
|
* handleSlash, summarize } */
|
|
11
11
|
export async function runAgentTurn(ctx, text) {
|
|
12
12
|
const { agent, state, pushLine, pushLabel, render, scheduleRender, ensureAssistantLabel, askPermission, askQuestion, handleSlash, summarize } = ctx
|
|
13
|
+
// 可注入覆盖(测试用);默认走真实实现
|
|
14
|
+
const runAgentImpl = ctx.runAgent ?? runAgent
|
|
15
|
+
const saveSessionImpl = ctx.saveSession ?? saveSession
|
|
13
16
|
pushLabel(`❯ You:`, ansi.bold + C.user)
|
|
14
17
|
pushLine(text, C.text)
|
|
15
18
|
|
|
@@ -133,12 +136,21 @@ export async function runAgentTurn(ctx, text) {
|
|
|
133
136
|
const stream = state.toolStreams[name]
|
|
134
137
|
const panel = state.outputPanels[name]
|
|
135
138
|
if (panel) {
|
|
136
|
-
// Tool with output panel: mark panel as done, conversation gets only summary
|
|
137
|
-
panel.done = true
|
|
138
139
|
delete state.toolStreams[name]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
panel._pendingDone = true // defer done until next render cycle flushes it
|
|
141
|
+
scheduleRender() // trigger one final render while panel is still alive
|
|
142
|
+
if (name === "advisor") {
|
|
143
|
+
const text = String(result ?? "")
|
|
144
|
+
const lines = text.split("\n")
|
|
145
|
+
const maxShow = Math.min(60, lines.length)
|
|
146
|
+
// Push as single multiline block so formatTables aligns MD table columns
|
|
147
|
+
const shown = lines.slice(0, maxShow).map((l) => ` ${l.slice(0, 200)}`).join("\n")
|
|
148
|
+
pushLine(shown, C.advisor)
|
|
149
|
+
if (lines.length > maxShow) pushLine(` ... (${lines.length - maxShow} more lines — call advisor again or scroll through the tool result for full output)`, C.dim)
|
|
150
|
+
} else {
|
|
151
|
+
const summary = formatPanelSummary(name, result)
|
|
152
|
+
if (summary) pushLine(` ${summary}`, C.dim)
|
|
153
|
+
}
|
|
142
154
|
setTimeout(() => {
|
|
143
155
|
delete state.outputPanels[name]
|
|
144
156
|
if (state.processing) render()
|
|
@@ -164,6 +176,7 @@ export async function runAgentTurn(ctx, text) {
|
|
|
164
176
|
}
|
|
165
177
|
panel.text = (panel.text ?? "") + chunk
|
|
166
178
|
if (panel.text.length > 4000) panel.text = panel.text.slice(-4000)
|
|
179
|
+
panel.seq = (panel.seq ?? 0) + 1 // render-loop cache key: survives the 4000-char cap
|
|
167
180
|
scheduleRender()
|
|
168
181
|
},
|
|
169
182
|
onPermissionRequest: (name, args) => askPermission(name, args),
|
|
@@ -197,99 +210,97 @@ export async function runAgentTurn(ctx, text) {
|
|
|
197
210
|
pushLine(` [task] ${done}/${items.length}${current ? ` ▶ ${current.title}` : ""}`, C.dim)
|
|
198
211
|
render()
|
|
199
212
|
},
|
|
200
|
-
onAdvisor: (note) => {
|
|
201
|
-
pushLine(` [advisor] ${note.replace(/\n/g, "\n ")}`, C.advisor)
|
|
202
|
-
},
|
|
203
213
|
// Incremental save: flush to disk every 5 tool turns — mid-crash loss window shrinks from an entire round to a few turns
|
|
204
214
|
onTurnEnd: (() => {
|
|
205
215
|
let n = 0
|
|
206
216
|
return () => {
|
|
207
217
|
if (++n % 5 !== 0) return
|
|
208
|
-
try {
|
|
218
|
+
try { saveSessionImpl(agent, state.lines) } catch (e) { console.error(`[session] incremental save failed: ${e.message}`) }
|
|
209
219
|
}
|
|
210
220
|
})(),
|
|
211
221
|
}
|
|
212
222
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (state.controller?.signal
|
|
223
|
+
// try/finally: every exit path — including an unexpected throw inside the catch
|
|
224
|
+
// block (e.g. the continue-permission UI) — must stop the ticker and reset state,
|
|
225
|
+
// otherwise the 1s render interval leaks and keeps firing forever.
|
|
226
|
+
try {
|
|
227
|
+
for (let resume = false; ; resume = true) {
|
|
228
|
+
try {
|
|
229
|
+
await runAgentImpl(agent, text, callbacks, { signal: state.controller.signal, resume })
|
|
230
|
+
flushStream()
|
|
231
|
+
break // Normal completion, exit loop
|
|
232
|
+
} catch (error) {
|
|
233
|
+
flushStream()
|
|
234
|
+
if (error.name === "AbortError" || state.controller?.signal.aborted) {
|
|
235
|
+
// Ctrl+I inject: the signal was aborted with an interrupt message — the agent loop
|
|
236
|
+
// may have already injected it into history, but the aborted signal prevents retry.
|
|
237
|
+
// Recreate the controller and resume from the same context.
|
|
238
|
+
if (state.controller?.signal?.reason?.interrupt) {
|
|
239
|
+
state.controller = new AbortController()
|
|
240
|
+
resume = true
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
pushLine("[stopped]", C.warn)
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
if (error instanceof ContinueError) {
|
|
247
|
+
pushLabel(`❯ Continue`, ansi.bold + C.warn)
|
|
248
|
+
pushLine(`Ran ${error.turn} turns (limit ${error.turn}). Continue?`, C.warn)
|
|
249
|
+
// Pause to ask: reuse permission mechanism
|
|
250
|
+
const willContinue = await new Promise((resolve) => {
|
|
251
|
+
state.permission = {
|
|
252
|
+
name: "continue",
|
|
253
|
+
args: { turns: error.turn },
|
|
254
|
+
resolve,
|
|
255
|
+
}
|
|
256
|
+
state.status = `Continue after ${error.turn} turns?`
|
|
257
|
+
render()
|
|
258
|
+
})
|
|
259
|
+
state.permission = null
|
|
260
|
+
if (!willContinue) {
|
|
261
|
+
pushLine("[continue cancelled]", C.warn)
|
|
262
|
+
break
|
|
263
|
+
}
|
|
264
|
+
pushLine("[continuing…]", C.tool)
|
|
265
|
+
// Recreate AbortController: once aborted, resume immediately fails (defensive; current path unreachable but tightly coupled)
|
|
225
266
|
state.controller = new AbortController()
|
|
226
|
-
resume = true
|
|
227
267
|
continue
|
|
228
268
|
}
|
|
229
|
-
pushLine(
|
|
269
|
+
pushLine(`[error] ${error.message}`, C.error)
|
|
230
270
|
break
|
|
231
271
|
}
|
|
232
|
-
if (error instanceof ContinueError) {
|
|
233
|
-
pushLabel(`❯ Continue`, ansi.bold + C.warn)
|
|
234
|
-
pushLine(`Ran ${error.turn} turns (limit ${error.turn}). Continue?`, C.warn)
|
|
235
|
-
// Pause to ask: reuse permission mechanism
|
|
236
|
-
const willContinue = await new Promise((resolve) => {
|
|
237
|
-
state.permission = {
|
|
238
|
-
name: "continue",
|
|
239
|
-
args: { turns: error.turn },
|
|
240
|
-
resolve,
|
|
241
|
-
}
|
|
242
|
-
state.status = `Continue after ${error.turn} turns?`
|
|
243
|
-
render()
|
|
244
|
-
})
|
|
245
|
-
state.permission = null
|
|
246
|
-
if (!willContinue) {
|
|
247
|
-
pushLine("[continue cancelled]", C.warn)
|
|
248
|
-
break
|
|
249
|
-
}
|
|
250
|
-
pushLine("[continuing…]", C.tool)
|
|
251
|
-
// Recreate AbortController: once aborted, resume immediately fails (defensive; current path unreachable but tightly coupled)
|
|
252
|
-
state.controller = new AbortController()
|
|
253
|
-
continue
|
|
254
|
-
}
|
|
255
|
-
pushLine(`[error] ${error.message}`, C.error)
|
|
256
|
-
break
|
|
257
272
|
}
|
|
273
|
+
} finally {
|
|
274
|
+
clearInterval(ticker)
|
|
275
|
+
state.processing = false
|
|
276
|
+
state.subTasks = {}
|
|
277
|
+
state.controller = null
|
|
278
|
+
state.status = "Ready"
|
|
279
|
+
// Auto-collapse todo panel when all tasks done (matching kimi-code TUI; agent.tasks are preserved)
|
|
280
|
+
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
281
|
+
state.tasks = []
|
|
282
|
+
}
|
|
283
|
+
// Save session after every turn (survives crashes)
|
|
284
|
+
try {
|
|
285
|
+
saveSessionImpl(agent, state.lines)
|
|
286
|
+
} catch {
|
|
287
|
+
// Save failure doesn't interrupt usage
|
|
288
|
+
}
|
|
289
|
+
render()
|
|
258
290
|
}
|
|
259
291
|
|
|
260
|
-
clearInterval(ticker)
|
|
261
|
-
state.processing = false
|
|
262
|
-
state.subTasks = {}
|
|
263
|
-
state.controller = null
|
|
264
|
-
state.status = "Ready"
|
|
265
|
-
// Auto-collapse todo panel when all tasks done (matching kimi-code TUI; agent.tasks are preserved)
|
|
266
|
-
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
267
|
-
state.tasks = []
|
|
268
|
-
}
|
|
269
|
-
// Save session after every turn (survives crashes)
|
|
270
|
-
try {
|
|
271
|
-
saveSession(agent, state.lines)
|
|
272
|
-
} catch {
|
|
273
|
-
// Save failure doesn't interrupt usage
|
|
274
|
-
}
|
|
275
|
-
render()
|
|
276
|
-
|
|
277
292
|
// Queued messages: auto-process next one
|
|
278
|
-
|
|
293
|
+
while (state.queue.length > 0 && !state.processing) {
|
|
279
294
|
const next = state.queue.shift()
|
|
280
|
-
// Queued slash commands execute directly
|
|
295
|
+
// Queued slash commands execute directly — check every item, not just the first
|
|
281
296
|
if (next.text.startsWith("/")) {
|
|
282
297
|
await handleSlash(next.text)
|
|
283
298
|
render()
|
|
284
|
-
|
|
285
|
-
if (state.queue.length > 0 && !state.processing) {
|
|
286
|
-
const next2 = state.queue.shift()
|
|
287
|
-
await runAgentTurn(ctx, next2.text)
|
|
288
|
-
}
|
|
289
|
-
} else {
|
|
290
|
-
pushLabel(`❯ You: (from queue)`, ansi.bold + C.user)
|
|
291
|
-
await runAgentTurn(ctx, next.text)
|
|
299
|
+
continue
|
|
292
300
|
}
|
|
301
|
+
pushLabel(`❯ You: (from queue)`, ansi.bold + C.user)
|
|
302
|
+
await runAgentTurn(ctx, next.text)
|
|
303
|
+
return
|
|
293
304
|
}
|
|
294
305
|
}
|
|
295
306
|
|