thincoder 0.12.60 → 0.12.62

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 (171) hide show
  1. package/CHANGELOG.md +49 -2
  2. package/README.md +8 -6
  3. package/bin/thincoder.mjs +38 -124
  4. package/package.json +3 -2
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +38 -17
  7. package/src/acp.mjs +19 -6
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +36 -134
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +124 -329
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +3 -2
  17. package/src/agent/completion.mjs +1 -1
  18. package/src/agent/dispatch.mjs +47 -12
  19. package/src/agent/helpers.mjs +71 -13
  20. package/src/agent/record-results.mjs +13 -5
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +24 -7
  23. package/src/agent/setup-reminders.mjs +16 -9
  24. package/src/agent/setup.mjs +92 -128
  25. package/src/agent/spawn-child.mjs +43 -11
  26. package/src/agent-tools/advisor-async.mjs +70 -180
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +69 -20
  29. package/src/agent-tools/async-settle.mjs +13 -0
  30. package/src/agent-tools/batch-segment.mjs +195 -0
  31. package/src/agent-tools/consult.mjs +28 -10
  32. package/src/agent-tools/design-token.mjs +14 -1
  33. package/src/agent-tools/digest-budget.mjs +76 -0
  34. package/src/agent-tools/eng.mjs +3 -3
  35. package/src/agent-tools/escalate-async.mjs +22 -13
  36. package/src/agent-tools/read-history.mjs +31 -6
  37. package/src/agent-tools/review-streak.mjs +93 -0
  38. package/src/agent-tools/settings.mjs +130 -17
  39. package/src/agent-tools/subagent-actions.mjs +18 -6
  40. package/src/agent-tools/subagent-async.mjs +66 -14
  41. package/src/agent-tools/subagent-panel.mjs +22 -15
  42. package/src/agent-tools/subagent-run.mjs +10 -7
  43. package/src/agent-tools/subagent-scheduler.mjs +57 -8
  44. package/src/agent-tools/subagent-spawn.mjs +69 -16
  45. package/src/agent-tools/subagent.mjs +175 -49
  46. package/src/agent-tools/verify.mjs +13 -34
  47. package/src/agent-tools.mjs +1 -0
  48. package/src/agent.mjs +42 -21
  49. package/src/cli/distill-command.mjs +2 -2
  50. package/src/cli/make-agent.mjs +23 -7
  51. package/src/cli/memory-command.mjs +2 -2
  52. package/src/cli/setup-wizard.mjs +29 -9
  53. package/src/completions.mjs +114 -0
  54. package/src/config-migrate.mjs +70 -0
  55. package/src/config.mjs +132 -63
  56. package/src/context.mjs +12 -1
  57. package/src/conventions.mjs +223 -0
  58. package/src/crash-reports.mjs +35 -8
  59. package/src/expand-home.mjs +16 -0
  60. package/src/generate-title.mjs +9 -4
  61. package/src/heap-watch.mjs +88 -0
  62. package/src/hooks.mjs +7 -3
  63. package/src/ledger.mjs +227 -0
  64. package/src/memory/code-index.mjs +9 -3
  65. package/src/memory/code-sync.mjs +77 -36
  66. package/src/memory/core.mjs +8 -9
  67. package/src/memory/delete.mjs +2 -0
  68. package/src/memory/docs.mjs +17 -11
  69. package/src/memory/file-walk.mjs +109 -0
  70. package/src/memory/scan.mjs +95 -0
  71. package/src/memory/schema.mjs +15 -3
  72. package/src/model-ref.mjs +66 -0
  73. package/src/model-specs.mjs +42 -8
  74. package/src/prompt-overlays.mjs +73 -16
  75. package/src/prompts/advisor-design.md +19 -9
  76. package/src/prompts/advisor-round1.md +8 -2
  77. package/src/prompts/advisor-round2.md +14 -3
  78. package/src/prompts/advisor-round3.md +14 -3
  79. package/src/prompts/common.md +115 -0
  80. package/src/prompts/consult-base.md +2 -0
  81. package/src/prompts/discipline-engineering.md +258 -0
  82. package/src/prompts/discipline-normal.md +185 -0
  83. package/src/prompts/persona-coder.md +21 -0
  84. package/src/prompts/persona-eng-coder.md +37 -0
  85. package/src/prompts/persona-eng-designer.md +60 -0
  86. package/src/prompts/persona-engineering.md +55 -0
  87. package/src/prompts/persona-explore.md +15 -0
  88. package/src/prompts/persona-normal.md +27 -0
  89. package/src/prompts/persona-plan.md +26 -0
  90. package/src/provider/anthropic.mjs +4 -4
  91. package/src/provider/core.mjs +13 -32
  92. package/src/provider/errors.mjs +26 -1
  93. package/src/provider/google.mjs +5 -6
  94. package/src/provider/index.mjs +2 -1
  95. package/src/provider/list-models.mjs +93 -0
  96. package/src/provider/rate.mjs +2 -1
  97. package/src/provider/responses.mjs +5 -3
  98. package/src/provider/sse.mjs +3 -4
  99. package/src/proxy.mjs +9 -14
  100. package/src/session-gc.mjs +9 -2
  101. package/src/session-guard.mjs +12 -0
  102. package/src/session-segments.mjs +100 -0
  103. package/src/session-slots.mjs +10 -2
  104. package/src/session-store.mjs +441 -0
  105. package/src/session.mjs +137 -99
  106. package/src/text-budget.mjs +46 -0
  107. package/src/token-ttl.mjs +2 -1
  108. package/src/tools/{system.mjs → bash.mjs} +6 -243
  109. package/src/tools/file.mjs +30 -10
  110. package/src/tools/git.md +1 -1
  111. package/src/tools/git.mjs +15 -34
  112. package/src/tools/index.mjs +4 -2
  113. package/src/tools/ops.mjs +20 -7
  114. package/src/tools/question.md +1 -0
  115. package/src/tools/question.mjs +26 -0
  116. package/src/tools/read.md +1 -1
  117. package/src/tools/read_image.md +1 -1
  118. package/src/tools/search.mjs +236 -0
  119. package/src/traces/trace-store.mjs +195 -64
  120. package/src/tui/agent-turn.mjs +32 -13
  121. package/src/tui/ansi.mjs +2 -0
  122. package/src/tui/clipboard.mjs +7 -1
  123. package/src/tui/cmd-advisor.mjs +3 -2
  124. package/src/tui/cmd-clear.mjs +2 -0
  125. package/src/tui/cmd-config.mjs +108 -37
  126. package/src/tui/cmd-eng.mjs +11 -27
  127. package/src/tui/cmd-exit.mjs +6 -8
  128. package/src/tui/cmd-model.mjs +14 -12
  129. package/src/tui/cmd-new.mjs +5 -1
  130. package/src/tui/cmd-reindex.mjs +7 -0
  131. package/src/tui/cmd-session.mjs +8 -4
  132. package/src/tui/cmd-submodel.mjs +8 -5
  133. package/src/tui/cmd-undo.mjs +4 -3
  134. package/src/tui/display-budget.mjs +184 -0
  135. package/src/tui/index.mjs +69 -40
  136. package/src/tui/key-handler-search.mjs +9 -1
  137. package/src/tui/key-handler.mjs +61 -17
  138. package/src/tui/key-modes.mjs +86 -8
  139. package/src/tui/layout.mjs +18 -10
  140. package/src/tui/ledger-surface.mjs +69 -0
  141. package/src/tui/model-catalog.mjs +89 -0
  142. package/src/tui/model-picker.mjs +498 -0
  143. package/src/tui/mouse.mjs +47 -10
  144. package/src/tui/pickers.mjs +28 -410
  145. package/src/tui/render-frame.mjs +38 -16
  146. package/src/tui/render-loop.mjs +2 -0
  147. package/src/tui/render-segments.mjs +5 -19
  148. package/src/tui/render.mjs +37 -5
  149. package/src/tui/slash-commands.mjs +2 -2
  150. package/src/tui/startup.mjs +45 -13
  151. package/src/tui/subagent-blocks.mjs +70 -90
  152. package/src/tui/subagent-children.mjs +125 -67
  153. package/src/tui/subagent-freeze.mjs +48 -45
  154. package/src/tui/subagent-panel.mjs +21 -66
  155. package/src/tui/suspension-drive.mjs +31 -83
  156. package/src/tui/tool-args.mjs +9 -4
  157. package/src/tui/tool-display.mjs +20 -5
  158. package/src/tui/tool-events.mjs +76 -30
  159. package/src/tui/tui-lifecycle.mjs +27 -7
  160. package/src/tui/wizard.mjs +52 -18
  161. package/src/tui/wrapped-spawn.mjs +54 -0
  162. package/src/prompts/coder.md +0 -13
  163. package/src/prompts/discipline.md +0 -84
  164. package/src/prompts/eng-coder.md +0 -19
  165. package/src/prompts/engineering-sub.md +0 -14
  166. package/src/prompts/engineering.md +0 -87
  167. package/src/prompts/explore.md +0 -12
  168. package/src/prompts/main.md +0 -34
  169. package/src/prompts/methodology-template.md +0 -38
  170. package/src/prompts/plan.md +0 -9
  171. package/src/prompts/system.md +0 -44
