minimal-agent 0.10.0 → 0.12.0

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 (573) hide show
  1. package/README.md +11 -3
  2. package/package.json +52 -5
  3. package/plugins/computer_use/.claude-plugin/plugin.json +6 -0
  4. package/plugins/computer_use/README.md +94 -0
  5. package/plugins/computer_use/app/_template.md +142 -0
  6. package/plugins/computer_use/app/chrome.md +165 -0
  7. package/plugins/computer_use/app/finder.md +76 -0
  8. package/plugins/computer_use/app/macos-system-settings.md +81 -0
  9. package/plugins/computer_use/app/microsoft-excel.md +80 -0
  10. package/plugins/computer_use/app/microsoft-powerpoint.md +80 -0
  11. package/plugins/computer_use/app/microsoft-word.md +79 -0
  12. package/plugins/computer_use/app/wechat.md +413 -0
  13. package/plugins/computer_use/app/weread.md +319 -0
  14. package/plugins/computer_use/commands/computeruse.md +25 -0
  15. package/plugins/computer_use/commands/cu.md +9 -0
  16. package/plugins/computer_use/plugin.js +26 -0
  17. package/plugins/computer_use/plugin.ts +30 -0
  18. package/plugins/computer_use/src/capture.js +90 -0
  19. package/plugins/computer_use/src/capture.ts +104 -0
  20. package/plugins/computer_use/src/commands.js +91 -0
  21. package/plugins/computer_use/src/commands.ts +103 -0
  22. package/plugins/computer_use/src/daemon.js +134 -0
  23. package/plugins/computer_use/src/daemon.ts +161 -0
  24. package/plugins/computer_use/src/manuals.js +135 -0
  25. package/plugins/computer_use/src/manuals.ts +159 -0
  26. package/plugins/computer_use/src/verify.js +168 -0
  27. package/plugins/computer_use/src/verify.ts +219 -0
  28. package/plugins/computer_use/test/commands.test.ts +73 -0
  29. package/plugins/cron-runner/plugin.ts +38 -0
  30. package/plugins/cron-runner/src/commands.ts +373 -0
  31. package/plugins/cron-runner/src/cronTime.ts +155 -0
  32. package/plugins/cron-runner/src/lock.ts +79 -0
  33. package/plugins/cron-runner/src/runs.ts +99 -0
  34. package/plugins/cron-runner/src/scheduleEngine.ts +76 -0
  35. package/plugins/cron-runner/src/scheduler.ts +376 -0
  36. package/plugins/cron-runner/src/schema.ts +139 -0
  37. package/plugins/cron-runner/src/service.ts +193 -0
  38. package/plugins/cron-runner/src/store.ts +333 -0
  39. package/plugins/cron-runner/src/types.ts +141 -0
  40. package/plugins/cron-runner/test/cronTime.test.ts +41 -0
  41. package/plugins/cron-runner/test/plugin-integration.test.ts +47 -0
  42. package/plugins/cron-runner/test/scheduleEngine.test.ts +41 -0
  43. package/plugins/cron-runner/test/service.test.ts +76 -0
  44. package/plugins/cron-runner/test/store-and-scheduler.test.ts +501 -0
  45. package/plugins/workflow-runner/plugin.js +56 -2
  46. package/plugins/workflow-runner/plugin.ts +113 -0
  47. package/plugins/workflow-runner/src/agentCommon.js +43 -4
  48. package/plugins/workflow-runner/src/agentCommon.ts +435 -0
  49. package/plugins/workflow-runner/src/controlFlow/branch.js +40 -0
  50. package/plugins/workflow-runner/src/controlFlow/branch.ts +46 -0
  51. package/plugins/workflow-runner/src/controlFlow/loop.js +211 -0
  52. package/plugins/workflow-runner/src/controlFlow/loop.ts +248 -0
  53. package/plugins/workflow-runner/src/controlFlow/pause.js +129 -0
  54. package/plugins/workflow-runner/src/controlFlow/pause.ts +141 -0
  55. package/plugins/workflow-runner/src/controlFlow/signal.js +21 -0
  56. package/plugins/workflow-runner/src/controlFlow/signal.ts +29 -0
  57. package/plugins/workflow-runner/src/controlFlow/types.js +18 -0
  58. package/plugins/workflow-runner/src/controlFlow/types.ts +63 -0
  59. package/plugins/workflow-runner/src/dslManifest.ts +127 -0
  60. package/plugins/workflow-runner/src/expressions.ts +622 -0
  61. package/plugins/workflow-runner/src/index.js +13 -4
  62. package/plugins/workflow-runner/src/index.ts +338 -0
  63. package/plugins/workflow-runner/src/jsonPayload.ts +84 -0
  64. package/plugins/workflow-runner/src/loader.js +23 -0
  65. package/plugins/workflow-runner/src/loader.ts +721 -0
  66. package/plugins/workflow-runner/src/multimodal.js +2 -1
  67. package/plugins/workflow-runner/src/multimodal.ts +64 -0
  68. package/plugins/workflow-runner/src/recovery.ts +149 -0
  69. package/plugins/workflow-runner/src/runner.js +75 -659
  70. package/plugins/workflow-runner/src/runner.ts +604 -0
  71. package/plugins/workflow-runner/src/stepExecutors/agent.ts +234 -0
  72. package/plugins/workflow-runner/src/stepExecutors/assert.ts +50 -0
  73. package/plugins/workflow-runner/src/stepExecutors/echo.js +31 -0
  74. package/plugins/workflow-runner/src/stepExecutors/echo.ts +38 -0
  75. package/plugins/workflow-runner/src/stepExecutors/llm.ts +133 -0
  76. package/plugins/workflow-runner/src/stepExecutors/set.js +49 -0
  77. package/plugins/workflow-runner/src/stepExecutors/set.ts +56 -0
  78. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  79. package/plugins/workflow-runner/src/stepExecutors/skill.ts +140 -0
  80. package/plugins/workflow-runner/src/stepExecutors/tool.js +80 -8
  81. package/plugins/workflow-runner/src/stepExecutors/tool.ts +256 -0
  82. package/plugins/workflow-runner/src/stepExecutors/verify.ts +68 -0
  83. package/plugins/workflow-runner/src/stepPipeline.js +277 -0
  84. package/plugins/workflow-runner/src/stepPipeline.ts +366 -0
  85. package/plugins/workflow-runner/src/taskIdentity.ts +405 -0
  86. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  87. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  88. package/plugins/workflow-runner/src/types.js +37 -1
  89. package/plugins/workflow-runner/src/types.ts +602 -0
  90. package/plugins/workflow-runner/src/workflowState.ts +733 -0
  91. package/plugins/workflow-runner/test/agent.test.ts +587 -0
  92. package/plugins/workflow-runner/test/agentLlmError.test.ts +373 -0
  93. package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +65 -0
  94. package/plugins/workflow-runner/test/captureMisbind.test.ts +99 -0
  95. package/plugins/workflow-runner/test/cli.e2e.test.ts +147 -0
  96. package/plugins/workflow-runner/test/context-files.test.ts +320 -0
  97. package/plugins/workflow-runner/test/e2e.test.ts +370 -0
  98. package/plugins/workflow-runner/test/engineRobustness.test.ts +226 -0
  99. package/plugins/workflow-runner/test/expressions.test.ts +397 -0
  100. package/plugins/workflow-runner/test/foundation.test.ts +126 -0
  101. package/plugins/workflow-runner/test/graceful.test.ts +139 -0
  102. package/plugins/workflow-runner/test/greedyInputs.load.test.ts +47 -0
  103. package/plugins/workflow-runner/test/hostCapability.test.ts +108 -0
  104. package/plugins/workflow-runner/test/illustrate.load.test.ts +183 -0
  105. package/plugins/workflow-runner/test/jsonPayload.test.ts +83 -0
  106. package/plugins/workflow-runner/test/loader.test.ts +617 -0
  107. package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +33 -0
  108. package/plugins/workflow-runner/test/multimodal.test.ts +342 -0
  109. package/plugins/workflow-runner/test/output-schema.test.ts +213 -0
  110. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +87 -0
  111. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +269 -0
  112. package/plugins/workflow-runner/test/ppt.load.test.ts +1220 -0
  113. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +220 -0
  114. package/plugins/workflow-runner/test/recovery.test.ts +265 -0
  115. package/plugins/workflow-runner/test/runner.test.ts +911 -0
  116. package/plugins/workflow-runner/test/runnerPreflight.test.ts +379 -0
  117. package/plugins/workflow-runner/test/skillTurnDone.test.ts +71 -0
  118. package/plugins/workflow-runner/test/stateLifecycle.test.ts +150 -0
  119. package/plugins/workflow-runner/test/stepPipeline.test.ts +114 -0
  120. package/plugins/workflow-runner/test/taskArbitration.test.ts +338 -0
  121. package/plugins/workflow-runner/test/taskIdentity.test.ts +237 -0
  122. package/plugins/workflow-runner/test/taskLifecycle.test.ts +270 -0
  123. package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +221 -0
  124. package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +29 -0
  125. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  126. package/plugins/workflow-runner/test/webppt.load.test.ts +250 -0
  127. package/plugins/workflow-runner/test/word.load.test.ts +208 -0
  128. package/plugins/workflow-runner/test/workflows.smoke.test.ts +76 -0
  129. package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +207 -0
  130. package/skills/browser-automation/SKILL.md +9 -19
  131. package/skills/web-crawler/SKILL.md +9 -11
  132. package/src/askBridge.ts +249 -0
  133. package/src/bootstrap/cwdArg.js +10 -3
  134. package/src/bootstrap/cwdArg.ts +31 -0
  135. package/src/bootstrap/workingDir.ts +48 -0
  136. package/src/cli/args.js +70 -7
  137. package/src/cli/args.ts +146 -0
  138. package/src/cli/computerUse.js +185 -0
  139. package/src/cli/computerUse.ts +221 -0
  140. package/src/cli/configMenu.js +274 -202
  141. package/src/cli/configMenu.tsx +521 -0
  142. package/src/cli/memoryAccess.js +45 -0
  143. package/src/cli/memoryAccess.ts +53 -0
  144. package/src/cli/print.js +119 -5
  145. package/src/cli/print.ts +597 -0
  146. package/src/cli/quickModelSetup.js +60 -0
  147. package/src/cli/quickModelSetup.tsx +96 -0
  148. package/src/cli/streamJson.js +80 -4
  149. package/src/cli/streamJson.ts +433 -0
  150. package/src/cli/wizards/advisorWizard.js +1 -0
  151. package/src/cli/wizards/advisorWizard.tsx +195 -0
  152. package/src/cli/wizards/agentRuntimeWizard.js +84 -0
  153. package/src/cli/wizards/agentRuntimeWizard.tsx +134 -0
  154. package/src/cli/wizards/browserWizard.js +55 -0
  155. package/src/cli/wizards/browserWizard.tsx +77 -0
  156. package/src/cli/wizards/contextAdvisorWizard.js +84 -0
  157. package/src/cli/wizards/contextAdvisorWizard.tsx +124 -0
  158. package/src/cli/wizards/fieldForm.js +7 -4
  159. package/src/cli/wizards/fieldForm.tsx +250 -0
  160. package/src/cli/wizards/imageWizard.js +4 -5
  161. package/src/cli/wizards/imageWizard.tsx +96 -0
  162. package/src/cli/wizards/openRouterWizard.tsx +48 -0
  163. package/src/cli/wizards/providerPresets.js +1 -8
  164. package/src/cli/wizards/providerPresets.ts +106 -0
  165. package/src/cli/wizards/providerWizard.js +152 -205
  166. package/src/cli/wizards/providerWizard.tsx +266 -0
  167. package/src/cli/wizards/searchWizard.tsx +47 -0
  168. package/src/cli/wizards/ttsWizard.js +4 -3
  169. package/src/cli/wizards/ttsWizard.tsx +61 -0
  170. package/src/config/configDefaults.js +25 -0
  171. package/src/config/configDefaults.ts +33 -0
  172. package/src/config/configDiagnostics.js +117 -0
  173. package/src/config/configDiagnostics.ts +166 -0
  174. package/src/config/configFile.js +52 -32
  175. package/src/config/configFile.ts +633 -0
  176. package/src/config/configManagement.js +126 -0
  177. package/src/config/configManagement.ts +159 -0
  178. package/src/config/configService.js +66 -11
  179. package/src/config/configService.ts +554 -0
  180. package/src/config/numericLimits.js +61 -0
  181. package/src/config/numericLimits.ts +80 -0
  182. package/src/config.ts +109 -0
  183. package/src/context/agentHome.ts +11 -0
  184. package/src/context/archive.ts +144 -0
  185. package/src/context/budgetPlan.js +54 -0
  186. package/src/context/budgetPlan.ts +88 -0
  187. package/src/context/compact.js +179 -106
  188. package/src/context/compact.ts +584 -0
  189. package/src/context/contextBundle.js +119 -0
  190. package/src/context/contextBundle.ts +176 -0
  191. package/src/context/contextLock.ts +84 -0
  192. package/src/context/microCompactLite.ts +218 -0
  193. package/src/context/persistContext.ts +177 -0
  194. package/src/context/reactiveCompact.js +19 -4
  195. package/src/context/reactiveCompact.ts +240 -0
  196. package/src/context/recentDirs.ts +42 -0
  197. package/src/context/sessionPath.ts +206 -0
  198. package/src/context/sessionRegistry.js +8 -2
  199. package/src/context/sessionRegistry.ts +414 -0
  200. package/src/context/snipCompact.ts +163 -0
  201. package/src/context/tokenCounter.ts +106 -0
  202. package/src/context/workspaceIndex.js +2 -2
  203. package/src/context/workspaceIndex.ts +751 -0
  204. package/src/entry.compiled.tsx +20 -0
  205. package/src/harness/claimValidation.js +9 -0
  206. package/src/harness/claimValidation.ts +22 -0
  207. package/src/harness/evidenceLedger.js +92 -0
  208. package/src/harness/evidenceLedger.ts +96 -0
  209. package/src/harness/stallGuard.js +60 -0
  210. package/src/harness/stallGuard.ts +81 -0
  211. package/src/humanIntervention.js +129 -0
  212. package/src/humanIntervention.ts +216 -0
  213. package/src/infra/verification.ts +295 -0
  214. package/src/irreversible/rules.js +216 -0
  215. package/src/irreversible/rules.ts +261 -0
  216. package/src/llm/client.js +40 -3
  217. package/src/llm/client.ts +621 -0
  218. package/src/llm/promptCache.js +27 -0
  219. package/src/llm/promptCache.ts +41 -0
  220. package/src/llm/providers/anthropicFormat.js +41 -5
  221. package/src/llm/providers/anthropicFormat.ts +290 -0
  222. package/src/llm/providers/openaiFormat.js +186 -8
  223. package/src/llm/providers/openaiFormat.ts +428 -0
  224. package/src/loop.js +614 -63
  225. package/src/loop.ts +1454 -0
  226. package/src/main.js +67 -10
  227. package/src/main.tsx +344 -0
  228. package/src/memory/codec.js +204 -0
  229. package/src/memory/codec.ts +249 -0
  230. package/src/memory/constants.js +34 -0
  231. package/src/memory/constants.ts +50 -0
  232. package/src/memory/episodicMemory.js +684 -0
  233. package/src/memory/episodicMemory.ts +815 -0
  234. package/src/memory/fileStore.js +148 -0
  235. package/src/memory/fileStore.ts +184 -0
  236. package/src/memory/memoryService.js +570 -0
  237. package/src/memory/memoryService.ts +859 -0
  238. package/src/memory/migration.js +128 -0
  239. package/src/memory/migration.ts +160 -0
  240. package/src/memory/repository.js +789 -0
  241. package/src/memory/repository.ts +893 -0
  242. package/src/memory/retrieval.js +108 -0
  243. package/src/memory/retrieval.ts +145 -0
  244. package/src/memory/searchIndex.js +107 -0
  245. package/src/memory/searchIndex.ts +153 -0
  246. package/src/memory/topicView.js +82 -0
  247. package/src/memory/topicView.ts +95 -0
  248. package/src/memory/turnJournal.js +95 -0
  249. package/src/memory/turnJournal.ts +120 -0
  250. package/src/memory/types.js +2 -0
  251. package/src/memory/types.ts +186 -0
  252. package/src/messageQueue.ts +123 -0
  253. package/src/multimodal.js +128 -46
  254. package/src/multimodal.ts +349 -0
  255. package/src/plugin-sdk.js +21 -1
  256. package/src/plugin-sdk.ts +134 -0
  257. package/src/plugins/builtinPluginIds.js +47 -0
  258. package/src/plugins/builtinPluginIds.ts +51 -0
  259. package/src/plugins/builtinRegistry.ts +43 -0
  260. package/src/plugins/commandRouter.js +40 -26
  261. package/src/plugins/commandRouter.ts +261 -0
  262. package/src/plugins/hookEngine.ts +341 -0
  263. package/src/plugins/pluginApi.ts +133 -0
  264. package/src/plugins/pluginLifecycle.ts +34 -0
  265. package/src/plugins/pluginLoader.ts +147 -0
  266. package/src/plugins/pluginManifest.js +127 -0
  267. package/src/plugins/pluginManifest.ts +152 -0
  268. package/src/plugins/pluginRunner.js +47 -2
  269. package/src/plugins/pluginRunner.ts +207 -0
  270. package/src/plugins/transcript.ts +208 -0
  271. package/src/prompts/lint.js +169 -0
  272. package/src/prompts/lint.ts +220 -0
  273. package/src/prompts/pluginList.js +32 -16
  274. package/src/prompts/pluginList.ts +86 -0
  275. package/src/prompts/policyRegistry.js +318 -0
  276. package/src/prompts/policyRegistry.ts +358 -0
  277. package/src/prompts/projectInstructions.js +24 -0
  278. package/src/prompts/projectInstructions.ts +81 -0
  279. package/src/prompts/promptAssembly.js +311 -202
  280. package/src/prompts/promptAssembly.ts +510 -0
  281. package/src/prompts/skillList.js +106 -45
  282. package/src/prompts/skillList.ts +198 -0
  283. package/src/prompts/system.js +31 -6
  284. package/src/prompts/system.ts +81 -0
  285. package/src/runtime/agentRuntimeContext.js +53 -0
  286. package/src/runtime/agentRuntimeContext.ts +112 -0
  287. package/src/session/runAgentTurn.js +71 -12
  288. package/src/session/runAgentTurn.ts +181 -0
  289. package/src/session/runTurn.js +29 -0
  290. package/src/session/runTurn.ts +160 -0
  291. package/src/session/runtimeStore.js +149 -0
  292. package/src/session/runtimeStore.ts +197 -0
  293. package/src/session/sessionState.js +79 -3
  294. package/src/session/sessionState.ts +134 -0
  295. package/src/toolGate.ts +50 -0
  296. package/src/tools/advisor/advisor.js +76 -15
  297. package/src/tools/advisor/advisor.ts +512 -0
  298. package/src/tools/agent/agent.js +245 -29
  299. package/src/tools/agent/agent.ts +837 -0
  300. package/src/tools/agent/agentTaskService.js +60 -0
  301. package/src/tools/agent/agentTaskService.ts +113 -0
  302. package/src/tools/agent/agentTypes.js +20 -8
  303. package/src/tools/agent/agentTypes.ts +94 -0
  304. package/src/tools/agent/coordinator.ts +111 -0
  305. package/src/tools/agent/worktree.ts +296 -0
  306. package/src/tools/ask/ask.js +17 -0
  307. package/src/tools/ask/ask.ts +122 -0
  308. package/src/tools/bash/attachImage.js +223 -0
  309. package/src/tools/bash/attachImage.ts +281 -0
  310. package/src/tools/bash/bash.js +132 -14
  311. package/src/tools/bash/bash.ts +642 -0
  312. package/src/tools/bash/semantics.ts +103 -0
  313. package/src/tools/bash/warnings.js +17 -11
  314. package/src/tools/bash/warnings.ts +117 -0
  315. package/src/tools/capabilities.js +42 -0
  316. package/src/tools/capabilities.ts +69 -0
  317. package/src/tools/computeruse/accessibilityPermission.js +261 -0
  318. package/src/tools/computeruse/accessibilityPermission.ts +294 -0
  319. package/src/tools/computeruse/axSearch.js +96 -0
  320. package/src/tools/computeruse/axSearch.ts +120 -0
  321. package/src/tools/computeruse/axSnap.js +272 -0
  322. package/src/tools/computeruse/axSnap.ts +349 -0
  323. package/src/tools/computeruse/capabilityLabels.js +95 -0
  324. package/src/tools/computeruse/capabilityLabels.ts +115 -0
  325. package/src/tools/computeruse/computerUseService.js +2235 -0
  326. package/src/tools/computeruse/computerUseService.ts +2686 -0
  327. package/src/tools/computeruse/computeruse.js +2742 -0
  328. package/src/tools/computeruse/computeruse.ts +3265 -0
  329. package/src/tools/computeruse/diagnosticTrace.js +154 -0
  330. package/src/tools/computeruse/diagnosticTrace.ts +195 -0
  331. package/src/tools/computeruse/diagnostics.js +369 -0
  332. package/src/tools/computeruse/diagnostics.ts +453 -0
  333. package/src/tools/computeruse/grants.js +53 -0
  334. package/src/tools/computeruse/grants.ts +70 -0
  335. package/src/tools/computeruse/humanBoundary.js +159 -0
  336. package/src/tools/computeruse/humanBoundary.ts +199 -0
  337. package/src/tools/computeruse/intervention.js +77 -0
  338. package/src/tools/computeruse/intervention.ts +110 -0
  339. package/src/tools/computeruse/manualSchema.js +709 -0
  340. package/src/tools/computeruse/manualSchema.ts +916 -0
  341. package/src/tools/computeruse/observationImages.js +705 -0
  342. package/src/tools/computeruse/observationImages.ts +889 -0
  343. package/src/tools/computeruse/protocol.js +60 -0
  344. package/src/tools/computeruse/protocol.ts +515 -0
  345. package/src/tools/computeruse/telemetryLog.js +57 -0
  346. package/src/tools/computeruse/telemetryLog.ts +67 -0
  347. package/src/tools/computeruse/vision/appGuide.js +325 -0
  348. package/src/tools/computeruse/vision/appGuide.ts +482 -0
  349. package/src/tools/computeruse/vision/focus.js +291 -0
  350. package/src/tools/computeruse/vision/focus.ts +382 -0
  351. package/src/tools/computeruse/vision/frames.js +248 -0
  352. package/src/tools/computeruse/vision/frames.ts +329 -0
  353. package/src/tools/computeruse/vision/geometry.js +131 -0
  354. package/src/tools/computeruse/vision/geometry.ts +163 -0
  355. package/src/tools/computeruse/vision/imaging.js +201 -0
  356. package/src/tools/computeruse/vision/imaging.ts +281 -0
  357. package/src/tools/computeruse/vision/png.js +274 -0
  358. package/src/tools/computeruse/vision/png.ts +317 -0
  359. package/src/tools/computeruse/vision/raster.js +320 -0
  360. package/src/tools/computeruse/vision/raster.ts +408 -0
  361. package/src/tools/computeruse/vision/settle.js +131 -0
  362. package/src/tools/computeruse/vision/settle.ts +220 -0
  363. package/src/tools/computeruse/vision/telemetry.js +196 -0
  364. package/src/tools/computeruse/vision/telemetry.ts +241 -0
  365. package/src/tools/computeruse/visionOcr.js +115 -0
  366. package/src/tools/computeruse/visionOcr.ts +128 -0
  367. package/src/tools/computeruse/waitFor.js +237 -0
  368. package/src/tools/computeruse/waitFor.ts +291 -0
  369. package/src/tools/download/download.js +10 -6
  370. package/src/tools/download/download.ts +187 -0
  371. package/src/tools/edit/edit.js +30 -18
  372. package/src/tools/edit/edit.ts +349 -0
  373. package/src/tools/edit/multi-edit.js +25 -14
  374. package/src/tools/edit/multi-edit.ts +223 -0
  375. package/src/tools/effectiveToolSet.js +73 -0
  376. package/src/tools/effectiveToolSet.ts +91 -0
  377. package/src/tools/genimage/genimage.ts +257 -0
  378. package/src/tools/genspeech/genspeech.ts +83 -0
  379. package/src/tools/glob/glob.js +176 -29
  380. package/src/tools/glob/glob.ts +283 -0
  381. package/src/tools/grep/grep.js +61 -19
  382. package/src/tools/grep/grep.ts +239 -0
  383. package/src/tools/grep/rgPath.ts +206 -0
  384. package/src/tools/index.js +99 -5
  385. package/src/tools/index.ts +293 -0
  386. package/src/tools/lsp/config.js +113 -0
  387. package/src/tools/lsp/config.ts +113 -0
  388. package/src/tools/lsp/lsp.js +256 -0
  389. package/src/tools/lsp/lsp.ts +274 -0
  390. package/src/tools/lsp/service.js +303 -0
  391. package/src/tools/lsp/service.ts +296 -0
  392. package/src/tools/memory/memory.js +520 -0
  393. package/src/tools/memory/memory.ts +625 -0
  394. package/src/tools/memory/policy.js +53 -0
  395. package/src/tools/memory/policy.ts +72 -0
  396. package/src/tools/notebook/notebookEdit.js +114 -0
  397. package/src/tools/notebook/notebookEdit.ts +126 -0
  398. package/src/tools/powershell/powershell.js +210 -0
  399. package/src/tools/powershell/powershell.ts +239 -0
  400. package/src/tools/presentresult/presentresult.js +145 -0
  401. package/src/tools/presentresult/presentresult.ts +156 -0
  402. package/src/tools/read/read.js +371 -163
  403. package/src/tools/read/read.ts +558 -0
  404. package/src/tools/shared/diffSummary.js +44 -0
  405. package/src/tools/shared/diffSummary.ts +63 -0
  406. package/src/tools/shared/fileActivity.js +51 -0
  407. package/src/tools/shared/fileActivity.ts +66 -0
  408. package/src/tools/shared/fileMutationBus.js +13 -0
  409. package/src/tools/shared/fileMutationBus.ts +14 -0
  410. package/src/tools/shared/fileState.js +73 -65
  411. package/src/tools/shared/fileState.ts +145 -0
  412. package/src/tools/shared/fileUtils.js +81 -4
  413. package/src/tools/shared/fileUtils.ts +504 -0
  414. package/src/tools/shared/killTree.ts +50 -0
  415. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  416. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  417. package/src/tools/shared/schemas.ts +20 -0
  418. package/src/tools/shared/toolOutputStore.js +97 -0
  419. package/src/tools/shared/toolOutputStore.ts +110 -0
  420. package/src/tools/skill/skill.js +86 -0
  421. package/src/tools/skill/skill.ts +94 -0
  422. package/src/tools/snip/snip.js +67 -0
  423. package/src/tools/snip/snip.ts +80 -0
  424. package/src/tools/tasks/backgroundTaskService.js +275 -0
  425. package/src/tools/tasks/backgroundTaskService.ts +369 -0
  426. package/src/tools/tasks/processEvidence.js +24 -0
  427. package/src/tools/tasks/processEvidence.ts +34 -0
  428. package/src/tools/tasks/taskManagement.js +163 -0
  429. package/src/tools/tasks/taskManagement.ts +180 -0
  430. package/src/tools/tasks/taskOutput.js +113 -0
  431. package/src/tools/tasks/taskOutput.ts +145 -0
  432. package/src/tools/tasks/taskStop.js +74 -0
  433. package/src/tools/tasks/taskStop.ts +99 -0
  434. package/src/tools/tasks/taskStore.js +46 -0
  435. package/src/tools/tasks/taskStore.ts +77 -0
  436. package/src/tools/terminal/terminalCapture.js +167 -0
  437. package/src/tools/terminal/terminalCapture.ts +179 -0
  438. package/src/tools/trash/trash.js +233 -0
  439. package/src/tools/trash/trash.ts +283 -0
  440. package/src/tools/types.js +1 -1
  441. package/src/tools/types.ts +24 -0
  442. package/src/tools/webbrowser/agentBrowserPath.ts +286 -0
  443. package/src/tools/webbrowser/browser.js +214 -11
  444. package/src/tools/webbrowser/browser.ts +293 -0
  445. package/src/tools/webbrowser/browserControl.js +153 -48
  446. package/src/tools/webbrowser/browserControl.ts +262 -0
  447. package/src/tools/webbrowser/browserDaemon.js +147 -39
  448. package/src/tools/webbrowser/browserDaemon.ts +270 -0
  449. package/src/tools/webbrowser/browserOutcome.js +273 -0
  450. package/src/tools/webbrowser/browserOutcome.ts +306 -0
  451. package/src/tools/webbrowser/browserRunRegistry.js +150 -0
  452. package/src/tools/webbrowser/browserRunRegistry.ts +179 -0
  453. package/src/tools/webbrowser/browserService.js +20 -6
  454. package/src/tools/webbrowser/browserService.ts +56 -0
  455. package/src/tools/webbrowser/browserStream.js +30 -1
  456. package/src/tools/webbrowser/browserStream.ts +216 -0
  457. package/src/tools/webbrowser/browserTabs.js +143 -0
  458. package/src/tools/webbrowser/browserTabs.ts +210 -0
  459. package/src/tools/webbrowser/webbrowser.js +831 -150
  460. package/src/tools/webbrowser/webbrowser.ts +1537 -0
  461. package/src/tools/webfetch/preapproved.ts +274 -0
  462. package/src/tools/webfetch/webfetch.js +24 -23
  463. package/src/tools/webfetch/webfetch.ts +383 -0
  464. package/src/tools/websearch/websearch.js +12 -11
  465. package/src/tools/websearch/websearch.ts +292 -0
  466. package/src/tools/worktree/sessionWorktree.js +128 -0
  467. package/src/tools/worktree/sessionWorktree.ts +158 -0
  468. package/src/tools/write/write.js +27 -44
  469. package/src/tools/write/write.ts +136 -0
  470. package/src/types.ts +960 -0
  471. package/src/ui/App.js +216 -12
  472. package/src/ui/App.tsx +879 -0
  473. package/src/ui/AskQuestion.tsx +300 -0
  474. package/src/ui/InputBox.js +77 -1
  475. package/src/ui/InputBox.tsx +567 -0
  476. package/src/ui/LiveArea.tsx +344 -0
  477. package/src/ui/MessageList.js +27 -6
  478. package/src/ui/MessageList.tsx +559 -0
  479. package/src/ui/ModelPicker.tsx +107 -0
  480. package/src/ui/QueuedMessagesBanner.tsx +51 -0
  481. package/src/ui/Root.js +6 -8
  482. package/src/ui/Root.tsx +100 -0
  483. package/src/ui/SessionPicker.tsx +169 -0
  484. package/src/ui/StatusLine.js +22 -1
  485. package/src/ui/StatusLine.tsx +145 -0
  486. package/src/ui/WorkspacePicker.tsx +84 -0
  487. package/src/ui/advisorCommand.js +54 -0
  488. package/src/ui/advisorCommand.ts +63 -0
  489. package/src/ui/computerCommand.js +19 -0
  490. package/src/ui/computerCommand.ts +35 -0
  491. package/src/ui/figures.ts +45 -0
  492. package/src/ui/hooks/askSlot.ts +112 -0
  493. package/src/ui/hooks/useChat.js +271 -7
  494. package/src/ui/hooks/useChat.ts +1578 -0
  495. package/src/ui/hooks/useInputHistory.ts +235 -0
  496. package/src/ui/hooks/usePasteHandler.ts +220 -0
  497. package/src/ui/hooks/useTerminalSize.ts +41 -0
  498. package/src/ui/hooks/useTextBuffer.ts +108 -0
  499. package/src/ui/hooks/useTokenUsage.ts +44 -0
  500. package/src/ui/liveViewport.ts +189 -0
  501. package/src/ui/markdown.ts +230 -0
  502. package/src/ui/textBuffer.ts +239 -0
  503. package/src/ui/theme.ts +35 -0
  504. package/src/utils/clipboardImage.js +358 -0
  505. package/src/utils/clipboardImage.ts +420 -0
  506. package/src/utils/debugLog.ts +50 -0
  507. package/src/utils/greenRoot.ts +35 -0
  508. package/src/utils/packageRoot.ts +40 -0
  509. package/src/utils/pathContains.js +28 -0
  510. package/src/utils/pathContains.ts +31 -0
  511. package/src/utils/resourcePaths.ts +71 -0
  512. package/src/utils/stripThink.ts +49 -0
  513. package/src/utils/zodToJson.ts +73 -0
  514. package/src/web/browserBridge.js +365 -66
  515. package/src/web/browserBridge.ts +672 -0
  516. package/src/web/protocol.ts +1804 -0
  517. package/vendor/computer-use/arm64-darwin/cu-helperd +0 -0
  518. package/workflows/ppt.yaml +13 -2
  519. package/workflows/wechat-reading-shots.yaml +315 -0
  520. package/workflows/word.yaml +11 -1
  521. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  522. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  523. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  524. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  525. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  526. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  527. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  528. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  529. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  530. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  531. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  532. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  533. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  534. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  535. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  536. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  537. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  538. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  539. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  540. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  541. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  542. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  543. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  544. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  545. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  546. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  547. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  548. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  549. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  550. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  551. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  552. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  553. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  554. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  555. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  556. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  557. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  558. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  559. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  560. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  561. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  562. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  563. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  564. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  565. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  566. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  567. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  568. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  569. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  570. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  571. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  572. /package/skills/ai-image/scripts/{generate_test.mjs → generate.selftest.mjs} +0 -0
  573. /package/skills/ai-image/scripts/{generate_from_plan_test.mjs → generate_from_plan.selftest.mjs} +0 -0
