hyacinth-ai 0.9.6

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 (704) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/commands.json +25 -0
  4. package/dist/agents/builtins.d.ts +8 -0
  5. package/dist/agents/builtins.js +50 -0
  6. package/dist/agents/config-loader.d.ts +15 -0
  7. package/dist/agents/config-loader.js +75 -0
  8. package/dist/agents/delegate-tool.d.ts +109 -0
  9. package/dist/agents/delegate-tool.js +407 -0
  10. package/dist/agents/index.d.ts +5 -0
  11. package/dist/agents/index.js +5 -0
  12. package/dist/agents/registry.d.ts +2 -0
  13. package/dist/agents/registry.js +2 -0
  14. package/dist/bypass/agents/companion/index.d.ts +57 -0
  15. package/dist/bypass/agents/companion/index.js +487 -0
  16. package/dist/bypass/agents/orchestrator/index.d.ts +17 -0
  17. package/dist/bypass/agents/orchestrator/index.js +414 -0
  18. package/dist/bypass/agents/orchestrator.d.ts +16 -0
  19. package/dist/bypass/agents/orchestrator.js +377 -0
  20. package/dist/bypass/agents/world-engine/agent.d.ts +41 -0
  21. package/dist/bypass/agents/world-engine/agent.js +385 -0
  22. package/dist/bypass/agents/world-engine/index.d.ts +52 -0
  23. package/dist/bypass/agents/world-engine/index.js +198 -0
  24. package/dist/bypass/agents/world-engine/sim.d.ts +14 -0
  25. package/dist/bypass/agents/world-engine/sim.js +120 -0
  26. package/dist/bypass/agents/world-engine/store.d.ts +110 -0
  27. package/dist/bypass/agents/world-engine/store.js +519 -0
  28. package/dist/bypass/agents/world-engine/ticker.d.ts +37 -0
  29. package/dist/bypass/agents/world-engine/ticker.js +214 -0
  30. package/dist/bypass/agents/world-engine/tools.d.ts +6 -0
  31. package/dist/bypass/agents/world-engine/tools.js +380 -0
  32. package/dist/bypass/agents/world-engine/types.d.ts +202 -0
  33. package/dist/bypass/agents/world-engine/types.js +56 -0
  34. package/dist/bypass/agents/world-engine.d.ts +20 -0
  35. package/dist/bypass/agents/world-engine.js +63 -0
  36. package/dist/bypass/base.d.ts +48 -0
  37. package/dist/bypass/base.js +126 -0
  38. package/dist/bypass/index.d.ts +5 -0
  39. package/dist/bypass/index.js +6 -0
  40. package/dist/bypass/manager.d.ts +41 -0
  41. package/dist/bypass/manager.js +179 -0
  42. package/dist/bypass/types.d.ts +63 -0
  43. package/dist/bypass/types.js +14 -0
  44. package/dist/channels/auto-detect.d.ts +25 -0
  45. package/dist/channels/auto-detect.js +77 -0
  46. package/dist/channels/builtin/http-webhook.d.ts +25 -0
  47. package/dist/channels/builtin/http-webhook.js +287 -0
  48. package/dist/channels/builtin/tui-channel.d.ts +33 -0
  49. package/dist/channels/builtin/tui-channel.js +61 -0
  50. package/dist/channels/builtin/tui-ws-session.d.ts +16 -0
  51. package/dist/channels/builtin/tui-ws-session.js +90 -0
  52. package/dist/channels/builtin/webui-output-handler.d.ts +30 -0
  53. package/dist/channels/builtin/webui-output-handler.js +93 -0
  54. package/dist/channels/index.d.ts +5 -0
  55. package/dist/channels/index.js +3 -0
  56. package/dist/channels/interface.d.ts +148 -0
  57. package/dist/channels/interface.js +15 -0
  58. package/dist/channels/manager.d.ts +43 -0
  59. package/dist/channels/manager.js +156 -0
  60. package/dist/channels/message-queue.d.ts +36 -0
  61. package/dist/channels/message-queue.js +91 -0
  62. package/dist/channels/plugins/clawbot/clawbot-auth.d.ts +50 -0
  63. package/dist/channels/plugins/clawbot/clawbot-auth.js +235 -0
  64. package/dist/channels/plugins/clawbot/clawbot-channel.d.ts +76 -0
  65. package/dist/channels/plugins/clawbot/clawbot-channel.js +566 -0
  66. package/dist/channels/plugins/clawbot/clawbot-client.d.ts +180 -0
  67. package/dist/channels/plugins/clawbot/clawbot-client.js +312 -0
  68. package/dist/channels/plugins/clawbot/clawbot-config.d.ts +30 -0
  69. package/dist/channels/plugins/clawbot/clawbot-config.js +28 -0
  70. package/dist/channels/plugins/clawbot/clawbot-message-queue.d.ts +27 -0
  71. package/dist/channels/plugins/clawbot/clawbot-message-queue.js +114 -0
  72. package/dist/channels/plugins/clawbot/clawbot-session.d.ts +10 -0
  73. package/dist/channels/plugins/clawbot/clawbot-session.js +26 -0
  74. package/dist/channels/plugins/clawbot/index.d.ts +17 -0
  75. package/dist/channels/plugins/clawbot/index.js +58 -0
  76. package/dist/channels/plugins/feishu/feishu-channel.d.ts +57 -0
  77. package/dist/channels/plugins/feishu/feishu-channel.js +602 -0
  78. package/dist/channels/plugins/feishu/feishu-client.d.ts +23 -0
  79. package/dist/channels/plugins/feishu/feishu-client.js +86 -0
  80. package/dist/channels/plugins/feishu/feishu-config.d.ts +53 -0
  81. package/dist/channels/plugins/feishu/feishu-config.js +35 -0
  82. package/dist/channels/plugins/feishu/feishu-event.d.ts +102 -0
  83. package/dist/channels/plugins/feishu/feishu-event.js +257 -0
  84. package/dist/channels/plugins/feishu/feishu-log-suppressor.d.ts +7 -0
  85. package/dist/channels/plugins/feishu/feishu-log-suppressor.js +34 -0
  86. package/dist/channels/plugins/feishu/feishu-message-queue.d.ts +27 -0
  87. package/dist/channels/plugins/feishu/feishu-message-queue.js +112 -0
  88. package/dist/channels/plugins/feishu/feishu-send.d.ts +44 -0
  89. package/dist/channels/plugins/feishu/feishu-send.js +232 -0
  90. package/dist/channels/plugins/feishu/feishu-session.d.ts +44 -0
  91. package/dist/channels/plugins/feishu/feishu-session.js +84 -0
  92. package/dist/channels/plugins/feishu/feishu-streaming.d.ts +49 -0
  93. package/dist/channels/plugins/feishu/feishu-streaming.js +185 -0
  94. package/dist/channels/plugins/feishu/feishu-transport.d.ts +29 -0
  95. package/dist/channels/plugins/feishu/feishu-transport.js +99 -0
  96. package/dist/channels/plugins/feishu/index.d.ts +22 -0
  97. package/dist/channels/plugins/feishu/index.js +68 -0
  98. package/dist/cli/doctor.d.ts +20 -0
  99. package/dist/cli/doctor.js +317 -0
  100. package/dist/context/cache-strategy.d.ts +76 -0
  101. package/dist/context/cache-strategy.js +123 -0
  102. package/dist/context/composer.d.ts +77 -0
  103. package/dist/context/composer.js +318 -0
  104. package/dist/context/compressor.d.ts +218 -0
  105. package/dist/context/compressor.js +909 -0
  106. package/dist/context/index.d.ts +14 -0
  107. package/dist/context/index.js +8 -0
  108. package/dist/context/interface.d.ts +33 -0
  109. package/dist/context/interface.js +2 -0
  110. package/dist/context/manifest-defaults.d.ts +51 -0
  111. package/dist/context/manifest-defaults.js +133 -0
  112. package/dist/context/manifest-loader.d.ts +16 -0
  113. package/dist/context/manifest-loader.js +103 -0
  114. package/dist/context/manifest-types.d.ts +26 -0
  115. package/dist/context/manifest-types.js +2 -0
  116. package/dist/context/precision/analyzer.d.ts +31 -0
  117. package/dist/context/precision/analyzer.js +131 -0
  118. package/dist/context/precision/companion.d.ts +27 -0
  119. package/dist/context/precision/companion.js +144 -0
  120. package/dist/context/precision/default.d.ts +14 -0
  121. package/dist/context/precision/default.js +17 -0
  122. package/dist/context/precision/index.d.ts +9 -0
  123. package/dist/context/precision/index.js +7 -0
  124. package/dist/context/precision/keyword-pool.d.ts +27 -0
  125. package/dist/context/precision/keyword-pool.js +66 -0
  126. package/dist/context/precision/strategy.d.ts +18 -0
  127. package/dist/context/precision/strategy.js +51 -0
  128. package/dist/context/precision/summary-store.d.ts +18 -0
  129. package/dist/context/precision/summary-store.js +67 -0
  130. package/dist/context/precision/types.d.ts +23 -0
  131. package/dist/context/precision/types.js +2 -0
  132. package/dist/context/profiles.d.ts +45 -0
  133. package/dist/context/profiles.js +92 -0
  134. package/dist/context/prompt-builder.d.ts +76 -0
  135. package/dist/context/prompt-builder.js +133 -0
  136. package/dist/context/retriever.d.ts +44 -0
  137. package/dist/context/retriever.js +203 -0
  138. package/dist/context/router.d.ts +122 -0
  139. package/dist/context/router.js +291 -0
  140. package/dist/context/section-resolver.d.ts +38 -0
  141. package/dist/context/section-resolver.js +398 -0
  142. package/dist/context/tokenizer.d.ts +23 -0
  143. package/dist/context/tokenizer.js +78 -0
  144. package/dist/dependency/analyzer.d.ts +33 -0
  145. package/dist/dependency/analyzer.js +242 -0
  146. package/dist/dependency/data-flow-tracker.d.ts +12 -0
  147. package/dist/dependency/data-flow-tracker.js +97 -0
  148. package/dist/dependency/function-parser.d.ts +34 -0
  149. package/dist/dependency/function-parser.js +295 -0
  150. package/dist/dependency/index.d.ts +14 -0
  151. package/dist/dependency/index.js +20 -0
  152. package/dist/dependency/parser.d.ts +10 -0
  153. package/dist/dependency/parser.js +93 -0
  154. package/dist/dependency/types.d.ts +57 -0
  155. package/dist/dependency/types.js +2 -0
  156. package/dist/env/env-collector.d.ts +45 -0
  157. package/dist/env/env-collector.js +221 -0
  158. package/dist/env/index.d.ts +3 -0
  159. package/dist/env/index.js +2 -0
  160. package/dist/event-store.d.ts +27 -0
  161. package/dist/event-store.js +71 -0
  162. package/dist/evolution/git-manager.d.ts +46 -0
  163. package/dist/evolution/git-manager.js +276 -0
  164. package/dist/evolution/index.d.ts +3 -0
  165. package/dist/evolution/index.js +2 -0
  166. package/dist/flow/bootstrap-flow.d.ts +19 -0
  167. package/dist/flow/bootstrap-flow.js +72 -0
  168. package/dist/flow/flow-registry.d.ts +12 -0
  169. package/dist/flow/flow-registry.js +74 -0
  170. package/dist/flow/flow-tools.d.ts +8 -0
  171. package/dist/flow/flow-tools.js +106 -0
  172. package/dist/flow/index.d.ts +6 -0
  173. package/dist/flow/index.js +8 -0
  174. package/dist/flow/todo-flow.d.ts +25 -0
  175. package/dist/flow/todo-flow.js +107 -0
  176. package/dist/flow/types.d.ts +78 -0
  177. package/dist/flow/types.js +12 -0
  178. package/dist/flows/bootstrap.d.ts +18 -0
  179. package/dist/flows/bootstrap.js +119 -0
  180. package/dist/flows/index.d.ts +6 -0
  181. package/dist/flows/index.js +11 -0
  182. package/dist/flows/todo.d.ts +26 -0
  183. package/dist/flows/todo.js +174 -0
  184. package/dist/flows/tools.d.ts +8 -0
  185. package/dist/flows/tools.js +119 -0
  186. package/dist/flows/types.d.ts +27 -0
  187. package/dist/flows/types.js +10 -0
  188. package/dist/gateway/cli.d.ts +5 -0
  189. package/dist/gateway/cli.js +1027 -0
  190. package/dist/gateway/factory.d.ts +80 -0
  191. package/dist/gateway/factory.js +832 -0
  192. package/dist/gateway/guardian.d.ts +8 -0
  193. package/dist/gateway/guardian.js +30 -0
  194. package/dist/gateway/index.d.ts +3 -0
  195. package/dist/gateway/index.js +3 -0
  196. package/dist/gateway/server.d.ts +17 -0
  197. package/dist/gateway/server.js +96 -0
  198. package/dist/gateway/tui.d.ts +16 -0
  199. package/dist/gateway/tui.js +3273 -0
  200. package/dist/hot-reload/agent-watcher.d.ts +24 -0
  201. package/dist/hot-reload/agent-watcher.js +175 -0
  202. package/dist/hot-reload/bundle-watcher.d.ts +12 -0
  203. package/dist/hot-reload/bundle-watcher.js +35 -0
  204. package/dist/hot-reload/channel-watcher.d.ts +19 -0
  205. package/dist/hot-reload/channel-watcher.js +54 -0
  206. package/dist/hot-reload/command-watcher.d.ts +14 -0
  207. package/dist/hot-reload/command-watcher.js +54 -0
  208. package/dist/hot-reload/config-watcher.d.ts +20 -0
  209. package/dist/hot-reload/config-watcher.js +58 -0
  210. package/dist/hot-reload/index.d.ts +12 -0
  211. package/dist/hot-reload/index.js +12 -0
  212. package/dist/hot-reload/manager.d.ts +58 -0
  213. package/dist/hot-reload/manager.js +243 -0
  214. package/dist/hot-reload/manifest-watcher.d.ts +9 -0
  215. package/dist/hot-reload/manifest-watcher.js +42 -0
  216. package/dist/hot-reload/mcp-watcher.d.ts +18 -0
  217. package/dist/hot-reload/mcp-watcher.js +64 -0
  218. package/dist/hot-reload/model-catalog-watcher.d.ts +18 -0
  219. package/dist/hot-reload/model-catalog-watcher.js +62 -0
  220. package/dist/hot-reload/plugin-watcher.d.ts +20 -0
  221. package/dist/hot-reload/plugin-watcher.js +56 -0
  222. package/dist/hot-reload/prompt-watcher.d.ts +6 -0
  223. package/dist/hot-reload/prompt-watcher.js +38 -0
  224. package/dist/hot-reload/provider-watcher.d.ts +16 -0
  225. package/dist/hot-reload/provider-watcher.js +45 -0
  226. package/dist/hot-reload/skill-watcher.d.ts +19 -0
  227. package/dist/hot-reload/skill-watcher.js +82 -0
  228. package/dist/hot-reload/tool-watcher.d.ts +16 -0
  229. package/dist/hot-reload/tool-watcher.js +168 -0
  230. package/dist/hot-reload/workflow-watcher.d.ts +19 -0
  231. package/dist/hot-reload/workflow-watcher.js +84 -0
  232. package/dist/index.d.ts +3 -0
  233. package/dist/index.js +6 -0
  234. package/dist/knowledge/fts5-retriever.d.ts +30 -0
  235. package/dist/knowledge/fts5-retriever.js +223 -0
  236. package/dist/knowledge/index.d.ts +11 -0
  237. package/dist/knowledge/index.js +14 -0
  238. package/dist/knowledge/indexer.d.ts +41 -0
  239. package/dist/knowledge/indexer.js +186 -0
  240. package/dist/knowledge/query-preprocessor.d.ts +13 -0
  241. package/dist/knowledge/query-preprocessor.js +63 -0
  242. package/dist/knowledge/retriever.d.ts +45 -0
  243. package/dist/knowledge/retriever.js +9 -0
  244. package/dist/knowledge/store.d.ts +27 -0
  245. package/dist/knowledge/store.js +61 -0
  246. package/dist/knowledge/structured-store.d.ts +78 -0
  247. package/dist/knowledge/structured-store.js +280 -0
  248. package/dist/knowledge/structured-tools.d.ts +12 -0
  249. package/dist/knowledge/structured-tools.js +151 -0
  250. package/dist/knowledge/tools.d.ts +20 -0
  251. package/dist/knowledge/tools.js +238 -0
  252. package/dist/knowledge/watcher.d.ts +28 -0
  253. package/dist/knowledge/watcher.js +113 -0
  254. package/dist/lifecycle/index.d.ts +12 -0
  255. package/dist/lifecycle/index.js +14 -0
  256. package/dist/lifecycle/interface.d.ts +94 -0
  257. package/dist/lifecycle/interface.js +2 -0
  258. package/dist/lifecycle/local-model.d.ts +62 -0
  259. package/dist/lifecycle/local-model.js +331 -0
  260. package/dist/lifecycle/manager.d.ts +70 -0
  261. package/dist/lifecycle/manager.js +374 -0
  262. package/dist/lifecycle/supervisor.d.ts +89 -0
  263. package/dist/lifecycle/supervisor.js +277 -0
  264. package/dist/lifecycle-integration-test.d.ts +2 -0
  265. package/dist/lifecycle-integration-test.js +72 -0
  266. package/dist/local-model/download-manager.d.ts +59 -0
  267. package/dist/local-model/download-manager.js +517 -0
  268. package/dist/local-model/index.d.ts +109 -0
  269. package/dist/local-model/index.js +233 -0
  270. package/dist/local-model/llamacpp-provider.d.ts +95 -0
  271. package/dist/local-model/llamacpp-provider.js +379 -0
  272. package/dist/local-model/model-bridge.d.ts +48 -0
  273. package/dist/local-model/model-bridge.js +256 -0
  274. package/dist/local-model/model-registry.d.ts +47 -0
  275. package/dist/local-model/model-registry.js +188 -0
  276. package/dist/local-model/types.d.ts +49 -0
  277. package/dist/local-model/types.js +2 -0
  278. package/dist/logging/index.d.ts +3 -0
  279. package/dist/logging/index.js +3 -0
  280. package/dist/logging/logger.d.ts +56 -0
  281. package/dist/logging/logger.js +94 -0
  282. package/dist/machine/flows/bootstrap.d.ts +18 -0
  283. package/dist/machine/flows/bootstrap.js +119 -0
  284. package/dist/machine/flows/index.d.ts +6 -0
  285. package/dist/machine/flows/index.js +11 -0
  286. package/dist/machine/flows/spec.d.ts +17 -0
  287. package/dist/machine/flows/spec.js +176 -0
  288. package/dist/machine/flows/todo.d.ts +26 -0
  289. package/dist/machine/flows/todo.js +175 -0
  290. package/dist/machine/flows/tools.d.ts +8 -0
  291. package/dist/machine/flows/tools.js +119 -0
  292. package/dist/machine/flows/types.d.ts +33 -0
  293. package/dist/machine/flows/types.js +10 -0
  294. package/dist/machine/index.d.ts +8 -0
  295. package/dist/machine/index.js +9 -0
  296. package/dist/machine/registry.d.ts +34 -0
  297. package/dist/machine/registry.js +98 -0
  298. package/dist/machine/runner.d.ts +41 -0
  299. package/dist/machine/runner.js +177 -0
  300. package/dist/machine/types.d.ts +121 -0
  301. package/dist/machine/types.js +9 -0
  302. package/dist/mcp/bridge.d.ts +33 -0
  303. package/dist/mcp/bridge.js +99 -0
  304. package/dist/mcp/client.d.ts +37 -0
  305. package/dist/mcp/client.js +161 -0
  306. package/dist/mcp/config.d.ts +15 -0
  307. package/dist/mcp/config.js +85 -0
  308. package/dist/mcp/index.d.ts +7 -0
  309. package/dist/mcp/index.js +7 -0
  310. package/dist/mcp/install-manager.d.ts +31 -0
  311. package/dist/mcp/install-manager.js +158 -0
  312. package/dist/mcp/lifecycle.d.ts +41 -0
  313. package/dist/mcp/lifecycle.js +175 -0
  314. package/dist/mcp/process-transport.d.ts +21 -0
  315. package/dist/mcp/process-transport.js +59 -0
  316. package/dist/mcp/side-effect.d.ts +5 -0
  317. package/dist/mcp/side-effect.js +21 -0
  318. package/dist/mcp/system.d.ts +95 -0
  319. package/dist/mcp/system.js +279 -0
  320. package/dist/memory/companion-session.d.ts +22 -0
  321. package/dist/memory/companion-session.js +77 -0
  322. package/dist/memory/compression-boundary.d.ts +20 -0
  323. package/dist/memory/compression-boundary.js +40 -0
  324. package/dist/memory/conversation.d.ts +34 -0
  325. package/dist/memory/conversation.js +107 -0
  326. package/dist/memory/events.d.ts +42 -0
  327. package/dist/memory/events.js +56 -0
  328. package/dist/memory/index.d.ts +7 -0
  329. package/dist/memory/index.js +6 -0
  330. package/dist/memory/memory-store.d.ts +11 -0
  331. package/dist/memory/memory-store.js +51 -0
  332. package/dist/memory/session-allowlist.d.ts +11 -0
  333. package/dist/memory/session-allowlist.js +50 -0
  334. package/dist/memory/session.d.ts +48 -0
  335. package/dist/memory/session.js +275 -0
  336. package/dist/memory/stats.d.ts +23 -0
  337. package/dist/memory/stats.js +59 -0
  338. package/dist/memory/summary.d.ts +7 -0
  339. package/dist/memory/summary.js +23 -0
  340. package/dist/multimodal/index.d.ts +66 -0
  341. package/dist/multimodal/index.js +210 -0
  342. package/dist/orchestrator/index.d.ts +5 -0
  343. package/dist/orchestrator/index.js +4 -0
  344. package/dist/orchestrator/loop.d.ts +331 -0
  345. package/dist/orchestrator/loop.js +2443 -0
  346. package/dist/orchestrator/plan-store.d.ts +81 -0
  347. package/dist/orchestrator/plan-store.js +167 -0
  348. package/dist/orchestrator/planner.d.ts +20 -0
  349. package/dist/orchestrator/planner.js +34 -0
  350. package/dist/parser/index.d.ts +2 -0
  351. package/dist/parser/index.js +2 -0
  352. package/dist/parser/router.d.ts +30 -0
  353. package/dist/parser/router.js +61 -0
  354. package/dist/plugins/api.d.ts +39 -0
  355. package/dist/plugins/api.js +85 -0
  356. package/dist/plugins/define.d.ts +27 -0
  357. package/dist/plugins/define.js +32 -0
  358. package/dist/plugins/index.d.ts +9 -0
  359. package/dist/plugins/index.js +5 -0
  360. package/dist/plugins/loader.d.ts +39 -0
  361. package/dist/plugins/loader.js +128 -0
  362. package/dist/plugins/manager.d.ts +61 -0
  363. package/dist/plugins/manager.js +257 -0
  364. package/dist/plugins/types.d.ts +82 -0
  365. package/dist/plugins/types.js +2 -0
  366. package/dist/prompts/agents/code-reviewer.md +16 -0
  367. package/dist/prompts/agents/security-auditor.md +17 -0
  368. package/dist/prompts/agents/test-writer.md +15 -0
  369. package/dist/prompts/attention/attention.md +12 -0
  370. package/dist/prompts/environment/environment.md +18 -0
  371. package/dist/prompts/environment/feishu.md +107 -0
  372. package/dist/prompts/flows/bootstrap-confirm.md +14 -0
  373. package/dist/prompts/flows/bootstrap-identity.md +11 -0
  374. package/dist/prompts/flows/bootstrap-soul.md +19 -0
  375. package/dist/prompts/flows/bootstrap-user.md +11 -0
  376. package/dist/prompts/flows/spec-checklist.md +27 -0
  377. package/dist/prompts/flows/spec-define.md +64 -0
  378. package/dist/prompts/flows/spec-executing-checklist.md +15 -0
  379. package/dist/prompts/flows/spec-executing-tasks.md +15 -0
  380. package/dist/prompts/flows/spec-spec.md +51 -0
  381. package/dist/prompts/flows/spec-tasks.md +32 -0
  382. package/dist/prompts/flows/todo-execution.md +7 -0
  383. package/dist/prompts/flows/todo-planning.md +18 -0
  384. package/dist/prompts/framework/architecture.md +131 -0
  385. package/dist/prompts/identity/identity.md +2 -0
  386. package/dist/prompts/loader.d.ts +24 -0
  387. package/dist/prompts/loader.js +102 -0
  388. package/dist/prompts/loader.ts +119 -0
  389. package/dist/prompts/orchestration/summary.md +14 -0
  390. package/dist/prompts/persona/BOOTSTRAP.md +5 -0
  391. package/dist/prompts/persona/IDENTITY.md +16 -0
  392. package/dist/prompts/persona/PartnerMemory.md +5 -0
  393. package/dist/prompts/persona/PartnerSoul.md +7 -0
  394. package/dist/prompts/persona/SOUL.md +30 -0
  395. package/dist/prompts/persona/USER.md +16 -0
  396. package/dist/prompts/precise/analyze.md +23 -0
  397. package/dist/prompts/precise/persona.md +18 -0
  398. package/dist/prompts/safety/coding-standards.md +5 -0
  399. package/dist/prompts/safety/safety.md +4 -0
  400. package/dist/prompts/skills/code-review.md +4 -0
  401. package/dist/prompts/skills/debug.md +7 -0
  402. package/dist/prompts/skills/framework-reference.md +131 -0
  403. package/dist/prompts/skills/refactor.md +6 -0
  404. package/dist/prompts/summary.md +22 -0
  405. package/dist/prompts/tools/framework-capabilities.md +241 -0
  406. package/dist/prompts/tools/tool-rules.md +15 -0
  407. package/dist/provider/anthropic.d.ts +62 -0
  408. package/dist/provider/anthropic.js +361 -0
  409. package/dist/provider/catalog.d.ts +58 -0
  410. package/dist/provider/catalog.js +97 -0
  411. package/dist/provider/compatible.d.ts +89 -0
  412. package/dist/provider/compatible.js +328 -0
  413. package/dist/provider/config.d.ts +23 -0
  414. package/dist/provider/config.js +158 -0
  415. package/dist/provider/deepseek.d.ts +25 -0
  416. package/dist/provider/deepseek.js +37 -0
  417. package/dist/provider/fallback.d.ts +51 -0
  418. package/dist/provider/fallback.js +113 -0
  419. package/dist/provider/gemini.d.ts +27 -0
  420. package/dist/provider/gemini.js +170 -0
  421. package/dist/provider/index.d.ts +36 -0
  422. package/dist/provider/index.js +42 -0
  423. package/dist/provider/interface.d.ts +62 -0
  424. package/dist/provider/interface.js +2 -0
  425. package/dist/provider/local-config.d.ts +37 -0
  426. package/dist/provider/local-config.js +146 -0
  427. package/dist/provider/local.d.ts +45 -0
  428. package/dist/provider/local.js +248 -0
  429. package/dist/provider/manager.d.ts +67 -0
  430. package/dist/provider/manager.js +355 -0
  431. package/dist/provider/mimo.d.ts +19 -0
  432. package/dist/provider/mimo.js +30 -0
  433. package/dist/provider/minimax.d.ts +22 -0
  434. package/dist/provider/minimax.js +33 -0
  435. package/dist/provider/model-catalog-loader.d.ts +42 -0
  436. package/dist/provider/model-catalog-loader.js +89 -0
  437. package/dist/provider/model-channel-registry.d.ts +130 -0
  438. package/dist/provider/model-channel-registry.js +488 -0
  439. package/dist/provider/model-router.d.ts +60 -0
  440. package/dist/provider/model-router.js +87 -0
  441. package/dist/provider/openai.d.ts +54 -0
  442. package/dist/provider/openai.js +274 -0
  443. package/dist/provider/qwen.d.ts +19 -0
  444. package/dist/provider/qwen.js +30 -0
  445. package/dist/provider/resilient.d.ts +46 -0
  446. package/dist/provider/resilient.js +181 -0
  447. package/dist/provider/retry.d.ts +16 -0
  448. package/dist/provider/retry.js +78 -0
  449. package/dist/provider/router.d.ts +68 -0
  450. package/dist/provider/router.js +145 -0
  451. package/dist/provider/tool-args-recovery.d.ts +36 -0
  452. package/dist/provider/tool-args-recovery.js +91 -0
  453. package/dist/provider/user-id.d.ts +13 -0
  454. package/dist/provider/user-id.js +53 -0
  455. package/dist/provider/zhipu.d.ts +19 -0
  456. package/dist/provider/zhipu.js +32 -0
  457. package/dist/registry/agent.registry.d.ts +57 -0
  458. package/dist/registry/agent.registry.js +224 -0
  459. package/dist/registry/base.d.ts +54 -0
  460. package/dist/registry/base.js +99 -0
  461. package/dist/registry/channel.registry.d.ts +53 -0
  462. package/dist/registry/channel.registry.js +87 -0
  463. package/dist/registry/index.d.ts +9 -0
  464. package/dist/registry/index.js +10 -0
  465. package/dist/registry/mcp.registry.d.ts +2 -0
  466. package/dist/registry/mcp.registry.js +2 -0
  467. package/dist/registry/plugin.registry.d.ts +33 -0
  468. package/dist/registry/plugin.registry.js +66 -0
  469. package/dist/registry/provider.registry.d.ts +51 -0
  470. package/dist/registry/provider.registry.js +104 -0
  471. package/dist/registry/skill.registry.d.ts +33 -0
  472. package/dist/registry/skill.registry.js +95 -0
  473. package/dist/registry/tool.registry.d.ts +68 -0
  474. package/dist/registry/tool.registry.js +185 -0
  475. package/dist/repair/loop-guard.d.ts +98 -0
  476. package/dist/repair/loop-guard.js +217 -0
  477. package/dist/repair/scavenge.d.ts +19 -0
  478. package/dist/repair/scavenge.js +81 -0
  479. package/dist/repair/storm.d.ts +7 -0
  480. package/dist/repair/storm.js +7 -0
  481. package/dist/rollback/index.d.ts +5 -0
  482. package/dist/rollback/index.js +4 -0
  483. package/dist/rollback/rollback-tool.d.ts +13 -0
  484. package/dist/rollback/rollback-tool.js +167 -0
  485. package/dist/rollback/turn-recorder.d.ts +37 -0
  486. package/dist/rollback/turn-recorder.js +183 -0
  487. package/dist/rollback/turn-store.d.ts +29 -0
  488. package/dist/rollback/turn-store.js +114 -0
  489. package/dist/rollback/types.d.ts +51 -0
  490. package/dist/rollback/types.js +9 -0
  491. package/dist/runtime/config-center.d.ts +92 -0
  492. package/dist/runtime/config-center.js +467 -0
  493. package/dist/runtime/config-schema.d.ts +201 -0
  494. package/dist/runtime/config-schema.js +2 -0
  495. package/dist/runtime/defaults.d.ts +3 -0
  496. package/dist/runtime/defaults.js +146 -0
  497. package/dist/schedule/cron.d.ts +24 -0
  498. package/dist/schedule/cron.js +145 -0
  499. package/dist/schedule/index.d.ts +6 -0
  500. package/dist/schedule/index.js +4 -0
  501. package/dist/schedule/persistence.d.ts +33 -0
  502. package/dist/schedule/persistence.js +106 -0
  503. package/dist/schedule/scheduler.d.ts +85 -0
  504. package/dist/schedule/scheduler.js +630 -0
  505. package/dist/schedule/types.d.ts +185 -0
  506. package/dist/schedule/types.js +2 -0
  507. package/dist/setup/config.d.ts +189 -0
  508. package/dist/setup/config.js +253 -0
  509. package/dist/setup/index.d.ts +3 -0
  510. package/dist/setup/index.js +3 -0
  511. package/dist/setup/model-defaults.d.ts +13 -0
  512. package/dist/setup/model-defaults.js +87 -0
  513. package/dist/setup/persona-bootstrap.d.ts +42 -0
  514. package/dist/setup/persona-bootstrap.js +379 -0
  515. package/dist/setup/wizard.d.ts +32 -0
  516. package/dist/setup/wizard.js +384 -0
  517. package/dist/skills/index.d.ts +4 -0
  518. package/dist/skills/index.js +4 -0
  519. package/dist/skills/loader.d.ts +12 -0
  520. package/dist/skills/loader.js +83 -0
  521. package/dist/skills/registry.d.ts +2 -0
  522. package/dist/skills/registry.js +2 -0
  523. package/dist/skills/skill-tool.d.ts +20 -0
  524. package/dist/skills/skill-tool.js +49 -0
  525. package/dist/tools/archive.d.ts +17 -0
  526. package/dist/tools/archive.js +131 -0
  527. package/dist/tools/ask-user.d.ts +9 -0
  528. package/dist/tools/ask-user.js +62 -0
  529. package/dist/tools/background-registry.d.ts +45 -0
  530. package/dist/tools/background-registry.js +164 -0
  531. package/dist/tools/bash.d.ts +56 -0
  532. package/dist/tools/bash.js +391 -0
  533. package/dist/tools/bundle-registry.d.ts +42 -0
  534. package/dist/tools/bundle-registry.js +239 -0
  535. package/dist/tools/bundle-tools.d.ts +11 -0
  536. package/dist/tools/bundle-tools.js +168 -0
  537. package/dist/tools/channel-info.d.ts +5 -0
  538. package/dist/tools/channel-info.js +38 -0
  539. package/dist/tools/compression.d.ts +7 -0
  540. package/dist/tools/compression.js +21 -0
  541. package/dist/tools/config.d.ts +23 -0
  542. package/dist/tools/config.js +255 -0
  543. package/dist/tools/db-query.d.ts +12 -0
  544. package/dist/tools/db-query.js +107 -0
  545. package/dist/tools/diagnostics.d.ts +30 -0
  546. package/dist/tools/diagnostics.js +111 -0
  547. package/dist/tools/diff-channel.d.ts +9 -0
  548. package/dist/tools/diff-channel.js +17 -0
  549. package/dist/tools/diff-files.d.ts +17 -0
  550. package/dist/tools/diff-files.js +88 -0
  551. package/dist/tools/disk-usage.d.ts +34 -0
  552. package/dist/tools/disk-usage.js +258 -0
  553. package/dist/tools/edit.d.ts +30 -0
  554. package/dist/tools/edit.js +235 -0
  555. package/dist/tools/executor.d.ts +32 -0
  556. package/dist/tools/executor.js +66 -0
  557. package/dist/tools/file-tracker.d.ts +20 -0
  558. package/dist/tools/file-tracker.js +44 -0
  559. package/dist/tools/filtered-registry.d.ts +29 -0
  560. package/dist/tools/filtered-registry.js +50 -0
  561. package/dist/tools/flow.d.ts +10 -0
  562. package/dist/tools/flow.js +180 -0
  563. package/dist/tools/git-tool.d.ts +24 -0
  564. package/dist/tools/git-tool.js +113 -0
  565. package/dist/tools/glob.d.ts +29 -0
  566. package/dist/tools/glob.js +152 -0
  567. package/dist/tools/grep.d.ts +14 -0
  568. package/dist/tools/grep.js +325 -0
  569. package/dist/tools/http-request.d.ts +14 -0
  570. package/dist/tools/http-request.js +126 -0
  571. package/dist/tools/index.d.ts +43 -0
  572. package/dist/tools/index.js +76 -0
  573. package/dist/tools/injection-filter.d.ts +6 -0
  574. package/dist/tools/injection-filter.js +71 -0
  575. package/dist/tools/insert.d.ts +13 -0
  576. package/dist/tools/insert.js +61 -0
  577. package/dist/tools/interface.d.ts +30 -0
  578. package/dist/tools/interface.js +2 -0
  579. package/dist/tools/json-edit.d.ts +15 -0
  580. package/dist/tools/json-edit.js +122 -0
  581. package/dist/tools/multi-edit.d.ts +9 -0
  582. package/dist/tools/multi-edit.js +146 -0
  583. package/dist/tools/path-sandbox.d.ts +14 -0
  584. package/dist/tools/path-sandbox.js +60 -0
  585. package/dist/tools/process-tools.d.ts +15 -0
  586. package/dist/tools/process-tools.js +95 -0
  587. package/dist/tools/python-bridge/bridge.d.ts +24 -0
  588. package/dist/tools/python-bridge/bridge.js +39 -0
  589. package/dist/tools/python-bridge/executor.d.ts +6 -0
  590. package/dist/tools/python-bridge/executor.js +69 -0
  591. package/dist/tools/python-bridge/index.d.ts +5 -0
  592. package/dist/tools/python-bridge/index.js +4 -0
  593. package/dist/tools/python-bridge/parser.d.ts +21 -0
  594. package/dist/tools/python-bridge/parser.js +258 -0
  595. package/dist/tools/read.d.ts +16 -0
  596. package/dist/tools/read.js +276 -0
  597. package/dist/tools/registry.d.ts +2 -0
  598. package/dist/tools/registry.js +2 -0
  599. package/dist/tools/restart.d.ts +10 -0
  600. package/dist/tools/restart.js +46 -0
  601. package/dist/tools/result-buffer.d.ts +66 -0
  602. package/dist/tools/result-buffer.js +146 -0
  603. package/dist/tools/runtime-control.d.ts +163 -0
  604. package/dist/tools/runtime-control.js +1780 -0
  605. package/dist/tools/symbol-references.d.ts +5 -0
  606. package/dist/tools/symbol-references.js +331 -0
  607. package/dist/tools/system-info.d.ts +3 -0
  608. package/dist/tools/system-info.js +55 -0
  609. package/dist/tools/write.d.ts +18 -0
  610. package/dist/tools/write.js +118 -0
  611. package/dist/tools/xref/index.d.ts +25 -0
  612. package/dist/tools/xref/index.js +25 -0
  613. package/dist/tools/xref/manager.d.ts +85 -0
  614. package/dist/tools/xref/manager.js +948 -0
  615. package/dist/tools/xref/parser.d.ts +34 -0
  616. package/dist/tools/xref/parser.js +64 -0
  617. package/dist/tools/xref/regex-parser.d.ts +26 -0
  618. package/dist/tools/xref/regex-parser.js +446 -0
  619. package/dist/tools/xref/schema.d.ts +104 -0
  620. package/dist/tools/xref/schema.js +68 -0
  621. package/dist/tools/xref/ts-parser.d.ts +20 -0
  622. package/dist/tools/xref/ts-parser.js +403 -0
  623. package/dist/tools/xref/xref-build.d.ts +20 -0
  624. package/dist/tools/xref/xref-build.js +128 -0
  625. package/dist/tools/xref/xref-graph.d.ts +24 -0
  626. package/dist/tools/xref/xref-graph.js +88 -0
  627. package/dist/tools/xref/xref-query.d.ts +30 -0
  628. package/dist/tools/xref/xref-query.js +123 -0
  629. package/dist/types.d.ts +217 -0
  630. package/dist/types.js +2 -0
  631. package/dist/ui/assistant-message.d.ts +7 -0
  632. package/dist/ui/assistant-message.js +18 -0
  633. package/dist/ui/chat-log.d.ts +52 -0
  634. package/dist/ui/chat-log.js +246 -0
  635. package/dist/ui/command-registry.d.ts +73 -0
  636. package/dist/ui/command-registry.js +838 -0
  637. package/dist/ui/diff-component.d.ts +14 -0
  638. package/dist/ui/diff-component.js +42 -0
  639. package/dist/ui/hyperlink-markdown.d.ts +10 -0
  640. package/dist/ui/hyperlink-markdown.js +21 -0
  641. package/dist/ui/markdown-message.d.ts +10 -0
  642. package/dist/ui/markdown-message.js +16 -0
  643. package/dist/ui/osc8-hyperlinks.d.ts +4 -0
  644. package/dist/ui/osc8-hyperlinks.js +142 -0
  645. package/dist/ui/pi-tui-editor.d.ts +35 -0
  646. package/dist/ui/pi-tui-editor.js +79 -0
  647. package/dist/ui/slash-commands.d.ts +16 -0
  648. package/dist/ui/slash-commands.js +29 -0
  649. package/dist/ui/slash-panel.d.ts +54 -0
  650. package/dist/ui/slash-panel.js +199 -0
  651. package/dist/ui/theme.d.ts +120 -0
  652. package/dist/ui/theme.js +154 -0
  653. package/dist/ui/tool-display.d.ts +13 -0
  654. package/dist/ui/tool-display.js +85 -0
  655. package/dist/ui/tool-execution.d.ts +22 -0
  656. package/dist/ui/tool-execution.js +87 -0
  657. package/dist/ui/tui-formatters.d.ts +2 -0
  658. package/dist/ui/tui-formatters.js +145 -0
  659. package/dist/ui/user-message.d.ts +5 -0
  660. package/dist/ui/user-message.js +11 -0
  661. package/dist/update/check.d.ts +8 -0
  662. package/dist/update/check.js +28 -0
  663. package/dist/update/config.d.ts +6 -0
  664. package/dist/update/config.js +18 -0
  665. package/dist/update/download.d.ts +3 -0
  666. package/dist/update/download.js +27 -0
  667. package/dist/update/index.d.ts +6 -0
  668. package/dist/update/index.js +5 -0
  669. package/dist/update/install.d.ts +3 -0
  670. package/dist/update/install.js +33 -0
  671. package/dist/update/types.d.ts +16 -0
  672. package/dist/update/types.js +2 -0
  673. package/dist/utils/diff.d.ts +9 -0
  674. package/dist/utils/diff.js +45 -0
  675. package/dist/utils/misc.d.ts +12 -0
  676. package/dist/utils/misc.js +34 -0
  677. package/dist/world-engine/agent.d.ts +41 -0
  678. package/dist/world-engine/agent.js +385 -0
  679. package/dist/world-engine/index.d.ts +45 -0
  680. package/dist/world-engine/index.js +160 -0
  681. package/dist/world-engine/sim.d.ts +14 -0
  682. package/dist/world-engine/sim.js +120 -0
  683. package/dist/world-engine/store.d.ts +110 -0
  684. package/dist/world-engine/store.js +519 -0
  685. package/dist/world-engine/ticker.d.ts +37 -0
  686. package/dist/world-engine/ticker.js +214 -0
  687. package/dist/world-engine/tools.d.ts +6 -0
  688. package/dist/world-engine/tools.js +380 -0
  689. package/dist/world-engine/types.d.ts +202 -0
  690. package/dist/world-engine/types.js +56 -0
  691. package/docs/api.md +180 -0
  692. package/docs/architecture.md +206 -0
  693. package/docs/diagnostics/2026-06-08-config-prompt-guidance.md +67 -0
  694. package/docs/superpowers/plans/2026-06-08-global-bootstrap-mode.md +89 -0
  695. package/docs/superpowers/specs/2026-06-08-global-bootstrap-mode-design.md +174 -0
  696. package/docs/user-guide.md +226 -0
  697. package/package.json +63 -0
  698. package/plugins/example-greeter/index.js +55 -0
  699. package/plugins/example-greeter/plugin.json +8 -0
  700. package/pnpm-workspace.yaml +5 -0
  701. package/provider_arch.md +179 -0
  702. package/start-webui.sh +27 -0
  703. package/tsconfig.tsbuildinfo +1 -0
  704. package/webui-channel-design.md +789 -0