@@ -0,0 +1,184 @@
1
+ /**
2
+ * display-budget.mjs — TUI 显示层额度(TUI-OOM-ROOTCAUSE 批——TUI.md §15.3)。
3
+ *
4
+ * 职责(§15.3.2):常量单源(D-TB4)+ 行/载体计长(`lineChars`)+ `state.lines` 总量
5
+ * 对账(`syncLineBudget`)。纯函数本体(`capText` / `appendCappedText`)住
6
+ * `src/text-budget.mjs`(零依赖)——与 agent 侧捕获共用(AGENT-LOOP.md §23.3.1,D2 单源);
7
+ * 本模块只承载 TUI 面常量与对账。
8
+ *
9
+ * 依赖方向:本模块只引 `./ansi.mjs` + `../text-budget.mjs`(叶子——不引 subagent 族,
10
+ * 冻结锚点平移由调用方经 `onTrim` 注入,避环)。
11
+ *
12
+ * 计量口径 = UTF-16 码元(`String.length`——TUI.md §15.2 表 1);行数维(5000 行环 /
13
+ * 500 行块环 / 200 条目环)**原样保留**,字符维为第二维(D-TB5——既有 N5/N6 语义不动)。
14
+ */
15
+ import { C } from "./ansi.mjs"
16
+ import { capText, appendCappedText, fillMarker } from "../text-budget.mjs"
17
+
18
+ export { capText }
19
+
20
+ // ─── 常量(§15.3.1——单源;数值改动一处生效)────────────────────────────────
21
+
22
+ /** 单行(pushLine/pushLabel/恢复行/流式 flush 行)字符额度。 */
23
+ export const LINE_MAX_CHARS = 64_000
24
+ /** 工具块 `argsJson` 总量。 */
25
+ export const ARGS_JSON_MAX_CHARS = 24_000
26
+ /** 工具块 `result` 行数组总量(与既有 400 行双维)。 */
27
+ export const TOOL_RESULT_MAX_CHARS = 64_000
28
+ /** 输出环单条目(超出:尾截断 + 标记)。 */
29
+ export const TOOL_OUTPUT_ENTRY_MAX_CHARS = 8_000
30
+ /** 输出环总量(超出:与既有 200 条目环同款丢最旧)。 */
31
+ export const TOOL_OUTPUT_TOTAL_MAX_CHARS = 128_000
32
+ /** 子代理块单环(与 500 显示行双维;裁最旧——省略标记 N6 语义不变)。 */
33
+ export const SUB_BLOCK_CHAR_LIMIT = 128_000
34
+ /** 评审载体(`_advisorBlocks` 累积 + `_frozenAdvisor`):头 32K + 尾 96K + 中段标记。 */
35
+ export const ADVISOR_TEXT_MAX_CHARS = 128_000
36
+ /** 流式缓冲(`state.streaming` / `state.reasoning`)累积额度。 */
37
+ export const STREAM_MAX_CHARS = 256_000
38
+ /** `state.lines` 全部行与载体文本总量(超出:与 5000 行环同款裁头 1000 行 + 收据行)。 */
39
+ export const LINES_CHAR_BUDGET = 2_000_000
40
+ /** 搜索匹配计数上限(超出截断 + 提示行)。 */
41
+ export const SEARCH_MATCH_CAP = 10_000
42
+
43
+ // ─── 标记形态(逐字——§15.3.2 进测试断言)──────────────────────────────────
44
+
45
+ export const LINE_TRUNC_MARKER = "… [line truncated: N chars omitted]"
46
+ export const MIDDLE_TRUNC_MARKER = "… [middle truncated: N chars omitted]"
47
+
48
+ /** 评审/流式额度头尾分配(头 32K + 尾 96K——保裁决尾部;与常量同基数)。 */
49
+ export const ADVISOR_CAP_OPTS = {
50
+ hard: ADVISOR_TEXT_MAX_CHARS,
51
+ head: 32_000,
52
+ tail: 96_000,
53
+ marker: MIDDLE_TRUNC_MARKER,
54
+ }
55
+ /** 流式累积额度(头/尾各半——flush 行再受 LINE_MAX_CHARS)。 */
56
+ export const STREAM_CAP_OPTS = {
57
+ hard: STREAM_MAX_CHARS,
58
+ head: Math.floor(STREAM_MAX_CHARS / 4),
59
+ tail: Math.floor(STREAM_MAX_CHARS / 2),
60
+ marker: MIDDLE_TRUNC_MARKER,
61
+ }
62
+ /** 行截断(单行巨内容场景):保留头部至 `LINE_MAX_CHARS` + 尾标记(行尾截断语义)。 */
63
+ export function capLine(text) {
64
+ return capText(text, {
65
+ max: LINE_MAX_CHARS,
66
+ keepHead: LINE_MAX_CHARS,
67
+ keepTail: 0,
68
+ marker: LINE_TRUNC_MARKER,
69
+ })
70
+ }
71
+
72
+ /** TUI 侧流式累积(alias——本体 text-budget.appendCappedText,D2 单源)。 */
73
+ export function appendCapped(prev, add, opts) {
74
+ return appendCappedText(prev, add, opts)
75
+ }
76
+
77
+ /** 评审文本单次截断(§15.3.1:头 32K + 尾 96K 保裁决尾部 + 中段标记——与累积侧同源)。 */
78
+ export function capAdvisorText(text) {
79
+ return capText(text, { max: ADVISOR_TEXT_MAX_CHARS, keepHead: 32_000, keepTail: 96_000, marker: MIDDLE_TRUNC_MARKER })
80
+ }
81
+
82
+ /**
83
+ * 行数组总量额度(尾截断 + 标记;首行保真——ARGS_JSON/TOOL_RESULT 双维用)。
84
+ * 行放不下时:可放部分 → 本行头截断;无空间 → 直接以标记行收尾。
85
+ * @returns {string[]} 原数组或(截断 + 标记行)新数组
86
+ */
87
+ export function capLines(lines, max, marker = MIDDLE_TRUNC_MARKER) {
88
+ const arr = Array.isArray(lines) ? lines : []
89
+ if (arr.length === 0) return arr
90
+ let total = 0
91
+ for (const s of arr) total += typeof s === "string" ? s.length : 0
92
+ if (total <= max) return arr
93
+ const out = []
94
+ let used = 0
95
+ for (const s of arr) {
96
+ const str = typeof s === "string" ? s : String(s ?? "")
97
+ const remain = max - used
98
+ if (str.length <= remain) { out.push(str); used += str.length; continue }
99
+ if (remain > 0) {
100
+ const kept = str.slice(0, remain)
101
+ out.push(kept)
102
+ used += kept.length
103
+ }
104
+ break
105
+ }
106
+ out.push(fillMarker(marker, total - used))
107
+ return out
108
+ }
109
+
110
+ // ─── 计长与总量对账(§15.3.2 / §15.3.4)───────────────────────────────────
111
+
112
+ const arrChars = (a) => {
113
+ if (!Array.isArray(a)) return 0
114
+ let n = 0
115
+ for (const s of a) n += typeof s === "string" ? s.length : 0
116
+ return n
117
+ }
118
+
119
+ /** 一行 + 其 `_toolBlock`/`_frozenSubTask`/`_frozenAdvisor` 字段计长(单一口径)。 */
120
+ export function lineChars(l) {
121
+ if (!l) return 0
122
+ let n = typeof l.text === "string" ? l.text.length : 0
123
+ const b = l._toolBlock
124
+ if (b) {
125
+ n += typeof b.argsSummary === "string" ? b.argsSummary.length : 0
126
+ n += typeof b.summary === "string" ? b.summary.length : 0
127
+ n += arrChars(b.argsJson) + arrChars(b.output) + arrChars(b.result)
128
+ }
129
+ if (l._frozenSubTask) n += l._frozenSubTask._charCount ?? 0
130
+ if (typeof l._frozenAdvisor === "string") n += l._frozenAdvisor.length
131
+ return n
132
+ }
133
+
134
+ /** 行增量记账(幂等——行对象上存上次计入值 `_budgetChars`;载体原地变更后重调即自愈)。 */
135
+ export function accountLine(state, l) {
136
+ if (!l) return
137
+ const prev = Number.isFinite(l._budgetChars) ? l._budgetChars : 0
138
+ const next = lineChars(l)
139
+ state._linesChars = Math.max(0, (state._linesChars ?? 0) + (next - prev))
140
+ l._budgetChars = next
141
+ }
142
+
143
+ /** 直算对账(双算法对照 / 初始化 / 漂移自愈——测试直算同源)。 */
144
+ export function accountAll(state) {
145
+ let total = 0
146
+ for (const l of state.lines ?? []) {
147
+ const n = lineChars(l)
148
+ l._budgetChars = n
149
+ total += n
150
+ }
151
+ state._linesChars = total
152
+ return total
153
+ }
154
+
155
+ /**
156
+ * `state.lines` 总量对账(§15.3.3 落点表末行):超 `LINES_CHAR_BUDGET` → 与 5000 行环
157
+ * 同款裁头 1000 行 + 收据行;`onTrim(state, removedCount)` 由调用方注入冻结锚点平移
158
+ * (display-budget 不引 subagent 族——避环)。幂等(未超限零动作)。
159
+ * @returns {number} 本轮裁掉的行数
160
+ */
161
+ export function syncLineBudget(state, { pushLineLike = null, onTrim = null } = {}) {
162
+ const lines = state.lines
163
+ if (!Array.isArray(lines)) return 0
164
+ if (!Number.isFinite(state._linesChars)) accountAll(state)
165
+ let removed = 0
166
+ while (state._linesChars > LINES_CHAR_BUDGET && lines.length > 1) {
167
+ const take = Math.min(1000, lines.length - 1)
168
+ const dropped = lines.splice(0, take)
169
+ let freed = 0
170
+ for (const l of dropped) freed += lineChars(l)
171
+ state._linesChars = Math.max(0, state._linesChars - freed)
172
+ removed += take
173
+ const receipt = `... [earlier messages trimmed — ${lines.length} lines remaining]`
174
+ if (typeof pushLineLike === "function") pushLineLike(receipt)
175
+ else {
176
+ const line = { text: receipt, color: C.dim }
177
+ line._budgetChars = receipt.length
178
+ lines.unshift(line)
179
+ state._linesChars += receipt.length
180
+ }
181
+ if (typeof onTrim === "function") onTrim(state, take)
182
+ }
183
+ return removed
184
+ }
package/src/tui/index.mjs CHANGED
@@ -25,20 +25,22 @@ import { closeAllMcp } from "../mcp.mjs"
25
25
  import { ansi, C } from "./ansi.mjs"
