thincoder 0.12.67 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +17 -5
  3. package/bin/thincoder.mjs +41 -344
  4. package/package.json +2 -2
  5. package/src/acp/bridge.mjs +5 -2
  6. package/src/acp/handlers-session.mjs +12 -1
  7. package/src/cli/make-agent.mjs +44 -119
  8. package/src/cli/memory-command.mjs +45 -3
  9. package/src/command-interactive.mjs +186 -0
  10. package/src/command-table.mjs +185 -0
  11. package/src/completions.mjs +36 -12
  12. package/src/crash-reports.mjs +42 -20
  13. package/src/distill.mjs +3 -3
  14. package/src/heap-watch.mjs +5 -14
  15. package/src/tui/agent-turn.mjs +88 -50
  16. package/src/tui/clipboard.mjs +74 -30
  17. package/src/tui/cmd-advisor.mjs +39 -13
  18. package/src/tui/cmd-clear.mjs +2 -2
  19. package/src/tui/cmd-config.mjs +17 -1
  20. package/src/tui/cmd-eng.mjs +1 -1
  21. package/src/tui/cmd-mcp.mjs +1 -1
  22. package/src/tui/cmd-new.mjs +2 -2
  23. package/src/tui/cmd-reindex.mjs +1 -1
  24. package/src/tui/cmd-session.mjs +13 -5
  25. package/src/tui/cmd-think.mjs +35 -17
  26. package/src/tui/cmd-timers.mjs +33 -0
  27. package/src/tui/conversation-writer.mjs +2 -2
  28. package/src/tui/display-budget.mjs +9 -9
  29. package/src/tui/fold-block.mjs +1 -1
  30. package/src/tui/index.mjs +20 -1
  31. package/src/tui/interaction.mjs +4 -2
  32. package/src/tui/key-handler-ctrlc.mjs +1 -1
  33. package/src/tui/key-handler-search.mjs +1 -1
  34. package/src/tui/key-handler.mjs +1 -1
  35. package/src/tui/key-modes.mjs +1 -1
  36. package/src/tui/layout.mjs +5 -5
  37. package/src/tui/lifecycle-records.mjs +199 -0
  38. package/src/tui/model-picker.mjs +16 -199
  39. package/src/tui/mouse.mjs +3 -3
  40. package/src/tui/pickers.mjs +6 -2
  41. package/src/tui/provider-admin.mjs +213 -0
  42. package/src/tui/queued-merge.mjs +4 -60
  43. package/src/tui/queued-pickup.mjs +3 -3
  44. package/src/tui/render-conversation.mjs +13 -4
  45. package/src/tui/render-frame.mjs +43 -6
  46. package/src/tui/render-loop.mjs +2 -3
  47. package/src/tui/render-segments.mjs +2 -2
  48. package/src/tui/render.mjs +1 -1
  49. package/src/tui/slash-commands.mjs +3 -0
  50. package/src/tui/startup.mjs +28 -3
  51. package/src/tui/subagent-blocks.mjs +27 -19
  52. package/src/tui/subagent-children.mjs +5 -5
  53. package/src/tui/subagent-freeze.mjs +39 -37
  54. package/src/tui/subagent-panel.mjs +7 -7
  55. package/src/tui/suspension-drive.mjs +172 -259
  56. package/src/tui/timer-watch.mjs +64 -0
  57. package/src/tui/tool-args.mjs +10 -71
  58. package/src/tui/tool-display.mjs +6 -6
  59. package/src/tui/tool-events.mjs +33 -28
  60. package/src/tui/tui-lifecycle.mjs +13 -4
  61. package/src/tui/tui-state.mjs +3 -2
  62. package/src/tui/turn-face.mjs +4 -1
  63. package/src/tui/wizard.mjs +7 -2
  64. package/src/tui/wrapped-spawn.mjs +3 -3
@@ -1,81 +1,20 @@
1
1
  /**
2
- * tool-args.mjs — 工具调用参数的可读展示(2026-08-30,对齐 vscode 端卡片头的参数可见性)。
2
+ * tool-args.mjs — 工具调用参数的可读展示(**转口档** —— B7 1a 收编:`describeToolArgs` 体迁核件
3
+ * 单源 `@thincoder/core/tool-args.mjs`,本档同名转口、消费面零改;`toolArgsLines`(恢复路径全量
4
+ * pretty JSON dim 行)留端)。
3
5
  *
4
- * vscode 端:tool-call 卡片头 = name + args(80 字符截断,悬停看全量)。
5
- * TUI 端此前:live 标题行 = 原始 JSON 前 80 字符(长路径截半、不可读);
6
- * 恢复的历史会话 = ` [tool] name` 完全没有参数(display snapshot 废弃后
7
- * historyToLines 是恢复的唯一路径,参数可见性随之丢失——用户报告)。
6
+ * 消费面(零改):`tool-events.mjs`(live 标题行)· `startup.mjs`(历史恢复)· `subagent-blocks.mjs`
7
+ * (子代理块头)。
8
8
  *
9
- * 单源 describeToolArgs():按工具挑关键参数的可读单行摘要(标题行/状态栏共用);
10
- * 未知工具回退紧凑 JSON。恢复路径另外输出全量 pretty JSON 作 dim 行
11
- * (走既有连续 dim 自动折叠——与工具结果的恢复惯例一致;TUI 无悬停,
12
- * 全量必须落行)。
9
+ * 已登记差额(B7 §2 修正轮 1 ⑨):默认分支截断 = 核件简版切片(码元 `slice`)替代本端
10
+ * `render.mjs` `sliceByWidth`(宽度感知 ∕ 全角计宽)——全角字符续接点可异于旧端(160 ∕ 80
11
+ * 两档触发);码元切可断代理对(展示面);机制句单源 = 核件档头。
13
12
  */