@@ -0,0 +1,3273 @@
1
+ /**
2
+ * TUI Gateway — pi-tui component-based terminal UI for the Agent framework.
3
+ *
4
+ * Powered by @earendil-works/pi-tui.
5
+ *
6
+ * Layout (top to bottom):
7
+ * headerContainer — Agent · model · Turns · Mode · Provider
8
+ * contextBarText — Context ████░░░░ 45%
9
+ * chatLog — message history (grows to fill)
10
+ * footerText — Ctrl+C exit | Ctrl+L clear | ...
11
+ * editor — user input area
12
+ */
13
+ import { execSync } from 'node:child_process';
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import os from 'node:os';
17
+ import http from 'node:http';
18
+ import { CombinedAutocompleteProvider, Container, Key, Loader, matchesKey, ProcessTerminal, Text, TUI, } from '@earendil-works/pi-tui';
19
+ import { StatsManager } from '../memory/stats.js';
20
+ import { ConfigManager } from '../setup/config.js';
21
+ import { getModelContextWindow } from '../setup/model-defaults.js';
22
+ import { RuntimeConfigCenter } from '../runtime/config-center.js';
23
+ import { ProviderManager } from '../provider/manager.js';
24
+ import { LifecycleSupervisor } from '../lifecycle/index.js';
25
+ import { createAgent } from './factory.js';
26
+ import { getBootstrapStatus, DEFAULT_PERSONA_DIR } from '../setup/persona-bootstrap.js';
27
+ import { createLogger } from '../logging/logger.js';
28
+ import { TuiChannel } from '../channels/builtin/tui-channel.js';
29
+ import { ChannelManager } from '../channels/manager.js';
30
+ import { MessageQueue, QueueMessageMode } from '../channels/index.js';
31
+ import { registerConfigChannels, getChannelPlugins } from '../channels/auto-detect.js';
32
+ import { filterCommands, getCommandsByCategory, getCategoryLabel, } from '../ui/slash-commands.js';
33
+ import { SlashSubPanel } from '../ui/slash-panel.js';
34
+ import { CommandRegistry } from '../ui/command-registry.js';
35
+ import { ChatLog } from '../ui/chat-log.js';
36
+ import { CustomEditor } from '../ui/pi-tui-editor.js';
37
+ import { theme, editorTheme } from '../ui/theme.js';
38
+ import { readRecentEvents } from '../event-store.js';
39
+ import { LocalModelModule } from '../local-model/index.js';
40
+ import { setAskUserHandler } from '../tools/ask-user.js';
41
+ const logger = createLogger('tui');
42
+ // ─── Constants ────────────────────────────────────────────────────────────
43
+ const BOX_H = '\u2500'; // ─
44
+ // ─── Status Bar Formatters ────────────────────────────────────────────────
45
+ function visualWidth(s) {
46
+ let w = 0;
47
+ for (const ch of s) {
48
+ const cp = ch.codePointAt(0) ?? 0;
49
+ // CJK, fullwidth forms, emoji: count as 2
50
+ w += (cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
51
+ (cp >= 0x2e80 && cp <= 0xa4cf) || // CJK Radicals, Kangxi, Ideographs
52
+ (cp >= 0xac00 && cp <= 0xd7a3) || // Hangul Syllables
53
+ (cp >= 0xf900 && cp <= 0xfaff) || // CJK Compatibility Ideographs
54
+ (cp >= 0xfe10 && cp <= 0xfe19) || // Vertical forms
55
+ (cp >= 0xfe30 && cp <= 0xfe6f) || // CJK Compatibility Forms
56
+ (cp >= 0xff00 && cp <= 0xff60) || // Fullwidth Forms
57
+ (cp >= 0xffe0 && cp <= 0xffe6) || // Fullwidth Signs
58
+ (cp >= 0x1f300 && cp <= 0x1f9ff) || // Emoji & Symbols
59
+ (cp >= 0x20000 && cp <= 0x2fa1f) // CJK Extension
60
+ ? 2 : 1;
61
+ }
62
+ return w;
63
+ }
64
+ function truncateByVisualWidth(s, maxVisualWidth) {
65
+ if (visualWidth(s) <= maxVisualWidth)
66
+ return s;
67
+ let w = 0;
68
+ let result = '';
69
+ for (const ch of s) {
70
+ const cw = visualWidth(ch);
71
+ if (w + cw + 3 > maxVisualWidth) {
72
+ result += '...';
73
+ break;
74
+ }
75
+ w += cw;
76
+ result += ch;
77
+ }
78
+ return result;
79
+ }
80
+ function formatStatusBar(info, modelName, providerInfo, modeLabel) {
81
+ const planInfo = info.planStepsTotal !== undefined
82
+ ? `Plan: ${info.planStepsDone ?? 0}/${info.planStepsTotal}`
83
+ : '';
84
+ const modelDisplay = truncateByVisualWidth(modelName, 28);
85
+ const providerPart = providerInfo
86
+ ? theme.dim(' | ') +
87
+ 'Provider: ' +
88
+ (providerInfo.isLocal ? theme.success : theme.accent)(providerInfo.providerLabel)
89
+ : '';
90
+ const parts = [
91
+ theme.fg(' Hyacinth'),
92
+ theme.dim(' \u00b7 '),
93
+ theme.accent(modelDisplay),
94
+ theme.dim(' | '),
95
+ `Turns: ${theme.fg(String(info.turnCount))}/${theme.fg(String(info.maxTurns))}`,
96
+ providerPart,
97
+ ];
98
+ if (planInfo) {
99
+ parts.push(theme.dim(' | '), planInfo);
100
+ }
101
+ if (modeLabel) {
102
+ parts.push(theme.dim(' | '), theme.accent(modeLabel));
103
+ }
104
+ return parts.join('');
105
+ }
106
+ function formatContextBar(tokensUsed, maxTokens, width = 40) {
107
+ const ratio = Math.min(tokensUsed / maxTokens, 1);
108
+ const filledW = Math.floor(ratio * width);
109
+ const emptyW = width - filledW;
110
+ const bar = '\u2588'.repeat(filledW) + '\u2591'.repeat(emptyW);
111
+ let colorFn;
112
+ if (ratio < 0.5)
113
+ colorFn = theme.success;
114
+ else if (ratio < 0.8)
115
+ colorFn = theme.warning;
116
+ else
117
+ colorFn = theme.error;
118
+ const usedK = (tokensUsed / 1000).toFixed(0) + 'K';
119
+ const maxK = (maxTokens / 1000).toFixed(0) + 'K';
120
+ const pct = (ratio * 100).toFixed(0) + '%';
121
+ return theme.dim('Context: ') + colorFn(bar) + ' ' + theme.fg(pct) + theme.dim(` (${usedK} / ${maxK} tokens)`);
122
+ }
123
+ // ─── Event Replay ─────────────────────────────────────────────────────────
124
+ /**
125
+ * Replay recent events from the previous session into the chat log.
126
+ * Shows the last conversation on TUI re-entry.
127
+ */
128
+ function replayEvents(chatLog, sessionDir) {
129
+ readRecentEvents(sessionDir, 50).then(events => {
130
+ if (events.length === 0)
131
+ return;
132
+ chatLog.addSystem('── Previous Session ──');
133
+ let pendingText = '';
134
+ let pendingThinking = '';
135
+ let userMsgSeen = false;
136
+ for (const event of events) {
137
+ switch (event.type) {
138
+ case 'user_input':
139
+ // Flush any pending thinking + text before showing user message
140
+ if (pendingThinking) {
141
+ pendingThinking = '';
142
+ }
143
+ if (pendingText) {
144
+ chatLog.addSystem(pendingText);
145
+ pendingText = '';
146
+ }
147
+ chatLog.addUser(event.content || '');
148
+ userMsgSeen = true;
149
+ break;
150
+ case 'text':
151
+ // Accumulate text chunks into one message
152
+ pendingText += event.content || '';
153
+ break;
154
+ case 'thinking':
155
+ pendingThinking += event.content || '';
156
+ break;
157
+ case 'tool_call':
158
+ // Flush pending thinking + text before tool call
159
+ if (pendingThinking) {
160
+ pendingThinking = '';
161
+ }
162
+ if (pendingText) {
163
+ chatLog.addSystem(pendingText);
164
+ pendingText = '';
165
+ }
166
+ chatLog.startTool(event.id || 'replay', event.name || 'unknown', JSON.stringify(event.input || {}).slice(0, 100));
167
+ break;
168
+ case 'tool_result':
169
+ chatLog.updateToolResult(event.tool_use_id || 'replay', (event.content || '').slice(0, 500), { isError: event.name === 'error' });
170
+ break;
171
+ case 'error':
172
+ // 跳过回放中的原始错误日志(避免旧 session 的 API 错误 JSON 污染 UI)
173
+ break;
174
+ case 'stop':
175
+ // Flush remaining thinking + text
176
+ if (pendingThinking) {
177
+ pendingThinking = '';
178
+ }
179
+ if (pendingText) {
180
+ chatLog.addSystem(pendingText);
181
+ pendingText = '';
182
+ }
183
+ break;
184
+ default:
185
+ break;
186
+ }
187
+ }
188
+ // Flush any remaining thinking + text
189
+ if (pendingThinking) {
190
+ pendingThinking = '';
191
+ }
192
+ if (pendingText) {
193
+ chatLog.addSystem(pendingText);
194
+ }
195
+ }).catch(() => {
196
+ // Silently ignore replay errors — don't block TUI startup
197
+ });
198
+ }
199
+ // ─── Main TUI ─────────────────────────────────────────────────────────────
200
+ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxContext, maxMessages = 10000, skipSetup, personaDir = DEFAULT_PERSONA_DIR, bootstrapStatus, localModelProvider, continuationMessage) {
201
+ // ── Force UTF-8 console encoding on Windows ──
202
+ if (process.platform === 'win32') {
203
+ try {
204
+ execSync('chcp 65001 > nul', { stdio: 'ignore' });
205
+ }
206
+ catch {
207
+ // non-fatal: terminal may still work with limited character support
208
+ }
209
+ }
210
+ // ── Redirect stderr to file ──
211
+ const logDir = path.join(os.homedir(), '.agent');
212
+ await fs.promises.mkdir(logDir, { recursive: true });
213
+ const logStream = fs.createWriteStream(path.join(logDir, 'tui.log'), { flags: 'a' });
214
+ const origStderrWrite = process.stderr.write.bind(process.stderr);
215
+ process.stderr.write = (chunk, ...args) => {
216
+ logStream.write(typeof chunk === 'string' ? chunk : String(chunk));
217
+ const cb = args[args.length - 1];
218
+ if (typeof cb === 'function')
219
+ cb();
220
+ return true;
221
+ };
222
+ // ── Provider ──
223
+ const configManager = new ConfigManager(process.cwd());
224
+ await configManager.loadEnvKeys();
225
+ let activeProvider = provider;
226
+ if (!provider.getModel() || provider.getModel() === '') {
227
+ try {
228
+ const manager = await ProviderManager.createFromConfigFile(undefined, process.cwd());
229
+ activeProvider = manager.getProvider();
230
+ }
231
+ catch {
232
+ // fallback
233
+ }
234
+ }
235
+ let providerTypeStart = activeProvider.getProviderType();
236
+ let modelName = activeProvider.getModel();
237
+ // ── Last known TurnInfo (updated each turn, used by /provider handler) ──
238
+ let lastTurnCount = 0;
239
+ let lastTokensUsed = 0;
240
+ // ── Lifecycle ──
241
+ const supervisor = new LifecycleSupervisor();
242
+ supervisor.installSignalHandlers();
243
+ // ── Local Model ──
244
+ const localModel = LocalModelModule.getInstance();
245
+ localModel.initialize(process.cwd());
246
+ let slashSubPanelActive = false; // 二级菜单激活时跳过主监听器的上下键处理
247
+ // ── TUI — pi-tui setup ──
248
+ const tui = new TUI(new ProcessTerminal());
249
+ const root = new Container();
250
+ // Header area: contains Text only (no Loader)
251
+ const headerContainer = new Container();
252
+ let headerText = new Text('', 0, 0);
253
+ headerContainer.addChild(headerText);
254
+ const contextBarText = new Text('', 0, 0);
255
+ const chatLog = new ChatLog();
256
+ const footerText = new Text('', 0, 0);
257
+ const editor = new CustomEditor(tui, editorTheme);
258
+ // Permission selection bar — shown between chat and editor during tool permission prompts
259
+ const permissionBar = new Text('', 0, 0);
260
+ const permissionQueue = [];
261
+ let permissionSelection = 0; // 0=Yes, 1=AOR, 2=Always, 3=No
262
+ let askUserState = null;
263
+ const askUserBar = new Text('', 0, 0);
264
+ const askUserContent = new Text('', 0, 0);
265
+ root.addChild(headerContainer);
266
+ root.addChild(chatLog);
267
+ root.addChild(contextBarText);
268
+ // Thinking indicator — below chatLog, above editor (prevents content jitter)
269
+ const thinkingBar = new Container();
270
+ let thinkingLoader = null;
271
+ root.addChild(thinkingBar);
272
+ root.addChild(footerText);
273
+ root.addChild(permissionBar);
274
+ root.addChild(askUserContent);
275
+ root.addChild(askUserBar);
276
+ root.addChild(editor);
277
+ tui.addChild(root);
278
+ tui.setFocus(editor);
279
+ // ── Header helpers — header always shows text (never Loader) ──
280
+ function updateHeaderText(text) {
281
+ headerText.setText(text);
282
+ }
283
+ // ── Thinking indicator helpers — Loader between chatLog and editor ──
284
+ function showThinkingIndicator(message) {
285
+ if (thinkingLoader) {
286
+ thinkingLoader.setMessage(message);
287
+ return thinkingLoader;
288
+ }
289
+ const loader = new Loader(tui, (spinner) => theme.accent(spinner), (text) => theme.accent(text), message);
290
+ thinkingBar.addChild(loader);
291
+ thinkingLoader = loader;
292
+ return loader;
293
+ }
294
+ function hideThinkingIndicator() {
295
+ if (thinkingLoader) {
296
+ thinkingLoader.stop();
297
+ thinkingBar.removeChild(thinkingLoader);
298
+ thinkingLoader = null;
299
+ }
300
+ }
301
+ // ── State for refreshStatus ──
302
+ let prevCompressCount = 0;
303
+ let compactionMessage = null;
304
+ let compressionLoader = null;
305
+ let compressionTimeout = null;
306
+ let prevProviderLabel = null;
307
+ let prevProviderIsLocal = false;
308
+ let fallbackMessage = null;
309
+ let isThinking = false;
310
+ function refreshStatus(info) {
311
+ // ── Track last known values for /provider handler ──
312
+ lastTurnCount = info.turnCount;
313
+ lastTokensUsed = info.tokensUsed;
314
+ // ── Read live config (overrides startup defaults) ──
315
+ const liveCfg = RuntimeConfigCenter.getInstance();
316
+ const liveMaxContext = liveCfg.get('session.maxContext');
317
+ const liveMaxTurns = liveCfg.get('session.maxTurns') || maxTurns;
318
+ const activeInfo = {
319
+ ...info,
320
+ maxTurns: liveMaxTurns,
321
+ };
322
+ // Compaction detection
323
+ if (info.compressCount > prevCompressCount) {
324
+ prevCompressCount = info.compressCount;
325
+ compactionMessage = '\u27f3 Compacting...';
326
+ setTimeout(() => {
327
+ compactionMessage = '\u2713 Compacted';
328
+ tui.requestRender();
329
+ setTimeout(() => {
330
+ compactionMessage = null;
331
+ tui.requestRender();
332
+ }, 2000);
333
+ }, 1000);
334
+ }
335
+ // Provider fallback detection \u2014 event-driven from onFallback callback
336
+ const providerInfo = loop.getProviderRoutingInfo();
337
+ if (providerInfo) {
338
+ if (prevProviderLabel !== null && providerInfo.providerLabel !== prevProviderLabel) {
339
+ // Provider changed via fallback \u2014 one-time notification already sent by onFallback
340
+ fallbackMessage = `\u26a0 Fallback: ${providerInfo.providerLabel}`;
341
+ setTimeout(() => { fallbackMessage = null; tui.requestRender(); }, 5000);
342
+ }
343
+ prevProviderLabel = providerInfo.providerLabel;
344
+ prevProviderIsLocal = providerInfo.isLocal;
345
+ }
346
+ const ap = loop.getActiveProvider();
347
+ const currentModel = ap.getModel();
348
+ const currentProviderType = ap.getProviderType();
349
+ const currentProviderInfo = providerInfo ?? { providerLabel: currentProviderType, isLocal: false, mode: 'auto' };
350
+ // Build mode label for header display
351
+ let modeHeaderLabel = null;
352
+ let statusContent = formatStatusBar(activeInfo, currentModel, currentProviderInfo, modeHeaderLabel);
353
+ // Compaction message
354
+ if (compactionMessage) {
355
+ statusContent += ' ' + theme.warning(compactionMessage);
356
+ }
357
+ // Fallback message
358
+ if (fallbackMessage) {
359
+ statusContent += ' ' + theme.warning(fallbackMessage);
360
+ }
361
+ // Update header (always Text)
362
+ if (isThinking && thinkingLoader) {
363
+ thinkingLoader.setMessage(statusContent);
364
+ }
365
+ else if (!isThinking) {
366
+ updateHeaderText(statusContent);
367
+ }
368
+ let ctxBar = formatContextBar(info.tokensUsed, liveMaxContext);
369
+ // 优先使用最新轮次的缓存命中率,否则从累计值计算
370
+ const hitRate = info.cacheHitRate != null
371
+ ? info.cacheHitRate.toFixed(1)
372
+ : (info.cacheHitTokens != null && info.cacheMissTokens != null)
373
+ ? (() => { const t = info.cacheHitTokens + info.cacheMissTokens; return t > 0 ? (info.cacheHitTokens / t * 100).toFixed(1) : null; })()
374
+ : null;
375
+ if (hitRate != null) {
376
+ ctxBar += theme.dim(` | Cache: ${hitRate}%`);
377
+ if (info.cacheHistory && info.cacheHistory.length > 1) {
378
+ ctxBar += theme.dim(` (${info.cacheHistory.length}t)`);
379
+ }
380
+ }
381
+ // Background process count
382
+ if (backgroundRegistry) {
383
+ const bgProcs = backgroundRegistry.list();
384
+ if (bgProcs.length > 0) {
385
+ const running = bgProcs.filter((p) => p.status === 'running').length;
386
+ ctxBar += theme.dim(' | ') + theme.accent(`⚙ ${running} bg`);
387
+ }
388
+ }
389
+ contextBarText.setText(ctxBar);
390
+ tui.requestRender();
391
+ updateTokenEstimate(); // 同步刷新 footer(工作流状态等)
392
+ }
393
+ // ── Output handler ──
394
+ let currentTextLine = '';
395
+ let pendingThinking = '';
396
+ let toolCounterFallback = 0;
397
+ let showThinking = false;
398
+ function showPermissionDialog(toolName, inputStr) {
399
+ permissionSelection = 0;
400
+ chatLog.addSystem(theme.warning(`\u250c Permission Required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`));
401
+ chatLog.addSystem(theme.warning('\u2502 ') + theme.fg(`${toolName}(${inputStr})`) + theme.warning(' \u2502'));
402
+ chatLog.addSystem(theme.warning(`\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`));
403
+ updatePermissionBar();
404
+ tui.requestRender();
405
+ }
406
+ function resolvePermission(result) {
407
+ const req = permissionQueue.shift();
408
+ if (!req)
409
+ return;
410
+ permissionBar.setText('');
411
+ chatLog.addSystem(result === 'no' ? theme.error(' \u25c6 Denied')
412
+ : result === 'aor' ? theme.warning(' \u25c6 AOR \u2014 all restrictions lifted')
413
+ : result === 'always' ? theme.success(' \u25c6 Always allowed')
414
+ : theme.success(' \u25c6 Approved'));
415
+ req.resolve(result);
416
+ if (permissionQueue.length > 0) {
417
+ const next = permissionQueue[0];
418
+ showPermissionDialog(next.toolName, next.inputStr);
419
+ }
420
+ tui.requestRender();
421
+ }
422
+ function updatePermissionBar() {
423
+ const labels = ['Yes', 'AOR', 'Always', 'No'];
424
+ const shortcuts = ['Y', 'O', 'A', 'N'];
425
+ const parts = labels.map((l, i) => {
426
+ const prefix = i === permissionSelection ? '\u25b6 ' : ' ';
427
+ if (i === permissionSelection) {
428
+ return theme.fg(`[ ${prefix}${l} (${shortcuts[i]}) ]`);
429
+ }
430
+ return theme.dim(` ${prefix}${l} (${shortcuts[i]}) `);
431
+ });
432
+ permissionBar.setText(theme.warning('\u250c Permission Required \u2500 ') +
433
+ parts.join(theme.dim(' \u2502 ')) +
434
+ theme.warning(' \u2500\u2500 Use \u2190\u2192 to select, Enter to confirm'));
435
+ }
436
+ // \u2500\u2500 Ask User \u6e32\u67d3 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
437
+ function renderAskUserForm() {
438
+ if (!askUserState) {
439
+ askUserContent.setText('');
440
+ askUserBar.setText('');
441
+ return;
442
+ }
443
+ const { questions, selectedOptions, customTexts, activeQuestion } = askUserState;
444
+ const supplementIdx = questions.length;
445
+ const totalTabs = supplementIdx + 1;
446
+ // \u6784\u5efa\u6807\u7b7e\u884c
447
+ let tabLine = '';
448
+ for (let i = 0; i < questions.length; i++) {
449
+ const isActive = i === activeQuestion;
450
+ const header = questions[i].header || `\u95ee\u9898${i + 1}`;
451
+ tabLine += isActive ? ` ${theme.fg(`[${header}]`)} ` : ` ${theme.dim(header)} `;
452
+ }
453
+ tabLine += activeQuestion === supplementIdx
454
+ ? ` ${theme.fg('[\u8865\u5145]')} `
455
+ : ` ${theme.dim('\u8865\u5145')} `;
456
+ // \u6784\u5efa\u5185\u5bb9
457
+ let content = theme.warning('\u250c Ask User \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500') + '\n';
458
+ content += tabLine + '\n';
459
+ content += theme.warning('\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500') + '\n';
460
+ if (activeQuestion < supplementIdx) {
461
+ // \u666e\u901a\u95ee\u9898
462
+ const q = questions[activeQuestion];
463
+ content += `${q.question}\n\n`;
464
+ const opts = q.options ?? [];
465
+ const sel = selectedOptions.get(activeQuestion) ?? new Set();
466
+ const isMulti = q.multiSelect ?? false;
467
+ for (let i = 0; i < opts.length; i++) {
468
+ const selected = sel.has(i);
469
+ const bullet = isMulti
470
+ ? (selected ? theme.fg('\u25c9') : '\u25cb')
471
+ : (selected ? theme.fg('\u25cf') : '\u25cb');
472
+ const highlight = i === askUserState.activeOption;
473
+ content += (highlight ? theme.fg(` ${bullet} ${opts[i]}`) : theme.dim(` ${bullet} ${opts[i]}`)) + '\n';
474
+ }
475
+ // \u81ea\u5b9a\u4e49\u8f93\u5165
476
+ if (q.customInput ?? false) {
477
+ const custom = customTexts.get(activeQuestion) ?? '';
478
+ content += `\n${theme.dim('\u81ea\u5b9a\u4e49:')} ${custom}${theme.dim('\u258c')}\n`;
479
+ }
480
+ }
481
+ else {
482
+ // "\u8865\u5145" tab
483
+ const custom = customTexts.get(supplementIdx) ?? '';
484
+ content += `${theme.fg('\u8865\u5145\u8bf4\u660e\uff08\u81ea\u7531\u8f93\u5165\uff0c\u6309 Enter \u63d0\u4ea4\u5168\u90e8\uff09')}\n\n`;
485
+ content += `${custom}${theme.dim('\u258c')}\n`;
486
+ }
487
+ content += theme.warning('\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500');
488
+ askUserContent.setText(content);
489
+ // \u5bfc\u822a\u680f
490
+ const currentLabel = activeQuestion < supplementIdx
491
+ ? (questions[activeQuestion].header || `\u95ee\u9898${activeQuestion + 1}`)
492
+ : '\u8865\u5145';
493
+ const nav = activeQuestion < supplementIdx
494
+ ? theme.warning(`\u2190\u2192 \u5207\u6362 \u2191\u2193 \u9009\u9879 \u7a7a\u683c \u9009\u4e2d \u21b5 ${currentLabel === '\u8865\u5145' ? '\u63d0\u4ea4' : '\u786e\u8ba4'}`)
495
+ : theme.warning(`\u2190\u2192 \u5207\u6362 \u21b5 \u63d0\u4ea4\u5168\u90e8\u7b54\u6848`);
496
+ askUserBar.setText(nav);
497
+ }
498
+ function resolveAskUser() {
499
+ if (!askUserState)
500
+ return;
501
+ const { questions, selectedOptions, customTexts, resolve } = askUserState;
502
+ const suppIdx = questions.length;
503
+ const result = {};
504
+ for (let i = 0; i < questions.length; i++) {
505
+ const q = questions[i];
506
+ const answers = [];
507
+ // \u9009\u4e2d\u7684\u9009\u9879
508
+ const sel = selectedOptions.get(i) ?? new Set();
509
+ for (const idx of sel) {
510
+ if (q.options && q.options[idx]) {
511
+ answers.push(q.options[idx]);
512
+ }
513
+ }
514
+ // \u81ea\u5b9a\u4e49\u8f93\u5165
515
+ const custom = customTexts.get(i) ?? '';
516
+ if (custom.trim()) {
517
+ answers.push(custom.trim());
518
+ }
519
+ // \u7528\u95ee\u9898\u539f\u6587\u4f5c\u4e3a key\uff08\u957f\u4e0a\u4e0b\u6587\u91cc\u6bd4 "0" "1" \u66f4\u6709\u8bed\u4e49\uff09
520
+ if (answers.length > 0) {
521
+ result[q.question] = answers;
522
+ }
523
+ }
524
+ // "\u8865\u5145" \u8f93\u5165
525
+ const suppText = customTexts.get(suppIdx) ?? '';
526
+ if (suppText.trim()) {
527
+ result['\u8865\u5145\u8bf4\u660e'] = [suppText.trim()];
528
+ }
529
+ askUserContent.setText('');
530
+ askUserBar.setText('');
531
+ const answeredCount = Object.keys(result).length;
532
+ chatLog.addSystem(theme.success(`\u25c6 Answered ${answeredCount} question(s)`));
533
+ askUserState = null;
534
+ resolve(JSON.stringify(result, null, 2));
535
+ tui.requestRender();
536
+ }
537
+ const tuiHandler = {
538
+ onText(content) {
539
+ currentTextLine += content;
540
+ },
541
+ onThinking(content) {
542
+ pendingThinking += content;
543
+ },
544
+ onTurnStart() {
545
+ isThinking = true;
546
+ pendingThinking = '';
547
+ const label = loop.pendingTaskName
548
+ ? `⏰ ${loop.pendingTaskName}`
549
+ : 'Thinking...';
550
+ showThinkingIndicator(theme.accent(label));
551
+ },
552
+ onToolUse(name, inputSummary, toolId) {
553
+ if (pendingThinking.trim()) {
554
+ if (showThinking) {
555
+ chatLog.addSystem(theme.dim('\u{1F9E0} Thinking:\n') + theme.dim(pendingThinking.trim()));
556
+ }
557
+ pendingThinking = '';
558
+ }
559
+ if (currentTextLine.trim()) {
560
+ chatLog.finalizeAssistant(currentTextLine.trim());
561
+ currentTextLine = '';
562
+ }
563
+ const summary = inputSummary.length > 100 ? inputSummary.slice(0, 97) + '...' : inputSummary;
564
+ const id = toolId ?? `tool_${Date.now()}_${++toolCounterFallback}`;
565
+ chatLog.startTool(id, name, summary);
566
+ tui.requestRender();
567
+ },
568
+ onToolResult(content, isError, toolId) {
569
+ if (toolId) {
570
+ chatLog.updateToolResult(toolId, content, { isError });
571
+ }
572
+ tui.requestRender();
573
+ },
574
+ onDiff(toolId, filePath, diffLines) {
575
+ if (toolId) {
576
+ chatLog.showDiff(toolId, filePath, diffLines);
577
+ }
578
+ tui.requestRender();
579
+ },
580
+ onStatus(message, level) {
581
+ // 压缩指示器
582
+ if (message === 'compress-start') {
583
+ if (!compressionLoader) {
584
+ compressionLoader = new Loader(tui, (spinner) => theme.accent(spinner), (text) => theme.accent(text), 'Compressing...');
585
+ thinkingBar.addChild(compressionLoader);
586
+ }
587
+ // 超时保护:30 秒后自动清除 Loader
588
+ if (compressionTimeout)
589
+ clearTimeout(compressionTimeout);
590
+ compressionTimeout = setTimeout(() => {
591
+ if (compressionLoader) {
592
+ compressionLoader.stop();
593
+ thinkingBar.removeChild(compressionLoader);
594
+ compressionLoader = null;
595
+ }
596
+ compressionTimeout = null;
597
+ }, 30000);
598
+ return;
599
+ }
600
+ if (message === 'compress-end') {
601
+ if (compressionTimeout) {
602
+ clearTimeout(compressionTimeout);
603
+ compressionTimeout = null;
604
+ }
605
+ if (compressionLoader) {
606
+ compressionLoader.stop();
607
+ thinkingBar.removeChild(compressionLoader);
608
+ compressionLoader = null;
609
+ }
610
+ return;
611
+ }
612
+ if (message.startsWith('compress-result:')) {
613
+ const parts = message.split(':');
614
+ const pre = parseInt(parts[1], 10);
615
+ const post = parseInt(parts[2], 10);
616
+ if (!isNaN(pre) && !isNaN(post)) {
617
+ const ratio = Math.round((1 - post / pre) * 100);
618
+ chatLog.addSystem(theme.success('Compressed: ') +
619
+ theme.dim(`${pre.toLocaleString()} → ${post.toLocaleString()} tokens`) +
620
+ theme.fg(` (${ratio}% reduced)`));
621
+ // 用压缩后的 token 数刷新 context bar
622
+ refreshStatus(loop.getTurnInfo(lastTurnCount, post));
623
+ }
624
+ return;
625
+ }
626
+ if (level === 'error') {
627
+ // 原始 API 错误细节走日志,TUI 只显示简洁消息
628
+ if (message.length > 150 || message.includes('{') || message.includes('\n')) {
629
+ process.stderr.write(`[tui:error] ${message}\n`);
630
+ chatLog.addSystem(theme.errorBright('[Error] ') + theme.dim('Provider request failed, auto-switching...'));
631
+ }
632
+ else {
633
+ chatLog.addSystem(theme.errorBright('[Error] ') + theme.error(message));
634
+ }
635
+ }
636
+ else if (level === 'warn') {
637
+ chatLog.addSystem(theme.warning(message));
638
+ }
639
+ else {
640
+ chatLog.addSystem(theme.fg(message));
641
+ // 如果消息是 provider 切换,同步刷新 UI
642
+ if (message.startsWith('Provider switched to ')) {
643
+ modelName = loop.getActiveProvider().getModel();
644
+ providerTypeStart = loop.getActiveProvider().getProviderType();
645
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
646
+ }
647
+ }
648
+ tui.requestRender();
649
+ },
650
+ onFlush() {
651
+ hideThinkingIndicator();
652
+ isThinking = false;
653
+ // 陪伴模式 session 切换 → 清空显示并重放新 session 历史,同步 sessionDir
654
+ if (loop._sessionSwitched) {
655
+ sessionDir = loop._sessionSwitched;
656
+ chatLog.clearAll();
657
+ replayEvents(chatLog, loop._sessionSwitched);
658
+ loop._sessionSwitched = undefined;
659
+ }
660
+ updateHeaderText(modelName);
661
+ const providerInfo = loop.getProviderRoutingInfo();
662
+ if (pendingThinking.trim()) {
663
+ if (showThinking) {
664
+ chatLog.addSystem(theme.dim('\u{1F9E0} Thinking:\n') + theme.dim(pendingThinking.trim()));
665
+ }
666
+ // thinking-only 模型兜底:thinking 有内容但 text 为空时,将 thinking 作为回复显示
667
+ if (!currentTextLine.trim()) {
668
+ currentTextLine = pendingThinking.trim();
669
+ }
670
+ pendingThinking = '';
671
+ }
672
+ if (currentTextLine.trim()) {
673
+ chatLog.finalizeAssistant(currentTextLine.trim());
674
+ currentTextLine = '';
675
+ }
676
+ tui.requestRender();
677
+ },
678
+ onPermissionRequest(toolName, input) {
679
+ const inputStr = Object.entries(input)
680
+ .map(([k, v]) => `${k}=${String(v).substring(0, 60)}`)
681
+ .join(', ');
682
+ return new Promise((resolve) => {
683
+ const isFirst = permissionQueue.length === 0;
684
+ permissionQueue.push({ resolve, toolName, inputStr });
685
+ if (isFirst) {
686
+ showPermissionDialog(toolName, inputStr);
687
+ }
688
+ });
689
+ },
690
+ onInterrupt() {
691
+ hideThinkingIndicator();
692
+ isThinking = false;
693
+ pendingThinking = '';
694
+ currentTextLine = '';
695
+ },
696
+ onAskUser(questions) {
697
+ return new Promise((resolve) => {
698
+ const selectedOptions = new Map();
699
+ const customTexts = new Map();
700
+ for (let i = 0; i < questions.length; i++) {
701
+ selectedOptions.set(i, new Set());
702
+ customTexts.set(i, '');
703
+ }
704
+ // "补充" tab custom text
705
+ customTexts.set(questions.length, '');
706
+ askUserState = {
707
+ questions,
708
+ selectedOptions,
709
+ customTexts,
710
+ activeQuestion: 0,
711
+ activeOption: 0,
712
+ resolve,
713
+ };
714
+ renderAskUserForm();
715
+ tui.requestRender();
716
+ });
717
+ },
718
+ };
719
+ // ── Bootstrap detection ──
720
+ const resolvedBootstrapStatus = bootstrapStatus ?? (await getBootstrapStatus(personaDir));
721
+ // ── Local Model (llama.cpp) ──
722
+ try {
723
+ const loadedModels = await supervisor.loadAndStartModels(process.cwd());
724
+ if (loadedModels.length > 0) {
725
+ chatLog.addSystem(theme.success('\uD83D\uDDA5 Local Model Server'));
726
+ for (const m of loadedModels) {
727
+ chatLog.addSystem(theme.success(' \u2022 ') + theme.accent(m.name) + theme.dim(' \u2192 ') + theme.accent(m.baseUrl));
728
+ }
729
+ }
730
+ }
731
+ catch (error) {
732
+ const msg = error instanceof Error ? error.message : String(error);
733
+ chatLog.addSystem(theme.warning('\u26a0 Local model server not available: ' + msg));
734
+ }
735
+ tui.requestRender();
736
+ // ── 远程模式下跳过 createAgent,这些组件需手动初始化 ──
737
+ try {
738
+ CommandRegistry.getInstance(process.cwd());
739
+ }
740
+ catch { /* already initialized */ }
741
+ try {
742
+ const { RuntimeConfigCenter } = await import('../runtime/config-center.js');
743
+ const { getDefaultConfig } = await import('../runtime/defaults.js');
744
+ const configCenter = RuntimeConfigCenter.getInstance();
745
+ // 尝试读取配置——如果未初始化会抛异常
746
+ configCenter.get('session.maxTurns');
747
+ }
748
+ catch {
749
+ // 初始化 RuntimeConfigCenter
750
+ const { ConfigManager } = await import('../setup/config.js');
751
+ const { getDefaultConfig } = await import('../runtime/defaults.js');
752
+ const { RuntimeConfigCenter: RCC } = await import('../runtime/config-center.js');
753
+ const cm = new ConfigManager(process.cwd());
754
+ await cm.loadEnvKeys();
755
+ const cfg = await cm.load();
756
+ RCC.getInstance().initialize(getDefaultConfig(), cm);
757
+ RCC.getInstance().merge(cfg);
758
+ }
759
+ // ── 检测统一后端是否已运行 ──
760
+ let remoteWs = null;
761
+ try {
762
+ const healthy = await new Promise((resolve) => {
763
+ const req = http.get('http://127.0.0.1:3000/api/health', (res) => {
764
+ let d = '';
765
+ res.on('data', (c) => d += c);
766
+ res.on('end', () => resolve(d.includes('"ok"')));
767
+ });
768
+ req.on('error', () => resolve(false));
769
+ req.setTimeout(1500, () => { req.destroy(); resolve(false); });
770
+ });
771
+ if (healthy) {
772
+ const { default: WebSocket } = await import('ws');
773
+ remoteWs = await new Promise((resolve) => {
774
+ const ws = new WebSocket('ws://127.0.0.1:3000/tui');
775
+ ws.on('open', () => resolve(ws));
776
+ ws.on('error', () => resolve(null));
777
+ setTimeout(() => resolve(null), 3000);
778
+ });
779
+ }
780
+ }
781
+ catch { /* fall through to standalone */ }
782
+ // ── Agent + Session(远程模式下跳过,使用 WebSocket 连接) ──
783
+ let agent;
784
+ let sessionDir;
785
+ if (!remoteWs) {
786
+ agent = await createAgent({
787
+ cwd: process.cwd(),
788
+ provider: activeProvider,
789
+ maxTurns,
790
+ maxContext,
791
+ outputHandler: tuiHandler,
792
+ sessionId,
793
+ shouldContinue,
794
+ maxMessages,
795
+ personaDir,
796
+ bootstrapStatus: resolvedBootstrapStatus,
797
+ localModelProvider,
798
+ channel: 'tui',
799
+ });
800
+ sessionDir = agent.sessionDir;
801
+ // Wire ask_user tool to TUI handler
802
+ if (tuiHandler.onAskUser) {
803
+ setAskUserHandler((questions) => tuiHandler.onAskUser(questions));
804
+ }
805
+ }
806
+ else {
807
+ agent = undefined;
808
+ sessionDir = '';
809
+ }
810
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
811
+ const loop = agent?.loop ?? {
812
+ getActiveProvider: () => activeProvider,
813
+ getProviderRoutingInfo: () => null,
814
+ getTurnInfo: (_tc, _tu) => ({ turnCount: lastTurnCount, maxTurns, tokensUsed: lastTokensUsed, maxContextTokens: maxContext, sessionId: '', compressCount: 0 }),
815
+ setLifecycleSupervisor: () => { },
816
+ run: async () => { },
817
+ switchSession: async () => { },
818
+ switchProvider: async () => { },
819
+ getModelSources: () => null,
820
+ composeStrategy: null,
821
+ setScheduler: () => { },
822
+ notifyTaskFired: async () => { },
823
+ subscribeConfig: () => { },
824
+ isBootstrapPending: () => false,
825
+ shutdown: async () => { },
826
+ };
827
+ // 远程模式下 agent 为 undefined,这些变量仅用于本地 TUI 功能(斜杠命令等),用 any 避免 null 检查
828
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
829
+ const backgroundRegistry = agent?.backgroundRegistry;
830
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
831
+ const modelRouter = agent?.modelRouter;
832
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
833
+ const knowledgeBase = agent?.knowledgeBase;
834
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
835
+ const composeStrategy = agent?.composeStrategy;
836
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
837
+ const sessionManager = agent?.sessionManager;
838
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
839
+ const contextComposer = agent?.contextComposer;
840
+ let originalSessionDir = null; // /precise on 前保存的原始 session
841
+ let preciseModeActive = false;
842
+ // 检测启动时恢复的 session 类型(仅本地模式)
843
+ if (!remoteWs) {
844
+ const currentSessionId = path.basename(sessionDir);
845
+ const sessions = await sessionManager.list();
846
+ const resumedSession = sessions.find((s) => s.id === currentSessionId);
847
+ if (resumedSession?.type === 'precise') {
848
+ const { PreciseStrategy } = await import('../context/precision/index.js');
849
+ loop.composeStrategy = new PreciseStrategy(sessionDir);
850
+ preciseModeActive = true;
851
+ const normalSessions = sessions.filter((s) => (s.type ?? 'normal') === 'normal');
852
+ if (normalSessions.length > 0) {
853
+ originalSessionDir = sessionManager.getSessionDir(normalSessions[0].id);
854
+ }
855
+ chatLog.addSystem(theme.dim('精确模式 session 已恢复'));
856
+ }
857
+ }
858
+ // 注入 LifecycleSupervisor,实现运行时 provider 切换时自动管理本地模型进程
859
+ loop.setLifecycleSupervisor(supervisor);
860
+ const statsManager = new StatsManager();
861
+ // ── Channel bridge ──
862
+ const tuiChannel = new TuiChannel();
863
+ const channelManager = new ChannelManager();
864
+ channelManager.register(tuiChannel, { enabled: true });
865
+ // 自动检测配置驱动渠道(飞书等),有配置则自动注册
866
+ await registerConfigChannels(channelManager, process.cwd());
867
+ // 调用所有插件的 onGatewayInit 钩子(如飞书 SDK 日志拦截)
868
+ const cleanupFns = [];
869
+ for (const plugin of getChannelPlugins()) {
870
+ if (plugin.onGatewayInit) {
871
+ const cleanup = plugin.onGatewayInit();
872
+ if (cleanup)
873
+ cleanupFns.push(cleanup);
874
+ }
875
+ }
876
+ // 为所有渠道注入 TUI 同步回调(渠道自行决定是否启用 tuiSync)
877
+ for (const state of channelManager.getAll()) {
878
+ if (state.handler.id === 'tui')
879
+ continue;
880
+ state.config = {
881
+ ...state.config,
882
+ onUserMessage: (label, content) => {
883
+ chatLog.addSystem(`💬 [${state.handler.name}:${label}] ${content}`);
884
+ tui.requestRender();
885
+ },
886
+ onAgentReply: (content) => {
887
+ chatLog.addSystem(`📤 [${state.handler.name}回复] ${content}`);
888
+ tui.requestRender();
889
+ },
890
+ };
891
+ }
892
+ tuiChannel.onReply = (content) => {
893
+ chatLog.addSystem(content);
894
+ tui.requestRender();
895
+ };
896
+ // 创建 AgentFactory(注入到渠道,渠道通过此接口创建 AgentLoop)
897
+ const agentFactory = {
898
+ createAgent: async (options) => {
899
+ return createAgent({
900
+ cwd: process.cwd(),
901
+ provider: activeProvider,
902
+ maxTurns: maxTurns ?? 20,
903
+ maxContext: maxContext ?? 40_000,
904
+ outputHandler: options.outputHandler,
905
+ sessionId: options.sessionId,
906
+ channel: options.channel ?? 'tui',
907
+ });
908
+ },
909
+ };
910
+ // 设置 TUI 渠道的消息处理回调
911
+ if (remoteWs) {
912
+ // ── 远程模式:通过 WebSocket 连接统一后端 ──
913
+ chatLog.addSystem(theme.success('🔗 Connected to running backend (port 3000)'));
914
+ remoteWs.on('message', (data) => {
915
+ try {
916
+ const msg = JSON.parse(data.toString());
917
+ switch (msg.type) {
918
+ case 'text':
919
+ tuiHandler.onText?.(msg.content);
920
+ break;
921
+ case 'thinking':
922
+ tuiHandler.onThinking?.(msg.content);
923
+ break;
924
+ case 'tool_use':
925
+ tuiHandler.onToolUse?.(msg.name, msg.inputSummary, msg.id);
926
+ break;
927
+ case 'tool_result':
928
+ tuiHandler.onToolResult?.(msg.content, msg.isError, msg.id);
929
+ break;
930
+ case 'diff':
931
+ tuiHandler.onDiff?.(msg.id, msg.filePath, msg.diffLines);
932
+ break;
933
+ case 'status':
934
+ tuiHandler.onStatus?.(msg.message, msg.level);
935
+ break;
936
+ case 'turn_start':
937
+ tuiHandler.onTurnStart?.();
938
+ break;
939
+ case 'flush':
940
+ tuiHandler.onFlush?.();
941
+ break;
942
+ case 'interrupt':
943
+ tuiHandler.onInterrupt?.();
944
+ break;
945
+ case 'turn_info': {
946
+ lastTurnCount = msg.turnCount ?? lastTurnCount;
947
+ lastTokensUsed = msg.tokensUsed ?? lastTokensUsed;
948
+ refreshStatus({ turnCount: lastTurnCount, maxTurns, tokensUsed: lastTokensUsed, maxContextTokens: maxContext, sessionId: '', compressCount: 0 });
949
+ break;
950
+ }
951
+ case 'error':
952
+ chatLog.addSystem(theme.errorBright('[Error] ') + theme.error(msg.message));
953
+ break;
954
+ }
955
+ }
956
+ catch { /* ignore */ }
957
+ });
958
+ remoteWs.on('close', () => {
959
+ chatLog.addSystem(theme.warning('⚠ Backend connection lost. Restart to reconnect.'));
960
+ });
961
+ tuiChannel.onHandleMessage = async (event) => {
962
+ if (remoteWs?.readyState === 1) {
963
+ remoteWs.send(JSON.stringify({ type: 'chat', content: event.content }));
964
+ }
965
+ };
966
+ }
967
+ else {
968
+ tuiChannel.onHandleMessage = async (event) => {
969
+ await loop.run(event.content);
970
+ const stats = await statsManager.get(sessionDir);
971
+ const tc = stats.turn_count ?? 0;
972
+ refreshStatus(loop.getTurnInfo(tc, stats.current_context_tokens ?? 0));
973
+ };
974
+ }
975
+ // 启动所有渠道(每个渠道自行处理消息)
976
+ await channelManager.startAll(agentFactory);
977
+ // ── ASCII Art loader ──────────────────────────────────────────────
978
+ async function loadAsciiArt(maxWidth = 54) {
979
+ const asciiDir = path.join(os.homedir(), '.agent', 'ascii');
980
+ const lastFile = path.join(asciiDir, '_last.txt');
981
+ try {
982
+ if (!fs.existsSync(asciiDir)) {
983
+ fs.mkdirSync(asciiDir, { recursive: true });
984
+ return null;
985
+ }
986
+ const files = fs.readdirSync(asciiDir);
987
+ const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'];
988
+ const imageFiles = files.filter(f => imageExts.includes(path.extname(f).toLowerCase()));
989
+ if (imageFiles.length === 0)
990
+ return null;
991
+ let prevName = '';
992
+ try {
993
+ prevName = fs.readFileSync(lastFile, 'utf-8').trim();
994
+ }
995
+ catch { /* first */ }
996
+ const candidates = imageFiles.length > 1 ? imageFiles.filter(f => f !== prevName) : imageFiles;
997
+ const pool = candidates.length > 0 ? candidates : imageFiles;
998
+ const picked = pool[Math.floor(Math.random() * pool.length)];
999
+ try {
1000
+ fs.writeFileSync(lastFile, picked, 'utf-8');
1001
+ }
1002
+ catch { /* ignore */ }
1003
+ const imgPath = path.join(asciiDir, picked);
1004
+ const cachePath = path.join(asciiDir, picked + '.txt');
1005
+ try {
1006
+ const imgStat = fs.statSync(imgPath);
1007
+ const cacheStat = fs.statSync(cachePath);
1008
+ if (cacheStat.mtimeMs >= imgStat.mtimeMs) {
1009
+ return JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
1010
+ }
1011
+ }
1012
+ catch { /* cache miss */ }
1013
+ const { imageToAscii } = await import('../orchestrator/loop.js');
1014
+ const result = await imageToAscii(imgPath, maxWidth);
1015
+ if (result) {
1016
+ try {
1017
+ fs.writeFileSync(cachePath, JSON.stringify(result), 'utf-8');
1018
+ }
1019
+ catch { /* ignore */ }
1020
+ }
1021
+ return result;
1022
+ }
1023
+ catch {
1024
+ return null;
1025
+ }
1026
+ }
1027
+ // ── Welcome ──
1028
+ const asciiArt = await loadAsciiArt(54);
1029
+ const cwd = process.cwd();
1030
+ const cwdDisplay = cwd.length > 50 ? '...' + cwd.slice(-47) : cwd;
1031
+ const boxLines = [];
1032
+ boxLines.push(theme.fg('\u256d' + BOX_H.repeat(58) + '\u256e'));
1033
+ if (asciiArt) {
1034
+ const artWidth = asciiArt.width;
1035
+ const pad = Math.max(0, 56 - artWidth);
1036
+ for (const line of asciiArt.text.split('\n')) {
1037
+ boxLines.push(theme.fg('\u2502 ') + line + ' '.repeat(pad) + theme.fg(' \u2502'));
1038
+ }
1039
+ }
1040
+ // Hyacinth \u6807\u9898\u884c
1041
+ const titleLine = theme.fg(' Hyacinth');
1042
+ const titlePad = Math.max(0, 56 - 9); // ' Hyacinth' = 9 chars visible
1043
+ boxLines.push(theme.fg('\u2502 ') + titleLine + ' '.repeat(titlePad) + theme.fg(' \u2502'));
1044
+ // CWD \u884c
1045
+ const cwdPad = Math.max(0, 56 - [...cwdDisplay].length);
1046
+ boxLines.push(theme.fg('\u2502 ') + theme.dim(cwdDisplay) + ' '.repeat(cwdPad) + theme.fg(' \u2502'));
1047
+ boxLines.push(theme.fg('\u2570' + BOX_H.repeat(58) + '\u256f'));
1048
+ chatLog.addSystem(boxLines.join('\n'));
1049
+ chatLog.addSystem('');
1050
+ chatLog.addSystem(theme.dim('Persona: ') + theme.accent(personaDir));
1051
+ chatLog.addSystem('');
1052
+ // ── Bootstrap ──
1053
+ if (resolvedBootstrapStatus === 'pending') {
1054
+ chatLog.addSystem(theme.warning('\u256d' + BOX_H.repeat(56) + '\u256e'));
1055
+ chatLog.addSystem(theme.warning('\u2502 \uD83D\uDD27 Bootstrap Init'));
1056
+ chatLog.addSystem(theme.warning('\u2502 AI will ask you setup questions to configure the persona.'));
1057
+ chatLog.addSystem(theme.warning('\u2570' + BOX_H.repeat(56) + '\u256f'));
1058
+ chatLog.addSystem('');
1059
+ try {
1060
+ // Flow 已在 factory.ts 中自动激活,发送空消息让模型根据 flow_injection 引导对话
1061
+ await loop.run('');
1062
+ }
1063
+ catch (error) {
1064
+ const message = error instanceof Error ? error.message : String(error);
1065
+ chatLog.addSystem(theme.errorBright('[Bootstrap Error] ') + theme.error(message));
1066
+ }
1067
+ }
1068
+ chatLog.addSystem(theme.dim('Type ') +
1069
+ theme.success('exit') +
1070
+ theme.dim(' to quit. ') +
1071
+ theme.success('Ctrl+C') +
1072
+ theme.dim(' twice to force. ') +
1073
+ theme.success('Ctrl+P') +
1074
+ theme.dim(' to toggle provider.'));
1075
+ chatLog.addSystem('');
1076
+ tui.requestRender();
1077
+ // Replay previous session events
1078
+ if (sessionDir) {
1079
+ replayEvents(chatLog, sessionDir);
1080
+ }
1081
+ // ── Input state ──
1082
+ let isProcessing = false;
1083
+ const messageQueue = new MessageQueue();
1084
+ let inputHistory = [];
1085
+ let historyIndex = -1;
1086
+ // ── History persistence ──
1087
+ const historyFilePath = path.join(os.homedir(), '.agent', 'history.json');
1088
+ const HISTORY_MAX = 500;
1089
+ try {
1090
+ if (fs.existsSync(historyFilePath)) {
1091
+ const raw = fs.readFileSync(historyFilePath, 'utf-8');
1092
+ const parsed = JSON.parse(raw);
1093
+ if (Array.isArray(parsed)) {
1094
+ inputHistory = parsed.slice(-HISTORY_MAX);
1095
+ }
1096
+ }
1097
+ }
1098
+ catch { /* ignore corrupt history */ }
1099
+ function saveHistory() {
1100
+ try {
1101
+ const dir = path.dirname(historyFilePath);
1102
+ if (!fs.existsSync(dir))
1103
+ fs.mkdirSync(dir, { recursive: true });
1104
+ fs.writeFileSync(historyFilePath, JSON.stringify(inputHistory.slice(-HISTORY_MAX), null, 2), 'utf-8');
1105
+ }
1106
+ catch { /* ignore write errors */ }
1107
+ }
1108
+ function updateTokenEstimate() {
1109
+ const text = editor.getText();
1110
+ const estimated = Math.ceil(text.length / 4);
1111
+ const ap = loop.getActiveProvider();
1112
+ const providerLabel = `${ap.getProviderType()} \u00b7 `;
1113
+ let footer = theme.dim(`Ctrl+C exit | Ctrl+L clear | Ctrl+P provider | Ctrl+F search | Ctrl+T tools`);
1114
+ // Context-aware hints
1115
+ if (isThinking) {
1116
+ footer += theme.accent(' | Esc to stop');
1117
+ }
1118
+ if (permissionQueue.length > 0) {
1119
+ footer += theme.warning(' | \u2190\u2192 select Enter confirm');
1120
+ }
1121
+ if (backgroundRegistry) {
1122
+ const bgCount = backgroundRegistry.list().filter((p) => p.status === 'running').length;
1123
+ if (bgCount > 0) {
1124
+ footer += theme.accent(` | \u2699 ${bgCount} bg process(es)`);
1125
+ }
1126
+ }
1127
+ footer += theme.dim(`\n${providerLabel}${ap.getModel()} \u00b7 ~${estimated} tokens`);
1128
+ if (messageQueue.size > 0) {
1129
+ footer += theme.fg(` | Queue: ${messageQueue.size}`);
1130
+ }
1131
+ footerText.setText(footer);
1132
+ }
1133
+ function truncateMsg(text, max = 40) {
1134
+ return text.length > max ? text.slice(0, max) + '...' : text;
1135
+ }
1136
+ /**
1137
+ * Process one message through the agent loop, then drain any queued messages.
1138
+ * Called by handleInput when the agent is idle.
1139
+ */
1140
+ async function processBatch(initialText) {
1141
+ let text = initialText;
1142
+ while (text !== undefined) {
1143
+ isProcessing = true;
1144
+ editor.setText('');
1145
+ chatLog.addUser('❯ ' + text);
1146
+ chatLog.addSystem(theme.dim('─'.repeat(30)));
1147
+ try {
1148
+ const sid = sessionDir.split(/[\\/]/).pop() ?? 'tui-default';
1149
+ await tuiChannel.sendMessage(sid, text);
1150
+ }
1151
+ catch (error) {
1152
+ const message = error instanceof Error ? error.message : String(error);
1153
+ chatLog.addSystem(theme.errorBright('[Error] ') + theme.error(message));
1154
+ }
1155
+ isProcessing = false;
1156
+ // Check queue for pending messages
1157
+ const next = messageQueue.dequeue();
1158
+ text = next?.text;
1159
+ }
1160
+ updateTokenEstimate();
1161
+ tui.requestRender();
1162
+ }
1163
+ async function handleInput(text) {
1164
+ let input = text.trim();
1165
+ if (!input) {
1166
+ // 定时任务完成后队列中有待处理消息 → 空回车触发队列消费
1167
+ if (!isProcessing && !loop.pendingTaskName && messageQueue.size > 0) {
1168
+ const next = messageQueue.dequeue();
1169
+ if (next)
1170
+ await processBatch(next.text);
1171
+ }
1172
+ return;
1173
+ }
1174
+ // Push to history
1175
+ if (inputHistory.length === 0 || inputHistory[inputHistory.length - 1] !== input) {
1176
+ inputHistory.push(input);
1177
+ if (inputHistory.length > HISTORY_MAX) {
1178
+ inputHistory.shift();
1179
+ }
1180
+ saveHistory();
1181
+ }
1182
+ historyIndex = -1;
1183
+ if (input === 'exit' || input === 'quit') {
1184
+ chatLog.addSystem(theme.dim('Goodbye.'));
1185
+ tui.requestRender();
1186
+ await channelManager.stopAll();
1187
+ await loop.shutdown();
1188
+ // 清理插件钩子
1189
+ for (const cleanup of cleanupFns)
1190
+ cleanup();
1191
+ await supervisor.shutdownAll();
1192
+ tui.stop();
1193
+ process.exit(0);
1194
+ }
1195
+ // 二级菜单:检查命令是否有 children
1196
+ if (input.startsWith('/')) {
1197
+ const spaceIdx = input.indexOf(' ');
1198
+ const cmdName = spaceIdx > 0 ? input.slice(1, spaceIdx) : input.slice(1);
1199
+ const cmdDef = CommandRegistry.getInstance().find(cmdName);
1200
+ if ((cmdDef?.children && cmdDef.children.length > 0) || !!cmdDef?.childrenProvider) {
1201
+ const args = spaceIdx > 0 ? input.slice(spaceIdx + 1).trim() : '';
1202
+ // Build dynamic entries for /model L1 panel
1203
+ let effectiveDef = cmdDef;
1204
+ if (cmdName === 'model') {
1205
+ const dynamicChildren = [];
1206
+ // a) Current online model
1207
+ const activeP = loop.getActiveProvider();
1208
+ dynamicChildren.push({
1209
+ name: 'current_online',
1210
+ description: `${activeP.getProviderType()} (online) - ${activeP.getModel()}`,
1211
+ icon: '\u2601',
1212
+ category: 'model',
1213
+ });
1214
+ // b) Local models
1215
+ for (const m of localModel.list()) {
1216
+ const isRunning = localModel.getBridge().isRunning(m.name);
1217
+ const status = isRunning ? '\u25C9 running' : '\u25CB stopped';
1218
+ dynamicChildren.push({
1219
+ name: 'local_' + m.name,
1220
+ description: `${m.name} (local) - ${status}`,
1221
+ icon: '\u{1F4BB}',
1222
+ category: 'model',
1223
+ });
1224
+ }
1225
+ effectiveDef = {
1226
+ ...cmdDef,
1227
+ children: [...dynamicChildren, ...(cmdDef.children ?? [])],
1228
+ };
1229
+ }
1230
+ if (!args) {
1231
+ // 无参数 → 弹出二级面板
1232
+ slashSubPanelActive = true;
1233
+ const panel = new SlashSubPanel(effectiveDef);
1234
+ panel.show(tui, (result) => {
1235
+ slashSubPanelActive = false;
1236
+ if (result) {
1237
+ // 如果叶子命令有 args 占位符 → 自动填充到输入框,让用户继续输入
1238
+ const leafDef = CommandRegistry.getInstance().find(result.path);
1239
+ if (leafDef?.args && !leafDef.children && !leafDef.childrenProvider) {
1240
+ editor.setText(`/${result.path} `);
1241
+ updateTokenEstimate();
1242
+ return;
1243
+ }
1244
+ handleSlashSubCommand(result.path, '');
1245
+ }
1246
+ });
1247
+ return;
1248
+ }
1249
+ // 有参数 → 解析子命令路径并直接执行
1250
+ const subSpaceIdx = args.indexOf(' ');
1251
+ const subCmdName = subSpaceIdx > 0 ? args.slice(0, subSpaceIdx) : args;
1252
+ const subArgs = subSpaceIdx > 0 ? args.slice(subSpaceIdx + 1) : '';
1253
+ const subCmd = cmdDef?.children?.find((c) => c.name.toLowerCase() === subCmdName.toLowerCase());
1254
+ if (subCmd) {
1255
+ handleSlashSubCommand(`${cmdName}/${subCmd.name}`, subArgs);
1256
+ return;
1257
+ }
1258
+ const childNames = (cmdDef?.children ?? []).map((c) => c.name).join(', ');
1259
+ chatLog.addSystem(theme.warning(`Unknown sub-command: /${cmdName} ${subCmdName}`) +
1260
+ theme.dim(`\nAvailable: /${cmdName} ${childNames}`));
1261
+ tui.requestRender();
1262
+ return;
1263
+ }
1264
+ }
1265
+ // ===== Sub-Command Handler =====
1266
+ /** 持久化单个配置字段到 JSON 文件(不展开默认值) */
1267
+ async function persistConfigField(key, value) {
1268
+ const configPath = path.join(os.homedir(), '.agent', 'config.json');
1269
+ try {
1270
+ const raw = await fs.promises.readFile(configPath, 'utf-8');
1271
+ const obj = JSON.parse(raw);
1272
+ const parts = key.split('.');
1273
+ let cur = obj;
1274
+ for (let i = 0; i < parts.length - 1; i++) {
1275
+ if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object') {
1276
+ cur[parts[i]] = {};
1277
+ }
1278
+ cur = cur[parts[i]];
1279
+ }
1280
+ cur[parts[parts.length - 1]] = value;
1281
+ await fs.promises.writeFile(configPath, JSON.stringify(obj, null, 2), 'utf-8');
1282
+ }
1283
+ catch { /* 文件不存在或解析失败,跳过持久化 */ }
1284
+ }
1285
+ /** /model thinking 公共逻辑 */
1286
+ function applyThinking(action) {
1287
+ const cfg = RuntimeConfigCenter.getInstance();
1288
+ const activeP = loop.getActiveProvider();
1289
+ const providerType = activeP.getProviderType();
1290
+ const effortOptions = {};
1291
+ if (providerType === 'deepseek') {
1292
+ effortOptions.high = { label: 'high', effort: 'high' };
1293
+ effortOptions.max = { label: 'max', effort: 'max' };
1294
+ }
1295
+ else if (providerType === 'anthropic') {
1296
+ effortOptions['4k'] = { label: '4K', effort: 4000 };
1297
+ effortOptions['8k'] = { label: '8K', effort: 8000 };
1298
+ effortOptions['16k'] = { label: '16K', effort: 16000 };
1299
+ effortOptions['32k'] = { label: '32K', effort: 32000 };
1300
+ }
1301
+ if (action === 'on') {
1302
+ cfg.set('provider.enableThinking', true);
1303
+ activeP.setThinking?.(true);
1304
+ persistConfigField('provider.enableThinking', true);
1305
+ chatLog.addSystem(theme.success('Thinking enabled'));
1306
+ }
1307
+ else if (action === 'off') {
1308
+ cfg.set('provider.enableThinking', false);
1309
+ activeP.setThinking?.(false);
1310
+ persistConfigField('provider.enableThinking', false);
1311
+ chatLog.addSystem(theme.success('Thinking disabled'));
1312
+ }
1313
+ else if (effortOptions[action]) {
1314
+ const opt = effortOptions[action];
1315
+ cfg.set('provider.enableThinking', true);
1316
+ activeP.setThinking?.(true, opt.effort);
1317
+ persistConfigField('provider.enableThinking', true);
1318
+ chatLog.addSystem(theme.success(`Thinking enabled (${opt.label})`));
1319
+ }
1320
+ else {
1321
+ const optsStr = Object.entries(effortOptions)
1322
+ .map(([k, v]) => ` ${k} → ${v.label}`)
1323
+ .join('\n');
1324
+ chatLog.addSystem(theme.warning(`Usage: /model thinking <on|off${optsStr ? '|' + Object.keys(effortOptions).join('|') : ''}>` + (optsStr ? '\n' + optsStr : '')));
1325
+ }
1326
+ tui.requestRender();
1327
+ updateTokenEstimate();
1328
+ }
1329
+ /**
1330
+ * 通用 handler 路由:根据 "module.method" 格式的 handler 字符串,
1331
+ * 动态查找并调用对应模块的方法。
1332
+ */
1333
+ function resolveHandler(handlerStr, args, localModel, chatLog, themeObj, tui) {
1334
+ const parts = handlerStr.split('.');
1335
+ if (parts.length < 2)
1336
+ return false;
1337
+ const moduleName = parts[0];
1338
+ const methodChain = parts.slice(1);
1339
+ let target;
1340
+ if (moduleName === 'localModel') {
1341
+ target = localModel;
1342
+ }
1343
+ else {
1344
+ return false;
1345
+ }
1346
+ let fn = target;
1347
+ for (const prop of methodChain) {
1348
+ if (fn && typeof fn === 'object' && prop in fn) {
1349
+ fn = fn[prop];
1350
+ }
1351
+ else {
1352
+ chatLog.addSystem(themeObj.error(`Command handler not found: ${handlerStr}`));
1353
+ tui.requestRender();
1354
+ return true;
1355
+ }
1356
+ }
1357
+ if (typeof fn !== 'function') {
1358
+ chatLog.addSystem(themeObj.error(`Command handler not found: ${handlerStr}`));
1359
+ tui.requestRender();
1360
+ return true;
1361
+ }
1362
+ try {
1363
+ const result = args ? fn(args) : fn();
1364
+ if (result instanceof Promise) {
1365
+ result.then((val) => {
1366
+ if (val !== undefined && val !== null) {
1367
+ const display = typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
1368
+ chatLog.addSystem(themeObj.success(display));
1369
+ }
1370
+ tui.requestRender();
1371
+ }).catch((err) => {
1372
+ chatLog.addSystem(themeObj.error(`Handler error: ${err.message}`));
1373
+ tui.requestRender();
1374
+ });
1375
+ }
1376
+ else if (result !== undefined && result !== null) {
1377
+ const display = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result);
1378
+ chatLog.addSystem(themeObj.success(display));
1379
+ }
1380
+ }
1381
+ catch (err) {
1382
+ chatLog.addSystem(themeObj.error(`Handler error: ${err.message}`));
1383
+ tui.requestRender();
1384
+ return true;
1385
+ }
1386
+ return true;
1387
+ }
1388
+ /**
1389
+ * 处理二级菜单子命令的执行。
1390
+ * path 格式: "model/switch" / "model/provider" 等
1391
+ */
1392
+ async function handleSlashSubCommand(cmdPath, restArgs) {
1393
+ const cfg = RuntimeConfigCenter.getInstance();
1394
+ if (cmdPath.startsWith('session/')) {
1395
+ let sub = cmdPath.slice('session/'.length);
1396
+ // 支持嵌套路径: session/<id>/load → sub = "<id>/load"
1397
+ const subParts = sub.split('/');
1398
+ if (subParts.length === 2 && (subParts[1] === 'load' || subParts[1] === 'delete')) {
1399
+ // session/<id>/load 或 session/<id>/delete
1400
+ const sessionId = subParts[0];
1401
+ if (subParts[1] === 'load') {
1402
+ const SessionManager = (await import('../memory/session.js')).SessionManager;
1403
+ const sm = new SessionManager(process.cwd());
1404
+ const dir = sm.getSessionDir(sessionId);
1405
+ const fsPromises = await import('node:fs/promises');
1406
+ try {
1407
+ await fsPromises.access(dir);
1408
+ }
1409
+ catch {
1410
+ chatLog.addSystem(theme.warning(`Session "${sessionId}" not found. Use /session list to see available sessions.`));
1411
+ tui.requestRender();
1412
+ updateTokenEstimate();
1413
+ return;
1414
+ }
1415
+ // 就地切换,不重启
1416
+ await loop.switchSession(dir);
1417
+ sessionDir = dir;
1418
+ chatLog.clearAll();
1419
+ replayEvents(chatLog, dir);
1420
+ chatLog.addSystem(theme.success(`已切换到 session ${sessionId}`));
1421
+ updateTokenEstimate();
1422
+ }
1423
+ else {
1424
+ const SessionManager = (await import('../memory/session.js')).SessionManager;
1425
+ const sm = new SessionManager(process.cwd());
1426
+ const dir = sm.getSessionDir(sessionId);
1427
+ // 保护当前活跃 session
1428
+ if (dir === sessionDir) {
1429
+ chatLog.addSystem(theme.warning(`Cannot delete the currently active session "${sessionId}". Switch to another session first.`));
1430
+ tui.requestRender();
1431
+ updateTokenEstimate();
1432
+ return;
1433
+ }
1434
+ try {
1435
+ const fsPromises = await import('node:fs/promises');
1436
+ await fsPromises.access(dir);
1437
+ await fsPromises.rm(dir, { recursive: true, force: true });
1438
+ chatLog.addSystem(theme.success(`Session ${sessionId} deleted`));
1439
+ }
1440
+ catch {
1441
+ chatLog.addSystem(theme.warning(`Session ${sessionId} not found`));
1442
+ }
1443
+ }
1444
+ tui.requestRender();
1445
+ updateTokenEstimate();
1446
+ return;
1447
+ }
1448
+ if (sub === 'list') {
1449
+ const SessionManager = (await import('../memory/session.js')).SessionManager;
1450
+ const sm = new SessionManager(process.cwd());
1451
+ const sessions = await sm.list();
1452
+ const modeType = preciseModeActive ? 'precise' : 'normal';
1453
+ const filtered = sessions.filter(s => (s.type ?? 'normal') === modeType);
1454
+ if (filtered.length === 0) {
1455
+ chatLog.addSystem(theme.dim(`No ${modeType} sessions found`));
1456
+ }
1457
+ else {
1458
+ for (const s of filtered) {
1459
+ const isCurrent = s.id === path.basename(sessionDir);
1460
+ const marker = isCurrent ? theme.success(' ← current') : '';
1461
+ const typeLabel = s.type ? theme.dim(` [${s.type}]`) : '';
1462
+ chatLog.addSystem(theme.accent(s.id) +
1463
+ theme.dim(` | created: ${s.createdAt}`) +
1464
+ theme.dim(` | updated: ${s.updatedAt}`) +
1465
+ typeLabel + marker);
1466
+ }
1467
+ chatLog.addSystem(theme.dim('─'.repeat(60)));
1468
+ chatLog.addSystem(theme.dim('加载会话: /session <完整ID>/load 例如: /session ' + (filtered[0]?.id ?? '') + '/load'));
1469
+ }
1470
+ }
1471
+ else if (sub === 'new') {
1472
+ chatLog.addSystem(theme.warning('请使用 hyacinth start 启动新会话(当前会话需要退出)'));
1473
+ }
1474
+ else if (sub === 'load') {
1475
+ const id = restArgs?.trim();
1476
+ if (!id) {
1477
+ chatLog.addSystem(theme.warning('用法: /session load <sessionId>'));
1478
+ }
1479
+ else {
1480
+ chatLog.addSystem(theme.warning(`请使用 hyacinth start --session ${id} 加载会话`));
1481
+ }
1482
+ }
1483
+ else if (sub === 'delete') {
1484
+ const id = restArgs?.trim();
1485
+ if (!id) {
1486
+ chatLog.addSystem(theme.warning('用法: /session delete <sessionId>'));
1487
+ }
1488
+ else {
1489
+ const SessionManager = (await import('../memory/session.js')).SessionManager;
1490
+ const sm = new SessionManager(process.cwd());
1491
+ // 检查 session 类型是否匹配当前模式
1492
+ const sessions = await sm.list();
1493
+ const target = sessions.find(s => s.id === id);
1494
+ if (!target) {
1495
+ chatLog.addSystem(theme.warning(`Session ${id} not found`));
1496
+ }
1497
+ else if ((target.type ?? 'normal') !== (preciseModeActive ? 'precise' : 'normal')) {
1498
+ chatLog.addSystem(theme.warning(`Session ${id} 是 [${target.type ?? 'normal'}] 类型,当前为 [${preciseModeActive ? 'precise' : 'normal'}] 模式,无法跨模式操作`));
1499
+ }
1500
+ else {
1501
+ const dir = sm.getSessionDir(id);
1502
+ try {
1503
+ const fsPromises = await import('node:fs/promises');
1504
+ await fsPromises.access(dir);
1505
+ await fsPromises.rm(dir, { recursive: true, force: true });
1506
+ chatLog.addSystem(theme.success(`Session ${id} deleted`));
1507
+ }
1508
+ catch {
1509
+ chatLog.addSystem(theme.warning(`Session ${id} not found`));
1510
+ }
1511
+ }
1512
+ }
1513
+ }
1514
+ tui.requestRender();
1515
+ return;
1516
+ }
1517
+ switch (cmdPath) {
1518
+ case 'model/settings/switch':
1519
+ case 'model/switch': {
1520
+ if (!restArgs) {
1521
+ chatLog.addSystem(theme.warning('Usage: /model switch <model-name>'));
1522
+ tui.requestRender();
1523
+ return;
1524
+ }
1525
+ const activeP = loop.getActiveProvider();
1526
+ const providerType = activeP.getProviderType();
1527
+ cfg.set(`provider.${providerType}.model`, restArgs);
1528
+ cfg.save().catch(() => { });
1529
+ chatLog.addSystem(theme.success('Model name set to ') + theme.fg(String(restArgs)) + theme.dim(` (provider: ${providerType})`));
1530
+ tui.requestRender();
1531
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
1532
+ return;
1533
+ }
1534
+ case 'model/settings/provider':
1535
+ case 'model/provider': {
1536
+ if (!restArgs) {
1537
+ chatLog.addSystem(theme.warning('Usage: /model provider <anthropic|openai|deepseek|gemini|groq|xai|mistral|openrouter|moonshot|qwen|zhipu|minimax|mimo|local>'));
1538
+ tui.requestRender();
1539
+ return;
1540
+ }
1541
+ if (restArgs === 'local') {
1542
+ const lmList = localModel.list();
1543
+ // 无已注册模型 → 检查本地模型配置或直接切
1544
+ if (lmList.length === 0) {
1545
+ const { getLocalProviderConfigLoader } = await import('../provider/local-config.js');
1546
+ const localCfg = getLocalProviderConfigLoader();
1547
+ if (localCfg?.defaultModel) {
1548
+ // 本地模型已配置 → 直接切换
1549
+ try {
1550
+ await loop.switchProvider('local');
1551
+ // 显式持久化 provider 选择(switchProvider 不再负责持久化)
1552
+ cfg.set('provider.active', 'local');
1553
+ cfg.save().catch(() => { });
1554
+ modelName = loop.getActiveProvider().getModel();
1555
+ providerTypeStart = loop.getActiveProvider().getProviderType();
1556
+ chatLog.addSystem(theme.success(`Switched to local (${localCfg.baseUrl}, ${localCfg.defaultModel})`));
1557
+ chatLog.addSystem(theme.dim('Register models via /model local/register for process management.'));
1558
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
1559
+ }
1560
+ catch (err) {
1561
+ chatLog.addSystem(theme.error(`Switch failed: ${err.message}`));
1562
+ }
1563
+ }
1564
+ else {
1565
+ chatLog.addSystem(theme.warning('No local models configured.'));
1566
+ chatLog.addSystem(theme.dim('Set a local model in config or register models via /model local/register.'));
1567
+ }
1568
+ tui.requestRender();
1569
+ return;
1570
+ }
1571
+ const targetName = localModel.getActive() ?? lmList[0].name;
1572
+ localModel.switch(targetName).then(async (info) => {
1573
+ if (info) {
1574
+ cfg.set('provider.local', {
1575
+ type: 'local',
1576
+ model: info.modelFile ?? targetName,
1577
+ baseUrl: info.baseUrl,
1578
+ });
1579
+ cfg.set('provider.local.modelKey', targetName);
1580
+ cfg.save().catch(() => { });
1581
+ try {
1582
+ await loop.switchProvider('local');
1583
+ // 显式持久化 provider 选择(switchProvider 不再负责持久化)
1584
+ cfg.set('provider.active', 'local');
1585
+ cfg.save().catch(() => { });
1586
+ modelName = loop.getActiveProvider().getModel();
1587
+ providerTypeStart = loop.getActiveProvider().getProviderType();
1588
+ chatLog.addSystem(theme.success(`Switched to local model: ${targetName} (port ${info.port})`));
1589
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
1590
+ }
1591
+ catch (swErr) {
1592
+ chatLog.addSystem(theme.error(`Failed: ${swErr.message}`));
1593
+ }
1594
+ }
1595
+ else {
1596
+ chatLog.addSystem(theme.error(`Failed to start ${targetName}`));
1597
+ }
1598
+ tui.requestRender();
1599
+ }).catch((e) => {
1600
+ chatLog.addSystem(theme.error(`Failed: ${e.message}`));
1601
+ tui.requestRender();
1602
+ });
1603
+ return;
1604
+ }
1605
+ localModel.getBridge().stopAll().catch(() => { });
1606
+ try {
1607
+ await loop.switchProvider(restArgs);
1608
+ // 显式持久化 provider 选择(switchProvider 不再负责持久化)
1609
+ cfg.set('provider.active', restArgs);
1610
+ cfg.save().catch(() => { });
1611
+ }
1612
+ catch (e) {
1613
+ chatLog.addSystem(theme.error(`Failed: ${e.message}`));
1614
+ tui.requestRender();
1615
+ return;
1616
+ }
1617
+ modelName = loop.getActiveProvider().getModel();
1618
+ providerTypeStart = loop.getActiveProvider().getProviderType();
1619
+ chatLog.addSystem(theme.success('Provider switched to ') + theme.fg(String(restArgs)) + theme.dim(' (persisted)'));
1620
+ tui.requestRender();
1621
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
1622
+ return;
1623
+ }
1624
+ case 'model/settings/source':
1625
+ case 'model/source': {
1626
+ if (!restArgs) {
1627
+ chatLog.addSystem(theme.warning('Usage: /model source <role> <main|local|channel-name>'));
1628
+ tui.requestRender();
1629
+ return;
1630
+ }
1631
+ const parts2 = restArgs.split(/\s+/).filter(Boolean);
1632
+ if (parts2.length < 2) {
1633
+ chatLog.addSystem(theme.warning('Usage: /model source <assessment|planning|compression|sub-agent|all> <main|local|channel-name>'));
1634
+ tui.requestRender();
1635
+ return;
1636
+ }
1637
+ const roleArg = parts2[0].toLowerCase();
1638
+ const sourceArg = parts2[1];
1639
+ const validRoles = ['assessment', 'planning', 'compression', 'sub-agent', 'all'];
1640
+ if (!validRoles.includes(roleArg)) {
1641
+ chatLog.addSystem(theme.warning('Role must be: assessment, planning, compression, sub-agent, or all'));
1642
+ tui.requestRender();
1643
+ return;
1644
+ }
1645
+ // 尝试用 ModelChannelRegistry 的 setRoleMapping(多通道模式)
1646
+ const modelRouter = loop.modelRouter;
1647
+ const registry = modelRouter?.getRegistry();
1648
+ const channelNames = registry ? registry.listChannelNames() : [];
1649
+ const isChannelName = channelNames.includes(sourceArg);
1650
+ try {
1651
+ if (isChannelName && registry) {
1652
+ // 映射到已注册的通道
1653
+ const roles = roleArg === 'all'
1654
+ ? ['assessment', 'planning', 'compression', 'sub-agent']
1655
+ : [roleArg];
1656
+ for (const r of roles) {
1657
+ registry.setRoleMapping(r, sourceArg);
1658
+ }
1659
+ chatLog.addSystem(theme.success(`Mapped ${roles.join(', ')} → channel "${sourceArg}"`));
1660
+ }
1661
+ else {
1662
+ chatLog.addSystem(theme.warning(`Channel "${sourceArg}" not found. Available: ${channelNames.join(', ') || '(none)'}. Use /channel add ${sourceArg} <provider> [model] to create it.`));
1663
+ }
1664
+ }
1665
+ catch (e) {
1666
+ chatLog.addSystem(theme.error(`Failed: ${e.message}`));
1667
+ }
1668
+ tui.requestRender();
1669
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
1670
+ return;
1671
+ }
1672
+ case 'model/settings/thinking':
1673
+ case 'model/thinking': {
1674
+ applyThinking(restArgs);
1675
+ return;
1676
+ }
1677
+ // 四级菜单:/model thinking on|off|high|max 通过子面板选择
1678
+ case 'model/settings/thinking/on':
1679
+ case 'model/thinking/on':
1680
+ applyThinking('on');
1681
+ return;
1682
+ case 'model/settings/thinking/off':
1683
+ case 'model/thinking/off':
1684
+ applyThinking('off');
1685
+ return;
1686
+ case 'model/settings/thinking/high':
1687
+ case 'model/thinking/high':
1688
+ applyThinking('high');
1689
+ return;
1690
+ case 'model/settings/thinking/max':
1691
+ case 'model/thinking/max':
1692
+ applyThinking('max');
1693
+ return;
1694
+ case 'model/settings/thinking/4k':
1695
+ case 'model/thinking/4k':
1696
+ applyThinking('4k');
1697
+ return;
1698
+ case 'model/settings/thinking/8k':
1699
+ case 'model/thinking/8k':
1700
+ applyThinking('8k');
1701
+ return;
1702
+ case 'model/settings/thinking/16k':
1703
+ case 'model/thinking/16k':
1704
+ applyThinking('16k');
1705
+ return;
1706
+ case 'model/settings/thinking/32k':
1707
+ case 'model/thinking/32k':
1708
+ applyThinking('32k');
1709
+ return;
1710
+ case 'model/settings/show-thinking':
1711
+ case 'model/show-thinking': {
1712
+ showThinking = !showThinking;
1713
+ chatLog.addSystem(theme.success(showThinking ? 'Thinking content will be shown' : 'Thinking content hidden'));
1714
+ tui.requestRender();
1715
+ updateTokenEstimate();
1716
+ return;
1717
+ }
1718
+ case 'model/settings/info':
1719
+ case 'model/info': {
1720
+ const providerName = String(cfg.get('provider.active') ?? 'unknown');
1721
+ const activeProvider = loop.getActiveProvider();
1722
+ const modelName = activeProvider ? activeProvider.getModel() : 'unknown';
1723
+ const lines = [];
1724
+ lines.push(theme.accent('=== Model Info ==='));
1725
+ lines.push(' Provider: ' + theme.fg(providerName));
1726
+ lines.push(' Model: ' + theme.fg(modelName));
1727
+ const routing = loop.getProviderRoutingInfo();
1728
+ if (routing) {
1729
+ lines.push(' Route: ' + theme.fg(routing.mode) + (routing.isLocal ? theme.success(' (local)') : theme.accent(' (online)')));
1730
+ }
1731
+ // 通道信息
1732
+ const modelRouter = loop.modelRouter;
1733
+ if (modelRouter) {
1734
+ const registry = modelRouter.getRegistry();
1735
+ const channels = registry.listChannels();
1736
+ const roles = registry.listRoles();
1737
+ if (channels.length > 1 || Object.keys(roles).some(r => roles[r] !== 'main')) {
1738
+ lines.push(theme.dim(' ── Channels ──'));
1739
+ for (const ch of channels) {
1740
+ const chRoles = Object.entries(roles)
1741
+ .filter(([, cn]) => cn === ch.name)
1742
+ .map(([r]) => r);
1743
+ const roleStr = chRoles.length > 0 ? ' ← ' + chRoles.join(', ') : '';
1744
+ lines.push(theme.dim(` ${ch.name}: ${ch.provider}${ch.model ? '/' + ch.model : ''}`) + theme.fg(roleStr));
1745
+ }
1746
+ }
1747
+ }
1748
+ // 旧 source 信息
1749
+ const sources = loop.getModelSources();
1750
+ if (sources) {
1751
+ const labels = { assessment: '评估', planning: '规划', compression: '压缩' };
1752
+ for (const [role, src] of Object.entries(sources)) {
1753
+ const label = labels[role] ?? role;
1754
+ const srcColor = src === 'local' ? theme.success(String(src)) : theme.accent(String(src));
1755
+ lines.push(' ' + theme.fg(label) + theme.dim(': ') + srcColor);
1756
+ }
1757
+ }
1758
+ chatLog.addSystem(lines.join('\n'));
1759
+ tui.requestRender();
1760
+ updateTokenEstimate();
1761
+ return;
1762
+ }
1763
+ case 'model/settings/context': {
1764
+ if (!restArgs) {
1765
+ const activeP = loop.getActiveProvider();
1766
+ const modelCtxWindow = getModelContextWindow(activeP.getProviderType(), activeP.getModel());
1767
+ chatLog.addSystem(theme.warning('Usage: /model settings context <tokens>') + theme.dim(` (1-${modelCtxWindow.toLocaleString()})`));
1768
+ tui.requestRender();
1769
+ updateTokenEstimate();
1770
+ return;
1771
+ }
1772
+ const tokens = parseInt(restArgs.trim(), 10);
1773
+ const activeP = loop.getActiveProvider();
1774
+ const modelCtxWindow = getModelContextWindow(activeP.getProviderType(), activeP.getModel());
1775
+ const upper = modelCtxWindow;
1776
+ if (isNaN(tokens) || tokens < 1 || tokens > upper) {
1777
+ chatLog.addSystem(theme.warning(`Usage: /model settings context <1-${upper.toLocaleString()}>`) + theme.dim(` (model: ${activeP.getModel()})`));
1778
+ tui.requestRender();
1779
+ updateTokenEstimate();
1780
+ return;
1781
+ }
1782
+ cfg.set('session.maxContext', tokens);
1783
+ cfg.save().catch(() => { });
1784
+ chatLog.addSystem(theme.success('Max context set to ') + theme.fg(tokens.toLocaleString() + ' tokens'));
1785
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
1786
+ updateTokenEstimate();
1787
+ return;
1788
+ }
1789
+ // ── 本地模型 L2: model/local/start ──
1790
+ case 'model/local/start': {
1791
+ const { detectLocalBackend } = await import('../provider/local-config.js');
1792
+ const detected = await detectLocalBackend();
1793
+ const ollamaBin = localModel.checkOllama();
1794
+ const llamacppBin = localModel.checkLlamacpp();
1795
+ const backend = restArgs?.toLowerCase();
1796
+ const validBackend = backend === 'ollama' || backend === 'llamacpp' || backend === 'llama.cpp';
1797
+ // 指定了 backend → 启动那个
1798
+ if (validBackend) {
1799
+ const target = (backend === 'llamacpp' || backend === 'llama.cpp') ? 'llamacpp' : 'ollama';
1800
+ if (target === 'ollama' && ollamaBin) {
1801
+ if (detected?.backend === 'ollama') {
1802
+ chatLog.addSystem(theme.success('Ollama 已在运行。使用 /model/local/switch 切换。'));
1803
+ }
1804
+ else {
1805
+ chatLog.addSystem(theme.accent('启动 Ollama...'));
1806
+ const info = await supervisor.startOllamaOnDemand(process.cwd());
1807
+ chatLog.addSystem(info ? theme.success('Ollama 已启动(框架管理进程)。') : theme.warning('启动失败,请手动运行 ollama serve。'));
1808
+ }
1809
+ }
1810
+ else if (target === 'llamacpp' && llamacppBin) {
1811
+ const regModels = localModel.list();
1812
+ if (regModels.length === 0) {
1813
+ chatLog.addSystem(theme.warning('无注册的 llama.cpp 模型。请先用 /model/local/register 注册。'));
1814
+ }
1815
+ else {
1816
+ localModel.start(regModels[0].name).then(async (info) => {
1817
+ if (info) {
1818
+ cfg.set('provider.local', { type: 'local', model: info.modelFile ?? regModels[0].name, baseUrl: info.baseUrl });
1819
+ cfg.set('provider.local.modelKey', regModels[0].name);
1820
+ cfg.save().catch(() => { });
1821
+ try {
1822
+ await loop.switchProvider('local');
1823
+ cfg.set('provider.active', 'local');
1824
+ cfg.save().catch(() => { });
1825
+ chatLog.addSystem(theme.success(`llama.cpp ${regModels[0].name} started`));
1826
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
1827
+ }
1828
+ catch (swErr) {
1829
+ chatLog.addSystem(theme.error(`Switch failed: ${swErr.message}`));
1830
+ }
1831
+ }
1832
+ tui.requestRender();
1833
+ }).catch((e) => { chatLog.addSystem(theme.error(`Failed: ${e.message}`)); tui.requestRender(); });
1834
+ }
1835
+ }
1836
+ else {
1837
+ chatLog.addSystem(theme.warning(`${backend} 未安装。`));
1838
+ }
1839
+ tui.requestRender();
1840
+ return;
1841
+ }
1842
+ // 指定了已注册模型名 → 按名启动
1843
+ if (restArgs) {
1844
+ const lm = localModel.list().find((m) => m.name === restArgs);
1845
+ if (lm) {
1846
+ localModel.start(restArgs).then(async (info) => {
1847
+ if (info) {
1848
+ cfg.set('provider.local', { type: 'local', model: info.modelFile ?? restArgs, baseUrl: info.baseUrl });
1849
+ cfg.set('provider.local.modelKey', restArgs);
1850
+ cfg.save().catch(() => { });
1851
+ try {
1852
+ await loop.switchProvider('local');
1853
+ cfg.set('provider.active', 'local');
1854
+ cfg.save().catch(() => { });
1855
+ chatLog.addSystem(theme.success(`${restArgs} started on port ${info.port}`));
1856
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
1857
+ }
1858
+ catch (swErr) {
1859
+ chatLog.addSystem(theme.error(`Switch failed: ${swErr.message}`));
1860
+ }
1861
+ }
1862
+ else {
1863
+ chatLog.addSystem(theme.error(`Failed to start ${restArgs}`));
1864
+ }
1865
+ tui.requestRender();
1866
+ }).catch((e) => { chatLog.addSystem(theme.error(`Failed: ${e.message}`)); tui.requestRender(); });
1867
+ }
1868
+ else {
1869
+ chatLog.addSystem(theme.warning(`Model "${restArgs}" not registered. Available: ${localModel.list().map((m) => m.name).join(', ') || 'none'}`));
1870
+ chatLog.addSystem(theme.dim('To start a backend: /model/local start ollama | /model/local start llamacpp'));
1871
+ tui.requestRender();
1872
+ }
1873
+ return;
1874
+ }
1875
+ // 无参 → 列出可用选项
1876
+ chatLog.addSystem(theme.fg('── 可用本地后端 ──'));
1877
+ if (detected)
1878
+ chatLog.addSystem(theme.success(`${detected.backend} 正在运行 — ${detected.baseUrl}`));
1879
+ if (ollamaBin)
1880
+ chatLog.addSystem(theme.dim(`Ollama ${detected?.backend === 'ollama' ? '(运行中)' : '— /model/local start ollama'}`));
1881
+ if (llamacppBin)
1882
+ chatLog.addSystem(theme.dim(`llama.cpp ${detected?.backend === 'llamacpp' ? '(运行中)' : '— /model/local start llamacpp'}`));
1883
+ if (!ollamaBin && !llamacppBin)
1884
+ chatLog.addSystem(theme.warning('本地模型服务未配置。请安装 Ollama 或 llama.cpp。'));
1885
+ const reg = localModel.list();
1886
+ if (reg.length > 0)
1887
+ chatLog.addSystem(theme.dim(`已注册模型: ${reg.map((m) => m.name).join(', ')}`));
1888
+ tui.requestRender();
1889
+ return;
1890
+ }
1891
+ // ── 本地模型 L2: model/local/stop ──
1892
+ case 'model/local/stop': {
1893
+ const { detectLocalBackend } = await import('../provider/local-config.js');
1894
+ const detected = await detectLocalBackend();
1895
+ const backend = restArgs?.toLowerCase();
1896
+ if (backend === 'ollama') {
1897
+ if (detected?.backend === 'ollama') {
1898
+ try {
1899
+ await supervisor.stopModel('ollama');
1900
+ chatLog.addSystem(theme.success('Ollama 已停止。'));
1901
+ }
1902
+ catch {
1903
+ chatLog.addSystem(theme.warning('无法停止 Ollama。请手动执行 ollama stop。'));
1904
+ }
1905
+ }
1906
+ else {
1907
+ chatLog.addSystem(theme.dim('Ollama 未在运行。'));
1908
+ }
1909
+ tui.requestRender();
1910
+ return;
1911
+ }
1912
+ if (backend === 'llamacpp' || backend === 'llama.cpp') {
1913
+ const running = localModel.getBridge().getAllStatus().filter((s) => s.state === 'running');
1914
+ if (running.length > 0) {
1915
+ for (const m of running) {
1916
+ localModel.getBridge().stop(m.name).catch(() => { });
1917
+ chatLog.addSystem(theme.success(`Stopped: ${m.name}`));
1918
+ }
1919
+ }
1920
+ else {
1921
+ chatLog.addSystem(theme.dim('llama.cpp 未在运行。'));
1922
+ }
1923
+ tui.requestRender();
1924
+ return;
1925
+ }
1926
+ // 无参 → 停止所有
1927
+ let stopped = 0;
1928
+ if (detected?.backend === 'ollama') {
1929
+ try {
1930
+ await supervisor.stopModel('ollama');
1931
+ stopped++;
1932
+ chatLog.addSystem(theme.success('Ollama 已停止。'));
1933
+ }
1934
+ catch {
1935
+ chatLog.addSystem(theme.warning('无法停止 Ollama。'));
1936
+ }
1937
+ }
1938
+ const running = localModel.getBridge().getAllStatus().filter((s) => s.state === 'running');
1939
+ for (const m of running) {
1940
+ localModel.getBridge().stop(m.name).catch(() => { });
1941
+ stopped++;
1942
+ chatLog.addSystem(theme.success(`Stopped: ${m.name}`));
1943
+ }
1944
+ if (stopped === 0)
1945
+ chatLog.addSystem(theme.dim('没有运行中的本地服务。'));
1946
+ tui.requestRender();
1947
+ return;
1948
+ }
1949
+ // ── 本地模型 L2: model/local/status ──
1950
+ case 'model/local/status': {
1951
+ const { detectLocalBackend } = await import('../provider/local-config.js');
1952
+ const detected = await detectLocalBackend();
1953
+ const ollamaBin = localModel.checkOllama();
1954
+ const llamacppBin = localModel.checkLlamacpp();
1955
+ chatLog.addSystem(theme.fg('── 本地模型状态 ──'));
1956
+ chatLog.addSystem(detected ? theme.success(`运行中: ${detected.backend} — ${detected.baseUrl}`) : theme.dim('运行中: 无'));
1957
+ chatLog.addSystem(theme.dim(`Ollama: ${ollamaBin ? '已安装' : '未安装'} | llama.cpp: ${llamacppBin ? '已安装' : '未安装'}`));
1958
+ const reg = localModel.list();
1959
+ if (reg.length > 0)
1960
+ chatLog.addSystem(theme.dim(`已注册模型: ${reg.map((m) => m.name).join(', ')}`));
1961
+ tui.requestRender();
1962
+ return;
1963
+ }
1964
+ // ── 本地模型 L2: model/local/switch ──
1965
+ case 'model/local/switch': {
1966
+ try {
1967
+ await loop.switchProvider('local');
1968
+ // 显式持久化 provider 选择(switchProvider 不再负责持久化)
1969
+ cfg.set('provider.active', 'local');
1970
+ cfg.save().catch(() => { });
1971
+ modelName = loop.getActiveProvider().getModel();
1972
+ providerTypeStart = loop.getActiveProvider().getProviderType();
1973
+ chatLog.addSystem(theme.success(`Switched to local (${loop.getActiveProvider().getProviderType()})`));
1974
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
1975
+ }
1976
+ catch (err) {
1977
+ chatLog.addSystem(theme.error(`Switch failed: ${err.message}`));
1978
+ }
1979
+ tui.requestRender();
1980
+ return;
1981
+ }
1982
+ // ── 本地模型 L2: model/local/register ──
1983
+ case 'model/local/register': {
1984
+ const found = await localModel.scanUnregistered();
1985
+ if (found.length === 0) {
1986
+ chatLog.addSystem(theme.dim('无新模型。已检查 models/ 目录 (GGUF) 和 ollama list。'));
1987
+ }
1988
+ else {
1989
+ for (const f of found) {
1990
+ localModel.registerModel({ name: f.name, modelFile: f.modelFile, backend: f.backend });
1991
+ chatLog.addSystem(theme.success(`Registered: ${f.name} (${f.backend ?? 'llama.cpp'})`));
1992
+ }
1993
+ }
1994
+ tui.requestRender();
1995
+ return;
1996
+ }
1997
+ // ── 本地模型 L2: model/local/unregister ──
1998
+ case 'model/local/unregister': {
1999
+ if (!restArgs) {
2000
+ chatLog.addSystem(theme.warning('Usage: /model/local unregister <name>'));
2001
+ tui.requestRender();
2002
+ return;
2003
+ }
2004
+ chatLog.addSystem(theme.success(localModel.unregister(restArgs)));
2005
+ tui.requestRender();
2006
+ return;
2007
+ }
2008
+ // ── 本地模型 L2: model/local/detect ──
2009
+ case 'model/local/detect': {
2010
+ const { detectLocalBackend } = await import('../provider/local-config.js');
2011
+ const detected = await detectLocalBackend();
2012
+ const ollamaBin = localModel.checkOllama();
2013
+ const llamacppBin = localModel.checkLlamacpp();
2014
+ chatLog.addSystem(theme.fg('── 本地模型检测 ──'));
2015
+ chatLog.addSystem(detected ? theme.success(`运行中: ${detected.backend} — ${detected.baseUrl}`) : theme.dim('运行中: 无'));
2016
+ chatLog.addSystem(theme.dim(`Ollama: ${ollamaBin ? '已安装 (' + ollamaBin + ')' : '未安装'}`));
2017
+ chatLog.addSystem(theme.dim(`llama.cpp: ${llamacppBin ? '已安装 (' + llamacppBin + ')' : '未安装'}`));
2018
+ const reg = localModel.list();
2019
+ if (reg.length > 0)
2020
+ chatLog.addSystem(theme.dim(`已注册模型: ${reg.map((m) => m.name).join(', ')}`));
2021
+ tui.requestRender();
2022
+ return;
2023
+ }
2024
+ default: {
2025
+ // 在线模型: model/online/<provider>/<modelName|config>
2026
+ if (cmdPath.startsWith('model/online/')) {
2027
+ const onlineParts = cmdPath.split('/');
2028
+ if (onlineParts.length >= 4) {
2029
+ const provider = onlineParts[2];
2030
+ const sub = onlineParts[3];
2031
+ if (sub === 'config') {
2032
+ chatLog.addSystem(theme.accent(`Configure ${provider}: Use /context <tokens> to adjust context window`));
2033
+ tui.requestRender();
2034
+ return;
2035
+ }
2036
+ localModel.getBridge().stopAll().catch(() => { });
2037
+ try {
2038
+ cfg.set(`provider.${provider}.model`, sub);
2039
+ cfg.save().catch(() => { });
2040
+ await loop.switchProvider(provider);
2041
+ // 显式持久化 provider 选择(switchProvider 不再负责持久化)
2042
+ cfg.set('provider.active', provider);
2043
+ cfg.save().catch(() => { });
2044
+ modelName = loop.getActiveProvider().getModel();
2045
+ providerTypeStart = loop.getActiveProvider().getProviderType();
2046
+ chatLog.addSystem(theme.success(`Switched to ${provider}/${sub}`));
2047
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
2048
+ }
2049
+ catch (err) {
2050
+ chatLog.addSystem(theme.error(`Switch failed: ${err.message}`));
2051
+ }
2052
+ tui.requestRender();
2053
+ return;
2054
+ }
2055
+ }
2056
+ // 本地模型 L1 直接切换: model/local_<modelName>
2057
+ if (cmdPath.startsWith('model/local_')) {
2058
+ const lmName = cmdPath.slice('model/local_'.length);
2059
+ const lm = localModel.list().find((m) => m.name === lmName);
2060
+ if (lm) {
2061
+ localModel.switch(lmName).then(async (info) => {
2062
+ if (info) {
2063
+ chatLog.addSystem(theme.success(`Local model ${lmName} started on port ${info.port}`));
2064
+ cfg.set('provider.local', {
2065
+ type: 'local',
2066
+ model: info.modelFile ?? lmName,
2067
+ baseUrl: info.baseUrl,
2068
+ });
2069
+ cfg.set('provider.local.modelKey', lmName);
2070
+ cfg.save().catch(() => { });
2071
+ try {
2072
+ await loop.switchProvider('local');
2073
+ // 显式持久化 provider 选择(switchProvider 不再负责持久化)
2074
+ cfg.set('provider.active', 'local');
2075
+ cfg.save().catch(() => { });
2076
+ modelName = loop.getActiveProvider().getModel();
2077
+ providerTypeStart = loop.getActiveProvider().getProviderType();
2078
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
2079
+ }
2080
+ catch (swErr) {
2081
+ chatLog.addSystem(theme.warning(`Switch to local: ${swErr.message}`));
2082
+ }
2083
+ }
2084
+ else {
2085
+ chatLog.addSystem(theme.error(`Failed to start ${lmName}`));
2086
+ }
2087
+ tui.requestRender();
2088
+ }).catch((e) => {
2089
+ chatLog.addSystem(theme.error(`Failed: ${e.message}`));
2090
+ tui.requestRender();
2091
+ });
2092
+ tui.requestRender();
2093
+ return;
2094
+ }
2095
+ }
2096
+ // 在线模型 L1 直接切换: model/current_online
2097
+ if (cmdPath === 'model/current_online') {
2098
+ chatLog.addSystem(theme.dim('Already using current online model'));
2099
+ tui.requestRender();
2100
+ return;
2101
+ }
2102
+ // ── 压缩器控制: compress/* ──
2103
+ if (cmdPath === 'compress/threshold' || cmdPath.startsWith('compress/threshold ')) {
2104
+ const val = parseFloat((restArgs || '').trim());
2105
+ if (isNaN(val) || val < 0 || val > 1) {
2106
+ chatLog.addSystem(theme.warning('Usage: /compress threshold <0.0-1.0>'));
2107
+ tui.requestRender();
2108
+ return;
2109
+ }
2110
+ cfg.set('context.compressThreshold', val);
2111
+ cfg.save().catch(() => { });
2112
+ chatLog.addSystem(theme.success('Compress threshold: ') + theme.fg(String(val)));
2113
+ tui.requestRender();
2114
+ updateTokenEstimate();
2115
+ return;
2116
+ }
2117
+ if (cmdPath === 'compress/emergency' || cmdPath.startsWith('compress/emergency ')) {
2118
+ const val = parseFloat((restArgs || '').trim());
2119
+ if (isNaN(val) || val < 0 || val > 1) {
2120
+ chatLog.addSystem(theme.warning('Usage: /compress emergency <0.0-1.0>'));
2121
+ tui.requestRender();
2122
+ return;
2123
+ }
2124
+ cfg.set('context.emergencyThreshold', val);
2125
+ cfg.save().catch(() => { });
2126
+ chatLog.addSystem(theme.success('Emergency threshold: ') + theme.fg(String(val)));
2127
+ tui.requestRender();
2128
+ updateTokenEstimate();
2129
+ return;
2130
+ }
2131
+ if (cmdPath === 'compress/depth' || cmdPath.startsWith('compress/depth ')) {
2132
+ const val = parseFloat((restArgs || '').trim());
2133
+ if (isNaN(val) || val < 0 || val > 1) {
2134
+ chatLog.addSystem(theme.warning('Usage: /compress depth <0.0-1.0>'));
2135
+ tui.requestRender();
2136
+ return;
2137
+ }
2138
+ cfg.set('context.compressDepth', val);
2139
+ cfg.save().catch(() => { });
2140
+ chatLog.addSystem(theme.success('Compress depth: ') + theme.fg(String(val)));
2141
+ tui.requestRender();
2142
+ updateTokenEstimate();
2143
+ return;
2144
+ }
2145
+ // ── 通道管理: channel/* ──
2146
+ if (cmdPath.startsWith('channel/')) {
2147
+ const modelRouter = loop.modelRouter;
2148
+ if (!modelRouter) {
2149
+ chatLog.addSystem(theme.warning('ModelRouter not available'));
2150
+ tui.requestRender();
2151
+ return;
2152
+ }
2153
+ const registry = modelRouter.getRegistry();
2154
+ if (cmdPath === 'channel/list') {
2155
+ const channels = registry.listChannels();
2156
+ const roles = registry.listRoles();
2157
+ if (channels.length === 0) {
2158
+ chatLog.addSystem(theme.dim('No model channels configured. All roles use main provider.'));
2159
+ }
2160
+ else {
2161
+ const lines = [theme.accent('=== Model Channels ===')];
2162
+ for (const ch of channels) {
2163
+ const chRoles = Object.entries(roles)
2164
+ .filter(([, cn]) => cn === ch.name)
2165
+ .map(([r]) => r);
2166
+ const roleStr = chRoles.length > 0 ? theme.dim(' → ') + theme.fg(chRoles.join(', ')) : '';
2167
+ lines.push(theme.fg(` ${ch.name}`) + theme.dim(`: ${ch.provider}/${ch.model || 'default'}`) + roleStr);
2168
+ }
2169
+ lines.push('');
2170
+ lines.push(theme.accent('=== Role Mappings ==='));
2171
+ for (const [role, channel] of Object.entries(roles)) {
2172
+ lines.push(theme.dim(` ${role}`) + ' → ' + theme.fg(String(channel)));
2173
+ }
2174
+ chatLog.addSystem(lines.join('\n'));
2175
+ }
2176
+ tui.requestRender();
2177
+ return;
2178
+ }
2179
+ if (cmdPath === 'channel/add') {
2180
+ if (!restArgs) {
2181
+ chatLog.addSystem(theme.warning('Usage: /channel add <name> [provider] [model]'));
2182
+ tui.requestRender();
2183
+ return;
2184
+ }
2185
+ const parts = restArgs.split(/\s+/).filter(Boolean);
2186
+ const [name, provider, model] = parts;
2187
+ try {
2188
+ registry.upsertChannel(name, { provider, model });
2189
+ const info = registry.getChannelInfo(name);
2190
+ chatLog.addSystem(theme.success(`Channel "${name}" added (${info?.provider}${model ? '/' + model : ''})`));
2191
+ }
2192
+ catch (e) {
2193
+ chatLog.addSystem(theme.error(`Failed: ${e.message}`));
2194
+ }
2195
+ tui.requestRender();
2196
+ return;
2197
+ }
2198
+ if (cmdPath === 'channel/remove') {
2199
+ if (!restArgs) {
2200
+ chatLog.addSystem(theme.warning('Usage: /channel remove <name>'));
2201
+ tui.requestRender();
2202
+ return;
2203
+ }
2204
+ const name = restArgs.trim();
2205
+ try {
2206
+ registry.removeChannel(name);
2207
+ chatLog.addSystem(theme.success(`Channel "${name}" removed`));
2208
+ }
2209
+ catch (e) {
2210
+ chatLog.addSystem(theme.error(`Failed: ${e.message}`));
2211
+ }
2212
+ tui.requestRender();
2213
+ return;
2214
+ }
2215
+ if (cmdPath === 'channel/role') {
2216
+ if (!restArgs) {
2217
+ chatLog.addSystem(theme.warning('Usage: /channel role <role> <channel>'));
2218
+ tui.requestRender();
2219
+ return;
2220
+ }
2221
+ const parts = restArgs.split(/\s+/).filter(Boolean);
2222
+ if (parts.length < 2) {
2223
+ chatLog.addSystem(theme.warning('Usage: /channel role <role> <channel>'));
2224
+ tui.requestRender();
2225
+ return;
2226
+ }
2227
+ const [role, channel] = parts;
2228
+ try {
2229
+ registry.setRoleMapping(role, channel);
2230
+ chatLog.addSystem(theme.success(`Role "${role}" → channel "${channel}"`));
2231
+ }
2232
+ catch (e) {
2233
+ chatLog.addSystem(theme.error(`Failed: ${e.message}`));
2234
+ }
2235
+ tui.requestRender();
2236
+ return;
2237
+ }
2238
+ // ── 通道子命令: channel/<name>/info | /model | /reset ──
2239
+ const chMatch = cmdPath.match(/^channel\/([^/]+)\/(info|model|reset)$/);
2240
+ if (chMatch) {
2241
+ const chName = chMatch[1];
2242
+ const action = chMatch[2];
2243
+ if (action === 'info') {
2244
+ const info = registry.getChannelInfo(chName);
2245
+ if (!info) {
2246
+ chatLog.addSystem(theme.warning(`Channel "${chName}" not found`));
2247
+ }
2248
+ else {
2249
+ const lines = [theme.accent(`=== Channel: ${info.name}${info.isMain ? ' (main)' : ''} ===`)];
2250
+ lines.push(theme.fg(' Provider: ') + info.provider);
2251
+ lines.push(theme.fg(' Model: ') + info.model);
2252
+ lines.push(theme.fg(' Type: ') + info.providerType);
2253
+ if (info.description)
2254
+ lines.push(theme.dim(' Desc: ') + info.description);
2255
+ if (info.roles.length > 0)
2256
+ lines.push(theme.fg(' Roles: ') + info.roles.join(', '));
2257
+ chatLog.addSystem(lines.join('\n'));
2258
+ }
2259
+ tui.requestRender();
2260
+ return;
2261
+ }
2262
+ if (action === 'model') {
2263
+ const parts = (restArgs || '').split(/\s+/).filter(Boolean);
2264
+ if (parts.length < 1) {
2265
+ chatLog.addSystem(theme.warning(`Usage: /channel/${chName}/model <provider> [model-name]`));
2266
+ tui.requestRender();
2267
+ return;
2268
+ }
2269
+ const provider = parts[0];
2270
+ const model = parts[1] || undefined;
2271
+ try {
2272
+ registry.setChannelModel(chName, provider, model);
2273
+ const updated = registry.getChannelInfo(chName);
2274
+ chatLog.addSystem(theme.success(`Channel "${chName}" model set → ${updated?.provider}/${updated?.model} (runtime only, not persisted)`));
2275
+ }
2276
+ catch (e) {
2277
+ chatLog.addSystem(theme.error(`Failed: ${e.message}`));
2278
+ }
2279
+ tui.requestRender();
2280
+ return;
2281
+ }
2282
+ if (action === 'reset') {
2283
+ try {
2284
+ registry.resetChannelModel(chName);
2285
+ const info = registry.getChannelInfo(chName);
2286
+ chatLog.addSystem(theme.success(`Channel "${chName}" reset → ${info?.provider}/${info?.model}`));
2287
+ }
2288
+ catch (e) {
2289
+ chatLog.addSystem(theme.error(`Failed: ${e.message}`));
2290
+ }
2291
+ tui.requestRender();
2292
+ return;
2293
+ }
2294
+ }
2295
+ }
2296
+ // 通用渠道 TUI 命令分发:cmdPath 如 "clawbot/login" → clawbot 渠道
2297
+ {
2298
+ const slashIdx = cmdPath.indexOf('/');
2299
+ if (slashIdx > 0) {
2300
+ const chId = cmdPath.slice(0, slashIdx);
2301
+ const chState = channelManager.get(chId);
2302
+ if (chState?.handler.handleTuiCommand) {
2303
+ const result = await chState.handler.handleTuiCommand(cmdPath, restArgs);
2304
+ if (result !== null) {
2305
+ chatLog.addSystem(result);
2306
+ tui.requestRender();
2307
+ return;
2308
+ }
2309
+ }
2310
+ }
2311
+ }
2312
+ // 通用 handler 路由:查找命令定义的 handler 字段
2313
+ const cmdDef = CommandRegistry.getInstance().find(cmdPath);
2314
+ if (cmdDef?.handler) {
2315
+ resolveHandler(cmdDef.handler, restArgs, localModel, chatLog, theme, tui);
2316
+ return;
2317
+ }
2318
+ chatLog.addSystem(theme.warning(`Unknown sub-command: /${cmdPath}`));
2319
+ tui.requestRender();
2320
+ }
2321
+ }
2322
+ }
2323
+ // Local slash commands
2324
+ if (input === '/help') {
2325
+ const grouped = getCommandsByCategory();
2326
+ const BOX_H_L = '\u2500';
2327
+ chatLog.addSystem(theme.fg('\u256d' + BOX_H_L.repeat(56) + '\u256e'));
2328
+ chatLog.addSystem(theme.fg('\u2502 Available Commands'));
2329
+ chatLog.addSystem(theme.fg('\u2570' + BOX_H_L.repeat(56) + '\u256f'));
2330
+ chatLog.addSystem('');
2331
+ for (const [category, cmds] of grouped) {
2332
+ const catLabel = getCategoryLabel(category);
2333
+ chatLog.addSystem(theme.accent('\u2500\u2500 ' + catLabel + ' \u2500\u2500'));
2334
+ for (const c of cmds) {
2335
+ if (c.deprecated) {
2336
+ chatLog.addSystem(' ' + theme.dim(c.icon) + ' ' + theme.dim('/' + c.name) +
2337
+ theme.dim(' ' + c.description) + theme.warning(' [deprecated]'));
2338
+ }
2339
+ else {
2340
+ const args = c.args ? theme.dim(' ' + c.args) : '';
2341
+ const hasChildren = c.children ? theme.accent(' \u25B8') : '';
2342
+ chatLog.addSystem(' ' + theme.accent(c.icon) + ' ' + theme.fg('/' + c.name) +
2343
+ hasChildren + args + theme.dim(' ' + c.description));
2344
+ }
2345
+ }
2346
+ chatLog.addSystem('');
2347
+ }
2348
+ tui.requestRender();
2349
+ updateTokenEstimate();
2350
+ return;
2351
+ }
2352
+ if (input === '/clear') {
2353
+ chatLog.clearAll();
2354
+ tui.requestRender();
2355
+ updateTokenEstimate();
2356
+ return;
2357
+ }
2358
+ if (input === '/collapse') {
2359
+ const expanded = chatLog.toggleToolsExpanded();
2360
+ chatLog.addSystem(theme.dim('Tool outputs ') + theme.fg(expanded ? 'expanded' : 'collapsed'));
2361
+ tui.requestRender();
2362
+ return;
2363
+ }
2364
+ if (input === '/orchestrator on') {
2365
+ if (loop.bypassManager) {
2366
+ await loop.bypassManager.activateAgent('orchestrator');
2367
+ const { RuntimeConfigCenter } = await import('../runtime/config-center.js');
2368
+ RuntimeConfigCenter.getInstance().set('bypass.orchestratorEnabled', true);
2369
+ chatLog.addSystem(theme.success('上下文编排旁路Agent 已开启'));
2370
+ }
2371
+ else {
2372
+ chatLog.addSystem(theme.warning('旁路Agent 管理器未初始化'));
2373
+ }
2374
+ tui.requestRender();
2375
+ return;
2376
+ }
2377
+ if (input === '/orchestrator off') {
2378
+ if (loop.bypassManager) {
2379
+ await loop.bypassManager.deactivateAgent('orchestrator');
2380
+ const { RuntimeConfigCenter } = await import('../runtime/config-center.js');
2381
+ RuntimeConfigCenter.getInstance().set('bypass.orchestratorEnabled', false);
2382
+ chatLog.addSystem(theme.dim('上下文编排旁路Agent 已关闭'));
2383
+ }
2384
+ else {
2385
+ chatLog.addSystem(theme.warning('旁路Agent 管理器未初始化'));
2386
+ }
2387
+ tui.requestRender();
2388
+ return;
2389
+ }
2390
+ if (input === '/default-mode companion') {
2391
+ const { RuntimeConfigCenter } = await import('../runtime/config-center.js');
2392
+ const configCenter = RuntimeConfigCenter.getInstance();
2393
+ configCenter.set('startup.defaultMode', 'companion');
2394
+ await configCenter.save();
2395
+ chatLog.addSystem(theme.success('启动默认模式已设为 陪伴模式 💫(下次启动生效)'));
2396
+ tui.requestRender();
2397
+ return;
2398
+ }
2399
+ if (input === '/default-mode normal') {
2400
+ const { RuntimeConfigCenter } = await import('../runtime/config-center.js');
2401
+ const configCenter = RuntimeConfigCenter.getInstance();
2402
+ configCenter.set('startup.defaultMode', 'normal');
2403
+ await configCenter.save();
2404
+ chatLog.addSystem(theme.dim('启动默认模式已设为 普通模式 💻(下次启动生效)'));
2405
+ tui.requestRender();
2406
+ return;
2407
+ }
2408
+ if (input === '/zone4 on') {
2409
+ knowledgeBase.setZone4Enabled(true);
2410
+ contextComposer.activeConditions.add('zone4_enabled');
2411
+ const { RuntimeConfigCenter: RCC } = await import('../runtime/config-center.js');
2412
+ RCC.getInstance().set('kb.zone4', true);
2413
+ chatLog.addSystem(theme.success('Zone 4 已开启'));
2414
+ tui.requestRender();
2415
+ return;
2416
+ }
2417
+ if (input === '/zone4 off') {
2418
+ knowledgeBase.setZone4Enabled(false);
2419
+ contextComposer.activeConditions.delete('zone4_enabled');
2420
+ const { RuntimeConfigCenter: RCC1 } = await import('../runtime/config-center.js');
2421
+ RCC1.getInstance().set('kb.zone4', false);
2422
+ chatLog.addSystem(theme.dim('Zone 4 已关闭(知识库同步停用)'));
2423
+ tui.requestRender();
2424
+ return;
2425
+ }
2426
+ if (input === '/kb on') {
2427
+ if (!knowledgeBase.zone4Enabled) {
2428
+ knowledgeBase.setZone4Enabled(true);
2429
+ contextComposer.activeConditions.add('zone4_enabled');
2430
+ const { RuntimeConfigCenter: RCC2 } = await import('../runtime/config-center.js');
2431
+ RCC2.getInstance().set('kb.zone4', true);
2432
+ chatLog.addSystem(theme.dim('Zone 4 已同步开启'));
2433
+ }
2434
+ knowledgeBase.enable();
2435
+ const { RuntimeConfigCenter: RCC3 } = await import('../runtime/config-center.js');
2436
+ RCC3.getInstance().set('kb.enabled', true);
2437
+ chatLog.addSystem(theme.success('知识库已开启 — Zone 4 将注入检索结果'));
2438
+ tui.requestRender();
2439
+ return;
2440
+ }
2441
+ if (input === '/kb off') {
2442
+ knowledgeBase.disable();
2443
+ const { RuntimeConfigCenter: RCC4 } = await import('../runtime/config-center.js');
2444
+ RCC4.getInstance().set('kb.enabled', false);
2445
+ chatLog.addSystem(theme.dim('知识库已关闭'));
2446
+ tui.requestRender();
2447
+ return;
2448
+ }
2449
+ if (input === '/precise on') {
2450
+ const { PreciseStrategy } = await import('../context/precision/index.js');
2451
+ if (!originalSessionDir)
2452
+ originalSessionDir = sessionDir;
2453
+ // 复用已有 precise session(保留积累的关键词和摘要)
2454
+ const existingPrecise = (await sessionManager.list())
2455
+ .find((s) => s.type === 'precise');
2456
+ const newSession = existingPrecise ?? await sessionManager.create('precise');
2457
+ const newSessionDir = sessionManager.getSessionDir(newSession.id);
2458
+ const precise = new PreciseStrategy(newSessionDir);
2459
+ loop.composeStrategy = precise;
2460
+ await loop.switchSession(newSessionDir);
2461
+ sessionDir = newSessionDir;
2462
+ preciseModeActive = true;
2463
+ const label = existingPrecise ? '恢复已有' : '新建';
2464
+ chatLog.addSystem(theme.success(`精确模式已开启 — ${label} session: ${newSession.id}`));
2465
+ chatLog.clearAll();
2466
+ tui.requestRender();
2467
+ return;
2468
+ }
2469
+ if (input === '/precise off') {
2470
+ if (!preciseModeActive) {
2471
+ chatLog.addSystem(theme.dim('精确模式未开启'));
2472
+ tui.requestRender();
2473
+ return;
2474
+ }
2475
+ const { DefaultStrategy } = await import('../context/precision/index.js');
2476
+ loop.composeStrategy = new DefaultStrategy();
2477
+ if (originalSessionDir) {
2478
+ await loop.switchSession(originalSessionDir);
2479
+ sessionDir = originalSessionDir;
2480
+ originalSessionDir = null;
2481
+ chatLog.clearAll();
2482
+ replayEvents(chatLog, sessionDir);
2483
+ }
2484
+ preciseModeActive = false;
2485
+ chatLog.addSystem(theme.dim('精确模式已关闭,已恢复原始 session'));
2486
+ tui.requestRender();
2487
+ return;
2488
+ }
2489
+ if (input === '/exit') {
2490
+ chatLog.addSystem(theme.dim('Goodbye.'));
2491
+ tui.requestRender();
2492
+ await channelManager.stopAll();
2493
+ await loop.shutdown();
2494
+ await supervisor.shutdownAll();
2495
+ tui.stop();
2496
+ process.exit(0);
2497
+ }
2498
+ const cfg = RuntimeConfigCenter.getInstance();
2499
+ if (input === '/status') {
2500
+ const s = cfg.getAll();
2501
+ const items = [
2502
+ ['Provider', s.provider.active],
2503
+ ['Model', s.provider[s.provider.active]],
2504
+ ['Active Workflow', 'none'],
2505
+ ['Max Context', `${s.session.maxContext.toLocaleString()} tokens`],
2506
+ ['Max Turns', s.session.maxTurns],
2507
+ ['Compress Threshold', s.context.compressThreshold.toFixed(2)],
2508
+ ['Confirmation', s.safety.requireConfirmation ? 'on' : 'off'],
2509
+ ['Scavenge', s.repair.scavenge.enabled ? 'on' : 'off'],
2510
+ ['Storm', s.repair.storm.enabled ? 'on' : 'off'],
2511
+ ['Storm Window', s.repair.storm.windowSize],
2512
+ ['Storm Threshold', s.repair.storm.threshold],
2513
+ ['Log Level', s.logging.level],
2514
+ ];
2515
+ chatLog.addSystem(theme.accent('\u2500\u2500 Status \u2500\u2500'));
2516
+ for (const [label, value] of items) {
2517
+ let display;
2518
+ if (typeof value === 'object' && value !== null && 'model' in value) {
2519
+ display = String(value.model);
2520
+ }
2521
+ else {
2522
+ display = String(value ?? 'N/A');
2523
+ }
2524
+ chatLog.addSystem(' ' + theme.fg(label) + theme.dim(': ') + theme.fg(display));
2525
+ }
2526
+ const sources = loop.getModelSources();
2527
+ if (sources) {
2528
+ chatLog.addSystem('');
2529
+ chatLog.addSystem(theme.accent('\u2500\u2500 Model Routing \u2500\u2500'));
2530
+ const labels = {
2531
+ assessment: '评估',
2532
+ planning: '规划',
2533
+ compression: '压缩',
2534
+ };
2535
+ const sourceColor = (src) => src === 'local' ? theme.success(src) : theme.accent(src);
2536
+ for (const [role, source] of Object.entries(sources)) {
2537
+ const label = labels[role] ?? role;
2538
+ chatLog.addSystem(' ' + theme.fg(label) + theme.dim(': ') + sourceColor(String(source)));
2539
+ }
2540
+ }
2541
+ chatLog.addSystem('');
2542
+ tui.requestRender();
2543
+ updateTokenEstimate();
2544
+ return;
2545
+ }
2546
+ // ── /context <tokens> ──
2547
+ if (input.startsWith('/context ')) {
2548
+ const tokens = parseInt(input.slice(9).trim(), 10);
2549
+ const activeP = loop.getActiveProvider();
2550
+ const modelCtxWindow = getModelContextWindow(activeP.getProviderType(), activeP.getModel());
2551
+ const upper = modelCtxWindow; // 当前模型支持的最大上下文
2552
+ if (isNaN(tokens) || tokens < 1 || tokens > upper) {
2553
+ chatLog.addSystem(theme.warning(`Usage: /context <1-${upper.toLocaleString()}>`) + theme.dim(` (model: ${activeP.getModel()})`));
2554
+ tui.requestRender();
2555
+ updateTokenEstimate();
2556
+ return;
2557
+ }
2558
+ cfg.set('session.maxContext', tokens);
2559
+ cfg.save().catch(() => { });
2560
+ chatLog.addSystem(theme.success('Max context set to ') + theme.fg(tokens.toLocaleString() + ' tokens'));
2561
+ refreshStatus(loop.getTurnInfo(lastTurnCount, lastTokensUsed));
2562
+ updateTokenEstimate();
2563
+ return;
2564
+ }
2565
+ // ── /turns <n> ──
2566
+ if (input.startsWith('/turns ')) {
2567
+ const n = parseInt(input.slice(7).trim(), 10);
2568
+ if (isNaN(n) || n < 1 || n > 100) {
2569
+ chatLog.addSystem(theme.warning('Usage: /turns <1-100>'));
2570
+ tui.requestRender();
2571
+ updateTokenEstimate();
2572
+ return;
2573
+ }
2574
+ cfg.set('session.maxTurns', n);
2575
+ cfg.save().catch(() => { });
2576
+ chatLog.addSystem(theme.success('Max turns set to ') + theme.fg(String(n)));
2577
+ tui.requestRender();
2578
+ updateTokenEstimate();
2579
+ return;
2580
+ }
2581
+ // ── /threshold <0.0-1.0> ──
2582
+ // ── /compress sub-commands (handled via handleSlashSubCommand) ──
2583
+ // ── /threshold <0.0-1.0> (deprecated: use /compress threshold) ──
2584
+ if (input.startsWith('/threshold ')) {
2585
+ const val = parseFloat(input.slice(11).trim());
2586
+ if (isNaN(val) || val < 0 || val > 1) {
2587
+ chatLog.addSystem(theme.warning('Usage: /threshold <0.0-1.0>'));
2588
+ tui.requestRender();
2589
+ updateTokenEstimate();
2590
+ return;
2591
+ }
2592
+ cfg.set('context.compressThreshold', val);
2593
+ cfg.save().catch(() => { });
2594
+ chatLog.addSystem(theme.success('Compression threshold set to ') + theme.fg(val.toFixed(2)));
2595
+ tui.requestRender();
2596
+ updateTokenEstimate();
2597
+ return;
2598
+ }
2599
+ // ── /confirm on|off ──
2600
+ if (input.startsWith('/confirm ')) {
2601
+ const arg = input.slice(9).trim();
2602
+ if (arg === 'on') {
2603
+ cfg.set('safety.requireConfirmation', true);
2604
+ cfg.save().catch(() => { });
2605
+ chatLog.addSystem(theme.success('Tool confirmation ') + theme.fg('enabled'));
2606
+ }
2607
+ else if (arg === 'off') {
2608
+ cfg.set('safety.requireConfirmation', false);
2609
+ cfg.save().catch(() => { });
2610
+ chatLog.addSystem(theme.warning('Tool confirmation ') + theme.fg('disabled'));
2611
+ }
2612
+ else {
2613
+ chatLog.addSystem(theme.warning('Usage: /confirm <on|off>'));
2614
+ }
2615
+ tui.requestRender();
2616
+ updateTokenEstimate();
2617
+ return;
2618
+ }
2619
+ // ── /log <level> ──
2620
+ if (input.startsWith('/log ')) {
2621
+ const arg = input.slice(5).trim();
2622
+ const validLevels = ['debug', 'info', 'warn', 'error', 'off'];
2623
+ if (validLevels.includes(arg)) {
2624
+ cfg.set('logging.level', arg);
2625
+ cfg.save().catch(() => { });
2626
+ chatLog.addSystem(theme.success('Log level set to ') + theme.fg(arg));
2627
+ }
2628
+ else {
2629
+ chatLog.addSystem(theme.warning('Usage: /log <debug|info|warn|error|off>'));
2630
+ }
2631
+ tui.requestRender();
2632
+ updateTokenEstimate();
2633
+ return;
2634
+ }
2635
+ // ── /scavenge on|off ──
2636
+ if (input.startsWith('/scavenge ')) {
2637
+ const arg = input.slice(10).trim();
2638
+ if (arg === 'on') {
2639
+ cfg.set('repair.scavenge.enabled', true);
2640
+ cfg.save().catch(() => { });
2641
+ chatLog.addSystem(theme.success('Scavenge repair ') + theme.fg('enabled'));
2642
+ }
2643
+ else if (arg === 'off') {
2644
+ cfg.set('repair.scavenge.enabled', false);
2645
+ cfg.save().catch(() => { });
2646
+ chatLog.addSystem(theme.warning('Scavenge repair ') + theme.fg('disabled'));
2647
+ }
2648
+ else {
2649
+ chatLog.addSystem(theme.warning('Usage: /scavenge <on|off>'));
2650
+ }
2651
+ tui.requestRender();
2652
+ updateTokenEstimate();
2653
+ return;
2654
+ }
2655
+ // ── /storm on|off ──
2656
+ if (input.startsWith('/storm ')) {
2657
+ const arg = input.slice(7).trim();
2658
+ if (arg === 'on') {
2659
+ cfg.set('repair.storm.enabled', true);
2660
+ cfg.save().catch(() => { });
2661
+ chatLog.addSystem(theme.success('Storm protection ') + theme.fg('enabled'));
2662
+ }
2663
+ else if (arg === 'off') {
2664
+ cfg.set('repair.storm.enabled', false);
2665
+ cfg.save().catch(() => { });
2666
+ chatLog.addSystem(theme.warning('Storm protection ') + theme.fg('disabled'));
2667
+ }
2668
+ else {
2669
+ chatLog.addSystem(theme.warning('Usage: /storm <on|off>'));
2670
+ }
2671
+ tui.requestRender();
2672
+ updateTokenEstimate();
2673
+ return;
2674
+ }
2675
+ // ── /storm-win <2-20> ──
2676
+ if (input.startsWith('/storm-win ')) {
2677
+ const n = parseInt(input.slice(11).trim(), 10);
2678
+ if (isNaN(n) || n < 2 || n > 20) {
2679
+ chatLog.addSystem(theme.warning('Usage: /storm-win <2-20>'));
2680
+ tui.requestRender();
2681
+ updateTokenEstimate();
2682
+ return;
2683
+ }
2684
+ cfg.set('repair.storm.windowSize', n);
2685
+ cfg.save().catch(() => { });
2686
+ chatLog.addSystem(theme.success('Storm window size set to ') + theme.fg(String(n)));
2687
+ tui.requestRender();
2688
+ updateTokenEstimate();
2689
+ return;
2690
+ }
2691
+ // ── /storm-th <1-10> ──
2692
+ if (input.startsWith('/storm-th ')) {
2693
+ const n = parseInt(input.slice(10).trim(), 10);
2694
+ if (isNaN(n) || n < 1 || n > 10) {
2695
+ chatLog.addSystem(theme.warning('Usage: /storm-th <1-10>'));
2696
+ tui.requestRender();
2697
+ updateTokenEstimate();
2698
+ return;
2699
+ }
2700
+ cfg.set('repair.storm.threshold', n);
2701
+ cfg.save().catch(() => { });
2702
+ chatLog.addSystem(theme.success('Storm threshold set to ') + theme.fg(String(n)));
2703
+ tui.requestRender();
2704
+ updateTokenEstimate();
2705
+ return;
2706
+ }
2707
+ // ── /schedule ──
2708
+ if (input === '/schedule') {
2709
+ const scheduler = loop.getScheduler();
2710
+ if (!scheduler) {
2711
+ chatLog.addSystem(theme.dim('No scheduler configured'));
2712
+ }
2713
+ else {
2714
+ const allTasks = scheduler.getTasks();
2715
+ const activeTasks = allTasks.filter((t) => t.enabled && t.nextRunAt !== null);
2716
+ const disabledCount = allTasks.length - activeTasks.length;
2717
+ if (activeTasks.length === 0) {
2718
+ chatLog.addSystem(theme.dim('No active scheduled tasks'));
2719
+ if (disabledCount > 0) {
2720
+ chatLog.addSystem(theme.dim(`(+${disabledCount} disabled/expired tasks)`));
2721
+ }
2722
+ }
2723
+ else {
2724
+ chatLog.addSystem(theme.accent(`Scheduled Tasks (${activeTasks.length} active)`));
2725
+ for (const task of activeTasks) {
2726
+ const nextRun = task.nextRunAt
2727
+ ? new Date(task.nextRunAt).toLocaleString()
2728
+ : 'N/A';
2729
+ const typeLabel = theme.dim(`[${task.scheduleType}]`);
2730
+ const runInfo = theme.dim(` | runs: ${task.runCount}`);
2731
+ chatLog.addSystem(' ' +
2732
+ theme.fg(task.name) +
2733
+ ' ' +
2734
+ typeLabel +
2735
+ theme.dim(' | next: ') +
2736
+ theme.fg(nextRun) +
2737
+ runInfo);
2738
+ }
2739
+ if (disabledCount > 0) {
2740
+ chatLog.addSystem(theme.dim(`(+${disabledCount} disabled/expired tasks not shown)`));
2741
+ }
2742
+ }
2743
+ }
2744
+ tui.requestRender();
2745
+ updateTokenEstimate();
2746
+ return;
2747
+ }
2748
+ // ── /schedule-add <name> <time> ──
2749
+ if (input.startsWith('/schedule-add ')) {
2750
+ const args = input.slice(14).trim();
2751
+ const spaceIdx = args.lastIndexOf(' ');
2752
+ if (spaceIdx < 0) {
2753
+ chatLog.addSystem(theme.warning('Usage: /schedule-add <name> <HH:mm>'));
2754
+ tui.requestRender();
2755
+ updateTokenEstimate();
2756
+ return;
2757
+ }
2758
+ const name = args.slice(0, spaceIdx).trim();
2759
+ const time = args.slice(spaceIdx + 1).trim();
2760
+ if (!/^\d{2}:\d{2}$/.test(time)) {
2761
+ chatLog.addSystem(theme.warning('Time must be in HH:mm format (e.g., 02:00)'));
2762
+ tui.requestRender();
2763
+ updateTokenEstimate();
2764
+ return;
2765
+ }
2766
+ await loop.addScheduledTask(name, 'daily', time);
2767
+ chatLog.addSystem(theme.success('Task added: ') +
2768
+ theme.fg(name) +
2769
+ theme.dim(' at ') +
2770
+ theme.fg(time) +
2771
+ theme.dim(' daily'));
2772
+ tui.requestRender();
2773
+ updateTokenEstimate();
2774
+ return;
2775
+ }
2776
+ // ── /restart ──
2777
+ if (input === '/restart') {
2778
+ chatLog.addSystem(theme.accent('Restarting...'));
2779
+ tui.requestRender();
2780
+ const fsSync = (await import('node:fs')).default;
2781
+ const restartFile = path.join(os.homedir(), '.agent', '.restart-session');
2782
+ fsSync.mkdirSync(path.dirname(restartFile), { recursive: true });
2783
+ fsSync.writeFileSync(restartFile, path.basename(sessionDir), 'utf-8');
2784
+ setTimeout(() => process.exit(42), 200);
2785
+ return;
2786
+ }
2787
+ // ── /new ──
2788
+ if (input === '/new') {
2789
+ chatLog.addSystem(theme.accent('Starting new session...'));
2790
+ tui.requestRender();
2791
+ const fsSync = (await import('node:fs')).default;
2792
+ const newFlag = path.join(os.homedir(), '.agent', '.new-session');
2793
+ fsSync.mkdirSync(path.dirname(newFlag), { recursive: true });
2794
+ fsSync.writeFileSync(newFlag, 'true', 'utf-8');
2795
+ setTimeout(() => process.exit(42), 200);
2796
+ return;
2797
+ }
2798
+ // \u2500\u2500 Message Queue Integration \u2500\u2500
2799
+ const mode = MessageQueue.detectMode(input);
2800
+ const cleanText = MessageQueue.stripMarkers(input);
2801
+ if (isProcessing || loop.pendingTaskName) {
2802
+ messageQueue.enqueue(cleanText, mode);
2803
+ if (mode === QueueMessageMode.Insert) {
2804
+ chatLog.addSystem(theme.warning(`\u23e9 Inserting: ${truncateMsg(cleanText)}`));
2805
+ loop.interrupt();
2806
+ }
2807
+ else {
2808
+ chatLog.addSystem(theme.dim(`\u23f3 Queued (#${messageQueue.size}): ${truncateMsg(cleanText)}`));
2809
+ }
2810
+ updateTokenEstimate();
2811
+ tui.requestRender();
2812
+ return;
2813
+ }
2814
+ await processBatch(cleanText);
2815
+ }
2816
+ // ── Editor callbacks ──
2817
+ editor.onSubmit = (text) => {
2818
+ void handleInput(text);
2819
+ };
2820
+ editor.onCtrlC = () => {
2821
+ editor.setText('');
2822
+ if (isProcessing) {
2823
+ loop.interrupt();
2824
+ }
2825
+ else {
2826
+ chatLog.addSystem(theme.warning('Ctrl+C \u2014 press again to exit'));
2827
+ }
2828
+ tui.requestRender();
2829
+ };
2830
+ editor.onEscape = () => {
2831
+ if (searchMode) {
2832
+ closeSearch();
2833
+ }
2834
+ tui.requestRender();
2835
+ };
2836
+ editor.onCtrlL = () => {
2837
+ chatLog.clearAll();
2838
+ tui.requestRender();
2839
+ };
2840
+ editor.onCtrlP = () => {
2841
+ loop.toggleProvider();
2842
+ modelName = loop.getActiveProvider().getModel();
2843
+ providerTypeStart = loop.getActiveProvider().getProviderType();
2844
+ const providerInfo = loop.getProviderRoutingInfo();
2845
+ if (providerInfo) {
2846
+ chatLog.addSystem(theme.accent('Provider toggled to ') +
2847
+ theme.fg(providerInfo.providerLabel) +
2848
+ theme.dim(` (${providerInfo.mode})`));
2849
+ }
2850
+ else {
2851
+ chatLog.addSystem(theme.dim('No ProviderRouter configured.'));
2852
+ }
2853
+ const statsP = statsManager.get(sessionDir);
2854
+ statsP.then((st) => {
2855
+ const tc = st.turn_count ?? 0;
2856
+ refreshStatus(loop.getTurnInfo(tc, st.current_context_tokens ?? 0));
2857
+ }).catch(() => { });
2858
+ tui.requestRender();
2859
+ };
2860
+ // ── Queue cancellation: Backspace on empty input pops last queued message ──
2861
+ editor.onBackspaceOnEmpty = () => {
2862
+ if (messageQueue.isEmpty())
2863
+ return;
2864
+ const removed = messageQueue.pop();
2865
+ if (removed) {
2866
+ chatLog.addSystem(theme.dim(`[queue] removed: "${truncateMsg(removed.text)}" (${messageQueue.size} remaining)`));
2867
+ updateTokenEstimate();
2868
+ tui.requestRender();
2869
+ }
2870
+ };
2871
+ // ── Slash command autocomplete ──
2872
+ const slashCommands = filterCommands('').map((cmd) => ({
2873
+ name: cmd.name,
2874
+ description: cmd.description,
2875
+ argumentHint: cmd.args,
2876
+ }));
2877
+ const autocomplete = new CombinedAutocompleteProvider(slashCommands, process.cwd());
2878
+ editor.setAutocompleteProvider(autocomplete);
2879
+ // ── Global input listeners (for shortcuts not caught by the editor) ──
2880
+ let ctrlCCount = 0;
2881
+ let lastCtrlCTime = 0;
2882
+ let searchMode = false;
2883
+ let searchQuery = '';
2884
+ let searchMatches = [];
2885
+ let searchMatchIndex = 0;
2886
+ tui.addInputListener((data) => {
2887
+ // ── Slash autocomplete: Space → accept completion + insert space (never submit) ──
2888
+ // 只有回车键才发送消息,空格键仅用于接受补全并插入空格
2889
+ if (matchesKey(data, Key.space)) {
2890
+ const text = editor.getText().trimStart();
2891
+ if (text.startsWith('/') && !text.includes(' ') && editor.isShowingAutocomplete()) {
2892
+ // 接受补全并插入空格,让用户继续输入参数
2893
+ editor.handleInput('\t');
2894
+ editor.handleInput(' ');
2895
+ return { consume: true };
2896
+ }
2897
+ }
2898
+ // ── Ask User form: full keyboard navigation ──
2899
+ if (askUserState) {
2900
+ const st = askUserState;
2901
+ const suppIdx = st.questions.length;
2902
+ const totalTabs = suppIdx + 1;
2903
+ const currentQ = st.activeQuestion < suppIdx ? st.questions[st.activeQuestion] : null;
2904
+ const opts = currentQ?.options ?? [];
2905
+ const isMulti = currentQ?.multiSelect ?? false;
2906
+ // Enter: submit (on "补充" tab or when focused on custom input)
2907
+ if (matchesKey(data, Key.enter)) {
2908
+ // 收集当前活动的自定义输入
2909
+ const editIdx = st.activeQuestion;
2910
+ const currentCustom = st.customTexts.get(editIdx) ?? '';
2911
+ if (currentCustom.trim()) {
2912
+ st.customTexts.set(editIdx, currentCustom.trim());
2913
+ }
2914
+ resolveAskUser();
2915
+ return { consume: true };
2916
+ }
2917
+ // Escape: cancel (discard form)
2918
+ if (matchesKey(data, Key.escape)) {
2919
+ askUserContent.setText('');
2920
+ askUserBar.setText('');
2921
+ askUserState = null;
2922
+ st.resolve('{}');
2923
+ tui.requestRender();
2924
+ return { consume: true };
2925
+ }
2926
+ // Left/Right: switch tabs
2927
+ if (matchesKey(data, Key.left)) {
2928
+ st.activeQuestion = (st.activeQuestion + totalTabs - 1) % totalTabs;
2929
+ st.activeOption = 0;
2930
+ renderAskUserForm();
2931
+ tui.requestRender();
2932
+ return { consume: true };
2933
+ }
2934
+ if (matchesKey(data, Key.right)) {
2935
+ st.activeQuestion = (st.activeQuestion + 1) % totalTabs;
2936
+ st.activeOption = 0;
2937
+ renderAskUserForm();
2938
+ tui.requestRender();
2939
+ return { consume: true };
2940
+ }
2941
+ // Up/Down: navigate options (only on question tabs)
2942
+ if (st.activeQuestion < suppIdx && opts.length > 0) {
2943
+ if (matchesKey(data, Key.up)) {
2944
+ st.activeOption = (st.activeOption + opts.length - 1) % opts.length;
2945
+ renderAskUserForm();
2946
+ tui.requestRender();
2947
+ return { consume: true };
2948
+ }
2949
+ if (matchesKey(data, Key.down)) {
2950
+ st.activeOption = (st.activeOption + 1) % opts.length;
2951
+ renderAskUserForm();
2952
+ tui.requestRender();
2953
+ return { consume: true };
2954
+ }
2955
+ }
2956
+ // Space: toggle checkbox (works for both multiSelect and single-select)
2957
+ if (matchesKey(data, Key.space)) {
2958
+ if (st.activeQuestion < suppIdx && opts.length > 0) {
2959
+ const sel = st.selectedOptions.get(st.activeQuestion) ?? new Set();
2960
+ if (sel.has(st.activeOption)) {
2961
+ sel.delete(st.activeOption); // 取消选中
2962
+ }
2963
+ else {
2964
+ if (isMulti) {
2965
+ sel.add(st.activeOption);
2966
+ }
2967
+ else {
2968
+ // 单选:清除其它,只选中当前
2969
+ sel.clear();
2970
+ sel.add(st.activeOption);
2971
+ }
2972
+ }
2973
+ st.selectedOptions.set(st.activeQuestion, sel);
2974
+ }
2975
+ renderAskUserForm();
2976
+ tui.requestRender();
2977
+ return { consume: true };
2978
+ }
2979
+ // Backspace: delete last char in custom text
2980
+ if (matchesKey(data, Key.backspace)) {
2981
+ const editIdx = st.activeQuestion;
2982
+ const current = st.customTexts.get(editIdx) ?? '';
2983
+ st.customTexts.set(editIdx, current.slice(0, -1));
2984
+ renderAskUserForm();
2985
+ tui.requestRender();
2986
+ return { consume: true };
2987
+ }
2988
+ // Typing characters → add to current tab's custom text buffer
2989
+ // data is a string; printable chars have length 1 and are not control chars
2990
+ if (typeof data === 'string' && data.length === 1 && data.charCodeAt(0) >= 32) {
2991
+ const editIdx = st.activeQuestion;
2992
+ const current = st.customTexts.get(editIdx) ?? '';
2993
+ st.customTexts.set(editIdx, current + data);
2994
+ renderAskUserForm();
2995
+ tui.requestRender();
2996
+ return { consume: true };
2997
+ }
2998
+ // Consume all other keys during ask_user
2999
+ return { consume: true };
3000
+ }
3001
+ // ── Permission bar: Left/Right arrows + Enter ──
3002
+ if (permissionQueue.length > 0) {
3003
+ if (matchesKey(data, Key.left)) {
3004
+ permissionSelection = (permissionSelection + 3) % 4;
3005
+ updatePermissionBar();
3006
+ tui.requestRender();
3007
+ return { consume: true };
3008
+ }
3009
+ if (matchesKey(data, Key.right)) {
3010
+ permissionSelection = (permissionSelection + 1) % 4;
3011
+ updatePermissionBar();
3012
+ tui.requestRender();
3013
+ return { consume: true };
3014
+ }
3015
+ if (matchesKey(data, Key.enter)) {
3016
+ const options = ['yes', 'aor', 'always', 'no'];
3017
+ resolvePermission(options[permissionSelection]);
3018
+ return { consume: true };
3019
+ }
3020
+ return { consume: true }; // consume all other keys during permission
3021
+ }
3022
+ // Ctrl+C double-tap exit (global, catches the case when editor is not the source)
3023
+ if (matchesKey(data, Key.ctrl('c'))) {
3024
+ const now = Date.now();
3025
+ if (now - lastCtrlCTime < 1000) {
3026
+ ctrlCCount++;
3027
+ }
3028
+ else {
3029
+ ctrlCCount = 1;
3030
+ }
3031
+ lastCtrlCTime = now;
3032
+ // Note: editor.onCtrlC handles the first press; this handles the second
3033
+ return undefined; // let editor handle Ctrl+C
3034
+ }
3035
+ // Ctrl+T: toggle tools expanded/collapsed
3036
+ if (matchesKey(data, Key.ctrl('t'))) {
3037
+ const expanded = chatLog.toggleToolsExpanded();
3038
+ chatLog.addSystem(theme.dim('Tools ') + theme.fg(expanded ? 'expanded' : 'collapsed'));
3039
+ tui.requestRender();
3040
+ return { consume: true };
3041
+ }
3042
+ // Ctrl+F: toggle search overlay
3043
+ if (matchesKey(data, Key.ctrl('f'))) {
3044
+ if (searchMode) {
3045
+ closeSearch();
3046
+ }
3047
+ else {
3048
+ openSearch();
3049
+ }
3050
+ return { consume: true };
3051
+ }
3052
+ // PgUp: scroll chat up
3053
+ if (matchesKey(data, Key.pageUp)) {
3054
+ chatLog.scrollToLine(-10);
3055
+ tui.requestRender();
3056
+ return { consume: true };
3057
+ }
3058
+ // PgDn: scroll chat down
3059
+ if (matchesKey(data, Key.pageDown)) {
3060
+ chatLog.scrollToLine(10);
3061
+ tui.requestRender();
3062
+ return { consume: true };
3063
+ }
3064
+ // Home: jump to top
3065
+ if (matchesKey(data, Key.home)) {
3066
+ chatLog.scrollToLine(0);
3067
+ tui.requestRender();
3068
+ return { consume: true };
3069
+ }
3070
+ // End: jump to bottom
3071
+ if (matchesKey(data, Key.end)) {
3072
+ chatLog.pinToBottom();
3073
+ tui.requestRender();
3074
+ return { consume: true };
3075
+ }
3076
+ // Arrow Up: history browse (when editor text is empty)
3077
+ if (matchesKey(data, Key.up)) {
3078
+ if (slashSubPanelActive)
3079
+ return undefined;
3080
+ const currentText = editor.getText();
3081
+ if (currentText === '' && inputHistory.length > 0) {
3082
+ if (historyIndex === -1) {
3083
+ historyIndex = inputHistory.length - 1;
3084
+ }
3085
+ else if (historyIndex > 0) {
3086
+ historyIndex--;
3087
+ }
3088
+ editor.setText(inputHistory[historyIndex]);
3089
+ tui.requestRender();
3090
+ return { consume: true };
3091
+ }
3092
+ }
3093
+ // Arrow Down: history browse
3094
+ if (matchesKey(data, Key.down)) {
3095
+ if (slashSubPanelActive)
3096
+ return undefined;
3097
+ if (historyIndex >= 0) {
3098
+ if (historyIndex < inputHistory.length - 1) {
3099
+ historyIndex++;
3100
+ editor.setText(inputHistory[historyIndex]);
3101
+ }
3102
+ else {
3103
+ historyIndex = -1;
3104
+ editor.setText('');
3105
+ }
3106
+ tui.requestRender();
3107
+ return { consume: true };
3108
+ }
3109
+ }
3110
+ return undefined;
3111
+ });
3112
+ // ── Search helpers ──
3113
+ let searchOverlayContainer = null;
3114
+ let searchOverlayText = null;
3115
+ /** Highlight occurrences of `query` in `text` (case-insensitive) using accent color */
3116
+ function highlightMatch(text, query, maxLen = 80) {
3117
+ if (!query)
3118
+ return theme.dim(text.slice(0, maxLen));
3119
+ const truncated = text.slice(0, maxLen);
3120
+ const lower = truncated.toLowerCase();
3121
+ const q = query.toLowerCase();
3122
+ let result = '';
3123
+ let idx = 0;
3124
+ while (idx < truncated.length) {
3125
+ const found = lower.indexOf(q, idx);
3126
+ if (found === -1) {
3127
+ result += theme.dim(truncated.slice(idx));
3128
+ break;
3129
+ }
3130
+ if (found > idx) {
3131
+ result += theme.dim(truncated.slice(idx, found));
3132
+ }
3133
+ result += theme.accent(truncated.slice(found, found + q.length));
3134
+ idx = found + q.length;
3135
+ }
3136
+ return result;
3137
+ }
3138
+ function performSearch(query) {
3139
+ searchQuery = query;
3140
+ const contentLines = chatLog.getContentLines();
3141
+ const cleanLines = contentLines.map((l) =>
3142
+ // eslint-disable-next-line no-control-regex
3143
+ l.replace(/\x1b\[[0-9;]*m/g, ''));
3144
+ searchMatches = [];
3145
+ const lowerQuery = query.toLowerCase();
3146
+ for (let i = 0; i < cleanLines.length; i++) {
3147
+ if (cleanLines[i].toLowerCase().includes(lowerQuery)) {
3148
+ searchMatches.push(i);
3149
+ }
3150
+ }
3151
+ searchMatchIndex = 0;
3152
+ if (searchOverlayText) {
3153
+ if (searchMatches.length > 0) {
3154
+ const preview = highlightMatch(cleanLines[searchMatches[0]], query);
3155
+ searchOverlayText.setText(theme.accent('Search: ') +
3156
+ theme.fg(query) +
3157
+ theme.dim(` [${searchMatchIndex + 1}/${searchMatches.length}]`) +
3158
+ '\n' +
3159
+ preview);
3160
+ }
3161
+ else {
3162
+ searchOverlayText.setText(theme.accent('Search: ') + theme.fg(query) + theme.dim(' [0/0]'));
3163
+ }
3164
+ }
3165
+ tui.requestRender();
3166
+ }
3167
+ function nextSearchMatch() {
3168
+ if (searchMatches.length === 0)
3169
+ return;
3170
+ searchMatchIndex = (searchMatchIndex + 1) % searchMatches.length;
3171
+ chatLog.scrollToLine(searchMatches[searchMatchIndex]);
3172
+ if (searchOverlayText) {
3173
+ const contentLines = chatLog.getContentLines();
3174
+ const cleanLines = contentLines.map((l) =>
3175
+ // eslint-disable-next-line no-control-regex
3176
+ l.replace(/\x1b\[[0-9;]*m/g, ''));
3177
+ const preview = highlightMatch(cleanLines[searchMatches[searchMatchIndex]] ?? '', searchQuery);
3178
+ searchOverlayText.setText(theme.accent('Search: ') +
3179
+ theme.fg(searchQuery) +
3180
+ theme.dim(` [${searchMatchIndex + 1}/${searchMatches.length}]`) +
3181
+ '\n' +
3182
+ preview);
3183
+ }
3184
+ tui.requestRender();
3185
+ }
3186
+ function openSearch() {
3187
+ searchMode = true;
3188
+ searchQuery = '';
3189
+ searchMatches = [];
3190
+ searchMatchIndex = 0;
3191
+ searchOverlayContainer = new Container();
3192
+ searchOverlayText = new Text(theme.accent('Search: ') + theme.dim('type to search, Enter for next, Esc to close'), 0, 0);
3193
+ searchOverlayContainer.addChild(searchOverlayText);
3194
+ tui.showOverlay(searchOverlayContainer);
3195
+ tui.requestRender();
3196
+ // Add a temporary input listener for search typing
3197
+ const searchListenerId = `search_${Date.now()}`;
3198
+ tui.addInputListener((data) => {
3199
+ if (!searchMode)
3200
+ return undefined;
3201
+ if (matchesKey(data, Key.escape)) {
3202
+ closeSearch();
3203
+ return { consume: true };
3204
+ }
3205
+ if (matchesKey(data, Key.enter)) {
3206
+ nextSearchMatch();
3207
+ return { consume: true };
3208
+ }
3209
+ if (matchesKey(data, Key.backspace)) {
3210
+ if (searchQuery.length > 0) {
3211
+ searchQuery = searchQuery.slice(0, -1);
3212
+ performSearch(searchQuery);
3213
+ }
3214
+ return { consume: true };
3215
+ }
3216
+ // Regular character input
3217
+ if (typeof data === 'string' && data.length === 1 && !data.startsWith('\x1b')) {
3218
+ searchQuery += data;
3219
+ performSearch(searchQuery);
3220
+ return { consume: true };
3221
+ }
3222
+ // Consume all other input while searching
3223
+ return { consume: true };
3224
+ });
3225
+ }
3226
+ function closeSearch() {
3227
+ searchMode = false;
3228
+ searchQuery = '';
3229
+ searchMatches = [];
3230
+ searchMatchIndex = 0;
3231
+ searchOverlayText = null;
3232
+ if (searchOverlayContainer) {
3233
+ tui.hideOverlay();
3234
+ searchOverlayContainer = null;
3235
+ }
3236
+ tui.requestRender();
3237
+ }
3238
+ // ── Initial render ──
3239
+ const stats = await statsManager.get(sessionDir);
3240
+ const initialInfo = loop.getTurnInfo(0, stats.current_context_tokens ?? 0);
3241
+ refreshStatus(initialInfo);
3242
+ updateTokenEstimate();
3243
+ // ── Start TUI ──
3244
+ tui.start();
3245
+ // ── Auto-send continuation message after restart ──
3246
+ if (continuationMessage) {
3247
+ // Use setImmediate to let the TUI render its initial frame first
3248
+ setImmediate(() => {
3249
+ handleInput(continuationMessage);
3250
+ });
3251
+ }
3252
+ // ── Wait for exit (Ctrl+C double-tap or Ctrl+D) ──
3253
+ const exitPromise = new Promise((resolve) => {
3254
+ const checkExitInterval = setInterval(() => {
3255
+ if (ctrlCCount >= 2) {
3256
+ clearInterval(checkExitInterval);
3257
+ resolve();
3258
+ }
3259
+ }, 200);
3260
+ });
3261
+ await exitPromise;
3262
+ // ── Cleanup ──
3263
+ tui.stop();
3264
+ await channelManager.stopAll();
3265
+ await loop.shutdown();
3266
+ // 清理插件钩子
3267
+ for (const cleanup of cleanupFns)
3268
+ cleanup();
3269
+ await supervisor.shutdownAll();
3270
+ process.stderr.write = origStderrWrite;
3271
+ logStream.end();
3272
+ }
3273
+ //# sourceMappingURL=tui.js.map