thincoder 0.12.59 → 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 +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- 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 +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -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 +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- 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 +18 -98
- package/src/provider/errors.mjs +101 -0
- 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/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- 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 +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +40 -38
- package/src/tui/interaction.mjs +3 -3
- 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 +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
package/src/tui/agent-turn.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* agent-turn.mjs — runAgentTurn:一个用户回合的驱动器(submit /
|
|
3
|
-
* + §17 挂起会话(suspensionSession:async 子代理后台运行期间主会话可继续对话)。
|
|
2
|
+
* agent-turn.mjs — runAgentTurn:一个用户回合的驱动器(submit / 队列递归入口)。
|
|
4
3
|
*
|
|
5
4
|
* 2026-08-30 拆分(回回 500 行硬限):回合生命周期(状态复位 → runAgent 循环 →
|
|
6
5
|
* 错误/Continue/中断处理 → finally 收尾 → 队列)留在这里;工具事件 → TUI 状态的
|
|
@@ -9,35 +8,36 @@
|
|
|
9
8
|
* (routeSub* / finishSubTask / freeze*SubTasks)在 subagent-blocks.mjs;标题生成
|
|
10
9
|
* 在 generate-title.mjs ensureSessionTitle。
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* 2026-09-05 module-split:§17 挂起会话段(suspensionSession/digestTurn/poolLive/
|
|
12
|
+
* poolCounts 等——agent-turn.mjs 535 > 500 硬限)verbatim 迁至 suspension-drive.mjs——
|
|
13
|
+
* 本文件回合尾经 suspensionSession 进入驱动器;驱动器内 digest/用户回合经 runAgentTurn
|
|
14
|
+
* 递归回本文件(函数级静态环——模块求值期无顶层调用,环安全——session-slots ↔
|
|
15
|
+
* session.mjs 同款先例)。
|
|
16
16
|
*/
|
|
17
17
|
import { runAgent, ContinueError } from "../agent.mjs"
|
|
18
18
|
import { saveSession } from "../session.mjs"
|
|
19
19
|
import { ansi, C } from "./ansi.mjs"
|
|
20
20
|
import { buildToolCallbacks, sweepToolBlocks } from "./tool-events.mjs"
|
|
21
|
-
import { freezeAllSubTasks
|
|
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 } from "./suspension-drive.mjs"
|
|
24
25
|
|
|
25
26
|
/** Exit-flush bound for the async end-of-run distillation (SEND-STALL-DISTILL §2.5):
|
|
26
27
|
* wait at most this long for the in-flight distill before the final session save —
|
|
27
28
|
* never let shutdown hang on the background summary call. */
|
|
28
29
|
const DISTILL_FLUSH_TIMEOUT_MS = 5000
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
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
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -295,24 +295,26 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
295
295
|
render()
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
// §17 偏差 #1
|
|
299
|
-
|
|
300
|
-
//
|
|
298
|
+
// §17 偏差 #1 兜底(INPUT-LOCK 单槽化——2026-09-09):释放窗口期 Enter 已入
|
|
299
|
+
// pendingInput 单槽——链条走到此处若池已空(挂起会话不会启动,下方 while 是最后一个
|
|
300
|
+
// 消费点)则转正队列照常续发,消息不滞留不并发。池非空时挂起会话先消费 pendingInput
|
|
301
|
+
// (D-S5 输入优先),无需此处处理。单槽语义下残余至多一条。
|
|
301
302
|
if (!skipSession && !state._suspAborted && (state.pendingInput?.length ?? 0) > 0 && !poolLive(agent)) {
|
|
302
|
-
state.queue.push(
|
|
303
|
+
state.queue.push({ text: String(state.pendingInput.shift()) })
|
|
303
304
|
}
|
|
304
305
|
|
|
305
|
-
//
|
|
306
|
+
// 交接消息自动续发(INPUT-LOCK 单消息——2026-09-09):submit 不再排队(busy 提交吞)
|
|
307
|
+
// + R15 攒批删——state.queue 只剩残项单消息(释放窗口兜底/挂起中止残余——零丢失
|
|
308
|
+
// 承诺)——逐条直发;斜杠命令直接执行(保序);回合后的续发由递归层同循环续取。
|
|
306
309
|
while (state.queue.length > 0 && !state.processing) {
|
|
307
|
-
const
|
|
308
|
-
//
|
|
309
|
-
if (
|
|
310
|
-
await handleSlash(
|
|
310
|
+
const head = state.queue.shift()
|
|
311
|
+
// 残项斜杠命令直接执行——保序、绝不合并
|
|
312
|
+
if (head.text.startsWith("/")) {
|
|
313
|
+
await handleSlash(head.text)
|
|
311
314
|
render()
|
|
312
315
|
continue
|
|
313
316
|
}
|
|
314
|
-
|
|
315
|
-
await runAgentTurn(ctx, next.text)
|
|
317
|
+
await runAgentTurn(ctx, head.text)
|
|
316
318
|
return
|
|
317
319
|
}
|
|
318
320
|
|
|
@@ -330,206 +332,12 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
330
332
|
// 竞态复现)。
|
|
331
333
|
state._suspAborted = false
|
|
332
334
|
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// ─── §17 挂起会话(AGENT-LOOP.md §17 D-S2/D-S9 状态机行表)────────────────
|
|
336
|
-
|
|
337
|
-
/** 后台池存活判据(D-S2/F5 口径):running/queued 子代理,或已 settle 未注入结果
|
|
338
|
-
* (_pendingAsyncResults 非空 = D-S3 "未注入")。回合尾与每次轮末都用它评估退出。 */
|
|
339
|
-
function poolLive(agent) {
|
|
340
|
-
const map = agent._asyncSubagents
|
|
341
|
-
return (map && map.size > 0) || (agent._pendingAsyncResults?.length ?? 0) > 0
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/** D-S3 ③ 记账清扫:回合边界竞态落下的已 settle 项(settle 回调未及移交——发生在
|
|
345
|
-
* 回合刚结束、_suspended 尚未置位的窗口)补入 pending。幂等:回调已移交的条目已
|
|
346
|
-
* 从 map 删除并带 _inPending 标记,不会重复入列。 */
|
|
347
|
-
function sweepSettledToPending(agent) {
|
|
348
|
-
const map = agent._asyncSubagents
|
|
349
|
-
if (!map || map.size === 0) return
|
|
350
|
-
agent._pendingAsyncResults ??= []
|
|
351
|
-
for (const e of [...map.values()]) {
|
|
352
|
-
if (e.done && !e._inPending) {
|
|
353
|
-
e._inPending = true
|
|
354
|
-
agent._pendingAsyncResults.push(e)
|
|
355
|
-
map.delete(String(e.id))
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/** 等待下一次 settle(running 子代理 promise 完成)或用户唤醒(Enter 入队 / Ctrl+C /
|
|
361
|
-
* 会话 abort)。唤醒器经 state._suspWake 单槽注入;abort 监听兜底。 */
|
|
362
|
-
function waitForSettleOrWake(agent, state) {
|
|
363
|
-
return new Promise((resolve) => {
|
|
364
|
-
let finished = false
|
|
365
|
-
const cleanup = () => {
|
|
366
|
-
state._suspWake = null
|
|
367
|
-
const i = (agent._asyncWaiters ?? []).indexOf(w)
|
|
368
|
-
if (i >= 0) agent._asyncWaiters.splice(i, 1)
|
|
369
|
-
agent._sessionAbort?.signal.removeEventListener("abort", onAbort)
|
|
370
|
-
}
|
|
371
|
-
const finish = (why) => {
|
|
372
|
-
if (finished) return
|
|
373
|
-
finished = true
|
|
374
|
-
cleanup()
|
|
375
|
-
resolve(why)
|
|
376
|
-
}
|
|
377
|
-
const w = () => finish("settle")
|
|
378
|
-
const wake = () => finish("wake")
|
|
379
|
-
const onAbort = () => finish("aborted")
|
|
380
|
-
;(agent._asyncWaiters ??= []).push(w)
|
|
381
|
-
state._suspWake = wake
|
|
382
|
-
if (agent._sessionAbort?.signal.aborted) { onAbort(); return }
|
|
383
|
-
agent._sessionAbort?.signal.addEventListener("abort", onAbort, { once: true })
|
|
384
|
-
})
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/** 后台模式状态行文本(D-S8;17.5.4 #6 顺手对齐):"后台 N 子代理运行中 · M 完成待消化"
|
|
388
|
-
* ——"运行中" = running + queued;"完成待消化" = pending 移交项 + §17.5 回合尾留池的
|
|
389
|
-
* settled 未消费项(挂起会话 sweep 前的可见窗口)。 */
|
|
390
|
-
function backgroundStatusText(agent) {
|
|
391
|
-
const map = agent._asyncSubagents
|
|
392
|
-
const running = map ? [...map.values()].filter((e) => e.status === "running").length : 0
|
|
393
|
-
const queued = agent._asyncQueue?.length ?? 0
|
|
394
|
-
const pending = agent._pendingAsyncResults?.length ?? 0
|
|
395
|
-
const doneInPool = map ? [...map.values()].filter((e) => e.done).length : 0 // §17.5 留池未消费
|
|
396
|
-
const awaiting = pending + doneInPool
|
|
397
|
-
const active = running + queued
|
|
398
|
-
return active > 0 || awaiting > 0
|
|
399
|
-
? `后台 ${active} 子代理运行中${awaiting ? ` · ${awaiting} 完成待消化` : ""}`
|
|
400
|
-
: "后台子代理收尾…"
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/** 消化轮:系统驱动的 auto-turn(D-S6)。手动档不传权限/问答 handler(D-S7 装配
|
|
404
|
-
* 契约——denied 不弹面板、不悬挂);AUTO 档沿用普通回调(autoApprove 短路自动
|
|
405
|
-
* 执行)。_suspended 保持 true:消化中 settle 延迟冻结 + 移交 pending。 */
|
|
406
|
-
async function digestTurn(ctx) {
|
|
407
|
-
const { agent, pushLine } = ctx
|
|
408
|
-
const manual = !agent.autoApprove
|
|
409
|
-
pushLine(manual
|
|
410
|
-
? "[auto-turn: digesting finished subagent reports…]"
|
|
411
|
-
: "[auto-turn: continuing background work…]", C.dim)
|
|
412
|
-
const digestCtx = manual
|
|
413
|
-
? { ...ctx, askPermission: null, askBatchPermission: null, askQuestion: null }
|
|
414
|
-
: ctx
|
|
415
|
-
// LOGGING:digest:* 事件(D-S9 消化轮边界——LOGGING.md F-L4 挂起态覆盖)
|
|
416
|
-
const d0 = Date.now()
|
|
417
|
-
const pend0 = agent?._pendingAsyncResults?.length ?? 0
|
|
418
|
-
logEvent("digest:start", { pendingN: pend0 })
|
|
419
|
-
await runAgentTurn(digestCtx, "", { autoTurn: true, skipSession: true })
|
|
420
|
-
logEvent("digest:end", { pendingN: agent?._pendingAsyncResults?.length ?? 0, ms: Date.now() - d0 })
|
|
421
|
-
}
|
|
422
335
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
* 续开合并消化轮或回挂起;pendingInput 非空 → 以该消息开新回合(不触发新 digest);
|
|
429
|
-
* - §17.5.5:每次消化/会话内用户回合消费 pending 后 → freezeReclaimDigestedBlocks
|
|
430
|
-
* 逐条冻结回收(消化完成块不滞留面板——不等池空;settle 锚点 splice——digest 总览
|
|
431
|
-
* 文本之前——round1 #1 裁定);
|
|
432
|
-
* - 退出:池空 + pending 空 + 无待处理输入 → freezeAllSubTasks 补发冻结(仅兜底
|
|
433
|
-
* 未消化残项——17.5.5 块回收与池空解耦)→ idle。
|
|
434
|
-
* _suspended 翻转:会话期 true(settle 回调据此延迟冻结 + 移交 pending);会话内
|
|
435
|
-
* 用户回合执行期翻 false(普通回合语义:settle 即冻结 + 回合尾直注入 ①)。
|
|
436
|
-
*/
|
|
437
|
-
async function suspensionSession(ctx) {
|
|
438
|
-
const { agent, state, render, pushLine } = ctx
|
|
439
|
-
state.pendingInput ??= []
|
|
440
|
-
state._suspAborted = false
|
|
441
|
-
agent._suspended = true
|
|
442
|
-
agent._sessionSignal = agent._sessionAbort.signal // 会话内 spawn 的 children 共享(subagent.mjs)
|
|
443
|
-
state.suspended = true
|
|
444
|
-
state._suspPending = false // 偏差 #1:进入真正挂起态——标志只在释放窗口期有效(此后由 state.suspended 分流)
|
|
445
|
-
const suspTick = setInterval(() => {
|
|
446
|
-
if (state.suspended && !state.processing) {
|
|
447
|
-
state.status = backgroundStatusText(agent)
|
|
448
|
-
render()
|
|
449
|
-
}
|
|
450
|
-
}, 1000)
|
|
451
|
-
state.status = backgroundStatusText(agent)
|
|
452
|
-
render()
|
|
453
|
-
// LOGGING:susp:* 事件(挂起态进入/退出——F-L4;挂起期输入事件 v1 不记——refinement #1)
|
|
454
|
-
const s0 = Date.now()
|
|
455
|
-
logEvent("susp:enter", poolCounts(agent))
|
|
456
|
-
try {
|
|
457
|
-
while (!state._suspAborted && !agent._sessionAbort.signal.aborted) {
|
|
458
|
-
sweepSettledToPending(agent)
|
|
459
|
-
// 1. 用户输入优先(D-S5):pendingInput 队列 + 消化期排队的 slash 命令
|
|
460
|
-
const queuedText = state.pendingInput.length > 0 ? state.pendingInput.shift()
|
|
461
|
-
: state.queue.length > 0 ? state.queue.shift().text : null
|
|
462
|
-
if (queuedText) {
|
|
463
|
-
if (String(queuedText).startsWith("/")) {
|
|
464
|
-
await ctx.handleSlash?.(queuedText)
|
|
465
|
-
render()
|
|
466
|
-
continue
|
|
467
|
-
}
|
|
468
|
-
agent._suspended = false // 用户回合 = 普通回合语义(① 直注入 + settle 即冻结)
|
|
469
|
-
await runAgentTurn(ctx, String(queuedText), { skipSession: true })
|
|
470
|
-
agent._suspended = true
|
|
471
|
-
// §17.5.5:该回合消化完 pending(run 首行注入)→ 逐条冻结回收驻留块
|
|
472
|
-
// (不等池空——settle 锚点 splice——digest 总览文本之前;与 digest 回收同规则)
|
|
473
|
-
freezeReclaimDigestedBlocks(state, agent._pendingAsyncResults ?? [])
|
|
474
|
-
state.status = backgroundStatusText(agent)
|
|
475
|
-
continue
|
|
476
|
-
}
|
|
477
|
-
// 2. pending 非空 → 合并消化轮(注入由 runAgent 首行统一完成——D-S3 单注入点)
|
|
478
|
-
if ((agent._pendingAsyncResults?.length ?? 0) > 0) {
|
|
479
|
-
await digestTurn(ctx)
|
|
480
|
-
// §17.5.5 实测修订(2026-09-03):digest 消化完成(pending 条目已注入)→ 逐条补发
|
|
481
|
-
// done 冻结回收——不等池空——块从面板移除进流(settle 锚点 splice 落位——digest
|
|
482
|
-
// 总览文本之前——round1 #1 裁定);池空 freeze-out 仅兜底未消化残项(挂起会话
|
|
483
|
-
// 结束统一清场)——块回收与池空解耦(T-H7/AC-H5)。
|
|
484
|
-
// 归属不变式:会话内任何 run 开始前 pinned 块(awaitingDigest)的条目必在 pending
|
|
485
|
-
// ——run 消费后不在 pending 的 pinned 块即本 run 消化者(无需快照即精确归属)。
|
|
486
|
-
freezeReclaimDigestedBlocks(state, agent._pendingAsyncResults ?? [])
|
|
487
|
-
state.status = backgroundStatusText(agent)
|
|
488
|
-
continue
|
|
489
|
-
}
|
|
490
|
-
// 3. 池空(无 running/queued/未注入)→ 自然退出回 idle(补发冻结在 finally)
|
|
491
|
-
if (!poolLive(agent)) break
|
|
492
|
-
// 4. 等下一 settle / 用户唤醒(Enter 入队、Ctrl+C)
|
|
493
|
-
await waitForSettleOrWake(agent, state)
|
|
494
|
-
}
|
|
495
|
-
} finally {
|
|
496
|
-
clearInterval(suspTick)
|
|
497
|
-
const aborted = state._suspAborted || agent._sessionAbort.signal.aborted
|
|
498
|
-
if (aborted && (agent._asyncSubagents?.size ?? 0) > 0) logEvent("ev:stopped", { poolN: agent._asyncSubagents?.size ?? 0, where: "suspension-abort" })
|
|
499
|
-
logEvent("susp:exit", { ...poolCounts(agent), ms: Date.now() - s0, reason: aborted ? "aborted" : "idle" })
|
|
500
|
-
agent._suspended = false
|
|
501
|
-
agent._sessionSignal = null
|
|
502
|
-
agent._sessionAbort = null
|
|
503
|
-
agent._sessionAbortAll = null // 偏差 #3:会话期 controller 集合随句柄一并释放
|
|
504
|
-
state.suspended = false
|
|
505
|
-
state._suspWake = null
|
|
506
|
-
state.suspAbortArmed = false // round2 偏差 #4:会话退出即解除挂起中止武装(防跨会话粘滞)
|
|
507
|
-
if (aborted) {
|
|
508
|
-
// §15 abort 语义:清池不注入(用户显式停——不注入陈旧错误)
|
|
509
|
-
agent._asyncSubagents?.clear()
|
|
510
|
-
agent._asyncQueue = []
|
|
511
|
-
agent._pendingAsyncResults = []
|
|
512
|
-
// §17 round2 偏差 #2-CLI(code review round2 #2-CLI):中止时不静默丢弃挂起期
|
|
513
|
-
// 排队的用户消息——Enter 已清空输入框并入 pendingInput(用户视为已发送),
|
|
514
|
-
// 残余转回 state.queue({text} 条目,下个普通回合的队列循环续发——零丢失)
|
|
515
|
-
// + 提示行明示去向(不静默丢)。
|
|
516
|
-
const queuedN = state.pendingInput?.length ?? 0
|
|
517
|
-
if (queuedN > 0) {
|
|
518
|
-
state.queue.push(...state.pendingInput.splice(0).map((t) => ({ text: String(t) })))
|
|
519
|
-
pushLine(`[background work stopped — ${queuedN} queued message${queuedN > 1 ? "s" : ""} will run as a normal turn]`, C.warn)
|
|
520
|
-
}
|
|
521
|
-
} else {
|
|
522
|
-
// D-S3 ③ 兜底:退出前残余(极端竞态)直注入再退——结果零丢失(AC-S2)
|
|
523
|
-
const residual = agent._pendingAsyncResults
|
|
524
|
-
if (residual?.length) {
|
|
525
|
-
const { injectAsyncResult } = await import("../agent-tools/subagent.mjs")
|
|
526
|
-
for (const e of residual.splice(0)) await injectAsyncResult(agent, e)
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
// 补发 done 冻结:驻留面板的 awaiting-digest 块随池空冻结进流(T-S14)
|
|
530
|
-
freezeAllSubTasks(state)
|
|
531
|
-
sweepToolBlocks(state)
|
|
532
|
-
state.status = "Ready"
|
|
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
|
|
533
341
|
render()
|
|
534
342
|
}
|
|
535
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,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
2
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
|
+
import { probeChannelModels, dedupeModels } from "./model-catalog.mjs"
|
|
3
3
|
/** Merge an embedding-key save into the raw config, backfilling baseURL/model from defaults.
|
|
4
4
|
* Keeps existing custom values (Ollama/local embedding); defaults are the single source
|
|
5
5
|
* (TUI.md §9.3D — NF1). Exported for unit tests. */
|
|
@@ -13,6 +13,13 @@ export function embeddingPatch(raw, embKey, defaults) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
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
|
+
|
|
16
23
|
/** /config command: view and set agent/embedding/proxy config. */
|
|
17
24
|
export async function handleConfigCommand(ctx, args = []) {
|
|
18
25
|
const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, maskKey, pickModelForSlot } = ctx
|
|
@@ -31,9 +38,10 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
31
38
|
async function setEmbedKey() {
|
|
32
39
|
const embKey = await askQuestion("Enter embedding API key (default: SiliconFlow bge-m3):")
|
|
33
40
|
if (!embKey) return false
|
|
41
|
+
// D-F5b 语义先盘后存(embeddingPatch 在磁盘 fresh raw 上合并——冲突放弃不留 ghost)
|
|
42
|
+
await persistRaw((raw) => { raw.embedding = embeddingPatch(raw, embKey, DEFAULTS.embedding) })
|
|
34
43
|
agent.config.embedding ??= {}
|
|
35
44
|
agent.config.embedding.apiKey = embKey
|
|
36
|
-
await persistRaw((raw) => { raw.embedding = embeddingPatch(raw, embKey, DEFAULTS.embedding) })
|
|
37
45
|
if (agent.memory) {
|
|
38
46
|
const { createEmbedder } = await import("../embedding.mjs")
|
|
39
47
|
agent.memory.embedder = createEmbedder(agent.config.embedding)
|
|
@@ -43,48 +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
|
-
/** 保存 config(mutate 改 raw)→ reloadConfig(provider
|
|
90
|
+
/** 保存 config(mutate 改 raw)→ reloadConfig(provider 代理无需重启即生效)。
|
|
91
|
+
* D-F5b:磁盘新鲜读 → mutate → 写前 mtime 门控(writeConfigAtomic——冲突放弃 +
|
|
92
|
+
* .bak 留现场);冲突时先 reloadConfig 采纳磁盘新值再 throw——调用方 try/catch
|
|
93
|
+
* 统一展示 "Save failed: config changed on disk concurrently — retry"。
|
|
94
|
+
* 写链走 ctx.persistRaw(与 TUI 其余 config 写点同源——createConfigHelpers 的
|
|
95
|
+
* configPath 测试注入缝随之生效;生产 = 同一 writeConfigAtomic 调用)。 */
|
|
82
96
|
async function saveProxy(mutate) {
|
|
83
|
-
|
|
84
|
-
mutate(
|
|
85
|
-
const { saveConfig } = await import("../config.mjs")
|
|
86
|
-
saveConfig(raw)
|
|
97
|
+
let failure = null
|
|
98
|
+
try { await persistRaw(mutate) } catch (e) { failure = e }
|
|
87
99
|
await reloadConfig()
|
|
100
|
+
if (failure) throw failure
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
// ── Proxy sub-menu loop:每轮重建 entries 显示最新状态,defaultIndex 记住上次位置 ──
|
|
@@ -229,6 +242,89 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
229
242
|
}
|
|
230
243
|
}
|
|
231
244
|
|
|
245
|
+
// ── 并发池子菜单(§11.1/§11.2——agent.poolLimits——读/改三域;保存经 saveProxy
|
|
246
|
+
// 落盘 + reloadConfig 热应用——下个 spawn 生效)──
|
|
247
|
+
async function poolMenu() {
|
|
248
|
+
let poolIdx = 0
|
|
249
|
+
for (;;) {
|
|
250
|
+
const pl = agent.config?.agent?.poolLimits ?? {}
|
|
251
|
+
// 显示回退与默认同源(DEFAULTS.agent.poolLimits——配置 DEFAULTS 与运行时回退常量
|
|
252
|
+
// ASYNC_POOL_LIMITS/ADVISOR_POOL_LIMIT 的耦合由 T-24a4 锚定断言(config-pool.test.mjs)
|
|
253
|
+
// 锁住——防默认值单侧漂移)
|
|
254
|
+
const cur = poolCur(pl, DEFAULTS)
|
|
255
|
+
const entries = [
|
|
256
|
+
{ type: "header", text: `Async pools: eng-coder ${cur("engCoder")} / other ${cur("other")} / advisor ${cur("advisor")}(默认 4/4/4——分域互不阻塞——subagent 超限排队/advisor 超限即拒)` },
|
|
257
|
+
{ type: "item", text: `eng-coder 池上限 = ${cur("engCoder")}`, action: "engCoder" },
|
|
258
|
+
{ type: "item", text: `其他角色池上限 = ${cur("other")}`, action: "other" },
|
|
259
|
+
{ type: "item", text: `advisor 评审池上限 = ${cur("advisor")}`, action: "advisor" },
|
|
260
|
+
]
|
|
261
|
+
const c = await showPicker("并发池(agent.poolLimits)", entries, { defaultIndex: poolIdx })
|
|
262
|
+
if (!c) return // Esc 返回主菜单
|
|
263
|
+
poolIdx = Math.max(0, entries.filter((e) => e.type === "item").indexOf(c))
|
|
264
|
+
const val = await askQuestion(`${c.action} pool limit (current: ${cur(c.action)} — positive integer ≥1):`)
|
|
265
|
+
if (!val) continue // 空输入不改动
|
|
266
|
+
const num = Number(val)
|
|
267
|
+
if (!Number.isInteger(num) || num < 1) { pushLine("Pool limit must be a positive integer (≥1)", C.error); continue }
|
|
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 → 回默认模型菜单
|
|
320
|
+
try {
|
|
321
|
+
await saveProxy((raw) => { raw.defaultModel = `${c.provider}:${me.model}` })
|
|
322
|
+
pushLabel("❯ Config", ansi.bold + C.tool)
|
|
323
|
+
pushLine(`config.defaultModel = ${c.provider}:${me.model}(新会话起点;会话模型不受影响)`, C.tool)
|
|
324
|
+
} catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
232
328
|
// ── Main config loop ──
|
|
233
329
|
let running = true
|
|
234
330
|
let mainIdx = 0 // 记住上次选中位置,改完一项回主菜单时恢复
|
|
@@ -238,11 +334,15 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
238
334
|
ec = agent.config?.embedding ?? {}
|
|
239
335
|
tc = agent.config?.traces ?? {}
|
|
240
336
|
const consultCount = (ac.consultModels ?? []).length
|
|
337
|
+
const pl = ac.poolLimits ?? {}
|
|
338
|
+
const cur = poolCur(pl, DEFAULTS)
|
|
241
339
|
const mainEntries = [
|
|
242
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"}` },
|
|
243
341
|
{ type: "item", text: `agent.maxTurns = ${ac.maxTurns ?? 200}`, action: "agent.maxTurns" },
|
|
244
342
|
{ type: "item", text: `agent.subagentTurns = ${ac.subagentTurns ?? 100}`, action: "agent.subagentTurns" },
|
|
343
|
+
{ type: "item", text: `并发池 agent.poolLimits = engCoder ${cur("engCoder")} / other ${cur("other")} / advisor ${cur("advisor")}(async 分域上限)`, action: "pool" },
|
|
245
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" },
|
|
246
346
|
{ type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
|
|
247
347
|
{ type: "item", text: `traces.enabled = ${tc.enabled === false ? "off" : "on"}(轨迹存档——发布默认关——隐私)`, action: "traces.enabled" },
|
|
248
348
|
{ type: "item", text: `traces.retentionHours = ${tc.retentionHours ?? 24} h(超期文件启动时清理)`, action: "traces.retentionHours" },
|
|
@@ -260,7 +360,8 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
260
360
|
|
|
261
361
|
if (choice.action === "view") {
|
|
262
362
|
pushLabel("❯ Config", ansi.bold + C.tool)
|
|
263
|
-
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)
|
|
264
365
|
pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
|
|
265
366
|
pushLine(`agent.maxTurns: ${ac.maxTurns ?? 200}`, C.dim)
|
|
266
367
|
pushLine(`agent.subagentTurns: ${ac.subagentTurns ?? 100}`, C.dim)
|
|
@@ -269,6 +370,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
269
370
|
pushLine(`traces.enabled: ${tc.enabled === false ? "off" : "on"}(默认 off——发布隐私——本地分析可开)`, C.dim)
|
|
270
371
|
pushLine(`traces.retentionHours: ${tc.retentionHours ?? 24}(超期文件启动清理——D-TR10)`, C.dim)
|
|
271
372
|
pushLine(`agent.consultModels: ${(ac.consultModels ?? []).map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ") || "(none)"}`, C.dim)
|
|
373
|
+
pushLine(`agent.poolLimits: { engCoder: ${cur("engCoder")}, other: ${cur("other")}, advisor: ${cur("advisor")} }(async 分域上限——默认 4/4/4——agent.poolLimits 可配)`, C.dim)
|
|
272
374
|
pushLine(`agent.consultTurns: ${ac.consultTurns ?? 40}`, C.dim)
|
|
273
375
|
pushLine(`agent.consultTimeoutMs: ${Math.round((ac.consultTimeoutMs ?? 600000) / 60000)} min`, C.dim)
|
|
274
376
|
pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${ec.model ?? ""})` : "disabled (FTS only)"}`, C.dim)
|
|
@@ -287,6 +389,16 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
287
389
|
continue
|
|
288
390
|
}
|
|
289
391
|
|
|
392
|
+
if (choice.action === "pool") {
|
|
393
|
+
await poolMenu()
|
|
394
|
+
continue
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (choice.action === "defaultModel") {
|
|
398
|
+
await defaultModelMenu()
|
|
399
|
+
continue
|
|
400
|
+
}
|
|
401
|
+
|
|
290
402
|
if (choice.action === "embedkey") {
|
|
291
403
|
await setEmbedKey() // 保存成功/取消都回主菜单(Esc 退出)
|
|
292
404
|
continue
|