berry-agent-sdk 0.1.0-alpha.2

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 (263) hide show
  1. package/README.md +78 -0
  2. package/dist/packages/berry-agent-sdk/src/client.d.ts +6 -0
  3. package/dist/packages/berry-agent-sdk/src/client.js +59 -0
  4. package/dist/packages/berry-agent-sdk/src/client.js.map +1 -0
  5. package/dist/packages/berry-agent-sdk/src/http.d.ts +24 -0
  6. package/dist/packages/berry-agent-sdk/src/http.js +232 -0
  7. package/dist/packages/berry-agent-sdk/src/http.js.map +1 -0
  8. package/dist/packages/berry-agent-sdk/src/index.d.ts +25 -0
  9. package/dist/packages/berry-agent-sdk/src/index.js +22 -0
  10. package/dist/packages/berry-agent-sdk/src/index.js.map +1 -0
  11. package/dist/packages/berry-agent-sdk/src/stdio.d.ts +23 -0
  12. package/dist/packages/berry-agent-sdk/src/stdio.js +238 -0
  13. package/dist/packages/berry-agent-sdk/src/stdio.js.map +1 -0
  14. package/dist/packages/berry-agent-sdk/src/types.d.ts +117 -0
  15. package/dist/packages/berry-agent-sdk/src/types.js +19 -0
  16. package/dist/packages/berry-agent-sdk/src/types.js.map +1 -0
  17. package/dist/src/agent/events.d.ts +11 -0
  18. package/dist/src/agent/events.js +2 -0
  19. package/dist/src/agent/events.js.map +1 -0
  20. package/dist/src/agent/index.d.ts +13 -0
  21. package/dist/src/agent/index.js +3 -0
  22. package/dist/src/agent/index.js.map +1 -0
  23. package/dist/src/agent/loop.d.ts +19 -0
  24. package/dist/src/agent/loop.js +140 -0
  25. package/dist/src/agent/loop.js.map +1 -0
  26. package/dist/src/agent/queue.d.ts +105 -0
  27. package/dist/src/agent/queue.js +120 -0
  28. package/dist/src/agent/queue.js.map +1 -0
  29. package/dist/src/agent/stream.d.ts +21 -0
  30. package/dist/src/agent/stream.js +80 -0
  31. package/dist/src/agent/stream.js.map +1 -0
  32. package/dist/src/agent/tools-batch.d.ts +49 -0
  33. package/dist/src/agent/tools-batch.js +222 -0
  34. package/dist/src/agent/tools-batch.js.map +1 -0
  35. package/dist/src/agent/types.d.ts +104 -0
  36. package/dist/src/agent/types.js +9 -0
  37. package/dist/src/agent/types.js.map +1 -0
  38. package/dist/src/channels/ask-queue.d.ts +44 -0
  39. package/dist/src/channels/ask-queue.js +83 -0
  40. package/dist/src/channels/ask-queue.js.map +1 -0
  41. package/dist/src/channels/commands.d.ts +56 -0
  42. package/dist/src/channels/commands.js +130 -0
  43. package/dist/src/channels/commands.js.map +1 -0
  44. package/dist/src/channels/engine/cell.d.ts +55 -0
  45. package/dist/src/channels/engine/cell.js +144 -0
  46. package/dist/src/channels/engine/cell.js.map +1 -0
  47. package/dist/src/channels/engine/diff.d.ts +3 -0
  48. package/dist/src/channels/engine/diff.js +144 -0
  49. package/dist/src/channels/engine/diff.js.map +1 -0
  50. package/dist/src/channels/engine/engine.d.ts +151 -0
  51. package/dist/src/channels/engine/engine.js +417 -0
  52. package/dist/src/channels/engine/engine.js.map +1 -0
  53. package/dist/src/channels/engine/index.d.ts +22 -0
  54. package/dist/src/channels/engine/index.js +9 -0
  55. package/dist/src/channels/engine/index.js.map +1 -0
  56. package/dist/src/channels/engine/input-keys.d.ts +39 -0
  57. package/dist/src/channels/engine/input-keys.js +87 -0
  58. package/dist/src/channels/engine/input-keys.js.map +1 -0
  59. package/dist/src/channels/engine/input.d.ts +174 -0
  60. package/dist/src/channels/engine/input.js +597 -0
  61. package/dist/src/channels/engine/input.js.map +1 -0
  62. package/dist/src/channels/engine/memory-io.d.ts +46 -0
  63. package/dist/src/channels/engine/memory-io.js +72 -0
  64. package/dist/src/channels/engine/memory-io.js.map +1 -0
  65. package/dist/src/channels/engine/process-io.d.ts +23 -0
  66. package/dist/src/channels/engine/process-io.js +39 -0
  67. package/dist/src/channels/engine/process-io.js.map +1 -0
  68. package/dist/src/channels/engine/types.d.ts +202 -0
  69. package/dist/src/channels/engine/types.js +18 -0
  70. package/dist/src/channels/engine/types.js.map +1 -0
  71. package/dist/src/channels/engine/width.d.ts +23 -0
  72. package/dist/src/channels/engine/width.js +123 -0
  73. package/dist/src/channels/engine/width.js.map +1 -0
  74. package/dist/src/channels/index.d.ts +25 -0
  75. package/dist/src/channels/index.js +16 -0
  76. package/dist/src/channels/index.js.map +1 -0
  77. package/dist/src/channels/registry.d.ts +33 -0
  78. package/dist/src/channels/registry.js +57 -0
  79. package/dist/src/channels/registry.js.map +1 -0
  80. package/dist/src/channels/sdk/admit.d.ts +32 -0
  81. package/dist/src/channels/sdk/admit.js +30 -0
  82. package/dist/src/channels/sdk/admit.js.map +1 -0
  83. package/dist/src/channels/sdk/backend.d.ts +19 -0
  84. package/dist/src/channels/sdk/backend.js +103 -0
  85. package/dist/src/channels/sdk/backend.js.map +1 -0
  86. package/dist/src/channels/sdk/cursor.d.ts +51 -0
  87. package/dist/src/channels/sdk/cursor.js +57 -0
  88. package/dist/src/channels/sdk/cursor.js.map +1 -0
  89. package/dist/src/channels/sdk/index.d.ts +20 -0
  90. package/dist/src/channels/sdk/index.js +21 -0
  91. package/dist/src/channels/sdk/index.js.map +1 -0
  92. package/dist/src/channels/sdk/jsonl.d.ts +47 -0
  93. package/dist/src/channels/sdk/jsonl.js +201 -0
  94. package/dist/src/channels/sdk/jsonl.js.map +1 -0
  95. package/dist/src/channels/sdk/protocol.d.ts +297 -0
  96. package/dist/src/channels/sdk/protocol.js +25 -0
  97. package/dist/src/channels/sdk/protocol.js.map +1 -0
  98. package/dist/src/channels/sdk/schema.d.ts +12 -0
  99. package/dist/src/channels/sdk/schema.js +92 -0
  100. package/dist/src/channels/sdk/schema.js.map +1 -0
  101. package/dist/src/channels/sdk/wire-core.d.ts +199 -0
  102. package/dist/src/channels/sdk/wire-core.js +447 -0
  103. package/dist/src/channels/sdk/wire-core.js.map +1 -0
  104. package/dist/src/channels/service.d.ts +66 -0
  105. package/dist/src/channels/service.js +170 -0
  106. package/dist/src/channels/service.js.map +1 -0
  107. package/dist/src/channels/tui/autocomplete/autocomplete.d.ts +32 -0
  108. package/dist/src/channels/tui/autocomplete/autocomplete.js +43 -0
  109. package/dist/src/channels/tui/autocomplete/autocomplete.js.map +1 -0
  110. package/dist/src/channels/tui/autocomplete/file-mentions.d.ts +18 -0
  111. package/dist/src/channels/tui/autocomplete/file-mentions.js +107 -0
  112. package/dist/src/channels/tui/autocomplete/file-mentions.js.map +1 -0
  113. package/dist/src/channels/tui/autocomplete/popup.d.ts +47 -0
  114. package/dist/src/channels/tui/autocomplete/popup.js +149 -0
  115. package/dist/src/channels/tui/autocomplete/popup.js.map +1 -0
  116. package/dist/src/channels/tui/autocomplete/provider.d.ts +38 -0
  117. package/dist/src/channels/tui/autocomplete/provider.js +8 -0
  118. package/dist/src/channels/tui/autocomplete/provider.js.map +1 -0
  119. package/dist/src/channels/tui/autocomplete/token.d.ts +22 -0
  120. package/dist/src/channels/tui/autocomplete/token.js +69 -0
  121. package/dist/src/channels/tui/autocomplete/token.js.map +1 -0
  122. package/dist/src/channels/tui/backend/ansi-rows.d.ts +86 -0
  123. package/dist/src/channels/tui/backend/ansi-rows.js +209 -0
  124. package/dist/src/channels/tui/backend/ansi-rows.js.map +1 -0
  125. package/dist/src/channels/tui/backend/main-screen.d.ts +90 -0
  126. package/dist/src/channels/tui/backend/main-screen.js +208 -0
  127. package/dist/src/channels/tui/backend/main-screen.js.map +1 -0
  128. package/dist/src/channels/tui/backend/osc.d.ts +79 -0
  129. package/dist/src/channels/tui/backend/osc.js +92 -0
  130. package/dist/src/channels/tui/backend/osc.js.map +1 -0
  131. package/dist/src/channels/tui/backend/transcript.d.ts +98 -0
  132. package/dist/src/channels/tui/backend/transcript.js +231 -0
  133. package/dist/src/channels/tui/backend/transcript.js.map +1 -0
  134. package/dist/src/channels/tui/backend/tui-backend.d.ts +324 -0
  135. package/dist/src/channels/tui/backend/tui-backend.js +916 -0
  136. package/dist/src/channels/tui/backend/tui-backend.js.map +1 -0
  137. package/dist/src/channels/tui/editor/editor-model.d.ts +141 -0
  138. package/dist/src/channels/tui/editor/editor-model.js +544 -0
  139. package/dist/src/channels/tui/editor/editor-model.js.map +1 -0
  140. package/dist/src/channels/tui/editor/editor-view.d.ts +36 -0
  141. package/dist/src/channels/tui/editor/editor-view.js +141 -0
  142. package/dist/src/channels/tui/editor/editor-view.js.map +1 -0
  143. package/dist/src/channels/tui/editor/editor.d.ts +50 -0
  144. package/dist/src/channels/tui/editor/editor.js +276 -0
  145. package/dist/src/channels/tui/editor/editor.js.map +1 -0
  146. package/dist/src/channels/tui/editor/undo-stack.d.ts +22 -0
  147. package/dist/src/channels/tui/editor/undo-stack.js +36 -0
  148. package/dist/src/channels/tui/editor/undo-stack.js.map +1 -0
  149. package/dist/src/channels/tui/editor/visual-lines.d.ts +39 -0
  150. package/dist/src/channels/tui/editor/visual-lines.js +127 -0
  151. package/dist/src/channels/tui/editor/visual-lines.js.map +1 -0
  152. package/dist/src/channels/tui/editor/word-nav.d.ts +10 -0
  153. package/dist/src/channels/tui/editor/word-nav.js +107 -0
  154. package/dist/src/channels/tui/editor/word-nav.js.map +1 -0
  155. package/dist/src/channels/tui/history/history-viewer.d.ts +133 -0
  156. package/dist/src/channels/tui/history/history-viewer.js +404 -0
  157. package/dist/src/channels/tui/history/history-viewer.js.map +1 -0
  158. package/dist/src/channels/tui/index.d.ts +16 -0
  159. package/dist/src/channels/tui/index.js +4 -0
  160. package/dist/src/channels/tui/index.js.map +1 -0
  161. package/dist/src/channels/tui/markdown/blocks.d.ts +42 -0
  162. package/dist/src/channels/tui/markdown/blocks.js +112 -0
  163. package/dist/src/channels/tui/markdown/blocks.js.map +1 -0
  164. package/dist/src/channels/tui/markdown/inline.d.ts +24 -0
  165. package/dist/src/channels/tui/markdown/inline.js +76 -0
  166. package/dist/src/channels/tui/markdown/inline.js.map +1 -0
  167. package/dist/src/channels/tui/markdown/markdown.d.ts +34 -0
  168. package/dist/src/channels/tui/markdown/markdown.js +183 -0
  169. package/dist/src/channels/tui/markdown/markdown.js.map +1 -0
  170. package/dist/src/channels/tui/memory/memory-viewer.d.ts +235 -0
  171. package/dist/src/channels/tui/memory/memory-viewer.js +541 -0
  172. package/dist/src/channels/tui/memory/memory-viewer.js.map +1 -0
  173. package/dist/src/channels/tui/overlay/alt-screen.d.ts +79 -0
  174. package/dist/src/channels/tui/overlay/alt-screen.js +106 -0
  175. package/dist/src/channels/tui/overlay/alt-screen.js.map +1 -0
  176. package/dist/src/channels/tui/overlay/overlay.d.ts +53 -0
  177. package/dist/src/channels/tui/overlay/overlay.js +58 -0
  178. package/dist/src/channels/tui/overlay/overlay.js.map +1 -0
  179. package/dist/src/channels/tui/overlay/select-confirm.d.ts +73 -0
  180. package/dist/src/channels/tui/overlay/select-confirm.js +157 -0
  181. package/dist/src/channels/tui/overlay/select-confirm.js.map +1 -0
  182. package/dist/src/channels/tui/panels/todo-panel.d.ts +24 -0
  183. package/dist/src/channels/tui/panels/todo-panel.js +55 -0
  184. package/dist/src/channels/tui/panels/todo-panel.js.map +1 -0
  185. package/dist/src/channels/tui/panels/tool-progress-panel.d.ts +37 -0
  186. package/dist/src/channels/tui/panels/tool-progress-panel.js +94 -0
  187. package/dist/src/channels/tui/panels/tool-progress-panel.js.map +1 -0
  188. package/dist/src/channels/tui/scroll/scroll-view.d.ts +96 -0
  189. package/dist/src/channels/tui/scroll/scroll-view.js +255 -0
  190. package/dist/src/channels/tui/scroll/scroll-view.js.map +1 -0
  191. package/dist/src/channels/tui/status/status-line.d.ts +46 -0
  192. package/dist/src/channels/tui/status/status-line.js +77 -0
  193. package/dist/src/channels/tui/status/status-line.js.map +1 -0
  194. package/dist/src/channels/tui/theme.d.ts +27 -0
  195. package/dist/src/channels/tui/theme.js +42 -0
  196. package/dist/src/channels/tui/theme.js.map +1 -0
  197. package/dist/src/channels/types.d.ts +84 -0
  198. package/dist/src/channels/types.js +12 -0
  199. package/dist/src/channels/types.js.map +1 -0
  200. package/dist/src/channels/ui-core.d.ts +80 -0
  201. package/dist/src/channels/ui-core.js +210 -0
  202. package/dist/src/channels/ui-core.js.map +1 -0
  203. package/dist/src/context/codes.d.ts +1 -0
  204. package/dist/src/context/codes.js +41 -0
  205. package/dist/src/context/codes.js.map +1 -0
  206. package/dist/src/context/events.d.ts +62 -0
  207. package/dist/src/context/events.js +161 -0
  208. package/dist/src/context/events.js.map +1 -0
  209. package/dist/src/context/index.d.ts +16 -0
  210. package/dist/src/context/index.js +14 -0
  211. package/dist/src/context/index.js.map +1 -0
  212. package/dist/src/context/logger.d.ts +67 -0
  213. package/dist/src/context/logger.js +137 -0
  214. package/dist/src/context/logger.js.map +1 -0
  215. package/dist/src/context/scope.d.ts +78 -0
  216. package/dist/src/context/scope.js +183 -0
  217. package/dist/src/context/scope.js.map +1 -0
  218. package/dist/src/context/workspace.d.ts +8 -0
  219. package/dist/src/context/workspace.js +105 -0
  220. package/dist/src/context/workspace.js.map +1 -0
  221. package/dist/src/contracts/agent-events.d.ts +73 -0
  222. package/dist/src/contracts/agent-events.js +17 -0
  223. package/dist/src/contracts/agent-events.js.map +1 -0
  224. package/dist/src/contracts/api.d.ts +249 -0
  225. package/dist/src/contracts/api.js +334 -0
  226. package/dist/src/contracts/api.js.map +1 -0
  227. package/dist/src/contracts/approval.d.ts +37 -0
  228. package/dist/src/contracts/approval.js +10 -0
  229. package/dist/src/contracts/approval.js.map +1 -0
  230. package/dist/src/contracts/env-ref.d.ts +41 -0
  231. package/dist/src/contracts/env-ref.js +33 -0
  232. package/dist/src/contracts/env-ref.js.map +1 -0
  233. package/dist/src/contracts/errors.d.ts +56 -0
  234. package/dist/src/contracts/errors.js +267 -0
  235. package/dist/src/contracts/errors.js.map +1 -0
  236. package/dist/src/contracts/events.d.ts +70 -0
  237. package/dist/src/contracts/events.js +330 -0
  238. package/dist/src/contracts/events.js.map +1 -0
  239. package/dist/src/contracts/index.d.ts +32 -0
  240. package/dist/src/contracts/index.js +27 -0
  241. package/dist/src/contracts/index.js.map +1 -0
  242. package/dist/src/contracts/llm.d.ts +278 -0
  243. package/dist/src/contracts/llm.js +13 -0
  244. package/dist/src/contracts/llm.js.map +1 -0
  245. package/dist/src/contracts/messages.d.ts +63 -0
  246. package/dist/src/contracts/messages.js +66 -0
  247. package/dist/src/contracts/messages.js.map +1 -0
  248. package/dist/src/contracts/redact.d.ts +46 -0
  249. package/dist/src/contracts/redact.js +224 -0
  250. package/dist/src/contracts/redact.js.map +1 -0
  251. package/dist/src/contracts/tools.d.ts +198 -0
  252. package/dist/src/contracts/tools.js +35 -0
  253. package/dist/src/contracts/tools.js.map +1 -0
  254. package/dist/src/contracts/types.d.ts +304 -0
  255. package/dist/src/contracts/types.js +51 -0
  256. package/dist/src/contracts/types.js.map +1 -0
  257. package/dist/src/contracts/ui.d.ts +122 -0
  258. package/dist/src/contracts/ui.js +16 -0
  259. package/dist/src/contracts/ui.js.map +1 -0
  260. package/dist/src/sdk/types.d.ts +144 -0
  261. package/dist/src/sdk/types.js +33 -0
  262. package/dist/src/sdk/types.js.map +1 -0
  263. package/package.json +29 -0