14
- import { sliceByWidth } from "./render.mjs"
15
13
  import { capLines, ARGS_JSON_MAX_CHARS } from "./display-budget.mjs"
16
-
17
- /** 单行参数摘要:按工具挑关键字段(vscode 卡片头对齐),未知工具回退 JSON。 */
18
- export function describeToolArgs(name, args) {
19
- if (!args || typeof args !== "object") return ""
20
- const a = args
21
- switch (name) {
22
- case "bash": case "cmd-shell": {
23
- const cmd = String(a.command ?? "").replace(/\s+/g, " ").trim()
24
- return cmd ? cmd + (a.workdir ? ` (in ${a.workdir})` : "") : ""
25
- }
26
- case "read": case "write": case "edit": case "hashline_edit": {
27
- const p = a.path ? String(a.path) : ""
28
- const extras = [
29
- a.offset ? `offset ${a.offset}` : "",
30
- a.limit ? `limit ${a.limit}` : "",
31
- name === "edit" && a.old_string ? `old: ${String(a.old_string).replace(/\s+/g, " ").trim().slice(0, 30)}` : "",
32
- ].filter(Boolean).join(", ")
33
- return p ? `"${p}"${extras ? " · " + extras : ""}` : extras
34
- }
35
- case "grep": case "glob": case "code_search": case "doc_search": case "search": {
36
- const pat = a.pattern ?? a.query ?? ""
37
- const p = a.path ? ` in "${a.path}"` : ""
38
- return `/${String(pat)}/${p}`
39
- }
40
- case "ls": {
41
- const p = a.path ? String(a.path) : "."
42
- return a.filter ? `${p} (filter: ${String(a.filter)})` : p
43
- }
44
- case "websearch": return String(a.query ?? "")
45
- case "subagent": case "coder": case "explore": case "plan": case "eng-coder": case "eng-designer": {
46
- const task = String(a.task ?? "").replace(/\s+/g, " ").trim()
47
- if (task) return task.slice(0, 60) + (task.length > 60 ? "…" : "")
48
- // action-only 调用(status/observe/send/cancel/escalate/consume-design…)无 task——action 兜底(2.4 批)
49
- return a.action ? `(${String(a.action)})` : ""
50
- }
51
- case "advisor": return String(a.type ?? "review")
52
- case "read_image": return String(a.path ?? "")
53
- case "question": return String(a.question ?? "").replace(/\s+/g, " ").trim().slice(0, 60)
54
- case "memory": {
55
- // §6 action-routed summary (D-M5): one readable line per action
56
- const action = String(a.action ?? "")
57
- if (action === "put") return String(a.title ?? "")
58
- if (action === "search") return String(a.query ?? "")
59
- if (action === "list") return [a.layer && `layer ${a.layer}`, a.type && `type ${a.type}`, a.keyword && `kw ${a.keyword}`].filter(Boolean).join(" ")
60
- if (action === "delete") return a.id ? `id ${a.id}${a.layer ? ` (layer ${a.layer})` : ""}` : `batch ${a.layer ?? ""} ${a.type ? `type ${a.type} ` : ""}${a.keyword ? `kw ${a.keyword}` : ""}`.trim()
61
- if (action === "clear") return `clear ${a.layer ?? ""}`
62
- return action || JSON.stringify(a)
63
- }
64
- case "lsp": return [a.subcommand, a.uri].filter(Boolean).map(String).join(" ")
65
- case "repo_outline": return a.path ? String(a.path) : ""
66
- case "checkpoint": return [a.checkpointAction ?? a.action, a.checkpointId, a.path].filter(Boolean).map(String).join(" ")
67
- case "git": return [a.action, a.ref, a.name, a.message].filter(Boolean).map(String).join(" ").slice(0, 60)
68
- default: {
69
- // MCP 工具 / 未知工具:单行紧凑 JSON(vscode 卡片头同样给原始 JSON 截断)。
70
- // 硬上限 160:超大 arguments(如 execute 的千字符 code)绝不进单行头。
71
- const s = JSON.stringify(a)
72
- return s.length > 160 ? sliceByWidth(s, 156) + "…" : s.length > 80 ? sliceByWidth(s, 78) + "…" : s
73
- }
74
- }
75
- }
14
+ export { describeToolArgs } from "@thincoder/core/tool-args.mjs"
76
15
 
