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/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
- * provider 无效(`_providerInvalid` 标记或 provider 为 null)→ 先弹模型选择 picker
52
- * (复用 openModelPicker,展示当前可用 providers);用户选定后继续正常启动。
53
- * 选择取消(Esc)→ 仍进入 TUI,推送提示行("未配置有效 provider,可用 /model 配置
54
- * 渠道与模型")——绝不因无 provider 拒绝进入。返回 true 表示弹过选择流程。
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
- if (!(agent._providerInvalid || !agent.provider)) return false
58
- await openModelPicker()
59
- if (!agent.provider) {
60
- // MODEL-MERGE-SESSION 引导 A(F-6):空槽 + defaultModel 未设 → 提示 /config 默认模型入口
61
- // (index.mjs 本行为 D-S2 取消提示扩展——设计文件清单外——随批上报)
62
- const hasProviders = (agent.providers?.length ?? 0) > 0
63
- pushLine(hasProviders && !agent.config?.defaultModel
64
- ? "尚未设置默认模型(config.defaultModel——新会话起点):/config → 默认模型 设置一次;/model 仅改本会话"
65
- : "未配置有效 provider,可用 /model 配置渠道与模型", C.warn)
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
- return true
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 首帧前 —— provider 无效(_providerInvalid / provider 为 null)
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(§2.30.3.4):台账可见面——首扫(setImmediate)+ 周期;dispose 挂进程退出(:278 cleanup 先例)
229
- const ledgerSurface = startLedgerSurface({ state, agent, pushLine, render })
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 + TUI 色表注入**两件事——端壳静态链不得达 `node:sqlite`(W8 契约②);
3
+ * 本档只剩**动态 import 转口**一事——端壳静态链不得达 `node:sqlite`(W8 契约②);
4
4
  * 载入失败 = 降级不崩(N1),扫描拍重试)。
5
5
  *
6
- * 三面语义(启动行 / 变化行 / 状态位)见核档头注;判活展示(F-LX1)亦随归核自动带上——
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
- /** 单次扫描(直驱面——直通核实现;TUI 色表 `C` 注入)。 */
30
+ /** 单次扫描(直驱面——直通核实现)。 */
33
31
  export async function runLedgerScan(opts = {}) {
34
32
  const core = await loadCoreNow()
35
- return core.runLedgerScan({ ...opts, colors: C })
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({ ...ctx, colors: C }) })
43
+ .then((core) => { if (!disposed) handle = core.startLedgerSurface(ctx) })
46
44
  .catch(() => { /* 载入失败 ⇒ 空句柄(N1) */ })
47
45
  return {
48
46
  dispose() {
@@ -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
- * 不写 config(/model 不再串扰全局默认——根治)。候选 = **运行期拉取**(`GET /models`——M2;
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 纯会话级:写槽(agent 内存态 + saveSession)——绝不写 config。
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
- // 内存态(agent 会话运行时)——不触碰 config
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——槽双字段恒非空)——config 文件零写
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)
@@ -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..."
@@ -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()