thincoder 0.12.60 → 0.12.61
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/CHANGELOG.md +24 -1
- package/README.md +8 -6
- package/bin/thincoder.mjs +27 -122
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +36 -134
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +124 -329
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +3 -2
- package/src/agent/completion.mjs +1 -1
- package/src/agent/dispatch.mjs +47 -12
- package/src/agent/helpers.mjs +71 -13
- package/src/agent/record-results.mjs +13 -5
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +21 -6
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +26 -9
- package/src/agent-tools/advisor-async.mjs +70 -180
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +69 -20
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +23 -10
- package/src/agent-tools/design-token.mjs +14 -1
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +3 -3
- package/src/agent-tools/escalate-async.mjs +16 -13
- package/src/agent-tools/read-history.mjs +13 -3
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +15 -6
- package/src/agent-tools/subagent-async.mjs +66 -14
- package/src/agent-tools/subagent-panel.mjs +22 -15
- package/src/agent-tools/subagent-run.mjs +9 -6
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +63 -16
- package/src/agent-tools/subagent.mjs +175 -49
- package/src/agent-tools/verify.mjs +13 -34
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +38 -21
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +23 -7
- package/src/cli/memory-command.mjs +2 -2
- package/src/cli/setup-wizard.mjs +29 -9
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +132 -63
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +7 -2
- package/src/expand-home.mjs +16 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +70 -31
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +10 -6
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/prompt-overlays.mjs +73 -16
- package/src/prompts/advisor-design.md +18 -8
- package/src/prompts/advisor-round1.md +7 -1
- package/src/prompts/advisor-round2.md +13 -2
- package/src/prompts/advisor-round3.md +13 -2
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +13 -32
- package/src/provider/errors.mjs +26 -1
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-slots.mjs +7 -2
- package/src/session.mjs +59 -38
- package/src/token-ttl.mjs +2 -1
- package/src/tools/{system.mjs → bash.mjs} +6 -243
- package/src/tools/file.mjs +30 -10
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +15 -34
- package/src/tools/index.mjs +4 -2
- package/src/tools/ops.mjs +20 -7
- package/src/tools/question.md +1 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -1
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tui/agent-turn.mjs +32 -13
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +108 -37
- package/src/tui/cmd-eng.mjs +11 -27
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/index.mjs +38 -37
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +47 -10
- package/src/tui/pickers.mjs +28 -410
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +5 -19
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/subagent-blocks.mjs +69 -90
- package/src/tui/subagent-children.mjs +50 -64
- package/src/tui/subagent-freeze.mjs +40 -43
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +28 -82
- package/src/tui/tool-args.mjs +4 -2
- package/src/tui/tool-display.mjs +4 -3
- package/src/tui/tool-events.mjs +23 -16
- package/src/tui/tui-lifecycle.mjs +18 -5
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -13
- package/src/prompts/discipline.md +0 -84
- package/src/prompts/eng-coder.md +0 -19
- package/src/prompts/engineering-sub.md +0 -14
- package/src/prompts/engineering.md +0 -87
- package/src/prompts/explore.md +0 -12
- package/src/prompts/main.md +0 -34
- package/src/prompts/methodology-template.md +0 -38
- package/src/prompts/plan.md +0 -9
- package/src/prompts/system.md +0 -44
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DESC } from "./shared.mjs";
|
|
2
|
+
|
|
3
|
+
export const questionTool = {
|
|
4
|
+
name: "question",
|
|
5
|
+
description: DESC("question"),
|
|
6
|
+
parameters: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
question: { type: "string", description: "The question to ask the user" },
|
|
10
|
+
options: {
|
|
11
|
+
type: "array",
|
|
12
|
+
items: { type: "string" },
|
|
13
|
+
description: "Single-choice options for the user to pick from (optional)",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
required: ["question"],
|
|
17
|
+
},
|
|
18
|
+
readonly: true,
|
|
19
|
+
async execute(args, ctx) {
|
|
20
|
+
if (!ctx.onQuestion) throw new Error("question tool not supported in this context (no UI to ask)")
|
|
21
|
+
// §22 D-Q3 机械限制(2026-09-06 用户裁定:100 字符 / 4 条——超限拒绝回模型,不弹卡、不调 onQuestion)
|
|
22
|
+
if (args.question.length > 100) return "(error: question too long (>100 chars) — ask ONE short question; background belongs in your normal reply text)"
|
|
23
|
+
if (Array.isArray(args.options) && args.options.length > 4) return "(error: too many options (>4) — offer at most 4 plain-string options)"
|
|
24
|
+
return ctx.onQuestion(args.question, args.options ?? [])
|
|
25
|
+
},
|
|
26
|
+
}
|
package/src/tools/read.md
CHANGED
|
@@ -11,7 +11,7 @@ Read a text file. Returns numbered lines. Use offset/limit to page large files.
|
|
|
11
11
|
Parameters:
|
|
12
12
|
- path (required): File path, relative to cwd or absolute (alias: filePath)
|
|
13
13
|
- offset: 1-based line number to start reading from
|
|
14
|
-
- limit: Max lines to return (default 2000)
|
|
14
|
+
- limit: Max lines to return (default 2000) — windows over files beyond that return head + `…(truncated: K lines in middle, use offset to continue)` + the file's real tail lines, so the file end is never hidden
|
|
15
15
|
- hashes: Include SHA256 content hashes per line (for hashline_edit). Set true before using hashline_edit.
|
|
16
16
|
|
|
17
17
|
Notes:
|
package/src/tools/read_image.md
CHANGED
|
@@ -5,4 +5,4 @@ Parameters:
|
|
|
5
5
|
|
|
6
6
|
Notes:
|
|
7
7
|
- Raster formats only (png/jpg/gif/webp): no mainstream vision API (Kimi, Anthropic, OpenAI, Gemini) accepts svg or bmp, and an unsupported image part in history makes every subsequent request fail with 400. svg files are returned as text source instead (readable by any model); bmp is rejected — convert to PNG first.
|
|
8
|
-
- This tool only works with models that support vision/image input (
|
|
8
|
+
- This tool only works with models that support vision/image input (capability is spec-driven — the model spec decides, not a hardcoded list). Models without vision support will receive an error — except svg, which needs no vision support since it is read as text.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DESC,
|
|
3
|
+
truncate,
|
|
4
|
+
resolveInCwd,
|
|
5
|
+
globToRegex,
|
|
6
|
+
splitGlobPatterns,
|
|
7
|
+
compileGlobMatchers,
|
|
8
|
+
normalizeEOL,
|
|
9
|
+
IGNORED_DIRS,
|
|
10
|
+
} from "./shared.mjs";
|
|
11
|
+
import { readFile, readdir, stat, lstat } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
/** Escape a string for literal regex matching (grep literal=true). */
|
|
15
|
+
function escapeRegExp(s) {
|
|
16
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------- glob
|
|
20
|
+
|
|
21
|
+
export const globTool = {
|
|
22
|
+
name: "glob",
|
|
23
|
+
description: DESC("glob"),
|
|
24
|
+
parameters: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
pattern: { type: "string", description: "Glob pattern — supports **, *, ?, [..], {a,b} braces, and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
28
|
+
path: { type: "string", description: "Directory to search in (default cwd)" },
|
|
29
|
+
},
|
|
30
|
+
required: ["pattern"],
|
|
31
|
+
},
|
|
32
|
+
readonly: true,
|
|
33
|
+
async execute(args, ctx) {
|
|
34
|
+
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
35
|
+
// §17 调用侧拆分(评审 #5 职责分层):空格分隔 include !exclude 多模式在这里拆;
|
|
36
|
+
// globToRegex 只收单个模式(数组由 compileGlobMatchers 收)。
|
|
37
|
+
let match
|
|
38
|
+
try {
|
|
39
|
+
const parts = splitGlobPatterns(args.pattern)
|
|
40
|
+
if (parts.length === 0) return "Error: pattern is required"
|
|
41
|
+
match = compileGlobMatchers(parts).test
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return `glob error: invalid pattern "${args.pattern}": ${e.message}`
|
|
44
|
+
}
|
|
45
|
+
const results = []
|
|
46
|
+
for await (const relPath of walkFiles(base)) {
|
|
47
|
+
if (match(relPath)) {
|
|
48
|
+
results.push(relPath)
|
|
49
|
+
if (results.length >= 1000) break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (results.length === 0) return "(no matches)"
|
|
53
|
+
return truncate(results.sort().join("\n"))
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Recursively traverse files, yield relative paths (skip IGNORED_DIRS) */
|
|
58
|
+
async function* walkFiles(dir, rel = "") {
|
|
59
|
+
let entries
|
|
60
|
+
try {
|
|
61
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
62
|
+
} catch {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
// Skip ignored dirs AND symbolic links (symlinks to directories would cause infinite loops)
|
|
67
|
+
if (e.isSymbolicLink()) continue
|
|
68
|
+
if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
|
|
69
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name
|
|
70
|
+
if (e.isDirectory()) {
|
|
71
|
+
yield* walkFiles(join(dir, e.name), relPath)
|
|
72
|
+
} else {
|
|
73
|
+
yield relPath
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------- grep
|
|
79
|
+
|
|
80
|
+
export const grepTool = {
|
|
81
|
+
name: "grep",
|
|
82
|
+
description: DESC("grep"),
|
|
83
|
+
parameters: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
pattern: { type: "string", description: "Regular expression, or a literal string when literal=true" },
|
|
87
|
+
path: { type: "string", description: "Directory or file to search (default cwd)" },
|
|
88
|
+
glob: { type: "string", description: "Only search files matching this glob — supports **, *, ?, [..], {a,b} braces and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
89
|
+
ignoreCase: { type: "boolean", description: "Case-insensitive match (default false)" },
|
|
90
|
+
literal: { type: "boolean", description: "Literal string match — no regex interpretation (default false)" },
|
|
91
|
+
before: { type: "integer", description: "Lines of context to show before each match (grep -B). Default 0" },
|
|
92
|
+
after: { type: "integer", description: "Lines of context to show after each match (grep -A). Default 0" },
|
|
93
|
+
},
|
|
94
|
+
required: ["pattern"],
|
|
95
|
+
},
|
|
96
|
+
readonly: true,
|
|
97
|
+
async execute(args, ctx) {
|
|
98
|
+
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
99
|
+
let regex
|
|
100
|
+
try {
|
|
101
|
+
const pat = args.literal ? escapeRegExp(String(args.pattern)) : args.pattern
|
|
102
|
+
regex = new RegExp(pat, args.ignoreCase ? "i" : "")
|
|
103
|
+
} catch (e) {
|
|
104
|
+
throw new Error(`grep pattern /${args.pattern}/ is not a valid regex: ${e.message}`, { cause: e })
|
|
105
|
+
}
|
|
106
|
+
// §17 调用侧拆分:glob 参数支持空格分隔 include !exclude 多模式(include/exclude 求交)。
|
|
107
|
+
let fileTest = null
|
|
108
|
+
if (args.glob) {
|
|
109
|
+
try {
|
|
110
|
+
const parts = splitGlobPatterns(args.glob)
|
|
111
|
+
if (parts.length > 0) fileTest = compileGlobMatchers(parts).test
|
|
112
|
+
} catch (e) {
|
|
113
|
+
throw new Error(`grep glob /${args.glob}/ is invalid: ${e.message}`, { cause: e })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const before = Math.max(0, Math.floor(args.before ?? 0))
|
|
117
|
+
const after = Math.max(0, Math.floor(args.after ?? 0))
|
|
118
|
+
const wantCtx = before > 0 || after > 0
|
|
119
|
+
const hits = [] // { file, line(1-based), text }
|
|
120
|
+
const fileLines = new Map() // file -> string[] (cached only when context lines requested)
|
|
121
|
+
|
|
122
|
+
async function search(file) {
|
|
123
|
+
let content
|
|
124
|
+
try {
|
|
125
|
+
// Large file guard: skip files over 10MB to prevent OOM
|
|
126
|
+
const fst = await stat(file)
|
|
127
|
+
if (fst.size > 10_000_000) return
|
|
128
|
+
content = normalizeEOL(await readFile(file, "utf8"))
|
|
129
|
+
} catch {
|
|
130
|
+
return // Skip unreadable files; binary files will be read as UTF-8 and searched (may produce garbled matches)
|
|
131
|
+
}
|
|
132
|
+
const lines = content.split("\n")
|
|
133
|
+
if (wantCtx) fileLines.set(file, lines)
|
|
134
|
+
for (let i = 0; i < lines.length; i++) {
|
|
135
|
+
if (regex.test(lines[i])) {
|
|
136
|
+
hits.push({ file, line: i + 1, text: lines[i] })
|
|
137
|
+
if (hits.length >= 200) return
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function walk(target, rel) {
|
|
143
|
+
if (hits.length >= 200) return
|
|
144
|
+
// Use lstat to avoid following symlinks — prevents ./evil → /etc from making grep scan the entire system
|
|
145
|
+
let s
|
|
146
|
+
try { s = await lstat(target) } catch { return }
|
|
147
|
+
if (!s.isDirectory()) {
|
|
148
|
+
// rel 为相对搜索基的路径(排除前缀如 !test/** 必须按目录路径作用——不能用裸文件名);
|
|
149
|
+
// 单文件目标(path 指向文件)时 rel 为空 → 退化为按文件名过滤(既有语义)。
|
|
150
|
+
if (!fileTest || fileTest(rel || target.split(/[\\/]/).pop())) await search(target)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
let entries
|
|
154
|
+
try {
|
|
155
|
+
entries = await readdir(target, { withFileTypes: true })
|
|
156
|
+
} catch {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
for (const e of entries) {
|
|
160
|
+
if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
|
|
161
|
+
await walk(join(target, e.name), rel ? `${rel}/${e.name}` : e.name)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await walk(base, "")
|
|
166
|
+
if (hits.length === 0) return "(no matches)"
|
|
167
|
+
|
|
168
|
+
// No context: keep original path:line: content format
|
|
169
|
+
if (!wantCtx) {
|
|
170
|
+
return truncate(hits.map((h) => `${h.file}:${h.line}: ${h.text}`).join("\n"))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// With context: matching lines use ':', context lines use '-' (like ripgrep); overlapping ranges in same file are merged and de-duplicated
|
|
174
|
+
const fileMatched = new Map() // file -> Set<line>
|
|
175
|
+
for (const h of hits) {
|
|
176
|
+
if (!fileMatched.has(h.file)) fileMatched.set(h.file, new Set())
|
|
177
|
+
fileMatched.get(h.file).add(h.line)
|
|
178
|
+
}
|
|
179
|
+
const out = []
|
|
180
|
+
for (const [file, matchedLines] of fileMatched) {
|
|
181
|
+
const lines = fileLines.get(file) ?? []
|
|
182
|
+
const lineSet = new Set()
|
|
183
|
+
for (const ml of matchedLines) {
|
|
184
|
+
for (let l = Math.max(1, ml - before); l <= Math.min(lines.length, ml + after); l++) lineSet.add(l)
|
|
185
|
+
}
|
|
186
|
+
for (const l of [...lineSet].sort((a, b) => a - b)) {
|
|
187
|
+
const sep = matchedLines.has(l) ? ":" : "-"
|
|
188
|
+
out.push(`${file}${sep}${l}${sep} ${lines[l - 1]}`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return truncate(out.join("\n"))
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const lsTool = {
|
|
196
|
+
name: "ls",
|
|
197
|
+
description: DESC("ls"),
|
|
198
|
+
parameters: {
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {
|
|
201
|
+
path: { type: "string", description: "Directory path (default cwd)" },
|
|
202
|
+
filter: { type: "string", description: "Only list entries matching this glob (e.g. '*.mjs', '*test*')" },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
readonly: true,
|
|
206
|
+
async execute(args, ctx) {
|
|
207
|
+
const abs = resolveInCwd(ctx, args.path ?? ".")
|
|
208
|
+
const filterRe = args.filter ? globToRegex(args.filter) : null
|
|
209
|
+
let entries
|
|
210
|
+
try {
|
|
211
|
+
entries = await readdir(abs, { withFileTypes: true })
|
|
212
|
+
} catch (e) {
|
|
213
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") throw new Error(`ls: ${args.path ?? "."} — ${e.code === "ENOTDIR" ? "not a directory" : "not found"}`, { cause: e })
|
|
214
|
+
throw e
|
|
215
|
+
}
|
|
216
|
+
const rows = await Promise.all(
|
|
217
|
+
entries
|
|
218
|
+
.filter((e) => !filterRe || filterRe.test(e.name))
|
|
219
|
+
.slice(0, 500)
|
|
220
|
+
.map(async (e) => {
|
|
221
|
+
const s = await stat(join(abs, e.name)).catch(() => null)
|
|
222
|
+
const isDir = e.isDirectory()
|
|
223
|
+
return {
|
|
224
|
+
dir: isDir,
|
|
225
|
+
name: e.name + (isDir ? "/" : ""),
|
|
226
|
+
size: s?.size ?? 0,
|
|
227
|
+
mtime: s ? s.mtime.toISOString().slice(0, 16).replace("T", " ") : "?",
|
|
228
|
+
}
|
|
229
|
+
}),
|
|
230
|
+
)
|
|
231
|
+
rows.sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1))
|
|
232
|
+
if (rows.length === 0) return "(no entries)"
|
|
233
|
+
const out = rows.map((r) => `${r.dir ? "d" : "-"} ${r.name.padEnd(40)} ${String(r.size).padStart(10)} ${r.mtime}`)
|
|
234
|
+
return truncate(out.join("\n"))
|
|
235
|
+
},
|
|
236
|
+
}
|
package/src/tui/agent-turn.mjs
CHANGED
|
@@ -21,13 +21,25 @@ import { buildToolCallbacks, sweepToolBlocks } from "./tool-events.mjs"
|
|
|
21
21
|
import { freezeAllSubTasks } from "./subagent-blocks.mjs" // freezeReclaimDigestedBlocks 随 §17 段迁 suspension-drive.mjs
|
|
22
22
|
import { ensureSessionTitle } from "../generate-title.mjs"
|
|
23
23
|
import { logEvent, errText } from "../log.mjs"
|
|
24
|
-
import { suspensionSession, poolLive
|
|
24
|
+
import { suspensionSession, poolLive } from "./suspension-drive.mjs"
|
|
25
25
|
|
|
26
26
|
/** Exit-flush bound for the async end-of-run distillation (SEND-STALL-DISTILL §2.5):
|
|
27
27
|
* wait at most this long for the in-flight distill before the final session save —
|
|
28
28
|
* never let shutdown hang on the background summary call. */
|
|
29
29
|
const DISTILL_FLUSH_TIMEOUT_MS = 5000
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* 第 33 批(TUI §14.3(d)——纯函数,供测试直驱):回合链尾「需要用户」谓词——agent 已停
|
|
33
|
+
* 且无自动续跑 ⇒ 置 attention 位。排除项语义(F13):`skipSession`(digest / 会话内回合
|
|
34
|
+
* ——由外层链尾统一置位)· 挂起两态 / 池 live / 队列非空(自动续跑中——不由用户接手)·
|
|
35
|
+
* processing(回合在跑)。
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
export function userNeededAtTurnEnd(state, agent, skipSession) {
|
|
39
|
+
return !skipSession && !state.suspended && !state._suspPending && !poolLive(agent)
|
|
40
|
+
&& state.queue.length === 0 && !state.processing
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
/**
|
|
32
44
|
* LOGGING(docs/design/LOGGING.md)包装:回合骨架事件(turn:start/turn:end——kind
|
|
33
45
|
* user/auto;result ok/stopped/error)。内层经 _logOutcome 载具回传终止原因(中止/
|
|
@@ -283,26 +295,25 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
283
295
|
render()
|
|
284
296
|
}
|
|
285
297
|
|
|
286
|
-
// §17 偏差 #1
|
|
287
|
-
|
|
288
|
-
//
|
|
298
|
+
// §17 偏差 #1 兜底(INPUT-LOCK 单槽化——2026-09-09):释放窗口期 Enter 已入
|
|
299
|
+
// pendingInput 单槽——链条走到此处若池已空(挂起会话不会启动,下方 while 是最后一个
|
|
300
|
+
// 消费点)则转正队列照常续发,消息不滞留不并发。池非空时挂起会话先消费 pendingInput
|
|
301
|
+
// (D-S5 输入优先),无需此处处理。单槽语义下残余至多一条。
|
|
289
302
|
if (!skipSession && !state._suspAborted && (state.pendingInput?.length ?? 0) > 0 && !poolLive(agent)) {
|
|
290
|
-
state.queue.push(
|
|
303
|
+
state.queue.push({ text: String(state.pendingInput.shift()) })
|
|
291
304
|
}
|
|
292
305
|
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
306
|
+
// 交接消息自动续发(INPUT-LOCK 单消息——2026-09-09):submit 不再排队(busy 提交吞)
|
|
307
|
+
// + R15 攒批删——state.queue 只剩残项单消息(释放窗口兜底/挂起中止残余——零丢失
|
|
308
|
+
// 承诺)——逐条直发;斜杠命令直接执行(保序);回合后的续发由递归层同循环续取。
|
|
296
309
|
while (state.queue.length > 0 && !state.processing) {
|
|
297
|
-
const head =
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (head.kind === "slash") {
|
|
310
|
+
const head = state.queue.shift()
|
|
311
|
+
// 残项斜杠命令直接执行——保序、绝不合并
|
|
312
|
+
if (head.text.startsWith("/")) {
|
|
301
313
|
await handleSlash(head.text)
|
|
302
314
|
render()
|
|
303
315
|
continue
|
|
304
316
|
}
|
|
305
|
-
pushLabel(`❯ You: (from queue)`, ansi.bold + C.user)
|
|
306
317
|
await runAgentTurn(ctx, head.text)
|
|
307
318
|
return
|
|
308
319
|
}
|
|
@@ -321,4 +332,12 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
321
332
|
// 竞态复现)。
|
|
322
333
|
state._suspAborted = false
|
|
323
334
|
}
|
|
335
|
+
|
|
336
|
+
// 第 33 批(TUI §14.3(d)):顶层链尾(队列续发循环与挂起会话退出**之后**)——无人自动
|
|
337
|
+
// 接手 ⇒ 置 attention 位(渲染层实时派生 blocked/awaiting;用户任意输入清位)。D-AT6:
|
|
338
|
+
// 不放在回合末 finally——那之后还有队列续发 / 挂起会话(digest 自动消费),不是「需要用户」。
|
|
339
|
+
if (userNeededAtTurnEnd(state, agent, skipSession)) {
|
|
340
|
+
state.attentionAwaiting = true
|
|
341
|
+
render()
|
|
342
|
+
}
|
|
324
343
|
}
|
package/src/tui/ansi.mjs
CHANGED
|
@@ -31,6 +31,7 @@ export const ansi = {
|
|
|
31
31
|
dim: `${ESC}[2m`,
|
|
32
32
|
bold: `${ESC}[1m`,
|
|
33
33
|
fg: (n) => `${ESC}[${30 + n}m`,
|
|
34
|
+
bg: (n) => `${ESC}[${40 + n}m`,
|
|
34
35
|
gray: `${ESC}[90m`,
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -44,5 +45,6 @@ export const C = {
|
|
|
44
45
|
dim: ansi.gray,
|
|
45
46
|
warn: ansi.fg(3),
|
|
46
47
|
advisor: `${ESC}[92m`, // bright green — visible on dark backgrounds
|
|
48
|
+
attention: ansi.bg(3) + ansi.fg(0), // 第 33 批(TUI §14.3/D-AT3):attention 态整行色对——黄底黑字(\x1b[43m\x1b[30m,与 VSC warningBackground 同色系;浅深终端均可见——不依赖主题配色)
|
|
47
49
|
fold: ansi.bold + ansi.fg(6), // bold cyan — fold markers must stay visible on light AND dark themes (dim white vanished on light backgrounds)
|
|
48
50
|
}
|
package/src/tui/clipboard.mjs
CHANGED
|
@@ -97,8 +97,14 @@ export function insertPastedText(state, rawText, activeQuestion = null) {
|
|
|
97
97
|
}
|
|
98
98
|
// IKBU3J (2026-08-28):Ctrl+I 注入框激活时,粘贴进注入文本(与按键路径同语义:去换行保单行),
|
|
99
99
|
// 不得落入主输入框——此前缺失该分支导致粘贴进 state.input、Esc 后残留主输入框。
|
|
100
|
+
// 第 31 批:注入框 = { chars, cursor } codepoint 模型——粘贴落 cursor 位置(§8.1 键表)。
|
|
100
101
|
if (state.interruptPrompt) {
|
|
101
|
-
state.interruptPrompt
|
|
102
|
+
const p = state.interruptPrompt
|
|
103
|
+
if (!Array.isArray(p.chars)) p.chars = p.chars ? [...p.chars] : [] // 形态防御(同 question 面——保文本不丢)
|
|
104
|
+
const cur = Math.max(0, Math.min(p.cursor ?? p.chars.length, p.chars.length))
|
|
105
|
+
const chars = [...rawText.replace(/[\r\n]+/g, "").replace(/\t/g, " ")]
|
|
106
|
+
p.chars.splice(cur, 0, ...chars)
|
|
107
|
+
p.cursor = cur + chars.length
|
|
102
108
|
return
|
|
103
109
|
}
|
|
104
110
|
const text = rawText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ")
|
package/src/tui/cmd-advisor.mjs
CHANGED
|
@@ -86,7 +86,7 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
86
86
|
async function thinkingPicker() {
|
|
87
87
|
let thinkIdx = 0
|
|
88
88
|
for (;;) {
|
|
89
|
-
const entries = buildThinkingEntries(agent, cfg)
|
|
89
|
+
const entries = await buildThinkingEntries(agent, cfg)
|
|
90
90
|
const c = await showPicker("Advisor Thinking", entries, { defaultIndex: thinkIdx })
|
|
91
91
|
if (!c) return
|
|
92
92
|
thinkIdx = Math.max(0, entries.filter((e) => e.type === "item").indexOf(c))
|
|
@@ -183,7 +183,8 @@ async function fetchAdvisorModels(agent) {
|
|
|
183
183
|
const result = new Map()
|
|
184
184
|
await Promise.all((agent.providers || []).map(async (p) => {
|
|
185
185
|
try {
|
|
186
|
-
|
|
186
|
+
// format 透传(M1 分派依据——claude/gemini 走 anthropic/google 拉取分支)
|
|
187
|
+
const models = await listModels({ baseURL: p.baseURL, apiKey: p.apiKey ?? "", format: p.format }, { signal: AbortSignal.timeout(10000) })
|
|
187
188
|
result.set(p.name, { models, error: null })
|
|
188
189
|
} catch (err) {
|
|
189
190
|
result.set(p.name, { models: [], error: err.message.slice(0, 40) })
|
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
import { probeChannelModels, dedupeModels } from "./model-catalog.mjs"
|
|
2
3
|
/** Merge an embedding-key save into the raw config, backfilling baseURL/model from defaults.
|
|
3
4
|
* Keeps existing custom values (Ollama/local embedding); defaults are the single source
|
|
4
5
|
* (TUI.md §9.3D — NF1). Exported for unit tests. */
|
|
@@ -12,6 +13,13 @@ export function embeddingPatch(raw, embKey, defaults) {
|
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** 并发池三键当前值读取(POOL-CONFIG-UNIFIED F-3——2026-09-09):配置值合法(≥1 整数)
|
|
17
|
+
* 读配置——非法/缺省回退 DEFAULTS(agent.poolLimits 三键——/config 并发池子菜单/
|
|
18
|
+
* 主菜单/view 三面共用同一读取——config-pool.test.mjs 单测面)。 */
|
|
19
|
+
export function poolCur(pl, defaults) {
|
|
20
|
+
return (k) => (Number.isInteger(pl?.[k]) && pl[k] >= 1 ? pl[k] : defaults.agent.poolLimits[k])
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
/** /config command: view and set agent/embedding/proxy config. */
|
|
16
24
|
export async function handleConfigCommand(ctx, args = []) {
|
|
17
25
|
const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, maskKey, pickModelForSlot } = ctx
|
|
@@ -43,50 +51,53 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
43
51
|
return true
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
/** 保存后的公共重载:loadConfig → injectProxy →
|
|
47
|
-
*
|
|
54
|
+
/** 保存后的公共重载:loadConfig → injectProxy → 恢复运行时 provider 选择。
|
|
55
|
+
* MODEL-MERGE-SESSION 三支改写(cfg 无 active*):运行时由「会话槽复合仍在 providers 中 →
|
|
56
|
+
* 保持槽值」或「config defaultModel(新会话起点)」决定——不再有渠道默认字段可回退。 */
|
|
48
57
|
async function reloadConfig() {
|
|
49
58
|
const { loadConfig } = await import("../config.mjs")
|
|
50
59
|
const { injectProxy } = await import("../proxy.mjs")
|
|
51
60
|
const cfg = loadConfig()
|
|
52
61
|
injectProxy(cfg.providersList, cfg)
|
|
53
|
-
const
|
|
54
|
-
const
|
|
62
|
+
const sessionName = agent.activeProvider
|
|
63
|
+
const sessionModel = agent.activeModel
|
|
55
64
|
agent.providers = cfg.providersList
|
|
56
65
|
agent.config = cfg
|
|
57
66
|
agent.config.agent ??= {}
|
|
58
|
-
const keep = cfg.providersList.find((p) => p.name ===
|
|
59
|
-
if (
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
const keep = sessionName ? cfg.providersList.find((p) => p.name === sessionName) : null
|
|
68
|
+
if (keep) {
|
|
69
|
+
// 会话 provider 仍存在 → 会话复合优先(槽值——不看 cfg 默认;模型空时落默认复合/渠道默认单值)
|
|
70
|
+
const dm = cfg.provider.name ? cfg.provider : null
|
|
71
|
+
const model = sessionModel ?? (dm && dm.name === sessionName ? dm.model : keep.model ?? "")
|
|
72
|
+
agent.activeProvider = keep.name
|
|
73
|
+
agent.activeModel = model || null
|
|
63
74
|
agent.provider = { ...keep }
|
|
64
|
-
|
|
65
|
-
} else if (runtimeName && runtimeName === cfg.activeProvider && runtimeModel) {
|
|
66
|
-
// Same provider, runtime had a model override — keep it
|
|
67
|
-
agent.activeProvider = cfg.activeProvider
|
|
68
|
-
agent.activeModel = runtimeModel
|
|
69
|
-
const p = cfg.providersList.find((pr) => pr.name === cfg.activeProvider)
|
|
70
|
-
agent.provider = p ? { ...p } : cfg.provider
|
|
71
|
-
agent.provider.model = runtimeModel
|
|
72
|
-
agent.provider.proxyUri = p?.proxyUri
|
|
75
|
+
agent.provider.model = model
|
|
73
76
|
} else {
|
|
74
|
-
|
|
75
|
-
agent.
|
|
76
|
-
agent.
|
|
77
|
-
agent.provider
|
|
77
|
+
// 会话 provider 没了(D-S3 静默保留处置)/尚未选 → 采纳 config defaultModel 运行时
|
|
78
|
+
agent.activeProvider = cfg.provider.name ?? ""
|
|
79
|
+
agent.activeModel = cfg.provider.model ?? null
|
|
80
|
+
agent.provider = cfg.provider.name ? { ...cfg.provider } : {}
|
|
81
|
+
}
|
|
82
|
+
// 运行时重建后同步注入结果(cfg.provider 系 loadConfig 内建——injectProxy 后建)
|
|
83
|
+
agent.provider.proxyUri = cfg.providersList.find((p) => p.name === agent.activeProvider)?.proxyUri
|
|
84
|
+
if (agent.config?.agent?.compactThresholdAuto) {
|
|
85
|
+
const { resolveCompactThreshold } = await import("../config.mjs")
|
|
86
|
+
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
78
87
|
}
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
/** 保存 config(mutate 改 raw)→ reloadConfig(provider 代理无需重启即生效)。
|
|
82
91
|
* D-F5b:磁盘新鲜读 → mutate → 写前 mtime 门控(writeConfigAtomic——冲突放弃 +
|
|
83
92
|
* .bak 留现场);冲突时先 reloadConfig 采纳磁盘新值再 throw——调用方 try/catch
|
|
84
|
-
* 统一展示 "Save failed: config changed on disk concurrently — retry"。
|
|
93
|
+
* 统一展示 "Save failed: config changed on disk concurrently — retry"。
|
|
94
|
+
* 写链走 ctx.persistRaw(与 TUI 其余 config 写点同源——createConfigHelpers 的
|
|
95
|
+
* configPath 测试注入缝随之生效;生产 = 同一 writeConfigAtomic 调用)。 */
|
|
85
96
|
async function saveProxy(mutate) {
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
let failure = null
|
|
98
|
+
try { await persistRaw(mutate) } catch (e) { failure = e }
|
|
88
99
|
await reloadConfig()
|
|
89
|
-
if (
|
|
100
|
+
if (failure) throw failure
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
// ── Proxy sub-menu loop:每轮重建 entries 显示最新状态,defaultIndex 记住上次位置 ──
|
|
@@ -231,19 +242,21 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
231
242
|
}
|
|
232
243
|
}
|
|
233
244
|
|
|
234
|
-
// ── 并发池子菜单(§
|
|
245
|
+
// ── 并发池子菜单(§11.1/§11.2——agent.poolLimits——读/改三域;保存经 saveProxy
|
|
235
246
|
// 落盘 + reloadConfig 热应用——下个 spawn 生效)──
|
|
236
247
|
async function poolMenu() {
|
|
237
248
|
let poolIdx = 0
|
|
238
249
|
for (;;) {
|
|
239
250
|
const pl = agent.config?.agent?.poolLimits ?? {}
|
|
240
251
|
// 显示回退与默认同源(DEFAULTS.agent.poolLimits——配置 DEFAULTS 与运行时回退常量
|
|
241
|
-
// ASYNC_POOL_LIMITS 的耦合由 T-24a4
|
|
242
|
-
|
|
252
|
+
// ASYNC_POOL_LIMITS/ADVISOR_POOL_LIMIT 的耦合由 T-24a4 锚定断言(config-pool.test.mjs)
|
|
253
|
+
// 锁住——防默认值单侧漂移)
|
|
254
|
+
const cur = poolCur(pl, DEFAULTS)
|
|
243
255
|
const entries = [
|
|
244
|
-
{ type: "header", text: `Async pools: eng-coder ${cur("engCoder")} / other ${cur("other")}(默认 4/4
|
|
256
|
+
{ type: "header", text: `Async pools: eng-coder ${cur("engCoder")} / other ${cur("other")} / advisor ${cur("advisor")}(默认 4/4/4——分域互不阻塞——subagent 超限排队/advisor 超限即拒)` },
|
|
245
257
|
{ type: "item", text: `eng-coder 池上限 = ${cur("engCoder")}`, action: "engCoder" },
|
|
246
258
|
{ type: "item", text: `其他角色池上限 = ${cur("other")}`, action: "other" },
|
|
259
|
+
{ type: "item", text: `advisor 评审池上限 = ${cur("advisor")}`, action: "advisor" },
|
|
247
260
|
]
|
|
248
261
|
const c = await showPicker("并发池(agent.poolLimits)", entries, { defaultIndex: poolIdx })
|
|
249
262
|
if (!c) return // Esc 返回主菜单
|
|
@@ -252,11 +265,62 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
252
265
|
if (!val) continue // 空输入不改动
|
|
253
266
|
const num = Number(val)
|
|
254
267
|
if (!Number.isInteger(num) || num < 1) { pushLine("Pool limit must be a positive integer (≥1)", C.error); continue }
|
|
255
|
-
|
|
268
|
+
// 写盘全对象(三键——未触碰的键带当前值重写——全非法/空输入不改动)
|
|
269
|
+
const next = { engCoder: cur("engCoder"), other: cur("other"), advisor: cur("advisor"), [c.action]: num }
|
|
270
|
+
try {
|
|
271
|
+
await saveProxy((raw) => { raw.agent ??= {}; raw.agent.poolLimits = { engCoder: next.engCoder, other: next.other, advisor: next.advisor } })
|
|
272
|
+
pushLabel("❯ Config", ansi.bold + C.tool)
|
|
273
|
+
pushLine(`agent.poolLimits = { engCoder: ${next.engCoder}, other: ${next.other}, advisor: ${next.advisor} }(下个 spawn 生效——分域互不阻塞)`, C.tool)
|
|
274
|
+
} catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── 默认模型子菜单(F-5:config.defaultModel 专用入口——L1 provider → L2 **运行期拉取**候选 →
|
|
279
|
+
// 写 config.defaultModel——saveProxy/writeConfigAtomic 通道——不落会话槽;不加手输行——O1 已裁)──
|
|
280
|
+
async function defaultModelMenu() {
|
|
281
|
+
let idx = 0
|
|
282
|
+
// M9 配置阶段准入:进本子菜单探一次各渠道 `/models`(复用 M1 + 会话缓存)。
|
|
283
|
+
// 探通 → 渠道可用 + 候选直接可用;探不通 → 不缓存不阻断(标「不可用」且不入可选来源)。
|
|
284
|
+
const probes = new Map()
|
|
285
|
+
pushLine("Fetching channel model lists (GET /models)…", C.dim)
|
|
286
|
+
await Promise.all(agent.providers.map(async (p) => {
|
|
287
|
+
const r = await probeChannelModels(p)
|
|
288
|
+
probes.set(p.name, r)
|
|
289
|
+
if (r.ok) delete p._unavailable
|
|
290
|
+
else p._unavailable = true
|
|
291
|
+
}))
|
|
292
|
+
for (;;) {
|
|
293
|
+
const dm = agent.config?.defaultModel ?? null
|
|
294
|
+
const entries = [
|
|
295
|
+
{ type: "header", text: `config.defaultModel = ${dm ?? "(not set — 新会话起点未配置)"}` },
|
|
296
|
+
...agent.providers.map((p) => {
|
|
297
|
+
const r = probes.get(p.name)
|
|
298
|
+
if (!r?.ok) return { type: "item", text: `${p.name.padEnd(10)} 不可用`, action: "provider", provider: p.name, unavailable: true }
|
|
299
|
+
const n = r.list.length
|
|
300
|
+
// M3③:渠道行显示渠道默认模型(单值)+ 探得候选数
|
|
301
|
+
return { type: "item", text: `${p.name.padEnd(10)} ${p.model ?? "(no default model)"} (${n} model${n === 1 ? "" : "s"})`, action: "provider", provider: p.name }
|
|
302
|
+
}),
|
|
303
|
+
]
|
|
304
|
+
const c = await showPicker("Default Model (新会话起点)", entries, { defaultIndex: idx })
|
|
305
|
+
if (!c) return // Esc → 返回主菜单
|
|
306
|
+
idx = Math.max(0, entries.filter((e) => e.type === "item").indexOf(c))
|
|
307
|
+
if (c.action !== "provider") continue
|
|
308
|
+
const r = probes.get(c.provider)
|
|
309
|
+
if (!r?.ok) {
|
|
310
|
+
// 准入判据:探不通的渠道不作为默认模型的可选来源——明示原因(M8 长句),不阻断菜单
|
|
311
|
+
pushLine(r?.message ?? `${c.provider} 不可用`, C.error)
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
if (r.list.length === 0) { pushLine(`${c.provider}: /models 未返回任何模型`, C.error); continue }
|
|
315
|
+
const me = await showPicker(`${c.provider} models`, [
|
|
316
|
+
{ type: "header", text: "选为 config.defaultModel(新会话起点——当前会话槽不受影响)" },
|
|
317
|
+
...dedupeModels(r.list).map((m) => ({ type: "item", text: m, action: "model", model: m })),
|
|
318
|
+
])
|
|
319
|
+
if (!me) continue // Esc → 回默认模型菜单
|
|
256
320
|
try {
|
|
257
|
-
await saveProxy((raw) => { raw.
|
|
321
|
+
await saveProxy((raw) => { raw.defaultModel = `${c.provider}:${me.model}` })
|
|
258
322
|
pushLabel("❯ Config", ansi.bold + C.tool)
|
|
259
|
-
pushLine(`
|
|
323
|
+
pushLine(`config.defaultModel = ${c.provider}:${me.model}(新会话起点;会话模型不受影响)`, C.tool)
|
|
260
324
|
} catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
|
|
261
325
|
}
|
|
262
326
|
}
|
|
@@ -271,13 +335,14 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
271
335
|
tc = agent.config?.traces ?? {}
|
|
272
336
|
const consultCount = (ac.consultModels ?? []).length
|
|
273
337
|
const pl = ac.poolLimits ?? {}
|
|
274
|
-
const cur = (
|
|
338
|
+
const cur = poolCur(pl, DEFAULTS)
|
|
275
339
|
const mainEntries = [
|
|
276
340
|
{ type: "header", text: `proxy=${proxySummary()} | maxTurns=${ac.maxTurns ?? 200} | compactThreshold=${ac.compactThreshold ?? 100000} | verifyGuard=${ac.verifyGuard === true ? "on" : "off"} | consult=${consultCount} model(s) | embedding=${agent.memory?.embedder ? "on" : "off"} | traces=${tc.enabled === false ? "off" : "on"}` },
|
|
277
341
|
{ type: "item", text: `agent.maxTurns = ${ac.maxTurns ?? 200}`, action: "agent.maxTurns" },
|
|
278
342
|
{ type: "item", text: `agent.subagentTurns = ${ac.subagentTurns ?? 100}`, action: "agent.subagentTurns" },
|
|
279
|
-
{ type: "item", text: `并发池 agent.poolLimits = engCoder ${cur("engCoder")} / other ${cur("other")}(async 分域上限)`, action: "pool" },
|
|
343
|
+
{ type: "item", text: `并发池 agent.poolLimits = engCoder ${cur("engCoder")} / other ${cur("other")} / advisor ${cur("advisor")}(async 分域上限)`, action: "pool" },
|
|
280
344
|
{ type: "item", text: `agent.compactThreshold = ${ac.compactThreshold ?? 100000}${agent.config?.agent?.compactThresholdAuto ? " (auto)" : ""}`, action: "agent.compactThreshold" },
|
|
345
|
+
{ type: "item", text: `config.defaultModel = ${agent.config?.defaultModel ?? "(未设置 — 新会话起点)"}`, action: "defaultModel" },
|
|
281
346
|
{ type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
|
|
282
347
|
{ type: "item", text: `traces.enabled = ${tc.enabled === false ? "off" : "on"}(轨迹存档——发布默认关——隐私)`, action: "traces.enabled" },
|
|
283
348
|
{ type: "item", text: `traces.retentionHours = ${tc.retentionHours ?? 24} h(超期文件启动时清理)`, action: "traces.retentionHours" },
|
|
@@ -295,7 +360,8 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
295
360
|
|
|
296
361
|
if (choice.action === "view") {
|
|
297
362
|
pushLabel("❯ Config", ansi.bold + C.tool)
|
|
298
|
-
pushLine(`
|
|
363
|
+
pushLine(`Session model: ${agent.activeProvider ? `${agent.activeProvider}:${agent.activeModel ?? ""}` : "(none)"}`, C.dim)
|
|
364
|
+
pushLine(`defaultModel (config): ${agent.config?.defaultModel ?? "(not set — 新会话起点)"}`, C.dim)
|
|
299
365
|
pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
|
|
300
366
|
pushLine(`agent.maxTurns: ${ac.maxTurns ?? 200}`, C.dim)
|
|
301
367
|
pushLine(`agent.subagentTurns: ${ac.subagentTurns ?? 100}`, C.dim)
|
|
@@ -304,7 +370,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
304
370
|
pushLine(`traces.enabled: ${tc.enabled === false ? "off" : "on"}(默认 off——发布隐私——本地分析可开)`, C.dim)
|
|
305
371
|
pushLine(`traces.retentionHours: ${tc.retentionHours ?? 24}(超期文件启动清理——D-TR10)`, C.dim)
|
|
306
372
|
pushLine(`agent.consultModels: ${(ac.consultModels ?? []).map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ") || "(none)"}`, C.dim)
|
|
307
|
-
pushLine(`agent.poolLimits: { engCoder: ${cur("engCoder")}, other: ${cur("other")} }(async 分域上限——默认 4/4——agent.poolLimits 可配)`, C.dim)
|
|
373
|
+
pushLine(`agent.poolLimits: { engCoder: ${cur("engCoder")}, other: ${cur("other")}, advisor: ${cur("advisor")} }(async 分域上限——默认 4/4/4——agent.poolLimits 可配)`, C.dim)
|
|
308
374
|
pushLine(`agent.consultTurns: ${ac.consultTurns ?? 40}`, C.dim)
|
|
309
375
|
pushLine(`agent.consultTimeoutMs: ${Math.round((ac.consultTimeoutMs ?? 600000) / 60000)} min`, C.dim)
|
|
310
376
|
pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${ec.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
|
|
@@ -328,6 +394,11 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
328
394
|
continue
|
|
329
395
|
}
|
|
330
396
|
|
|
397
|
+
if (choice.action === "defaultModel") {
|
|
398
|
+
await defaultModelMenu()
|
|
399
|
+
continue
|
|
400
|
+
}
|
|
401
|
+
|
|
331
402
|
if (choice.action === "embedkey") {
|
|
332
403
|
await setEmbedKey() // 保存成功/取消都回主菜单(Esc 退出)
|
|
333
404
|
continue
|