@oh-my-pi/pi-coding-agent 15.12.3 → 15.13.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 (457) hide show
  1. package/CHANGELOG.md +347 -7
  2. package/dist/cli.js +1615 -1231
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  8. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  9. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  10. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  11. package/dist/types/autoresearch/types.d.ts +1 -1
  12. package/dist/types/cli/args.d.ts +19 -2
  13. package/dist/types/cli/models-cli.d.ts +49 -0
  14. package/dist/types/cli/session-picker.d.ts +1 -1
  15. package/dist/types/cli/setup-cli.d.ts +1 -1
  16. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  17. package/dist/types/collab/protocol.d.ts +1 -1
  18. package/dist/types/commands/launch.d.ts +0 -3
  19. package/dist/types/commands/models.d.ts +33 -0
  20. package/dist/types/commands/say.d.ts +24 -0
  21. package/dist/types/commands/token.d.ts +25 -0
  22. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  23. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  24. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  25. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  26. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  27. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  28. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  29. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  30. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  31. package/dist/types/commit/changelog/generate.d.ts +1 -1
  32. package/dist/types/commit/shared-llm.d.ts +1 -1
  33. package/dist/types/config/keybindings.d.ts +3 -3
  34. package/dist/types/config/model-registry.d.ts +17 -0
  35. package/dist/types/config/models-config-schema.d.ts +13 -1
  36. package/dist/types/config/models-config.d.ts +8 -2
  37. package/dist/types/config/settings-schema.d.ts +281 -58
  38. package/dist/types/edit/hashline/params.d.ts +1 -1
  39. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  40. package/dist/types/edit/modes/patch.d.ts +1 -1
  41. package/dist/types/edit/modes/replace.d.ts +1 -1
  42. package/dist/types/export/html/index.d.ts +2 -1
  43. package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
  44. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  46. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  47. package/dist/types/extensibility/extensions/types.d.ts +49 -3
  48. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  49. package/dist/types/extensibility/hooks/types.d.ts +2 -2
  50. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  51. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  52. package/dist/types/extensibility/shared-events.d.ts +1 -1
  53. package/dist/types/extensibility/skills.d.ts +10 -0
  54. package/dist/types/goals/guided-setup.d.ts +18 -0
  55. package/dist/types/goals/state.d.ts +1 -1
  56. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  57. package/dist/types/hindsight/transcript.d.ts +1 -1
  58. package/dist/types/index.d.ts +5 -0
  59. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  60. package/dist/types/lsp/types.d.ts +1 -1
  61. package/dist/types/main.d.ts +4 -3
  62. package/dist/types/mcp/manager.d.ts +8 -0
  63. package/dist/types/mcp/startup-events.d.ts +11 -0
  64. package/dist/types/memories/index.d.ts +7 -0
  65. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  66. package/dist/types/mnemopi/config.d.ts +28 -0
  67. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  68. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  69. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  70. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  71. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  72. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +1 -0
  74. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  75. package/dist/types/modes/components/session-selector.d.ts +1 -1
  76. package/dist/types/modes/components/status-line/component.d.ts +9 -5
  77. package/dist/types/modes/components/status-line/types.d.ts +2 -1
  78. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  79. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  80. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  81. package/dist/types/modes/components/usage-row.d.ts +3 -0
  82. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  83. package/dist/types/modes/controllers/event-controller.d.ts +0 -17
  84. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  85. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  86. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  87. package/dist/types/modes/image-references.d.ts +6 -0
  88. package/dist/types/modes/interactive-mode.d.ts +27 -6
  89. package/dist/types/modes/magic-keywords.d.ts +13 -1
  90. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  91. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  92. package/dist/types/modes/runtime-init.d.ts +4 -0
  93. package/dist/types/modes/theme/theme.d.ts +13 -2
  94. package/dist/types/modes/types.d.ts +8 -7
  95. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  96. package/dist/types/registry/agent-registry.d.ts +17 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +28 -35
  99. package/dist/types/session/agent-storage.d.ts +2 -1
  100. package/dist/types/session/indexed-session-storage.d.ts +3 -3
  101. package/dist/types/session/messages.d.ts +8 -10
  102. package/dist/types/session/session-context.d.ts +39 -0
  103. package/dist/types/session/session-entries.d.ts +159 -0
  104. package/dist/types/session/session-listing.d.ts +69 -0
  105. package/dist/types/session/session-loader.d.ts +16 -0
  106. package/dist/types/session/session-manager.d.ts +85 -462
  107. package/dist/types/session/session-migrations.d.ts +12 -0
  108. package/dist/types/session/session-paths.d.ts +25 -0
  109. package/dist/types/session/session-persistence.d.ts +8 -0
  110. package/dist/types/session/session-storage.d.ts +11 -7
  111. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  112. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  113. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  114. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  115. package/dist/types/stt/asr-client.d.ts +90 -0
  116. package/dist/types/stt/asr-protocol.d.ts +97 -0
  117. package/dist/types/stt/asr-worker.d.ts +2 -0
  118. package/dist/types/stt/downloader.d.ts +38 -0
  119. package/dist/types/stt/endpointer.d.ts +59 -0
  120. package/dist/types/stt/index.d.ts +5 -1
  121. package/dist/types/stt/models.d.ts +120 -0
  122. package/dist/types/stt/recorder.d.ts +17 -0
  123. package/dist/types/stt/stt-controller.d.ts +6 -0
  124. package/dist/types/stt/transcriber.d.ts +5 -7
  125. package/dist/types/stt/wav.d.ts +29 -0
  126. package/dist/types/system-prompt.d.ts +4 -0
  127. package/dist/types/task/executor.d.ts +2 -0
  128. package/dist/types/task/index.d.ts +9 -1
  129. package/dist/types/task/types.d.ts +37 -1
  130. package/dist/types/tools/ask.d.ts +1 -1
  131. package/dist/types/tools/ast-edit.d.ts +1 -1
  132. package/dist/types/tools/ast-grep.d.ts +1 -1
  133. package/dist/types/tools/bash.d.ts +3 -3
  134. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  135. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  136. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  137. package/dist/types/tools/browser/registry.d.ts +16 -3
  138. package/dist/types/tools/browser/render.d.ts +2 -0
  139. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  140. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  141. package/dist/types/tools/browser.d.ts +3 -1
  142. package/dist/types/tools/checkpoint.d.ts +1 -1
  143. package/dist/types/tools/debug.d.ts +1 -1
  144. package/dist/types/tools/eval-render.d.ts +1 -1
  145. package/dist/types/tools/eval.d.ts +1 -1
  146. package/dist/types/tools/find.d.ts +1 -1
  147. package/dist/types/tools/gh.d.ts +1 -1
  148. package/dist/types/tools/image-gen.d.ts +1 -1
  149. package/dist/types/tools/index.d.ts +14 -2
  150. package/dist/types/tools/inspect-image.d.ts +1 -1
  151. package/dist/types/tools/irc.d.ts +2 -1
  152. package/dist/types/tools/job.d.ts +1 -1
  153. package/dist/types/tools/learn.d.ts +51 -0
  154. package/dist/types/tools/manage-skill.d.ts +40 -0
  155. package/dist/types/tools/memory-edit.d.ts +1 -1
  156. package/dist/types/tools/memory-recall.d.ts +1 -1
  157. package/dist/types/tools/memory-reflect.d.ts +1 -1
  158. package/dist/types/tools/memory-retain.d.ts +1 -1
  159. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  160. package/dist/types/tools/read.d.ts +1 -1
  161. package/dist/types/tools/render-mermaid.d.ts +1 -1
  162. package/dist/types/tools/renderers.d.ts +7 -11
  163. package/dist/types/tools/resolve.d.ts +1 -1
  164. package/dist/types/tools/review.d.ts +1 -1
  165. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  166. package/dist/types/tools/search.d.ts +1 -1
  167. package/dist/types/tools/ssh.d.ts +2 -2
  168. package/dist/types/tools/todo.d.ts +2 -2
  169. package/dist/types/tools/tts.d.ts +26 -1
  170. package/dist/types/tools/write.d.ts +2 -2
  171. package/dist/types/tts/downloader.d.ts +20 -0
  172. package/dist/types/tts/index.d.ts +8 -0
  173. package/dist/types/tts/models.d.ts +82 -0
  174. package/dist/types/tts/player.d.ts +32 -0
  175. package/dist/types/tts/runtime.d.ts +6 -0
  176. package/dist/types/tts/streaming-player.d.ts +41 -0
  177. package/dist/types/tts/tts-client.d.ts +93 -0
  178. package/dist/types/tts/tts-protocol.d.ts +95 -0
  179. package/dist/types/tts/tts-worker.d.ts +2 -0
  180. package/dist/types/tts/vocalizer.d.ts +41 -0
  181. package/dist/types/tts/wav.d.ts +8 -0
  182. package/dist/types/utils/clipboard.d.ts +4 -3
  183. package/dist/types/utils/image-loading.d.ts +18 -1
  184. package/dist/types/utils/thinking-display.d.ts +17 -0
  185. package/dist/types/utils/tool-choice.d.ts +8 -0
  186. package/dist/types/utils/tools-manager.d.ts +2 -1
  187. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  188. package/dist/types/web/scrapers/github.d.ts +1 -1
  189. package/dist/types/web/search/index.d.ts +1 -1
  190. package/package.json +17 -16
  191. package/src/async/job-manager.ts +49 -0
  192. package/src/autolearn/controller.ts +139 -0
  193. package/src/autolearn/managed-skills.ts +257 -0
  194. package/src/autoresearch/state.ts +1 -1
  195. package/src/autoresearch/storage.ts +2 -1
  196. package/src/autoresearch/tools/init-experiment.ts +1 -1
  197. package/src/autoresearch/tools/log-experiment.ts +1 -1
  198. package/src/autoresearch/tools/run-experiment.ts +1 -1
  199. package/src/autoresearch/tools/update-notes.ts +1 -1
  200. package/src/autoresearch/types.ts +1 -1
  201. package/src/cli/args.ts +56 -10
  202. package/src/cli/auth-gateway-cli.ts +1 -1
  203. package/src/cli/bench-cli.ts +1 -1
  204. package/src/cli/dry-balance-cli.ts +1 -1
  205. package/src/cli/models-cli.ts +427 -0
  206. package/src/cli/session-picker.ts +2 -1
  207. package/src/cli/setup-cli.ts +148 -47
  208. package/src/cli/setup-model-picker.ts +43 -0
  209. package/src/cli-commands.ts +3 -0
  210. package/src/cli.ts +45 -13
  211. package/src/collab/host.ts +10 -13
  212. package/src/collab/protocol.ts +1 -1
  213. package/src/commands/launch.ts +0 -3
  214. package/src/commands/models.ts +61 -0
  215. package/src/commands/say.ts +102 -0
  216. package/src/commands/setup.ts +1 -1
  217. package/src/commands/token.ts +89 -0
  218. package/src/commit/agentic/tools/analyze-file.ts +4 -1
  219. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  220. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  221. package/src/commit/agentic/tools/git-overview.ts +1 -1
  222. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  223. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  224. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  225. package/src/commit/agentic/tools/schemas.ts +1 -1
  226. package/src/commit/agentic/tools/split-commit.ts +1 -1
  227. package/src/commit/analysis/summary.ts +1 -1
  228. package/src/commit/changelog/generate.ts +1 -1
  229. package/src/commit/shared-llm.ts +1 -1
  230. package/src/config/keybindings.ts +2 -2
  231. package/src/config/model-discovery.ts +11 -5
  232. package/src/config/model-registry.ts +79 -21
  233. package/src/config/model-resolver.ts +2 -2
  234. package/src/config/models-config-schema.ts +5 -2
  235. package/src/config/models-config.ts +2 -1
  236. package/src/config/settings-schema.ts +266 -32
  237. package/src/config/settings.ts +10 -0
  238. package/src/discovery/builtin.ts +23 -1
  239. package/src/discovery/claude-plugins.ts +44 -5
  240. package/src/discovery/helpers.ts +41 -1
  241. package/src/edit/hashline/params.ts +1 -1
  242. package/src/edit/modes/apply-patch.ts +1 -1
  243. package/src/edit/modes/patch.ts +1 -1
  244. package/src/edit/modes/replace.ts +1 -1
  245. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  246. package/src/eval/agent-bridge.ts +1 -1
  247. package/src/eval/completion-bridge.ts +1 -1
  248. package/src/eval/js/shared/prelude.txt +69 -17
  249. package/src/export/html/index.ts +3 -6
  250. package/src/export/html/template.js +24 -2
  251. package/src/export/html/tool-views.generated.js +2 -2
  252. package/src/extensibility/custom-commands/loader.ts +1 -1
  253. package/src/extensibility/custom-commands/types.ts +2 -2
  254. package/src/extensibility/custom-tools/loader.ts +1 -1
  255. package/src/extensibility/custom-tools/types.ts +2 -2
  256. package/src/extensibility/extensions/loader.ts +2 -2
  257. package/src/extensibility/extensions/model-api.ts +41 -0
  258. package/src/extensibility/extensions/runner.ts +4 -0
  259. package/src/extensibility/extensions/types.ts +54 -3
  260. package/src/extensibility/extensions/wrapper.ts +41 -5
  261. package/src/extensibility/hooks/index.ts +2 -1
  262. package/src/extensibility/hooks/loader.ts +1 -1
  263. package/src/extensibility/hooks/types.ts +2 -2
  264. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  265. package/src/extensibility/plugins/loader.ts +30 -19
  266. package/src/extensibility/plugins/manager.ts +221 -90
  267. package/src/extensibility/shared-events.ts +1 -1
  268. package/src/extensibility/skills.ts +101 -5
  269. package/src/goals/guided-setup.ts +133 -0
  270. package/src/goals/state.ts +1 -1
  271. package/src/goals/tools/goal-tool.ts +1 -1
  272. package/src/hindsight/transcript.ts +1 -1
  273. package/src/index.ts +5 -0
  274. package/src/internal-urls/docs-index.generated.ts +13 -10
  275. package/src/internal-urls/history-protocol.ts +1 -1
  276. package/src/internal-urls/local-protocol.ts +29 -7
  277. package/src/lsp/types.ts +1 -1
  278. package/src/main.ts +27 -32
  279. package/src/mcp/config-writer.ts +7 -3
  280. package/src/mcp/manager.ts +11 -0
  281. package/src/mcp/startup-events.ts +21 -0
  282. package/src/mcp/transports/stdio.ts +2 -1
  283. package/src/memories/index.ts +149 -12
  284. package/src/memories/storage.ts +2 -1
  285. package/src/memory-backend/local-backend.ts +11 -5
  286. package/src/mnemopi/backend.ts +1 -0
  287. package/src/mnemopi/config.ts +112 -12
  288. package/src/modes/acp/acp-agent.ts +8 -53
  289. package/src/modes/acp/acp-event-mapper.ts +5 -1
  290. package/src/modes/components/agent-hub.ts +51 -5
  291. package/src/modes/components/assistant-message.ts +12 -44
  292. package/src/modes/components/compaction-summary-message.ts +125 -26
  293. package/src/modes/components/custom-editor.test.ts +96 -0
  294. package/src/modes/components/custom-editor.ts +164 -8
  295. package/src/modes/components/index.ts +1 -0
  296. package/src/modes/components/logout-account-selector.ts +130 -0
  297. package/src/modes/components/mcp-add-wizard.ts +1 -1
  298. package/src/modes/components/model-selector.ts +2 -2
  299. package/src/modes/components/session-selector.ts +1 -1
  300. package/src/modes/components/settings-defs.ts +7 -0
  301. package/src/modes/components/status-line/component.ts +54 -157
  302. package/src/modes/components/status-line/segments.ts +1 -1
  303. package/src/modes/components/status-line/types.ts +2 -1
  304. package/src/modes/components/tool-execution.ts +82 -43
  305. package/src/modes/components/transcript-container.ts +70 -1
  306. package/src/modes/components/tree-selector.ts +1 -1
  307. package/src/modes/components/usage-row.ts +18 -0
  308. package/src/modes/components/user-message.ts +4 -2
  309. package/src/modes/controllers/command-controller.ts +14 -16
  310. package/src/modes/controllers/event-controller.ts +101 -73
  311. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  312. package/src/modes/controllers/input-controller.ts +311 -57
  313. package/src/modes/controllers/mcp-command-controller.ts +44 -3
  314. package/src/modes/controllers/selector-controller.ts +68 -12
  315. package/src/modes/controllers/streaming-reveal.ts +4 -3
  316. package/src/modes/gradient-highlight.ts +21 -9
  317. package/src/modes/image-references.ts +20 -0
  318. package/src/modes/interactive-mode.ts +288 -48
  319. package/src/modes/magic-keywords.ts +27 -5
  320. package/src/modes/rpc/rpc-mode.ts +146 -14
  321. package/src/modes/rpc/rpc-subagents.ts +2 -2
  322. package/src/modes/rpc/rpc-types.ts +8 -2
  323. package/src/modes/runtime-init.ts +28 -3
  324. package/src/modes/theme/theme.ts +99 -51
  325. package/src/modes/types.ts +6 -7
  326. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  327. package/src/modes/utils/ui-helpers.ts +36 -7
  328. package/src/priority.json +5 -1
  329. package/src/prompts/agents/task.md +1 -0
  330. package/src/prompts/goals/guided-goal-interview.md +8 -0
  331. package/src/prompts/goals/guided-goal-system.md +12 -0
  332. package/src/prompts/memories/read-path.md +6 -0
  333. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  334. package/src/prompts/system/autolearn-guidance.md +7 -0
  335. package/src/prompts/system/autolearn-nudge.md +3 -0
  336. package/src/prompts/system/eager-task.md +7 -0
  337. package/src/prompts/system/eager-todo.md +11 -6
  338. package/src/prompts/system/empty-stop-retry.md +4 -6
  339. package/src/prompts/system/subagent-system-prompt.md +4 -0
  340. package/src/prompts/system/system-prompt.md +10 -5
  341. package/src/prompts/system/title-marker-instruction.md +1 -0
  342. package/src/prompts/system/title-system-marker.md +16 -0
  343. package/src/prompts/tools/job.md +1 -0
  344. package/src/prompts/tools/learn.md +7 -0
  345. package/src/prompts/tools/manage-skill.md +9 -0
  346. package/src/prompts/tools/task.md +3 -0
  347. package/src/registry/agent-registry.ts +30 -0
  348. package/src/sdk.ts +103 -43
  349. package/src/secrets/obfuscator.ts +1 -1
  350. package/src/session/agent-session.ts +331 -318
  351. package/src/session/agent-storage.ts +18 -9
  352. package/src/session/history-storage.ts +3 -2
  353. package/src/session/indexed-session-storage.ts +7 -10
  354. package/src/session/messages.ts +9 -11
  355. package/src/session/session-context.ts +352 -0
  356. package/src/session/session-dump-format.ts +4 -2
  357. package/src/session/session-entries.ts +194 -0
  358. package/src/session/session-listing.ts +588 -0
  359. package/src/session/session-loader.ts +106 -0
  360. package/src/session/session-manager.ts +968 -3064
  361. package/src/session/session-migrations.ts +78 -0
  362. package/src/session/session-paths.ts +193 -0
  363. package/src/session/session-persistence.ts +131 -0
  364. package/src/session/session-storage.ts +91 -30
  365. package/src/session/snapcompact-inline.ts +21 -1
  366. package/src/session/snapcompact-savings-journal.ts +113 -0
  367. package/src/session/tool-choice-queue.ts +23 -11
  368. package/src/slash-commands/builtin-registry.ts +40 -4
  369. package/src/slash-commands/helpers/logout.ts +88 -0
  370. package/src/stt/asr-client.ts +520 -0
  371. package/src/stt/asr-protocol.ts +65 -0
  372. package/src/stt/asr-worker.ts +790 -0
  373. package/src/stt/downloader.ts +107 -47
  374. package/src/stt/endpointer.ts +259 -0
  375. package/src/stt/index.ts +5 -1
  376. package/src/stt/models.ts +150 -0
  377. package/src/stt/recorder.ts +247 -60
  378. package/src/stt/stt-controller.ts +201 -22
  379. package/src/stt/transcriber.ts +37 -68
  380. package/src/stt/wav.ts +173 -0
  381. package/src/system-prompt.ts +8 -0
  382. package/src/task/agents.ts +1 -2
  383. package/src/task/executor.ts +49 -15
  384. package/src/task/index.ts +60 -6
  385. package/src/task/render.ts +83 -8
  386. package/src/task/types.ts +54 -1
  387. package/src/tools/ask.ts +9 -1
  388. package/src/tools/ast-edit.ts +1 -1
  389. package/src/tools/ast-grep.ts +1 -1
  390. package/src/tools/bash.ts +5 -4
  391. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  392. package/src/tools/browser/cmux/rpc.ts +156 -0
  393. package/src/tools/browser/cmux/socket-client.ts +309 -0
  394. package/src/tools/browser/registry.ts +37 -3
  395. package/src/tools/browser/render.ts +6 -1
  396. package/src/tools/browser/tab-protocol.ts +2 -0
  397. package/src/tools/browser/tab-supervisor.ts +189 -18
  398. package/src/tools/browser/tab-worker.ts +1 -1
  399. package/src/tools/browser.ts +16 -1
  400. package/src/tools/checkpoint.ts +1 -1
  401. package/src/tools/debug.ts +1 -1
  402. package/src/tools/eval-render.ts +4 -3
  403. package/src/tools/eval.ts +11 -6
  404. package/src/tools/fetch.ts +13 -2
  405. package/src/tools/find.ts +1 -1
  406. package/src/tools/gh.ts +1 -1
  407. package/src/tools/github-cache.ts +2 -1
  408. package/src/tools/image-gen.ts +1 -1
  409. package/src/tools/index.ts +43 -5
  410. package/src/tools/inspect-image.ts +3 -1
  411. package/src/tools/irc.ts +11 -3
  412. package/src/tools/job.ts +15 -3
  413. package/src/tools/learn.ts +144 -0
  414. package/src/tools/manage-skill.ts +104 -0
  415. package/src/tools/memory-edit.ts +1 -1
  416. package/src/tools/memory-recall.ts +1 -1
  417. package/src/tools/memory-reflect.ts +1 -1
  418. package/src/tools/memory-retain.ts +1 -1
  419. package/src/tools/plan-mode-guard.ts +53 -19
  420. package/src/tools/read.ts +8 -2
  421. package/src/tools/render-mermaid.ts +1 -1
  422. package/src/tools/renderers.ts +7 -11
  423. package/src/tools/report-tool-issue.ts +3 -2
  424. package/src/tools/resolve.ts +1 -1
  425. package/src/tools/review.ts +1 -1
  426. package/src/tools/search-tool-bm25.ts +1 -1
  427. package/src/tools/search.ts +1 -1
  428. package/src/tools/ssh.ts +5 -4
  429. package/src/tools/todo.ts +2 -2
  430. package/src/tools/tts.ts +204 -93
  431. package/src/tools/write.ts +19 -3
  432. package/src/tts/downloader.ts +64 -0
  433. package/src/tts/index.ts +8 -0
  434. package/src/tts/models.ts +137 -0
  435. package/src/tts/player.ts +137 -0
  436. package/src/tts/runtime.ts +21 -0
  437. package/src/tts/streaming-player.ts +266 -0
  438. package/src/tts/tts-client.ts +647 -0
  439. package/src/tts/tts-protocol.ts +60 -0
  440. package/src/tts/tts-worker.ts +497 -0
  441. package/src/tts/vocalizer.ts +162 -0
  442. package/src/tts/wav.ts +58 -0
  443. package/src/utils/clipboard.ts +35 -18
  444. package/src/utils/image-loading.ts +35 -4
  445. package/src/utils/thinking-display.ts +37 -0
  446. package/src/utils/title-generator.ts +48 -5
  447. package/src/utils/tool-choice.ts +16 -0
  448. package/src/utils/tools-manager.test.ts +25 -0
  449. package/src/utils/tools-manager.ts +19 -1
  450. package/src/web/scrapers/github.ts +96 -0
  451. package/src/web/search/index.ts +14 -1
  452. package/src/web/search/providers/searxng.ts +13 -1
  453. package/dist/types/cli/list-models.d.ts +0 -30
  454. package/dist/types/stt/setup.d.ts +0 -18
  455. package/src/cli/list-models.ts +0 -194
  456. package/src/stt/setup.ts +0 -52
  457. package/src/stt/transcribe.py +0 -70
