thincoder 0.12.62 → 0.12.63
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 +9 -0
- package/README.md +13 -12
- package/bin/thincoder.mjs +53 -27
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -379
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +1 -1
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +1 -1
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +6 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +24 -1
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +33 -11
- package/src/tui/index.mjs +9 -9
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +26 -10
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +10 -5
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +14 -14
- package/src/tui/subagent-blocks.mjs +20 -3
- package/src/tui/subagent-freeze.mjs +73 -2
- package/src/tui/suspension-drive.mjs +46 -22
- package/src/tui/tool-events.mjs +11 -8
- package/src/tui/wizard.mjs +3 -3
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -244
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -243
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -204
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -473
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -295
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -309
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -482
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -459
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -417
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -392
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -88
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/ledger.mjs +0 -227
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -415
- package/src/memory/core.mjs +0 -299
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -419
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/scan.mjs +0 -95
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -258
- package/src/prompts/discipline-normal.md +0 -185
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -60
- package/src/prompts/persona-engineering.md +0 -55
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -221
- package/src/session-guard.mjs +0 -59
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-segments.mjs +0 -100
- package/src/session-slots.mjs +0 -492
- package/src/session-store.mjs +0 -441
- package/src/session.mjs +0 -492
- package/src/skills.mjs +0 -153
- package/src/text-budget.mjs +0 -46
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -355
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { newSession, resetSessionState, bindRecordStore, slotPath } from "
|
|
1
|
+
import { newSession, resetSessionState, bindRecordStore, slotPath } from "@thincoder/core/session.mjs"
|
|
2
2
|
import { C } from "./ansi.mjs"
|
|
3
3
|
|
|
4
4
|
/** /new command: start a new session in a fresh slot.
|
|
@@ -6,8 +6,9 @@ import { C } from "./ansi.mjs"
|
|
|
6
6
|
export async function handleNewCommand(ctx) {
|
|
7
7
|
const { agent, state, pushLine, showPicker, render } = ctx
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// F-MI7:newSession = async(探测束)——doNewSession 随之为 async,调用点 await
|
|
10
|
+
const doNewSession = async () => {
|
|
11
|
+
const slot = await newSession(agent.cwd)
|
|
11
12
|
// 2026-08-31 会诊 F3:resetSessionState 清全量会话态(_fullHistory/title/_sessionStart/
|
|
12
13
|
// 多槽设计凭证(_engDesignTokens——单值镜像 _engDesignToken 已退役 D3)/压缩与验证计数
|
|
13
14
|
// 等)——原实现只清 agent.history,新会话首次落盘把旧会话完整人类线 + 旧标题写进新
|
|
@@ -30,8 +31,8 @@ export async function handleNewCommand(ctx) {
|
|
|
30
31
|
{ type: "item", text: "Yes, start new session in a new slot", action: "yes" },
|
|
31
32
|
{ type: "item", text: "Cancel", action: "no" },
|
|
32
33
|
], { defaultIndex: 1 })
|
|
33
|
-
if (e?.action === "yes") doNewSession()
|
|
34
|
+
if (e?.action === "yes") await doNewSession()
|
|
34
35
|
return
|
|
35
36
|
}
|
|
36
|
-
doNewSession()
|
|
37
|
+
await doNewSession()
|
|
37
38
|
}
|
package/src/tui/cmd-reindex.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { C } from "./ansi.mjs"
|
|
|
4
4
|
* ctx: { agent, distillOpts, pushLine } */
|
|
5
5
|
export async function handleReindexCommand(ctx) {
|
|
6
6
|
const { agent, distillOpts, pushLine } = ctx
|
|
7
|
-
const { syncDir, codeSync, docSync } = await import("
|
|
7
|
+
const { syncDir, codeSync, docSync } = await import("@thincoder/core/memory.mjs")
|
|
8
8
|
pushLine("[reindex] Rebuilding index...", C.tool)
|
|
9
9
|
agent.memory.db.prepare("DELETE FROM files").run()
|
|
10
10
|
agent.memory.db.prepare("DELETE FROM code_chunks").run()
|
package/src/tui/cmd-restore.mjs
CHANGED
|
@@ -5,14 +5,14 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
5
5
|
* (CHECKPOINT.md D8). ctx: { agent, showPicker, pushLine, pushLabel } */
|
|
6
6
|
export async function handleRestoreCommand(ctx) {
|
|
7
7
|
const { agent, showPicker, pushLine, pushLabel } = ctx
|
|
8
|
-
const { listCheckpoints, rewind, isGitRepo } = await import("
|
|
8
|
+
const { listCheckpoints, rewind, isGitRepo } = await import("@thincoder/core/git/checkpoint.mjs")
|
|
9
9
|
if (!isGitRepo(agent.cwd)) {
|
|
10
10
|
pushLine("[rewind] not a git repository, checkpoints unavailable", C.error)
|
|
11
11
|
return
|
|
12
12
|
}
|
|
13
13
|
// F6 lazy fallback——与 git 工具 checkpoint list/create 入口一致(git-checkpoint.mjs):外部
|
|
14
14
|
// git commit 后(HEAD 时间 > 最新快照)先清空过期快照,/restore 不列出 commit 前状态。
|
|
15
|
-
const { lazyClearIfCommitted } = await import("
|
|
15
|
+
const { lazyClearIfCommitted } = await import("@thincoder/core/tools/git-checkpoint.mjs")
|
|
16
16
|
await lazyClearIfCommitted(agent.cwd)
|
|
17
17
|
const cps = await listCheckpoints(agent.cwd)
|
|
18
18
|
if (cps.length === 0) {
|
package/src/tui/cmd-session.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker, sessionDescriptor } from "
|
|
1
|
+
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker, sessionDescriptor, loadSlotFile } from "@thincoder/core/session.mjs"
|
|
2
|
+
import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
|
|
2
3
|
import { ansi, C } from "./ansi.mjs"
|
|
3
4
|
import { restoreLines } from "./startup.mjs"
|
|
4
5
|
import { stringWidth, sliceByWidth } from "./render.mjs"
|
|
@@ -81,6 +82,26 @@ export async function handleSessionCommand(ctx) {
|
|
|
81
82
|
// 2026-08-31 会诊 deepseek 🔴:目标槽被另一活进程占用时 switchToSlot 不认领(避免
|
|
82
83
|
// 劫持对方 active),本次会话仍读到数据,但下次保存会 fork 到新槽——提前提示。
|
|
83
84
|
const occ = slotOccupancy(agent.cwd, e.slot)
|
|
85
|
+
// #41 先判后切(MANIFEST.md §2.8 F3 · 父侧裁定 3):目标槽**合值**(§2.2 会话权威值
|
|
86
|
+
// 「槽带 `engineering` 字段 ? 槽值 : `config.agent.engineering`」)=== true 时,判据前置于
|
|
87
|
+
// `switchToSlot`——判据落其后会留下「指针已切、会话未换」的半态(下次 `saveSession` 把旧
|
|
88
|
+
// 会话写回目标槽 = 跨槽覆盖)。预读 = `loadSlotFile`(**纯读**——无认领副作用)。
|
|
89
|
+
// 触发条件 = 合值(遗留槽(无 `engineering` 字段)自工程会话切入时合值 = true ⇒ 预判据
|
|
90
|
+
// 照触发——F3 触发条件口径);槽不可读(preview null)不判:该次切换本就以「Slot not
|
|
91
|
+
// found」收场(switchToSlot 返回 null),无半态面。拒 ⇒ 切槽整体不发生(switchToSlot /
|
|
92
|
+
// applySession 均不调——零副作用)+ 原因行;准 ⇒ 既有切换 + applySession 之后一行附着。
|
|
93
|
+
const preview = loadSlotFile(agent.cwd, e.slot)
|
|
94
|
+
const slotEng = preview?.engineering
|
|
95
|
+
const targetEng = preview != null && (slotEng !== undefined ? slotEng === true : agent.config?.agent?.engineering === true)
|
|
96
|
+
let manifestAfterSwitch = null
|
|
97
|
+
if (targetEng) {
|
|
98
|
+
const r = resolveEngineeringManifest(agent.cwd, { writer: "main" })
|
|
99
|
+
if (!r.ok) {
|
|
100
|
+
pushLine(`Cannot switch to slot ${e.slot} — ${r.message} (slot unchanged)`, C.warn)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
manifestAfterSwitch = r.manifest
|
|
104
|
+
}
|
|
84
105
|
const data = switchToSlot(agent.cwd, e.slot)
|
|
85
106
|
if (!data) {
|
|
86
107
|
pushLine(`Slot ${e.slot} not found`, C.dim)
|
|
@@ -90,6 +111,8 @@ export async function handleSessionCommand(ctx) {
|
|
|
90
111
|
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
112
|
}
|
|
92
113
|
applySession(agent, data, occ.occupied ? {} : { slot: e.slot })
|
|
114
|
+
// #41 准翻列:附着动作 = 翻转面自己赋值(`agent.manifest` ← 判据结果——相位行当回合起活)。
|
|
115
|
+
if (manifestAfterSwitch) agent.manifest = manifestAfterSwitch
|
|
93
116
|
// Rebuild from history (lazy) — the display snapshot is deprecated.
|
|
94
117
|
// §14.3.6:描述符 { history(尾窗+±1), total, base }——标签口径 = total(非窗口长度);
|
|
95
118
|
// 未绑定(占用分支——模式 F)回退全量数组。
|
package/src/tui/cmd-skills.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
4
4
|
* ctx: { agent, pushLine, pushLabel } */
|
|
5
5
|
export async function handleSkillsCommand(ctx) {
|
|
6
6
|
const { agent, pushLine, pushLabel } = ctx
|
|
7
|
-
const { loadSkills } = await import("
|
|
7
|
+
const { loadSkills } = await import("@thincoder/core/skills.mjs")
|
|
8
8
|
const skills = await loadSkills(agent.cwd)
|
|
9
9
|
pushLabel(`❯ Skills`, ansi.bold + C.tool)
|
|
10
10
|
if (skills.length === 0) {
|
package/src/tui/cmd-think.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
5
5
|
|
|
6
6
|
export async function handleThinkCommand(ctx, args = []) {
|
|
7
7
|
const { agent, showPicker, syncProviderField, pushLine, pushLabel } = ctx
|
|
8
|
-
const { specForModel } = await import("
|
|
8
|
+
const { specForModel } = await import("@thincoder/core/config.mjs")
|
|
9
9
|
|
|
10
10
|
// Fast path: direct args — exit immediately
|
|
11
11
|
const cur = agent.provider
|
|
@@ -33,7 +33,8 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
33
33
|
if (autoThinkEnabled) { pushLine("Auto-think is ON — manual settings are overridden each turn; turn Auto off first via /think", C.error); return }
|
|
34
34
|
await applyThink({ action: "effort", level }, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue)
|
|
35
35
|
pushLabel("❯ Think", ansi.bold + C.tool)
|
|
36
|
-
|
|
36
|
+
// none = 关思考档(applyThink 内归一到 off 路径)⇒ 回执同 off 文案,不比照档位报“…none”
|
|
37
|
+
pushLine(level === "none" ? "Thinking: OFF" : `Thinking effort: ${level}`, C.tool)
|
|
37
38
|
return
|
|
38
39
|
}
|
|
39
40
|
if (sub) { pushLine("Usage: /think [on|off|effort <level>]", C.error); return }
|
|
@@ -78,18 +79,29 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
78
79
|
pushLine(`Auto-think: ${newAuto ? "ON" : "OFF"}`, C.tool)
|
|
79
80
|
return // exit loop — no useful actions remain when auto mode just changed (T8b 锁定——toggle 即退是已批准语义——2026-09-05 曾误改 OFF 回菜单——测试抓回归——已撤销)
|
|
80
81
|
} else if (e.action === "effort") {
|
|
81
|
-
pushLine(`Reasoning effort: ${e.level}`, C.tool)
|
|
82
|
+
pushLine(e.level === "none" ? "Thinking: OFF" : `Reasoning effort: ${e.level}`, C.tool)
|
|
82
83
|
} else {
|
|
84
|
+
// 与菜单头(:47-48)同式:`thinking:null`(NF1 显式 off)必须落 OFF——本式缺 `!== null` 守卫时
|
|
85
|
+
// effort 型 off 后回执误报 ON(同一状态、两处公式——单一口径 = 面板式)。
|
|
83
86
|
const nowEnabled = cur.thinking?.type === thinkOnValue
|
|
84
|
-
|| (cur.thinking?.type === undefined && !isCustomThink)
|
|
87
|
+
|| (cur.thinking !== null && cur.thinking?.type === undefined && !isCustomThink)
|
|
85
88
|
pushLine(`Thinking: ${nowEnabled ? "ON" : "OFF"}`, C.tool)
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
/** Shared apply logic — extracted from handleThinkCommand for reuse in both fast path and loop
|
|
91
|
-
|
|
93
|
+
/** Shared apply logic — extracted from handleThinkCommand for reuse in both fast path and loop.
|
|
94
|
+
* Named export (2026-09-20): the on-default-effort rule (§2.8) is unit-tested by driving this
|
|
95
|
+
* directly with a synthetic spec — behavior otherwise unchanged.
|
|
96
|
+
* Effort 档位 `"none"` = 关思考 ⇒ 入口归一到 off 路径(与菜单 off 项同形——见内注释)。 */
|
|
97
|
+
export async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue) {
|
|
92
98
|
const cur = agent.provider
|
|
99
|
+
// `effort` 档位取 `"none"` = **关思考**(非“强度零”):与本面 off 动作同语义 ⇒ 归一到 off 路径
|
|
100
|
+
// (各族取其原生 off 形:effort 型 = `thinking:null`+删 effort;type 型 = `{type:"disabled"}`)。
|
|
101
|
+
// 不归一的两处违约:① 菜单头同拍显示「Thinking: ON | Effort: none」自相矛盾(off 标记缺失);
|
|
102
|
+
// ② 残留 `reasoningEffort:"none"` 会被载荷层当强度档送(`provider/core.mjs:197-204`),与百炼
|
|
103
|
+
// qwen 侧 off 判据(`config.mjs:133-139` 的 `thinking === null ⇒ false`)错位 ⇒ 服务端仍思考。
|
|
104
|
+
if (e.action === "effort" && e.level === "none") e = { action: "off" }
|
|
93
105
|
if (e.action === "auto") {
|
|
94
106
|
const cfg = agent.config.agent ??= {}
|
|
95
107
|
cfg.autoThink = !cfg.autoThink
|
|
@@ -113,9 +125,10 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
|
|
|
113
125
|
// Explicit off persists thinking:null (NF1 convention — distinguishable from autoThink's
|
|
114
126
|
// delete/undefined); "on" deletes the marker so enable_thinking maps from effort again.
|
|
115
127
|
if (!enable) { cur.thinking = null; delete cur.reasoningEffort }
|
|
116
|
-
// "on" 默认 effort 取 spec
|
|
117
|
-
// (enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400
|
|
118
|
-
|
|
128
|
+
// "on" 默认 effort 取 spec 枚举的**首个非 "none"** 档(批次档 §1.8-② / 设计 §2.8 D-9):硬编码 "high"
|
|
129
|
+
// 对 qwen3.8-max(enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置);而直接取枚举
|
|
130
|
+
// 首项在新档(首项 "none")等于把思考关掉 ⇒ 跳过 "none";枚举全 "none" 时与无枚举同型 ⇒ 回退 "high"
|
|
131
|
+
else { delete cur.thinking; if (!cur.reasoningEffort) cur.reasoningEffort = spec.reasoningEffortEnum?.find((v) => v !== "none") ?? "high" }
|
|
119
132
|
await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
|
|
120
133
|
if (!enable) await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
|
|
121
134
|
else await syncProviderField(agent.activeProvider, "reasoningEffort", cur.reasoningEffort)
|
|
@@ -11,7 +11,7 @@ export function createConfigHelpers(agent, opts = {}) {
|
|
|
11
11
|
/** 磁盘新鲜读 → mutate(单操作)→ mtime 门控写;冲突时 throw(提示语见下——TUI
|
|
12
12
|
* slash 分发层统一 [error] 行展示;调用方若自带 try/catch 展示 Save failed 同文案)。 */
|
|
13
13
|
async function persistRaw(mutate) {
|
|
14
|
-
const { writeConfigAtomic, configPath } = await import("
|
|
14
|
+
const { writeConfigAtomic, configPath } = await import("@thincoder/core/config.mjs")
|
|
15
15
|
const r = writeConfigAtomic(cfgPath ?? configPath, mutate)
|
|
16
16
|
if (!r.ok) throw new Error("config changed on disk concurrently — retry")
|
|
17
17
|
}
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 职责(§15.3.2):常量单源(D-TB4)+ 行/载体计长(`lineChars`)+ `state.lines` 总量
|
|
5
5
|
* 对账(`syncLineBudget`)。纯函数本体(`capText` / `appendCappedText`)住
|
|
6
|
-
* `
|
|
6
|
+
* `thincoder-core/text-budget.mjs`(零依赖)——与 agent 侧捕获共用(AGENT-LOOP.md §23.3.1,D2 单源);
|
|
7
7
|
* 本模块只承载 TUI 面常量与对账。
|
|
8
8
|
*
|
|
9
|
-
* 依赖方向:本模块只引 `./ansi.mjs` +
|
|
9
|
+
* 依赖方向:本模块只引 `./ansi.mjs` + `@thincoder/core/text-budget.mjs`(叶子——不引 subagent 族,
|
|
10
10
|
* 冻结锚点平移由调用方经 `onTrim` 注入,避环)。
|
|
11
11
|
*
|
|
12
12
|
* 计量口径 = UTF-16 码元(`String.length`——TUI.md §15.2 表 1);行数维(5000 行环 /
|
|
13
13
|
* 500 行块环 / 200 条目环)**原样保留**,字符维为第二维(D-TB5——既有 N5/N6 语义不动)。
|
|
14
14
|
*/
|
|
15
15
|
import { C } from "./ansi.mjs"
|
|
16
|
-
import { capText, appendCappedText, fillMarker } from "
|
|
16
|
+
import { capText, appendCappedText, fillMarker } from "@thincoder/core/text-budget.mjs"
|
|
17
17
|
|
|
18
18
|
export { capText }
|
|
19
19
|
|
|
@@ -35,8 +35,10 @@ export const SUB_BLOCK_CHAR_LIMIT = 128_000
|
|
|
35
35
|
export const ADVISOR_TEXT_MAX_CHARS = 128_000
|
|
36
36
|
/** 流式缓冲(`state.streaming` / `state.reasoning`)累积额度。 */
|
|
37
37
|
export const STREAM_MAX_CHARS = 256_000
|
|
38
|
-
/** `state.lines`
|
|
38
|
+
/** `state.lines` 全部行与载体文本总量(超出:最小步进裁头 + 冻结锚点平移 + 收据行——保底见下行)。 */
|
|
39
39
|
export const LINES_CHAR_BUDGET = 2_000_000
|
|
40
|
+
/** 字符超额裁剪保底行数——裁剪不得使 `state.lines` 低于该值;触底仍未脱额即停(接受超额——上界 = 保底 × 单行上限)。 */
|
|
41
|
+
export const LINES_TRIM_FLOOR = 200
|
|
40
42
|
/** 搜索匹配计数上限(超出截断 + 提示行)。 */
|
|
41
43
|
export const SEARCH_MATCH_CAP = 10_000
|
|
42
44
|
|
|
@@ -140,6 +142,14 @@ export function accountLine(state, l) {
|
|
|
140
142
|
l._budgetChars = next
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
/** 行移除负向出账(§5.5 记账口径——增删均须过账;不变式 `_linesChars === Σ lineChars`):
|
|
146
|
+
* `state._linesChars -= lineChars(l)`,钳 0。与 `accountLine` 不可互替——后者以行对象
|
|
147
|
+
* `_budgetChars` 幂等(行从 `state.lines` 脱落后重调增量恒 0 ⇒ 账不降),移除需本导出。 */
|
|
148
|
+
export function releaseLine(state, l) {
|
|
149
|
+
if (!l) return
|
|
150
|
+
state._linesChars = Math.max(0, (state._linesChars ?? 0) - lineChars(l))
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
/** 直算对账(双算法对照 / 初始化 / 漂移自愈——测试直算同源)。 */
|
|
144
154
|
export function accountAll(state) {
|
|
145
155
|
let total = 0
|
|
@@ -152,10 +162,19 @@ export function accountAll(state) {
|
|
|
152
162
|
return total
|
|
153
163
|
}
|
|
154
164
|
|
|
165
|
+
/** 收据行长度预留(`syncLineBudget` 单轮 need 计入的收据余量:前缀 32 + N 数字位 + 尾 17 < 96——覆 7 位数余量)。 */
|
|
166
|
+
const RECEIPT_RESERVE_CHARS = 96
|
|
167
|
+
|
|
155
168
|
/**
|
|
156
|
-
* `state.lines` 总量对账(§15.3.3 落点表末行):超 `LINES_CHAR_BUDGET` →
|
|
157
|
-
*
|
|
158
|
-
*
|
|
169
|
+
* `state.lines` 总量对账(§15.3.3 落点表末行):超 `LINES_CHAR_BUDGET` → **最小步进**裁头 +
|
|
170
|
+
* 收据行,且不使 `lines.length` 低于 `LINES_TRIM_FLOOR`(触底 = 内层 break——接受超额,
|
|
171
|
+
* 不靠守卫退出:FLOOR+1 且残余超额时守卫仍为真)。每轮裁「至额度内所需最少行数」
|
|
172
|
+
* (need = 超额 + 收据上界预留;上限 = 可裁至保底的行数);`onTrim(state, removedCount)` 由
|
|
173
|
+
* 调用方注入冻结锚点平移(display-budget 不引 subagent 族——避环)。`pushLineLike` 注入面(无生产调用):
|
|
174
|
+
* 仅**头插**语义下自洽——尾插会使收据位置、收据 N 口径与「净位移 = 丢行数 − 1」约定失真。
|
|
175
|
+
* 幂等(未超限 / 行数 ≤ 保底)。
|
|
176
|
+
* 旧颗粒 `min(1000, lines.length-1)` 在行数 ≤1001 且超额时一轮裁到 1 行(用户实测收据
|
|
177
|
+
* 「1 lines remaining」——TUI-HISTORY-TRIM 批修复:保底 200 + 最小步进)。
|
|
159
178
|
* @returns {number} 本轮裁掉的行数
|
|
160
179
|
*/
|
|
161
180
|
export function syncLineBudget(state, { pushLineLike = null, onTrim = null } = {}) {
|
|
@@ -163,11 +182,13 @@ export function syncLineBudget(state, { pushLineLike = null, onTrim = null } = {
|
|
|
163
182
|
if (!Array.isArray(lines)) return 0
|
|
164
183
|
if (!Number.isFinite(state._linesChars)) accountAll(state)
|
|
165
184
|
let removed = 0
|
|
166
|
-
while (state._linesChars > LINES_CHAR_BUDGET && lines.length >
|
|
167
|
-
const
|
|
168
|
-
const
|
|
185
|
+
while (state._linesChars > LINES_CHAR_BUDGET && lines.length > LINES_TRIM_FLOOR) {
|
|
186
|
+
const limit = lines.length - LINES_TRIM_FLOOR
|
|
187
|
+
const need = state._linesChars - LINES_CHAR_BUDGET + RECEIPT_RESERVE_CHARS
|
|
188
|
+
let take = 0
|
|
169
189
|
let freed = 0
|
|
170
|
-
|
|
190
|
+
while (take < limit && freed < need) { freed += lineChars(lines[take]); take++ }
|
|
191
|
+
lines.splice(0, take)
|
|
171
192
|
state._linesChars = Math.max(0, state._linesChars - freed)
|
|
172
193
|
removed += take
|
|
173
194
|
const receipt = `... [earlier messages trimmed — ${lines.length} lines remaining]`
|
|
@@ -179,6 +200,7 @@ export function syncLineBudget(state, { pushLineLike = null, onTrim = null } = {
|
|
|
179
200
|
state._linesChars += receipt.length
|
|
180
201
|
}
|
|
181
202
|
if (typeof onTrim === "function") onTrim(state, take)
|
|
203
|
+
if (take >= limit) break // 触底(可裁上限用尽)——接受超额;收敛靠本 break,不靠守卫退出
|
|
182
204
|
}
|
|
183
205
|
return removed
|
|
184
206
|
}
|
package/src/tui/index.mjs
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
import { emitKeypressEvents } from "node:readline"
|
|
22
22
|
import { PassThrough } from "node:stream"
|
|
23
|
-
import { saveSession } from "
|
|
24
|
-
import { closeAllMcp } from "
|
|
23
|
+
import { saveSession } from "@thincoder/core/session.mjs"
|
|
24
|
+
import { closeAllMcp } from "@thincoder/core/mcp.mjs"
|
|
25
25
|
import { ansi, C } from "./ansi.mjs"
|
|
26
26
|
import { createRenderLoop } from "./render-loop.mjs"
|
|
27
27
|
import { makeDimsState } from "./dims.mjs"
|
|
@@ -45,11 +45,11 @@ import { startLedgerSurface } from "./ledger-surface.mjs"
|
|
|
45
45
|
export { upgradeFailureText, pendingNoticeReady } from "./update-notice.mjs"
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* SESSION.md §8 D-S2 — TUI 启动首帧前的 provider 重选流程:
|
|
48
|
+
* SESSION.md §6.8 D-S2 — TUI 启动首帧前的 provider 重选流程:
|
|
49
49
|
* provider 无效(`_providerInvalid` 标记或 provider 为 null)→ 先弹模型选择 picker
|
|
50
50
|
* (复用 openModelPicker,展示当前可用 providers);用户选定后继续正常启动。
|
|
51
|
-
* 选择取消(Esc)→ 仍进入 TUI,推送提示行("未配置有效 provider,可用 /model
|
|
52
|
-
*
|
|
51
|
+
* 选择取消(Esc)→ 仍进入 TUI,推送提示行("未配置有效 provider,可用 /model 配置
|
|
52
|
+
* 渠道与模型")——绝不因无 provider 拒绝进入。返回 true 表示弹过选择流程。
|
|
53
53
|
*/
|
|
54
54
|
export async function promptProviderIfInvalid(agent, openModelPicker, pushLine) {
|
|
55
55
|
if (!(agent._providerInvalid || !agent.provider)) return false
|
|
@@ -60,7 +60,7 @@ export async function promptProviderIfInvalid(agent, openModelPicker, pushLine)
|
|
|
60
60
|
const hasProviders = (agent.providers?.length ?? 0) > 0
|
|
61
61
|
pushLine(hasProviders && !agent.config?.defaultModel
|
|
62
62
|
? "尚未设置默认模型(config.defaultModel——新会话起点):/config → 默认模型 设置一次;/model 仅改本会话"
|
|
63
|
-
: "未配置有效 provider,可用 /model
|
|
63
|
+
: "未配置有效 provider,可用 /model 配置渠道与模型", C.warn)
|
|
64
64
|
}
|
|
65
65
|
return true
|
|
66
66
|
}
|
|
@@ -406,7 +406,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
406
406
|
const { persistRaw, syncProviderField, maskKey } = createConfigHelpers(agent)
|
|
407
407
|
|
|
408
408
|
// Model picker + generic picker: implemented in pickers.mjs
|
|
409
|
-
const { closePicker, showPicker, popPicker, renderPickerLines, openModelPicker, selectModel, setProviderKey, pickModelForSlot } = createPickers({
|
|
409
|
+
const { closePicker, showPicker, popPicker, renderPickerLines, openModelPicker, selectModel, setProviderKey, pickModelForSlot, confirmDelete } = createPickers({
|
|
410
410
|
agent, state, render, ansi, C, pushLine, pushLabel, persistRaw, askQuestion, maskKey,
|
|
411
411
|
})
|
|
412
412
|
|
|
@@ -423,7 +423,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
423
423
|
const { handleSlash, completions, handleTab } = createSlashCommands({
|
|
424
424
|
agent, state, distillOpts,
|
|
425
425
|
pushLine, pushLabel, render,
|
|
426
|
-
showPicker, closePicker, askQuestion, askPermission,
|
|
426
|
+
showPicker, closePicker, askQuestion, askPermission, confirmDelete,
|
|
427
427
|
persistRaw, syncProviderField, maskKey,
|
|
428
428
|
openModelPicker: () => openModelPicker(),
|
|
429
429
|
selectModel,
|
|
@@ -458,7 +458,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
458
458
|
|
|
459
459
|
// ---------------------------------------------------------- Startup screen + background indexing
|
|
460
460
|
|
|
461
|
-
// SESSION.md §8 D-S2:startTUI 首帧前 —— provider 无效(_providerInvalid / provider 为 null)
|
|
461
|
+
// SESSION.md §6.8 D-S2:startTUI 首帧前 —— provider 无效(_providerInvalid / provider 为 null)
|
|
462
462
|
// → 先弹模型选择 picker(keyStream 已挂 keypress,Esc/Enter 可用);Esc → 提示行,仍进 TUI
|
|
463
463
|
await promptProviderIfInvalid(agent, () => openModelPicker(), pushLine)
|
|
464
464
|
|
package/src/tui/interaction.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ansi, C } from "./ansi.mjs"
|
|
2
|
-
import { detectDanger } from "
|
|
2
|
+
import { detectDanger } from "@thincoder/core/tools/shared.mjs"
|
|
3
|
+
import { askPermission as coreAskPermission } from "@thincoder/core/permission.mjs"
|
|
3
4
|
|
|
4
5
|
/** Interaction primitives: permission approval + question input.
|
|
5
6
|
* Extracted from index.mjs, receives closure dependencies via createInteraction(ctx).
|
|
@@ -9,6 +10,11 @@ import { detectDanger } from "../tools/shared.mjs"
|
|
|
9
10
|
* answer mode (the user supplements/corrects the AI's preset choices). */
|
|
10
11
|
export const QUESTION_CUSTOM = "\u0001custom-answer"
|
|
11
12
|
|
|
13
|
+
/** y/n-only modal family(判据单源——key-modes / render-frame 同引):`continue`(回合帽)
|
|
14
|
+
* 与 `retry`(provider 失败面)同形 ⇒ 仅 `y` / `n`。`a` 不属该族键面(会翻会话级 AUTO)。 */
|
|
15
|
+
const YES_NO_MODALS = new Set(["continue", "retry"])
|
|
16
|
+
export function isYesNoModal(name) { return YES_NO_MODALS.has(name) }
|
|
17
|
+
|
|
12
18
|
export function createInteraction(ctx) {
|
|
13
19
|
const { agent, state, pushLine, pushLabel, render, summarize } = ctx
|
|
14
20
|
|
|
@@ -62,12 +68,15 @@ export function createInteraction(ctx) {
|
|
|
62
68
|
pushLine(` [auto] ${name}${argSummary ? ` ${argSummary}` : ""}`, C.warn)
|
|
63
69
|
return Promise.resolve(true)
|
|
64
70
|
}
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
// 闸语义 / 请示流程 = 核单源(P2 批 §2.20);本端只供展示面——卡片预览经 `io.ask` 缝
|
|
72
|
+
// 注入(permissionPreview 渲染在输入框上方,与 "Allow?" 提示同帧)。
|
|
73
|
+
return coreAskPermission(name, args, {
|
|
74
|
+
ask: ({ name: n, toolArgs }) => new Promise((resolve) => {
|
|
75
|
+
state.permissionPreview = formatPermission(n, toolArgs)
|
|
76
|
+
state.permission = { name: n, args: toolArgs, resolve }
|
|
77
|
+
state.status = `Waiting: ${n}`
|
|
78
|
+
render()
|
|
79
|
+
}),
|
|
71
80
|
})
|
|
72
81
|
}
|
|
73
82
|
|
package/src/tui/key-modes.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { C } from "./ansi.mjs"
|
|
9
9
|
import { readClipboardText, insertPastedText } from "./clipboard.mjs"
|
|
10
|
-
import { QUESTION_CUSTOM } from "./interaction.mjs"
|
|
10
|
+
import { QUESTION_CUSTOM, isYesNoModal } from "./interaction.mjs"
|
|
11
11
|
import { inputContentWidth } from "./layout.mjs"
|
|
12
12
|
import { moveCursorVertical } from "./render.mjs"
|
|
13
13
|
|
|
@@ -35,14 +35,19 @@ export function denyModalForOwner(state, key, { pushLine, render } = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** permission 确认模态:y/n/a(a = approve + AUTO ON);batch(§16 D-B1):a/o/n(Esc = deny)。
|
|
38
|
+
* `continue` / `retry` = y/n-only 族(判据单源 `isYesNoModal`——X8 跟进①:`retry` 框不
|
|
39
|
+
* 广告 `a`,零会话级 AUTO 副作用)。
|
|
38
40
|
* consume:valid 键或 Esc 走 resolve 分支;其余键静默吞掉(模态独占)。 */
|
|
39
41
|
export function handlePermissionMode(str, key, ctx) {
|
|
40
42
|
const { state, agent, pushLine, render } = ctx
|
|
41
43
|
if (!state.permission) return false
|
|
42
44
|
const answer = (str || "").toLowerCase()
|
|
45
|
+
// `yOnly` = 本框键面仅 y/n(continue / retry);`isContinue` 仅保轨迹行豁免(continue 有
|
|
46
|
+
// 自有输出行——retry **保留** `[approved]` / `[denied]` 轨迹行)。
|
|
43
47
|
const isContinue = state.permission.name === "continue"
|
|
48
|
+
const yOnly = isYesNoModal(state.permission.name)
|
|
44
49
|
const isBatch = Boolean(state.permission.batch)
|
|
45
|
-
const validKeys =
|
|
50
|
+
const validKeys = yOnly ? ["y", "n"] : isBatch ? ["a", "o", "n"] : ["y", "n", "a"]
|
|
46
51
|
if (validKeys.includes(answer) || key.name === "escape") {
|
|
47
52
|
const { resolve, name } = state.permission
|
|
48
53
|
state.permission = null
|
|
@@ -58,13 +63,13 @@ export function handlePermissionMode(str, key, ctx) {
|
|
|
58
63
|
render()
|
|
59
64
|
return true
|
|
60
65
|
}
|
|
61
|
-
if (answer === "a" && !
|
|
66
|
+
if (answer === "a" && !yOnly) {
|
|
62
67
|
agent.autoApprove = true
|
|
63
68
|
agent._pendingReminders = agent._pendingReminders ?? []
|
|
64
69
|
agent._pendingReminders.push("[System reminder: AUTO mode is now ON. All tool calls are automatically approved. Use /auto to disable.]")
|
|
65
70
|
pushLine(` [auto] AUTO ON: tool calls no longer prompt for approval (/auto to disable)`, C.warn)
|
|
66
71
|
}
|
|
67
|
-
const approved = answer === "y" || (answer === "a" && !
|
|
72
|
+
const approved = answer === "y" || (answer === "a" && !yOnly)
|
|
68
73
|
// leave trail: record approval/denial in conversation (continue prompt has its own output, don't duplicate)
|
|
69
74
|
if (!isContinue) {
|
|
70
75
|
pushLine(` [${approved ? "approved" : "denied"}] ${name}`, approved ? C.dim : C.error)
|
|
@@ -5,21 +5,35 @@
|
|
|
5
5
|
* 越线 / 池达阈值——一次性去重 + **送达门**:未送达不记账)③ 状态位(`state.ledger`——L1
|
|
6
6
|
* 常驻标记,`render-frame.mjs` 消费;**不受启动行门约束**——F2 按标记态照显)。
|
|
7
7
|
* 只读台账;唯一写面 = 去重档(`~/.thincoder/ledger-notify.json`)。
|
|
8
|
+
*
|
|
9
|
+
* M2/KD-M2-3:核台账面**动态 import**(`@thincoder/core/ledger.mjs` 静态链 `node:sqlite`——
|
|
10
|
+
* 静态 import 会把 `node:sqlite` 拽进端壳静态面);载入失败 = 降级不崩(N1),扫描拍重试。
|
|
8
11
|
*/
|
|
9
|
-
import { blameAges, detailScans, discoverFamily, formatDetailLine, formatMarker, loadNotifyState, NOTIFY_FILE, notifyKey, planChangeLines, REFRESH_MS, saveNotifyState, summarizeLedger } from "../ledger.mjs"
|
|
10
12
|
import { C } from "./ansi.mjs"
|
|
11
13
|
|
|
14
|
+
/** 核台账面惰性载入(KD-M2-3 动态 import——失败不缓存,下次扫描拍重试;N1 降级不崩)。 */
|
|
15
|
+
let _core = null
|
|
16
|
+
function loadCore() {
|
|
17
|
+
if (!_core) {
|
|
18
|
+
_core = import("@thincoder/core/ledger.mjs")
|
|
19
|
+
_core.catch(() => { _core = null })
|
|
20
|
+
}
|
|
21
|
+
return _core
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
/** 单次扫描(直驱面——timer 包装见 `startLedgerSurface`)。
|
|
13
25
|
* `startup=true` = 会话首扫(补启动行;变化行在前、明细行在后)。 */
|
|
14
|
-
export function runLedgerScan({ state, agent = null, anchor = null, notifyFile =
|
|
26
|
+
export async function runLedgerScan({ state, agent = null, anchor = null, notifyFile = undefined, pushLine = () => {}, render = () => {}, startup = false } = {}) {
|
|
27
|
+
const core = await loadCore()
|
|
28
|
+
const { buildScan, detailScans, discoverFamily, formatDetailLine, formatMarker, loadNotifyState, NOTIFY_FILE, notifyKey, planChangeLines, saveNotifyState } = core
|
|
15
29
|
const base = anchor ?? agent?.cwd ?? process.cwd()
|
|
16
30
|
const family = discoverFamily(base)
|
|
17
31
|
const scans = []
|
|
18
32
|
for (const p of family.projects) {
|
|
19
|
-
try { scans.push(
|
|
33
|
+
try { scans.push(buildScan({ cwd: p.root })) } catch { /* 台账不可读 → 该项目跳过(余者照常——N1/T110①) */ }
|
|
20
34
|
}
|
|
21
35
|
const current = family.current ? scans.find((s) => s.root === family.current.root) ?? null : null
|
|
22
|
-
const notify = loadNotifyState(notifyFile)
|
|
36
|
+
const notify = loadNotifyState(notifyFile ?? NOTIFY_FILE)
|
|
23
37
|
const plans = scans.map((s) => ({ s, plan: planChangeLines(notify.ledgers[notifyKey(s.ledger)], s) }))
|
|
24
38
|
const changeLines = plans.flatMap(({ plan }) => plan.lines)
|
|
25
39
|
const showStartup = startup && scans.some((s) => s.actionable)
|
|
@@ -38,7 +52,7 @@ export function runLedgerScan({ state, agent = null, anchor = null, notifyFile =
|
|
|
38
52
|
if (changeLines.length ? delivered : changed) {
|
|
39
53
|
const ledgers = { ...notify.ledgers }
|
|
40
54
|
for (const { s, plan } of plans) ledgers[notifyKey(s.ledger)] = { ...plan.next, updatedAt: Date.now() }
|
|
41
|
-
saveNotifyState(notifyFile, { version: 1, ledgers })
|
|
55
|
+
saveNotifyState(notifyFile ?? NOTIFY_FILE, { version: 1, ledgers })
|
|
42
56
|
}
|
|
43
57
|
state.ledger = { marker: current ? formatMarker(current) : null, warn: Boolean(current && current.aged > 0), scannedAt: Date.now() }
|
|
44
58
|
render()
|
|
@@ -48,15 +62,17 @@ export function runLedgerScan({ state, agent = null, anchor = null, notifyFile =
|
|
|
48
62
|
export function startLedgerSurface(ctx = {}) {
|
|
49
63
|
let disposed = false
|
|
50
64
|
let timer = null
|
|
51
|
-
const tick = (startup) => {
|
|
65
|
+
const tick = async (startup) => {
|
|
52
66
|
if (disposed) return
|
|
53
|
-
if (!startup && ctx.state?.processing) return //
|
|
54
|
-
try { runLedgerScan({ ...ctx, startup }) } catch { /* 可见面尽力——不崩 TUI(N1) */ }
|
|
67
|
+
if (!startup && ctx.state?.processing) return // 扫描不打断回合帧
|
|
68
|
+
try { await runLedgerScan({ ...ctx, startup }) } catch { /* 可见面尽力——不崩 TUI(N1) */ }
|
|
55
69
|
}
|
|
56
|
-
setImmediate(() => {
|
|
70
|
+
setImmediate(async () => {
|
|
57
71
|
if (disposed) return
|
|
58
72
|
tick(true)
|
|
59
|
-
|
|
73
|
+
const core = await loadCore().catch(() => null)
|
|
74
|
+
if (disposed || !core) return // 载入失败 → 无周期(N1——降级不崩)
|
|
75
|
+
timer = setInterval(() => tick(false), core.REFRESH_MS)
|
|
60
76
|
timer.unref?.()
|
|
61
77
|
})
|
|
62
78
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* model-catalog.mjs — 会话级模型清单 helper(PROVIDER.md §16.2 M2/M9——2026-09-10 MODEL-SELECTION)。
|
|
3
3
|
*
|
|
4
|
-
* 清单唯一权威 = provider 运行期拉取(`GET /models`——provider/list-models.mjs 按 format 分派)。
|
|
4
|
+
* 清单唯一权威 = provider 运行期拉取(`GET /models`——thincoder-core/provider/list-models.mjs 按 format 分派)。
|
|
5
5
|
* 本模块提供:
|
|
6
6
|
* - `getProviderModels(providerConfig)`:拉取 + **会话级缓存**(TTL 60s;失败不缓存——下次重试);
|
|
7
7
|
* - `probeChannelModels(providerConfig)`:配置阶段准入探(M9——探通入缓存候选直接可用 / 探不通返回
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
* - `modelListFailureText(error)`:M8 失败消息本体(逐字长句——界面明示的唯一断言对象);
|
|
10
10
|
* - `dedupeModels` / `modelSeries`:显示归并(自 model-picker.mjs 迁入——会话面与槽位面共用)。
|
|
11
11
|
*
|
|
12
|
-
* 缓存时钟可注入(`_catalogHooks.now`——先例 rate.mjs `_rateHooks`):测试假时钟确定性断言 T6 不依赖壁钟。
|
|
12
|
+
* 缓存时钟可注入(`_catalogHooks.now`——先例 thincoder-core/provider/rate.mjs `_rateHooks`):测试假时钟确定性断言 T6 不依赖壁钟。
|
|
13
13
|
*/
|
|
14
|
-
import { listModels } from "
|
|
14
|
+
import { listModels } from "@thincoder/core/provider/list-models.mjs"
|
|
15
15
|
|
|
16
16
|
/** 会话级缓存 TTL(§16.4 a2:连续操作不重复付网络;60s 新鲜窗口)。 */
|
|
17
17
|
const CACHE_TTL_MS = 60_000
|
|
18
18
|
|
|
19
|
-
/** 测试钩子(先例
|
|
19
|
+
/** 测试钩子(先例 thincoder-core/provider/rate.mjs `_rateHooks`):时钟可注入——T6 假时钟断言。 */
|
|
20
20
|
export const _catalogHooks = { now: () => Date.now() }
|
|
21
21
|
|
|
22
22
|
/** 会话内缓存:key = name + baseURL(同渠道不同端点各自成条);value = { models, at }。 */
|
package/src/tui/model-picker.mjs
CHANGED
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
* 渠道管理流(add/remove/key/context)是 provider 级 config 写——语义不变。F-4 (ISSUE-FIX-BATCH):
|
|
11
11
|
* removeProviderFlow 级联清理悬挂引用(cascadeRemoveProvider——文件尾导出)。
|
|
12
12
|
* ctx: { agent, state, render, ansi, C, pushLine, persistRaw, askQuestion, maskKey, showPicker,
|
|
13
|
-
* closePicker, renderPickerLines }
|
|
13
|
+
* closePicker, renderPickerLines, confirmDelete }(后四者为 pickers.mjs 通用 picker 绑定——闭包入参,环安全)。
|
|
14
14
|
*/
|
|
15
15
|
import { sliceByWidth } from "./render.mjs"
|
|
16
|
-
import { PROVIDER_PRESETS as PRESETS, providerSpec, specMatch } from "
|
|
17
|
-
import { saveSession } from "
|
|
16
|
+
import { PROVIDER_PRESETS as PRESETS, providerSpec, specMatch } from "@thincoder/core/config.mjs"
|
|
17
|
+
import { saveSession } from "@thincoder/core/session.mjs"
|
|
18
18
|
import { getProviderModels, probeChannelModels, modelListFailureText, dedupeModels } from "./model-catalog.mjs"
|
|
19
19
|
|
|
20
20
|
/** createModelPicker(ctx) → { openModelPicker, selectModel, setProviderKey, setContextFlow, pickModelForSlot } */
|
|
21
21
|
export function createModelPicker(ctx) {
|
|
22
|
-
const { agent, state, pushLine, persistRaw, askQuestion, maskKey, showPicker, closePicker, renderPickerLines } = ctx
|
|
22
|
+
const { agent, state, pushLine, persistRaw, askQuestion, maskKey, showPicker, closePicker, renderPickerLines, confirmDelete = () => { throw new Error("ctx.confirmDelete missing — deletion refused (fail-closed)") } } = ctx
|
|
23
23
|
const { ansi, C } = ctx
|
|
24
24
|
|
|
25
25
|
/** entry 唯一标识:异步更新 entries 后按它恢复选中项 */
|
|
@@ -231,7 +231,7 @@ export function createModelPicker(ctx) {
|
|
|
231
231
|
agent.provider = { ...target }
|
|
232
232
|
agent.provider.model = item.model
|
|
233
233
|
if (agent.config?.agent?.compactThresholdAuto) {
|
|
234
|
-
const { resolveCompactThreshold } = await import("
|
|
234
|
+
const { resolveCompactThreshold } = await import("@thincoder/core/config.mjs")
|
|
235
235
|
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
236
236
|
}
|
|
237
237
|
// 写会话槽(saveSession——槽双字段恒非空)——config 文件零写
|
|
@@ -280,7 +280,7 @@ export function createModelPicker(ctx) {
|
|
|
280
280
|
/** Slot 面 fetch:模型直接并入可选行(旧 pickModelForSlot 语义——自由面;槽位面零改——
|
|
281
281
|
* 仅随 M1 获得三 format 支持 + 占位行(同会话面:0 item 的 picker 不会打开))。 */
|
|
282
282
|
async function fetchSlotModels(providerName, providerConfig, entries) {
|
|
283
|
-
const { listModels } = await import("
|
|
283
|
+
const { listModels } = await import("@thincoder/core/provider/index.mjs")
|
|
284
284
|
let selKey = null
|
|
285
285
|
try {
|
|
286
286
|
const models = await listModels(
|
|
@@ -379,6 +379,7 @@ export function createModelPicker(ctx) {
|
|
|
379
379
|
...candidates.map((p) => ({ type: "item", text: `${p.name} (${defaultModelLabel(p)})`, name: p.name })),
|
|
380
380
|
])
|
|
381
381
|
if (!se) return
|
|
382
|
+
if (!(await confirmDelete(`Remove provider ${se.name}?`))) return
|
|
382
383
|
await persistRaw((raw) => {
|
|
383
384
|
raw.providers ??= []
|
|
384
385
|
const idx = raw.providers.findIndex((p) => p?.name === se.name)
|
|
@@ -460,7 +461,7 @@ export function createModelPicker(ctx) {
|
|
|
460
461
|
if (newCtx === undefined) delete agent.provider.context
|
|
461
462
|
else agent.provider.context = newCtx
|
|
462
463
|
if (agent.config?.agent?.compactThresholdAuto) {
|
|
463
|
-
const { resolveCompactThreshold } = await import("
|
|
464
|
+
const { resolveCompactThreshold } = await import("@thincoder/core/config.mjs")
|
|
464
465
|
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
465
466
|
}
|
|
466
467
|
}
|