thincoder 0.12.68 → 0.12.70
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 +32 -0
- package/README.md +15 -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 +50 -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 +18 -8
- package/src/command-table.mjs +8 -1
- package/src/completions.mjs +7 -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 +37 -20
- package/src/tui/ledger-surface.mjs +5 -7
- package/src/tui/model-picker.mjs +15 -6
- package/src/tui/startup.mjs +12 -2
- package/src/tui/wizard.mjs +1 -1
package/src/tui/index.mjs
CHANGED
|
@@ -47,24 +47,40 @@ import { createTimerWatch, fireTimerWake } from "./timer-watch.mjs"
|
|
|
47
47
|
export { upgradeFailureText, pendingNoticeReady } from "./update-notice.mjs"
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* SESSION.md §6.8 D-S2 — TUI 启动首帧前的 provider
|
|
51
|
-
*
|
|
52
|
-
* (复用 openModelPicker,展示当前可用 providers
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* SESSION.md §6.8 D-S2 — TUI 启动首帧前的 provider 分流(判据单源 = PROVIDER.md §6.22):
|
|
51
|
+
* · **invalid 类**(`state === "invalid"` ∨ `providerInvalidReason` 非空——核「provider 不完整面」
|
|
52
|
+
* 快照的合成式)→ 先弹模型选择 picker(复用 openModelPicker,展示当前可用 providers);
|
|
53
|
+
* 用户选定后继续正常启动。选择取消(Esc)→ 仍进入 TUI,推送提示行(措辞指渠道 ∕ 密钥——
|
|
54
|
+
* 绝不因无 provider 拒绝进入)。
|
|
55
|
+
* · **`fallback`**(无有效 defaultModel 但可运行)→ **不弹 picker**(不打断)+ 提示行明示
|
|
56
|
+
* (「尚未设置默认模型:本次使用 `<渠道>[:<模型>]`——…」;`model` 缺省 ⇒ 仅渠道名)。
|
|
57
|
+
* 返回 true 表示弹过选择流程。
|
|
55
58
|
*/
|
|
56
59
|
export async function promptProviderIfInvalid(agent, openModelPicker, pushLine) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
const cfg = agent.config ?? {}
|
|
61
|
+
// invalid 类(合成式——单源 = PROVIDER.md §6.22):`state === "invalid"` ∨ `providerInvalidReason`
|
|
62
|
+
// 非空(核 provider 不完整面快照);`!agent.provider`(`{}` 已被上游清成 null)同判。
|
|
63
|
+
// 结构不全(缺 baseURL 等)不在本门——归发送失败面(#840 `provider` 类词);负向锁 = PROVIDER.md:437。
|
|
64
|
+
const invalidClass = cfg.providerState === "invalid"
|
|
65
|
+
|| (typeof cfg.providerInvalidReason === "string" && cfg.providerInvalidReason !== "")
|
|
66
|
+
|| !agent.provider
|
|
67
|
+
if (invalidClass) {
|
|
68
|
+
await openModelPicker()
|
|
69
|
+
if (!agent.provider) {
|
|
70
|
+
// D-S2 取消提示(#841 措辞收正 = 渠道 ∕ 密钥——配置入口 /config)
|
|
71
|
+
pushLine("未配置有效渠道(渠道 ∥ 密钥缺位):/config 设置渠道与密钥;/model 选定后本会话生效并成为默认模型", C.warn)
|
|
72
|
+
}
|
|
73
|
+
return true
|
|
66
74
|
}
|
|
67
|
-
|
|
75
|
+
if (cfg.providerState === "fallback") {
|
|
76
|
+
const name = agent.provider?.name
|
|
77
|
+
if (name) {
|
|
78
|
+
const model = typeof agent.provider.model === "string" && agent.provider.model ? agent.provider.model : null
|
|
79
|
+
pushLine(`尚未设置默认模型:本次使用 \`${name}${model ? `:${model}` : ""}\`——/config → 默认模型 设置一次(或 /model 选定即成为默认模型)`, C.warn)
|
|
80
|
+
}
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
return false
|
|
68
84
|
}
|
|
69
85
|
|
|
70
86
|
/**
|
|
@@ -220,17 +236,18 @@ export async function startTUI(agent, opts = {}) {
|
|
|
220
236
|
|
|
221
237
|
// ---------------------------------------------------------- Startup screen + background indexing
|
|
222
238
|
|
|
223
|
-
// SESSION.md §6.8 D-S2:startTUI
|
|
224
|
-
// → 先弹模型选择 picker(keyStream 已挂 keypress,Esc/Enter 可用);Esc → 提示行,仍进 TUI
|
|
239
|
+
// SESSION.md §6.8 D-S2:startTUI 首帧前——按核统一态分流(单源 = PROVIDER.md §6.22):
|
|
240
|
+
// invalid 类 → 先弹模型选择 picker(keyStream 已挂 keypress,Esc/Enter 可用);Esc → 提示行,仍进 TUI;
|
|
241
|
+
// fallback → 不弹 picker + 提示行明示(本次使用 `<渠道>[:<模型>]`)
|
|
225
242
|
await promptProviderIfInvalid(agent, () => openModelPicker(), pushLine)
|
|
226
243
|
|
|
227
244
|
showStartup({ agent, state, opts, pushLine, pushLabel, render, startWizard })
|
|
228
|
-
// LEDGER-SURFACE
|
|
229
|
-
const ledgerSurface = startLedgerSurface({ state, agent,
|
|
245
|
+
// LEDGER-SURFACE:台账可见面——首扫(setImmediate)+ 周期;dispose 挂进程退出(同 cleanup 先例)
|
|
246
|
+
const ledgerSurface = startLedgerSurface({ state, agent, render })
|
|
230
247
|
// 端壳契约 = **恒同步返回 `{ dispose }`**(K-LX3 归核后与核形一致——核句柄异步就绪后桥接;
|
|
231
248
|
// 载入失败 ⇒ 空句柄)⇒ 退钩同步 `.dispose()`(零 TypeError)。
|
|
232
249
|
process.on("exit", () => ledgerSurface.dispose())
|
|
233
|
-
backgroundIndex({ agent, state, render })
|
|
250
|
+
backgroundIndex({ agent, state, render, pushLine })
|
|
234
251
|
|
|
235
252
|
// Check for updates (non-blocking, after startup screen)——实现 update-notice.mjs
|
|
236
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() {
|
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"
|
|
@@ -263,12 +265,20 @@ export function showStartup(ctx) {
|
|
|
263
265
|
render()
|
|
264
266
|
}
|
|
265
267
|
|
|
268
|
+
/** home 根防护(#867 · `MEMORY.md` §6.14 L-④):`cwd` = 用户主目录(resolve 后;win32 大小写不敏感)。 */
|
|
269
|
+
const isHomeDir = (dir) => typeof dir === "string" && dir !== "" &&
|
|
270
|
+
(process.platform === "win32" ? resolve(dir).toLowerCase() === resolve(homedir()).toLowerCase() : resolve(dir) === resolve(homedir()))
|
|
271
|
+
|
|
266
272
|
/** Background indexing (runs after startup screen, non-blocking); progress shown in status bar, not conversation.
|
|
267
273
|
* Prefers git diff incremental (fast); falls back to full scan when git is unavailable or on first run. */
|
|
268
274
|
export async function backgroundIndex(ctx) {
|
|
269
|
-
const { agent, state, render } = ctx
|
|
270
|
-
const { codeSync, docSync, gitSync } = await import("@thincoder/core/memory.mjs")
|
|
275
|
+
const { agent, state, render, pushLine } = ctx
|
|
271
276
|
const cwd = agent.cwd
|
|
277
|
+
if (isHomeDir(cwd)) { // #867:home 根 ⇒ 跳过索引(三 sync 零调用;不阻断启动)
|
|
278
|
+
pushLine("[index] Skipped: working directory is the home directory — start in a project dir", C.warn)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
const { codeSync, docSync, gitSync } = await import("@thincoder/core/memory.mjs")
|
|
272
282
|
let codeFiles = 0, docFiles = 0
|
|
273
283
|
|
|
274
284
|
state.status = "Indexing..."
|
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()
|