26
26
  import { createRenderLoop } from "./render-loop.mjs"
27
27
  import { makeDimsState } from "./dims.mjs"
28
- import { SLASH_COMMANDS, SLASH_ALIASES, createSlashCommands } from "./slash-commands.mjs"
28
+ import { SLASH_COMMANDS, createSlashCommands } from "./slash-commands.mjs"
29
29
  import { createWizard } from "./wizard.mjs"
30
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"
34
34
  import { pasteClipboardImage as pasteClipboardImageImpl, insertPastedText, translateShiftEnter, stripKeyboardProtocol } from "./clipboard.mjs"
35
- import { parseMouseClicks, handleWheel, createMouseDispatch } from "./mouse.mjs"
35
+ import { parseMouseClicks, handleWheel, createMouseDispatch, mouseOob } from "./mouse.mjs"
36
36
  import { runAgentTurn } from "./agent-turn.mjs"
37
- import { createKeyHandler, convMaxScroll } from "./key-handler.mjs"
37
+ import { createKeyHandler, convMaxScroll, clearAttention } from "./key-handler.mjs"
38
38
  import { showStartup, backgroundIndex, createLoadOlder } from "./startup.mjs"
39
39
  import { shiftFreezeAnchors } from "./subagent-blocks.mjs"
40
+ import { capLine, accountLine, accountAll, syncLineBudget } from "./display-budget.mjs"
40
41
  import { createConfigHelpers } from "./config-helpers.mjs"
