thincoder 0.12.59 → 0.12.60

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.
Files changed (127) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +2 -2
  3. package/bin/thincoder.mjs +80 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +7 -4
  6. package/src/advisor/messages.mjs +24 -4
  7. package/src/advisor/run.mjs +35 -33
  8. package/src/advisor.mjs +25 -6
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +102 -19
  11. package/src/agent/helpers.mjs +36 -0
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +18 -2
  16. package/src/agent/spawn-child.mjs +29 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +110 -108
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +121 -102
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +24 -29
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/read-history.mjs +155 -31
  25. package/src/agent-tools/recent-changes.mjs +2 -1
  26. package/src/agent-tools/settings.mjs +7 -17
  27. package/src/agent-tools/subagent-actions.mjs +168 -130
  28. package/src/agent-tools/subagent-async.mjs +129 -174
  29. package/src/agent-tools/subagent-panel.mjs +153 -0
  30. package/src/agent-tools/subagent-run.mjs +202 -0
  31. package/src/agent-tools/subagent-scheduler.mjs +45 -21
  32. package/src/agent-tools/subagent-spawn.mjs +406 -0
  33. package/src/agent-tools/subagent.mjs +107 -555
  34. package/src/agent-tools/verify.mjs +118 -270
  35. package/src/agent.mjs +57 -190
  36. package/src/cli/distill-command.mjs +10 -4
  37. package/src/cli/make-agent.mjs +3 -1
  38. package/src/cli/memory-command.mjs +2 -1
  39. package/src/cli/permission.mjs +2 -2
  40. package/src/cli/setup-wizard.mjs +17 -12
  41. package/src/config.mjs +56 -8
  42. package/src/context.mjs +5 -147
  43. package/src/crash-reports.mjs +123 -0
  44. package/src/distill.mjs +11 -11
  45. package/src/explore-distill.mjs +155 -0
  46. package/src/memory/code-sync.mjs +2 -1
  47. package/src/memory/core.mjs +6 -193
  48. package/src/memory/delete.mjs +234 -0
  49. package/src/memory/docs.mjs +58 -48
  50. package/src/memory.mjs +3 -1
  51. package/src/peer-domains.mjs +265 -0
  52. package/src/peer-instances.mjs +231 -0
  53. package/src/prompt-overlays.mjs +25 -0
  54. package/src/prompts/advisor-design.md +9 -76
  55. package/src/prompts/advisor-round1.md +9 -68
  56. package/src/prompts/advisor-round2.md +7 -54
  57. package/src/prompts/advisor-round3.md +7 -54
  58. package/src/prompts/coder.md +7 -50
  59. package/src/prompts/consult-base.md +4 -24
  60. package/src/prompts/discipline.md +26 -44
  61. package/src/prompts/eng-coder.md +7 -32
  62. package/src/prompts/engineering-sub.md +3 -23
  63. package/src/prompts/engineering.md +53 -306
  64. package/src/prompts/explore.md +3 -12
  65. package/src/prompts/main.md +10 -32
  66. package/src/prompts/methodology-template.md +28 -48
  67. package/src/prompts/plan.md +2 -9
  68. package/src/prompts/system.md +16 -35
  69. package/src/provider/core.mjs +6 -67
  70. package/src/provider/errors.mjs +76 -0
  71. package/src/provider/retry.mjs +8 -45
  72. package/src/session-gc.mjs +214 -0
  73. package/src/session-guard.mjs +47 -0
  74. package/src/session-rename.mjs +38 -0
  75. package/src/session-slots.mjs +181 -58
  76. package/src/session.mjs +48 -89
  77. package/src/token-ttl.mjs +273 -0
  78. package/src/tools/checklist-sync.mjs +181 -0
  79. package/src/tools/checklist.mjs +52 -39
  80. package/src/tools/edit-batch.mjs +109 -10
  81. package/src/tools/edit-diff.mjs +110 -27
  82. package/src/tools/edit.md +17 -12
  83. package/src/tools/execute.mjs +31 -4
  84. package/src/tools/file.mjs +11 -6
  85. package/src/tools/git.mjs +14 -6
  86. package/src/tools/glob-dialect.mjs +130 -0
  87. package/src/tools/glob.md +3 -3
  88. package/src/tools/grep.md +1 -1
  89. package/src/tools/index.mjs +5 -6
  90. package/src/tools/ops.mjs +175 -3
  91. package/src/tools/patch.mjs +3 -3
  92. package/src/tools/question.md +3 -0
  93. package/src/tools/read.md +0 -1
  94. package/src/tools/shared.mjs +14 -13
  95. package/src/tools/system.mjs +44 -9
  96. package/src/tools/wait_for.md +22 -0
  97. package/src/tui/agent-turn.mjs +17 -228
  98. package/src/tui/cmd-config.mjs +48 -7
  99. package/src/tui/cmd-eng.mjs +20 -16
  100. package/src/tui/cmd-mcp.mjs +8 -2
  101. package/src/tui/cmd-new.mjs +3 -2
  102. package/src/tui/cmd-session.mjs +19 -4
  103. package/src/tui/cmd-think.mjs +10 -10
  104. package/src/tui/cmd-upgrade.mjs +19 -4
  105. package/src/tui/config-helpers.mjs +28 -16
  106. package/src/tui/distill-cmd.mjs +1 -1
  107. package/src/tui/index.mjs +3 -2
  108. package/src/tui/interaction.mjs +3 -3
  109. package/src/tui/mouse.mjs +7 -1
  110. package/src/tui/pickers.mjs +40 -22
  111. package/src/tui/render-segments.mjs +27 -10
  112. package/src/tui/startup.mjs +4 -0
  113. package/src/tui/subagent-blocks.mjs +95 -263
  114. package/src/tui/subagent-children.mjs +176 -0
  115. package/src/tui/subagent-freeze.mjs +172 -0
  116. package/src/tui/subagent-panel.mjs +61 -23
  117. package/src/tui/suspension-drive.mjs +351 -0
  118. package/src/tui/tool-args.mjs +3 -3
  119. package/src/tui/tool-display.mjs +142 -0
  120. package/src/tui/tool-events.mjs +37 -173
  121. package/src/tui/tui-lifecycle.mjs +29 -0
  122. package/src/tui/update-notice.mjs +4 -0
  123. package/src/tui/wizard.mjs +12 -6
  124. package/src/tools/pdf-parse-text.mjs +0 -497
  125. package/src/tools/pdf-parse-xref.mjs +0 -499
  126. package/src/tools/pdf.mjs +0 -155
  127. package/src/tools/read_pdf.md +0 -21