@@ -0,0 +1,1578 @@
1
+ /**
2
+ * ============================================================
3
+ * src/ui/hooks/useChat.ts —— 把 loop.runQuery 桥接到 React 状态
4
+ * ------------------------------------------------------------
5
+ * 这个 hook 是 UI 层的核心:
6
+ * - 维护 history 数组(用 ref 持久引用,version 计数器触发重渲染)
7
+ * - 维护"当前正在打字"的 streamingText
8
+ * - 维护"工作过程显示"用的 progressStage / reasoningPreview / runningTools
9
+ * - 暴露 submit() 给 InputBox 调用、abort() 给 Ctrl+C 处理
10
+ *
11
+ * 为什么 history 用 ref 而不是 state?
12
+ * runQuery 内部会 mutate history 数组(push 新消息、压缩时清空重建),
13
+ * React state 的"不可变更新"规则不适合。用 ref 持有可变引用,
14
+ * 用 version 计数器驱动 setState 触发 re-render 是更顺手的写法。
15
+ * ============================================================
16
+ */
17
+
18
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
19
+ import { stat } from 'node:fs/promises';
20
+
21
+ import { getWorkingDir, setWorkingDir } from '../../bootstrap/workingDir.js';
22
+ import { forceCompact } from '../../context/compact.js';
23
+ import {
24
+ clearContext,
25
+ loadContext,
26
+ saveContext,
27
+ } from '../../context/persistContext.js';
28
+ import { resetReactiveCompactState } from '../../context/reactiveCompact.js';
29
+ import { acquireContextLock } from '../../context/contextLock.js';
30
+ import { workspaceIndexService } from '../../context/workspaceIndex.js';
31
+ import {
32
+ getActiveSession,
33
+ setActiveSession,
34
+ updateSessionMetadata,
35
+ } from '../../context/sessionRegistry.js';
36
+ import {
37
+ resolveSessionFile,
38
+ sessionFileFor,
39
+ } from '../../context/sessionPath.js';
40
+ import { runQuery } from '../../loop.js';
41
+ import { buildBaseSystemPrompt, buildTransientPromptBundle } from '../../prompts/system.js';
42
+ import { createPromptCacheContext } from '../../llm/promptCache.js';
43
+ import {
44
+ createAgentRuntimeContext,
45
+ filterToolsForRuntime,
46
+ memoryContextFromRuntime,
47
+ } from '../../runtime/agentRuntimeContext.js';
48
+ import { startTurnJournal, type TurnJournal } from '../../memory/turnJournal.js';
49
+ import { ALL_TOOLS, getToolByName } from '../../tools/index.js';
50
+ import { createInputQueue } from '../../messageQueue.js';
51
+ import {
52
+ createAskCoordinator,
53
+ setAskHandler,
54
+ type AskAnswer,
55
+ type AskQuestionSpec,
56
+ type AskSource,
57
+ } from '../../askBridge.js';
58
+ import { createAskSlot } from './askSlot.js';
59
+ import { figures } from '../figures.js';
60
+ import { clearFileState } from '../../tools/shared/fileState.js';
61
+ import { getOrCreateSessionState, sessionStateKey } from '../../session/sessionState.js';
62
+ import { hydrateSessionWorktree } from '../../tools/worktree/sessionWorktree.js';
63
+ import type {
64
+ LoopEvent,
65
+ Message,
66
+ ProgressStage,
67
+ Provider,
68
+ ToolResult,
69
+ ToolStatusState,
70
+ } from '../../types.js';
71
+ import { runAdvisor } from '../../tools/advisor/advisor.js';
72
+ import type { PluginEvent } from '../../plugins/pluginApi.js';
73
+ import { runWithPlugins } from '../../plugins/pluginRunner.js';
74
+ import { dlog } from '../../utils/debugLog.js';
75
+ import type { ComputerUseCliMode } from '../../cli/computerUse.js';
76
+ import { createCliComputerRun, createComputerUseHostGate } from '../../cli/computerUse.js';
77
+ import {
78
+ computerUseService,
79
+ } from '../../tools/computeruse/computerUseService.js';
80
+ import {
81
+ HumanInterventionCoordinator,
82
+ type PendingHumanIntervention,
83
+ } from '../../humanIntervention.js';
84
+ import { createComputerInterventionAdapter } from '../../tools/computeruse/intervention.js';
85
+ import type {
86
+ ComputerUseRunContext,
87
+ } from '../../tools/computeruse/protocol.js';
88
+
89
+ export interface PluginProgressState {
90
+ pluginId: string;
91
+ /** 块内/迭代进度数字;undefined = 纯文案进度(UI 不渲染数字,只显示 message) */
92
+ current?: number;
93
+ max?: number;
94
+ message?: string;
95
+ }
96
+
97
+ /** loop.ts 每轮 yield 的 context_usage 事件(去掉 type 字段)。 */
98
+ export type ContextUsageInfo = Omit<Extract<LoopEvent, { type: 'context_usage' }>, 'type'>;
99
+
100
+ export interface UseChatResult {
101
+ history: Message[];
102
+ /** 当前流式中的文本(assistant 还在打字) */
103
+ streamingText: string;
104
+ /** 是否正在等模型 */
105
+ isLoading: boolean;
106
+ /** 最近一次错误(红色提示,永久进 scrollback 区上方) */
107
+ error: string | null;
108
+ /**
109
+ * 暂态错误(黄色,LiveArea 状态行下方一行)。喂给 LiveArea,可被覆盖刷新。
110
+ *
111
+ * 用途:工作流步执行中 LLM 调用失败(会被该步重试)时,把错误存这里而非作为
112
+ * 永久红字提交 —— 重试成功 / 步结束后由 useChat 清空,自然消失,不留痕。
113
+ * 同时承载 runner 的"重试 N/M…"进度(覆盖刷新,让用户一眼看出在第几次重试)。
114
+ */
115
+ transientError: string | null;
116
+ /** 用户按了 ESC/Ctrl+C 中断了任务(显示一行提示而不是红色错误) */
117
+ interrupted: boolean;
118
+ /** 当前插件循环状态(迭代进度) */
119
+ pluginProgress: PluginProgressState | null;
120
+ /** compact / clearHistory 触发的代号,作为 <Static key={}> remount 触发器 */
121
+ compactGeneration: number;
122
+ /** 当前阶段标签('idle' 时 LiveArea 隐藏) */
123
+ progressStage: ProgressStage;
124
+ /** 思维链流式预览(环形 buffer,保留最近 ~600 字符 / ~10 行) */
125
+ reasoningPreview: string;
126
+ /** 当前所有运行中工具(用数组而不是 Map,便于 React render;按 callId 顺序稳定) */
127
+ runningTools: ToolStatusState[];
128
+ /**
129
+ * 内核最近一次 yield 的真实上下文用量;启动后/尚未跑过一轮时为 null,
130
+ * StatusLine 此时降级回本地 useTokenUsage 估算。
131
+ */
132
+ contextUsage: ContextUsageInfo | null;
133
+ /** 默认关闭;只有 /computer takeover 明确开启后,后续回合才获得宿主上下文。 */
134
+ computerUseMode: ComputerUseCliMode;
135
+ setComputerUseMode: (mode: ComputerUseCliMode) => void;
136
+ /** 精确停止当前 TUI 回合的 Computer Use;不会影响其他 Session。 */
137
+ stopComputerUse: () => Promise<string>;
138
+ pendingComputerIntervention: PendingHumanIntervention | null;
139
+ resumeComputerUse: () => Promise<string>;
140
+ cancelComputerUse: () => Promise<string>;
141
+ /** 当前激活的 session id(默认 'default')。多 session 切换用 */
142
+ activeSessionId: string;
143
+ /** 排队等待执行的输入数量 */
144
+ queuedCount: number;
145
+ /** 排队中的输入项(只读快照,用于UI显示队列列表) */
146
+ queuedItems: readonly import('../../messageQueue.js').QueuedInput[];
147
+ /** 提交一次用户输入 */
148
+ submit: (input: string) => Promise<void>;
149
+ /** 中断当前执行 */
150
+ abort: () => void;
151
+ /** 清空对话上下文(保留 system prompt);isLoading 时无效 */
152
+ clearHistory: () => void;
153
+ /** 手动压缩对话上下文(不看阈值);isLoading 时无效 */
154
+ compactNow: () => Promise<void>;
155
+ /**
156
+ * `/advisor ask` 手动咨询顾问模型。与工具路径共用过程显示(LiveArea 进度行)
157
+ * 并可被 ESC 中断;isLoading 时拒绝执行并返回带原因的失败结果。
158
+ */
159
+ runManualAdvisor: (note?: string) => Promise<ToolResult>;
160
+ /**
161
+ * v2 新增:切换到指定 session(同 cwd 下)。
162
+ * 1. 保存当前 history 到当前 session 文件
163
+ * 2. 写 active-sessions.json 标记新 active id
164
+ * 3. 加载新 session 的历史(如不存在则新建空 history 含 fresh system prompt)
165
+ * 4. 触发 <Static> remount 让 MessageList 重画
166
+ * isLoading 时拒绝执行。
167
+ *
168
+ * `opts.force === true` 时即便 id 等于当前 active 也强制重新加载(/restore 用)。
169
+ */
170
+ switchSession: (id: string, opts?: { force?: boolean }) => Promise<void>;
171
+ /** Save the current Session and load a Session owned by another Workspace. */
172
+ switchWorkspace: (cwd: string, sessionId: string) => Promise<boolean>;
173
+ /**
174
+ * v2.x 新增:用 system+_ephemeral 形式把临时提示落进 scrollback。
175
+ *
176
+ * 替代直接 process.stdout.write 旁路 Ink(slash 命令输出等)。
177
+ * Ink 的 <Static> 会按 normal commit 流程把该消息推进 scrollback,
178
+ * 既显示给用户,又不破坏 log-update 的 previousLineCount 计数。
179
+ *
180
+ * - 持久化时 saveContext 会过滤掉这些 ephemeral 消息(不污染历史文件)
181
+ * - MessageList 识别 _ephemeral=true 走暗灰单行样式
182
+ * - 已知限制:本期不过滤 LLM 入参,短消息无害;下版本再做
183
+ */
184
+ ephemeralLine: (text: string) => void;
185
+ /**
186
+ * 当前待用户回答的提问(AskUserQuestion 工具触发);null = 无。
187
+ * 非 null 时 App 渲染 <AskQuestion> 独占输入焦点(不渲染 InputBox)。
188
+ */
189
+ pendingAsk: AskQuestionSpec[] | null;
190
+ /** 当前提问的来源;子任务提问时 kind:'subagent' + description,供 AskQuestion 显示徽标。 */
191
+ pendingAskSource: AskSource | null;
192
+ /** 同一所有者尚未展示的排队问题数。 */
193
+ pendingAskQueueCount: number;
194
+ /** 回答当前提问 → resolve 工具侧 Promise,让主循环 / workflow 继续。 */
195
+ answerAsk: (answers: AskAnswer[]) => void;
196
+ /** 取消当前提问;子任务来源只取消该子任务。 */
197
+ cancelAsk: () => void;
198
+ /** 切换当前使用的 provider(/model 命令使用);立即生效,后续请求使用新 provider。 */
199
+ setProvider: (provider: Provider) => void;
200
+ /** 获取当前 provider ref(用于状态行显示最新模型名)。 */
201
+ getCurrentProvider: () => Provider;
202
+ }
203
+
204
+ /**
205
+ * reasoningPreview 环形 buffer 容量(字符)。
206
+ * LiveArea 现在多行显示思维链尾部(最近 N 行),需要喂饱足够文本;
207
+ * 2400 字符约 30~40 行中文,LiveArea wrapToDisplayLines 后再取尾部最近 N 行。
208
+ * 上界由 LiveArea 的 contentMaxLines 兜底,这里只管"留够素材"。
209
+ */
210
+ const REASONING_PREVIEW_MAX_CHARS = 2400;
211
+
212
+ /**
213
+ * streamingText 环形 buffer 容量(字符)—— 仅用于 live 区"答案流式预览"。
214
+ *
215
+ * ★ 安全性:完整答案由 loop.ts 独立组装进 history(loop.ts 的 assistantText →
216
+ * assistantMsg.content,整条 push),streamingText 是**纯预览**,环形截断不丢
217
+ * 最终结果(落定后 <Static> 渲染完整 message.content 接管)。
218
+ * 截断保证长答案流式时 LiveArea 只需 wrap 末段、render 成本恒定。
219
+ */
220
+ const STREAM_PREVIEW_MAX_CHARS = 1500;
221
+
222
+ /**
223
+ * Streaming text setState 节流参数。
224
+ *
225
+ * Why throttle:LLM 流式输出 60+ chunk/秒,原本每个 delta 立即 setStreamingText
226
+ * 在 Windows PowerShell 终端下 Ink ANSI cursor 重绘跟不上,导致每个字打成新行
227
+ * (已确认根因,Plan agent 验证过)。这里用 leading-edge throttle:首个 delta
228
+ * 启动 100ms timer,期间所有 delta 累积到 ref,到点 flush 一次 setStreamingText。
229
+ *
230
+ * STREAM_FLUSH_MS : 100ms 节流(React rerender 频率 ≤ 10 Hz,叠加 Ink 内部
231
+ * 32ms throttle 后实际重画 ≤ 10 Hz,Windows 终端能跟上)
232
+ * STREAM_FLUSH_MAX_CHARS : 缓冲超 200 字符立即 flush(防长段中文 100ms 内塞太
233
+ * 多触发 React 大量 reconcile)
234
+ *
235
+ * Why not debounce:debounce 在 token 持续到达时永远刷新定时器,流式期间可能
236
+ * 完全不更新 UI,等于"看不见"。leading-edge throttle 保证稳定 10 Hz 出现新内容。
237
+ */
238
+ const STREAM_FLUSH_MS = 100;
239
+ const STREAM_FLUSH_MAX_CHARS = 200;
240
+
241
+ /** 按字符尾部截断(多字节字符 JS 字符串以 UTF-16 code unit 计,对中文已足够"差不多")。 */
242
+ function sliceLatest(s: string, maxChars: number): string {
243
+ if (s.length <= maxChars) return s;
244
+ return s.slice(s.length - maxChars);
245
+ }
246
+
247
+ function useThrottledFlush({
248
+ delayMs,
249
+ maxChars,
250
+ flushEmpty,
251
+ onFlush,
252
+ }: {
253
+ delayMs: number;
254
+ maxChars?: number;
255
+ flushEmpty?: boolean;
256
+ onFlush: (buffered: string) => void;
257
+ }) {
258
+ const bufferRef = useRef<string>('');
259
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
260
+
261
+ const flush = useCallback(() => {
262
+ if (timerRef.current) {
263
+ clearTimeout(timerRef.current);
264
+ timerRef.current = null;
265
+ }
266
+ const buffered = bufferRef.current;
267
+ bufferRef.current = '';
268
+ if (buffered.length === 0 && !flushEmpty) return;
269
+ onFlush(buffered);
270
+ }, [flushEmpty, onFlush]);
271
+
272
+ const cancel = useCallback(() => {
273
+ if (timerRef.current) {
274
+ clearTimeout(timerRef.current);
275
+ timerRef.current = null;
276
+ }
277
+ bufferRef.current = '';
278
+ }, []);
279
+
280
+ const schedule = useCallback(() => {
281
+ if (timerRef.current !== null) return;
282
+ timerRef.current = setTimeout(flush, delayMs);
283
+ }, [delayMs, flush]);
284
+
285
+ const append = useCallback(
286
+ (delta: string) => {
287
+ bufferRef.current += delta;
288
+ if (typeof maxChars === 'number' && bufferRef.current.length >= maxChars) {
289
+ flush();
290
+ return;
291
+ }
292
+ schedule();
293
+ },
294
+ [flush, maxChars, schedule],
295
+ );
296
+
297
+ return useMemo(
298
+ () => ({ append, flush, cancel, schedule }),
299
+ [append, flush, cancel, schedule],
300
+ );
301
+ }
302
+
303
+ export function useChat(args: {
304
+ provider: Provider;
305
+ initialHistory: Message[];
306
+ /**
307
+ * 压缩完成后被回调,供 UI 层清屏。
308
+ */
309
+ onCompactDone?: () => void;
310
+ }): UseChatResult {
311
+ // 历史用 ref(可变),用 version 触发 re-render
312
+ const historyRef = useRef<Message[]>(args.initialHistory.slice());
313
+ const [version, setVersion] = useState(0);
314
+ const bump = useCallback(() => setVersion((v) => v + 1), []);
315
+
316
+ // provider 用 ref 存储,支持运行时切换(/model 命令切换基座模型)
317
+ const providerRef = useRef<Provider>(args.provider);
318
+
319
+ const [streamingText, setStreamingText] = useState('');
320
+ const [isLoading, setIsLoading] = useState(false);
321
+ const [error, setError] = useState<string | null>(null);
322
+ const [transientError, setTransientError] = useState<string | null>(null);
323
+ const [interrupted, setInterrupted] = useState(false);
324
+
325
+ /**
326
+ * 暂态错误状态机的跨事件可变上下文(用 ref,不需要为它单独重渲染)。
327
+ * - activeStepId:当前正在执行的 workflow step id(workflow_step_start 记下,
328
+ * 该 step 的 workflow_step_end 清除);null = 不在任何 step 内。
329
+ * - pendingLlmError:该 step 执行中捕获到的最近一次 llm_error 文案(暂存,
330
+ * 等 step 结束才决定丢弃 or 提升为永久红字)。
331
+ * handleEvent 通过 setters.workflowCtx 同步读写这个对象(pure 逻辑,测试可注入 stub)。
332
+ */
333
+ const workflowCtxRef = useRef<{
334
+ activeStepId: string | null;
335
+ pendingLlmError: string | null;
336
+ }>({ activeStepId: null, pendingLlmError: null });
337
+ // 工作流步骤 id → 人话进度名(title)映射:start 记入、end/skipped 回查,
338
+ // 让 scrollback 三种进度行(▶/✓/⊘)都显示同一人话名而非 id。跨事件持 ref。
339
+ const stepTitlesRef = useRef<Map<string, string>>(new Map());
340
+ const [pluginProgress, setPluginProgress] =
341
+ useState<PluginProgressState | null>(null);
342
+
343
+ // ---- 工作过程显示相关 state ----
344
+ const [compactGeneration, setCompactGeneration] = useState(0);
345
+ const [progressStage, setProgressStage] = useState<ProgressStage>('idle');
346
+ const [reasoningPreview, setReasoningPreview] = useState('');
347
+ /**
348
+ * 内核每轮 yield 的真实上下文用量(含 tools-schema 开销 + 真实 provider usage,
349
+ * 见 loop.ts 的 context_usage 事件)。收到过至少一次前 StatusLine 用它渲染,
350
+ * 比 useTokenUsage 的本地估算更准;未收到过(如刚启动、纯只读问答走了只读快速
351
+ * 路径)时该值仍是 null,StatusLine 降级回 useTokenUsage 估算。
352
+ */
353
+ const [contextUsage, setContextUsage] = useState<ContextUsageInfo | null>(null);
354
+ const [computerUseMode, setComputerUseMode] =
355
+ useState<ComputerUseCliMode>('disabled');
356
+ const promptToolSignatureRef = useRef('');
357
+ const activeComputerRunRef = useRef<ComputerUseRunContext | null>(null);
358
+ const [computerInterventions] = useState(() => new HumanInterventionCoordinator());
359
+ const [pendingComputerIntervention, setPendingComputerIntervention] =
360
+ useState<PendingHumanIntervention | null>(null);
361
+ useEffect(() => computerInterventions.subscribe((event) => {
362
+ if (event.type === 'started') {
363
+ setPendingComputerIntervention(event.intervention);
364
+ } else {
365
+ setPendingComputerIntervention((current) =>
366
+ current?.id === event.resolution.intervention.id ? null : current);
367
+ }
368
+ }), [computerInterventions]);
369
+ /**
370
+ * 工具并行执行:以 callId 为 key 保存所有运行中工具。
371
+ * 用 Map 是为了 O(1) 增删;对外 memo 物化成稳定数组(Map 迭代顺序 = 插入顺序,
372
+ * 给 React 渲染天然稳定)。
373
+ */
374
+ const [runningToolsMap, setRunningToolsMap] = useState<
375
+ Map<string, ToolStatusState>
376
+ >(new Map());
377
+ const runningTools = useMemo(
378
+ () => Array.from(runningToolsMap.values()),
379
+ [runningToolsMap],
380
+ );
381
+ const abortRef = useRef<AbortController | null>(null);
382
+
383
+ const [inputQueue] = useState(() => createInputQueue());
384
+ const [queuedCount, setQueuedCount] = useState(0);
385
+ const [queuedItems, setQueuedItems] = useState<readonly import('../../messageQueue.js').QueuedInput[]>([]);
386
+ const runningToolsRef = useRef<Map<string, { toolName: string; isCancelable: boolean }>>(new Map());
387
+
388
+ useEffect(() => {
389
+ return inputQueue.subscribe(() => {
390
+ setQueuedCount(inputQueue.size());
391
+ setQueuedItems(inputQueue.getSnapshot());
392
+ });
393
+ }, [inputQueue]);
394
+
395
+ // ---- AskUserQuestion:执行中向用户提问 ----
396
+ // pendingAsk 非 null 时 App 渲染 <AskQuestion> 独占输入(不渲染 InputBox)。
397
+ // 单槽状态机(resolve/reject 持有、abort 归属校验、悬挂防护)抽在
398
+ // askSlot.ts(纯函数可单测);这里只桥接 React 状态。
399
+ const [pendingAsk, setPendingAsk] = useState<AskQuestionSpec[] | null>(null);
400
+ // 提问来源:子任务提问时 kind:'subagent' + description → AskQuestion 显示徽标。
401
+ const [pendingAskSource, setPendingAskSource] = useState<AskSource | null>(null);
402
+ const [pendingAskQueueCount, setPendingAskQueueCount] = useState(0);
403
+ const [askSlot] = useState(() =>
404
+ createAskSlot((questions, source) => {
405
+ setPendingAsk(questions);
406
+ setPendingAskSource(questions ? source ?? null : null);
407
+ }),
408
+ );
409
+
410
+ const streamingFlush = useThrottledFlush({
411
+ delayMs: STREAM_FLUSH_MS,
412
+ maxChars: STREAM_FLUSH_MAX_CHARS,
413
+ onFlush: useCallback((buffered: string) => {
414
+ setStreamingText((prev) =>
415
+ sliceLatest(prev + buffered, STREAM_PREVIEW_MAX_CHARS),
416
+ );
417
+ }, []),
418
+ });
419
+
420
+ // ---- reasoning preview setState 节流 ----
421
+ // 用户反馈思考阶段需要看到流式思维链。reasoning delta 频率跟 text delta 一样高
422
+ // (60+ chunk/秒),不节流的话每个 delta 立即 setReasoningPreview → LiveArea
423
+ // rerender → Ink 重绘扛不住。复用 streaming 一样的 100ms / 200 字符节流参数。
424
+ const reasoningFlush = useThrottledFlush({
425
+ delayMs: STREAM_FLUSH_MS,
426
+ maxChars: STREAM_FLUSH_MAX_CHARS,
427
+ onFlush: useCallback((buffered: string) => {
428
+ setReasoningPreview((prev) =>
429
+ sliceLatest(prev + buffered, REASONING_PREVIEW_MAX_CHARS),
430
+ );
431
+ }, []),
432
+ });
433
+
434
+ // ---- tool_start/end 的 bump 节流 ----
435
+ // tool_start / tool_end 单独触发 bump 会导致每个工具事件都是一次顶层 rerender;
436
+ // 200ms 内合并成单次 bump,避免高频"工具状态切换"压垮 Ink ANSI 重绘。
437
+ // 注意:assistant_message 走立即 bump(不节流),因为 history 增长时 Static 需要
438
+ // 立即 commit 新 item,不能延迟。
439
+ const toolBumpFlush = useThrottledFlush({
440
+ delayMs: 200,
441
+ flushEmpty: true,
442
+ onFlush: useCallback(() => {
443
+ bump();
444
+ }, [bump]),
445
+ });
446
+
447
+ /**
448
+ * 把 turn 结束 / 切换 session / clearHistory 时的"工作过程显示 state
449
+ * 归位"集中成一个函数。React 18 自动 batch 这一组 setState(同一个 microtask 内
450
+ * 调用),结果就是 1 次 commit。集中后调用点更清晰,也方便后续再优化。
451
+ */
452
+ const resetTurnState = useCallback(() => {
453
+ setStreamingText('');
454
+ setProgressStage('idle');
455
+ setReasoningPreview('');
456
+ setRunningToolsMap(new Map());
457
+ // 暂态错误是"过程级"提示,turn 结束 / 切换时一并清掉,不残留到下一轮
458
+ setTransientError(null);
459
+ workflowCtxRef.current = { activeStepId: null, pendingLlmError: null };
460
+ }, []);
461
+
462
+ /** turn 结束 / 中断 / 切换时一并取消所有节流 buffer 的 pending timer。 */
463
+ const cancelAllThrottles = useCallback(() => {
464
+ streamingFlush.cancel();
465
+ reasoningFlush.cancel();
466
+ toolBumpFlush.cancel();
467
+ }, [streamingFlush, reasoningFlush, toolBumpFlush]);
468
+
469
+ // ---- v2 multi-session ----
470
+ // activeSessionId 启动时同步默认 'default';mount 后异步读 active-sessions.json
471
+ // 更新为该 cwd 真正激活的 session。所有持久化路径都按 ref 取最新值,
472
+ // ref 比 state 顺手——switchSession 内同步切换 ref 后下一行立刻 saveContext 能用新值。
473
+ const activeSessionIdRef = useRef<string>('default');
474
+ const [activeSessionId, setActiveSessionId] = useState<string>('default');
475
+
476
+ /** 当前 active session 对应的持久化文件路径(同步,按 ref 取) */
477
+ const sessionFile = useCallback(() => {
478
+ return sessionFileFor(getWorkingDir(), activeSessionIdRef.current);
479
+ }, []);
480
+
481
+ // 卸载时取消任何进行中的请求
482
+ useEffect(() => {
483
+ return () => {
484
+ abortRef.current?.abort();
485
+ };
486
+ }, []);
487
+
488
+ // mount 时把「弹选择界面」的处理器注册到 askBridge;unmount 注销。
489
+ // 两层组合:createAskCoordinator(FIFO 队列——主循环与并行 general 子 Agent 都可能
490
+ // 提问,用户一次只看一个问题,其余排队;实例按 TUI 挂载创建,不同进程互不阻塞)
491
+ // 包住 askSlot.handler(呈现层单槽 + abort 归属校验——同轮多次提问共享 turn signal,
492
+ // 归属校验保证已答完提问的 abort 监听器不会清掉后来提问的槽,否则后者 promise
493
+ // 永不落定 → 整轮死挂,见 askSlot.ts 头注)。
494
+ useEffect(() => {
495
+ setAskHandler(createAskCoordinator(askSlot.handler, (status) => {
496
+ setPendingAskQueueCount(status.queuedCount ?? 0);
497
+ }));
498
+ return () => setAskHandler(null);
499
+ }, [askSlot]);
500
+
501
+ /** 用户答完当前提问:清 pending + resolve 工具侧 Promise,执行继续。 */
502
+ const answerAsk = useCallback(
503
+ (answers: AskAnswer[]) => askSlot.answer(answers),
504
+ [askSlot],
505
+ );
506
+
507
+ const cancelAsk = useCallback(() => {
508
+ if (pendingAskSource?.kind === 'subagent') askSlot.cancel();
509
+ else abortRef.current?.abort();
510
+ }, [askSlot, pendingAskSource]);
511
+
512
+ // Root 已按 active session 加载初始历史;这里同步同一 active id 给交互状态。
513
+ useEffect(() => {
514
+ let cancelled = false;
515
+ void (async () => {
516
+ try {
517
+ const id = await getActiveSession(getWorkingDir());
518
+ if (cancelled) return;
519
+ activeSessionIdRef.current = id;
520
+ setActiveSessionId(id);
521
+ } catch {
522
+ // 读失败保持 default,不影响功能
523
+ }
524
+ })();
525
+ return () => {
526
+ cancelled = true;
527
+ };
528
+ }, []);
529
+
530
+ /**
531
+ * 最新 submit 的 latest-ref。runManualAdvisor 定义在 submit 之前(它不能依赖
532
+ * submit 的闭包),但结束时要把排队输入交回去执行 —— 用 ref 打破定义顺序,
533
+ * 而不是把两者的定义顺序倒过来(那会让 submit 依赖 runManualAdvisor,更绕)。
534
+ */
535
+ const submitRef = useRef<((input: string) => Promise<void>) | null>(null);
536
+
537
+ /**
538
+ * `/advisor ask` 的执行体:让手动咨询和工具咨询共用同一套过程显示。
539
+ *
540
+ * 此前 App.tsx 里是裸 `await runAdvisor(...)` —— 顾问最长跑 180 秒,这期间 TUI
541
+ * 一个字都不动,也按不停。工具路径早就修过同款问题(advisor.ts 的 reportProgress
542
+ * 节流发布,源于 2026-07「2m05s 无变化被误判死机」的真实反馈),手动路径却没跟上:
543
+ * 同一个功能两条路径,用户**主动发起**的那条体验反而更差。
544
+ *
545
+ * 复用既有的 runningTools + progressStage(不新增 UI 概念):塞一个运行中条目给
546
+ * LiveArea,进度行由 runAdvisor 的 reportProgress 驱动;挂 abortRef 让 ESC/Ctrl+C
547
+ * 真能掐断,而不是干等到超时。
548
+ */
549
+ const runManualAdvisor = useCallback(
550
+ async (note?: string): Promise<ToolResult> => {
551
+ if (isLoading) {
552
+ return { ok: false, error: '当前有任务在执行;等它结束或按 ESC 中断后再咨询顾问。' };
553
+ }
554
+ const callId = 'manual-advisor';
555
+ const ac = new AbortController();
556
+ abortRef.current = ac;
557
+ setIsLoading(true);
558
+ setProgressStage('tool_executing');
559
+ setRunningToolsMap(new Map([[callId, {
560
+ toolName: 'Advisor',
561
+ argsPreview: note ?? '',
562
+ status: 'running' as const,
563
+ argsFriendly: '正在咨询顾问模型',
564
+ }]]));
565
+ try {
566
+ return await runAdvisor(
567
+ note ? { note } : {},
568
+ ac.signal,
569
+ {
570
+ history: historyRef.current,
571
+ advisorCalls: { used: 0, max: 1 },
572
+ advisorInvocation: 'manual',
573
+ reportProgress: (message) => {
574
+ // advisor.ts 侧已有 200ms 节流,这里直接落状态即可。
575
+ setRunningToolsMap((prev) => {
576
+ const current = prev.get(callId);
577
+ if (!current) return prev;
578
+ return new Map(prev).set(callId, { ...current, progress: message });
579
+ });
580
+ },
581
+ },
582
+ );
583
+ } finally {
584
+ setIsLoading(false);
585
+ setProgressStage('idle');
586
+ setRunningToolsMap(new Map());
587
+ abortRef.current = null;
588
+ // ⚠️ 必须消费排队输入。设了 isLoading 之后,用户在咨询期间敲的内容会被
589
+ // submit 送进 inputQueue,而队列此前**只在 submit 自己的 finally 里**出队
590
+ // (见下方 consumeQueued)—— 手动咨询不走那条路径,排队的话就会静静躺着,
591
+ // 用户看到「排队 1 条」却永远不执行,直到下次提交才错序跑掉。
592
+ const queued = inputQueue.dequeue();
593
+ if (queued) queueMicrotask(() => { void submitRef.current?.(queued.value); });
594
+ }
595
+ },
596
+ [isLoading, inputQueue],
597
+ );
598
+
599
+ const submit = useCallback(
600
+ async (input: string) => {
601
+ const trimmed = input.trim();
602
+ if (trimmed.length === 0) return;
603
+
604
+ if (isLoading) {
605
+ inputQueue.enqueue(trimmed, 'next');
606
+ const running = Array.from(runningToolsRef.current.values());
607
+ const allCancelable = running.length > 0 && running.every(t => t.isCancelable);
608
+ if (allCancelable) {
609
+ abortRef.current?.abort('interrupt');
610
+ }
611
+ bump();
612
+ return;
613
+ }
614
+
615
+ setIsLoading(true);
616
+ setError(null);
617
+ setTransientError(null);
618
+ setInterrupted(false);
619
+ cancelAllThrottles();
620
+ setStreamingText('');
621
+ setPluginProgress(null);
622
+ // 进入新一轮,把工作过程显示相关 state 全部归位
623
+ setProgressStage('idle');
624
+ setReasoningPreview('');
625
+ setRunningToolsMap(new Map());
626
+ // 重置暂态错误状态机(防上一轮工作流残留)
627
+ workflowCtxRef.current = { activeStepId: null, pendingLlmError: null };
628
+
629
+ const ac = new AbortController();
630
+ abortRef.current = ac;
631
+ let releaseContextLock: (() => Promise<void>) | null = null;
632
+ let turnJournal: TurnJournal | null = null;
633
+ let turnJournalStart = 0;
634
+ dlog('ui', 'turn START', { input: trimmed.slice(0, 80) });
635
+
636
+ // 长时间运行时(如工作流 agent 步骤)的定时 token 刷新:
637
+ // runQuery 的 text/reasoning 事件不触发 bump,导致 StatusLine token 数
638
+ // 可能冻结数十秒。此定时器每 10s 强制刷新一次,确保用户看到上下文增长。
639
+ const tokenRefreshTimer = setInterval(() => {
640
+ bump();
641
+ }, 10_000);
642
+
643
+ try {
644
+ await updateSessionMetadata(getWorkingDir(), activeSessionIdRef.current, {
645
+ lastMessagePreview: trimmed,
646
+ }).catch(() => {});
647
+ const activeFile = sessionFileFor(getWorkingDir(), activeSessionIdRef.current);
648
+ releaseContextLock = await acquireContextLock(activeFile, ac.signal);
649
+ // Gateway/Cron may have written this cwd since the TUI rendered its last frame.
650
+ // Reload after acquiring the cross-process lock so this turn extends the newest history.
651
+ const latest = await loadContext(activeFile);
652
+ if (latest) {
653
+ const ephemeral = historyRef.current.filter((message) => message.role === 'system' && message._ephemeral);
654
+ historyRef.current.length = 0;
655
+ historyRef.current.push(...latest, ...ephemeral);
656
+ }
657
+ const workspace = await workspaceIndexService.findByPath(getWorkingDir()).catch(() => null);
658
+ const computerRun =
659
+ computerUseMode !== 'disabled'
660
+ ? createCliComputerRun({
661
+ mode: computerUseMode,
662
+ surface: 'tui',
663
+ conversationKey: `workspace:${workspace?.id ?? getWorkingDir()}:session:${activeSessionIdRef.current}`,
664
+ userInput: trimmed,
665
+ allowedFileRoots: [getWorkingDir()],
666
+ supervised: true,
667
+ })
668
+ : undefined;
669
+ activeComputerRunRef.current = computerRun ?? null;
670
+ const computerIntervention = computerRun
671
+ ? createComputerInterventionAdapter({
672
+ coordinator: computerInterventions,
673
+ context: computerRun,
674
+ observe: async (signal) => {
675
+ // Resume is a canonical boundary: it invalidates every old
676
+ // coordinate/AX reference and clears the native takeover
677
+ // cancellation for this run only.
678
+ const response = await computerUseService.request(
679
+ computerRun,
680
+ { action: 'observe' },
681
+ signal,
682
+ );
683
+ return response.ok ? response.state : undefined;
684
+ },
685
+ })
686
+ : undefined;
687
+ const toolGate = computerRun ? createComputerUseHostGate(computerRun) : undefined;
688
+ const turnSessionState = getOrCreateSessionState(sessionStateKey({
689
+ surface: 'tui', workspaceId: workspace?.id,
690
+ cwd: workspace?.id ? undefined : getWorkingDir(),
691
+ sessionId: activeSessionIdRef.current,
692
+ }));
693
+ await hydrateSessionWorktree({
694
+ sessionState: turnSessionState, workspaceId: workspace?.id,
695
+ sessionId: activeSessionIdRef.current, cwd: getWorkingDir(),
696
+ });
697
+ const effectiveCwd = turnSessionState.activeWorktree?.path ?? getWorkingDir();
698
+ const runtimeContext = createAgentRuntimeContext({
699
+ surface: 'tui',
700
+ profile: 'main',
701
+ cwd: effectiveCwd,
702
+ workspaceId: workspace?.id,
703
+ sessionId: activeSessionIdRef.current,
704
+ owner: 'local-owner',
705
+ memoryAccess: 'read-write',
706
+ interactiveAsk: true,
707
+ computer: computerRun,
708
+ computerIntervention,
709
+ toolGate,
710
+ });
711
+ const turnTools = filterToolsForRuntime(ALL_TOOLS, runtimeContext);
712
+ const toolSignature = `${effectiveCwd}\0${turnTools.map((tool) => tool.name).join(',')}`;
713
+ if (promptToolSignatureRef.current !== toolSignature) {
714
+ const stablePrompt = await buildBaseSystemPrompt(effectiveCwd, turnTools);
715
+ const rest = historyRef.current[0]?.role === 'system'
716
+ ? historyRef.current.slice(1)
717
+ : [...historyRef.current];
718
+ historyRef.current.length = 0;
719
+ historyRef.current.push({ role: 'system', content: stablePrompt }, ...rest);
720
+ promptToolSignatureRef.current = toolSignature;
721
+ }
722
+ // 每回合只刷新易变层;稳定层仅在实际能力目录变化时重建。
723
+ const turnTransient = await buildTransientPromptBundle({
724
+ cwd: effectiveCwd,
725
+ tools: turnTools,
726
+ runtime: {
727
+ surface: 'tui',
728
+ cwd: effectiveCwd,
729
+ currentUserInput: trimmed,
730
+ workspaceId: workspace?.id,
731
+ sessionId: activeSessionIdRef.current,
732
+ interactiveAsk: true,
733
+ supervisedBrowser: false,
734
+ memoryAccess: 'read-write',
735
+ },
736
+ }).catch(() => undefined);
737
+ // 会话原文记账(与 session/runTurn.ts 共用同一份实现)。
738
+ // TUI 不经过 runTurn,此前整条路径都不入库,SearchConversations 对 TUI 用户静默失效。
739
+ turnJournalStart = historyRef.current.length;
740
+ turnJournal = await startTurnJournal({
741
+ memoryContext: memoryContextFromRuntime(runtimeContext),
742
+ userInput: trimmed,
743
+ }).catch(() => null);
744
+ for await (const ev of runWithPlugins(trimmed, {
745
+ provider: providerRef.current,
746
+ history: historyRef.current,
747
+ signal: ac.signal,
748
+ runtimeContext,
749
+ allowedTools: turnTools.map((tool) => tool.name),
750
+ sessionState: turnSessionState,
751
+ parseInlineImages: true,
752
+ ...(turnTransient ? { transientBundle: turnTransient } : {}),
753
+ promptCache: createPromptCacheContext(
754
+ `workspace:${workspace?.id ?? getWorkingDir()}:session:${activeSessionIdRef.current}`,
755
+ ),
756
+ hasQueuedInput: () => inputQueue.size() > 0,
757
+ memoryContext: memoryContextFromRuntime(runtimeContext),
758
+ ...(computerRun ? { computerRun } : {}),
759
+ ...(computerIntervention ? { computerIntervention } : {}),
760
+ ...(toolGate ? { toolGate } : {}),
761
+ onToolStart: (name, id) => {
762
+ const tool = getToolByName(name);
763
+ runningToolsRef.current.set(id, {
764
+ toolName: name,
765
+ isCancelable: tool?.interruptBehavior?.() === 'cancel',
766
+ });
767
+ },
768
+ onToolEnd: (id) => {
769
+ runningToolsRef.current.delete(id);
770
+ },
771
+ })) {
772
+ handleEvent(ev, {
773
+ setStreamingText,
774
+ setError,
775
+ setInterrupted,
776
+ setPluginProgress,
777
+ setCompactGeneration,
778
+ setProgressStage,
779
+ setReasoningPreview,
780
+ setRunningToolsMap,
781
+ setContextUsage,
782
+ // 暂态错误(工作流步内 llm_error / 重试进度)→ LiveArea 黄字
783
+ setTransientError,
784
+ workflowCtx: workflowCtxRef.current,
785
+ // 步骤 id→人话名映射:scrollback 进度行三态(▶/✓/⊘)统一显示 title
786
+ stepTitles: stepTitlesRef.current,
787
+ // 节流相关 —— buffer 累积 + 100ms timer flush
788
+ appendStreamingDelta: streamingFlush.append,
789
+ flushStreamingBuffer: streamingFlush.flush,
790
+ cancelStreamingFlush: streamingFlush.cancel,
791
+ // reasoning preview 节流 + 落定清空
792
+ appendReasoningDelta: reasoningFlush.append,
793
+ cancelReasoningFlush: reasoningFlush.cancel,
794
+ // tool_start/end 节流 bump
795
+ scheduleToolBump: toolBumpFlush.schedule,
796
+ cancelToolBump: toolBumpFlush.cancel,
797
+ bump,
798
+ // 工作流进度逐步可见(持久暗色行)
799
+ emitProgressLine: ephemeralLine,
800
+ });
801
+ }
802
+ } catch (e) {
803
+ setError(`未捕获异常:${(e as Error).message}`);
804
+ dlog('ui', 'turn THREW', { err: (e as Error).message });
805
+ } finally {
806
+ dlog('ui', 'turn END (finally)');
807
+ clearInterval(tokenRefreshTimer);
808
+ setIsLoading(false);
809
+ cancelAllThrottles();
810
+ resetTurnState();
811
+ setPluginProgress(null);
812
+ abortRef.current = null;
813
+ if (activeComputerRunRef.current) {
814
+ computerInterventions.abort(activeComputerRunRef.current);
815
+ }
816
+ activeComputerRunRef.current = null;
817
+ runningToolsRef.current.clear();
818
+ // 记账失败不影响落盘与解锁(它是附加能力,不是功能)。
819
+ await turnJournal?.finish(historyRef.current, turnJournalStart).catch(() => undefined);
820
+ turnJournal = null;
821
+ // v2 multi-session:持久化到当前 active session 文件。
822
+ try {
823
+ await saveContext(historyRef.current, sessionFileFor(getWorkingDir(), activeSessionIdRef.current));
824
+ } finally {
825
+ await releaseContextLock?.();
826
+ }
827
+
828
+ const consumeQueued = () => {
829
+ const next = inputQueue.dequeue();
830
+ if (!next) return;
831
+ queueMicrotask(() => {
832
+ void submit(next.value);
833
+ });
834
+ };
835
+ consumeQueued();
836
+ }
837
+ },
838
+ [
839
+ bump,
840
+ isLoading,
841
+ sessionFile,
842
+ streamingFlush,
843
+ cancelAllThrottles,
844
+ reasoningFlush,
845
+ toolBumpFlush,
846
+ resetTurnState,
847
+ inputQueue,
848
+ computerUseMode,
849
+ computerInterventions,
850
+ ],
851
+ );
852
+
853
+ // latest-ref 赋值:runManualAdvisor 的 finally 靠它把排队输入交回 submit。
854
+ // 渲染体内直接赋值是 latest-ref 的常规写法;用 useEffect 会晚一帧,而咨询
855
+ // 结束时机与渲染无关,晚一帧就可能取到上一轮的闭包。
856
+ submitRef.current = submit;
857
+
858
+ const abort = useCallback(() => {
859
+ inputQueue.clear();
860
+ runningToolsRef.current.clear();
861
+ if (activeComputerRunRef.current) {
862
+ computerInterventions.abort(activeComputerRunRef.current);
863
+ }
864
+ abortRef.current?.abort();
865
+ }, [computerInterventions, inputQueue]);
866
+
867
+ const stopComputerUse = useCallback(async (): Promise<string> => {
868
+ setComputerUseMode('disabled');
869
+ const context = activeComputerRunRef.current;
870
+ if (!context) return '已撤销后续回合授权;当前没有正在运行的 Computer Use。';
871
+ computerInterventions.abort(context);
872
+ const response = await computerUseService.request(context, { action: 'stop' });
873
+ return response.ok
874
+ ? '已停止当前回合的 Computer Use,并撤销后续回合授权。'
875
+ : `已撤销后续回合授权;原生停止失败:${response.error?.message ?? '未知错误'}`;
876
+ }, [computerInterventions]);
877
+
878
+ const resumeComputerUse = useCallback(async (): Promise<string> => {
879
+ const pending = pendingComputerIntervention;
880
+ if (!pending) return '当前没有等待人工处理的 Computer Use。';
881
+ const resumed = computerInterventions.resume({
882
+ id: pending.id,
883
+ resumeToken: pending.resumeToken,
884
+ ownerId: pending.ownerId,
885
+ conversationKey: pending.conversationKey,
886
+ runId: pending.runId,
887
+ });
888
+ return resumed
889
+ ? '已恢复原 Computer Use 运行,将先重新观察目标应用再继续。'
890
+ : '人工介入请求已失效,请重新观察目标应用。';
891
+ }, [computerInterventions, pendingComputerIntervention]);
892
+
893
+ const cancelComputerUse = useCallback(async (): Promise<string> => {
894
+ const pending = pendingComputerIntervention;
895
+ if (!pending) return stopComputerUse();
896
+ const cancelled = computerInterventions.cancel({
897
+ id: pending.id,
898
+ resumeToken: pending.resumeToken,
899
+ ownerId: pending.ownerId,
900
+ conversationKey: pending.conversationKey,
901
+ runId: pending.runId,
902
+ });
903
+ setComputerUseMode('disabled');
904
+ return cancelled
905
+ ? '已取消当前人工介入并停止该 Computer Use 任务。'
906
+ : '人工介入请求已失效。';
907
+ }, [computerInterventions, pendingComputerIntervention, stopComputerUse]);
908
+
909
+ const setProvider = useCallback((next: Provider) => {
910
+ providerRef.current = next;
911
+ }, []);
912
+
913
+ const getCurrentProvider = useCallback(() => providerRef.current, []);
914
+
915
+ /**
916
+ * v2.x 新增:把一段临时提示文本作为 _ephemeral 的 system 消息插入 history。
917
+ *
918
+ * 用途:替代 slash 命令分支直接 process.stdout.write 旁路 Ink 的写法
919
+ * (那种写法每次都让 ink log-update 的 previousLineCount 失步,
920
+ * 下一帧 eraseLines 会留下未擦行)。
921
+ *
922
+ * 流程:push 进 historyRef → bump() 触发 history useMemo 重算 →
923
+ * MessageList <Static> 把它作为新 item commit 进 scrollback。
924
+ * saveContext 写盘时过滤掉这些 ephemeral 消息。
925
+ */
926
+ const ephemeralLine = useCallback(
927
+ (text: string) => {
928
+ historyRef.current.push({
929
+ role: 'system',
930
+ content: text,
931
+ _ephemeral: true,
932
+ timestamp: Date.now(),
933
+ });
934
+ bump();
935
+ },
936
+ [bump],
937
+ );
938
+
939
+ /**
940
+ * /new —— 清空历史,重新生成 system prompt(带最新 skills + 项目指令)。
941
+ * 不复用启动时的初始 history 快照,避免把磁盘里的损坏数据带回来。
942
+ * isLoading 时拒绝执行,避免和正在跑的 runQuery 抢状态。
943
+ */
944
+ const clearHistory = useCallback(async () => {
945
+ if (isLoading) return;
946
+
947
+ // 生成全新的 system prompt(实时扫 skills + 拼项目指令),
948
+ // 与启动时 main.tsx buildInitialHistory 行为完全一致
949
+ const newSystemPrompt = await buildBaseSystemPrompt(getWorkingDir(), ALL_TOOLS);
950
+ historyRef.current.length = 0;
951
+ historyRef.current.push({ role: 'system', content: newSystemPrompt });
952
+ promptToolSignatureRef.current = '';
953
+
954
+ // 允许新 session 再次触发反应式压缩自救
955
+ resetReactiveCompactState();
956
+
957
+ cancelAllThrottles();
958
+ setError(null);
959
+ setInterrupted(false);
960
+ // 清空工作过程显示状态,避免 /new 后旧 turn 残留闪现
961
+ resetTurnState();
962
+ // 强制 MessageList 内 <Static> remount,避免旧消息卡在屏幕上
963
+ setCompactGeneration((g) => g + 1);
964
+ bump();
965
+
966
+ // 清空磁盘上的持久化文件(防止下次启动又加载损坏数据)
967
+ await clearContext();
968
+ // 清空 pre-read guard 状态表(防止跨 session 的 Read 记录污染新会话)
969
+ const workspace = await workspaceIndexService.findByPath(getWorkingDir()).catch(() => null);
970
+ clearFileState(getOrCreateSessionState(sessionStateKey({
971
+ surface: 'tui',
972
+ workspaceId: workspace?.id,
973
+ cwd: workspace?.id ? undefined : getWorkingDir(),
974
+ sessionId: activeSessionIdRef.current,
975
+ })).fileReads);
976
+ // 刚清空,不需要再保存一次"空历史"
977
+ }, [bump, isLoading, cancelAllThrottles, resetTurnState]);
978
+
979
+ /**
980
+ * /compact —— 手动跑一次压缩(不看阈值)。
981
+ * isLoading 时拒绝执行;压缩自身的 setIsLoading(true) 会让 InputBox 显示 ⏳。
982
+ *
983
+ * ✅ 关键改进:压缩失败时不保存 history(可能包含损坏数据)
984
+ */
985
+ const compactNow = useCallback(async () => {
986
+ if (isLoading) return;
987
+ setIsLoading(true);
988
+ // 手动压缩仍然把 progressStage 切到 compacting,给 LiveArea 显示用
989
+ setProgressStage('compacting');
990
+ setError(null);
991
+ // 压缩前清掉任何待 flush 的 streaming buffer(避免压缩中 timer 触发干扰)
992
+ cancelAllThrottles();
993
+ let success = false;
994
+ try {
995
+ const cwd = getWorkingDir();
996
+ const workspace = await workspaceIndexService.findByPath(cwd).catch(() => null);
997
+ const r = await forceCompact(
998
+ historyRef.current,
999
+ args.provider,
1000
+ undefined,
1001
+ undefined,
1002
+ createPromptCacheContext(
1003
+ `workspace:${workspace?.id ?? cwd}:session:${activeSessionIdRef.current}`,
1004
+ ),
1005
+ );
1006
+ historyRef.current.length = 0;
1007
+ historyRef.current.push(...r.messages);
1008
+ // 压缩后 MessageList 要 remount,丢掉旧 <Static> 缓存
1009
+ setCompactGeneration((g) => g + 1);
1010
+ bump();
1011
+ success = true; // ✅ 标记成功
1012
+ } catch (e) {
1013
+ setError(`手动压缩失败:${(e as Error).message}`);
1014
+ // ❌ 不修改 historyRef.current,保持原样(不保存可能损坏的数据)
1015
+ } finally {
1016
+ setProgressStage('idle');
1017
+ setIsLoading(false);
1018
+ // ✅ 只在成功时才持久化,避免保存损坏的历史到磁盘
1019
+ if (success) {
1020
+ // v2 multi-session:持久化到当前 active session 文件
1021
+ saveContext(historyRef.current, sessionFile()).catch(() => {});
1022
+ args.onCompactDone?.();
1023
+ }
1024
+ }
1025
+ }, [
1026
+ args.provider,
1027
+ args.onCompactDone,
1028
+ bump,
1029
+ isLoading,
1030
+ sessionFile,
1031
+ cancelAllThrottles,
1032
+ ]);
1033
+
1034
+ /**
1035
+ * /session <id> —— 切换到指定 session(同 cwd 下)。
1036
+ *
1037
+ * 流程:
1038
+ * 1) saveContext 保住当前 session 的最新历史
1039
+ * 2) setActiveSession 更新注册表
1040
+ * 3) 改 activeSessionIdRef + state(让 UI 显示 / 持久化路径都走新 id)
1041
+ * 4) resolveSessionFile + loadContext 读新 session 历史(不存在则新建空)
1042
+ * 5) 替换 historyRef.current(保留 system prompt 在头部)
1043
+ * 6) bump + setCompactGeneration 触发 Static remount
1044
+ *
1045
+ * isLoading 时拒绝执行,避免与 runQuery 抢状态。
1046
+ */
1047
+ const switchSession = useCallback(
1048
+ async (id: string, opts?: { force?: boolean }) => {
1049
+ if (isLoading) return;
1050
+ const trimmed = id.trim();
1051
+ if (trimmed.length === 0) return;
1052
+ // force=true 时即便同 id 也强制重新加载(/restore 覆盖文件后用)
1053
+ if (!opts?.force && trimmed === activeSessionIdRef.current) return;
1054
+
1055
+ const cwd = getWorkingDir();
1056
+
1057
+ // 1. 保住当前 session
1058
+ try {
1059
+ await saveContext(historyRef.current, sessionFileFor(cwd, activeSessionIdRef.current));
1060
+ } catch {
1061
+ // 保存失败不阻塞切换(用户可能就是想切走止损)
1062
+ }
1063
+
1064
+ // 2. 更新注册表
1065
+ try {
1066
+ await setActiveSession(cwd, trimmed);
1067
+ } catch {
1068
+ // 注册失败不阻塞切换
1069
+ }
1070
+
1071
+ // 3. 切 ref + state
1072
+ activeSessionIdRef.current = trimmed;
1073
+ setActiveSessionId(trimmed);
1074
+
1075
+ // 4. 加载新 session 历史(resolveSessionFile 支持 v1 → v2 路径兜底)
1076
+ let loaded: Message[] | null = null;
1077
+ try {
1078
+ const newPath = await resolveSessionFile(cwd, trimmed);
1079
+ loaded = await loadContext(newPath);
1080
+ } catch {
1081
+ loaded = null;
1082
+ }
1083
+
1084
+ // 5. 替换 historyRef,保留启动时的 system prompt 在头部
1085
+ const systemMsg = historyRef.current.find((m) => m.role === 'system');
1086
+ historyRef.current.length = 0;
1087
+ if (systemMsg) historyRef.current.push(systemMsg);
1088
+ if (loaded && loaded.length > 0) {
1089
+ // loaded 可能含旧 system prompt(旧版 last-context.json 保存了一份),跳过
1090
+ const rest = loaded[0]?.role === 'system' ? loaded.slice(1) : loaded;
1091
+ historyRef.current.push(...rest);
1092
+ }
1093
+
1094
+ // 6. 清掉中间态、触发 Static remount
1095
+ cancelAllThrottles();
1096
+ setError(null);
1097
+ setInterrupted(false);
1098
+ resetTurnState();
1099
+ setCompactGeneration((g) => g + 1);
1100
+ bump();
1101
+ },
1102
+ [bump, isLoading, cancelAllThrottles, resetTurnState],
1103
+ );
1104
+
1105
+ const switchWorkspace = useCallback(async (cwd: string, sessionId: string) => {
1106
+ if (isLoading) return false;
1107
+ const targetCwd = cwd.trim();
1108
+ const targetSessionId = sessionId.trim();
1109
+ if (!targetCwd || !targetSessionId) return false;
1110
+
1111
+ let loaded: Message[] | null = null;
1112
+ let targetExists = false;
1113
+ try {
1114
+ const targetFile = await resolveSessionFile(targetCwd, targetSessionId);
1115
+ try { targetExists = (await stat(targetFile)).isFile(); } catch { targetExists = false; }
1116
+ loaded = await loadContext(targetFile);
1117
+ if (targetExists && loaded === null) throw new Error('目标会话历史损坏或无法读取');
1118
+ } catch (error) {
1119
+ setError(error instanceof Error ? error.message : String(error));
1120
+ bump();
1121
+ return false;
1122
+ }
1123
+
1124
+ let systemPrompt: string;
1125
+ try { systemPrompt = await buildBaseSystemPrompt(targetCwd, ALL_TOOLS); }
1126
+ catch (error) {
1127
+ setError(`无法打开工作区:${error instanceof Error ? error.message : String(error)}`);
1128
+ bump();
1129
+ return false;
1130
+ }
1131
+
1132
+ const previousCwd = getWorkingDir();
1133
+ try {
1134
+ await saveContext(historyRef.current, sessionFileFor(previousCwd, activeSessionIdRef.current));
1135
+ } catch {
1136
+ // A damaged old workspace must not trap the user in it.
1137
+ }
1138
+
1139
+ try { await setActiveSession(targetCwd, targetSessionId); }
1140
+ catch (error) {
1141
+ setError(`无法切换 Session:${error instanceof Error ? error.message : String(error)}`);
1142
+ bump();
1143
+ return false;
1144
+ }
1145
+ setWorkingDir(targetCwd);
1146
+ activeSessionIdRef.current = targetSessionId;
1147
+ setActiveSessionId(targetSessionId);
1148
+
1149
+ historyRef.current.length = 0;
1150
+ historyRef.current.push({ role: 'system', content: systemPrompt });
1151
+ promptToolSignatureRef.current = '';
1152
+ if (loaded?.length) {
1153
+ historyRef.current.push(...(loaded[0]?.role === 'system' ? loaded.slice(1) : loaded));
1154
+ }
1155
+
1156
+ const targetWorkspace = await workspaceIndexService.findByPath(targetCwd).catch(() => null);
1157
+ clearFileState(getOrCreateSessionState(sessionStateKey({
1158
+ surface: 'tui',
1159
+ workspaceId: targetWorkspace?.id,
1160
+ cwd: targetWorkspace?.id ? undefined : targetCwd,
1161
+ sessionId: targetSessionId,
1162
+ })).fileReads);
1163
+ resetReactiveCompactState();
1164
+ cancelAllThrottles();
1165
+ setContextUsage(null);
1166
+ setError(null);
1167
+ setInterrupted(false);
1168
+ resetTurnState();
1169
+ setCompactGeneration((generation) => generation + 1);
1170
+ bump();
1171
+ return true;
1172
+ }, [bump, isLoading, cancelAllThrottles, resetTurnState]);
1173
+
1174
+ // history 用 useMemo 托管:version 变化时返回 historyRef.current 的浅拷贝。
1175
+ // 为什么必须 .slice():runQuery 内部 mutate 同一个数组(push 新消息),如果这里
1176
+ // 返回同一引用,Ink <Static items={history}> 浅比较看不到 prop 变化 → 不 commit
1177
+ // 新增 message → turn 结束 LiveArea 消失后屏幕空白,看起来像"输出被吃掉"。
1178
+ // slice 后元素引用不变,只是数组容器是新的,足够让 Static 检测到 length 增长并 commit。
1179
+ const history = useMemo(() => historyRef.current.slice(), [version]);
1180
+
1181
+ return {
1182
+ history,
1183
+ streamingText,
1184
+ isLoading,
1185
+ error,
1186
+ transientError,
1187
+ interrupted,
1188
+ pluginProgress,
1189
+ compactGeneration,
1190
+ progressStage,
1191
+ reasoningPreview,
1192
+ runningTools,
1193
+ contextUsage,
1194
+ computerUseMode,
1195
+ setComputerUseMode,
1196
+ stopComputerUse,
1197
+ pendingComputerIntervention,
1198
+ resumeComputerUse,
1199
+ cancelComputerUse,
1200
+ activeSessionId,
1201
+ queuedCount,
1202
+ queuedItems,
1203
+ submit,
1204
+ abort,
1205
+ clearHistory,
1206
+ compactNow,
1207
+ runManualAdvisor,
1208
+ switchSession,
1209
+ switchWorkspace,
1210
+ ephemeralLine,
1211
+ pendingAsk,
1212
+ pendingAskSource,
1213
+ pendingAskQueueCount,
1214
+ answerAsk,
1215
+ cancelAsk,
1216
+ setProvider,
1217
+ getCurrentProvider,
1218
+ };
1219
+ }
1220
+
1221
+ /**
1222
+ * 把一个事件派发到对应的 setState。
1223
+ *
1224
+ * 接受 LoopEvent(框架已识别)或 PluginEvent(插件私有,type 可为任意字符串)。
1225
+ * switch 只匹配 LoopEvent 字面量 type;未识别走 default no-op,等同静默忽略。
1226
+ *
1227
+ * 导出供单测直接调用(避免在 bun:test 里跑 React hook 渲染的复杂度)。
1228
+ */
1229
+ export interface HandleEventSetters {
1230
+ setStreamingText: (updater: (prev: string) => string) => void;
1231
+ setError: (e: string | null) => void;
1232
+ setInterrupted: (b: boolean) => void;
1233
+ setPluginProgress: (s: PluginProgressState | null) => void;
1234
+ setCompactGeneration: (updater: (prev: number) => number) => void;
1235
+ setProgressStage: (s: ProgressStage) => void;
1236
+ setReasoningPreview: (updater: (prev: string) => string) => void;
1237
+ setRunningToolsMap: (
1238
+ updater: (prev: Map<string, ToolStatusState>) => Map<string, ToolStatusState>,
1239
+ ) => void;
1240
+ /**
1241
+ * 内核 context_usage 事件的落地 setter(可选——不传时该事件被静默忽略,
1242
+ * 行为等同修复前)。StatusLine 优先用它渲染,缺省才降级回本地估算。
1243
+ */
1244
+ setContextUsage?: (usage: ContextUsageInfo) => void;
1245
+ /** streaming text 节流相关回调。 */
1246
+ appendStreamingDelta: (delta: string) => void;
1247
+ flushStreamingBuffer: () => void;
1248
+ cancelStreamingFlush: () => void;
1249
+ /** reasoning preview 节流(跟 streaming 一样 100ms / 200 字符)。 */
1250
+ appendReasoningDelta: (delta: string) => void;
1251
+ /** assistant_message 落定时取消 reasoning 待 flush buffer。 */
1252
+ cancelReasoningFlush: () => void;
1253
+ /** tool_start / tool_end 的节流 bump(200ms 内合并)。 */
1254
+ scheduleToolBump: () => void;
1255
+ cancelToolBump: () => void;
1256
+ bump: () => void;
1257
+ /**
1258
+ * 把一行工作流进度作为持久暗色行落进 scrollback(复用 ephemeralLine)。
1259
+ * 让 /workflow 执行过程在 TUI 里逐步可见、且出错后仍停留在屏幕上——用户一眼看出停在哪步。
1260
+ * 可选:非工作流场景(测试 / -p)不传也不影响。
1261
+ */
1262
+ emitProgressLine?: (text: string) => void;
1263
+ /**
1264
+ * 暂态错误 setter(喂 LiveArea 黄字,可被覆盖刷新)。
1265
+ * 可选:不传时退化为"工作流步内 llm_error 不再永久红字残留 + 重试聚合"特性关闭,
1266
+ * 非工作流路径行为完全不变(既有单测不传也不受影响)。
1267
+ */
1268
+ setTransientError?: (e: string | null) => void;
1269
+ /**
1270
+ * 暂态错误状态机的跨事件可变上下文(由 useChat 持 ref 注入)。
1271
+ * - activeStepId:当前正在执行的 workflow step id(null = 不在任何 step 内)
1272
+ * - pendingLlmError:该 step 内捕获的最近一次 llm_error 文案(暂存待裁决)
1273
+ * handleEvent 同步读写它;可选——不传时降级(同 setTransientError 缺省)。
1274
+ */
1275
+ workflowCtx?: { activeStepId: string | null; pendingLlmError: string | null };
1276
+ /**
1277
+ * 工作流步骤 id → 已插值人话进度名(title)的映射(由 useChat 持 ref 注入)。
1278
+ * workflow_step_start 带 title 时记入,step_end / step_skipped 据 id 回查,
1279
+ * 让 scrollback 的 ▶/✓/✗/⊘ 三种进度行都显示同一个人话名而非 id + 块内序号。
1280
+ * 可选——不传时退化为「end/skipped 显示 id」(既有单测不传也不受影响)。
1281
+ */
1282
+ stepTitles?: Map<string, string>;
1283
+ }
1284
+
1285
+ export function handleEvent(
1286
+ ev: LoopEvent | PluginEvent,
1287
+ setters: HandleEventSetters,
1288
+ ): void {
1289
+ // ---- 工作流进度事件(WorkflowEvent,不在 LoopEvent union 里)----
1290
+ // 渲染成持久的暗色行落进 scrollback:跑到哪一步、成功/失败/跳过都看得见,
1291
+ // 且出错后仍停留在屏幕上 —— 解决"工作流执行过程不可见、不知道停在哪"。
1292
+ const wf = ev as {
1293
+ type: string;
1294
+ id?: string;
1295
+ kind?: string;
1296
+ index?: number;
1297
+ total?: number;
1298
+ ok?: boolean;
1299
+ error?: string;
1300
+ reason?: string;
1301
+ name?: string;
1302
+ totalSteps?: number;
1303
+ title?: string;
1304
+ };
1305
+ // 据 id 回查人话进度名:声明了 title 的步走 title,否则回落 id(end/skipped 用)。
1306
+ const titleOrId = (id: string | undefined): string =>
1307
+ (id && setters.stepTitles?.get(id)) || id || '?';
1308
+ switch (wf.type) {
1309
+ case 'workflow_start':
1310
+ setters.emitProgressLine?.(`${figures.step} 工作流 ${wf.name} 开始(共 ${wf.totalSteps} 步)`);
1311
+ // 新工作流开始:清空上一条工作流残留的 title 映射,避免同名步串味。
1312
+ setters.stepTitles?.clear();
1313
+ // 工作流开始:立即刷新 token(可能已积累大量前置对话上下文)
1314
+ setters.bump();
1315
+ return;
1316
+ case 'workflow_step_start':
1317
+ // 声明了 title 的步显示人话名(与状态行一致、不显示块内相对序号——嵌套块
1318
+ // 的 i/total 拼在一起会被误读成"跳步");未声明 title 的步保持 legacy 格式。
1319
+ // ⎿ 前缀:step 行挂在「▶ 工作流 x 开始」下的层次连接符(figures.elbow 语汇,
1320
+ // 与工具结果/蜂群树统一)。ephemeral 是尾部追加消息,不触任何既有前缀字节。
1321
+ if (wf.title) {
1322
+ if (wf.id) setters.stepTitles?.set(wf.id, wf.title);
1323
+ setters.emitProgressLine?.(` ${figures.elbow} ${figures.step} ${wf.title}`);
1324
+ } else {
1325
+ setters.emitProgressLine?.(
1326
+ ` ${figures.elbow} ${figures.step} ${(wf.index ?? 0) + 1}/${wf.total ?? '?'} ${wf.id} (${wf.kind})`,
1327
+ );
1328
+ }
1329
+ // 暂态错误状态机:记下当前正在执行的 step id,进入"该步内的 llm_error 暂态化"窗口。
1330
+ // 进新步先清掉上一步可能残留的暂态错误(上一步已 end、不应跨步显示)。
1331
+ if (setters.workflowCtx) {
1332
+ setters.workflowCtx.activeStepId = wf.id ?? null;
1333
+ setters.workflowCtx.pendingLlmError = null;
1334
+ }
1335
+ setters.setTransientError?.(null);
1336
+ return;
1337
+ case 'workflow_step_end':
1338
+ setters.emitProgressLine?.(
1339
+ ` ${figures.elbow} ${wf.ok ? figures.ok : figures.fail} ${titleOrId(wf.id)}${wf.error ? `:${wf.error}` : ''}`,
1340
+ );
1341
+ // 暂态错误裁决(仅当 end 的是当前活跃步):
1342
+ // ok:true → 重试/恢复成功 → 丢弃 pending llm_error,短暂显示"已恢复"后随刷新消失。
1343
+ // ok:false → 该步彻底失败 → 把暂态错误提升为永久红字(chat.error,进 scrollback 上方)。
1344
+ if (setters.workflowCtx && setters.workflowCtx.activeStepId === wf.id) {
1345
+ const pending = setters.workflowCtx.pendingLlmError;
1346
+ if (wf.ok) {
1347
+ if (pending) {
1348
+ // 成功收回:留一条灰底"已恢复"提示,下一次状态行刷新自然覆盖消失
1349
+ setters.setTransientError?.(`${figures.ok} ${wf.id} 已恢复(此前 LLM 调用失败已重试成功)`);
1350
+ } else {
1351
+ setters.setTransientError?.(null);
1352
+ }
1353
+ } else {
1354
+ // 彻底失败:暂态升永久红字。优先用 step.error,回落到 pending llm_error。
1355
+ const finalMsg = wf.error ?? pending;
1356
+ if (finalMsg) setters.setError(finalMsg);
1357
+ setters.setTransientError?.(null);
1358
+ }
1359
+ setters.workflowCtx.activeStepId = null;
1360
+ setters.workflowCtx.pendingLlmError = null;
1361
+ }
1362
+ // 步骤完成:强制刷新 token(agent/llm 步骤可能写入大量上下文)
1363
+ setters.bump();
1364
+ return;
1365
+ case 'workflow_step_skipped':
1366
+ setters.emitProgressLine?.(` ${figures.elbow} ${figures.skip} ${titleOrId(wf.id)}:${wf.reason ?? ''}`);
1367
+ return;
1368
+ case 'workflow_done':
1369
+ setters.emitProgressLine?.(`${figures.ok} 工作流 ${wf.name} 完成`);
1370
+ // 工作流结束:最终刷新(反映完整执行后的上下文量)
1371
+ setters.bump();
1372
+ return;
1373
+ }
1374
+
1375
+ // 收口到 LoopEvent:未识别的 PluginEvent type 走 default no-op
1376
+ const e = ev as LoopEvent;
1377
+ switch (e.type) {
1378
+ case 'user_message_committed':
1379
+ // loop 刚把用户消息 push 进 history。立即 bump → history useMemo 重算 →
1380
+ // <Static> 把用户消息 commit 进 scrollback,第一时间置顶(T-A-O-R)。
1381
+ setters.bump();
1382
+ break;
1383
+ case 'text':
1384
+ setters.appendStreamingDelta(e.delta);
1385
+ break;
1386
+ case 'assistant_message':
1387
+ // history 已被 runQuery 直接 push。先 flush 残留 streaming buffer
1388
+ // 把末段 text 落到 streamingText state(avoid 视觉跳跃),再清空
1389
+ // streamingText —— Static 已 commit 完整 assistant message 接管显示。
1390
+ setters.flushStreamingBuffer();
1391
+ setters.setStreamingText(() => '');
1392
+ // 该段 reasoning 已随 assistant 消息进 history → MessageList 提交进 scrollback(💭 块)。
1393
+ // 清空 Zone B 的 live 预览:① 本轮 streaming 阶段不再回显旧 reasoning;
1394
+ // ② 多轮工具调用时,下一轮 thinking 从空白起步,不残留上一轮思维链。
1395
+ setters.cancelReasoningFlush();
1396
+ setters.setReasoningPreview(() => '');
1397
+ setters.cancelToolBump();
1398
+ setters.setProgressStage('idle');
1399
+ setters.setRunningToolsMap(() => new Map());
1400
+ setters.bump();
1401
+ break;
1402
+ case 'tool_start':
1403
+ // 把工具加入运行集合;并行多个工具时这里会累积。
1404
+ // v2: argsFriendly 是 loop.ts 通过 LoopEvent 扩展字段传过来的人话描述
1405
+ // (如 "Reading src/foo.ts")。LoopEvent 类型上没声明此字段时通过
1406
+ // (e as { argsFriendly?: string }) 安全读取,缺省 fallback 在 UI 层。
1407
+ setters.setRunningToolsMap((prev) => {
1408
+ const next = new Map(prev);
1409
+ next.set(e.toolCallId, {
1410
+ toolName: e.toolName,
1411
+ argsPreview: e.argsPreview,
1412
+ status: 'running',
1413
+ argsFriendly: (e as { argsFriendly?: string }).argsFriendly,
1414
+ });
1415
+ return next;
1416
+ });
1417
+ // 工具状态切换走 200ms 节流,避免连续 tool_start/end 高频 rerender
1418
+ // 压垮 Ink ANSI 重绘。
1419
+ setters.scheduleToolBump();
1420
+ break;
1421
+ case 'tool_progress':
1422
+ setters.setRunningToolsMap((prev) => {
1423
+ const current = prev.get(e.toolCallId);
1424
+ if (!current) return prev;
1425
+ const next = new Map(prev);
1426
+ next.set(e.toolCallId, { ...current, progress: e.message, progressDetail: e.detail });
1427
+ return next;
1428
+ });
1429
+ setters.scheduleToolBump();
1430
+ break;
1431
+ case 'tool_end':
1432
+ // 仅删除对应 callId 的 entry;其余并行工具继续
1433
+ setters.setRunningToolsMap((prev) => {
1434
+ if (!prev.has(e.toolCallId)) return prev;
1435
+ const next = new Map(prev);
1436
+ next.delete(e.toolCallId);
1437
+ return next;
1438
+ });
1439
+ setters.scheduleToolBump();
1440
+ break;
1441
+ case 'tool_messages_committed':
1442
+ setters.cancelStreamingFlush();
1443
+ setters.setStreamingText(() => '');
1444
+ setters.cancelReasoningFlush();
1445
+ setters.setReasoningPreview(() => '');
1446
+ setters.cancelToolBump();
1447
+ setters.setProgressStage('idle');
1448
+ setters.setRunningToolsMap(() => new Map());
1449
+ setters.bump();
1450
+ break;
1451
+ case 'compact_start':
1452
+ // reactive 自救场景:LLM 失败前可能已经 yield 过部分 text delta,
1453
+ // 压缩前清掉残留 streamingText + 待 flush buffer,避免重发后新文本追加在
1454
+ // 旧 buffer 后面。autoCompact 场景下 streamingText 本来就是空。
1455
+ // 注意:progressStage 由 loop 端的 stage_change 'compacting' 负责切换。
1456
+ setters.cancelStreamingFlush();
1457
+ setters.setStreamingText(() => '');
1458
+ setters.cancelReasoningFlush();
1459
+ setters.setReasoningPreview(() => '');
1460
+ setters.bump();
1461
+ break;
1462
+ case 'compact_done':
1463
+ // 触发 MessageList 内 <Static> remount,丢弃旧消息缓存
1464
+ setters.setCompactGeneration((g) => g + 1);
1465
+ setters.bump();
1466
+ break;
1467
+ case 'harness_diagnostics':
1468
+ // Diagnostics are intentionally local-safe and currently consumed by CLI/stream hosts.
1469
+ break;
1470
+ case 'turn_done':
1471
+ setters.bump();
1472
+ break;
1473
+ case 'interrupted':
1474
+ setters.setInterrupted(true);
1475
+ setters.bump();
1476
+ break;
1477
+ case 'error': {
1478
+ const code = (e as { code?: string }).code;
1479
+ // compact_failed:非致命压缩失败(loop 会继续硬上)。不当红字,作为黄字暂态提示。
1480
+ if (code === 'compact_failed') {
1481
+ setters.setTransientError?.(`压缩失败(已忽略继续):${e.error}`);
1482
+ setters.bump();
1483
+ break;
1484
+ }
1485
+ // 工作流步执行中的 max_turns 是可恢复截断信号:runner 会在共享 history 上续跑。
1486
+ // 这里不要落入永久红字错误区;顶层非工作流 max_turns 仍保持原有错误展示。
1487
+ if (
1488
+ code === 'max_turns' &&
1489
+ setters.workflowCtx &&
1490
+ setters.workflowCtx.activeStepId !== null &&
1491
+ setters.setTransientError
1492
+ ) {
1493
+ setters.setTransientError('↻ 当前步骤达到轮数预算,正在续跑…');
1494
+ setters.bump();
1495
+ break;
1496
+ }
1497
+ // 工作流步执行中的 llm_error:暂态而非永久红字 ——
1498
+ // 该步通常会被重试,这里只暂存 + 喂 LiveArea 黄字,等 step_end 裁决去留。
1499
+ // 非工作流场景(无活跃 step,或缺 setTransientError 降级)走原有永久红字路径。
1500
+ if (
1501
+ code === 'llm_error' &&
1502
+ setters.workflowCtx &&
1503
+ setters.workflowCtx.activeStepId !== null &&
1504
+ setters.setTransientError
1505
+ ) {
1506
+ setters.workflowCtx.pendingLlmError = e.error;
1507
+ setters.setTransientError(`⚠ LLM 调用失败(重试中):${e.error}`);
1508
+ setters.bump();
1509
+ break;
1510
+ }
1511
+ // 其余:顶层 halt(无 code)、非工作流 llm_error、aborted 等 → 永久红字。
1512
+ setters.setError(e.error);
1513
+ setters.bump();
1514
+ break;
1515
+ }
1516
+ case 'warning': {
1517
+ // 通用非致命警告。workflow 这类插件已另发 plugin_progress 把同文案镜像到状态行。
1518
+ // 这里额外把"重试 N/M"类警告聚合到 LiveArea 黄色暂态行(覆盖刷新,不 append),
1519
+ // 让用户一眼看出"正在第 N/M 次重试"而非"卡住"。其余 warning 仍 no-op。
1520
+ const msg = (e as { message?: string }).message;
1521
+ if (
1522
+ setters.setTransientError &&
1523
+ typeof msg === 'string' &&
1524
+ msg.includes('重试')
1525
+ ) {
1526
+ setters.setTransientError(`↻ ${msg}`);
1527
+ setters.bump();
1528
+ }
1529
+ break;
1530
+ }
1531
+ case 'plugin_progress':
1532
+ setters.setPluginProgress({
1533
+ pluginId: e.pluginId,
1534
+ current: e.current,
1535
+ max: e.max,
1536
+ message: e.message,
1537
+ });
1538
+ // 重试进度也镜像到暂态行(覆盖刷新):runner 在重试时发 "↻ 重试 x N/M…"。
1539
+ // 与 warning 分支互为补充(plugin_progress 文案更短,适合状态行下方一行)。
1540
+ if (
1541
+ setters.setTransientError &&
1542
+ typeof e.message === 'string' &&
1543
+ e.message.includes('↻ 重试')
1544
+ ) {
1545
+ setters.setTransientError(e.message);
1546
+ }
1547
+ setters.bump();
1548
+ break;
1549
+ case 'reasoning':
1550
+ // reasoning delta 频率跟 text delta 一样高(60+/秒),不节流的话每个 delta 立即
1551
+ // setReasoningPreview → LiveArea rerender → Ink 重绘扛不住。
1552
+ setters.appendReasoningDelta(e.delta);
1553
+ // 不调用 bump:reasoningPreview 是 LiveArea 单独的 state,history 没变化,
1554
+ // 避免高频 reasoning token 引起 MessageList re-render
1555
+ break;
1556
+ case 'stage_change':
1557
+ // 注意:tool_executing → streaming 切回时不清 runningTools,
1558
+ // 工具集合的清空时机是各自 tool_end
1559
+ setters.setProgressStage(e.stage);
1560
+ break;
1561
+ case 'context_usage':
1562
+ // 内核每轮算出的真实用量(含 tools-schema 开销 + 真实 provider usage)——
1563
+ // 比 StatusLine 本地 useTokenUsage 估算更准,落地后 StatusLine 优先用它。
1564
+ setters.setContextUsage?.({
1565
+ usedTokens: e.usedTokens,
1566
+ contextWindow: e.contextWindow,
1567
+ remainingTokens: e.remainingTokens,
1568
+ compactThreshold: e.compactThreshold,
1569
+ source: e.source,
1570
+ });
1571
+ break;
1572
+ default:
1573
+ // 落到这里 = 插件私有事件(PluginEvent,type 任意字符串),TUI 不识别即忽略。
1574
+ // 这里**故意不**写 console.warn —— Ink 渲染期间写 stdout/stderr 会撕裂终端画面。
1575
+ // 要排查插件 yield 了拼错的事件 type,请用 `minimal-agent -p --verbose`(print.ts 落 stderr)。
1576
+ break;
1577
+ }
1578
+ }