@@ -90,6 +90,13 @@ const CONDITIONS: Record<string, () => boolean> = {
90
90
  return false;
91
91
  }
92
92
  },
93
+ autolearnActive: () => {
94
+ try {
95
+ return Settings.instance.get("autolearn.enabled") === true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ },
93
100
  autoThinkingActive: () => {
94
101
  try {
95
102
  return Settings.instance.get("defaultThinkingLevel") === "auto";
@@ -1,7 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
4
- import { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
5
4
  import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
6
5
  import { getProjectDir } from "@oh-my-pi/pi-utils";
7
6
  import { $ } from "bun";
@@ -11,7 +10,6 @@ import * as git from "../../../utils/git";
11
10
  import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color";
12
11
  import { sanitizeStatusText } from "../../shared";
13
12
  import { theme } from "../../theme/theme";
14
- import { computeNonMessageTokens } from "../../utils/context-usage";
15
13
  import { canReuseCachedPr, createPrCacheContext, isSamePrCacheContext, type PrCacheContext } from "./git-utils";
16
14
  import { getPreset } from "./presets";
17
15
  import { renderSegment, type SegmentContext } from "./segments";
@@ -26,30 +24,15 @@ import type {
26
24
  } from "./types";
27
25
 
28
26
  // ═══════════════════════════════════════════════════════════════════════════
29
- // Per-message token cache
27
+ // Context-usage memo
30
28
  // ═══════════════════════════════════════════════════════════════════════════
31
29
 
32
30
  /**
33
- * Symbol-keyed sidecar tagged onto each `AgentMessage` to memoize its
34
- * `estimateTokens` result. Keyed by message identity (the object itself);
35
- * a cheap content fingerprint detects in-place mutations (post-hoc error
36
- * attachment, retry-truncated branch rebuild, etc.) and forces recompute.
37
- *
38
- * Cache lives on the message — multiple `StatusLineComponent` instances
39
- * share it for free, and entries collect with the message itself when the
40
- * conversation is replaced or compacted.
41
- */
42
- const kTokenCache = Symbol("statusLine.tokenCache");
43
- interface TaggedMessage {
44
- [kTokenCache]?: { fingerprint: string; tokens: number };
45
- }
46
-
47
- /**
48
- * Cheap structural fingerprint mirroring `estimateTokens`'s content walk.
49
- * O(blocks) — only reads string `.length` and primitives, never copies or
50
- * serializes content. Any in-place mutation that alters total tokenized
51
- * content also alters one of the byte-length sums or block counts captured
52
- * here, forcing the cached `estimateTokens` value to be recomputed.
31
+ * Cheap structural fingerprint of a message's tokenizable content. O(blocks) —
32
+ * only reads string `.length` and primitives, never copies or serializes.
33
+ * Detects in-place growth of the streaming tail (and other in-place mutations)
34
+ * so the cached `getContextUsage()` result is recomputed when — and only when —
35
+ * the numbers it depends on change.
53
36
  */
54
37
  function messageFingerprint(msg: AgentMessage): string {
55
38
  const role = (msg as { role?: string }).role ?? "";
@@ -107,29 +90,17 @@ function messageFingerprint(msg: AgentMessage): string {
107
90
  return `${role}:${ts}:${textLen}:${blocks}:${images}`;
108
91
  }
109
92
 
110
- /**
111
- * Token count for a single message, using the per-message sidecar cache.
112
- * The caller MUST skip caching for the last message during streaming —
113
- * it may still be growing and its tokens belong recomputed each refresh.
114
- */
115
- function tokensForMessage(msg: AgentMessage): number {
116
- const fp = messageFingerprint(msg);
117
- const tagged = msg as TaggedMessage;
118
- const cached = tagged[kTokenCache];
119
- if (cached && cached.fingerprint === fp) return cached.tokens;
120
- const tokens = estimateTokens(msg);
121
- tagged[kTokenCache] = { fingerprint: fp, tokens };
122
- return tokens;
123
- }
124
-
125
- interface MessageTokenTotalsCache {
93
+ interface ContextUsageMemo {
126
94
  messagesRef: readonly AgentMessage[];
127
- stableCount: number;
128
- stableTokens: number;
129
- lastStableMessage: AgentMessage | undefined;
130
- lastStableFingerprint: string | undefined;
95
+ length: number;
96
+ lastFingerprint: string | undefined;
97
+ modelContextWindow: number;
98
+ usedTokens: number | null;
99
+ contextWindow: number;
131
100
  }
132
101
 
102
+ const EMPTY_MESSAGES: readonly AgentMessage[] = [];
103
+
133
104
  function hasContextSegment(segments: readonly StatusLineSegmentId[]): boolean {
134
105
  return segments.includes("context_pct") || segments.includes("context_total");
135
106
  }
@@ -176,19 +147,12 @@ export class StatusLineComponent implements Component {
176
147
  } | null = null;
177
148
  #usageFetchedAt = 0;
178
149
  #usageInFlight = false;
179
- // Context breakdown — incremental rolling cache. The status line refreshes
180
- // on every agent event, so the hot path must not re-tokenize the full
181
- // message list. Stable messages are accumulated once; normal streaming
182
- // refreshes only recompute the current tail message and newly appended
183
- // entries. History rewrites/compaction replace or shrink the message array
184
- // and rebuild this cache. Stable messages are treated as immutable after
185
- // promotion, matching the normal append-only session flow.
186
- // Cached non-message total (system prompt + tools + skills). Invalidated
187
- // when the inputs-identity fingerprint changes (model swap, skill toggle,
188
- // tool registration).
189
- #nonMessageTokensCache: number | undefined;
190
- #nonMessageInputsKey: string | undefined;
191
- #messageTokenTotalsCache: MessageTokenTotalsCache | undefined;
150
+ // Context-usage memo. The status line redraws on every agent event, so the
151
+ // hot path must not recompute context tokens unless an input changed.
152
+ // `getContextUsage()` anchors on the last assistant's real prompt-token
153
+ // count (matching the provider and the `/context` panel), so a stable
154
+ // message list + model window yields a stable result we can return verbatim.
155
+ #contextUsageCache: ContextUsageMemo | undefined;
192
156
 
193
157
  constructor(private session: AgentSession) {
194
158
  this.#settings = {
@@ -310,9 +274,7 @@ export class StatusLineComponent implements Component {
310
274
  this.#cachedUsage = null;
311
275
  this.#usageFetchedAt = 0;
312
276
  this.#usageInFlight = false;
313
- this.#nonMessageTokensCache = undefined;
314
- this.#nonMessageInputsKey = undefined;
315
- this.#messageTokenTotalsCache = undefined;
277
+ this.#contextUsageCache = undefined;
316
278
  this.#lastTokensPerSecond = null;
317
279
  this.#lastTokensPerSecondTimestamp = null;
318
280
  }
@@ -544,109 +506,44 @@ export class StatusLineComponent implements Component {
544
506
  }
545
507
 
546
508
  /**
547
- * Compute the (cached) used-tokens / context-window totals for the
548
- * status-line context% segment. Exposed (non-private) so unit tests can
549
- * verify the incremental-cache invariants; not part of any external
550
- * API.
509
+ * Used-tokens / context-window totals for the status-line context% segment,
510
+ * memoized so the per-event redraw stays O(1) when nothing changed.
511
+ *
512
+ * The numerator comes from `session.getContextUsage()`, which anchors on the
513
+ * last assistant's real prompt-token count — so the bar matches the provider
514
+ * and the `/context` panel — and reports `null` while that count is unknown
515
+ * (right after compaction, before the next response). Exposed (non-private)
516
+ * for unit tests and the collab host's state broadcast.
551
517
  */
552
- getCachedContextBreakdown(): { usedTokens: number; contextWindow: number } {
553
- const messages = this.session.messages ?? [];
554
- const contextWindow = this.session.model?.contextWindow ?? 0;
555
-
556
- // 1) Non-message tokens (system prompt + tools + skills). Refresh only
557
- // when the inputs identity fingerprint changes — usually never
558
- // during a streaming turn. ~10-30 ms when it does refresh.
559
- const inputsKey = this.#computeNonMessageInputsKey();
560
- if (this.#nonMessageTokensCache === undefined || this.#nonMessageInputsKey !== inputsKey) {
561
- this.#nonMessageTokensCache = computeNonMessageTokens(this.session);
562
- this.#nonMessageInputsKey = inputsKey;
563
- }
564
-
565
- // 2) Message tokens — incremental rolling total. The sidecar cache lives
566
- // on each stable message object (all but the current tail). Normal
567
- // streaming turns only recompute the last message and newly appended
568
- // entries. Full rebuild only when the message array is replaced,
569
- // shrinks, or the recently-promoted stable tail mutates in place.
570
- const messagesTokens = this.#getCachedMessageTokens(messages);
571
-
572
- const usedTokens = this.#nonMessageTokensCache + messagesTokens;
573
- return { usedTokens, contextWindow };
574
- }
575
-
576
- #getCachedMessageTokens(messages: readonly AgentMessage[]): number {
577
- const cache = this.#messageTokenTotalsCache;
578
- if (!cache || cache.messagesRef !== messages || messages.length <= cache.stableCount) {
579
- return this.#rebuildMessageTokenTotals(messages);
580
- }
581
-
582
- let stableTokens = cache.stableTokens;
583
- let stableCount = cache.stableCount;
584
- const stableLimit = Math.max(0, messages.length - 1);
518
+ getCachedContextBreakdown(): { usedTokens: number | null; contextWindow: number } {
519
+ const messages = this.session.messages ?? EMPTY_MESSAGES;
520
+ const modelContextWindow = this.session.model?.contextWindow ?? 0;
521
+ const length = messages.length;
522
+ const lastFingerprint = length > 0 ? messageFingerprint(messages[length - 1]!) : undefined;
585
523
 
524
+ const cache = this.#contextUsageCache;
586
525
  if (
587
- cache.lastStableMessage &&
588
- stableCount > 0 &&
589
- messages[stableCount - 1] === cache.lastStableMessage &&
590
- cache.lastStableFingerprint !== undefined &&
591
- cache.lastStableFingerprint !== messageFingerprint(cache.lastStableMessage)
526
+ cache &&
527
+ cache.messagesRef === messages &&
528
+ cache.length === length &&
529
+ cache.lastFingerprint === lastFingerprint &&
530
+ cache.modelContextWindow === modelContextWindow
592
531
  ) {
593
- return this.#rebuildMessageTokenTotals(messages);
594
- }
595
-
596
- while (stableCount < stableLimit) {
597
- const promoted = messages[stableCount]!;
598
- stableTokens += tokensForMessage(promoted);
599
- stableCount++;
600
- }
601
-
602
- const lastStableMessage = stableCount > 0 ? messages[stableCount - 1] : undefined;
603
- const lastStableFingerprint = lastStableMessage ? messageFingerprint(lastStableMessage) : undefined;
604
- const lastMessage = messages.at(-1);
605
- const lastTokens = lastMessage ? estimateTokens(lastMessage) : 0;
606
- this.#messageTokenTotalsCache = {
607
- messagesRef: messages,
608
- stableCount,
609
- stableTokens,
610
- lastStableMessage,
611
- lastStableFingerprint,
612
- };
613
- return stableTokens + lastTokens;
614
- }
615
-
616
- #rebuildMessageTokenTotals(messages: readonly AgentMessage[]): number {
617
- let stableTokens = 0;
618
- const stableLimit = Math.max(0, messages.length - 1);
619
- for (let i = 0; i < stableLimit; i++) {
620
- stableTokens += tokensForMessage(messages[i]!);
532
+ return { usedTokens: cache.usedTokens, contextWindow: cache.contextWindow };
621
533
  }
622
534
 
623
- const lastStableMessage = stableLimit > 0 ? messages[stableLimit - 1] : undefined;
624
- const lastStableFingerprint = lastStableMessage ? messageFingerprint(lastStableMessage) : undefined;
625
- const lastMessage = messages.at(-1);
626
- const lastTokens = lastMessage ? estimateTokens(lastMessage) : 0;
627
-
628
- this.#messageTokenTotalsCache = {
535
+ const usage = this.session.getContextUsage();
536
+ const usedTokens = usage?.tokens ?? null;
537
+ const contextWindow = usage?.contextWindow ?? modelContextWindow;
538
+ this.#contextUsageCache = {
629
539
  messagesRef: messages,
630
- stableCount: stableLimit,
631
- stableTokens,
632
- lastStableMessage,
633
- lastStableFingerprint,
540
+ length,
541
+ lastFingerprint,
542
+ modelContextWindow,
543
+ usedTokens,
544
+ contextWindow,
634
545
  };
635
- return stableTokens + lastTokens;
636
- }
637
-
638
- /**
639
- * Build an identity fingerprint for the non-message inputs (system prompt,
640
- * tools, skills). When this changes, the non-message token cache must be
641
- * recomputed. Cheap: just lengths + first-string-length. Doesn't need to
642
- * be cryptographically unique — only stable for the same inputs.
643
- */
644
- #computeNonMessageInputsKey(): string {
645
- const sp = this.session.systemPrompt ?? [];
646
- const tools = this.session.agent?.state?.tools ?? [];
647
- const skills = this.session.skills ?? [];
648
- const modelId = this.session.model?.id ?? "";
649
- return `${modelId}|${sp.length}:${sp[0]?.length ?? 0}|${tools.length}|${skills.length}`;
546
+ return { usedTokens, contextWindow };
650
547
  }
651
548
 
652
549
  #buildSegmentContext(
@@ -673,14 +570,14 @@ export class StatusLineComponent implements Component {
673
570
  tokensPerSecond: this.#getTokensPerSecond(),
674
571
  };
675
572
 
676
- let contextTokens = 0;
677
573
  let contextWindow = state.model?.contextWindow ?? this.session.model?.contextWindow ?? 0;
574
+ let contextPercent: number | null = 0;
678
575
  if (includeContext) {
679
576
  const breakdown = this.getCachedContextBreakdown();
680
- contextTokens = breakdown.usedTokens;
681
577
  contextWindow = breakdown.contextWindow || contextWindow;
578
+ contextPercent =
579
+ breakdown.usedTokens === null ? null : contextWindow > 0 ? (breakdown.usedTokens / contextWindow) * 100 : 0;
682
580
  }
683
- let contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
684
581
 
685
582
  // Collab guest: context comes from the host's state frames — the local
686
583
  // replica does no accounting of its own.
@@ -364,7 +364,7 @@ const contextPctSegment: StatusLineSegment = {
364
364
  const autoIcon = ctx.autoCompactEnabled && theme.icon.auto ? ` ${theme.icon.auto}` : "";
365
365
  const text = `${formatContextUsage(pct, window)}${autoIcon}`;
366
366
 
367
- const color = getContextUsageThemeColor(getContextUsageLevel(pct, window));
367
+ const color = getContextUsageThemeColor(getContextUsageLevel(pct ?? 0, window));
368
368
  const content = withIcon(theme.icon.context, theme.fg(color, text));
369
369
 
370
370
  return { content, visible: true };
@@ -71,7 +71,8 @@ export interface SegmentContext {
71
71
  cost: number;
72
72
  tokensPerSecond: number | null;
73
73
  };
74
- contextPercent: number;
74
+ /** Context usage percent, or null when unknown (e.g. right after compaction). */
75
+ contextPercent: number | null;
75
76
  contextWindow: number;
76
77
  autoCompactEnabled: boolean;
77
78
  subagentCount: number;
@@ -8,6 +8,7 @@ import {
8
8
  Image,
9
9
  ImageProtocol,
10
10
  imageFallback,
11
+ type NativeScrollbackLiveRegion,
11
12
  Spacer,
12
13
  TERMINAL,
13
14
  Text,
@@ -16,7 +17,7 @@ import {
16
17
  import { getProjectDir, logger, sanitizeText } from "@oh-my-pi/pi-utils";
17
18
  import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "../../edit";
18
19
  import type { Theme } from "../../modes/theme/theme";
19
- import { theme } from "../../modes/theme/theme";
20
+ import { getThemeEpoch, theme } from "../../modes/theme/theme";
20
21
  import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
21
22
  import { EVAL_DEFAULT_PREVIEW_LINES } from "../../tools/eval";
22
23
  import { isWaitingPollDetails } from "../../tools/job";
@@ -148,10 +149,16 @@ export interface ToolExecutionHandle {
148
149
  /** Drive pending-tool redraws at 30fps for live tool headers and displaceable
149
150
  * poll blocks. The TUI throttles at the same cadence, and static frames diff to
150
151
  * a no-op redraw at ~zero cost. */
151
- const SPINNER_RENDER_INTERVAL_MS = 1000 / 30;
152
+ export const SPINNER_RENDER_INTERVAL_MS = 1000 / 30;
152
153
  /** Advance the spinner glyph at its classic ~12.5fps step, decoupled from the
153
154
  * render cadence (mirrors `Loader`). */
154
- const SPINNER_GLYPH_ADVANCE_MS = 80;
155
+ export const SPINNER_GLYPH_ADVANCE_MS = 80;
156
+
157
+ /** Phase-locked spinner glyph index shared by every live tool block so parallel
158
+ * spinners advance in lockstep instead of each tracking its own start time. */
159
+ export function sharedSpinnerFrame(frameCount: number, now: number = performance.now()): number {
160
+ return frameCount > 0 ? Math.floor(now / SPINNER_GLYPH_ADVANCE_MS) % frameCount : 0;
161
+ }
155
162
 
156
163
  // Stable per-instance counter so each tool execution's inline images get a
157
164
  // graphics id that survives child re-creation (the image budget keys off it).
@@ -160,7 +167,7 @@ let toolExecutionInstanceSeq = 0;
160
167
  /**
161
168
  * Component that renders a tool call with its result (updateable)
162
169
  */
163
- export class ToolExecutionComponent extends Container {
170
+ export class ToolExecutionComponent extends Container implements NativeScrollbackLiveRegion {
164
171
  #contentBox: Box; // Used for custom tools and bash visual truncation
165
172
  #contentText: Text; // For built-in tools (with its own padding/bg)
166
173
  #multiFileBoxes: (Box | Spacer)[] = []; // Extra boxes for multi-file edit results
@@ -176,6 +183,22 @@ export class ToolExecutionComponent extends Container {
176
183
  #editAllowFuzzy: boolean | undefined;
177
184
  #snapshots?: SnapshotStore;
178
185
  #isPartial = true;
186
+ #resultVersion = 0;
187
+ #lastDisplayKey: string | undefined;
188
+ // Bumped whenever a render input that #rebuildDisplay consumes but the memo
189
+ // key cannot cheaply hash changes: streamed call args, the async edit-diff
190
+ // preview, and Kitty PNG conversions. Folded into the dirty key so those
191
+ // updates are not swallowed by the memo (see #updateDisplay).
192
+ #displayInputVersion = 0;
193
+ // Set once #rebuildDisplay has populated the display. Replaces a
194
+ // #contentBox.children.length probe so the memo fast-path also covers the
195
+ // #contentText fallback path (which leaves #contentBox empty).
196
+ #displayBuilt = false;
197
+ // Number of Image children the last rebuild emitted. Only when this is > 0 does
198
+ // the memo key fold in viewport-dependent image sizing (resolveImageOptions),
199
+ // so a terminal resize re-shapes image-bearing results to rescale them without
200
+ // forcing the common image-free result to re-shape on every resize tick.
201
+ #renderedImageCount = 0;
179
202
  #tool?: AgentTool;
180
203
  #ui: TUI;
181
204
  #cwd: string;
@@ -196,7 +219,6 @@ export class ToolExecutionComponent extends Container {
196
219
  // Spinner animation for partial task results
197
220
  #spinnerFrame?: number;
198
221
  #spinnerInterval?: NodeJS.Timeout;
199
- #lastSpinnerAdvanceAt = 0;
200
222
  // Todo write completion strikethrough reveal animation
201
223
  #todoStrikeInterval?: NodeJS.Timeout;
202
224
  // Track if args are still being streamed (for edit/write spinner)
@@ -281,6 +303,7 @@ export class ToolExecutionComponent extends Container {
281
303
  // signals "nothing meaningful changed" and the renderer can skip.
282
304
  if (args === this.#args) return;
283
305
  this.#args = args;
306
+ this.#displayInputVersion++;
284
307
  this.#updateSpinnerAnimation();
285
308
  this.#editDiffInFlight = this.#runPreviewDiff();
286
309
  this.#updateDisplay();
@@ -365,6 +388,7 @@ export class ToolExecutionComponent extends Container {
365
388
  if (controller.signal.aborted) return;
366
389
  if (previews) {
367
390
  this.#editDiffPreview = isStreaming ? stabilizeStreamingPreviews(previews) : previews;
391
+ this.#displayInputVersion++;
368
392
  this.#updateDisplay();
369
393
  this.#ui.requestRender();
370
394
  }
@@ -393,6 +417,7 @@ export class ToolExecutionComponent extends Container {
393
417
  return;
394
418
  }
395
419
  this.#result = result;
420
+ this.#resultVersion++;
396
421
  this.#isPartial = isPartial;
397
422
  // A `job` poll that found every watched job still running is transient
398
423
  // "still waiting" chrome; keep the block displaceable so the next `job`
@@ -446,6 +471,7 @@ export class ToolExecutionComponent extends Container {
446
471
  .toBase64()
447
472
  .then(data => {
448
473
  this.#convertedImages.set(index, { data, mimeType: "image/png" });
474
+ this.#displayInputVersion++;
449
475
  this.#updateDisplay();
450
476
  this.#ui.requestRender();
451
477
  })
@@ -470,32 +496,18 @@ export class ToolExecutionComponent extends Container {
470
496
  // once the block leaves the live region.
471
497
  const needsSpinner = isStreamingArgs || isPartialTask || this.isDisplaceableBlock();
472
498
  if (needsSpinner && !this.#spinnerInterval) {
473
- const now = performance.now();
474
499
  const frameCount = theme.spinnerFrames.length;
475
- this.#lastSpinnerAdvanceAt = now;
476
- if (frameCount > 0 && this.#spinnerFrame === undefined) {
477
- this.#spinnerFrame = 0;
478
- this.#renderState.spinnerFrame = 0;
479
- }
500
+ const frame = sharedSpinnerFrame(frameCount);
501
+ this.#spinnerFrame = frame;
502
+ this.#renderState.spinnerFrame = frame;
480
503
  this.#spinnerInterval = setInterval(() => {
481
504
  // If a detached task interval from an older render path is still live,
482
505
  // stop it the instant the block leaves the repaintable region.
483
506
  if (this.#maybeFreezeBackgroundTask()) return;
484
507
  const now = performance.now();
485
508
  const frameCount = theme.spinnerFrames.length;
486
- // Redraw at 30fps, but keep the spinner glyph phase-locked to its
487
- // classic ~12.5fps cadence. Advancing the anchor by elapsed frames
488
- // instead of resetting to `now` avoids the 30fps timer quantizing the
489
- // glyph down to one step every three ticks.
490
- if (frameCount > 0) {
491
- const elapsed = now - this.#lastSpinnerAdvanceAt;
492
- if (elapsed >= SPINNER_GLYPH_ADVANCE_MS) {
493
- const steps = Math.floor(elapsed / SPINNER_GLYPH_ADVANCE_MS);
494
- this.#spinnerFrame = ((this.#spinnerFrame ?? 0) + steps) % frameCount;
495
- this.#renderState.spinnerFrame = this.#spinnerFrame;
496
- this.#lastSpinnerAdvanceAt += steps * SPINNER_GLYPH_ADVANCE_MS;
497
- }
498
- }
509
+ this.#spinnerFrame = sharedSpinnerFrame(frameCount, now);
510
+ this.#renderState.spinnerFrame = this.#spinnerFrame;
499
511
  this.#ui.requestRender();
500
512
  }, SPINNER_RENDER_INTERVAL_MS);
501
513
  } else if (!needsSpinner && this.#spinnerInterval) {
@@ -568,6 +580,17 @@ export class ToolExecutionComponent extends Container {
568
580
  }
569
581
  }
570
582
 
583
+ /**
584
+ * Standalone harnesses may mount a tool component directly under `TUI`
585
+ * instead of inside `TranscriptContainer`. In that shape the component must
586
+ * report its own live-region seam for provisional previews, or the core
587
+ * renderer treats it like shell output and commits tail-window edit/eval/bash
588
+ * previews to immutable native scrollback before the result replaces them.
589
+ */
590
+ getNativeScrollbackLiveRegionStart(): number | undefined {
591
+ return !this.isTranscriptBlockFinalized() && !this.isTranscriptBlockCommitStable() ? 0 : undefined;
592
+ }
593
+
571
594
  /**
572
595
  * Whether this block has reached a terminal state for transcript freezing.
573
596
  * Reports `false` while it can still visually change so the
@@ -591,28 +614,20 @@ export class ToolExecutionComponent extends Container {
591
614
 
592
615
  /**
593
616
  * Whether this still-live block's settled rows may enter native scrollback
594
- * (see `FinalizableBlock.isTranscriptBlockCommitStable`). Classification is
595
- * per renderer (`ToolRenderer.provisionalPendingPreview`): tail-window
596
- * streaming views (edit's streamed-diff tail, bash/ssh command caps, eval
597
- * cells) are re-anchored top-first by the result render, so promoting
598
- * their visually static head — e.g. an edit preview idling on its last
599
- * frame while the apply + LSP pass runs — would strand a stale copy of
600
- * the call box above the final block the moment the result lands. Every
601
- * other pending preview streams top-anchored append-shaped rows the
602
- * result render preserves (a task call's context/assignment markdown, a
603
- * write's content), so it stays commit-eligible — a call taller than the
604
- * viewport scrolls into native history mid-stream instead of reading as
605
- * cut off until the result. Expanded blocks always stream top-anchored
606
- * (the over-tall write/eval scrollback contract). Displaceable waiting
607
- * polls are removed wholesale by the next poll and must never commit.
617
+ * (see `FinalizableBlock.isTranscriptBlockCommitStable`). Renderers classify
618
+ * pending views by durability instead of by tool name: a provisional view is
619
+ * allowed to be useful on screen, but finalization may replace or re-anchor
620
+ * it wholesale, so committing any of its rows would strand stale preview
621
+ * bytes in immutable scrollback. Non-provisional views stream rows whose
622
+ * committed prefix survives the remaining transitions.
608
623
  */
609
624
  isTranscriptBlockCommitStable(): boolean {
610
625
  if (this.#displaceable) return false;
611
- if (this.#expanded || this.isTranscriptBlockFinalized()) return true;
612
- if ((this.#tool as { provisionalPendingPreview?: boolean } | undefined)?.provisionalPendingPreview) {
613
- return false;
614
- }
615
- return !toolRenderers[this.#toolName]?.provisionalPendingPreview;
626
+ if (this.isTranscriptBlockFinalized()) return true;
627
+ const tool = this.#tool as { provisionalPendingPreview?: boolean | "collapsed" } | undefined;
628
+ const provisionalPendingPreview =
629
+ tool?.provisionalPendingPreview ?? toolRenderers[this.#toolName]?.provisionalPendingPreview;
630
+ return provisionalPendingPreview !== true && (provisionalPendingPreview !== "collapsed" || this.#expanded);
616
631
  }
617
632
 
618
633
  /**
@@ -674,6 +689,29 @@ export class ToolExecutionComponent extends Container {
674
689
  }
675
690
 
676
691
  #updateDisplay(): void {
692
+ // `TERMINAL.imageProtocol` is resolved by an async capability probe during
693
+ // TUI startup, so a result rendered before it lands must re-shape once it
694
+ // does (it gates Image children vs text fallback in #rebuildDisplay); keyed
695
+ // here for the same reason markdown.ts keys its render cache on it.
696
+ const key = `${this.#resultVersion}|${this.#expanded}|${this.#isPartial}|${this.#spinnerFrame ?? "-"}|${this.#showImages}|${getThemeEpoch()}|${this.#displayInputVersion}|${this.#backgroundTaskFrozen}|${TERMINAL.imageProtocol ?? "-"}|${this.#imageSizeKey()}`;
697
+ if (key === this.#lastDisplayKey && this.#displayBuilt) return;
698
+ this.#lastDisplayKey = key;
699
+
700
+ this.#rebuildDisplay();
701
+ this.#displayBuilt = true;
702
+ }
703
+
704
+ // Viewport-/settings-dependent image sizing folded into the memo key only when
705
+ // the last rebuild actually emitted images, so a terminal resize re-shapes an
706
+ // image-bearing result (to rescale it) without re-shaping every image-free
707
+ // result on each resize tick.
708
+ #imageSizeKey(): string {
709
+ if (this.#renderedImageCount === 0) return "-";
710
+ const o = resolveImageOptions();
711
+ return `${o.maxWidthCells}:${o.maxHeightCells ?? "-"}`;
712
+ }
713
+
714
+ #rebuildDisplay(): void {
677
715
  // Sync shared mutable render state for component closures
678
716
  this.#renderState.expanded = this.#expanded;
679
717
  this.#renderState.isPartial = this.#isPartial;
@@ -917,6 +955,7 @@ export class ToolExecutionComponent extends Container {
917
955
  }
918
956
  }
919
957
  }
958
+ this.#renderedImageCount = this.#imageComponents.length;
920
959
  }
921
960
 
922
961
  #getCallArgsForRender(): any {