thincoder 0.12.69 → 0.12.71
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 +37 -0
- package/README.md +11 -3
- package/bin/node-version-gate.cjs +28 -0
- package/bin/thincoder.cjs +2 -0
- package/bin/thincoder.mjs +2 -0
- package/package.json +2 -2
- package/src/acp/bridge.mjs +54 -12
- package/src/acp/handlers-session.mjs +95 -44
- package/src/acp/handlers-slots.mjs +41 -14
- package/src/acp/read-data.mjs +127 -0
- package/src/acp/resource-link.mjs +124 -0
- package/src/acp/session.mjs +1 -1
- package/src/acp.mjs +15 -6
- package/src/command-interactive.mjs +10 -1
- package/src/command-table.mjs +8 -1
- package/src/completions.mjs +7 -4
- package/src/prompt-injections.mjs +21 -4
- package/src/tui/cmd-model.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +10 -0
- package/src/tui/cmd-undo.mjs +16 -38
- package/src/tui/config-helpers.mjs +34 -1
- package/src/tui/index.mjs +5 -5
- package/src/tui/ledger-surface.mjs +5 -7
- package/src/tui/lifecycle-records.mjs +12 -4
- package/src/tui/model-picker.mjs +15 -6
- package/src/tui/startup.mjs +14 -2
- package/src/tui/suspension-drive.mjs +9 -2
- package/src/tui/wizard.mjs +1 -1
package/src/tui/index.mjs
CHANGED
|
@@ -68,7 +68,7 @@ export async function promptProviderIfInvalid(agent, openModelPicker, pushLine)
|
|
|
68
68
|
await openModelPicker()
|
|
69
69
|
if (!agent.provider) {
|
|
70
70
|
// D-S2 取消提示(#841 措辞收正 = 渠道 ∕ 密钥——配置入口 /config)
|
|
71
|
-
pushLine("未配置有效渠道(渠道 ∥ 密钥缺位):/config 设置渠道与密钥;/model
|
|
71
|
+
pushLine("未配置有效渠道(渠道 ∥ 密钥缺位):/config 设置渠道与密钥;/model 选定后本会话生效并成为默认模型", C.warn)
|
|
72
72
|
}
|
|
73
73
|
return true
|
|
74
74
|
}
|
|
@@ -76,7 +76,7 @@ export async function promptProviderIfInvalid(agent, openModelPicker, pushLine)
|
|
|
76
76
|
const name = agent.provider?.name
|
|
77
77
|
if (name) {
|
|
78
78
|
const model = typeof agent.provider.model === "string" && agent.provider.model ? agent.provider.model : null
|
|
79
|
-
pushLine(`尚未设置默认模型:本次使用 \`${name}${model ? `:${model}` : ""}\`——/config → 默认模型
|
|
79
|
+
pushLine(`尚未设置默认模型:本次使用 \`${name}${model ? `:${model}` : ""}\`——/config → 默认模型 设置一次(或 /model 选定即成为默认模型)`, C.warn)
|
|
80
80
|
}
|
|
81
81
|
return false
|
|
82
82
|
}
|
|
@@ -242,12 +242,12 @@ export async function startTUI(agent, opts = {}) {
|
|
|
242
242
|
await promptProviderIfInvalid(agent, () => openModelPicker(), pushLine)
|
|
243
243
|
|
|
244
244
|
showStartup({ agent, state, opts, pushLine, pushLabel, render, startWizard })
|
|
245
|
-
// LEDGER-SURFACE
|
|
246
|
-
const ledgerSurface = startLedgerSurface({ state, agent,
|
|
245
|
+
// LEDGER-SURFACE:台账可见面——首扫(setImmediate)+ 周期;dispose 挂进程退出(同 cleanup 先例)
|
|
246
|
+
const ledgerSurface = startLedgerSurface({ state, agent, render })
|
|
247
247
|
// 端壳契约 = **恒同步返回 `{ dispose }`**(K-LX3 归核后与核形一致——核句柄异步就绪后桥接;
|
|
248
248
|
// 载入失败 ⇒ 空句柄)⇒ 退钩同步 `.dispose()`(零 TypeError)。
|
|
249
249
|
process.on("exit", () => ledgerSurface.dispose())
|
|
250
|
-
backgroundIndex({ agent, state, render })
|
|
250
|
+
backgroundIndex({ agent, state, render, pushLine })
|
|
251
251
|
|
|
252
252
|
// Check for updates (non-blocking, after startup screen)——实现 update-notice.mjs
|
|
253
253
|
// (D-S1c):有 picker 打开时不硬抢——挂到 state.pendingNotice,picker 全部关闭后由
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ledger-surface.mjs — TUI 台账可见面胶水(K-LX3 归核:实现单源 = `@thincoder/core/ledger-surface.mjs`,
|
|
3
|
-
* 本档只剩**动态 import
|
|
3
|
+
* 本档只剩**动态 import 转口**一事——端壳静态链不得达 `node:sqlite`(W8 契约②);
|
|
4
4
|
* 载入失败 = 降级不崩(N1),扫描拍重试)。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* 本档零实现、零复刻(KD-M2-3)。
|
|
6
|
+
* 状态位语义见核档头注;判活展示(F-LX1)亦随归核自动带上——本档零实现、零复刻(KD-M2-3)。
|
|
8
7
|
*
|
|
9
8
|
* 两端契约(#233——端壳与核形一致):`startLedgerSurface` **恒同步返回 `{ dispose }`**——
|
|
10
9
|
* 核句柄异步就绪(动态 import)后桥接;载入失败 ⇒ 空句柄(`dispose` 恒可调);`dispose()`
|
|
11
10
|
* 早于核就绪 ⇒ 置位且**跳过启动**(核侧零调用——零周期;N1 降级不崩)。调用点因此恒可按
|
|
12
11
|
* 同步形态挂进程退钩(`process.on("exit", () => handle.dispose())`)。
|
|
13
12
|
*/
|
|
14
|
-
import { C } from "./ansi.mjs"
|
|
15
13
|
|
|
16
14
|
/** 核台账面惰性载入(动态 import——失败不缓存,下次扫描拍重试;N1 降级不崩)。 */
|
|
17
15
|
let _core = null
|
|
@@ -29,10 +27,10 @@ let _loadCoreForTest = null
|
|
|
29
27
|
export function _setLoadCoreForTest(fn) { _loadCoreForTest = fn }
|
|
30
28
|
const loadCoreNow = () => (_loadCoreForTest ?? loadCore)()
|
|
31
29
|
|
|
32
|
-
/**
|
|
30
|
+
/** 单次扫描(直驱面——直通核实现)。 */
|
|
33
31
|
export async function runLedgerScan(opts = {}) {
|
|
34
32
|
const core = await loadCoreNow()
|
|
35
|
-
return core.runLedgerScan(
|
|
33
|
+
return core.runLedgerScan(opts)
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
/** TUI 挂载(首扫 + 周期——直通核实现):**恒同步返回 `{ dispose }`**——核句柄异步就绪后
|
|
@@ -42,7 +40,7 @@ export function startLedgerSurface(ctx = {}) {
|
|
|
42
40
|
let disposed = false
|
|
43
41
|
let handle = null
|
|
44
42
|
loadCoreNow()
|
|
45
|
-
.then((core) => { if (!disposed) handle = core.startLedgerSurface(
|
|
43
|
+
.then((core) => { if (!disposed) handle = core.startLedgerSurface(ctx) })
|
|
46
44
|
.catch(() => { /* 载入失败 ⇒ 空句柄(N1) */ })
|
|
47
45
|
return {
|
|
48
46
|
dispose() {
|
|
@@ -33,9 +33,11 @@ export function digestCapRecord(turns) {
|
|
|
33
33
|
return { kind: "digest", status: "cap", mode: "stop", turns }
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/** digest 收尾记录(终态行同点):`ok` ∥ `ms`(行文案 `seconds` 由同一 `ms`
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
/** digest 收尾记录(终态行同点):`ok` ∥ `ms`(行文案 `seconds` 由同一 `ms` 派生——同值单算式);
|
|
37
|
+
* `unsettled` = 本轮未销账条数(消化账务批 · 2026-10-05 · 台账 #930 —— 残余行判据 ∥ 重建同调;
|
|
38
|
+
* 零未销账 ⇒ 键缺席——零噪音,与帧面「> 0 才携」同判)。 */
|
|
39
|
+
export function digestEndRecord({ ok, ms, unsettled = 0 }) {
|
|
40
|
+
return { kind: "digest", status: "end", ok, ms, ...(Number.isFinite(unsettled) && unsettled > 0 ? { unsettled } : {}) }
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/** subagent 归档快照记录(冻结载体行插入同点):meta = 冻结时点块头事实(含 `pool`/`queued?`——§6.26)
|
|
@@ -114,7 +116,13 @@ export function digestTraceLines(rec, startN = null) {
|
|
|
114
116
|
if (rec.status === "end") {
|
|
115
117
|
if (!Number.isFinite(startN) || startN <= 0 || !Number.isFinite(rec.ms)) return []
|
|
116
118
|
const seconds = (rec.ms / 1000).toFixed(1) // 与记录 `ms` 同值单算式
|
|
117
|
-
|
|
119
|
+
const lines = [{ text: t(rec.ok === false ? "digest.aborted" : "digest.done", { n: startN, seconds }), color: C.dim }]
|
|
120
|
+
// 未销账残余行(消化账务批 · 2026-10-05 · 台账 #930 · §6.31.6):终态行之后再落一行 dim
|
|
121
|
+
// (核字典 `digest.residue` 单源——端侧零自持字面);零未销账 ⇒ 零行(零噪音)。
|
|
122
|
+
// 随终态行同门(重建面 `startN` 不可得 ⇒ 零行——fail-closed)。
|
|
123
|
+
const unsettled = Number.isFinite(rec.unsettled) && rec.unsettled > 0 ? rec.unsettled : 0
|
|
124
|
+
if (unsettled > 0) lines.push({ text: t("digest.residue", { n: unsettled }), color: C.dim })
|
|
125
|
+
return lines
|
|
118
126
|
}
|
|
119
127
|
return []
|
|
120
128
|
}
|
package/src/tui/model-picker.mjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* model-picker.mjs — /model two-level picker + provider management + session-slot model
|
|
3
3
|
* selection(MODEL-MERGE-SESSION 拆分产物——/model 两级面自 pickers.mjs 迁出)。
|
|
4
4
|
*
|
|
5
|
-
* 语义(2026-09-10 MODEL-SELECTION v2):selectModel 写**会话槽**(agent 内存态 + saveSession
|
|
6
|
-
*
|
|
5
|
+
* 语义(2026-09-10 MODEL-SELECTION v2):selectModel 写**会话槽**(agent 内存态 + saveSession);
|
|
6
|
+
* 会话级模型面**实变** ⇒ 同拍写回 config.defaultModel(判据句 6——写面单点 = `config-helpers.mjs`
|
|
7
|
+
* `carryoverDefaultModel`;等值 ∕ 回声零写)。候选 = **运行期拉取**(`GET /models`——M2;
|
|
7
8
|
* 拉到的行直接可选);显式 `provider:model` 一律放行(M4——仅[空值/裸值/未知 provider]无效)。
|
|
8
9
|
* 切换成功回显规格来源(M6);渠道默认模型 = `providers[].model` 单值(M3——显示回退读取)。
|
|
9
10
|
* 配置写入面(加渠道 / 设 key)探一次 `/models`(M9——探不通标「不可用」+ 明示原因,不阻断保存)。
|
|
@@ -15,7 +16,8 @@
|
|
|
15
16
|
*/
|
|
16
17
|
import { sliceByWidth } from "./render.mjs"
|
|
17
18
|
import { providerSpec, specMatch } from "@thincoder/core/config.mjs"
|
|
18
|
-
import { saveSession } from "@thincoder/core/session.mjs"
|
|
19
|
+
import { saveSession, loadSlotFile } from "@thincoder/core/session.mjs"
|
|
20
|
+
import { carryoverDefaultModel } from "./config-helpers.mjs"
|
|
19
21
|
import { getProviderModels, modelListFailureText, dedupeModels } from "./model-catalog.mjs"
|
|
20
22
|
import { createProviderAdmin } from "./provider-admin.mjs"
|
|
21
23
|
|
|
@@ -218,7 +220,8 @@ export function createModelPicker(ctx) {
|
|
|
218
220
|
return entries
|
|
219
221
|
}
|
|
220
222
|
|
|
221
|
-
/** F-3 /model
|
|
223
|
+
/** F-3 /model 会话级:写槽(agent 内存态 + saveSession);选定实变 ⇒ 同拍写回
|
|
224
|
+
* config.defaultModel(判据句 6——新会话起点随动;等值 ∕ 回声零写;写回失败不反扑会话写、记错零静默)。
|
|
222
225
|
* 放行语义(M4):仅未知 provider 拒——显式 p:m 一律放行(候选外/多冒号不再拒)。 */
|
|
223
226
|
async function selectModel(item) {
|
|
224
227
|
closePicker()
|
|
@@ -229,7 +232,9 @@ export function createModelPicker(ctx) {
|
|
|
229
232
|
if (!item.model) {
|
|
230
233
|
throw new Error(`Missing model name for provider: ${item.provider}`)
|
|
231
234
|
}
|
|
232
|
-
//
|
|
235
|
+
// 写前读:槽复合基线(判据句 6 实变单元——档缺 ⇒ null;`_slot` 未钉 ⇒ 无槽面)
|
|
236
|
+
const slotBefore = agent._slot == null ? null : loadSlotFile(agent.cwd, agent._slot)
|
|
237
|
+
// 内存态(agent 会话运行时)——config 写仅经写回单点(下方实变节)
|
|
233
238
|
agent.activeProvider = target.name
|
|
234
239
|
agent.activeModel = item.model
|
|
235
240
|
agent.provider = { ...target }
|
|
@@ -238,8 +243,12 @@ export function createModelPicker(ctx) {
|
|
|
238
243
|
const { resolveCompactThreshold } = await import("@thincoder/core/config.mjs")
|
|
239
244
|
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
240
245
|
}
|
|
241
|
-
// 写会话槽(saveSession
|
|
246
|
+
// 写会话槽(saveSession——槽双字段恒非空)——写回单点在其后(槽先配置后)
|
|
242
247
|
saveSession(agent)
|
|
248
|
+
// 选定写回(判据句 6——定序槽先配置后;失败不反扑会话写、记错零静默)
|
|
249
|
+
const carried = await carryoverDefaultModel({ provider: target.name, model: item.model, slotBefore })
|
|
250
|
+
if (carried.ok !== true) console.error(`[model] default model carryover failed: ${carried.reason}`)
|
|
251
|
+
else if (carried.written === true && agent.config) agent.config.defaultModel = `${target.name}:${item.model}` // 内存镜像(/config 显示面新鲜——wizard.mjs:210-211 先例)
|
|
243
252
|
// M6 切换回显:规格来源一行(DEFAULT 兜底 → 警示色 + /config 提示)
|
|
244
253
|
const { matched } = specMatch(agent.provider.model)
|
|
245
254
|
const spec = providerSpec(agent.provider)
|
package/src/tui/startup.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { homedir } from "node:os"
|
|
2
|
+
import { resolve } from "node:path"
|
|
1
3
|
import { listSlots, ledgerHealth } from "@thincoder/core/session.mjs"
|
|
2
4
|
import { stripAtRefs } from "@thincoder/core/file-refs.mjs"
|
|
3
5
|
import { ansi, C } from "./ansi.mjs"
|
|
@@ -242,6 +244,8 @@ export function showStartup(ctx) {
|
|
|
242
244
|
// R25(F-R25c):上次运行异常终止提示——bin 入口扫描 crash-reports(24h 窗)经
|
|
243
245
|
// startTUI opts.crashNotice 传入——无匹配为 undefined → 不提示(负例)
|
|
244
246
|
if (opts.crashNotice) pushLine(opts.crashNotice, C.warn)
|
|
247
|
+
// F-LX3(批 2026-10-05-ledger-variant-db-notice · 台账 #935 · LEDGER.md §12.2):变体键库首跑提示(恰一行;无命中不传)
|
|
248
|
+
if (opts.ledgerVariantNotice) pushLine(opts.ledgerVariantNotice, C.warn)
|
|
245
249
|
|
|
246
250
|
// Recover previous session: rebuild from history (lazy — display snapshot is
|
|
247
251
|
// deprecated; it drifted out of sync with history on VS Code writes).
|
|
@@ -263,12 +267,20 @@ export function showStartup(ctx) {
|
|
|
263
267
|
render()
|
|
264
268
|
}
|
|
265
269
|
|
|
270
|
+
/** home 根防护(#867 · `MEMORY.md` §6.14 L-④):`cwd` = 用户主目录(resolve 后;win32 大小写不敏感)。 */
|
|
271
|
+
const isHomeDir = (dir) => typeof dir === "string" && dir !== "" &&
|
|
272
|
+
(process.platform === "win32" ? resolve(dir).toLowerCase() === resolve(homedir()).toLowerCase() : resolve(dir) === resolve(homedir()))
|
|
273
|
+
|
|
266
274
|
/** Background indexing (runs after startup screen, non-blocking); progress shown in status bar, not conversation.
|
|
267
275
|
* Prefers git diff incremental (fast); falls back to full scan when git is unavailable or on first run. */
|
|
268
276
|
export async function backgroundIndex(ctx) {
|
|
269
|
-
const { agent, state, render } = ctx
|
|
270
|
-
const { codeSync, docSync, gitSync } = await import("@thincoder/core/memory.mjs")
|
|
277
|
+
const { agent, state, render, pushLine } = ctx
|
|
271
278
|
const cwd = agent.cwd
|
|
279
|
+
if (isHomeDir(cwd)) { // #867:home 根 ⇒ 跳过索引(三 sync 零调用;不阻断启动)
|
|
280
|
+
pushLine("[index] Skipped: working directory is the home directory — start in a project dir", C.warn)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
const { codeSync, docSync, gitSync } = await import("@thincoder/core/memory.mjs")
|
|
272
284
|
let codeFiles = 0, docFiles = 0
|
|
273
285
|
|
|
274
286
|
state.status = "Indexing..."
|
|
@@ -28,6 +28,8 @@ import { upstreamAskLabelVars } from "@thincoder/core/agent-tools/parent-channel
|
|
|
28
28
|
import { poolLive, startSuspension } from "@thincoder/core/agent/suspension.mjs"
|
|
29
29
|
// #726 写点①:痕行 ∥ 起跑·收尾记录同点双动作(记录形/文案 = lifecycle-records 单一实现)
|
|
30
30
|
import { pushRecord } from "@thincoder/core/context.mjs"
|
|
31
|
+
// 消化账务批 · 2026-10-05 · 台账 #930(§6.31.6):残余行判据单源(核件 `unsettledCount`)
|
|
32
|
+
import { unsettledCount } from "@thincoder/core/agent/digest-account.mjs"
|
|
31
33
|
import { digestEndRecord, digestStartRecord, digestTraceLines, recordCarrier } from "./lifecycle-records.mjs"
|
|
32
34
|
|
|
33
35
|
export { poolLive }
|
|
@@ -90,7 +92,9 @@ function backgroundStatusText(agent) {
|
|
|
90
92
|
* 起跑行 ∥ 计数行 ∥ cap 行 ∥ 终态行:不改 ∥ 不删 ∥ 不退场(零清理机器;退场机随拆);② 起跑窗
|
|
91
93
|
* (沿 #754 裁 A)——起跑行族落盘后 ⇒ 起跑快照(起跑刻 pending 单容器)逐条冻结入流,落位 = 本族之后;
|
|
92
94
|
* ③ 终态行 = **到达序追加于当刻流末**(`pushLine`——零就地换文 ∥ 不动原起跑行)。
|
|
93
|
-
* 导出面 = 批内件直驱(生产唯一调用 = 本档 `driveTurn`)。
|
|
95
|
+
* 导出面 = 批内件直驱(生产唯一调用 = 本档 `driveTurn`)。
|
|
96
|
+
* **消化账务批(2026-10-05 · 台账 #930 · §6.31.6)**:收尾 `unsettled` 随记录(非上行轮——与桌面 ∥
|
|
97
|
+
* VSC 同判据;判据单源 = 核 `unsettledCount`)⇒ 终态行之后再落残余行(`digest.residue`——lifecycle-records 单实现)。 */
|
|
94
98
|
export async function digestTurn(ctx, upstream = false) {
|
|
95
99
|
const { agent, state, pushLine } = ctx
|
|
96
100
|
const manual = !agent.autoApprove
|
|
@@ -111,7 +115,10 @@ export async function digestTurn(ctx, upstream = false) {
|
|
|
111
115
|
const outcome = await runAgentTurn(digestCtx, "", { autoTurn: true, upstreamTurn: upstream, skipSession: true })
|
|
112
116
|
const ms = Date.now() - d0
|
|
113
117
|
logEvent("digest:end", { pendingN: pendingFamilyCount(agent), ms, ...(upstream ? { upstream: true } : {}) })
|
|
114
|
-
|
|
118
|
+
// 消化账务批(§6.31.6):非上行轮且 > 0 才携 `unsettled`(残余行载荷——与桌面 ∥ VSC 同判据;
|
|
119
|
+
// 消化轮 = `autoTurn ∧ ¬upstreamTurn`,§6.31.2)
|
|
120
|
+
const unsettled = upstream ? 0 : unsettledCount(agent)
|
|
121
|
+
const endRec = digestEndRecord({ ok: outcome === "ok", ms, unsettled })
|
|
115
122
|
// ③ 终态行 = 到达序追加于当刻流末(自然形——零就地换文 ∥ 不动原起跑行)
|
|
116
123
|
for (const l of digestTraceLines(endRec, pend0)) pushLine(l.text, l.color)
|
|
117
124
|
pushRecord(carrier, endRec) // 收尾记录(与终态行同点)
|
package/src/tui/wizard.mjs
CHANGED
|
@@ -162,7 +162,7 @@ export function createWizard(ctx) {
|
|
|
162
162
|
state.wizard = null
|
|
163
163
|
// MODEL-MERGE-SESSION 引导 A(F-6):有 provider 但 defaultModel 未设时指引 /config 入口
|
|
164
164
|
const hint = (agent.providers?.length ?? 0) > 0 && !agent.config?.defaultModel
|
|
165
|
-
? "Skipped initial setup. 已配置渠道但 config.defaultModel 未设——新会话无起点:/config → 默认模型 设置一次(或 /model
|
|
165
|
+
? "Skipped initial setup. 已配置渠道但 config.defaultModel 未设——新会话无起点:/config → 默认模型 设置一次(或 /model 选定即成为默认模型)。"
|
|
166
166
|
: "Skipped initial setup. Use /model to add providers and configure API keys anytime."
|
|
167
167
|
pushLine(hint, C.dim)
|
|
168
168
|
render()
|