41
42
  import { createUpdateNotice, pendingNoticeReady } from "./update-notice.mjs"
43
+ import { startLedgerSurface } from "./ledger-surface.mjs"
42
44
 
43
45
  export { upgradeFailureText, pendingNoticeReady } from "./update-notice.mjs"
44
46
 
@@ -53,7 +55,12 @@ export async function promptProviderIfInvalid(agent, openModelPicker, pushLine)
53
55
  if (!(agent._providerInvalid || !agent.provider)) return false
54
56
  await openModelPicker()
55
57
  if (!agent.provider) {
56
- pushLine("未配置有效 provider,可用 /model 选择或 /provider 配置", C.warn)
58
+ // MODEL-MERGE-SESSION 引导 A(F-6):空槽 + defaultModel 未设 → 提示 /config 默认模型入口
59
+ // (index.mjs 本行为 D-S2 取消提示扩展——设计文件清单外——随批上报)
60
+ const hasProviders = (agent.providers?.length ?? 0) > 0
61
+ pushLine(hasProviders && !agent.config?.defaultModel
62
+ ? "尚未设置默认模型(config.defaultModel——新会话起点):/config → 默认模型 设置一次;/model 仅改本会话"
63
+ : "未配置有效 provider,可用 /model 选择或 /provider 配置", C.warn)
57
64
  }
58
65
  return true
59
66
  }
@@ -106,12 +113,14 @@ export async function startTUI(agent, opts = {}) {
106
113
  reasoning: "", // thinking stream buffer (dimmed display)
107
114
  completion: null, // Tab completion state { candidates, index }
108
115
 
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)
116
+ 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: [](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)
110
117
  currentTool: null, // currently executing tool name (shown in status bar)
111
118
  processingStarted: 0, // current turn start time (status bar timer)
112
119
  status: "Ready",
