thincoder 0.12.66 → 0.12.68
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 +52 -0
- package/README.md +23 -5
- package/bin/thincoder.mjs +43 -331
- package/package.json +2 -2
- package/src/acp/bridge.mjs +13 -12
- package/src/acp/handlers-session.mjs +12 -1
- package/src/cli/make-agent.mjs +44 -119
- package/src/cli/memory-command.mjs +45 -3
- package/src/cli/permission.mjs +1 -1
- package/src/command-interactive.mjs +186 -0
- package/src/command-table.mjs +185 -0
- package/src/completions.mjs +36 -12
- package/src/crash-reports.mjs +42 -20
- package/src/distill.mjs +3 -3
- package/src/heap-watch.mjs +5 -14
- package/src/prompt-injections.mjs +1 -1
- package/src/tui/agent-turn.mjs +114 -69
- package/src/tui/clipboard.mjs +74 -30
- package/src/tui/cmd-advisor.mjs +54 -13
- package/src/tui/cmd-clear.mjs +2 -2
- package/src/tui/cmd-config.mjs +26 -2
- package/src/tui/cmd-eng.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +1 -1
- package/src/tui/cmd-new.mjs +2 -2
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-session.mjs +13 -5
- package/src/tui/cmd-think.mjs +35 -17
- package/src/tui/cmd-timers.mjs +33 -0
- package/src/tui/conversation-writer.mjs +2 -2
- package/src/tui/display-budget.mjs +9 -9
- package/src/tui/fold-block.mjs +1 -1
- package/src/tui/index.mjs +23 -5
- package/src/tui/interaction.mjs +5 -3
- package/src/tui/key-handler-busy.mjs +17 -12
- package/src/tui/key-handler-ctrlc.mjs +5 -5
- package/src/tui/key-handler-edit.mjs +12 -8
- package/src/tui/key-handler-search.mjs +1 -1
- package/src/tui/key-handler.mjs +2 -2
- package/src/tui/key-modes.mjs +1 -1
- package/src/tui/layout.mjs +5 -5
- package/src/tui/ledger-surface.mjs +26 -5
- package/src/tui/lifecycle-records.mjs +199 -0
- package/src/tui/model-picker.mjs +16 -199
- package/src/tui/mouse.mjs +5 -5
- package/src/tui/pickers.mjs +6 -2
- package/src/tui/provider-admin.mjs +213 -0
- package/src/tui/queued-merge.mjs +12 -0
- package/src/tui/queued-pickup.mjs +34 -0
- package/src/tui/render-conversation.mjs +49 -4
- package/src/tui/render-frame.mjs +54 -11
- package/src/tui/render-loop.mjs +2 -3
- package/src/tui/render-segments.mjs +4 -4
- package/src/tui/render.mjs +1 -1
- package/src/tui/slash-commands.mjs +3 -0
- package/src/tui/startup.mjs +28 -3
- package/src/tui/subagent-blocks.mjs +37 -29
- package/src/tui/subagent-children.mjs +5 -5
- package/src/tui/subagent-freeze.mjs +43 -41
- package/src/tui/subagent-panel.mjs +11 -11
- package/src/tui/suspension-drive.mjs +173 -253
- package/src/tui/timer-watch.mjs +64 -0
- package/src/tui/tool-args.mjs +10 -71
- package/src/tui/tool-display.mjs +7 -7
- package/src/tui/tool-events.mjs +40 -35
- package/src/tui/tui-lifecycle.mjs +13 -4
- package/src/tui/tui-state.mjs +3 -2
- package/src/tui/turn-face.mjs +6 -3
- package/src/tui/wizard.mjs +7 -2
- package/src/tui/wrapped-spawn.mjs +3 -3
package/src/tui/cmd-clear.mjs
CHANGED
|
@@ -9,14 +9,14 @@ export async function handleClearCommand(ctx) {
|
|
|
9
9
|
], { defaultIndex: 1 })
|
|
10
10
|
if (e?.action === "yes") {
|
|
11
11
|
state.lines = []
|
|
12
|
-
state._linesChars = 0 // TUI-OOM-ROOTCAUSE
|
|
12
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.3):行集清空 → 字符账同步归零
|
|
13
13
|
state.streaming = ""
|
|
14
14
|
render()
|
|
15
15
|
}
|
|
16
16
|
return
|
|
17
17
|
}
|
|
18
18
|
state.lines = []
|
|
19
|
-
state._linesChars = 0 // TUI-OOM-ROOTCAUSE
|
|
19
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.3):行集清空 → 字符账同步归零
|
|
20
20
|
state.streaming = ""
|
|
21
21
|
render()
|
|
22
22
|
}
|
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -20,6 +20,14 @@ export function poolCur(pl, defaults) {
|
|
|
20
20
|
return (k) => (Number.isInteger(pl?.[k]) && pl[k] >= 1 ? pl[k] : defaults.agent.poolLimits[k])
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Effort 菜单档位构造(#16 双 `none` 去重——枚举自带 `none` 的族如 qwen3.8-flash 时原来出两行):
|
|
24
|
+
* `none` 恒为首项且唯一,其余档序保持(`filter` 保序)。语义面零改——`none` = 清档,
|
|
25
|
+
* 消费点 `:205`/`:232` 只认 `=== "none"`;空枚举在 `pickEffort` 处已早退 null、不经本函数。
|
|
26
|
+
* Exported for unit tests (`cmd-config-effort.test.mjs`). */
|
|
27
|
+
export function effortMenuLevels(enumList) {
|
|
28
|
+
return ["none", ...(enumList ?? []).filter((l) => l !== "none")]
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
/** /config command: view and set agent/embedding/proxy config. */
|
|
24
32
|
export async function handleConfigCommand(ctx, args = []) {
|
|
25
33
|
const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, maskKey, pickModelForSlot } = ctx
|
|
@@ -27,6 +35,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
27
35
|
let ac = agent.config?.agent ?? {}
|
|
28
36
|
let ec = agent.config?.embedding ?? {}
|
|
29
37
|
let tc = agent.config?.traces ?? {}
|
|
38
|
+
let dc = agent.config?.diagnostics ?? {}
|
|
30
39
|
|
|
31
40
|
// agent.config.proxy 已被 loadConfig 归一化为 { uri, web, model } | undefined
|
|
32
41
|
function proxySummary() {
|
|
@@ -177,7 +186,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
177
186
|
// min/low/medium/high/max list made the user pick values that the runtime then
|
|
178
187
|
// silently dropped as out-of-enum (2026-08-17 audit). Show the model's real enum.
|
|
179
188
|
if (!enumList || enumList.length === 0) return null // model has no effort — skip
|
|
180
|
-
const levels =
|
|
189
|
+
const levels = effortMenuLevels(enumList) // "none" = clear the effort(自带去重——#16)
|
|
181
190
|
const entries = levels.map((l) => ({ type: "item", text: l === current ? `${l} ← current` : l, action: l }))
|
|
182
191
|
const c = await showPicker("Reasoning effort", entries, { defaultIndex: Math.max(0, levels.indexOf(current ?? "none")) })
|
|
183
192
|
return c ? c.action : null // Esc → null (keep unchanged)
|
|
@@ -242,7 +251,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
242
251
|
}
|
|
243
252
|
}
|
|
244
253
|
|
|
245
|
-
// ──
|
|
254
|
+
// ── 并发池子菜单(agent.poolLimits——读/改三域;保存经 saveProxy
|
|
246
255
|
// 落盘 + reloadConfig 热应用——下个 spawn 生效)──
|
|
247
256
|
async function poolMenu() {
|
|
248
257
|
let poolIdx = 0
|
|
@@ -333,6 +342,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
333
342
|
ac = agent.config?.agent ?? {}
|
|
334
343
|
ec = agent.config?.embedding ?? {}
|
|
335
344
|
tc = agent.config?.traces ?? {}
|
|
345
|
+
dc = agent.config?.diagnostics ?? {}
|
|
336
346
|
const consultCount = (ac.consultModels ?? []).length
|
|
337
347
|
const pl = ac.poolLimits ?? {}
|
|
338
348
|
const cur = poolCur(pl, DEFAULTS)
|
|
@@ -346,6 +356,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
346
356
|
{ type: "item", text: `agent.verifyGuard = ${ac.verifyGuard === true ? "on" : "off"}`, action: "agent.verifyGuard" },
|
|
347
357
|
{ type: "item", text: `traces.enabled = ${tc.enabled === false ? "off" : "on"}(轨迹存档——发布默认关——隐私)`, action: "traces.enabled" },
|
|
348
358
|
{ type: "item", text: `traces.retentionHours = ${tc.retentionHours ?? 24} h(超期文件启动时清理)`, action: "traces.retentionHours" },
|
|
359
|
+
{ type: "item", text: `diagnostics.heapSnapshot = ${dc.heapSnapshot === true ? "on" : "off"}(堆快照采集——默认关——桌面会话即时生效)`, action: "diagnostics.heapSnapshot" },
|
|
349
360
|
{ type: "item", text: `agent.consultModels = ${consultCount} model(s)${consultCount ? ` (${(ac.consultModels ?? []).map((m) => m.provider + ":" + m.model).join(", ")})` : ""}`, action: "consult" },
|
|
350
361
|
{ type: "item", text: `agent.consultTurns = ${ac.consultTurns ?? 40}`, action: "agent.consultTurns" },
|
|
351
362
|
{ type: "item", text: `agent.consultTimeoutMs = ${Math.round((ac.consultTimeoutMs ?? 600000) / 60000)} min`, action: "agent.consultTimeoutMs" },
|
|
@@ -369,6 +380,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
369
380
|
pushLine(`agent.verifyGuard: ${ac.verifyGuard === true ? "on" : "off"}`, C.dim)
|
|
370
381
|
pushLine(`traces.enabled: ${tc.enabled === false ? "off" : "on"}(默认 off——发布隐私——本地分析可开)`, C.dim)
|
|
371
382
|
pushLine(`traces.retentionHours: ${tc.retentionHours ?? 24}(超期文件启动清理——D-TR10)`, C.dim)
|
|
383
|
+
pushLine(`diagnostics.heapSnapshot: ${dc.heapSnapshot === true ? "on" : "off"}(堆快照采集——默认关——桌面会话即时生效)`, C.dim)
|
|
372
384
|
pushLine(`agent.consultModels: ${(ac.consultModels ?? []).map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ") || "(none)"}`, C.dim)
|
|
373
385
|
pushLine(`agent.poolLimits: { engCoder: ${cur("engCoder")}, other: ${cur("other")}, advisor: ${cur("advisor")} }(async 分域上限——默认 4/4/4——agent.poolLimits 可配)`, C.dim)
|
|
374
386
|
pushLine(`agent.consultTurns: ${ac.consultTurns ?? 40}`, C.dim)
|
|
@@ -429,6 +441,18 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
429
441
|
continue // 回主菜单(Esc 退出)
|
|
430
442
|
}
|
|
431
443
|
|
|
444
|
+
if (choice.action === "diagnostics.heapSnapshot") {
|
|
445
|
+
const newVal = dc.heapSnapshot !== true // 显式写布尔(默认关——fail-closed 显示判定同向)
|
|
446
|
+
try {
|
|
447
|
+
await saveProxy((raw) => {
|
|
448
|
+
raw.diagnostics = { ...(raw.diagnostics ?? {}), heapSnapshot: newVal }
|
|
449
|
+
})
|
|
450
|
+
pushLabel("❯ Config", ansi.bold + C.tool)
|
|
451
|
+
pushLine(`diagnostics.heapSnapshot = ${newVal ? "on" : "off"}(桌面会话即时生效 ∕ 本进程下次启动)`, C.tool)
|
|
452
|
+
} catch (error) { pushLine(`Save failed: ${error.message}`, C.error) }
|
|
453
|
+
continue // 回主菜单(Esc 退出)
|
|
454
|
+
}
|
|
455
|
+
|
|
432
456
|
// Embedding model is fixed (BAAI/bge-m3, SiliconFlow) — no picker; it's over-engineering
|
|
433
457
|
// to expose model choice when the vector index format assumes one embedding space.
|
|
434
458
|
|
package/src/tui/cmd-eng.mjs
CHANGED
|
@@ -52,7 +52,7 @@ export async function handleEngCommand(ctx) {
|
|
|
52
52
|
// FR31 ③ / KD10(翻转点②·CLI `/eng`):ON ⇒ 清 plan 残留(内存位 + 未注入的 plan 提示语);
|
|
53
53
|
// OFF 方向不动(普通模式零改——FR31 边界)。提示行仅在真清到东西时出(不喧宾)。
|
|
54
54
|
const planReset = agent.config.agent.engineering ? clearPlanMode(agent) : false
|
|
55
|
-
// §
|
|
55
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.10 D-24b: per-review instances die with the mode (fresh convergence cycles
|
|
56
56
|
// on the next toggle).
|
|
57
57
|
agent._advisorRuns = new Map()
|
|
58
58
|
if (!agent.config.agent.engineering) {
|
package/src/tui/cmd-mcp.mjs
CHANGED
|
@@ -181,7 +181,7 @@ Return ONLY the JSON object:`,
|
|
|
181
181
|
}],
|
|
182
182
|
tools: [],
|
|
183
183
|
signal: AbortSignal.timeout(15_000),
|
|
184
|
-
// §
|
|
184
|
+
// TRACES.md §6.1 D-TR4/D-TR6(2026-09-04 fix round1):/mcp ai 生成调用经 chat() 唯一
|
|
185
185
|
// 采集点——补轨迹元数据 + traces 开关透传(agent.config.traces.enabled——
|
|
186
186
|
// 关=不落盘必须全覆盖——与 agent.mjs/context.mjs 同模式)
|
|
187
187
|
logCtx: {
|
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -15,12 +15,12 @@ export async function handleNewCommand(ctx) {
|
|
|
15
15
|
// slot(实锤 .19/.3 双副本)。_slot 更新为粘性新槽位。
|
|
16
16
|
resetSessionState(agent)
|
|
17
17
|
agent._slot = slot
|
|
18
|
-
// §14
|
|
18
|
+
// SESSION.md §6.14:/new 绑定新槽记录存储(baseHistory 空——identity 此时为 null = 待固化;
|
|
19
19
|
// 首次保存/首条追加时补写现场身份)
|
|
20
20
|
bindRecordStore(agent, { slotFile: slotPath(agent.cwd, slot), identity: agent._sessionStart ?? null, baseHistory: [] })
|
|
21
21
|
state.tasks = []
|
|
22
22
|
state.lines = []
|
|
23
|
-
state._linesChars = 0 // TUI-OOM-ROOTCAUSE
|
|
23
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.3):行集清空 → 字符账同步归零
|
|
24
24
|
state.streaming = ""
|
|
25
25
|
render()
|
|
26
26
|
pushLine(`New session started (slot ${slot}; /session to switch back)`, C.dim)
|
package/src/tui/cmd-reindex.mjs
CHANGED
|
@@ -45,7 +45,7 @@ export async function handleReindexCommand(ctx) {
|
|
|
45
45
|
const unlisted = cr?.unlistedExts?.count ? cr.unlistedExts : dr?.unlistedExts
|
|
46
46
|
if (unlisted?.count > 0) {
|
|
47
47
|
const sample = unlisted.exts.map((e) => e.ext).join(" ")
|
|
48
|
-
pushLine(` ${unlisted.count} file(s) with unlisted extension(s) skipped (${sample}) — declare them in .
|
|
48
|
+
pushLine(` ${unlisted.count} file(s) with unlisted extension(s) skipped (${sample}) — declare them in PROJECT-MANIFEST.json (index.codeExtensions / index.docExtensions) to index them`, C.warn)
|
|
49
49
|
}
|
|
50
50
|
pushLine(`[reindex] Done, ${total} entries total. Vectors will be lazily generated on next search.`, C.tool)
|
|
51
51
|
}
|
package/src/tui/cmd-session.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker, sessionDescriptor, loadSlotFile } from "@thincoder/core/session.mjs"
|
|
1
|
+
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker, sessionDescriptor, loadSlotFile, ledgerHealth } from "@thincoder/core/session.mjs"
|
|
2
2
|
import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
|
|
3
3
|
import { ansi, C } from "./ansi.mjs"
|
|
4
4
|
import { restoreLines } from "./startup.mjs"
|
|
@@ -20,7 +20,7 @@ export async function handleRenameCommand(ctx, args) {
|
|
|
20
20
|
pushLine(`Title too long (max 80 chars)`, C.error)
|
|
21
21
|
return
|
|
22
22
|
}
|
|
23
|
-
//
|
|
23
|
+
// renameSlot 契约 { ok, reason }——失败原因对用户可见(F3)
|
|
24
24
|
const r = renameSlot(agent.cwd, slot, title)
|
|
25
25
|
if (!r.ok) {
|
|
26
26
|
const why = {
|
|
@@ -58,7 +58,14 @@ export async function handleSessionCommand(ctx) {
|
|
|
58
58
|
return `${dt.getMonth() + 1}/${dt.getDate()} ${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`
|
|
59
59
|
}
|
|
60
60
|
const truncate = (s, n) => (stringWidth(s) <= n ? s : sliceByWidth(s, Math.max(1, n - 1)) + "…")
|
|
61
|
+
// F-L4(账本可靠批 · SESSION.md §6.25 判据句 4):账本异常 ⇒ 列表头部警示行(正常 ⇒ 一行不加
|
|
62
|
+
// ——负断言);在场条件 = `refused > 0 ∨ scene`(不继承会话计数条件)。文案 = 三端同句(zh 在册字面)。
|
|
63
|
+
const lh = ledgerHealth(agent.cwd)
|
|
64
|
+
const ledgerWarn = (lh.refused > 0 || lh.scene)
|
|
65
|
+
? `会话账本异常(${lh.refused > 0 ? lh.lastReason : "scene"})——打开会话即自动补回${lh.scene ? ";损坏现场档保留 30 天" : ""}`
|
|
66
|
+
: null
|
|
61
67
|
const entries = [
|
|
68
|
+
...(ledgerWarn ? [{ type: "header", text: ledgerWarn }] : []),
|
|
62
69
|
{ type: "header", text: `Sessions (● = active, ↑↓ select, Enter switch, Esc cancel; /rename <title> renames the active one)` },
|
|
63
70
|
...slots.map((s) => {
|
|
64
71
|
// 2026-08-31 会诊:行宽必须按显示宽度截断(原按 UTF-16 length 截 40 = 中文 80 格,
|
|
@@ -66,7 +73,8 @@ export async function handleSessionCommand(ctx) {
|
|
|
66
73
|
// wrap → 残影);title 此前完全不截断(可任意长)也是超宽源,一并宽度截断。
|
|
67
74
|
const rawLabel = s.title || (s.firstMessage ? `"${s.firstMessage}"` : "(empty)")
|
|
68
75
|
const label = truncate(rawLabel, 36)
|
|
69
|
-
|
|
76
|
+
// 计数不可得(`null`)⇒ 「— turns」占位(禁显示 0——真 0 与未知可分;§6.25 判据句 4)
|
|
77
|
+
const turns = s.turnCount == null ? "— turns" : `${s.turnCount} turns`
|
|
70
78
|
const when = shortDate(s.updatedAt)
|
|
71
79
|
const model = s.activeProvider ? ` — ${s.activeProvider}` : ""
|
|
72
80
|
const marker = s.slot === highlightSlot ? " ●" : ""
|
|
@@ -114,11 +122,11 @@ export async function handleSessionCommand(ctx) {
|
|
|
114
122
|
// #41 准翻列:附着动作 = 翻转面自己赋值(`agent.manifest` ← 判据结果——相位行当回合起活)。
|
|
115
123
|
if (manifestAfterSwitch) agent.manifest = manifestAfterSwitch
|
|
116
124
|
// Rebuild from history (lazy) — the display snapshot is deprecated.
|
|
117
|
-
// §14
|
|
125
|
+
// SESSION.md §6.14:描述符 { history(尾窗+±1), total, base }——标签口径 = total(非窗口长度);
|
|
118
126
|
// 未绑定(占用分支——模式 F)回退全量数组。
|
|
119
127
|
const desc = sessionDescriptor(agent, data)
|
|
120
128
|
state.lines = []
|
|
121
|
-
state._linesChars = 0 // TUI-OOM-ROOTCAUSE
|
|
129
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.3):切槽重建行集 → 字符账归零,restoreLines 重新入账
|
|
122
130
|
restoreLines(state, desc)
|
|
123
131
|
state.tasks = agent.tasks ?? []
|
|
124
132
|
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
package/src/tui/cmd-think.mjs
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Interactive loop UX — stays in menu after each action, Esc to exit.
|
|
3
3
|
* ctx: { agent, showPicker, syncProviderField, pushLine, pushLabel } */
|
|
4
4
|
import { ansi, C } from "./ansi.mjs"
|
|
5
|
+
// off 形族单源(`docs/core/design/MODEL-SPECS.md` §16.2 / §16.4)——取形与「有效 off 路径」判据
|
|
6
|
+
// 一律自本档取,本档零副本(叶档:零 import 纯函数)。
|
|
7
|
+
import { thinkOffShape, thinkOffPath } from "@thincoder/core/think-off.mjs"
|
|
5
8
|
|
|
6
9
|
export async function handleThinkCommand(ctx, args = []) {
|
|
7
10
|
const { agent, showPicker, syncProviderField, pushLine, pushLabel } = ctx
|
|
@@ -14,11 +17,26 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
14
17
|
const thinkOnValue = spec.thinkEnabledValue ?? "enabled"
|
|
15
18
|
const isCustomThink = thinkOnValue !== "enabled"
|
|
16
19
|
const effortLevels = spec.reasoningEffortEnum ?? ["high", "max"]
|
|
20
|
+
// 有效 off 路径(判据单源 = `@thincoder/core/think-off.mjs`;§16.4)——无 ⇒ 不提供 / 不宣称 off。
|
|
21
|
+
const offPath = thinkOffPath(spec)
|
|
22
|
+
|
|
23
|
+
/** 环内头与回执共用的状态式(§16.4):无有效 off 路径 ⇒ 恒报 ON——残留 off 标记(`null` /
|
|
24
|
+
* `{type:"disabled"}`)不生效(服务端恒思考 / 形不发字段),「Thinking: OFF」会是假断言。
|
|
25
|
+
* `thinking:null` 是显式 off 标记(NF1 约定)——不得落入"未设置即 ON"的默认显示(评审 #3);
|
|
26
|
+
* 用 `!== null` 而非 `!= null`:undefined(从未设置)须保持既有 ON 显示(qwen3.x 服务端默认开思考)。 */
|
|
27
|
+
const thinkingOn = () => !offPath || cur.thinking?.type === thinkOnValue
|
|
28
|
+
|| (cur.thinking !== null && cur.thinking?.type === undefined && !isCustomThink)
|
|
17
29
|
|
|
18
30
|
const autoThinkEnabled = agent.config?.agent?.autoThink === true
|
|
19
31
|
const sub = args[0]?.toLowerCase()
|
|
20
32
|
if (sub === "on" || sub === "off") {
|
|
21
33
|
if (autoThinkEnabled) { pushLine("Auto-think is ON — manual settings are overridden each turn; turn Auto off first via /think", C.error); return }
|
|
34
|
+
// §16.4:无有效 off 路径 ⇒ 拒绝该动作(不写盘)——服务端恒思考 / 形不发字段,供一个不生效
|
|
35
|
+
// 的入口 + 措辞免责 = 半实现(§16.12 项 3)。`/think on` 与档位面零改。
|
|
36
|
+
if (sub === "off" && !offPath) {
|
|
37
|
+
pushLine("No off path for this model — thinking cannot be turned off here (use /think effort <level>)", C.error)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
22
40
|
await applyThink({ action: sub }, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue)
|
|
23
41
|
pushLabel("❯ Think", ansi.bold + C.tool)
|
|
24
42
|
pushLine(`Thinking: ${sub}`, C.tool)
|
|
@@ -43,16 +61,16 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
43
61
|
let mainIdx = 0
|
|
44
62
|
for (;;) {
|
|
45
63
|
const autoOn = agent.config?.agent?.autoThink === true
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
const thinkingEnabled = cur.thinking?.type === thinkOnValue
|
|
49
|
-
|| (cur.thinking !== null && cur.thinking?.type === undefined && !isCustomThink)
|
|
64
|
+
// 头 / 回执同式(`thinkingOn` 单口径)——状态与回执不得各自成式。
|
|
65
|
+
const thinkingEnabled = thinkingOn()
|
|
50
66
|
|
|
51
67
|
const entries = [
|
|
52
68
|
{ type: "header", text: `Auto: ${autoOn ? "ON" : "OFF"} | Thinking: ${thinkingEnabled ? "ON" : "OFF"} | Effort: ${cur.reasoningEffort || "—"}` },
|
|
53
69
|
{ type: "item", text: `Auto: ${autoOn ? "ON" : "OFF"}`, action: "auto" },
|
|
54
70
|
]
|
|
55
|
-
|
|
71
|
+
// Thinking 开关项:非 effort 族 ∧ 有有效 off 路径才渲染(§16.4——无有效 off 路径族不提供该动作,
|
|
72
|
+
// 入口在位 = 对无效操作的假告知)。
|
|
73
|
+
if (!isEffortOnly && offPath && !autoOn) {
|
|
56
74
|
entries.push({ type: "item", text: `Thinking: ${thinkingEnabled ? "ON" : "OFF"}`, action: thinkingEnabled ? "off" : "on" })
|
|
57
75
|
}
|
|
58
76
|
if (!autoOn) {
|
|
@@ -66,10 +84,6 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
66
84
|
if (!e) return // Esc
|
|
67
85
|
mainIdx = Math.max(0, entries.filter((en) => en.type === "item").indexOf(e))
|
|
68
86
|
|
|
69
|
-
const prevAuto = autoOn
|
|
70
|
-
const prevThinking = thinkingEnabled
|
|
71
|
-
const prevEffort = cur.reasoningEffort
|
|
72
|
-
|
|
73
87
|
await applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue)
|
|
74
88
|
|
|
75
89
|
// Feedback
|
|
@@ -81,11 +95,9 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
81
95
|
} else if (e.action === "effort") {
|
|
82
96
|
pushLine(e.level === "none" ? "Thinking: OFF" : `Reasoning effort: ${e.level}`, C.tool)
|
|
83
97
|
} else {
|
|
84
|
-
//
|
|
85
|
-
// effort 型 off 后回执误报 ON
|
|
86
|
-
|
|
87
|
-
|| (cur.thinking !== null && cur.thinking?.type === undefined && !isCustomThink)
|
|
88
|
-
pushLine(`Thinking: ${nowEnabled ? "ON" : "OFF"}`, C.tool)
|
|
98
|
+
// 与菜单头(`thinkingOn`)同式:`thinking:null`(NF1 显式 off)落 OFF;无有效 off 路径族
|
|
99
|
+
// 恒报 ON(§16.4——残留 off 标记不生效;本式缺守卫时 effort 型 off 后回执误报 ON = 红)。
|
|
100
|
+
pushLine(`Thinking: ${thinkingOn() ? "ON" : "OFF"}`, C.tool)
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
103
|
}
|
|
@@ -93,7 +105,9 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
93
105
|
/** Shared apply logic — extracted from handleThinkCommand for reuse in both fast path and loop.
|
|
94
106
|
* Named export (2026-09-20): the on-default-effort rule (§2.8) is unit-tested by driving this
|
|
95
107
|
* directly with a synthetic spec — behavior otherwise unchanged.
|
|
96
|
-
* Effort 档位 `"none"` = 关思考 ⇒ 入口归一到 off 路径(与菜单 off 项同形——见内注释)。
|
|
108
|
+
* Effort 档位 `"none"` = 关思考 ⇒ 入口归一到 off 路径(与菜单 off 项同形——见内注释)。
|
|
109
|
+
* `spec` 同时供 off 取形(单源 = `@thincoder/core/think-off.mjs`);`isCustomThink` 参数为调用面
|
|
110
|
+
* 既有形状保留(off 支取形已归单源,本档不再自行分族)。 */
|
|
97
111
|
export async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue) {
|
|
98
112
|
const cur = agent.provider
|
|
99
113
|
// `effort` 档位取 `"none"` = **关思考**(非“强度零”):与本面 off 动作同语义 ⇒ 归一到 off 路径
|
|
@@ -124,7 +138,8 @@ export async function applyThink(e, agent, syncProviderField, spec, isEffortOnly
|
|
|
124
138
|
if (isEffortOnly) {
|
|
125
139
|
// Explicit off persists thinking:null (NF1 convention — distinguishable from autoThink's
|
|
126
140
|
// delete/undefined); "on" deletes the marker so enable_thinking maps from effort again.
|
|
127
|
-
|
|
141
|
+
// off 取形同走单源(`thinkOffShape`——effort 族 ⇒ `null`;本支与下方非 effort 支同源零副本)。
|
|
142
|
+
if (!enable) { cur.thinking = thinkOffShape(spec); delete cur.reasoningEffort }
|
|
128
143
|
// "on" 默认 effort 取 spec 枚举的**首个非 "none"** 档(批次档 §1.8-② / 设计 §2.8 D-9):硬编码 "high"
|
|
129
144
|
// 对 qwen3.8-max(enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置);而直接取枚举
|
|
130
145
|
// 首项在新档(首项 "none")等于把思考关掉 ⇒ 跳过 "none";枚举全 "none" 时与无枚举同型 ⇒ 回退 "high"
|
|
@@ -137,7 +152,10 @@ export async function applyThink(e, agent, syncProviderField, spec, isEffortOnly
|
|
|
137
152
|
cur.thinking = { type: thinkOnValue }
|
|
138
153
|
if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
139
154
|
} else {
|
|
140
|
-
|
|
155
|
+
// off 取形 = 单源 `thinkOffShape`(§15.4-2 / §16.2 生产者表第 2 行):非 effort 族 ⇒
|
|
156
|
+
// `{type:"disabled"}`——自定义开值族随 §15.4-2 表改判由 `undefined`(不发字段 = 服务端
|
|
157
|
+
// 默认 = 想)改本形(达载荷层 = 真 off)。
|
|
158
|
+
cur.thinking = thinkOffShape(spec)
|
|
141
159
|
delete cur.reasoningEffort
|
|
142
160
|
}
|
|
143
161
|
await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cmd-timers.mjs — `/timers`:在途 timer 只读列表(最薄)。
|
|
3
|
+
*
|
|
4
|
+
* 机制单源 = `docs/core/design/AGENT-LOOP-ASYNC-POOL.md` §6.30;显示形态单源 = `docs/cli/design/TUI.md`
|
|
5
|
+
* §7.6(逐条 dim 行 `⏰<i> · 剩余 <mm:ss> · <message 首行(截断)>`;零在途 ⇒ 一行 `无在途 timer`)。
|
|
6
|
+
* **取消面不做**(取消 = 控制面——门 / 回执 / 持久化语义;停轮 = Ctrl+C,停机制 = `agent.timerWake` 关)。
|
|
7
|
+
* 取值 = `agent._pendingTimers` 活读(每次调用现算——零缓存);剩余按 `expiresAt - now` 现算。
|
|
8
|
+
*/
|
|
9
|
+
import { C } from "./ansi.mjs"
|
|
10
|
+
import { sanitizeDisplay, sliceByWidth, stringWidth } from "./render.mjs"
|
|
11
|
+
|
|
12
|
+
/** 剩余 mm:ss(到期未送达 ⇒ 夹到 `00:00`——到期态另由状态行警示色承载,见 TUI.md §7.6)。 */
|
|
13
|
+
function remainText(expiresAt, now) {
|
|
14
|
+
const sec = Math.max(0, Math.ceil((expiresAt - now) / 1000))
|
|
15
|
+
return `${String(Math.floor(sec / 60)).padStart(2, "0")}:${String(sec % 60).padStart(2, "0")}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** message 首行(截断——60 显示列 + `…`,同会话标题段口径)。 */
|
|
19
|
+
function headText(message) {
|
|
20
|
+
const head = sanitizeDisplay(String(message ?? "").split("\n")[0])
|
|
21
|
+
return stringWidth(head) > 60 ? sliceByWidth(head, 59) + "…" : head
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function handleTimersCommand(ctx) {
|
|
25
|
+
const { agent, pushLine } = ctx
|
|
26
|
+
const timers = agent?._pendingTimers ?? []
|
|
27
|
+
if (timers.length === 0) {
|
|
28
|
+
pushLine("无在途 timer", C.dim)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
const now = Date.now()
|
|
32
|
+
timers.forEach((t, i) => pushLine(`⏰${i + 1} · 剩余 ${remainText(t.expiresAt, now)} · ${headText(t.message)}`, C.dim))
|
|
33
|
+
}
|
|
@@ -11,7 +11,7 @@ import { shiftFreezeAnchors } from "./subagent-blocks.mjs"
|
|
|
11
11
|
* assistantLabeled: boolean }} */
|
|
12
12
|
export function createConversationWriter({ state, render }) {
|
|
13
13
|
const pushLine = (text, color, kind) => {
|
|
14
|
-
// TUI-OOM-ROOTCAUSE(TUI.md §
|
|
14
|
+
// TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.1/§5.3):行额度单点——行文本过 capLine
|
|
15
15
|
// (LINE_MAX_CHARS——单行巨内容/无换行巨 chunk 的堵口)+ 总量账 + 预算对账。
|
|
16
16
|
const line = { text: capLine(text), color, _kind: kind }
|
|
17
17
|
state.lines.push(line)
|
|
@@ -31,7 +31,7 @@ export function createConversationWriter({ state, render }) {
|
|
|
31
31
|
|
|
32
32
|
/** Message block label: blank line + label line. Breathing space between user/assistant messages */
|
|
33
33
|
const pushLabel = (text, color) => {
|
|
34
|
-
// 行额度同 pushLine(§
|
|
34
|
+
// 行额度同 pushLine(§5.1——恢复行/标签行同口径)
|
|
35
35
|
if (state.lines.length > 0) {
|
|
36
36
|
const blank = { text: "", color: C.dim }
|
|
37
37
|
state.lines.push(blank)
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* display-budget.mjs — TUI 显示层额度(TUI-OOM-ROOTCAUSE 批——TUI.md §
|
|
2
|
+
* display-budget.mjs — TUI 显示层额度(TUI-OOM-ROOTCAUSE 批——TUI-SESSION-VIEW.md §5)。
|
|
3
3
|
*
|
|
4
|
-
* 职责(§
|
|
4
|
+
* 职责(§5.1/§5.2):常量单源(D-TB4)+ 行/载体计长(`lineChars`)+ `state.lines` 总量
|
|
5
5
|
* 对账(`syncLineBudget`)。纯函数本体(`capText` / `appendCappedText`)住
|
|
6
|
-
* `thincoder-core/text-budget.mjs`(零依赖)——与 agent 侧捕获共用(AGENT-LOOP.md §
|
|
6
|
+
* `thincoder-core/text-budget.mjs`(零依赖)——与 agent 侧捕获共用(AGENT-LOOP.md §6.15,D2 单源);
|
|
7
7
|
* 本模块只承载 TUI 面常量与对账。
|
|
8
8
|
*
|
|
9
9
|
* 依赖方向:本模块只引 `./ansi.mjs` + `@thincoder/core/text-budget.mjs`(叶子——不引 subagent 族,
|
|
10
10
|
* 冻结锚点平移由调用方经 `onTrim` 注入,避环)。
|
|
11
11
|
*
|
|
12
|
-
* 计量口径 = UTF-16 码元(`String.length`——TUI.md §
|
|
12
|
+
* 计量口径 = UTF-16 码元(`String.length`——TUI-SESSION-VIEW.md §5.1);行数维(5000 行环 /
|
|
13
13
|
* 500 行块环 / 200 条目环)**原样保留**,字符维为第二维(D-TB5——既有 N5/N6 语义不动)。
|
|
14
14
|
*/
|
|
15
15
|
import { C } from "./ansi.mjs"
|
|
@@ -17,7 +17,7 @@ import { capText, appendCappedText, fillMarker } from "@thincoder/core/text-budg
|
|
|
17
17
|
|
|
18
18
|
export { capText }
|
|
19
19
|
|
|
20
|
-
// ─── 常量(§
|
|
20
|
+
// ─── 常量(§5.1——单源;数值改动一处生效)────────────────────────────────
|
|
21
21
|
|
|
22
22
|
/** 单行(pushLine/pushLabel/恢复行/流式 flush 行)字符额度。 */
|
|
23
23
|
export const LINE_MAX_CHARS = 64_000
|
|
@@ -42,7 +42,7 @@ export const LINES_TRIM_FLOOR = 200
|
|
|
42
42
|
/** 搜索匹配计数上限(超出截断 + 提示行)。 */
|
|
43
43
|
export const SEARCH_MATCH_CAP = 10_000
|
|
44
44
|
|
|
45
|
-
// ─── 标记形态(逐字——§
|
|
45
|
+
// ─── 标记形态(逐字——§5.1 进测试断言)──────────────────────────────────
|
|
46
46
|
|
|
47
47
|
export const LINE_TRUNC_MARKER = "… [line truncated: N chars omitted]"
|
|
48
48
|
export const MIDDLE_TRUNC_MARKER = "… [middle truncated: N chars omitted]"
|
|
@@ -76,7 +76,7 @@ export function appendCapped(prev, add, opts) {
|
|
|
76
76
|
return appendCappedText(prev, add, opts)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** 评审文本单次截断(§
|
|
79
|
+
/** 评审文本单次截断(§5.1:头 32K + 尾 96K 保裁决尾部 + 中段标记——与累积侧同源)。 */
|
|
80
80
|
export function capAdvisorText(text) {
|
|
81
81
|
return capText(text, { max: ADVISOR_TEXT_MAX_CHARS, keepHead: 32_000, keepTail: 96_000, marker: MIDDLE_TRUNC_MARKER })
|
|
82
82
|
}
|
|
@@ -109,7 +109,7 @@ export function capLines(lines, max, marker = MIDDLE_TRUNC_MARKER) {
|
|
|
109
109
|
return out
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// ─── 计长与总量对账(§
|
|
112
|
+
// ─── 计长与总量对账(§5.2 / §5.4)───────────────────────────────────
|
|
113
113
|
|
|
114
114
|
const arrChars = (a) => {
|
|
115
115
|
if (!Array.isArray(a)) return 0
|
|
@@ -166,7 +166,7 @@ export function accountAll(state) {
|
|
|
166
166
|
const RECEIPT_RESERVE_CHARS = 96
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
* `state.lines` 总量对账(§
|
|
169
|
+
* `state.lines` 总量对账(§5.3 落点表末行):超 `LINES_CHAR_BUDGET` → **最小步进**裁头 +
|
|
170
170
|
* 收据行,且不使 `lines.length` 低于 `LINES_TRIM_FLOOR`(触底 = 内层 break——接受超额,
|
|
171
171
|
* 不靠守卫退出:FLOOR+1 且残余超额时守卫仍为真)。每轮裁「至额度内所需最少行数」
|
|
172
172
|
* (need = 超额 + 收据上界预留;上限 = 可裁至保底的行数);`onTrim(state, removedCount)` 由
|
package/src/tui/fold-block.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 最高占屏 60%,底控件必落在视口内,展开永远可逆。
|
|
8
8
|
*
|
|
9
9
|
* 消费方(render-conversation.mjs 六处折叠点):
|
|
10
|
-
* 1. 子agent 活动区块(运行中 / 冻结,
|
|
10
|
+
* 1. 子agent 活动区块(运行中 / 冻结,docs/cli/design/TUI.md §6.8)
|
|
11
11
|
* 2. advisor 评审块(运行中 _advisorBlocks / 冻结 _frozenAdvisor)
|
|
12
12
|
* 3. 长消息折叠(long-N)/ 连续 dim 折叠(fold-N)
|
|
13
13
|
*
|
package/src/tui/index.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import { createTuiState } from "./tui-state.mjs"
|
|
|
42
42
|
import { createInputFace } from "./input-face.mjs"
|
|
43
43
|
import { createConversationWriter } from "./conversation-writer.mjs"
|
|
44
44
|
import { createTurnFace } from "./turn-face.mjs"
|
|
45
|
+
import { createTimerWatch, fireTimerWake } from "./timer-watch.mjs"
|
|
45
46
|
|
|
46
47
|
export { upgradeFailureText, pendingNoticeReady } from "./update-notice.mjs"
|
|
47
48
|
|
|
@@ -141,10 +142,26 @@ export async function startTUI(agent, opts = {}) {
|
|
|
141
142
|
})
|
|
142
143
|
|
|
143
144
|
// 回合面(turn-face.mjs,原址 :366):submit / interaction(权限 + 提问)/ 图片粘贴 / turnCtx / turn
|
|
144
|
-
const { submit, turnCtx, askPermission, askQuestion, pasteClipboardImage } = createTurnFace({
|
|
145
|
+
const { submit, turn, turnCtx, askPermission, askQuestion, pasteClipboardImage } = createTurnFace({
|
|
145
146
|
agent, state, pushLine, pushLabel, render, ensureAssistantLabel, summarize, conversation,
|
|
146
147
|
handleSlash: (t) => handleSlash(t),
|
|
147
148
|
})
|
|
149
|
+
// timer 空闲自唤醒闩(AGENT-LOOP-ASYNC-POOL.md §6.30.5 载体②):装配点 = 回合面之后(`turn` 已就绪);
|
|
150
|
+
// 触发 ⇒ 到期批送达 + 开 timer 轮(顶层链——`{ autoTurn: true, timerTurn: true }`);
|
|
151
|
+
// 开关关(`agent.timerWake` false)⇒ 闩惰性(sync 内判——零注册)。
|
|
152
|
+
turnCtx.timerWatch = createTimerWatch({
|
|
153
|
+
agent,
|
|
154
|
+
// 火面异常兜底(先例 = key-handler `submit().catch(...)`):无人值守的空闲轮报错不得走
|
|
155
|
+
// 进程级 `unhandledRejection`(闩已自撤)——落错误行 + 重同步(在途项仍在 ⇒ 链尾/此处再武装)。
|
|
156
|
+
onFire: () => fireTimerWake(turnCtx, { runTurn: turn }).catch((e) => {
|
|
157
|
+
pushLine(`[error] ${e?.message ?? e}`, C.error)
|
|
158
|
+
turnCtx.timerWatch?.sync()
|
|
159
|
+
}),
|
|
160
|
+
})
|
|
161
|
+
// #448①「关闭后补评估」(KD-6):用户自发模态(picker ∕ wizard)全部退场 ⇒ 闩重武装——模态期被
|
|
162
|
+
// 抑制的到期件即达(无在途 / 开关关 ⇒ `sync()` 自判零注册——幂等)。两关闭点(picker 栈空 ∕
|
|
163
|
+
// wizard 退场)同注本回调;抑制本体 = `timer-watch.mjs` `fireTimerWake` 模态门(同谓词 `modalOpen`)。
|
|
164
|
+
const reevalTimerWake = () => { turnCtx.timerWatch?.sync() }
|
|
148
165
|
|
|
149
166
|
// ---------------------------------------------------------- Slash Commands
|
|
150
167
|
|
|
@@ -154,12 +171,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
154
171
|
// Model picker + generic picker: implemented in pickers.mjs
|
|
155
172
|
const { closePicker, showPicker, popPicker, renderPickerLines, openModelPicker, selectModel, setProviderKey, pickModelForSlot, confirmDelete } = createPickers({
|
|
156
173
|
agent, state, render, ansi, C, pushLine, pushLabel, persistRaw, askQuestion, maskKey,
|
|
174
|
+
onModalClose: reevalTimerWake, // #448①「关闭后补评估」(picker 栈空点)
|
|
157
175
|
})
|
|
158
176
|
|
|
159
177
|
// First-launch config wizard: implemented in wizard.mjs, closure deps passed via ctx
|
|
160
178
|
const { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, wizardProviderItems } = createWizard({
|
|
161
179
|
agent, state, pushLine, pushLabel, render, persistRaw,
|
|
162
180
|
openModelPicker: () => openModelPicker(),
|
|
181
|
+
onModalClose: reevalTimerWake, // #448①「关闭后补评估」(wizard 退场点)
|
|
163
182
|
})
|
|
164
183
|
|
|
165
184
|
// /distill: impl in distill-cmd.mjs, ctx-passed
|
|
@@ -208,10 +227,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
208
227
|
showStartup({ agent, state, opts, pushLine, pushLabel, render, startWizard })
|
|
209
228
|
// LEDGER-SURFACE(§2.30.3.4):台账可见面——首扫(setImmediate)+ 周期;dispose 挂进程退出(:278 cleanup 先例)
|
|
210
229
|
const ledgerSurface = startLedgerSurface({ state, agent, pushLine, render })
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
process.on("exit", () => { void Promise.resolve(ledgerSurface).then((s) => s?.dispose?.()) })
|
|
230
|
+
// 端壳契约 = **恒同步返回 `{ dispose }`**(K-LX3 归核后与核形一致——核句柄异步就绪后桥接;
|
|
231
|
+
// 载入失败 ⇒ 空句柄)⇒ 退钩同步 `.dispose()`(零 TypeError)。
|
|
232
|
+
process.on("exit", () => ledgerSurface.dispose())
|
|
215
233
|
backgroundIndex({ agent, state, render })
|
|
216
234
|
|
|
217
235
|
// Check for updates (non-blocking, after startup screen)——实现 update-notice.mjs
|
package/src/tui/interaction.mjs
CHANGED
|
@@ -63,12 +63,14 @@ export function createInteraction(ctx) {
|
|
|
63
63
|
|
|
64
64
|
function askPermission(name, args) {
|
|
65
65
|
// auto mode: fully authorized, no more prompts
|
|
66
|
-
|
|
66
|
+
// TURN-CAP-CONTINUE.md §1 #7 / D-TC15(2026-09-26):`continue` 不在 AUTO 授权面——续期恒须人答;
|
|
67
|
+
// 无人值守档(depth-0 digest / AUTO)撞帽即收口(静默自续 = F8 禁止)。
|
|
68
|
+
if (agent.autoApprove && name !== "continue") {
|
|
67
69
|
const argSummary = summarize(args)
|
|
68
70
|
pushLine(` [auto] ${name}${argSummary ? ` ${argSummary}` : ""}`, C.warn)
|
|
69
71
|
return Promise.resolve(true)
|
|
70
72
|
}
|
|
71
|
-
// 闸语义 / 请示流程 = 核单源(P2 批 §2.20);本端只供展示面——卡片预览经 `io.ask` 缝
|
|
73
|
+
// 闸语义 / 请示流程 = 核单源(P2 批 docs/batches/2026-09-20-mechanism-parity-batch.md §2.20);本端只供展示面——卡片预览经 `io.ask` 缝
|
|
72
74
|
// 注入(permissionPreview 渲染在输入框上方,与 "Allow?" 提示同帧)。
|
|
73
75
|
return coreAskPermission(name, args, {
|
|
74
76
|
ask: ({ name: n, toolArgs }) => new Promise((resolve) => {
|
|
@@ -80,7 +82,7 @@ export function createInteraction(ctx) {
|
|
|
80
82
|
})
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
/** Batch permission ask (
|
|
85
|
+
/** Batch permission ask (D-B1): one merged prompt covering N
|
|
84
86
|
* non-readonly tools from the same toolCalls array — approve all / one by one /
|
|
85
87
|
* deny. Verdicts resolve as "approveAll" | "oneByOne" | "deny". */
|
|
86
88
|
const BATCH_PREVIEW_LINES_PER_TOOL = 8
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { C } from "./ansi.mjs"
|
|
2
|
+
import { QUEUED_MAX_ITEMS } from "./queued-merge.mjs"
|
|
2
3
|
|
|
3
4
|
/** busy 门禁(2026-09-22 structure-debt §2.1 分族 4 · #226):processing(含 digest)期
|
|
4
|
-
* Enter
|
|
5
|
-
*
|
|
5
|
+
* Enter 队列受理(容量 QUEUED_MAX_ITEMS)+ 吞面四(模态 / 斜杠 / 空 / 队满)+ 满队提示 +
|
|
6
|
+
* 挂起面唤醒,Tab 恒吞;块体自 key-handler.mjs createKeyHandler 逐字搬移(仅去缩进;函数体自持原块入口判定)。
|
|
6
7
|
* 路由 = 分派器 createKeyHandler(守卫 = busy 块内返回条件:tab / 无 meta 的 Enter、回车)。 */
|
|
7
8
|
export function handleBusyEnter(str, key, state, ctx) {
|
|
8
9
|
const { pushLine, render } = ctx
|
|
9
10
|
if (state.processing) {
|
|
10
|
-
// busy 门禁(INPUT-LOCK C'——2026-09-09 + F16 busy
|
|
11
|
-
// 扩面 2026-09-22):processing(含 digest
|
|
12
|
-
// 提交 =
|
|
13
|
-
// ⇒ 清框入 pendingInput
|
|
14
|
-
//
|
|
11
|
+
// busy 门禁(INPUT-LOCK C'——2026-09-09 + F16 busy 队列受理——2026-09-21 · busy-extend
|
|
12
|
+
// 扩面 2026-09-22 · queue-visible 多槽 + 容量 8——2026-09-24):processing(含 digest——单一
|
|
13
|
+
// 判据)输入不禁——打字照常回显;Enter 提交 = 入队(放行判据五条 = TUI-INPUT-BOX.md §4.1:
|
|
14
|
+
// 非模态 · 非斜杠 · 非空 · 未满队 ⇒ 清框入 pendingInput——步边界 pickup / 回合尾 drain /
|
|
15
|
+
// 挂起 driver 送达;挂起两态不再是吞面);
|
|
16
|
+
// 吞面四 = 模态 / 斜杠 / 空 / 队满(吞 + 提示,文本保留;斜杠同禁发——白名单已删);
|
|
15
17
|
// 多行换行(meta/enter——编辑)照常放行;Tab 仍吞。**第 31 批**:↑↓ 分流见下方三规则块。
|
|
16
18
|
if (key.name === "tab") return
|
|
17
19
|
if ((key.name === "return" && !key.meta) || (str === "\r" && !key.meta)) {
|
|
@@ -23,18 +25,21 @@ export function handleBusyEnter(str, key, state, ctx) {
|
|
|
23
25
|
pushLine(`[主会话处理中 —— 消息未发送(回合结束后请重按 Enter)]`, C.warn)
|
|
24
26
|
render(); return
|
|
25
27
|
}
|
|
26
|
-
// 条件 5
|
|
27
|
-
if (state.pendingInput.length
|
|
28
|
-
pushLine(`[主会话处理中 ——
|
|
28
|
+
// 条件 5:满队(第 9 条)= 拒绝 + 提示 + 文本保留(容量 8——多槽裁定连带;与挂起态满队分支同构)
|
|
29
|
+
if (state.pendingInput.length >= QUEUED_MAX_ITEMS) {
|
|
30
|
+
pushLine(`[主会话处理中 —— 已排队 ${QUEUED_MAX_ITEMS} 条消息,请等其处理完成后再发送]`, C.warn)
|
|
29
31
|
render(); return
|
|
30
32
|
}
|
|
31
|
-
//
|
|
33
|
+
// 入队(判据全满足):清框 + history 收录 + 草稿复位 + 队列填充(§4.1 执行序)
|
|
32
34
|
state.input = []; state.cursor = 0
|
|
33
35
|
state.history.push(text)
|
|
34
36
|
state.historyIndex = -1
|
|
35
37
|
state._draft = null
|
|
36
38
|
state.pendingInput.push(text)
|
|
37
|
-
//
|
|
39
|
+
// F4「新提交消息 → 恢复跟随」(§7.5 跟随——排队提交同列;与 turn-face.mjs submit 同款两行)
|
|
40
|
+
state.scroll = 0
|
|
41
|
+
state._followTail = true
|
|
42
|
+
// 挂起面入队同款唤醒(§4.1)——busy 期该槽恒 null ⇒ 零动作;普通 busy 面零唤醒
|
|
38
43
|
if (state.suspended || state._suspPending) state._suspWake?.()
|
|
39
44
|
render(); return
|
|
40
45
|
}
|