@@ -0,0 +1,916 @@
1
+ import { isStandardMessage } from '../../../contracts/index.js';
2
+ import { CellGrid, InputDecoder, ProcessTerminalIO } from '../../engine/index.js';
3
+ import { MainScreen } from './main-screen.js';
4
+ import { LiveTranscript, shortIdOf } from './transcript.js';
5
+ import { OscDisplay, buildOsc52Copy } from './osc.js';
6
+ import { StatusLine } from '../status/status-line.js';
7
+ import { TodoPanel } from '../panels/todo-panel.js';
8
+ import { ToolProgressPanel } from '../panels/tool-progress-panel.js';
9
+ import { sessionColor } from '../theme.js';
10
+ import { buildSgr, SGR_RESET } from './ansi-rows.js';
11
+ import { Editor } from '../editor/editor.js';
12
+ import { OverlayStack } from '../overlay/overlay.js';
13
+ import { AltScreenHost } from '../overlay/alt-screen.js';
14
+ import { HistoryViewer } from '../history/history-viewer.js';
15
+ import { MemoryViewer } from '../memory/memory-viewer.js';
16
+ import { ConfirmPanel, SelectPanel } from '../overlay/select-confirm.js';
17
+ import { AutocompletePopup } from '../autocomplete/popup.js';
18
+ import { CombinedAutocompleteProvider } from '../autocomplete/autocomplete.js';
19
+ /** 主屏形进屏模式串:粘贴开 + kitty 推栈(disambiguate 最小位)+ 探测哨兵(无光标藏无 1049——与 Engine 全屏形分立) */
20
+ const ENTER_MAIN = '\x1b[?2004h' + '\x1b[>1u' + '\x1b[?u' + '\x1b[c';
21
+ /** 出屏模式串(与进屏严格对称反序——单源常量) */
22
+ const LEAVE_MAIN = '\x1b[<u' + '\x1b[?2004l';
23
+ /** 渲染合并帧率帽缺省(对齐 Engine DEFAULT_FPS_CAP——批 10f-3 性能回归锁校准定值 60,实机校准后收紧留批 12) */
24
+ const DEFAULT_FPS_CAP = 60;
25
+ /** lone-ESC 判定窗缺省(对齐 Engine DEFAULT_ESCAPE_WINDOW_MS) */
26
+ const DEFAULT_ESCAPE_WINDOW_MS = 30;
27
+ /** 状态行转轮自驱间隔(ms——注入调度后自重排) */
28
+ const TICK_INTERVAL_MS = 100;
29
+ /** notify 档位符号(正文着色纪律——纯符号不配色,与摘要行会话色分立) */
30
+ const NOTIFY_SYMBOLS = Object.freeze({
31
+ info: '·',
32
+ success: '✓',
33
+ warn: '⚠',
34
+ error: '✖',
35
+ });
36
+ /** 零账(归零基线) */
37
+ const ZERO_USAGE = Object.freeze({
38
+ input: 0,
39
+ output: 0,
40
+ cacheRead: 0,
41
+ cacheWrite: 0,
42
+ totalTokens: 0,
43
+ cost: 0,
44
+ currency: null,
45
+ });
46
+ /** token 数千位分组(1,234,567——usage 行「格式化」定形) */
47
+ function formatTokenCount(n) {
48
+ return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
49
+ }
50
+ /**
51
+ * TUI 后端:单终端 inline 主屏 + 自持输入管线。构造后须 start()(模式串 +
52
+ * 清屏 + 滚动区确立)再接核事件;stop() 对称出屏(模式串反序 + raw 复原)。
53
+ */
54
+ export class TuiBackend {
55
+ id = 'tui';
56
+ capabilities = Object.freeze({
57
+ notify: true,
58
+ confirm: true, // 浮层面板呈现(批 10e-2 交互纵切实装)
59
+ select: true,
60
+ input: true,
61
+ approval: true,
62
+ setStatus: true,
63
+ setWidget: false,
64
+ });
65
+ /** 当前交互会话位(装配焦点联动可改——提交/打断柄的机器位) */
66
+ sessionId;
67
+ io;
68
+ screen;
69
+ transcript = new LiveTranscript();
70
+ statusLine = new StatusLine();
71
+ editor;
72
+ popup;
73
+ stack = new OverlayStack();
74
+ /** overlay 锚定注册表(content 身份键 → 本帧 region——renderFixed 行账重建) */
75
+ overlayLayout = new Map();
76
+ /** 装配柄(07 §4.3 两柄 + 命令柄) */
77
+ onSubmit;
78
+ onInterrupt;
79
+ onQuit;
80
+ dispatchCommand;
81
+ /* ---- 输入管线态 ---- */
82
+ decoder;
83
+ escapeWindowMs;
84
+ escapeHandle = null;
85
+ unsubInput = null;
86
+ unsubResize = null;
87
+ running = false;
88
+ /** 起动过位(lifecycle 的 idle / disposed 分权——一次性事实,start 幂等位之外) */
89
+ startedOnce = false;
90
+ priorRaw = false;
91
+ disarmExitRestore = null;
92
+ /* ---- 主屏挂起态(批 10f-4——AltScreenPrimary 交出面) ---- */
93
+ /** 挂起位(suspendMain 置位:渲染请求安全 no-op 闸 + 输入已卸订) */
94
+ suspendedMain = false;
95
+ /** 停屏期到达的瞬时行缓冲(notify / 件 9 摘要行 / 撤销说明行——复起补显射界,2026-09-07 勘正笔) */
96
+ suspendedTransients = [];
97
+ /* ---- 副屏装配态(批 10f-4 特性腿——件 8 /history;mm 批 /memory 并席) ---- */
98
+ /** 副屏宿主(构造晚于 io 赋值——类字段初始化序:宿主构造须见 io 实值) */
99
+ altHost;
100
+ /** 在场副屏句柄(单值承载 /history 与 /memory 两件——无嵌套备屏律;开、q/Esc/Ctrl+D/ask 收四路共闭) */
101
+ altHandle = null;
102
+ /**
103
+ * 记忆管理面材料位(mm 批——后置注入):构造期不可达(memory 件的 dao
104
+ * 生命周期在插件 apply 内、且 channels 不可 import memory——边表),装配
105
+ * 根 boot 完成后经 setMemoryScreen 注入真身;null = 材料缺席(openMemory
106
+ * 返 false——/memory 命令核侧 notify 降级提示)。
107
+ */
108
+ memoryScreen = null;
109
+ /* ---- 渲染合并态(schedule 注入后活——否则同步直出) ---- */
110
+ scheduleFn;
111
+ cancelFn;
112
+ now;
113
+ minFrameMs;
114
+ pendingOps = [];
115
+ needFixed = false;
116
+ frameHandle = null;
117
+ tickHandle = null;
118
+ lastFlushAt = Number.NEGATIVE_INFINITY;
119
+ /** input-ask 在飞体(null = 常态——提交落 onSubmit) */
120
+ inputAsk = null;
121
+ /* ---- 呈现面件 4/5/6 态 ---- */
122
+ todoFor;
123
+ todoPanel = new TodoPanel();
124
+ toolPanel = new ToolProgressPanel();
125
+ /** 当前 turn 的 assistant 消息用量暂存(turn_end 累加——件 6 等价性条款) */
126
+ pendingUsage = null;
127
+ usageTotal = ZERO_USAGE;
128
+ /* ---- 呈现面件 7 态(终端外显) ---- */
129
+ /** title 基线(`berry-agent` 或 `berry-agent <版本>`——起屏与复原落点) */
130
+ titleBaseline;
131
+ osc;
132
+ /**
133
+ * 各会话在飞净计数(agent_start +1 / agent_end -1——按信封 sessionId 归
134
+ * 账,clamp ≥ 0;归零删条目防会话退出残键)。聚焦位不参账:run 跨切焦时
135
+ * start/end 恒落同一会话账——按事件时刻聚焦位分两路会跨路不对称(末路
136
+ * end 被 clamp 吞致忙态永残留,07 件 7 判据勘正)。
137
+ */
138
+ inFlightBySession = new Map();
139
+ /** 进度态上次写出(忙闲迁移门——同态静默,周期重发归保活自持) */
140
+ progressBusy = false;
141
+ constructor(io, options = {}) {
142
+ this.io = io;
143
+ this.sessionId = options.sessionId ?? 'main';
144
+ this.onSubmit = options.onSubmit;
145
+ this.onInterrupt = options.onInterrupt;
146
+ this.onQuit = options.onQuit;
147
+ this.dispatchCommand = options.dispatchCommand;
148
+ this.todoFor = options.todoFor;
149
+ this.scheduleFn = options.schedule ?? null;
150
+ this.cancelFn = options.cancelSchedule ?? ((h) => clearTimeout(h));
151
+ this.now = options.now ?? Date.now;
152
+ this.minFrameMs = 1000 / (options.fpsCap ?? DEFAULT_FPS_CAP);
153
+ this.escapeWindowMs = options.escapeWindowMs ?? DEFAULT_ESCAPE_WINDOW_MS;
154
+ // 件 7:title 基线(version 注入缺席 = 裸名);外显件复用本件调度注入
155
+ // (schedule 缺席 = 保活缺位——同步测试语义,与渲染合并同构)
156
+ this.titleBaseline = options.version ? `berry-agent ${options.version}` : 'berry-agent'; // 空串同缺席归裸名(无尾随空格脏基线)
157
+ this.osc = new OscDisplay(io, {
158
+ baseline: this.titleBaseline,
159
+ schedule: this.scheduleFn ?? undefined,
160
+ cancel: this.cancelFn,
161
+ });
162
+ this.decoder = new InputDecoder({ now: this.now, escapeWindowMs: this.escapeWindowMs });
163
+ this.editor = new Editor({
164
+ onSubmit: (text) => this.handleSubmit(text),
165
+ onChange: () => this.handleEditorChange(),
166
+ maxVisibleLines: options.maxVisibleLines,
167
+ });
168
+ this.popup = new AutocompletePopup(new CombinedAutocompleteProvider(options.autocomplete ?? {}), this.editor.model);
169
+ this.stack.onChange = () => this.touchFixed();
170
+ this.screen = new MainScreen(io, { fixedHeight: 4 }); // 初始高:编辑器 3 + 状态行 1(动态更新经 setFixed)
171
+ // 副屏宿主(构造放 constructor 尾——io 与注入面已赋值;引擎选项与主屏同源:
172
+ // 假钟 / 帧帽 / lone-ESC 窗直通,调度无注入时给同步直出包装——副屏首帧
173
+ // 确定性与 lone-ESC 即决同主屏测试语义)
174
+ this.altHost = new AltScreenHost(this, io, {
175
+ engineOptions: {
176
+ now: this.now,
177
+ fpsCap: options.fpsCap,
178
+ escapeWindowMs: this.escapeWindowMs,
179
+ schedule: this.scheduleFn ??
180
+ ((fn) => {
181
+ fn();
182
+ return null;
183
+ }),
184
+ cancelSchedule: this.scheduleFn !== null ? this.cancelFn : () => { },
185
+ },
186
+ });
187
+ }
188
+ /** TUI 恒有观众(07 §4.3 观众探针定值) */
189
+ hasAudience() {
190
+ return true;
191
+ }
192
+ /** 生命周期判定位(AltScreenPrimary 窄介面面:idle 未启 / running 持屏 / suspended 挂起 / disposed 终退) */
193
+ get lifecycle() {
194
+ if (!this.startedOnce)
195
+ return 'idle';
196
+ if (!this.running)
197
+ return 'disposed';
198
+ return this.suspendedMain ? 'suspended' : 'running';
199
+ }
200
+ /** 启动:主屏形模式串 + raw + 输入订阅 + 清屏滚动区 + 固定区首画 */
201
+ start() {
202
+ if (this.running)
203
+ return;
204
+ this.running = true;
205
+ this.startedOnce = true; // 一次性事实(stop 后 disposed——start 不再可回)
206
+ this.priorRaw = this.io.isRaw();
207
+ this.io.write(ENTER_MAIN);
208
+ this.armExitRestore();
209
+ this.osc.setTitle(this.titleBaseline); // 件 7:起屏基线 title(OSC 0——值缓存首写)
210
+ this.io.setRawMode(true);
211
+ this.unsubInput = this.io.onInput(this.handleInput);
212
+ // 显式放流(共享 io 换防接缝——副屏 Engine 复用同 io 场景;首启 no-op)
213
+ this.io.resume();
214
+ this.screen.start();
215
+ this.renderFixed();
216
+ this.unsubResize = this.io.onResize(() => this.handleResize());
217
+ if (this.scheduleFn !== null)
218
+ this.armTick();
219
+ }
220
+ /** 对称出屏:模式串反序 + 输入卸订 + 定时器全收 + 外显复原 + raw 复原(终退不可复用) */
221
+ stop() {
222
+ if (!this.running)
223
+ return;
224
+ this.closeAlt(); // 防御位:在场副屏先收(装配纪律先收再退——泄漏则副屏 Engine 残活)
225
+ this.running = false;
226
+ // 挂起期主屏已出屏(suspendMain 已写出屏串)——重写会污染在场副屏;
227
+ // 装配纪律恒「先收副屏再退出」,本闸是防御位非编舞路
228
+ if (!this.suspendedMain)
229
+ this.io.write(LEAVE_MAIN);
230
+ this.unsubInput?.();
231
+ this.unsubInput = null;
232
+ this.unsubResize?.();
233
+ this.unsubResize = null;
234
+ this.cancelTimer('frame');
235
+ this.cancelTimer('tick');
236
+ this.cancelTimer('escape');
237
+ this.osc.restore(); // 件 7:复原两写点(title 基线 + 进度清零)+ 保活停针(名册语义)
238
+ this.disarmExitRestore?.();
239
+ this.io.pause();
240
+ if (!this.suspendedMain)
241
+ this.io.setRawMode(this.priorRaw); // 挂起期 raw 已复先验——不二次复原
242
+ }
243
+ /**
244
+ * 主屏挂起(AltScreenPrimary 交出半场——批 10f-4;07 §4.1 件 8「主屏挂起 /
245
+ * 复起交出面(UiBackend 实装件自持)」条款):
246
+ * 出屏模式串 + 卸输入监听 + 在途转义一窗全丢 + 停流 + raw 复先验(Engine
247
+ * suspend 三件套同形——共享 io 换防,副屏随后 start 重装重放流);渲染请求
248
+ * 安全 no-op(requestRender / flush 挂起闸——停屏期零写出);帧合并 / tick /
249
+ * lone-ESC 窗定时器全收(防后台空转);硬退复原钩解除(挂起期副屏自担其屏
250
+ * 的退出复原)。
251
+ *
252
+ * 件 7 osc 保活不停(批内裁):外显态(title / 忙态 OSC 9;4)属终端级非
253
+ * 主屏 cell 内容——停屏期终端标签页注意力语义照常(onEnvelope 照常归账),
254
+ * OSC 序列不落 cell 网格不扰动副屏画面。
255
+ *
256
+ * 停屏期账不丢:durable 事件照常归约行集模型(复起全帧重画携带——树已含
257
+ * 停屏期全部事件);瞬时行(notify / 件 9 摘要行 / 撤销说明行)入
258
+ * suspendedTransients 缓冲——复起补显射界含瞬时行(树按字面不含不入树的
259
+ * 瞬时行,2026-09-07 遗漏审计批补笔)。
260
+ */
261
+ suspendMain() {
262
+ if (!this.running || this.suspendedMain)
263
+ return; // 幂等 + 无挂起对象防御
264
+ this.suspendedMain = true;
265
+ this.io.write(LEAVE_MAIN); // 出屏模式串(与 start 进屏严格对称反序——单源常量)
266
+ this.unsubInput?.();
267
+ this.unsubInput = null;
268
+ this.decoder.discardPending(); // 在途转义 / 粘贴 / 预编辑一窗全丢(Engine 换防同形)
269
+ this.io.pause();
270
+ this.io.setRawMode(this.priorRaw); // raw 复先验(副屏随后自设 raw)
271
+ this.cancelTimer('frame'); // 在飞帧收口(挂起期零写出的调度半边)
272
+ this.cancelTimer('tick'); // 状态行转轮停摆(防后台空转——复起重摆)
273
+ this.cancelTimer('escape'); // lone-ESC 窗收口(decoder 已弃在途态)
274
+ this.disarmExitRestore?.(); // 出屏解除硬退复原钩(挂起期副屏自担)
275
+ }
276
+ /**
277
+ * 主屏复起(AltScreenPrimary 交出半场的对称复位):进屏模式串 + 硬退钩
278
+ * 重武装 + raw 重设 + 输入重装 + 显式放流(已被 pause 的流再挂监听不自动回
279
+ * flowing——Engine resume 同形)+ **全帧重画不走(通道)repaint**:主屏既有
280
+ * 权威全量重建路(几何真值重取 + 清屏 + 行集全量重写)+ 瞬时行缓冲补吐。
281
+ *
282
+ * 不走(通道)repaint 的行为锁:通道 repaint 按投影重建行集,投影不含停屏
283
+ * 期直写主屏的瞬时行——全帧重画以行集 + 瞬时缓冲为真相,瞬时行在场即补显
284
+ * (07 件 8「repaint 清树会抹掉停屏期入树的瞬时行」+ 2026-09-07 勘正笔)。
285
+ * 停屏期 resize 由几何真值重取吸收(挂起期 handleResize 安全 no-op——此刻
286
+ * 写出会污染在场副屏)。
287
+ */
288
+ resumeMain() {
289
+ if (!this.running || !this.suspendedMain)
290
+ return; // 幂等 + 非挂起态防御
291
+ this.suspendedMain = false;
292
+ this.io.write(ENTER_MAIN); // 进屏模式串(与出屏对称)
293
+ this.armExitRestore(); // 复进屏重武装(arm 幂等——先解除旧钩再挂)
294
+ this.io.setRawMode(true);
295
+ this.unsubInput = this.io.onInput(this.handleInput);
296
+ this.io.resume(); // 显式放流(副屏 dispose 已 pause——共享 io 换防接缝)
297
+ // 排队旧帧作废 + 固定区脏位重建(全帧重画是新真相——挂起期积压 op 合并无意义)
298
+ this.pendingOps = [];
299
+ this.needFixed = false;
300
+ // 全帧重画:几何真值重取(吸收停屏期 resize)+ 清屏 + 行集全量重写(含停屏期 durable 事件)
301
+ this.screen.handleResize(this.transcript.snapshot);
302
+ // 瞬时行缓冲补吐(复起补显射界含停屏期瞬时行——2026-09-07 勘正笔)
303
+ const transients = this.suspendedTransients;
304
+ this.suspendedTransients = [];
305
+ if (transients.length > 0)
306
+ this.screen.appendTransient(transients);
307
+ this.renderFixed();
308
+ if (this.scheduleFn !== null)
309
+ this.armTick(); // 状态行转轮复摆
310
+ }
311
+ /* ---------------- 副屏装配面(批 10f-4 特性腿——件 8 /history;mm 批 /memory 并席) ---------------- */
312
+ /**
313
+ * 开副屏回看器(UiBackend 可选能力面实装——通道核 /history 命令到达扇出):
314
+ * 挂起主屏 → 1049 副屏 HistoryViewer(同一渲染管线全量档——件 8 数据源
315
+ * 条款)。已在副屏 no-op(无嵌套备屏);主屏不在 running 态 open 被拒
316
+ * (句柄 null——保持无副屏态)。Ctrl+C / Ctrl+D 副屏键面经 viewer 装配柄
317
+ * 透传本件两柄(与主屏同键面)。
318
+ */
319
+ openHistory(sessionId, messages) {
320
+ if (this.altHandle !== null)
321
+ return;
322
+ const handle = this.altHost.open(new HistoryViewer({
323
+ sessionId,
324
+ messages,
325
+ columns: this.io.size().columns,
326
+ onExit: () => this.closeAlt(),
327
+ onInterrupt: this.onInterrupt,
328
+ onQuit: this.onQuit,
329
+ // OSC 52 复制写出柄(mu-2):release 选区行间拼 LF 到达 → 铸序列直写
330
+ // io(终端支持不可探测——尽力写出无反馈,件 8 细则)
331
+ onCopy: (text) => this.io.write(buildOsc52Copy(text)),
332
+ }));
333
+ this.altHandle = handle; // null = 主屏未 running 被拒——如实保持无副屏
334
+ }
335
+ /**
336
+ * 记忆管理面材料注入(mm 批——后置 init 位):装配根 boot 完成后从
337
+ * core:memory 服务面取材料注入(ownerKeys/DAO 窄面/消毒函数/导出闭包——
338
+ * 真身经装配传真)。null = 撤材料(件卸载形——后续 openMemory 返 false)。
339
+ */
340
+ setMemoryScreen(deps) {
341
+ this.memoryScreen = deps;
342
+ }
343
+ /**
344
+ * 开副屏记忆管理面(UiBackend 可选能力面实装——通道核 /memory 命令到达
345
+ * 扇出;06 §7 形态定形注):材料缺席或已在副屏返 false(核侧 notify 降级
346
+ * 提示);退出三柄本件自持(打断 = 当前交互会话位——与提交柄同锚)。
347
+ */
348
+ openMemory() {
349
+ if (this.memoryScreen === null || this.altHandle !== null)
350
+ return false;
351
+ const deps = this.memoryScreen;
352
+ const handle = this.altHost.open(new MemoryViewer({
353
+ ...deps,
354
+ onExit: () => this.closeAlt(),
355
+ onInterrupt: () => this.onInterrupt?.(this.sessionId), // 零参形——装配闭包已知目标会话
356
+ onQuit: this.onQuit,
357
+ }));
358
+ if (handle === null)
359
+ return false; // 主屏未 running 被拒——如实报 false
360
+ this.altHandle = handle;
361
+ return true;
362
+ }
363
+ /**
364
+ * 收副屏(UiBackend 可选能力面实装——UiCore ask 入口扇出「先收副屏再入
365
+ * 提问队列」,07 §4.1 件 8 注意力优先级 ask > 回看条款;viewer 退出键
366
+ * 同路收口;/history 与 /memory 两件共口——在场者谁收谁)。幂等:无副屏 no-op。
367
+ */
368
+ collapseAltScreen() {
369
+ this.closeAlt();
370
+ }
371
+ /** 副屏统一收口:句柄 close(AltScreenHost 编舞:出副屏 + 主屏复起全帧重画) */
372
+ closeAlt() {
373
+ const handle = this.altHandle;
374
+ if (handle === null)
375
+ return;
376
+ this.altHandle = null; // 先置空防重入(viewer onExit 与 ask 收起竞发)
377
+ handle.close(); // 幂等(句柄 closed 位自守)
378
+ }
379
+ /** 一次性通知:正文瞬时行直写(级别符号前缀——不进行集) */
380
+ notify(message, opts) {
381
+ const symbol = NOTIFY_SYMBOLS[opts?.level ?? 'info'];
382
+ this.appendTransientLine(`${symbol} ${message}`);
383
+ }
384
+ /**
385
+ * 瞬时说明行入正文流(notify 与 ask 撤销说明行共用路——07 §4.3「曾在屏
386
+ * 者由通道上撤销说明行」):op 入合并队列 + 渲染请求(同步直出模式立即
387
+ * 落地;注入调度随帧合并——transient 到达序保持)。挂起期入缓冲账不丢
388
+ * (复起补显射界含瞬时行——批 10f-4)。
389
+ */
390
+ appendTransientLine(line) {
391
+ if (this.suspendedMain) {
392
+ this.suspendedTransients.push(line); // 停屏期瞬时行缓冲(不入 op 队列——复起不走合并直补吐)
393
+ return;
394
+ }
395
+ this.pendingOps.push({ kind: 'transient', lines: [line] });
396
+ this.requestRender();
397
+ }
398
+ /** 状态行文案(last-writer-wins——StatusLine 件语义) */
399
+ setStatus(_sessionId, status) {
400
+ this.statusLine.setStatus(status);
401
+ this.touchFixed();
402
+ }
403
+ /** 活体信封呈现:渲染归约 + 摘要行分叉 + 聚焦态状态面消费 */
404
+ onEnvelope(env, focused) {
405
+ const summary = this.transcript.applyEvent(env, focused);
406
+ if (summary !== null) {
407
+ // 摘要行统一走 appendTransientLine(挂起期入缓冲不丢——批 10f-4 改道)
408
+ this.appendTransientLine(summaryToAnsi(summary));
409
+ }
410
+ else {
411
+ this.enqueuePresent();
412
+ }
413
+ this.requestRender();
414
+ this.trackProgress(env); // 件 7:按会话净计数(终端级注意力——任一会话在飞即忙)
415
+ if (focused)
416
+ this.applyFocusedEvent(env.event);
417
+ }
418
+ /** 重画呈现:投影重建行集 + 清屏全量重写(widget 槽值不支撑——忽略) */
419
+ onRepaint(sessionId, projection, _widget) {
420
+ // 模型半场照常(repaint 是新真相——挂起期也不丢投影:复起全帧重画携带)
421
+ this.transcript.loadProjection(projection);
422
+ this.resetUsage(); // 件 6:清行并归零(切焦清账重计——尾注射界)
423
+ this.toolPanel.clear(); // 件 5:瞬时面不跨 repaint 保存
424
+ this.refreshTodo(); // 件 4:刷新三时点之一
425
+ this.osc.setTitle(`${this.titleBaseline} · ${shortIdOf(sessionId)}`); // 件 7:title 点缀会话短 id(终端级外显——挂起期照常,批 10f-4 裁)
426
+ // 权威全量重建——排队旧帧作废(repaint 是新真相,合并无意义)
427
+ this.pendingOps = [];
428
+ this.needFixed = false;
429
+ if (this.suspendedMain)
430
+ return; // 挂起闸:屏上零写出(复起全帧重画携带新投影)
431
+ this.screen.repaint(this.transcript.snapshot);
432
+ this.renderFixed();
433
+ }
434
+ /** 状态行动画推帧(自驱定时器内调;公开面留装配侧手动驱动——忙态外零开销) */
435
+ tick() {
436
+ if (!this.statusLine.isBusy)
437
+ return;
438
+ this.statusLine.tick();
439
+ this.touchFixed();
440
+ }
441
+ /** run 级用量累计观测面(件 6——装配/宿主侧二次消费) */
442
+ get usageView() {
443
+ return { ...this.usageTotal };
444
+ }
445
+ /** resize 编舞:几何重取 + 主屏全量重画 + 固定区按新几何重建(权威重建不走队列) */
446
+ handleResize() {
447
+ if (this.suspendedMain)
448
+ return; // 挂起闸:停屏期零写出(此刻写出污染在场副屏)——几何真值由复起全帧重画重取吸收
449
+ this.pendingOps = [];
450
+ this.needFixed = false;
451
+ this.screen.handleResize(this.transcript.snapshot);
452
+ this.renderFixed();
453
+ }
454
+ /* ---------------- 阻塞四件(浮层面板呈现——07 §4.3) ---------------- */
455
+ /** 是/否确认:ConfirmPanel 浮层(Enter → true / Esc → false;signal abort 保守值 + 关层 + 撤销说明行) */
456
+ confirm(message, opts) {
457
+ return new Promise((resolve) => {
458
+ const panel = new ConfirmPanel({ message });
459
+ const handle = this.openAskLayer(panel, () => resolve(false), opts?.signal, '⏹ 已取消确认');
460
+ panel.onFinish = (confirmed) => {
461
+ handle.close();
462
+ resolve(confirmed);
463
+ };
464
+ });
465
+ }
466
+ /** 单选:SelectPanel 浮层(Enter → value / Esc → '';ui-core 校验不在集保守值同收 '') */
467
+ select(message, choices, opts) {
468
+ return new Promise((resolve) => {
469
+ const panel = new SelectPanel({
470
+ title: message,
471
+ options: choices.map((c) => ({ value: c.value, label: c.label })),
472
+ });
473
+ const handle = this.openAskLayer(panel, () => resolve(''), opts?.signal, '⏹ 已取消选择');
474
+ panel.onFinish = (value) => {
475
+ handle.close();
476
+ resolve(value);
477
+ };
478
+ });
479
+ }
480
+ /**
481
+ * 自由文本:input-ask 形——提示行入固定区 + 编辑器转应答车(提交即应答,
482
+ * 弹层抑制);signal abort → '' 保守值 + 残稿清框 + 撤销说明行(07 §4.3
483
+ * 撤销面——提示行曾在固定区在屏,abort 收口正文流落 ⏹ 行)。
484
+ */
485
+ input(message, opts) {
486
+ return new Promise((resolve) => {
487
+ const ask = { message, resolve };
488
+ this.inputAsk = ask;
489
+ this.editor.setText(''); // 应答起始清框(草稿让位——提交路模型自清)
490
+ this.popup.refresh(); // 应答期弹层抑制前,在层按空框重算自隐
491
+ opts?.signal?.addEventListener('abort', () => {
492
+ if (this.inputAsk !== ask)
493
+ return; // 已应答收场——迟到 abort no-op(无说明行)
494
+ this.inputAsk = null;
495
+ this.appendTransientLine('⏹ 已取消提问');
496
+ this.editor.setText('');
497
+ this.popup.refresh();
498
+ resolve('');
499
+ this.touchFixed();
500
+ }, { once: true });
501
+ this.touchFixed();
502
+ });
503
+ }
504
+ /**
505
+ * 审批:SelectPanel 浮层四值(批准/拒绝/总是批准/取消——Esc 面板保守值 ''
506
+ * 映射 cancel,与 07 §4.3 收口律对齐);always 草案经 hint 段呈现(回写
507
+ * 归通道核 settleApprovalAlways——本件只呈现与回值)。signal abort 收口
508
+ * 'cancel' 保守值 + 撤销说明行(文案「审批」区分于 input/confirm/select
509
+ * 三件——07 §4.3 撤销面)。
510
+ */
511
+ askApproval(_sessionId, request, opts) {
512
+ // _sessionId:会话归属位随批 13b-3 后端面签名携带——TUI 呈现不消费(单屏
513
+ // 焦点态无会话路由需求),SDK 通道后端以此路由 ask 帧
514
+ return new Promise((resolve) => {
515
+ const title = request.toolName !== undefined ? `⚙ ${request.toolName}:${request.summary}` : request.summary;
516
+ const panel = new SelectPanel({
517
+ title,
518
+ options: [
519
+ { value: 'approve', label: '批准' },
520
+ { value: 'reject', label: '拒绝' },
521
+ { value: 'always', label: '总是批准', hint: request.suggestedEntry },
522
+ { value: 'cancel', label: '取消' },
523
+ ],
524
+ });
525
+ const handle = this.openAskLayer(panel, () => resolve('cancel'), opts?.signal, '⏹ 已取消审批');
526
+ panel.onFinish = (value) => {
527
+ handle.close();
528
+ // 值集即 ApprovalAskAnswer 四值(面板 Esc 收 '' → cancel 映射)
529
+ resolve(value === '' ? 'cancel' : value);
530
+ };
531
+ });
532
+ }
533
+ /* ---------------- 内部:输入管线 ---------------- */
534
+ /** 输入处理器:decoder 喂入 + lone-ESC 窗定时器(同步直出模式无窗即决) + 事件路由 */
535
+ handleInput = (chunk) => {
536
+ if (!this.running)
537
+ return; // stop 后残听防御
538
+ this.decoder.feed(chunk);
539
+ if (this.decoder.hasPendingEscape) {
540
+ if (this.scheduleFn !== null) {
541
+ if (this.escapeHandle === null) {
542
+ this.escapeHandle = this.scheduleFn(() => {
543
+ this.escapeHandle = null;
544
+ this.decoder.settle();
545
+ this.flushDecoderEvents();
546
+ }, this.escapeWindowMs);
547
+ }
548
+ }
549
+ else {
550
+ this.decoder.settle(); // 同步直出无窗即决(测试语义——单 chunk 喂入不拆序)
551
+ }
552
+ }
553
+ this.flushDecoderEvents();
554
+ };
555
+ /** 排空 decoder 事件队列并逐件路由 */
556
+ flushDecoderEvents() {
557
+ for (const ev of this.decoder.take())
558
+ this.routeEvent(ev);
559
+ }
560
+ /**
561
+ * 事件路由四层(07 §4.3 拦截链序)。
562
+ * 层① 全局键先于 overlay(ctrl+c 打断 run——ask 链收口经装配侧信号折入;
563
+ * ctrl+d 仅 overlay 空 + 编辑器空框才全局退出,否则让路面板/编辑器);
564
+ * 层② overlay 模态独占(未消费键不穿透);层③ 补全弹层非模态穿透;
565
+ * 层④ 编辑器(未消费键终局丢弃——escape 等无全局绑定)。
566
+ */
567
+ routeEvent(ev) {
568
+ if (ev.kind === 'key' && ev.phase === 'press' && ev.ctrl && !ev.alt && !ev.shift && !ev.meta) {
569
+ if (ev.key === 'c') {
570
+ this.onInterrupt?.(this.sessionId);
571
+ return;
572
+ }
573
+ if (ev.key === 'd' && this.stack.size === 0 && this.editor.model.isEmpty()) {
574
+ this.onQuit?.();
575
+ return;
576
+ }
577
+ }
578
+ if (this.stack.routeEvent(ev))
579
+ return;
580
+ if (this.popup.visible && this.popup.handleEvent(ev)) {
581
+ this.touchFixed(); // 弹层高亮/隐层——固定区重建
582
+ return;
583
+ }
584
+ if (this.editor.handleEvent(ev))
585
+ this.touchFixed();
586
+ }
587
+ /** 编辑器提交路由:input-ask 应答优先 → '/' 命令柄(false 兜底)→ onSubmit */
588
+ handleSubmit(text) {
589
+ const ask = this.inputAsk;
590
+ if (ask !== null) {
591
+ this.inputAsk = null;
592
+ ask.resolve(text);
593
+ this.popup.refresh(); // 应答期抑制的补全层此刻按空框重算自隐
594
+ this.touchFixed();
595
+ return;
596
+ }
597
+ if (text.startsWith('/') && this.dispatchCommand !== undefined) {
598
+ this.dispatchCommand(text)
599
+ .then((handled) => {
600
+ if (!handled)
601
+ this.onSubmit?.(this.sessionId, text); // 未命中兜底(03 §2.2 驱动侧语义)
602
+ })
603
+ .catch((err) => {
604
+ // 命令处理器异常不静默不崩进程——呈现面兜底(命令面纪律归命令面)
605
+ this.notify(`命令异常:${String(err)}`, { level: 'error' });
606
+ });
607
+ return;
608
+ }
609
+ this.onSubmit?.(this.sessionId, text);
610
+ }
611
+ /** 编辑器内容变更:补全层重取(应答期抑制)+ 固定区脏位 */
612
+ handleEditorChange() {
613
+ if (this.inputAsk === null)
614
+ this.popup.refresh();
615
+ this.touchFixed();
616
+ }
617
+ /* ---------------- 内部:ask 浮层 ---------------- */
618
+ /**
619
+ * 开 ask 浮层:锚定注册表锚 + signal abort 保守值收口 + 撤销说明行 + 重绘
620
+ * 请求。
621
+ *
622
+ * 「曾在屏者」判据(07 §4.3 语义纪律撤销面——confirm / select / askApproval
623
+ * 三路共用本层,统一处理):abort 传播到后端呈现时层仍未关(在 overlay 栈
624
+ * 中)= 曾在屏 → 撤销说明行入正文流;面板已 done(onFinish 先关层)后迟
625
+ * 到的 abort 是 no-op,不误写。说明行只标撤销收场本身——保守值收口(与提
626
+ * 问队列收口三则「保守值同撤销面」同源条款:审批项收 'cancel'、阻塞件各
627
+ * 收保守值)由 promise 回值承载,行文不重复。
628
+ */
629
+ openAskLayer(content, abort, signal, cancelLine) {
630
+ const handle = this.stack.open(content, this.anchorFor(content));
631
+ signal?.addEventListener('abort', () => {
632
+ if (!handle.closed)
633
+ this.appendTransientLine(cancelLine); // 曾在屏才写——面板 done 后迟到 abort 不误写
634
+ handle.close();
635
+ abort(); // 面板 done 锁下迟到 abort 是 no-op(已应答)
636
+ this.touchFixed();
637
+ }, { once: true });
638
+ this.touchFixed();
639
+ return handle;
640
+ }
641
+ /** 锚定闭包:读 renderFixed 行账(首帧前回退固定区顶整宽) */
642
+ anchorFor(content) {
643
+ return (frame) => this.overlayLayout.get(content) ?? { row: 0, col: 0, width: frame.width, height: content.measure(frame.width) };
644
+ }
645
+ /* ---------------- 内部:渲染合并 ---------------- */
646
+ /** present op 入队(连续 present 合并留末次——到达序相对 transient 保持) */
647
+ enqueuePresent() {
648
+ const last = this.pendingOps[this.pendingOps.length - 1];
649
+ if (last !== undefined && last.kind === 'present')
650
+ this.pendingOps.pop();
651
+ this.pendingOps.push({ kind: 'present', blocks: [...this.transcript.snapshot] });
652
+ }
653
+ /** 固定区脏位 + 渲染请求(touch 固定区的统一入口) */
654
+ touchFixed() {
655
+ this.needFixed = true;
656
+ this.requestRender();
657
+ }
658
+ /** 渲染请求(合并位):同步直出立即 flush;注入调度则 fps 帽下排帧 */
659
+ requestRender() {
660
+ if (!this.running || this.suspendedMain)
661
+ return; // 挂起闸:渲染请求安全 no-op(停屏期零写出)
662
+ if (this.scheduleFn === null) {
663
+ this.flush();
664
+ return;
665
+ }
666
+ if (this.frameHandle === null) {
667
+ const due = Math.max(0, this.lastFlushAt + this.minFrameMs - this.now());
668
+ this.frameHandle = this.scheduleFn(() => {
669
+ this.frameHandle = null;
670
+ this.flush();
671
+ }, due);
672
+ }
673
+ }
674
+ /** 帧落地:op 队列按到达序执行 + 固定区脏位一帧一次重建 */
675
+ flush() {
676
+ if (!this.running || this.suspendedMain)
677
+ return; // 挂起闸(防御位——suspendMain 已收在飞帧回调)
678
+ this.lastFlushAt = this.now();
679
+ const ops = this.pendingOps;
680
+ this.pendingOps = [];
681
+ for (const op of ops) {
682
+ if (op.kind === 'present')
683
+ this.screen.present(op.blocks);
684
+ else
685
+ this.screen.appendTransient(op.lines);
686
+ }
687
+ if (this.needFixed) {
688
+ this.needFixed = false;
689
+ this.renderFixed();
690
+ }
691
+ }
692
+ /** 状态行转轮自驱定时器(注入调度后自重排;忙态外 tick 零开销) */
693
+ armTick() {
694
+ this.tickHandle = this.scheduleFn(() => {
695
+ this.tickHandle = null;
696
+ this.tick();
697
+ this.armTick();
698
+ }, TICK_INTERVAL_MS);
699
+ }
700
+ /** 定时器收口(frame/tick/escape 三名) */
701
+ cancelTimer(which) {
702
+ const handle = which === 'frame' ? this.frameHandle : which === 'tick' ? this.tickHandle : this.escapeHandle;
703
+ if (handle !== null)
704
+ this.cancelFn(handle);
705
+ if (which === 'frame')
706
+ this.frameHandle = null;
707
+ else if (which === 'tick')
708
+ this.tickHandle = null;
709
+ else
710
+ this.escapeHandle = null;
711
+ }
712
+ /* ---------------- 内部:状态面与固定区 ---------------- */
713
+ /**
714
+ * 聚焦事件的固定区消费面:run 启停驱动忙态与 usage 累计(件 6)、工具
715
+ * 执行驱动状态行工具名与进度面板(件 3/5)、tool_execution_end/agent_end
716
+ * 驱动 todo 刷新(件 4)。(执行层事件正文零渲染——07 §4.1 直播路渲染
717
+ * 单源的刻意分立。)
718
+ */
719
+ applyFocusedEvent(event) {
720
+ switch (event.type) {
721
+ case 'agent_start':
722
+ this.statusLine.start();
723
+ this.resetUsage(); // 件 6:归零清行(上一 run 尾注不跨 run)
724
+ this.toolPanel.clear(); // 件 5:瞬时面清板
725
+ this.touchFixed();
726
+ break;
727
+ case 'agent_end':
728
+ this.statusLine.stop();
729
+ // 件 6:落行(与 setStatus 同载体 last-writer-wins)
730
+ this.statusLine.setStatus(`✓ 用量 ${formatTokenCount(this.usageTotal.totalTokens)}`);
731
+ this.toolPanel.clear();
732
+ this.refreshTodo(); // 件 4:刷新三时点之三
733
+ this.touchFixed();
734
+ break;
735
+ case 'tool_execution_start':
736
+ this.statusLine.setTool(event.name);
737
+ this.toolPanel.begin(event.toolCallId, event.name); // 件 5:建档不建行
738
+ this.touchFixed();
739
+ break;
740
+ case 'tool_execution_end':
741
+ this.statusLine.setTool(null);
742
+ this.toolPanel.end(event.toolCallId); // 件 5:end 即摘行
743
+ this.refreshTodo(); // 件 4:刷新三时点之二(写后即显)
744
+ this.touchFixed();
745
+ break;
746
+ case 'tool_execution_update':
747
+ this.toolPanel.applyUpdate(event.toolCallId, event.update); // 件 5:首 update 建行
748
+ this.touchFixed();
749
+ break;
750
+ case 'message_end':
751
+ // 件 6 数据源:assistant 消息终值暂存(累加时点 turn_end——一 turn 恰
752
+ // 一 assistant 消息,两时点等价;spec 条款按本仓轻载荷形取 message_end 面)
753
+ if (isStandardMessage(event.message) && event.message.role === 'assistant') {
754
+ this.pendingUsage = event.message.usage;
755
+ }
756
+ break; // 正文换装已走直播路——固定区零扰动
757
+ case 'turn_end':
758
+ this.accumulateUsage(); // 件 6:累加时点(非呈现时点)
759
+ break;
760
+ default:
761
+ break; // 消息族其余/turn 族其余不触固定区
762
+ }
763
+ }
764
+ /** usage 归零清行(agent_start / repaint——件 6 清账重计条款) */
765
+ resetUsage() {
766
+ this.pendingUsage = null;
767
+ this.usageTotal = ZERO_USAGE;
768
+ this.statusLine.setStatus('');
769
+ }
770
+ /** turn_end 累加(暂存的 assistant 用量并入 run 级账本;cost 在场累货币额) */
771
+ accumulateUsage() {
772
+ const pending = this.pendingUsage;
773
+ if (pending === null)
774
+ return;
775
+ this.pendingUsage = null;
776
+ const prev = this.usageTotal;
777
+ this.usageTotal = {
778
+ input: prev.input + pending.input,
779
+ output: prev.output + pending.output,
780
+ cacheRead: prev.cacheRead + pending.cacheRead,
781
+ cacheWrite: prev.cacheWrite + pending.cacheWrite,
782
+ totalTokens: prev.totalTokens + pending.totalTokens,
783
+ cost: prev.cost + (pending.cost?.total ?? 0),
784
+ currency: prev.currency ?? pending.cost?.currency ?? null,
785
+ };
786
+ }
787
+ /** todo 面板刷新(todoFor 注入缺席 = 面板缺席零变化——件 4 条款) */
788
+ refreshTodo() {
789
+ if (this.todoFor === undefined)
790
+ return;
791
+ this.todoPanel.update(this.todoFor(this.sessionId));
792
+ }
793
+ /**
794
+ * 件 7 进度态按会话净计数(任一会话在飞即忙——终端标签页注意力模型):
795
+ * agent_start +1 / agent_end -1,均按信封 sessionId 归账、clamp ≥ 0(进程
796
+ * 内事件无错过窗——clamp 只防重复 end 不穿底;归零删条目)。聚焦位不参
797
+ * 账(07 件 7 判据勘正——事件时刻聚焦位分两路在切焦场景与主句背离)。
798
+ * 忙闲迁移才写 progress 序列(同态静默——周期重发归 osc 保活自持)。
799
+ * stop 后静默短路(终退不再写字节)。
800
+ */
801
+ trackProgress(env) {
802
+ if (!this.running)
803
+ return; // 停后残事件防御——与 requestRender 同闸
804
+ const { event } = env;
805
+ if (event.type === 'agent_start') {
806
+ this.inFlightBySession.set(env.sessionId, (this.inFlightBySession.get(env.sessionId) ?? 0) + 1);
807
+ }
808
+ else if (event.type === 'agent_end') {
809
+ // clamp ≥ 0:净计数不越零(重复 end 不累积负账);归零删条目
810
+ const next = Math.max(0, (this.inFlightBySession.get(env.sessionId) ?? 0) - 1);
811
+ if (next > 0)
812
+ this.inFlightBySession.set(env.sessionId, next);
813
+ else
814
+ this.inFlightBySession.delete(env.sessionId);
815
+ }
816
+ else {
817
+ return; // run 启停族之外零扰动
818
+ }
819
+ let busy = false;
820
+ for (const count of this.inFlightBySession.values()) {
821
+ if (count > 0) {
822
+ busy = true;
823
+ break;
824
+ }
825
+ }
826
+ if (busy !== this.progressBusy) {
827
+ this.progressBusy = busy;
828
+ this.osc.setProgress(busy);
829
+ }
830
+ }
831
+ /**
832
+ * 固定区 v2 重建(自上而下段序):overlay 段(各层量高叠放 + 锚定注册表
833
+ * 行账)→ todo 面板(件 4——todoFor 缺席/空表即零行)→ input-ask 提示行
834
+ * → 补全弹层 → 编辑器(动态量高;聚焦态 = 无 overlay 占焦)→ 工具进度
835
+ * 面板(件 5——与状态行分职互补相邻)→ 状态行。编辑光标经 EditorView
836
+ * setCursor 声明 → MainScreen.setFixed 声明位落 cup。
837
+ */
838
+ renderFixed() {
839
+ const columns = this.io.size().columns;
840
+ const contents = this.stack.contents;
841
+ const overlayHeights = contents.map((c) => c.measure(columns));
842
+ const overlayHeight = overlayHeights.reduce((sum, h) => sum + h, 0);
843
+ const todoHeight = this.todoPanel.measure(columns);
844
+ const askHeight = this.inputAsk !== null ? 1 : 0;
845
+ const popupHeight = this.popup.visible ? this.popup.measure(columns) : 0;
846
+ const editorHeight = this.editor.measure(columns);
847
+ const toolHeight = this.toolPanel.measure(columns);
848
+ const total = overlayHeight + todoHeight + askHeight + popupHeight + editorHeight + toolHeight + 1;
849
+ const grid = new CellGrid(columns, total);
850
+ let row = 0;
851
+ // 段一:overlay 段(栈序自上而下叠放;锚定注册表即本段行账)
852
+ for (let i = 0; i < contents.length; i++) {
853
+ const height = overlayHeights[i];
854
+ this.overlayLayout.set(contents[i], { row, col: 0, width: columns, height });
855
+ contents[i].render(grid, { row, col: 0, width: columns, height });
856
+ row += height;
857
+ }
858
+ // 段二:todo 面板(件 4——输入框上方紧凑面板;清板即零行)
859
+ if (todoHeight > 0) {
860
+ this.todoPanel.render(grid, { row, col: 0, width: columns, height: todoHeight });
861
+ row += todoHeight;
862
+ }
863
+ // 段三:input-ask 提示行(应答期编辑器转应答车的引导位)
864
+ if (this.inputAsk !== null) {
865
+ grid.writeText(row, 0, `? ${this.inputAsk.message}`, { dim: true });
866
+ row += 1;
867
+ }
868
+ // 段四:补全弹层(可见才占位——非模态浮层)
869
+ if (this.popup.visible) {
870
+ this.popup.render(grid, { row, col: 0, width: columns, height: popupHeight });
871
+ row += popupHeight;
872
+ }
873
+ // 段五:编辑器(overlay 占焦期非聚焦——边框普通态 + 不抢光标声明)
874
+ this.editor.setFocused(this.stack.size === 0);
875
+ this.editor.render(grid, { row, col: 0, width: columns, height: editorHeight });
876
+ row += editorHeight;
877
+ // 段六:工具进度面板(件 5——正在流 partial 的工具各占一行;清板即零行)
878
+ if (toolHeight > 0) {
879
+ this.toolPanel.render(grid, { row, col: 0, width: columns, height: toolHeight });
880
+ row += toolHeight;
881
+ }
882
+ // 段七:状态行(固定区末行)
883
+ this.statusLine.render(grid, { row, col: 0, width: columns, height: 1 });
884
+ this.screen.setFixed(grid);
885
+ }
886
+ /** 硬退复原钩子(仅真 ProcessTerminalIO——注入 io 零污染;Engine 同形) */
887
+ armExitRestore() {
888
+ if (!(this.io instanceof ProcessTerminalIO))
889
+ return;
890
+ this.disarmExitRestore?.();
891
+ const restore = () => {
892
+ try {
893
+ this.io.write(LEAVE_MAIN);
894
+ this.io.setRawMode(false);
895
+ this.osc.restore(); // 件 7:硬退复原两写点(title 基线 + 进度清零——与 stop 同收口)
896
+ }
897
+ catch {
898
+ // 复位尽力而为——退出路径不允许二次异常
899
+ }
900
+ };
901
+ process.on('exit', restore);
902
+ this.disarmExitRestore = () => {
903
+ process.removeListener('exit', restore);
904
+ this.disarmExitRestore = null;
905
+ };
906
+ }
907
+ }
908
+ /**
909
+ * 摘要行 ANSI 序列化:行首段(档位符号 + 会话短 id)着会话区分色——
910
+ * 非 accent 家族的第二着色位(07 §4.1 呈现面件 9),label 段裸文本。
911
+ */
912
+ function summaryToAnsi(line) {
913
+ const head = `${line.symbol} ${line.shortId}`;
914
+ return buildSgr({ fg: sessionColor(line.shortId) }) + head + SGR_RESET + ` ${line.label}`;
915
+ }
916
+ //# sourceMappingURL=tui-backend.js.map