thincoder 0.12.60 → 0.12.62
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 +49 -2
- package/README.md +8 -6
- package/bin/thincoder.mjs +38 -124
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- package/src/acp.mjs +19 -6
- 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 +24 -7
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +43 -11
- 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/async-settle.mjs +13 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +28 -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 +22 -13
- package/src/agent-tools/read-history.mjs +31 -6
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +18 -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 +10 -7
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +69 -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 +42 -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/context.mjs +12 -1
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +35 -8
- package/src/expand-home.mjs +16 -0
- package/src/generate-title.mjs +9 -4
- package/src/heap-watch.mjs +88 -0
- package/src/hooks.mjs +7 -3
- package/src/ledger.mjs +227 -0
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +77 -36
- package/src/memory/core.mjs +8 -9
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +17 -11
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/scan.mjs +95 -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 +19 -9
- package/src/prompts/advisor-round1.md +8 -2
- package/src/prompts/advisor-round2.md +14 -3
- package/src/prompts/advisor-round3.md +14 -3
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- package/src/prompts/discipline-engineering.md +258 -0
- package/src/prompts/discipline-normal.md +185 -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 +60 -0
- package/src/prompts/persona-engineering.md +55 -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-gc.mjs +9 -2
- package/src/session-guard.mjs +12 -0
- package/src/session-segments.mjs +100 -0
- package/src/session-slots.mjs +10 -2
- package/src/session-store.mjs +441 -0
- package/src/session.mjs +137 -99
- package/src/text-budget.mjs +46 -0
- 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/traces/trace-store.mjs +195 -64
- 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-clear.mjs +2 -0
- 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-new.mjs +5 -1
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +8 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/display-budget.mjs +184 -0
- package/src/tui/index.mjs +69 -40
- package/src/tui/key-handler-search.mjs +9 -1
- 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/ledger-surface.mjs +69 -0
- 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 +38 -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/startup.mjs +45 -13
- package/src/tui/subagent-blocks.mjs +70 -90
- package/src/tui/subagent-children.mjs +125 -67
- package/src/tui/subagent-freeze.mjs +48 -45
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +31 -83
- package/src/tui/tool-args.mjs +9 -4
- package/src/tui/tool-display.mjs +20 -5
- package/src/tui/tool-events.mjs +76 -30
- package/src/tui/tui-lifecycle.mjs +27 -7
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +54 -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
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-clear.mjs
CHANGED
|
@@ -9,12 +9,14 @@ export async function handleClearCommand(ctx) {
|
|
|
9
9
|
], { defaultIndex: 1 })
|
|
10
10
|
if (e?.action === "yes") {
|
|
11
11
|
state.lines = []
|
|
12
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(§15.3.2):行集清空 → 字符账同步归零
|
|
12
13
|
state.streaming = ""
|
|
13
14
|
render()
|
|
14
15
|
}
|
|
15
16
|
return
|
|
16
17
|
}
|
|
17
18
|
state.lines = []
|
|
19
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(§15.3.2):行集清空 → 字符账同步归零
|
|
18
20
|
state.streaming = ""
|
|
19
21
|
render()
|
|
20
22
|
}
|
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
|
package/src/tui/cmd-eng.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/** /eng command: toggle engineering mode.
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
2
|
+
* No prerequisites (FR11): the mode works on any project — no methodology file,
|
|
3
|
+
* no docs tree, no git. The retired gate demanded a methodology file and offered
|
|
4
|
+
* to create it from a template that no longer ships — that option crashed
|
|
5
|
+
* (PORTABILITY P14). ctx: { agent, pushLine, pushLabel } */
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from "node:fs"
|
|
7
|
+
import { dirname } from "node:path"
|
|
7
8
|
import { ansi, C } from "./ansi.mjs"
|
|
8
9
|
import { activeSlot, slotPath } from "../session.mjs"
|
|
9
10
|
import { purgeExpiredDesignTokens } from "../token-ttl.mjs"
|
|
10
11
|
|
|
11
|
-
const templateDir = join(fileURLToPath(import.meta.url), "..", "..", "prompts")
|
|
12
12
|
import { ENG_OFF_REMINDER } from "../agent.mjs"
|
|
13
13
|
|
|
14
14
|
/** Atomic slot write (same shape as session.mjs writeSessionFile — kept local to avoid a
|
|
@@ -26,29 +26,13 @@ export function writeSessionFile(p, data) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export async function handleEngCommand(ctx) {
|
|
29
|
-
const { agent, pushLine, pushLabel
|
|
29
|
+
const { agent, pushLine, pushLabel } = ctx
|
|
30
30
|
agent.config.agent ??= {}
|
|
31
|
-
const methodologyPath = join(agent.cwd, "METHODOLOGY.md")
|
|
32
|
-
|
|
33
|
-
// Toggle on: check METHODOLOGY.md exists
|
|
34
|
-
if (!agent.config.agent.engineering) {
|
|
35
|
-
if (!existsSync(methodologyPath)) {
|
|
36
|
-
pushLabel("❯ Eng", ansi.bold + C.tool)
|
|
37
|
-
pushLine("METHODOLOGY.md not found in project root.", C.warn)
|
|
38
|
-
const choice = await showPicker("Create METHODOLOGY.md?", [
|
|
39
|
-
{ type: "header", text: "Engineering mode requires a methodology file" },
|
|
40
|
-
{ type: "item", text: "Yes, create from template", action: "create" },
|
|
41
|
-
{ type: "item", text: "No, cancel", action: "cancel" },
|
|
42
|
-
])
|
|
43
|
-
if (!choice || choice.action !== "create") return
|
|
44
|
-
const src = join(templateDir, "methodology-template.md")
|
|
45
|
-
copyFileSync(src, methodologyPath)
|
|
46
|
-
pushLine(`Created METHODOLOGY.md (from template) → edit it to fit your project`, C.tool)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
31
|
|
|
32
|
+
// Toggle on/off — no prerequisite check (FR11: engineering mode depends on no
|
|
33
|
+
// project file at all; a missing docs tree or git repo must not block it).
|
|
50
34
|
agent.config.agent.engineering = !agent.config.agent.engineering
|
|
51
|
-
// §
|
|
35
|
+
// §11.2 D-24b: per-review instances die with the mode (fresh convergence cycles
|
|
52
36
|
// on the next toggle).
|
|
53
37
|
agent._advisorRuns = new Map()
|
|
54
38
|
if (!agent.config.agent.engineering) {
|
|
@@ -67,7 +51,7 @@ export async function handleEngCommand(ctx) {
|
|
|
67
51
|
pushLabel("❯ Eng", ansi.bold + C.tool)
|
|
68
52
|
pushLine(`Engineering mode: ${agent.config.agent.engineering ? "ON" : "OFF"} (session)`, C.tool)
|
|
69
53
|
if (agent.config.agent.engineering) {
|
|
70
|
-
pushLine(` →
|
|
54
|
+
pushLine(` → design-before-code enforced (design review + user approval before code)`, C.dim)
|
|
71
55
|
if (clearedExpired > 0) {
|
|
72
56
|
pushLine(` → cleared ${clearedExpired} expired design token${clearedExpired === 1 ? "" : "s"}; valid tokens from prior reviews stay usable`, C.dim)
|
|
73
57
|
}
|
package/src/tui/cmd-exit.mjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
/** /exit command: exit TUI
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export async function handleExitCommand(_ctx) {
|
|
8
|
-
process.exit(0)
|
|
1
|
+
/** /exit command: exit TUI via ctx.exit(F-2——RESIZE-MOUSE-LEAK-FIX——与 Ctrl+C 同退出
|
|
2
|
+
* 形态:cleanup 同步执行 + 延迟 exit——非同派发路径)。不再直调 process.exit(0)(零提前量——
|
|
3
|
+
* 清理押 'exit' 事件);handleSlash 返回后的无条件 render() 由 render-loop tuiActive 守卫
|
|
4
|
+
* 抑制(cleanup 已清活动态——不把 TUI 帧重绘到已恢复的主屏)。 */
|
|
5
|
+
export async function handleExitCommand(ctx) {
|
|
6
|
+
ctx.exit?.() // ctx.exit 由 index.mjs 装配(cleanup + 100ms 延迟 exit)
|
|
9
7
|
}
|
package/src/tui/cmd-model.mjs
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { C } from "./ansi.mjs"
|
|
2
|
+
import { parseModelRef } from "../config.mjs"
|
|
2
3
|
|
|
3
|
-
/** /model command: open model picker, or switch
|
|
4
|
+
/** /model command: open the session model picker, or switch the SESSION model directly via
|
|
5
|
+
* `/model <provider>:<model>`(MODEL-MERGE-SESSION——裁定③:裸 provider 拒——显式 p:m;
|
|
6
|
+
* MODEL-SELECTION v2:显式复合一律放行——仅[空值/裸值/未知 provider]无效(M4),候选清单
|
|
7
|
+
* 由运行期拉取决定——不再有成员校验)。
|
|
8
|
+
* /model 是会话级操作(写槽——不写 config)——config 默认模型走 /config → 默认模型。
|
|
4
9
|
* ctx: { agent, openModelPicker, selectModel, pushLine } */
|
|
5
10
|
export async function handleModelCommand(ctx, args = []) {
|
|
6
|
-
const raw = args[0]?.
|
|
11
|
+
const raw = args[0]?.trim()
|
|
7
12
|
if (!raw) {
|
|
8
13
|
ctx.openModelPicker().catch((e) => ctx.pushLine(`[error] ${e.message}`, C.error))
|
|
9
14
|
return
|
|
10
15
|
}
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (!target) {
|
|
18
|
-
const available = ctx.agent.providers.map((p) => p.name).join(", ")
|
|
19
|
-
ctx.pushLine(`Unknown provider: ${providerName} (available: ${available})`, C.error)
|
|
16
|
+
// 首冒号复合解析(parseModelRef——裸 provider / 未知 provider / 空模型段 → ok:false;
|
|
17
|
+
// 候选外/多冒号一律放行——M4)
|
|
18
|
+
const parsed = parseModelRef(raw, ctx.agent.providers)
|
|
19
|
+
if (!parsed.ok) {
|
|
20
|
+
ctx.pushLine(`[error] ${parsed.reason}`, C.error)
|
|
21
|
+
ctx.pushLine(`/model 用法: /model provider:model(会话级——config 默认走 /config → 默认模型)`, C.dim)
|
|
20
22
|
return
|
|
21
23
|
}
|
|
22
|
-
await ctx.selectModel({ provider:
|
|
24
|
+
await ctx.selectModel({ provider: parsed.provider.name, model: parsed.model }).catch((e) => ctx.pushLine(`[error] ${e.message}`, C.error))
|
|
23
25
|
}
|
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { newSession, resetSessionState } from "../session.mjs"
|
|
1
|
+
import { newSession, resetSessionState, bindRecordStore, slotPath } from "../session.mjs"
|
|
2
2
|
import { C } from "./ansi.mjs"
|
|
3
3
|
|
|
4
4
|
/** /new command: start a new session in a fresh slot.
|
|
@@ -14,8 +14,12 @@ export async function handleNewCommand(ctx) {
|
|
|
14
14
|
// slot(实锤 .19/.3 双副本)。_slot 更新为粘性新槽位。
|
|
15
15
|
resetSessionState(agent)
|
|
16
16
|
agent._slot = slot
|
|
17
|
+
// §14.3.4:/new 绑定新槽记录存储(baseHistory 空——identity 此时为 null = 待固化;
|
|
18
|
+
// 首次保存/首条追加时补写现场身份)
|
|
19
|
+
bindRecordStore(agent, { slotFile: slotPath(agent.cwd, slot), identity: agent._sessionStart ?? null, baseHistory: [] })
|
|
17
20
|
state.tasks = []
|
|
18
21
|
state.lines = []
|
|
22
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(§15.3.2):行集清空 → 字符账同步归零
|
|
19
23
|
state.streaming = ""
|
|
20
24
|
render()
|
|
21
25
|
pushLine(`New session started (slot ${slot}; /session to switch back)`, C.dim)
|
package/src/tui/cmd-reindex.mjs
CHANGED
|
@@ -40,5 +40,12 @@ export async function handleReindexCommand(ctx) {
|
|
|
40
40
|
])
|
|
41
41
|
pushLine(` code: ${cr.total} files, +${cr.updated} ~${cr.skipped} -${cr.removed}`, C.dim)
|
|
42
42
|
pushLine(` doc: ${dr.total} files, +${dr.updated} ~${dr.skipped} -${dr.removed}`, C.dim)
|
|
43
|
+
// Unlisted extensions (PORTABILITY PO-9): files the index skipped because no
|
|
44
|
+
// index claims their extension — visible here + declarable in the project.
|
|
45
|
+
const unlisted = cr?.unlistedExts?.count ? cr.unlistedExts : dr?.unlistedExts
|
|
46
|
+
if (unlisted?.count > 0) {
|
|
47
|
+
const sample = unlisted.exts.map((e) => e.ext).join(" ")
|
|
48
|
+
pushLine(` ${unlisted.count} file(s) with unlisted extension(s) skipped (${sample}) — declare them in .thincoder/conventions.json (index.codeExtensions / index.docExtensions) to index them`, C.warn)
|
|
49
|
+
}
|
|
43
50
|
pushLine(`[reindex] Done, ${total} entries total. Vectors will be lazily generated on next search.`, C.tool)
|
|
44
51
|
}
|
package/src/tui/cmd-session.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker } from "../session.mjs"
|
|
1
|
+
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker, sessionDescriptor } from "../session.mjs"
|
|
2
2
|
import { ansi, C } from "./ansi.mjs"
|
|
3
3
|
import { restoreLines } from "./startup.mjs"
|
|
4
4
|
import { stringWidth, sliceByWidth } from "./render.mjs"
|
|
@@ -89,14 +89,18 @@ export async function handleSessionCommand(ctx) {
|
|
|
89
89
|
if (occ.occupied) {
|
|
90
90
|
pushLine(`⚠ Slot ${e.slot} is being used by another live process (${occ.owner}) — continuing here will create a new copy on the next save`, C.warn)
|
|
91
91
|
}
|
|
92
|
-
applySession(agent, data)
|
|
92
|
+
applySession(agent, data, occ.occupied ? {} : { slot: e.slot })
|
|
93
93
|
// Rebuild from history (lazy) — the display snapshot is deprecated.
|
|
94
|
+
// §14.3.6:描述符 { history(尾窗+±1), total, base }——标签口径 = total(非窗口长度);
|
|
95
|
+
// 未绑定(占用分支——模式 F)回退全量数组。
|
|
96
|
+
const desc = sessionDescriptor(agent, data)
|
|
94
97
|
state.lines = []
|
|
95
|
-
|
|
98
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(§15.3.2):切槽重建行集 → 字符账归零,restoreLines 重新入账
|
|
99
|
+
restoreLines(state, desc)
|
|
96
100
|
state.tasks = agent.tasks ?? []
|
|
97
101
|
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
98
102
|
state.tasks = []
|
|
99
103
|
}
|
|
100
|
-
pushLabel(`── Switched to slot ${e.slot} (${
|
|
104
|
+
pushLabel(`── Switched to slot ${e.slot} (${desc.total} messages) ──`, C.warn)
|
|
101
105
|
render()
|
|
102
106
|
}
|
package/src/tui/cmd-submodel.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
2
|
|
|
3
3
|
/** Subagent role slots — each has an independent model override slot */
|
|
4
|
-
export const SUBMODEL_SLOTS = ["explore", "plan", "coder", "eng-coder"]
|
|
4
|
+
export const SUBMODEL_SLOTS = ["explore", "plan", "coder", "eng-coder", "eng-designer"]
|
|
5
5
|
|
|
6
6
|
/** Human-readable effective display with inheritance source. role=null → global slot.
|
|
7
7
|
* source semantics: "type" = role-specific override (subagentModels[role]),
|
|
@@ -21,12 +21,14 @@ function slotDisplay(agent, role) {
|
|
|
21
21
|
|
|
22
22
|
/** /submodel command: subagent model config — picker menu (default) or direct args.
|
|
23
23
|
* ctx: { agent, pushLine, showPicker, askQuestion, persistRaw, pickModelForSlot }
|
|
24
|
-
* /submodel → picker: global +
|
|
24
|
+
* /submodel → picker: global + 5 role slots
|
|
25
25
|
* /submodel <value> → set global default
|
|
26
26
|
* /submodel <type> <value> → set a role slot
|
|
27
27
|
* /submodel <type> → show a slot
|
|
28
28
|
* /submodel reset [type] → clear global (or a slot)
|
|
29
|
-
* value forms: provider:model | provider name | model name (same as subagent tool model arg)
|
|
29
|
+
* value forms: provider:model | provider name | model name (same as subagent tool model arg) —
|
|
30
|
+
* MODEL-MERGE-SESSION:会话模型 = provider:model 复合(agent.activeProvider/activeModel 双字段
|
|
31
|
+
* 恒非空——无渠道默认字段——父会话模型引用一律复合值)。 */
|
|
30
32
|
export async function handleSubmodelCommand(ctx, args = []) {
|
|
31
33
|
const { agent, pushLine, showPicker, askQuestion, persistRaw, pickModelForSlot } = ctx
|
|
32
34
|
const input = args.join(" ").trim()
|
|
@@ -85,7 +87,7 @@ export async function handleSubmodelCommand(ctx, args = []) {
|
|
|
85
87
|
return
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
// ── Picker menu: global +
|
|
90
|
+
// ── Picker menu: global + 5 role slots ──
|
|
89
91
|
// for(;;) loop relies on showPicker's async/Promise suspension — every iteration
|
|
90
92
|
// awaits a user choice; Esc (null) exits. Mirrors openModelPicker's menu-loop pattern.
|
|
91
93
|
for (;;) {
|
|
@@ -132,7 +134,8 @@ export async function handleSubmodelCommand(ctx, args = []) {
|
|
|
132
134
|
pushLine(`Subagent global model set to \`${value}\`.`, C.text)
|
|
133
135
|
}
|
|
134
136
|
} else if (sub.action === "parent") {
|
|
135
|
-
|
|
137
|
+
// 会话复合值语义(MODEL-MERGE-SESSION——activeModel 恒非空——不再回退渠道字段)
|
|
138
|
+
const value = `${agent.activeProvider}:${agent.activeModel ?? ""}`
|
|
136
139
|
if (role) {
|
|
137
140
|
await persist((a) => { a.subagentModels ??= {}; a.subagentModels[role] = value })
|
|
138
141
|
} else {
|
package/src/tui/cmd-undo.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync, writeFileSync, unlinkSync, readFileSync } from "node:fs"
|
|
9
|
-
import { join } from "node:path"
|
|
9
|
+
import { isAbsolute, join } from "node:path"
|
|
10
10
|
import { ansi, C } from "./ansi.mjs"
|
|
11
11
|
|
|
12
12
|
const MAX_UNDO = 50
|
|
@@ -20,7 +20,8 @@ export function snapshotForUndo(agent, toolName, args, cwd) {
|
|
|
20
20
|
const path = args.path ?? args.file
|
|
21
21
|
if (!path || typeof path !== "string") return
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// 绝对路径直接用(path.join 对绝对段不重置——Windows 反斜杠路径也不按 "/" 切分);相对路径整段 join(cwd)。
|
|
24
|
+
const abs = isAbsolute(path) ? path : join(cwd, path)
|
|
24
25
|
let backup = null
|
|
25
26
|
try {
|
|
26
27
|
if (existsSync(abs)) {
|
|
@@ -68,7 +69,7 @@ export async function handleUndoCommand(ctx) {
|
|
|
68
69
|
const e = await showPicker("Undo", entries)
|
|
69
70
|
if (!e) return
|
|
70
71
|
const item = stack[e.idx]
|
|
71
|
-
const abs = join(agent.cwd,
|
|
72
|
+
const abs = isAbsolute(item.path) ? item.path : join(agent.cwd, item.path)
|
|
72
73
|
|
|
73
74
|
try {
|
|
74
75
|
if (item.backup === null) {
|