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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/make-agent.mjs +7 -0
- package/src/config.mjs +46 -19
- package/src/prompts/coder.md +5 -2
- package/src/prompts/discipline.md +8 -5
- package/src/prompts/system.md +8 -5
- package/src/provider/anthropic.mjs +190 -0
- package/src/provider/core.mjs +36 -4
- package/src/provider/google.mjs +197 -0
- package/src/proxy.mjs +236 -0
- package/src/tools/fetch.md +2 -1
- package/src/tools/git.mjs +120 -154
- package/src/tools/index.mjs +9 -9
- package/src/tools/linter.mjs +46 -32
- package/src/tools/web.mjs +103 -82
- package/src/tools/websearch.md +5 -3
- package/src/tui/agent-turn.mjs +12 -13
- package/src/tui/cmd-advisor.mjs +29 -41
- 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 +64 -38
- package/src/tui/key-handler.mjs +48 -18
- package/src/tui/layout.mjs +12 -2
- package/src/tui/pickers.mjs +151 -182
- package/src/tui/render-frame.mjs +29 -11
- package/src/tui/slash-commands.mjs +26 -16
package/src/tools/web.mjs
CHANGED
|
@@ -1,11 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { DESC, truncate, stripTags, htmlToText } 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
|
+
const blocks = html.split('<li class="b_algo"').slice(1)
|
|
13
|
+
for (const block of blocks) {
|
|
14
|
+
const link = block.match(/<h2[^>]*><a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/)
|
|
15
|
+
if (!link) continue
|
|
16
|
+
const snippet = block.match(/<p[^>]*>([\s\S]*?)<\/p>/)
|
|
17
|
+
results.push({ href: link[1], title: stripTags(link[2]), snippet: snippet ? stripTags(snippet[1]) : "" })
|
|
18
|
+
}
|
|
19
|
+
return results
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bingUrl(query, page) {
|
|
23
|
+
let u = `https://www.bing.com/search?q=${encodeURIComponent(query)}&setlang=en&setmkt=en-US`
|
|
24
|
+
if (page > 1) u += `&first=${(page - 1) * 10 + 1}`
|
|
25
|
+
return u
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ENGINES = [{ name: "bing", label: "Bing", url: bingUrl, extract: extractBing, ua: UA }]
|
|
29
|
+
const ENGINE_NAMES = ENGINES.map(e => e.name)
|
|
30
|
+
|
|
31
|
+
async function fetchEngine(engine, query, page, ctx) {
|
|
32
|
+
const ctrl = new AbortController()
|
|
33
|
+
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT)
|
|
34
|
+
try {
|
|
35
|
+
const response = await proxyFetch(engine.url(query, page), {
|
|
36
|
+
headers: { "User-Agent": engine.ua, "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8" },
|
|
37
|
+
signal: ctrl.signal,
|
|
38
|
+
}, resolveWebProxy(ctx))
|
|
39
|
+
if (!response.ok) return null
|
|
40
|
+
const html = await response.text()
|
|
41
|
+
const results = engine.extract(html)
|
|
42
|
+
return { engine: engine.name, results }
|
|
43
|
+
} catch { return null }
|
|
44
|
+
finally { clearTimeout(timer) }
|
|
45
|
+
}
|
|
9
46
|
|
|
10
47
|
export const websearchTool = {
|
|
11
48
|
name: "websearch",
|
|
@@ -14,108 +51,92 @@ export const websearchTool = {
|
|
|
14
51
|
type: "object",
|
|
15
52
|
properties: {
|
|
16
53
|
query: { type: "string", description: "Search query" },
|
|
17
|
-
limit: { type: "number", description: "Max results (default 8)" },
|
|
54
|
+
limit: { type: "number", description: "Max results (default 8, max 20)" },
|
|
55
|
+
engine: { type: "string", enum: ENGINE_NAMES, description: "Specific engine — \"bing\" (Bing). Omit to search all engines concurrently." },
|
|
56
|
+
page: { type: "number", description: "Page number for pagination (1-based, default 1). Only used when engine is specified." },
|
|
18
57
|
},
|
|
19
58
|
required: ["query"],
|
|
20
59
|
},
|
|
21
60
|
readonly: true,
|
|
22
61
|
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}`)
|
|
62
|
+
const limit = Math.min(args.limit ?? 8, 20)
|
|
63
|
+
const page = Math.max(1, args.page ?? 1)
|
|
64
|
+
if (args.engine) {
|
|
65
|
+
const engine = ENGINES.find(e => e.name === args.engine)
|
|
66
|
+
if (!engine) return `Unknown engine '${args.engine}'. Available: ${ENGINE_NAMES.join(", ")}`
|
|
67
|
+
const fetched = await fetchEngine(engine, args.query, page, ctx)
|
|
68
|
+
if (!fetched || fetched.results.length === 0) return `(no results from ${engine.label})`
|
|
69
|
+
return truncate(fetched.results.slice(0, limit).map((r, i) => `${i + 1}. ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
|
|
37
70
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
const promises = ENGINES.map(e => fetchEngine(e, args.query, 1, ctx))
|
|
72
|
+
const fetched = (await Promise.all(promises)).filter(Boolean)
|
|
73
|
+
if (fetched.length === 0) return "(no results — all search engines failed)"
|
|
74
|
+
const merged = [], indexes = fetched.map(() => 0)
|
|
75
|
+
let done = false
|
|
76
|
+
while (!done && merged.length < limit) {
|
|
77
|
+
done = true
|
|
78
|
+
for (let i = 0; i < fetched.length; i++) {
|
|
79
|
+
if (indexes[i] < fetched[i].results.length) {
|
|
80
|
+
merged.push({ ...fetched[i].results[indexes[i]], _engine: fetched[i].engine })
|
|
81
|
+
indexes[i]++; done = false
|
|
82
|
+
if (merged.length >= limit) break
|
|
83
|
+
}
|
|
84
|
+
}
|
|
52
85
|
}
|
|
53
|
-
|
|
54
|
-
return truncate(
|
|
55
|
-
results.map((r, i) => `${i + 1}. ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"),
|
|
56
|
-
)
|
|
86
|
+
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
87
|
},
|
|
58
88
|
}
|
|
59
89
|
|
|
90
|
+
// ── Fetch tool (with proxy support) ──────
|
|
60
91
|
|
|
61
|
-
// ---------------------------------------------------------------- ls
|
|
62
|
-
|
|
63
|
-
/** SSRF protection: block internal private-network/metadata endpoints (localhost allowed — user's dev server, tests depend on it) */
|
|
64
92
|
function isPrivateUrl(urlStr) {
|
|
65
|
-
let u
|
|
66
|
-
try { u = new URL(urlStr) } catch { return true }
|
|
93
|
+
let u; try { u = new URL(urlStr) } catch { return true }
|
|
67
94
|
const host = u.hostname.toLowerCase()
|
|
68
|
-
// localhost / 127.x allowed (user's dev server on local machine, test mock server)
|
|
69
95
|
if (host === "localhost" || host === "0.0.0.0" || host.endsWith(".localhost")) return false
|
|
70
96
|
if (host === "127.0.0.1" || host.startsWith("127.")) return false
|
|
71
|
-
// cloud metadata endpoint
|
|
72
97
|
if (host === "169.254.169.254" || host === "metadata.google.internal") return true
|
|
73
|
-
// IPv4 private ranges
|
|
74
98
|
const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
|
|
75
|
-
if (m) {
|
|
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
|
|
99
|
+
if (m) { const [a, b] = [Number(m[1]), Number(m[2])]; if (a === 10||a === 172&&b>=16&&b<=31||a === 192&&b===168||a === 169&&b===254||a===0) return true }
|
|
84
100
|
if (host === "::1" || host === "fe80::1" || host.startsWith("fc") || host.startsWith("fd")) return true
|
|
85
101
|
return false
|
|
86
102
|
}
|
|
87
103
|
|
|
104
|
+
// proxyFetch returns a native Response (Headers object, needs .get()) without proxy,
|
|
105
|
+
// but a Response-like with a plain lowercase-keyed Record through the CONNECT tunnel — handle both.
|
|
106
|
+
function headerOf(res, name) {
|
|
107
|
+
const h = res.headers
|
|
108
|
+
if (!h) return null
|
|
109
|
+
if (typeof h.get === "function") return h.get(name)
|
|
110
|
+
return h[name.toLowerCase()] ?? null
|
|
111
|
+
}
|
|
112
|
+
|
|
88
113
|
export const fetchTool = {
|
|
89
114
|
name: "fetch",
|
|
90
115
|
description: DESC("fetch"),
|
|
91
|
-
parameters: {
|
|
92
|
-
type: "object",
|
|
93
|
-
properties: {
|
|
94
|
-
url: { type: "string", description: "http/https URL" },
|
|
95
|
-
},
|
|
96
|
-
required: ["url"],
|
|
97
|
-
},
|
|
116
|
+
parameters: { type: "object", properties: { url: { type: "string", description: "http/https URL" } }, required: ["url"] },
|
|
98
117
|
readonly: true,
|
|
99
118
|
async execute(args, ctx) {
|
|
100
119
|
if (!/^https?:\/\//.test(args.url)) throw new Error("url must start with http:// or https://")
|
|
101
120
|
if (isPrivateUrl(args.url)) throw new Error("fetch blocked: internal/private/metadata addresses are not allowed")
|
|
102
|
-
let response
|
|
103
121
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
const proxyUri = resolveWebProxy(ctx)
|
|
123
|
+
const response = await proxyFetch(args.url, { headers: { "User-Agent": UA } }, proxyUri)
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
if ([301, 302, 307, 308].includes(response.status)) {
|
|
126
|
+
const loc = headerOf(response, "location")
|
|
127
|
+
if (loc) {
|
|
128
|
+
const r2 = await proxyFetch(loc, { headers: { "User-Agent": UA } }, proxyUri)
|
|
129
|
+
if (!r2.ok) throw new Error(`fetch failed: HTTP ${r2.status}`)
|
|
130
|
+
const ct2 = headerOf(r2, "content-type") ?? ""
|
|
131
|
+
const b2 = await r2.text()
|
|
132
|
+
return ct2.includes("text/html") ? truncate(htmlToText(b2)) : truncate(b2)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`fetch failed: HTTP ${response.status}`)
|
|
136
|
+
}
|
|
137
|
+
const ct = headerOf(response, "content-type") ?? ""
|
|
138
|
+
const body = await response.text()
|
|
139
|
+
return ct.includes("text/html") ? truncate(htmlToText(body)) : truncate(body)
|
|
140
|
+
} catch (e) { throw new Error(`fetch failed: ${e.cause?.code ?? e.message}`) }
|
|
120
141
|
},
|
|
121
142
|
}
|
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
|
|
|
@@ -205,14 +208,14 @@ export async function runAgentTurn(ctx, text) {
|
|
|
205
208
|
let n = 0
|
|
206
209
|
return () => {
|
|
207
210
|
if (++n % 5 !== 0) return
|
|
208
|
-
try {
|
|
211
|
+
try { saveSessionImpl(agent, state.lines) } catch (e) { console.error(`[session] incremental save failed: ${e.message}`) }
|
|
209
212
|
}
|
|
210
213
|
})(),
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
for (let resume = false; ; resume = true) {
|
|
214
217
|
try {
|
|
215
|
-
await
|
|
218
|
+
await runAgentImpl(agent, text, callbacks, { signal: state.controller.signal, resume })
|
|
216
219
|
flushStream()
|
|
217
220
|
break // Normal completion, exit loop
|
|
218
221
|
} catch (error) {
|
|
@@ -268,28 +271,24 @@ export async function runAgentTurn(ctx, text) {
|
|
|
268
271
|
}
|
|
269
272
|
// Save session after every turn (survives crashes)
|
|
270
273
|
try {
|
|
271
|
-
|
|
274
|
+
saveSessionImpl(agent, state.lines)
|
|
272
275
|
} catch {
|
|
273
276
|
// Save failure doesn't interrupt usage
|
|
274
277
|
}
|
|
275
278
|
render()
|
|
276
279
|
|
|
277
280
|
// Queued messages: auto-process next one
|
|
278
|
-
|
|
281
|
+
while (state.queue.length > 0 && !state.processing) {
|
|
279
282
|
const next = state.queue.shift()
|
|
280
|
-
// Queued slash commands execute directly
|
|
283
|
+
// Queued slash commands execute directly — check every item, not just the first
|
|
281
284
|
if (next.text.startsWith("/")) {
|
|
282
285
|
await handleSlash(next.text)
|
|
283
286
|
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)
|
|
287
|
+
continue
|
|
292
288
|
}
|
|
289
|
+
pushLabel(`❯ You: (from queue)`, ansi.bold + C.user)
|
|
290
|
+
await runAgentTurn(ctx, next.text)
|
|
291
|
+
return
|
|
293
292
|
}
|
|
294
293
|
}
|
|
295
294
|
|
package/src/tui/cmd-advisor.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** /advisor command: toggle advisor on/off, select model.
|
|
2
|
-
* ctx: { agent,
|
|
2
|
+
* ctx: { agent, showPicker, pushLine } */
|
|
3
3
|
import { C } from "./ansi.mjs"
|
|
4
4
|
|
|
5
5
|
export async function handleAdvisorCommand(ctx) {
|
|
6
|
-
const { agent,
|
|
6
|
+
const { agent, showPicker, pushLine } = ctx
|
|
7
7
|
const cfg = agent.config.advisor ??= {}
|
|
8
8
|
const enabled = cfg.enabled === true
|
|
9
9
|
const curProvider = cfg.provider || agent.activeProvider
|
|
@@ -14,55 +14,43 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
14
14
|
{ type: "item", text: `Model: ${curProvider}/${curModel}`, action: "model" },
|
|
15
15
|
]
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
await openAdvisorModelPicker(ctx).catch(err => pushLine(`[error] ${err.message}`, C.error))
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
})
|
|
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.]")
|
|
26
|
+
}
|
|
27
|
+
} else if (e.action === "model") {
|
|
28
|
+
await openAdvisorModelPicker(ctx).catch(err => pushLine(`[error] ${err.message}`, C.error))
|
|
29
|
+
}
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
async function openAdvisorModelPicker(ctx) {
|
|
37
|
-
const { agent,
|
|
33
|
+
const { agent, showPicker, pushLine } = ctx
|
|
38
34
|
const providers = agent.providers || []
|
|
39
35
|
|
|
40
36
|
// Build flat list: each provider's name + a "use current model" entry
|
|
41
37
|
const entries = []
|
|
42
|
-
let idx = 0
|
|
43
38
|
for (const p of providers) {
|
|
44
39
|
const mark = p.name === agent.activeProvider ? "* " : " "
|
|
45
40
|
entries.push({ type: "item", text: `${mark}${p.name} — ${p.baseURL}`, action: "set_provider", provider: p.name, model: p.model })
|
|
46
|
-
idx++
|
|
47
41
|
}
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
cfg.model = e.model
|
|
63
|
-
pushLine(`Advisor: ${e.provider}/${e.model}`, C.dim)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
})
|
|
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)
|
|
55
|
+
}
|
|
68
56
|
}
|
package/src/tui/cmd-clear.mjs
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
/** /clear command: clear screen (confirm to prevent accidental trigger).
|
|
2
|
-
* ctx: { state,
|
|
2
|
+
* ctx: { state, showPicker, render } */
|
|
3
3
|
export async function handleClearCommand(ctx) {
|
|
4
|
-
const { state,
|
|
4
|
+
const { state, showPicker, render } = ctx
|
|
5
5
|
if (state.lines.length > 0) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
state.lines = []
|
|
16
|
-
state.streaming = ""
|
|
17
|
-
render()
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
})
|
|
6
|
+
const e = await showPicker("Clear screen?", [
|
|
7
|
+
{ type: "item", text: "Yes, clear all conversation output", action: "yes" },
|
|
8
|
+
{ type: "item", text: "Cancel", action: "no" },
|
|
9
|
+
], { defaultIndex: 1 })
|
|
10
|
+
if (e?.action === "yes") {
|
|
11
|
+
state.lines = []
|
|
12
|
+
state.streaming = ""
|
|
13
|
+
render()
|
|
14
|
+
}
|
|
21
15
|
return
|
|
22
16
|
}
|
|
23
17
|
state.lines = []
|