113
- queue: [], // queued messages while processing: [{ text }], auto-dequeued when current turn finishes
114
- interruptPrompt: null, // Ctrl+I interrupt message input: { text: "" } or null
120
+ ledger: { marker: null, warn: false, scannedAt: 0 }, // LEDGER-SURFACE(§2.30.3.4):L1 标记位——ledger-surface 写 / render-frame 读
121
+ queue: [], // 交接残项单容器 [{ text }](INPUT-LOCK:submit 不再排队——仅释放窗口兜底/挂起中止残余——回合尾队列循环续发,至多一条)
122
+ interruptPrompt: null, // Ctrl+I inject box(第 31 批——TUI-INPUT-BOX.md §8): { chars: string[], cursor: number } or null
123
+ attentionAwaiting: false, // 第 33 批(TUI §14.3(f)):回合结束等待输入——链尾置位 / 用户输入清位;不落盘、不进会话
115
124
  search: null, // Ctrl+F search mode: { query: "", matches: [{lineIndex, charIndex}], index: 0 } or null
116
125
  expandedBlocks: new Set(), // block hashes that are expanded (Enter toggles)
117
126
  foldEnabled: true, // global fold toggle — /fold on|off
@@ -121,13 +130,16 @@ export async function startTUI(agent, opts = {}) {
121
130
  _historyLoaded: 0, // messages loaded from the TAIL of _fullHistory
122
131
  _historyTotal: 0, // total messages in the restored session
123
132
  _hasOlder: false, // more earlier messages remain unloaded
124
- _agent: null, // §19.6 D-P1 面板镜像挂载点:startTUI 装配时置 agent 引用——subagent-blocks
125
- // syncPanelSnapshot 经此写 agent._panelSnapshot(块级状态变更即刷——subagent
126
- // action:"panel" 读——模型可查面板视图 + 门控补发冻结)。headless/VS Code 无此
127
- // 挂载 = 无镜像 → panel 动作恒降级池视图(CLI-only 完整能力——AC-P4)。
133
+ _linesChars: 0, // TUI-OOM-ROOTCAUSE(§15.3.2):state.lines 全部行与载体文本总量(字符账——唯一新增状态位)
134
+ _agent: null, // TUI state → agent 回指挂载点:startTUI 装配时置 agent 引用——
135
+ // ① SYNC-CANCEL ⏹ 门控读 state._agent._syncChildAborts(subagent-panel.mjs,2026-09-09);
136
+ // ② CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):agent._tuiState = state 反向挂载——
137
+ // action:"panel" 经 ctx.state(= agent._tuiState)**读时现算**面板块(computePanelBlocks
138
+ // ——subTasks 活值纯推导——单账本)。headless/VS Code 无此装配 = 无面板 → panel 动作
139
+ // 恒降级池视图(CLI-only 完整能力——AC-P4)。
128
140
  }
129
141
  state._agent = agent
130
- agent._panelSnapshot = [] // 镜像初始态 = 空面板(非"无镜像"——[] 与 undefined 区分)
142
+ agent._tuiState = state // F-3:面板视图现算通道(替代退役的手工面板镜像)
131
143
 
132
144
  // On session restore, if all tasks are completed, auto-collapse the todo panel (match runtime behavior)
133
145
  if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