77
16
  /** 恢复路径用:全量参数 pretty JSON 的 dim 行(非空才输出)。
78
- * 与工具结果的恢复惯例一致——完整落行;**总量额度**(TUI-OOM-ROOTCAUSE §15.3.1
17
+ * 与工具结果的恢复惯例一致——完整落行;**总量额度**(TUI-OOM-ROOTCAUSE·TUI-SESSION-VIEW.md §5.1
79
18
  * ARGS_JSON_MAX_CHARS——`write`/`apply_patch` 整文件内容进显示层的堵口):超出尾截断
80
19
  * + 标记,首行保真。 */
81
20
  export function toolArgsLines(args) {
@@ -7,7 +7,7 @@
7
7
  * 不禁内容变更(.set/.delete/.push),调用方行为与同模块时代一致。
8
8
  */
9
9
 
10
- // TUI-OOM-ROOTCAUSE(TUI.md §15.3.1):显示层额度常量/工具行定位(display-budget 叶子)。
10
+ // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.1):显示层额度常量/工具行定位(display-budget 叶子)。
11
11
  import { capLines, TOOL_RESULT_MAX_CHARS } from "./display-budget.mjs"
12
12
 
13
13
  // Tool execution start timestamps (performance.now ms). Keyed by tool_call id
@@ -77,7 +77,7 @@ export function sweepToolBlocks(state) {
77
77
  * the human needs only the text part (model gets images via the multimodal
78
78
  * channel); 2) results beyond maxRows are truncated in the block (full
79
79
  * text always lives in history for the model). Returns row array.
80
- * TUI-OOM-ROOTCAUSE(§15.3.1 TOOL_RESULT_MAX_CHARS):总量额度与 400 行双维
80
+ * TUI-OOM-ROOTCAUSE(§5.1 TOOL_RESULT_MAX_CHARS):总量额度与 400 行双维
81
81
  * (单行任意大——无 `\n` 巨 chunk/JSON 尾行),尾截断 + 标记。 */
82
82
  export function slimToolResultForDisplay(result, maxRows = 400) {
83
83
  let displayResult = result
@@ -112,10 +112,10 @@ export function settleToolBlock(state, name, toolId, summary) {
112
112
  }
113
113
  }
114
114
 
115
- /** Async spawn detection (§15 D-A1): the subagent tool's async:true result is a
115
+ /** Async spawn detection (AGENT-LOOP-SUBAGENT.md §6.7.3 D-A1): the subagent tool's async:true result is a
116
116
  * status JSON ({id, role, status: running|queued}), NOT a report — the child
117
- * keeps running, so its activity block must not be frozen at spawn time (it
118
- * freezes via the ⟦ev⟧done event at settle — §15 D-A3). */
117
+ * keeps running, so its activity block must not be frozen at spawn time(块保 live——settle 发
118
+ * ⟦ev⟧settled;冻结落消费窗——#746 · ASYNC-POOL §6.8)。 */
119
119
  export function isAsyncSpawnResult(result) {
120
120
  try {
121
121
  const o = JSON.parse(result)
@@ -125,7 +125,7 @@ export function isAsyncSpawnResult(result) {
125
125
  }
126
126
  }
127
127
 
128
- /** §7.2.3 spawn 门拒错误探测(T-F5):subagent spawn 的机械拒绝以 {status:"error"}
128
+ /** docs/cli/design/TUI.md §6.8 spawn 门拒错误探测(T-F5):subagent spawn 的机械拒绝以 {status:"error"}
129
129
  * JSON 返回(例:manual auto-turn digest spawn 门——digest 语义 organize-only)——
130
130
  * 错误路径不得触发完成冻结(round1 #1——错误路径不冻结任何 running 块)。该形态只
131
131
  * 出现在无 subKey 的拒绝路径(成功路径恒带 dispatch 传的 ctx._subagentKey)。 */
@@ -22,11 +22,11 @@ import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
22
22
  import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "@thincoder/core/advisor/run.mjs"
23
23
  import {
24
24
  SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
25
- routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, freezeDoneSubTasks,
25
+ routeSubToolOutput, finishSubTask, finishSubTaskKey, freezeDoneSubTasks,
26
26
  ensureCompressPanel, markCompressFailed, markCompressDone, markCompressFallback,
27
27
  shiftFreezeAnchors,
28
28
  } from "./subagent-blocks.mjs"
29
- // 第 27 批 §12.3②:前缀正则换名 + import 源改文法模块(纯换名——语义零改)。
29
+ // 第 27 批:前缀正则换名 + import 源改文法模块(纯换名——语义零改)。
30
30
  import { RELAY_PREFIX_RE } from "@thincoder/core/agent/relay-prefix.mjs"
31
31
  import { TURN_CAP_MARK, STOPPED_MARK } from "@thincoder/core/agent/spawn-child.mjs"
32
32
  // 批 1 CORE-DEFECT-FIXES B3:onWait 相位值域 + 文案单源(消费面禁各自枚举——PROVIDER.md §6.20)
@@ -40,14 +40,14 @@ import {
40
40
  findToolBlock, findToolLine, slimToolResultForDisplay, sweepToolBlocks,
41
41
  } from "./tool-display.mjs"
42
42
  export { sweepToolBlocks, slimToolResultForDisplay } from "./tool-display.mjs"
43
- // TUI-OOM-ROOTCAUSE(TUI.md §15.3.1/§15.3.3):显示层额度(行/载体/输出环/评审/流式)。
43
+ // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.1/§5.3):显示层额度(行/载体/输出环/评审/流式)。
44
44
  import {
45
45
  capText, capLine, capLines, appendCapped, capAdvisorText, accountLine, syncLineBudget,
46
46
  LINE_TRUNC_MARKER, MIDDLE_TRUNC_MARKER, ADVISOR_CAP_OPTS, STREAM_CAP_OPTS,
47
47
  TOOL_OUTPUT_ENTRY_MAX_CHARS, TOOL_OUTPUT_TOTAL_MAX_CHARS, ADVISOR_TEXT_MAX_CHARS,
48
48
  } from "./display-budget.mjs"
49
49
 
50
- /** 输出环字符维总量(§15.3.1 TOOL_OUTPUT_TOTAL_MAX_CHARS——与 200 条目环同款丢最旧)。 */
50
+ /** 输出环字符维总量(§5.1 TOOL_OUTPUT_TOTAL_MAX_CHARS——与 200 条目环同款丢最旧)。 */
51
51
  function trimOutputChars(block) {
52
52
  let total = 0
53
53
  for (const s of block.output) total += s.length
@@ -60,9 +60,9 @@ function trimOutputChars(block) {
60
60
  * askPermission, askQuestion, saveSessionImpl } */
61
61
  export function buildToolCallbacks(deps) {
62
62
  const { agent, state, pushLine, render, scheduleRender, ensureAssistantLabel, askPermission, askBatchPermission, askQuestion, saveSessionImpl } = deps
63
- // 行集总量对账便捷式(§15.3.3——冻结锚点平移交 subagent 族函数)
63
+ // 行集总量对账便捷式(§5.3——冻结锚点平移交 subagent 族函数)
64
64
  const budgetSync = () => syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
65
- /** Advisor 有序块累积 + 额度(§15.3.1 ADVISOR_TEXT_MAX_CHARS——头 32K/尾 96K/中段标记)。 */
65
+ /** Advisor 有序块累积 + 额度(§5.1 ADVISOR_TEXT_MAX_CHARS——头 32K/尾 96K/中段标记)。 */
66
66
  const pushAdvisorChunk = (raw, kind) => {
67
67
  const blocks = state._advisorBlocks ??= []
68
68
  const last = blocks.at(-1)
@@ -100,7 +100,8 @@ export function buildToolCallbacks(deps) {
100
100
  // block content, never the main stream (D1). Routing details in subagent-blocks.
101
101
  if (routeSubToken(state, t, scheduleRender)) return
102
102
  ensureAssistantLabel()
103
- state.streaming = appendCapped(state.streaming, t, STREAM_CAP_OPTS) // §15.3.1 STREAM_MAX_CHARS
103
+ state.streaming = appendCapped(state.streaming, t, STREAM_CAP_OPTS) // §5.1 STREAM_MAX_CHARS
104
+ state.lastOutputAt = Date.now() // 停滞轻显形(TUI.md §7.7):①流式——主文本增量
104
105
  scheduleRender()
105
106
  },
106
107
  onReasoning: (t) => {
@@ -108,7 +109,8 @@ export function buildToolCallbacks(deps) {
108
109
  // buffer as kind=think (F2: same treatment as the main reasoning stream).
109
110
  if (routeSubReasoning(state, t, scheduleRender)) return
110
111
  ensureAssistantLabel()
111
- state.reasoning = appendCapped(state.reasoning, t, STREAM_CAP_OPTS) // §15.3.1 STREAM_MAX_CHARS
112
+ state.reasoning = appendCapped(state.reasoning, t, STREAM_CAP_OPTS) // §5.1 STREAM_MAX_CHARS
113
+ state.lastOutputAt = Date.now() // 停滞轻显形(TUI.md §7.7):①流式——推理增量
112
114
  scheduleRender()
113
115
  },
114
116
  onToolCall: (name, args, toolId) => {
@@ -127,6 +129,7 @@ export function buildToolCallbacks(deps) {
127
129
  flushStream()
128
130
  ensureAssistantLabel()
129
131
  state.currentTool = name
132
+ state.lastOutputAt = Date.now() // 停滞轻显形(TUI.md §7.7):②工具面——调用(标题行 ∕ 状态栏)
130
133
  // Advisor's effective model (resolved once for the status line + inline title below).
131
134
  const advModel = name === "advisor" ? (() => { try { return resolveAdvisorProvider(agent).model } catch { return null } })() : null
132
135
  // Update status bar with current tool and key arguments for user visibility
@@ -170,15 +173,16 @@ export function buildToolCallbacks(deps) {
170
173
  done: false,
171
174
  },
172
175
  })
173
- accountLine(state, state.lines[state.lines.length - 1]) // 载体入总量账(§15.3.4)
176
+ accountLine(state, state.lines[state.lines.length - 1]) // 载体入总量账(§5.4)
174
177
  budgetSync()
175
178
  tickStart(name, toolId)
176
179
  },
177
- // §7.2.3(方案 e):dispatch runOne 把工具 ctx 上的 _subagentKey(sync spawn/
180
+ // docs/cli/design/TUI.md §6.8(方案 e):dispatch runOne 把工具 ctx 上的 _subagentKey(sync spawn/
178
181
  // escalate 成功路径设置——relayPrefix 去尾)作为第 4 参传来——undefined 兼容既有
179
182
  // 签名(普通工具/老回调/错误路径不带 key)。
180
183
  onToolResult: (name, result, toolId, subKey) => {
181
184
  state.currentTool = null
185
+ state.lastOutputAt = Date.now() // 停滞轻显形(TUI.md §7.7):②工具面——结果(含子代理 ∕ escalate ∕ advisor 面同刻)
182
186
  // AGENT-LOOP-SUBAGENT.md §6.7 merged family: route per the action recorded at onToolCall (no record = default spawn).
183
187
  let subAction = null
184
188
  if (name === "subagent") {
@@ -199,11 +203,11 @@ export function buildToolCallbacks(deps) {
199
203
  // never be marked done (its result lands in the subagent block, not the
200
204
  // carrier) and the turn sweep would mislabel it "(interrupted)".
201
205
  settleToolBlock(state, name, toolId, "completed")
202
- // Async spawn (§15 D-A1): the result is a status JSON, not a report — the
206
+ // Async spawn (AGENT-LOOP-SUBAGENT.md §6.7.3 D-A1): the result is a status JSON, not a report — the
203
207
  // child KEEPS running; skip the freeze (it would tombstone a live block and
204
- // drop its relay stream). The block freezes on the ⟦ev⟧done settle event.
208
+ // drop its relay stream). 块保 live——settle 发 ⟦ev⟧settled;冻结落消费窗(#746 · ASYNC-POOL §6.8)。
205
209
  if (!isAsyncSpawnResult(result)) {
206
- // §7.2.3 sync spawn 完成精确冻结:结果按 key 归属三支——
210
+ // §6.8 sync spawn 完成精确冻结:结果按 key 归属三支——
207
211
  // ① dispatch 同步成功路径带 subKey(ctx._subagentKey = relayPrefix 去尾):
208
212
  // finishSubTaskKey 按 key 精确冻——不再落 finishSubTask 的"最早 started"
209
213
  // 启发式(async eng-coder 先启动时 explore 完成会误冻其块——7.2.3.1/T-F2);
@@ -232,12 +236,12 @@ export function buildToolCallbacks(deps) {
232
236
  // 飞刀 post-op report landed under the subagent tool name — freeze the
233
237
  // escalate#N activity block (no preview; legacy surface).
234
238
  settleToolBlock(state, name, toolId, "completed")
235
- // §25 D-R17b (R17): async escalate ack ({id, role:"escalate", status:
239
+ // ESCALATE.md §5 D-R17b (R17): async escalate ack ({id, role:"escalate", status:
236
240
  // running|queued}) — the child KEEPS running — skip the freeze like the
237
- // async spawn path (the block freezes on the ⟦ev⟧done/stopped settle
238
- // event at flight end). Sync results (async:false) freeze below.
241
+ // async spawn path (settle ⇒ ⟦ev⟧settled 驻留;冻结落消费窗——#746 · ASYNC-POOL §6.8;
242
+ // cancel 径 ⟦ev⟧stopped 即冻——照旧). Sync results (async:false) freeze below.
239
243
  if (!isAsyncSpawnResult(result)) {
240
- // §7.2.3(round1 #2):escalate 成功返回带 subKey(escalate#N)→ 精确冻;
244
+ // §6.8(round1 #2):escalate 成功返回带 subKey(escalate#N)→ 精确冻;
241
245
  // 失败/老回调无 subKey → 不冻结任何块(F-2 收窄——finishSubTask 恒 no-op,
242
246
  // 块由回合尾 freezeAllSubTasks 兜底清场)。
243
247
  // SYNC-CANCEL(R6):同上——escalate 路径同款扩展(sync escalate 无 registry——
@@ -251,12 +255,12 @@ export function buildToolCallbacks(deps) {
251
255
  freezeDoneSubTasks(state)
252
256
  }
253
257
  } else if (name === "consult_stop") {
254
- // R17(§25 D-R17a——check 已删):consult_stop = 取消指定会诊会话。被 abort 的
255
- // children 各自 settle(settleChild)时发 ⟦ev⟧done 冻结自己的卡——此处**不做按
256
- // 角色整组清扫**(会诊会话可并发——按角色会误冻其他仍在运行的会话的卡,冻结即
257
- // 截断其活动流——tool-events advisor 复评 🟡1 修正);取消的即时可见性由 abort
258
- // settle 事件承担(abort 解绕通常在同回合内完成——卡片在其 child settle 即冻结)。
259
- // 仅结果本身落本工具调用自己的载体块(下方通用分支)。
258
+ // R17(CONSULTATION.md §6.2 D-R17a——check 已删):consult_stop = 取消指定会诊会话。被取消会话的
259
+ // children 各自 settle(settleChild)时发 ⟦ev⟧stopped 即折自己的卡(cancel 径——消费永不来);
260
+ // 未取消边 settled 的子块驻留待消化(消费窗三端收口——#748)——此处**不做按角色整组
261
+ // 清扫**(会诊会话可并发——按角色会误冻其他仍在运行的会话的卡,冻结即截断其活动流——
262
+ // tool-events advisor 复评 🟡1 修正);取消的即时可见性由 abort settle 事件承担(abort
263
+ // 解绕通常在同回合内完成——卡片在其 child settle 即折)。仅结果本身落本工具调用自己的载体块(下方通用分支)。
260
264
  }
261
265
  if (!isSubagent && !isEscalate && name !== "advisor") {
262
266
  // Result lands INSIDE the block (restore parity — the restored carrier
@@ -269,7 +273,7 @@ export function buildToolCallbacks(deps) {
269
273
  // screen of garbage (user report 2026-08-30). The model gets the image
270
274
  // via the multimodal channel (agent.mjs), the human needs only the
271
275
  // text part: strip image parts from the displayed result.
272
- block.result = slimToolResultForDisplay(result) // §15.3.1 RESULT 额度(行 400 + 字符 64K 双维)
276
+ block.result = slimToolResultForDisplay(result) // §5.1 RESULT 额度(行 400 + 字符 64K 双维)
273
277
  block.summary = formatToolSummary(name, result)
274
278
  block.done = true
275
279
  const started = tickTake(name, toolId)
@@ -290,7 +294,7 @@ export function buildToolCallbacks(deps) {
290
294
  // _advisorBlocks keep rendering the running view until cleared at turn end.
291
295
  const blocks = state._advisorBlocks ?? []
292
296
  if (blocks.length > 0) {
293
- // 冻结文本额度(§15.3.1:头 32K + 尾 96K 保裁决尾部 + 中段标记——非静默截断,
297
+ // 冻结文本额度(§5.1:头 32K + 尾 96K 保裁决尾部 + 中段标记——非静默截断,
294
298
  // 代码评审 #3:capLines(...)[0] 会丢标记且截尾)
295
299
  const text = capAdvisorText(blocks
296
300
  .map((b) => b.text.replaceAll(ADVISOR_THINKING_PLACEHOLDER, ""))
@@ -326,6 +330,7 @@ export function buildToolCallbacks(deps) {
326
330
  }
327
331
  if (!part.text) return
328
332
  if (routeSubToolOutput(state, name, part, scheduleRender)) return
333
+ state.lastOutputAt = Date.now() // 停滞轻显形(TUI.md §7.7):②工具面——输出流(含 advisor 输出)
329
334
  if (name === "advisor") {
330
335
  // Accumulate to buffer — formatTables + wrapText in render-conversation
331
336
  // handles markdown formatting, same as main agent response.
@@ -338,14 +343,14 @@ export function buildToolCallbacks(deps) {
338
343
  const isString = typeof chunk === "string"
339
344
  const raw = isString ? chunk : String(chunk?.text ?? "")
340
345
  const kind = isString ? "text" : (chunk?.kind ?? "text")
341
- pushAdvisorChunk(raw, kind) // §15.3.1 累积额度(多块丢最旧)
346
+ pushAdvisorChunk(raw, kind) // §5.1 累积额度(多块丢最旧)
342
347
  scheduleRender()
343
348
  return
344
349
  }
345
350
  // Append into the CURRENT tool block's output buffer (the block is the
346
351
  // display; no _live scroll lines anymore). N2-style cap keeps memory
347
352
  // bounded: keep the LAST 200 output lines per call; 字符维双维
348
- // (§15.3.1:单条目 ≤ TOOL_OUTPUT_ENTRY_MAX_CHARS、总量 ≤ TOOL_OUTPUT_TOTAL_MAX_CHARS 丢最旧)。
353
+ // (§5.1:单条目 ≤ TOOL_OUTPUT_ENTRY_MAX_CHARS、总量 ≤ TOOL_OUTPUT_TOTAL_MAX_CHARS 丢最旧)。
349
354
  const block = findToolBlock(state, name, toolId)
350
355
  if (block) {
351
356
  for (const line of part.text.split("\n")) {
@@ -365,7 +370,7 @@ export function buildToolCallbacks(deps) {
365
370
  // handlers — permission requests then deny WITHOUT a panel (AGENT-LOOP-ASYNC-POOL.md §6.8 D-S7: no modal
366
371
  // during unattended digestion) and question errors out instead of hanging.
367
372
  ...(askPermission ? { onPermissionRequest: (name, args) => askPermission(name, args) } : {}),
368
- // Merged batch ask (§16 D-B1): one confirmation for N non-readonly tools in
373
+ // Merged batch ask (D-B1): one confirmation for N non-readonly tools in
369
374
  // the same response — "approve all / one by one / deny" (key-handler resolves
370
375
  // the verdict string; approveAll is batch-scope only, never the AUTO flag).
371
376
  ...(askBatchPermission ? { onBatchPermissionRequest: (req) => askBatchPermission(req) } : {}),
@@ -43,7 +43,7 @@ export function writeCleanupSequence(write = (s) => process.stdout.write(s)) {
43
43
  // T-R25a.2 注入 mock 走同一 setter)。
44
44
  let tuiActive = false
45
45
 
46
- /** 置/清 TUI 活动态(TUI 启动接管处与退出清理处调用;测试 env 门注入同走此 setter)。 */
46
+ /** 置/清 TUI 活动态(TUI 启动接管处与退出清理处调用;测试钩(argv 门)注入同走此 setter)。 */
47
47
  export function setTuiActive(active) {
48
48
  tuiActive = active === true
49
49
  }
@@ -53,13 +53,22 @@ export function isTuiActive() {
53
53
  return tuiActive
54
54
  }
55
55
 
56
+ // R25 崩溃恢复测试缝(T-R25a.2——argv 门注入):bin 入口解析 `--test-cleanup-out=<路径>` 后经
57
+ // setter 注入(启动时一次性读点——单半例外,复位面无消费者);生产零调用 → null(恒 stdout)。
58
+ let _cleanupOut = null
59
+
60
+ /** 测试缝 setter(仅测试注入——生产零调用)。 */
61
+ export function _setCleanupOutPathForTest(path) {
62
+ _cleanupOut = typeof path === "string" && path.length > 0 ? path : null
63
+ }
64
+
56
65
  /** R25 崩溃恢复:仅 TUI 活动态执行 writeCleanupSequence(复用本模块清理序列——符号锚)。
57
- * 测试缝(T-R25a.2——env 门注入):THINCODER_TEST_CLEANUP_OUT 指向文件时序列写入该文件
58
- * (观察恢复被调 + stdout 管道零 ANSI)——生产不设该 env → 恒 stdout(零变化)。
66
+ * 测试缝(T-R25a.2——argv 门注入):`_setCleanupOutPathForTest` 指到文件时序列写入该文件
67
+ * (观察恢复被调 + stdout 管道零 ANSI)——生产不指 → 恒 stdout(零变化)。
59
68
  * @returns {boolean} 是否执行了恢复(false = TUI 未启动——调用方无需处理) */
60
69
  export function restoreTerminalAfterCrash() {
61
70
  if (!tuiActive) return false
62
- const seamOut = process.env.THINCODER_TEST_CLEANUP_OUT
71
+ const seamOut = _cleanupOut
63
72
  if (seamOut) {
64
73
  writeCleanupSequence((s) => appendFileSync(seamOut, s, "utf8"))
65
74
  } else {
@@ -37,9 +37,10 @@ export function createTuiState({ cols, rows, agent }) {
37
37
  reasoning: "", // thinking stream buffer (dimmed display)
38
38
  completion: null, // Tab completion state { candidates, index }
39
39
 
40
- 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(AGENT-LOOP-ASYNC-POOL.md §6.8 挂起中间态), _freezeAt(冻结锚点), stopped, children: [](SUBAGENT-TAIL:嵌套子代理**守护载体**——内容行并入本块 blocks——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(SUBAGENT-TAIL 单环:内层行同环计数、单载体最旧先行——TUI.md §6)
40
+ subTasks: {}, // sub-agent activity blocks (docs/cli/design/TUI.md §6.8 D4): { "coder#1": { key, role, model, started, done, doneAt, blocks: [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError, dropped, blockEpoch, awaitingDigest(AGENT-LOOP-ASYNC-POOL.md §6.8 挂起中间态), _freezeAt(冻结锚点), stopped, children: [](SUBAGENT-TAIL:嵌套子代理**守护载体**——内容行并入本块 blocks——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(SUBAGENT-TAIL 单环:内层行同环计数、单载体最旧先行——TUI.md §6)
41
41
  currentTool: null, // currently executing tool name (shown in status bar)
42
42
  processingStarted: 0, // current turn start time (status bar timer)
43
+ lastOutputAt: 0, // 停滞轻显形(TUI.md §7.7 · stall-indicator 批):最近一次可见输出时刻(静默起算——回合起刻为初始锚;0 = 未起算)
43
44
  status: "Ready",
44
45
  ledger: { marker: null, warn: false, scannedAt: 0 }, // LEDGER-SURFACE(§2.30.3.4):L1 标记位——ledger-surface 写 / render-frame 读
45
46
  queue: [], // 交接残项单容器 [{ text }](INPUT-LOCK:submit 不再排队——仅释放窗口兜底/挂起中止残余——回合尾队列循环续发,至多一条)
@@ -54,7 +55,7 @@ export function createTuiState({ cols, rows, agent }) {
54
55
  _historyLoaded: 0, // messages loaded from the TAIL of _fullHistory
55
56
  _historyTotal: 0, // total messages in the restored session
56
57
  _hasOlder: false, // more earlier messages remain unloaded
57
- _linesChars: 0, // TUI-OOM-ROOTCAUSE(§15.3.2):state.lines 全部行与载体文本总量(字符账——唯一新增状态位)
58
+ _linesChars: 0, // TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.2):state.lines 全部行与载体文本总量(字符账——唯一新增状态位)
58
59
  _agent: null, // TUI state → agent 回指挂载点:startTUI 装配时置 agent 引用——
59
60
  // ① SYNC-CANCEL ⏹ 门控读 state._agent._syncChildAborts(subagent-panel.mjs,2026-09-09);
60
61
  // ② CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):agent._tuiState = state 反向挂载——
@@ -55,10 +55,13 @@ export function createTurnFace(ctx) {
55
55
  const turnCtx = {
56
56
  agent, state, pushLine, pushLabel, render, scheduleRender: render, ensureAssistantLabel,
57
57
  askPermission, askQuestion, askBatchPermission, handleSlash: null,
58
+ // AGENT-LOOP-ASYNC-POOL.md §6.30.5 载体②:空闲 idle deadline 闩(index.mjs 装配后回填——同 handleSlash 惰性回填法)
59
+ timerWatch: null,
58
60
  get assistantLabeled() { return conversation.assistantLabeled },
59
61
  set assistantLabeled(v) { conversation.assistantLabeled = v },
60
62
  }
61
- const turn = (text) => runAgentTurn(turnCtx, text)
63
+ // opts 透传(timer 空闲唤醒轮 = `{ autoTurn: true, timerTurn: true }`——§6.30.3 开轮两处①)
64
+ const turn = (text, opts) => runAgentTurn(turnCtx, text, opts)
62
65
 
63
66
  return { submit, turn, turnCtx, askPermission, askQuestion, askBatchPermission, pasteClipboardImage }
64
67
  }
@@ -16,7 +16,7 @@ import { probeChannelModels } from "./model-catalog.mjs"
16
16
  * Returns { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, finishWizard }
17
17
  */
18
18
  export function createWizard(ctx) {
19
- const { agent, state, pushLine, pushLabel, render, persistRaw } = ctx
19
+ const { agent, state, pushLine, pushLabel, render, persistRaw, onModalClose = null } = ctx
20
20
 
21
21
  /** Candidates for the menu step: existing providers (marked "no key" if missing), unadded presets, custom
22
22
  * MODEL-SELECTION v2:渠道默认模型 = 单值 `model`(preset 自带;候选清单运行期拉取) */
@@ -166,6 +166,8 @@ export function createWizard(ctx) {
166
166
  : "Skipped initial setup. Use /model to add providers and configure API keys anytime."
167
167
  pushLine(hint, C.dim)
168
168
  render()
169
+ // #448①「关闭后补评估」:wizard 面退场(无 picker 同在场)⇒ 通知链尾(闩重武装)
170
+ if (state.picker == null) ctx.onModalClose?.()
169
171
  }
170
172
 
171
173
  /** Wizard complete: write provider (update if exists) with its single default model (`model`),
@@ -234,7 +236,10 @@ export function createWizard(ctx) {
234
236
  pushLine(`Vector search disabled (memory falls back to text-only search). Run /config embedkey <key> to enable.`, C.dim)
235
237
  }
236
238
  pushLine(`Select model (Esc to keep ${agent.activeModel})`, C.dim)
237
- ctx.openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
239
+ // #448①「关闭后补评估」:收尾链落定(模型 picker 关闭 ∕ 零弹面两态)⇒ 通知链尾(闩重武装——幂等)。
240
+ ctx.openModelPicker()
241
+ .catch((e) => pushLine(`[error] ${e.message}`, C.error))
242
+ .then(() => { if (state.picker == null && state.wizard == null) ctx.onModalClose?.() })
238
243
  }
239
244
 
240
245
  return { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, finishWizard, wizardProviderItems }
@@ -44,10 +44,10 @@ export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPat
44
44
  try { writeImpl(RECOVERY_SEQUENCE) } catch { /* 尽力面:终端已关/管道已断——不阻断收尾 */ }
45
45
  }
46
46
  // F3③(CRASH-REPORTS):近堆上限快照落点定向——--diagnostic-dir 为 Node 选项,须在脚本路径前;
47
- // 无条件注入(gate 单点在 crash-reports.mjs——包装器不判 env);未触发快照时零副作用。
48
- const child = spawnImpl(process.execPath, [`--diagnostic-dir=${dir}`, script, ...process.argv.slice(2)], {
47
+ // 无条件注入(判定单点在 bin 入口读配置键 `diagnostics.heapSnapshot` 后经参数传入——包装器不判);未触发快照时零副作用。
48
+ const child = spawnImpl(process.execPath, [`--diagnostic-dir=${dir}`, script, "--tui-wrapped", ...process.argv.slice(2)], {
49
49
  stdio: ["inherit", "inherit", "pipe"], // 子 stderr pipe → tee;stdin/stdout 继承(TTY 原样)
50
- env: { ...process.env, THINCODER_TUI_WRAPPED: "1" }, // env 门——子内判定不包装——纯现逻辑(红线)
50
+ // argv 门(子内 bin 顶部自剥离 `--tui-wrapped`、判定不包装——纯现逻辑(红线));env 直传
51
51
  windowsHide: false,
52
52
  })
53
53
  child.stderr.on("data", (chunk) => { try { process.stderr.write(chunk) } catch { /* 终端已关——日志仍落 */ } note(chunk) }) // ② tee 双写:终端实时 + 日志