@@ -9,8 +9,9 @@ export async function handleNewCommand(ctx) {
9
9
  const doNewSession = () => {
10
10
  const slot = newSession(agent.cwd)
11
11
  // 2026-08-31 会诊 F3:resetSessionState 清全量会话态(_fullHistory/title/_sessionStart/
12
- // _engDesignToken/压缩与验证计数等)——原实现只清 agent.history,新会话首次落盘把旧
13
- // 会话完整人类线 + 旧标题写进新 slot(实锤 .19/.3 双副本)。_slot 更新为粘性新槽位。
12
+ // 多槽设计凭证(_engDesignTokens——单值镜像 _engDesignToken 已退役 D3)/压缩与验证计数
13
+ // 等)——原实现只清 agent.history,新会话首次落盘把旧会话完整人类线 + 旧标题写进新
14
+ // slot(实锤 .19/.3 双副本)。_slot 更新为粘性新槽位。
14
15
  resetSessionState(agent)
15
16
  agent._slot = slot
16
17
  state.tasks = []
@@ -1,4 +1,4 @@
1
- import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy } from "../session.mjs"
1
+ import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker } from "../session.mjs"
2
2
  import { ansi, C } from "./ansi.mjs"
3
3
  import { restoreLines } from "./startup.mjs"
4
4
  import { stringWidth, sliceByWidth } from "./render.mjs"
@@ -19,8 +19,16 @@ export async function handleRenameCommand(ctx, args) {
19
19
  pushLine(`Title too long (max 80 chars)`, C.error)
20
20
  return
21
21
  }