@@ -160,6 +172,10 @@ export async function startTUI(agent, opts = {}) {
160
172
  let text = mousePending + utf8Decoder.decode(chunk, { stream: true })
161
173
  mousePending = ""
162
174
 
175
+ // 第 33 批(TUI §14.3(e) 鼠标点):stdin 数据到达即用户在场 ⇒ 清 attention 位
176
+ // (单点覆盖下方滚轮分支与 onMouseClick;键盘入口在 key-handler)。
177
+ clearAttention(state, render)
178
+
163
179
  // Bracketed paste: terminal wraps pasted text in \x1b[200~ ... \x1b[201~
164
180
  // Route pasted content to the active text target (question answer / input box) in one shot,
165
181
  // avoiding slow char-by-char keypress render — see insertPastedText in clipboard.mjs
@@ -209,6 +225,10 @@ export async function startTUI(agent, opts = {}) {
209
225
  for (const m of text.matchAll(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/g)) {
210
226
  const button = Number(m[1])
211
227
  if (button === 64 || button === 65) {
228
+ // F-3 sane-gate ③(RESIZE-MOUSE-LEAK-FIX):越界 wheel 禁止会话滚动——handleWheel
229
+ // 对越界返回未消费会穿到 fallback 滚动(mouse.mjs 内 gate 覆盖不到此路径)
230
+ const dims = state.dims ? state.dims.get() : { cols: process.stdout.columns || 80, rows: process.stdout.rows || 24 }
231
+ if (mouseOob(Number(m[2]), Number(m[3]), dims)) continue
212
232
  const consumed = handleWheel(mouseCtx(), button, Number(m[2]), Number(m[3]))
213
233
  if (!consumed) {
214
234
  if (button === 64) {
@@ -260,7 +280,11 @@ export async function startTUI(agent, opts = {}) {
260
280
  process.on("exit", cleanup)
261
281
 
262
282
  const pushLine = (text, color, kind) => {
263
- state.lines.push({ text, color, _kind: kind })
283
+ // TUI-OOM-ROOTCAUSE(TUI.md §15.3.1/§15.3.3):行额度单点——行文本过 capLine
284
+ // (LINE_MAX_CHARS——单行巨内容/无换行巨 chunk 的堵口)+ 总量账 + 预算对账。
285
+ const line = { text: capLine(text), color, _kind: kind }
286
+ state.lines.push(line)
287
+ accountLine(state, line)
264
288
  if (state.lines.length > 5000) {
265
289
  state.lines.splice(0, 1000)
266
290
  state.lines.unshift({ text: `... [earlier messages trimmed — ${state.lines.length} lines remaining]`, color: C.dim })
@@ -268,14 +292,24 @@ export async function startTUI(agent, opts = {}) {
268
292
  // 流位置)——不校正则池空补发冻结(freezeSubTaskLines splice)落点漂移;校正量
269
293
  // = 净位移(裁 1000 补 1 标记行 → −999,code review round1 #3)。
270
294
  shiftFreezeAnchors(state, 1000)
295
+ accountAll(state) // 行对象集合已变(splice/unshift 直写)——直算重对账
271
296
  }
297
+ syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
272
298
  render()
273
299
  }
274
300
 
275
301
  /** Message block label: blank line + label line. Breathing space between user/assistant messages */
276
302
  const pushLabel = (text, color) => {
277
- if (state.lines.length > 0) state.lines.push({ text: "", color: C.dim })
278
- state.lines.push({ text, color })
303
+ // 行额度同 pushLine(§15.3.1——恢复行/标签行同口径)
304
+ if (state.lines.length > 0) {
305
+ const blank = { text: "", color: C.dim }
306
+ state.lines.push(blank)
307
+ accountLine(state, blank)
308
+ }
309
+ const line = { text: capLine(text), color }
310
+ state.lines.push(line)
311
+ accountLine(state, line)
312
+ syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
279
313
  render()
280
314
  }
281
315
 
@@ -300,6 +334,13 @@ export async function startTUI(agent, opts = {}) {
300
334
  // 声明在此(render 已可用)——data 回调(滚轮分支)与 createKeyHandler ctx(PgUp)共用。
301
335
  const loadOlder = createLoadOlder({ agent, state, render })
302
336
 
337
+ // TUI-OOM-ROOTCAUSE(CRASH-REPORTS.md §8.3 订阅接线):堆预警行入对话流(pushLine 已 render)——
338
+ // 不新增 TUI 定时器(采样定时器住 heap-watch 模块,bin 入口武装);订阅失败不影响启动。
339
+ try {
340
+ const { onHeapWarn } = await import("../heap-watch.mjs")
341
+ onHeapWarn((line) => pushLine(line, C.warn))
342
+ } catch { /* 尽力面 */ }
343
+
303
344
  // Resize events are genuine dimension changes on every terminal (2026-08-31
304
345
  // simplification: the earlier settle-timer/double-confirm machinery was built
305
346
  // on the misdiagnosed ConPTY-stale hypothesis and even stalled drag-shrink).
@@ -314,6 +355,14 @@ export async function startTUI(agent, opts = {}) {
314
355
  async function submit() {
315
356
  const text = state.input.join("").trim()
316
357
  if (!text) return
358
+ // INPUT-LOCK 防御(C'——2026-09-09)+ INPUT-LOCK-BEHAVIOR-REVISED(2026-09-09——白名单
359
+ // 已删——busy 全拒):提交吞的门禁在 key-handler(busy Enter 已吞 + 提示,文本保留)——
360
+ // submit 只在非 busy 期达此;防御直呼/上游改动:busy 期拒绝(不清输入框不吞内容)。
361
+ if (state.processing) {
362
+ pushLine(`[主会话处理中 —— 消息未发送(回合结束后请重按 Enter)]`, C.warn)
363
+ render()
364
+ return
365
+ }
317
366
  state.input = []
318
367
  state.cursor = 0
319
368
  const wasInHistory = state.historyIndex !== -1
@@ -323,37 +372,14 @@ export async function startTUI(agent, opts = {}) {
323
372
  state.scroll = 0
324
373
  state._followTail = true // 2026-08-31 会诊 deepseek:新消息恢复跟随(注释曾承诺、实现缺漏)
325
374
 
326
- // Slash commands: handled locally, don't enter agent loop
375
+ // Slash commands: handled locally, don't enter agent loop. busy 期提交在 key-handler
376
+ // 门禁被吞(白名单已删——斜杠同吞——INPUT-LOCK-BEHAVIOR-REVISED)——submit 仅非 busy 期可达。
327
377
  if (text.startsWith("/")) {
328
- if (state.processing) {
329
- // While processing: allowlisted commands execute directly (they only touch
330
- // local TUI/agent config, never the in-flight turn); the rest are queued
331
- const cmd0 = text.split(/\s+/)[0].toLowerCase()
332
- const resolved0 = SLASH_ALIASES[cmd0] ?? cmd0
333
- const safeDuringProcessing = new Set(["/help", "/exit", "/model", "/submodel", "/shell", "/think", "/config", "/skills", "/mcp", "/goal", "/session"])
334
- if (safeDuringProcessing.has(resolved0)) {
335
- await handleSlash(text)
336
- render()
337
- } else {
338
- state.queue.push({ text })
339
- render()
340
- }
341
- return
342
- }
343
378
  await handleSlash(text)
344
379
  render()
345
380
  return
346
381
  }
347
382
 
348
- // While processing: queue for later, don't execute immediately.
349
- // The queue panel (renderQueue) already shows pending items — don't also
350
- // push to the conversation area, or the text scrolls up with streaming tokens.
351
- if (state.processing) {
352
- state.queue.push({ text })
353
- render()
354
- return
355
- }
356
-
357
383
  await turn(text)
358
384
  }
359
385
 
@@ -437,6 +463,9 @@ export async function startTUI(agent, opts = {}) {
437
463
  await promptProviderIfInvalid(agent, () => openModelPicker(), pushLine)
438
464
 
439
465
  showStartup({ agent, state, opts, pushLine, pushLabel, render, startWizard })
466
+ // LEDGER-SURFACE(§2.30.3.4):台账可见面——首扫(setImmediate)+ 周期;dispose 挂进程退出(:278 cleanup 先例)
467
+ const ledgerSurface = startLedgerSurface({ state, agent, pushLine, render })
468
+ process.on("exit", () => ledgerSurface.dispose())
440
469
  backgroundIndex({ agent, state, render })
441
470
 
442
471
  // Check for updates (non-blocking, after startup screen)——实现 update-notice.mjs
@@ -1,20 +1,28 @@
1
- /** Perform search on state.lines, update state.search.matches and clamp index */
1
+ /** Perform search on state.lines, update state.search.matches and clamp index
2
+ * TUI-OOM-ROOTCAUSE(TUI.md §15.3.1 SEARCH_MATCH_CAP):匹配计数上限——超限截断 +
3
+ * `state.search.capped` 提示标志(渲染面提示行;次要无界面)。
4
+ */
5
+ import { SEARCH_MATCH_CAP } from "./display-budget.mjs"
6
+
2
7
  export function performSearch(state) {
3
8
  if (!state.search) return
4
9
  const query = state.search.query.toLowerCase()
5
10
  state.search.matches = []
11
+ state.search.capped = false
6
12
  // Clear old highlights
7
13
  state.lines.forEach(l => delete l._searchMatches)
8
14
 
9
15
  if (!query) { state.search.index = 0; return }
10
16
 
11
17
  state.lines.forEach((line, lineIndex) => {
18
+ if (state.search.matches.length >= SEARCH_MATCH_CAP) { state.search.capped = true; return }
12
19
  const text = (line.text || "").toLowerCase()
13
20
  let charIndex = text.indexOf(query)
14
21
  if (charIndex !== -1) {
15
22
  line._searchMatches = []
16
23
  }
17
24
  while (charIndex !== -1) {
25
+ if (state.search.matches.length >= SEARCH_MATCH_CAP) { state.search.capped = true; break }
18
26
  state.search.matches.push({ lineIndex, charIndex })
19
27
  line._searchMatches.push(charIndex)
20
28
  charIndex = text.indexOf(query, charIndex + 1)
@@ -1,10 +1,22 @@
1
1
  import { ansi, C } from "./ansi.mjs"
2
2
  import { readClipboardText, insertPastedText } from "./clipboard.mjs"
3
- import { computeLayout } from "./layout.mjs"
3
+ import { computeLayout, inputContentWidth } from "./layout.mjs"
4
+ import { moveCursorVertical } from "./render.mjs"
4
5
  import { handleSearchKey } from "./key-handler-search.mjs"
5
6
  import { countConvLines } from "./render-conversation.mjs"
6
7
  import { handlePermissionMode, handleQuestionMode, handleInterruptMode } from "./key-modes.mjs"
7
8
 
9
+ /** 第 33 批(TUI §14.3(e)——attention 清位):输入即在场——仅当原值为真时置假 + `render()`
10
+ * (已假则零副作用、零重绘)。键盘入口与本文件(`onKeypress`)与鼠标输入路径(index.mjs
11
+ * stdin data 处理器)两点共用;blocked 两态无需清位(实时派生)。
12
+ * @returns {boolean} 本次是否发生清位 */
13
+ export function clearAttention(state, render) {
14
+ if (!state.attentionAwaiting) return false
15
+ state.attentionAwaiting = false
16
+ render()
17
+ return true
18
+ }
19
+
8
20
  /** Current conversation max scroll offset (display lines beyond the visible panel). */
9
21
  export function convMaxScroll(state) {
10
22
  const d = state.dims ? state.dims.get() : {} // Single source (ConPTY instability, 2026-08-30) — cached dims
@@ -24,6 +36,9 @@ export function createKeyHandler(ctx) {
24
36
  const { agent, state, render, popPicker, renderPickerLines, handleSlash, handleTab, submit, pasteClipboardImage, wizardChooseProvider, wizardSubmitText, cancelWizard, wizardProviderItems, renderWizard, pushLine, cleanup, showPicker, loadOlder } = ctx
25
37
 
26
38
  return function onKeypress(str, key = {}) {
39
+ // 第 33 批(TUI §14.3(e) 键盘点):模态分派**之前**——任意按键 = 用户在场 ⇒ 清 attention
40
+ // 位(仅复位呈现态字段,按键语义 / 模态判定 / busy 门禁零改)。
41
+ clearAttention(state, render)
27
42
  // permission confirm: y/n/a (a = approve + AUTO ON); batch (§16 D-B1): a/o/n (Esc = deny)
28
43
  // ——模态实现 key-modes.mjs handlePermissionMode(2026-09-03 D-S4)
29
44
  if (handlePermissionMode(str, key, { state, agent, pushLine, render })) return
@@ -59,12 +74,13 @@ export function createKeyHandler(ctx) {
59
74
  state.suspended
60
75
  if (hasStopTarget) {
61
76
  // 当前回合平 abort(无 interrupt——命中 agent.mjs 回合收尾清池分支——全停)
62
- if (state.processing && state.controller) state.controller.abort()
77
+ // §20.3 站点 #11(第 24 批):整批 / 会话级停 = stop(reason 载荷)
78
+ if (state.processing && state.controller) state.controller.abort({ abortTrigger: "stop", abortDetail: "session-stop" })
63
79
  // 会话 abort 集合 = 链条内全部 controller(含 Ctrl+I/ContinueError 重建的旧
64
80
  // controller——children 不逃逸,round1 偏差 #3)。挂起态才标记 _suspAborted +
65
81
  // 唤醒 driver(driver 收尾清池)——非挂起语境置位会粘滞阻塞未来挂起会话重入
66
82
  // (round2 偏差 #1 语义)。
67
- for (const c of agent._sessionAbortAll ?? (agent._sessionAbort ? [agent._sessionAbort] : [])) c?.abort()
83
+ for (const c of agent._sessionAbortAll ?? (agent._sessionAbort ? [agent._sessionAbort] : [])) c?.abort({ abortTrigger: "stop", abortDetail: "session-stop" })
68
84
  if (state.suspended) {
69
85
  state._suspAborted = true
70
86
  state._suspWake?.()
@@ -159,7 +175,8 @@ export function createKeyHandler(ctx) {
159
175
  // Ctrl+I (or Tab during processing): interrupt and inject a message
160
176
  if ((key.ctrl && !key.alt && key.name === "i") || (key.name === "tab" && state.processing && !state.interruptPrompt)) {
161
177
  if (state.processing && state.controller && !state.interruptPrompt) {
162
- state.interruptPrompt = { text: "" }
178
+ // 注入框状态模型(第 31 批——TUI-INPUT-BOX.md §1 不变量 9):{ chars, cursor },空态 = { chars: [], cursor: 0 }
179
+ state.interruptPrompt = { chars: [], cursor: 0 }
163
180
  render()
164
181
  }
165
182
  return
@@ -263,17 +280,19 @@ export function createKeyHandler(ctx) {
263
280
  }
264
281
 
265
282
  if (state.processing) {
266
- // during processing, allow input (queued), but block arrow-key history and Tab completion
267
- if (key.name === "tab" || key.name === "up" || key.name === "down") return
268
- // Ctrl+D: remove last item from queue
269
- if (key.ctrl && key.name === "d") {
270
- if (state.queue.length > 0) {
271
- state.queue.pop()
272
- render()
273
- }
283
+ // busy 门禁(INPUT-LOCK C'——2026-09-09 + INPUT-LOCK-BEHAVIOR-REVISED——2026-09-09):
284
+ // processing(含 digest——单一判据)输入不禁——吞提交不吞字符(打字照常回显);Enter
285
+ // 提交吞 + busy 提示——斜杠同禁发(白名单已删——/exit 也发不出——退出靠 Ctrl+C 终端
286
+ // 层通道——门禁前不误伤);空 Enter 静默(text 非空才吞);多行换行
287
+ // (meta/enter——编辑)照常放行。**第 31 批**:↑↓ 不再在此全屏蔽——分流下沉到下方
288
+ // 三规则块(竖移放行 / 历史导航禁;TUI-INPUT-BOX.md §3);Tab 仍吞(死条件在案——原样保留)。
289
+ if (key.name === "tab") return
290
+ const isSend = (key.name === "return" && !key.meta) || (str === "\r" && !key.meta)
291
+ if (isSend && state.input.join("").trim()) {
292
+ pushLine(`[主会话处理中 —— 消息未发送(回合结束后请重按 Enter)]`, C.warn)
293
+ render()
274
294
  return
275
295
  }
276
- // remaining printable characters go into input box normally
277
296
  }
278
297
 
279
298
  // Tab: slash-command completion (cycle candidates); other input ignored (\t would blow up input box, never inserted directly)
@@ -291,7 +310,24 @@ export function createKeyHandler(ctx) {
291
310
  return
292
311
  }
293
312
 
294
- // input history
313
+ // ↑/↓ 三规则(TUI-INPUT-BOX.md §3——第 31 批):① 翻历史中恒历史(多行条目不竖移)
314
+ // ② 竖移优先(可视行口径——折行与 \n 同权;显示列保持 + 短行端钳制)③ 边界回落历史(↑)
315
+ // /无动作(↓)。processing 期:竖移放行(纯编辑——与字符/退格/←→ 同权)、历史导航禁
316
+ // (规则 ①/③ 的历史分支吞——不进入、不切换)。
317
+ if (key.name === "up" || key.name === "down") {
318
+ if (state.historyIndex === -1) { // 规则 ① 优先——翻历史中不竖移
319
+ const cols = (state.dims?.get() ?? {}).cols ?? (process.stdout.columns || 80)
320
+ const next = moveCursorVertical(state.input, state.cursor, inputContentWidth(cols), key.name)
321
+ if (next !== null) {
322
+ state.cursor = next // 规则 ②:竖移(草稿/历史指针零涉——D-31.6)
323
+ render()
324
+ return
325
+ }
326
+ }
327
+ if (state.processing) return // 规则 ①/③ 的历史分支吞——processing 期历史导航禁
328
+ }
329
+
330
+ // input history(规则 ① 恒历史 + 规则 ③ 边界回落——历史语义零改)
295
331
  if (key.name === "up") {
296
332
  if (state.history.length) {
297
333
  // Save current input as draft when entering history mode (historyIndex === -1).
@@ -372,15 +408,23 @@ export function createKeyHandler(ctx) {
372
408
  render()
373
409
  } else {
374
410
  const text = state.input.join("").trim()
375
- // §17 D-S5/F3/F7 + 偏差 #1:挂起态(suspended 或释放窗口 _suspPending)Enter =
376
- // 新回合输入(非打断)——入 pendingInput 由挂起会话调度;输入框零干扰(F3)。
411
+ // §17 D-S5/F3/F7 + 偏差 #1 + INPUT-LOCK 单槽化(F-6——2026-09-09):挂起态
412
+ // (suspended 或释放窗口 _suspPending)Enter = 新回合输入(非打断)——填
413
+ // pendingInput 单槽(至多一条待交接——busy 提交吞在 L265 门禁先拦,digest 期
414
+ // 不会到这——仅挂起空闲/释放窗口触达)由挂起会话调度;输入框零干扰(F3)。
415
+ // 槽满(竞态防御——不覆盖不丢失)→ 吞 + 提示,文本保留在输入框。
377
416
  if ((state.suspended || state._suspPending) && text && !text.startsWith("/")) {
417
+ state.pendingInput ??= []
418
+ if (state.pendingInput.length > 0) {
419
+ pushLine(`[主会话处理中 —— 已有一条消息待发送,请等其处理完成后再发送]`, C.warn)
420
+ render()
421
+ return
422
+ }
378
423
  state.input = []
379
424
  state.cursor = 0
380
425
  state.history.push(text)
381
426
  state.historyIndex = -1
382
427
  state._draft = null
383
- state.pendingInput ??= []
384
428
  state.pendingInput.push(text)
385
429
  state._suspWake?.()
386
430
  render()