22
- if (!renameSlot(agent.cwd, slot, title)) {
23
- pushLine(`Rename failed active session (slot ${slot}) not found`, C.error)
22
+ // §12.2.5:renameSlot 契约 { ok, reason }——失败原因对用户可见(F3)
23
+ const r = renameSlot(agent.cwd, slot, title)
24
+ if (!r.ok) {
25
+ const why = {
26
+ "file-missing": `active session (slot ${slot}) not found`,
27
+ "parse-failure": `session file (slot ${slot}) is corrupted`,
28
+ "mtime-conflict": "session changed on disk concurrently — retry",
29
+ "invalid-slot": `invalid slot ${slot}`,
30
+ }[r.reason] ?? r.reason
31
+ pushLine(`Rename failed — ${why}`, C.error)
24
32
  return
25
33
  }
26
34
  agent.title = title
@@ -37,6 +45,13 @@ export async function handleSessionCommand(ctx) {
37
45
  pushLine("No sessions (use /new to start a new session)", C.dim)
38
46
  return
39
47
  }
48
+ // 2026-09-05 §10 D-5:本端高亮按端记录(● = 记录槽 ∈ 列表 ? 记录槽 : manifest active
49
+ // 回退——含"记录槽已被对端删除"的守卫);listSlots 的 manifest active 语义不变
50
+ // (ACP session/list 零变化——manifest active 保留为列表回退高亮)
51
+ const rec = readEndMarker(agent.cwd)
52
+ const highlightSlot = (rec?.slot != null && slots.some((s) => s.slot === rec.slot))
53
+ ? rec.slot
54
+ : (slots.find((s) => s.isActive)?.slot ?? null)
40
55
  const shortDate = (d) => {
41
56
  const dt = new Date(d)
42
57
  return `${dt.getMonth() + 1}/${dt.getDate()} ${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`
@@ -53,7 +68,7 @@ export async function handleSessionCommand(ctx) {
53
68
  const turns = s.turnCount > 0 ? `${s.turnCount} turns` : "0 turns"
54
69
  const when = shortDate(s.updatedAt)
55
70
  const model = s.activeProvider ? ` — ${s.activeProvider}` : ""
56
- const marker = s.isActive ? " ●" : ""
71
+ const marker = s.slot === highlightSlot ? " ●" : ""
57
72
  return {
58
73
  type: "item",
59
74
  text: `Slot ${s.slot} │ ${turns} │ ${when} │ ${label}${model}${marker}`,
@@ -97,16 +97,16 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
97
97
  // 开 auto = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null
98
98
  // 会让 auto 每轮写入的 reasoning_effort 与 enable_thinking:false 矛盾同发(F2 违约)。
99
99
  // 仅清 null:thinking-type 模型的 {type:"disabled"} 不在评审 #1 范围,保持既有语义。
100
- if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
100
+ if (cur.thinking === null) { delete cur.thinking; await syncProviderField(agent.activeProvider, "thinking", undefined) }
101
101
  delete cur.reasoningEffort
102
- await syncProviderField("reasoningEffort", undefined)
102
+ await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
103
103
  }
104
104
  } else if (e.action === "effort") {
105
105
  // 选档位 = 要思考:清显式 off 标记(thinking:null,NF1 约定;交付评审 #1)——残留 null 会让
106
106
  // enable_thinking:false 与 reasoning_effort 矛盾同发(F2 违约)。仅清 null(同上注释)。
107
- if (cur.thinking === null) { delete cur.thinking; await syncProviderField("thinking", undefined) }
107
+ if (cur.thinking === null) { delete cur.thinking; await syncProviderField(agent.activeProvider, "thinking", undefined) }
108
108
  cur.reasoningEffort = e.level
109
- await syncProviderField("reasoningEffort", e.level)
109
+ await syncProviderField(agent.activeProvider, "reasoningEffort", e.level)
110
110
  } else {
111
111
  const enable = e.action === "on"
112
112
  if (isEffortOnly) {
@@ -116,9 +116,9 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
116
116
  // "on" 默认 effort 取 spec 枚举首值(交付评审 #2):硬编码 "high" 对 qwen3.8-max
117
117
  // (enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置)
118
118
  else { delete cur.thinking; if (!cur.reasoningEffort) cur.reasoningEffort = spec.reasoningEffortEnum?.[0] ?? "high" }
119
- await syncProviderField("thinking", cur.thinking)
120
- if (!enable) await syncProviderField("reasoningEffort", undefined)
121
- else await syncProviderField("reasoningEffort", cur.reasoningEffort)
119
+ await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
120
+ if (!enable) await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
121
+ else await syncProviderField(agent.activeProvider, "reasoningEffort", cur.reasoningEffort)
122
122
  } else {
123
123
  if (enable) {
124
124
  cur.thinking = { type: thinkOnValue }
@@ -127,11 +127,11 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
127
127
  cur.thinking = isCustomThink ? undefined : { type: "disabled" }
128
128
  delete cur.reasoningEffort
129
129
  }
130
- await syncProviderField("thinking", cur.thinking)
130
+ await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
131
131
  if (enable) {
132
- await syncProviderField("reasoningEffort", cur.reasoningEffort)
132
+ await syncProviderField(agent.activeProvider, "reasoningEffort", cur.reasoningEffort)
133
133
  } else {
134
- await syncProviderField("reasoningEffort", undefined)
134
+ await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
135
135
  }
136
136
  }
137
137
  }
@@ -1,9 +1,10 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
+ import { upgradeFailureText } from "./update-notice.mjs"
2
3
 
3
4
  /** /upgrade command: check for updates and optionally upgrade.
4
5
  * ctx: { agent, pushLine, pushLabel, showPicker } */
5
6
  export async function handleUpgradeCommand(ctx) {
6
- const { pushLine, pushLabel, showPicker } = ctx
7
+ const { pushLine, pushLabel, showPicker, render } = ctx
7
8
  const { checkForUpdate } = await import("../upgrade.mjs")
8
9
  const { readFileSync } = await import("node:fs")
9
10
 
@@ -31,13 +32,27 @@ export async function handleUpgradeCommand(ctx) {
31
32
  pushLine(`Upgrading to ${result.latest}...`, C.tool)
32
33
  const { exec } = await import("node:child_process")
33
34
  const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
34
- cp.stdout?.on("data", () => {})
35
- cp.stderr?.on("data", () => {})
35
+ let stdout = ""
36
+ let done = false
37
+ cp.stdout?.on("data", (d) => { stdout += d })
38
+ cp.stderr?.on("data", (d) => { stdout += d })
39
+ // 2026-09-05(advisor 🟡#5/#7):异步回调必须显式 render(pushLine 只写 lines 不绘制——
40
+ // handleSlash 返回后的统一 render 先于 exec 完成,结论行要等下次输入才可见);失败文案
41
+ // 复用 upgradeFailureText(附 npm 输出尾 ≤3 行——与 update-notice 后台路径同诊断能力)
42
+ cp.on("error", (e) => {
43
+ if (done) return
44
+ done = true
45
+ pushLine(`✗ Upgrade failed (${e.message}). Run \`thincoder upgrade\` manually.`, C.error)
46
+ render?.()
47
+ })
36
48
  cp.on("close", (code) => {
49
+ if (done) return
50
+ done = true
37
51
  if (code === 0) {
38
52
  pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
39
53
  } else {
40
- pushLine(`✗ Upgrade failed (exit ${code}). Run \`thincoder upgrade\` manually.`, C.error)
54
+ pushLine(upgradeFailureText(code, stdout), C.error)
41
55
  }
56
+ render?.()
42
57
  })
43
58
  }
@@ -1,26 +1,38 @@
1
- import { existsSync, readFileSync } from "node:fs"
2
-
3
1
  /** persistRaw / syncProviderField / maskKey: config read/write helpers.
4
2
  * Extracted from index.mjs for shared use by slash-commands, wizard, and pickers.
5
- * createConfigHelpers(agent) returns { persistRaw, syncProviderField, maskKey } */
6
- export function createConfigHelpers(agent) {
7
- /** Read config.json mutate persist to disk */
3
+ * createConfigHelpers(agent) returns { persistRaw, syncProviderField, maskKey }
4
+ * R10 F5(D-F5b):persistRaw 全链(磁盘新鲜读→mutate→写)收口进 config.mjs
5
+ * writeConfigAtomic——mtime 门控 + .bak + 冲突放弃(见 MULTI-INSTANCE-COLLAB.md §2a.2)。
6
+ * createConfigHelpers(agent, opts):opts.configPath 为测试注入缝(默认 configPath——
7
+ * 生产 index.mjs 单参调用零变化)。 */
8
+ export function createConfigHelpers(agent, opts = {}) {
9
+ const cfgPath = opts.configPath ?? null // null → 默认 configPath(动态 import 解)
10
+
11
+ /** 磁盘新鲜读 → mutate(单操作)→ mtime 门控写;冲突时 throw(提示语见下——TUI
12
+ * slash 分发层统一 [error] 行展示;调用方若自带 try/catch 展示 Save failed 同文案)。 */
8
13
  async function persistRaw(mutate) {
9
- const { saveConfig, configPath } = await import("../config.mjs")
10
- const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
11
- mutate(raw)
12
- saveConfig(raw)
14
+ const { writeConfigAtomic, configPath } = await import("../config.mjs")
15
+ const r = writeConfigAtomic(cfgPath ?? configPath, mutate)
16
+ if (!r.ok) throw new Error("config changed on disk concurrently — retry")
13
17
  }
14
18
 
15
- /** Sync a field of the current provider to the providers array and persist to disk */
16
- async function syncProviderField(field, value) {
17
- const target = agent.providers.find((p) => p.name === agent.activeProvider)
18
- if (!target) return
19
- if (value === undefined) delete target[field]
20
- else target[field] = value
19
+ /** Sync a field of a provider (by name) to the providers array and persist to disk.
20
+ * D-F5a(#1):磁盘 fresh raw.providers 上单字段补丁——不取 agent.providers 整数组
21
+ * (长跑进程的内存 providers 快照不得整体写回——会抹掉对端对磁盘其他改动的保留)。
22
+ * 内存 agent.providers 目标项同步镜像(旧行为:改内存 + 整节写盘——镜像保留内存
23
+ * 一致性,磁盘写只剩单字段)。value === undefined 删除该字段。 */
24
+ async function syncProviderField(name, field, value) {
25
+ const mem = agent.providers?.find((p) => p?.name === name)
26
+ if (!mem) return // 内存无此 provider(旧语义 early return——不产生空写)
21
27
  await persistRaw((raw) => {
22
- raw.providers = agent.providers
28
+ raw.providers ??= []
29
+ const target = raw.providers.find((p) => p?.name === name)
30
+ if (!target) return // 磁盘目标已被对端删除 → 只做内存镜像,不落盘
31
+ if (value === undefined) delete target[field]
32
+ else target[field] = value
23
33
  })
34
+ if (value === undefined) delete mem[field]
35
+ else mem[field] = value
24
36
  }
25
37
 
26
38
  /** Mask API key for display */
@@ -22,7 +22,7 @@ export async function runDistill(ctx) {
22
22
  }
23
23
  let saved = 0
24
24
  for (const c of candidates) {
25
- pushLine(`── Candidate [${c.type}] ${c.title} (scope: ${c.scope ?? "personal"})`, C.warn)
25
+ pushLine(`── Candidate [${c.type}] ${c.title} (layer: ${c.layer ?? "personal"})`, C.warn)
26
26
  for (const line of c.content.split("\n").slice(0, 6)) pushLine(` ${line}`, C.dim)
27
27
  if (c.type === "rule") pushLine(" (rule type — consider writing manually; press y to extract)", C.warn)
28
28
  const accept = await askPermission("distill-save", { title: c.title })
package/src/tui/index.mjs CHANGED
@@ -27,7 +27,7 @@ import { createRenderLoop } from "./render-loop.mjs"
27
27
  import { makeDimsState } from "./dims.mjs"
28
28
  import { SLASH_COMMANDS, SLASH_ALIASES, createSlashCommands } from "./slash-commands.mjs"
29
29
  import { createWizard } from "./wizard.mjs"
30
- import { writeStartupSequence, createExitCleanup } from "./tui-lifecycle.mjs"
30
+ import { writeStartupSequence, setTuiActive, createExitCleanup } from "./tui-lifecycle.mjs"
31
31
  import { createPickers } from "./pickers.mjs"
32
32
  import { runDistill as runDistillImpl } from "./distill-cmd.mjs"
33
33
  import { createInteraction } from "./interaction.mjs"
@@ -106,7 +106,7 @@ export async function startTUI(agent, opts = {}) {
106
106
  reasoning: "", // thinking stream buffer (dimmed display)
107
107
  completion: null, // Tab completion state { candidates, index }
108
108
 
109
- subTasks: {}, // sub-agent activity blocks (§7.2 D4): { "coder#1": { key, role, model, started, done, doneAt, blocks: [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError, dropped, blockEpoch, awaitingDigest(§17 挂起中间态), _freezeAt(冻结锚点) } } — rendered as collapsible in-conversation blocks; persists across turns (blocks are the child activity's ONLY carrier — child tool calls never enter the parent history); bounded by the N2 500-line per-child ring buffer
109
+ subTasks: {}, // sub-agent activity blocks (§7.2 D4): { "coder#1": { key, role, model, started, done, doneAt, blocks: [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError, dropped, blockEpoch, awaitingDigest(§17 挂起中间态), _freezeAt(冻结锚点), stopped, children: [](§27 R23 嵌套子代理子块载体——subagent-children.mjs) } } — rendered as collapsible in-conversation blocks; persists across turns (blocks are the child activity's ONLY carrier — child tool calls never enter the parent history); bounded by the N2 500-line per-child ring buffer(R23:子块计入外层同配额——树级 trim)
110
110
  currentTool: null, // currently executing tool name (shown in status bar)
111
111
  processingStarted: 0, // current turn start time (status bar timer)
112
112
  status: "Ready",
@@ -147,6 +147,7 @@ export async function startTUI(agent, opts = {}) {
147
147
  // modifyOtherKeys lvl 2 (\x1b[>4;2m): Shift+Enter → \x1b[27;2;13~ (mintty / Git Bash)
148
148
  // translateShiftEnter (stdin layer) maps both to \x1b\r → meta+return → multiline branch.
149
149
  writeStartupSequence()
150
+ setTuiActive(true) // R25(F-R25a):终端接管完成——置 TUI 活动态(崩溃钩子恢复判定源)
150
151
 
151
152
  const utf8Decoder = new TextDecoder("utf-8", { fatal: false })
152
153
 
@@ -47,9 +47,9 @@ export function createInteraction(ctx) {
47
47
  const action = String(args.action ?? "")
48
48
  if (action === "put") return [`[${args.type ?? ""}] ${args.title ?? ""}`, ...cap(args.content ?? "", 500).split("\n")]
49
49
  if (action === "delete") return args.id
50
- ? [`delete single: ${args.id} (scope ${args.scope})`]
51
- : [`batch delete scope=${args.scope} type=${args.type ?? ""} keyword=${args.keyword ?? ""}`, `confirm=${args.confirm}`]
52
- if (action === "clear") return [`clear personal memory`, `scope=${args.scope} confirm=${args.confirm}`]
50
+ ? [`delete single: ${args.id}${args.layer ? ` (layer ${args.layer})` : ""}`]
51
+ : [`batch delete layer=${args.layer ?? ""} type=${args.type ?? ""} keyword=${args.keyword ?? ""}`, `confirm=${args.confirm}`]
52
+ if (action === "clear") return [`clear personal memory`, `layer=${args.layer ?? ""} confirm=${args.confirm}`]
53
53
  return [cap(summarize(args), 300)]
54
54
  }
55
55
  return [cap(summarize(args), 300)]
package/src/tui/mouse.mjs CHANGED
@@ -18,6 +18,7 @@ import { computeLayout, subagentLineIndex } from "./layout.mjs"
18
18
  import { buildConvLines, convViewport } from "./render-conversation.mjs"
19
19
  import { toggleFoldBlock, scrollFoldBlock, foldScrollOffset } from "./fold-block.mjs"
20
20
  import { cancelAsyncSubagent } from "../agent-tools/subagent-async.mjs"
21
+ import { cancelAsyncAdvisor } from "../agent-tools/advisor-async.mjs"
21
22
  import { C } from "./ansi.mjs"
22
23
 
23
24
  /** 2026-08-31 滚轮事件分派(用户需求"展开块能滚动阅读全文"):坐标命中展开块内容行 →
@@ -184,7 +185,12 @@ export function createMouseDispatch({ agent, state, pushLine, render, popPicker
184
185
  const cancelSubagent = (key) => {
185
186
  try {
186
187
  const id = key.slice(key.lastIndexOf("#") + 1)
187
- const r = cancelAsyncSubagent(agent, id)
188
+ const isAdvisorBlock = key.startsWith("advisor#")
189
+ // §24 D-24b (②-6b): ⏹ on an advisor block cancels the background review
190
+ // (directed abort → cancelled settle: no pending entry / no token).
191
+ const r = isAdvisorBlock
192
+ ? cancelAsyncAdvisor(agent, id)
193
+ : cancelAsyncSubagent(agent, id)
188
194
  if (r?.status === "error") {
189
195
  // 池内无此条目但区块仍 live(!done)→ 阻塞型(sync)spawn 中——无池条目可定向
190
196
  // 中止(§19.5 cancel 只针对 async 后台子代理);给出可操作指引而非神秘 unknown id
@@ -300,22 +300,24 @@ export function createPickers(ctx) {
300
300
  const target = agent.providers.find((pp) => pp.name === item.provider)
301
301
  if (!target) return
302
302
  const providerDefault = target.model
303
+ const nextActiveModel = item.model !== providerDefault ? item.model : null // default model → clear activeModel
304
+ // D-F5a(selectModel——清单外同型写回补正)先盘后存:磁盘 fresh raw 单操作(model + active 指针)——冲突放弃不留 ghost
305
+ await persistRaw((raw) => {
306
+ raw.providers ??= []
307
+ const t = raw.providers.find((p) => p?.name === item.provider)
308
+ if (t) t.model = item.model
309
+ raw.activeProvider = item.provider
310
+ raw.activeModel = nextActiveModel || undefined // null → omit from config
311
+ })
303
312
  target.model = item.model
304
313
  agent.activeProvider = item.provider
305
- // If selecting the provider's default model, clear activeModel; otherwise set it
306
- agent.activeModel = item.model !== providerDefault ? item.model : null
314
+ agent.activeModel = nextActiveModel
307
315
  agent.provider = { ...target }
308
316
  if (agent.config?.agent?.compactThresholdAuto) {
309
317
  const { resolveCompactThreshold } = await import("../config.mjs")
310
318
  // provider 对象(非 model 字符串)——阈值跟随 providers[].context 覆盖(PROVIDER.md §15 T-C2)
311
319
  agent.config.agent.compactThreshold = resolveCompactThreshold(null, target).value
312
320
  }
313
- await persistRaw((raw) => {
314
- // 落盘前剥离运行时注入的 proxyUri(由 loadConfig + injectProxy 在加载时重建)
315
- raw.providers = agent.providers.map(({ proxyUri: _, ...p }) => p)
316
- raw.activeProvider = item.provider
317
- raw.activeModel = agent.activeModel || undefined // null → omit from config
318
- })
319
321
  agent.config.activeProvider = item.provider
320
322
  agent.config.activeModel = agent.activeModel
321
323
  if (!agent.provider.apiKey) {
@@ -354,8 +356,8 @@ export function createPickers(ctx) {
354
356
  const cfg = { name, baseURL, model }
355
357
  if (format.name === "anthropic" || format.name === "google") cfg.format = format.name
356
358
  else if (format.name !== "openai") return // 防御:未知格式(理论不可达——picker 枚举)
359
+ await persistRaw((raw) => { raw.providers ??= []; raw.providers.push(cfg) }) // D-F5a(#3)先盘后存
357
360
  agent.providers.push(cfg)
358
- await persistRaw((raw) => { raw.providers = agent.providers })
359
361
  const key = await askQuestion(`Enter API key for ${name} (skip if none):`)
360
362
  if (key) await setProviderKey(name, key)
361
363
  return
@@ -368,8 +370,8 @@ export function createPickers(ctx) {
368
370
  if (preset.maxTokens) cfg.maxTokens = preset.maxTokens
369
371
  if (preset.chatPath) cfg.chatPath = preset.chatPath
370
372
  if (preset.format) cfg.format = preset.format
373
+ await persistRaw((raw) => { raw.providers ??= []; raw.providers.push(cfg) }) // D-F5a(#2)先盘后存
371
374
  agent.providers.push(cfg)
372
- await persistRaw((raw) => { raw.providers = agent.providers })
373
375
  const key = await askQuestion(`Enter API key for ${se.name} (skip if none):`)
374
376
  if (key) await setProviderKey(se.name, key)
375
377
  }
@@ -382,8 +384,13 @@ export function createPickers(ctx) {
382
384
  ...candidates.map((p) => ({ type: "item", text: `${p.name} (${p.model})`, name: p.name })),
383
385
  ])
384
386
  if (!se) return
387
+ // D-F5a(#4)先盘后存:磁盘 fresh raw 上 splice;冲突放弃不留下内存 ghost
388
+ await persistRaw((raw) => {
389
+ raw.providers ??= []
390
+ const idx = raw.providers.findIndex((p) => p?.name === se.name)
391
+ if (idx !== -1) raw.providers.splice(idx, 1)
392
+ })
385
393
  agent.providers.splice(agent.providers.findIndex((p) => p.name === se.name), 1)
386
- await persistRaw((raw) => { raw.providers = agent.providers })
387
394
  }
388
395
 
389
396
  async function setKeyFlow() {
@@ -399,14 +406,18 @@ export function createPickers(ctx) {
399
406
  async function setProviderKey(name, key) {
400
407
  const target = agent.providers.find((p) => p.name === name)
401
408
  if (!target) return
409
+ // D-F5a(#5)先盘后存:磁盘 fresh raw 上改目标项 apiKey;冲突放弃不留下内存 ghost
410
+ await persistRaw((raw) => {
411
+ raw.providers ??= []
412
+ const t = raw.providers.find((p) => p?.name === name)
413
+ if (t) t.apiKey = key
414
+ })
402
415
  target.apiKey = key
403
416
  if (name === agent.activeProvider) agent.provider.apiKey = key
404
- await persistRaw((raw) => { raw.providers = agent.providers })
405
417
  }
406
418
 
407
- /** /model provider 管理:context 窗口字段(K 单位,PROVIDER.md §15 D-C4/D-C5)——
408
- * picker 选 provider + 表单输入(复用 syncProviderField 的落盘模式:改 agent.providers 目标项
409
- * → persistRaw 全量写盘);空输入清空(回 spec 值);非法输入报错不落盘(D-C1 语义同 loadConfig)。 */
419
+ /** /model provider 管理:context 窗口字段(K 单位,PROVIDER.md §15 D-C4/D-C5)——picker 选
420
+ * provider + 表单输入;空清空/非法不落盘(D-C1);落盘 = D-F5a(#6)fresh 单字段补丁——先盘后存。 */
410
421
  async function setContextFlow() {
411
422
  const se = await showPicker("Set Context Window", [
412
423
  { type: "header", text: "Select provider" },
@@ -423,26 +434,33 @@ export function createPickers(ctx) {
423
434
  const val = (await askQuestion(
424
435
  `Context window for ${se.name} in K units (current: ${current ? `${current}K` : "spec default"} — e.g. 128 = 128K; empty to clear):`
425
436
  ))?.trim() ?? ""
426
- if (val === "") {
427
- delete target.context
428
- } else {
437
+ let newCtx // undefined = 清空(回 spec 值)
438
+ if (val !== "") {
429
439
  const n = Number(val)
430
440
  if (!Number.isInteger(n) || n <= 0) {
431
441
  pushLine(`Invalid context: "${val}" — must be a positive integer in K units (e.g. 128 = 128K)`, C.error)
432
442
  return
433
443
  }
434
- target.context = n
444
+ newCtx = n
435
445
  }
446
+ await persistRaw((raw) => {
447
+ raw.providers ??= []
448
+ const t = raw.providers.find((p) => p?.name === se.name)
449
+ if (!t) return
450
+ if (newCtx === undefined) delete t.context
451
+ else t.context = newCtx
452
+ })
453
+ if (newCtx === undefined) delete target.context
454
+ else target.context = newCtx
436
455
  if (se.name === agent.activeProvider) {
437
456
  // 运行时 provider 同步(同 setProviderKey 先例:只补 context,不重建对象以免丢 activeModel 覆盖)
438
- if (target.context === undefined) delete agent.provider.context
439
- else agent.provider.context = target.context
457
+ if (newCtx === undefined) delete agent.provider.context
458
+ else agent.provider.context = newCtx
440
459
  if (agent.config?.agent?.compactThresholdAuto) {
441
460
  const { resolveCompactThreshold } = await import("../config.mjs")
442
461
  agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
443
462
  }
444
463
  }
445
- await persistRaw((raw) => { raw.providers = agent.providers })
446
464
  pushLine(target.context !== undefined
447
465
  ? `ctx = ${target.context}K (${target.context * 1024} tokens)`
448
466
  : `context cleared — using model spec (${fmtContextK(providerSpec(target).context)})`, C.tool)
@@ -10,7 +10,7 @@ import { formatTables, sanitizeDisplay, sliceByWidth, wrapText } from "./render.
10
10
  import {
11
11
  foldTailLines, isExpanded, renderBlockTimeline, renderExpandedBlock, renderFoldedHead, renderMathAndMarkdown,
12
12
  } from "./fold-block.mjs"
13
- import { styleSubLabelRow } from "./subagent-panel.mjs"
13
+ import { renderSubChildSections, subChildFoldKey } from "./subagent-panel.mjs"
14
14
  import { SUBAGENT_ROLES } from "./subagent-blocks.mjs"
15
15
 
16
16
  const _toolSegCache = new WeakMap()
@@ -70,7 +70,8 @@ export function toolSeg(state, l, i, cols, maxRows) {
70
70
 
71
71
  /** 冻结子agent 活动块渲染(§7.2 D4):state.lines 载体 {_frozenSubTask: sub}(subagent-blocks.mjs
72
72
  * freezeSubTaskLines 推入)。折叠 = 身份头 + tail 3;展开 = 共享组件(60% 屏幕封顶 + 底部可达
73
- * 折叠控制)。折叠键 `sub-${key}`——与运行中面板区块同键,折叠态跨冻结边界无缝延续。 */
73
+ * 折叠控制)。折叠键 `sub-${key}`——与运行中面板区块同键,折叠态跨冻结边界无缝延续。
74
+ * §27 R23:子块段(renderSubChildSections)随冻结载体渲染(冻结含子块——T-R23d.1)。 */
74
75
  function frozenSubTaskLines(state, sub, cols, maxRows) {
75
76
  const foldKey = `sub-${sub.key}`
76
77
  const elapsed = Math.floor(((sub.doneAt ?? Date.now()) - sub.started) / 1000)
@@ -92,7 +93,7 @@ function frozenSubTaskLines(state, sub, cols, maxRows) {
92
93
  if (isExpanded(state, foldKey)) {
93
94
  // Expanded: shared component renders blank + ▼ control + full timeline,
94
95
  // capped at 60% of the screen with a bottom collapse control.
95
- const body = renderBlockTimeline(sub.blocks, cols).map(styleSubLabelRow)
96
+ const body = renderBlockTimeline(sub.blocks, cols)
96
97
  out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
97
98
  } else {
98
99
  // Folded: the header line itself is the control (▶ affordance), then tail 3.
@@ -105,22 +106,38 @@ function frozenSubTaskLines(state, sub, cols, maxRows) {
105
106
  out.push({ text: `│ ${sliceByWidth(line, cols - 4)}`, color: C.dim, _skipDimFold: true })
106
107
  }
107
108
  }
109
+ // §27 R23:子块段随冻结载体(折叠/展开两态都渲染——子块自身折叠键独立控制)。
110
+ out.push(...renderSubChildSections(state, sub, cols, maxRows))
108
111
  return out
109
112
  }
110
113
 
111
- /** 冻结子agent 段入口(2026-08-31 段缓存:冻结后内容不变——签名含 sub.key + blocks 计数;
112
- * 注:foldKey 由位置键升级为 sub.key(loadOlder unshift 后不重绑——sub 自身携带 key)。 */
114
+ /** 冻结子agent 段入口(2026-08-31 段缓存:冻结后内容不变——签名含 sub.key + blocks 计数 +
115
+ * 子块树签名(R23——children 折叠态/滚动/行数参与——toggle 子块/翻窗必须失效);注:
116
+ * foldKey 由位置键升级为 sub.key(loadOlder unshift 后不重绑——sub 自身携带 key)。 */
113
117
  export function frozenSubSeg(state, l, i, cols, maxRows) {
114
118
  const fKey = `sub-${l._frozenSubTask.key}`
119
+ // §27 R23:子块树渲染输入签名(key/done/stopped/blocks 数/折叠态/窗口偏移——递归)
120
+ const sub = l._frozenSubTask
121
+ let childSig = ""
122
+ const walk = (carrier, path) => {
123
+ for (const c of carrier.children ?? []) {
124
+ const innerPath = path ? `${path}/${c.key}` : c.key
125
+ const fk = subChildFoldKey(sub.key, innerPath)
126
+ childSig += `${innerPath}:${c.done ? 1 : 0}:${c.stopped ? 1 : 0}:${c.blocks?.length ?? 0}:${state.expandedBlocks?.has(fk) ? 1 : 0}:${state._foldScroll?.get(fk) ?? 0};`
127
+ walk(c, innerPath)
128
+ }
129
+ }
130
+ walk(sub, "")
115
131
  const fSig = [
116
- cols, maxRows ?? 0, l._frozenSubTask.key, l._frozenSubTask.blocks?.length ?? 0,
117
- l._frozenSubTask.done ? 1 : 0, state.foldEnabled === false ? 0 : 1,
132
+ cols, maxRows ?? 0, sub.key, sub.blocks?.length ?? 0,
133
+ sub.done ? 1 : 0, state.foldEnabled === false ? 0 : 1,
118
134
  state.expandedBlocks?.has(fKey) ? 1 : 0, state._foldScroll?.get(fKey) ?? 0,
135
+ childSig,
119
136
  ].join("|")
120
137
  const hit = _frozenSubCache.get(l)
121
- if (hit && hit.textRef === l._frozenSubTask && hit.sig === fSig) return hit.rows
122
- const rows = frozenSubTaskLines(state, l._frozenSubTask, cols, maxRows)
123
- _frozenSubCache.set(l, { textRef: l._frozenSubTask, sig: fSig, rows })
138
+ if (hit && hit.textRef === sub && hit.sig === fSig) return hit.rows
139
+ const rows = frozenSubTaskLines(state, sub, cols, maxRows)
140
+ _frozenSubCache.set(l, { textRef: sub, sig: fSig, rows })
124
141
  return rows
125
142
  }
126
143
 
@@ -187,6 +187,10 @@ export function showStartup(ctx) {
187
187
  }
188
188
  pushLine(`Tools: ${agent.tools.map((t) => t.name).join(", ")}`, C.dim)
189
189
 
190
+ // R25(F-R25c):上次运行异常终止提示——bin 入口扫描 crash-reports(24h 窗)经
191
+ // startTUI opts.crashNotice 传入——无匹配为 undefined → 不提示(负例)
192
+ if (opts.crashNotice) pushLine(opts.crashNotice, C.warn)
193
+
190
194
  // Recover previous session: rebuild from history (lazy — display snapshot is
191
195
  // deprecated; it drifted out of sync with history on VS Code writes).
192
196
  if (opts.restored?.history?.length) {