@oh-my-pi/pi-coding-agent 15.10.9 → 15.10.11

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 (352) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/dist/cli.js +23087 -0
  3. package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
  4. package/dist/types/async/job-manager.d.ts +18 -0
  5. package/dist/types/cli/args.d.ts +1 -1
  6. package/dist/types/cli/dry-balance-cli.d.ts +1 -1
  7. package/dist/types/cli/gallery-cli.d.ts +1 -1
  8. package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
  9. package/dist/types/cli/usage-cli.d.ts +72 -0
  10. package/dist/types/commands/launch.d.ts +1 -1
  11. package/dist/types/commands/read.d.ts +1 -1
  12. package/dist/types/commands/usage.d.ts +25 -0
  13. package/dist/types/config/append-only-context-mode.d.ts +2 -1
  14. package/dist/types/config/model-discovery.d.ts +55 -0
  15. package/dist/types/config/model-registry.d.ts +20 -219
  16. package/dist/types/config/model-resolver.d.ts +16 -10
  17. package/dist/types/config/model-roles.d.ts +28 -0
  18. package/dist/types/config/models-config-schema.d.ts +523 -42
  19. package/dist/types/config/models-config.d.ts +385 -0
  20. package/dist/types/config/settings-schema.d.ts +12 -16
  21. package/dist/types/config/settings.d.ts +1 -1
  22. package/dist/types/debug/log-viewer.d.ts +1 -1
  23. package/dist/types/debug/raw-sse.d.ts +1 -1
  24. package/dist/types/debug/terminal-info.d.ts +0 -1
  25. package/dist/types/eval/backend.d.ts +0 -2
  26. package/dist/types/eval/idle-timeout.d.ts +0 -4
  27. package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
  28. package/dist/types/export/html/template.generated.d.ts +1 -1
  29. package/dist/types/extensibility/extensions/types.d.ts +3 -3
  30. package/dist/types/hindsight/mental-models.d.ts +17 -8
  31. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  32. package/dist/types/internal-urls/types.d.ts +1 -1
  33. package/dist/types/lsp/edits.d.ts +9 -0
  34. package/dist/types/lsp/index.d.ts +2 -2
  35. package/dist/types/lsp/types.d.ts +2 -0
  36. package/dist/types/lsp/utils.d.ts +3 -0
  37. package/dist/types/mcp/json-rpc.d.ts +5 -0
  38. package/dist/types/mnemopi/state.d.ts +11 -1
  39. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  40. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  41. package/dist/types/modes/components/bash-execution.d.ts +1 -1
  42. package/dist/types/modes/components/copy-selector.d.ts +1 -1
  43. package/dist/types/modes/components/dynamic-border.d.ts +1 -1
  44. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
  45. package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
  46. package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
  47. package/dist/types/modes/components/footer.d.ts +1 -1
  48. package/dist/types/modes/components/hook-editor.d.ts +5 -0
  49. package/dist/types/modes/components/hook-input.d.ts +4 -0
  50. package/dist/types/modes/components/hook-selector.d.ts +1 -1
  51. package/dist/types/modes/components/model-selector.d.ts +1 -1
  52. package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
  53. package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
  54. package/dist/types/modes/components/session-selector.d.ts +1 -1
  55. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  56. package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
  57. package/dist/types/modes/components/transcript-container.d.ts +31 -26
  58. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  59. package/dist/types/modes/components/user-message-selector.d.ts +1 -1
  60. package/dist/types/modes/components/user-message.d.ts +2 -1
  61. package/dist/types/modes/components/visual-truncate.d.ts +1 -1
  62. package/dist/types/modes/components/welcome.d.ts +19 -3
  63. package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
  64. package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
  65. package/dist/types/modes/interactive-mode.d.ts +1 -1
  66. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  67. package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
  68. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  69. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
  70. package/dist/types/modes/types.d.ts +2 -1
  71. package/dist/types/session/agent-session.d.ts +1 -1
  72. package/dist/types/session/auth-broker-config.d.ts +4 -0
  73. package/dist/types/session/session-manager.d.ts +1 -1
  74. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
  75. package/dist/types/ssh/connection-manager.d.ts +8 -0
  76. package/dist/types/task/discovery.d.ts +1 -2
  77. package/dist/types/task/parallel.d.ts +2 -2
  78. package/dist/types/task/worktree.d.ts +2 -0
  79. package/dist/types/tiny/title-client.d.ts +1 -1
  80. package/dist/types/tools/ask.d.ts +4 -0
  81. package/dist/types/tools/conflict-detect.d.ts +16 -0
  82. package/dist/types/tools/github-cache.d.ts +7 -0
  83. package/dist/types/tools/sqlite-reader.d.ts +3 -0
  84. package/dist/types/tools/todo.d.ts +2 -0
  85. package/dist/types/tui/output-block.d.ts +3 -3
  86. package/dist/types/utils/changelog.d.ts +8 -0
  87. package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
  88. package/dist/types/web/scrapers/types.d.ts +12 -0
  89. package/dist/types/web/search/providers/codex.d.ts +1 -1
  90. package/dist/types/web/search/providers/gemini.d.ts +1 -1
  91. package/examples/extensions/tools.ts +5 -4
  92. package/package.json +14 -11
  93. package/scripts/build-binary.ts +18 -23
  94. package/scripts/bundle-dist.ts +81 -0
  95. package/scripts/{dev-launch → omp} +1 -1
  96. package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
  97. package/src/async/job-manager.ts +57 -3
  98. package/src/autoresearch/dashboard.ts +1 -1
  99. package/src/autoresearch/prompt-setup.md +6 -6
  100. package/src/autoresearch/prompt.md +6 -6
  101. package/src/capability/fs.ts +10 -0
  102. package/src/cli/args.ts +1 -1
  103. package/src/cli/auth-gateway-cli.ts +1 -3
  104. package/src/cli/dry-balance-cli.ts +1 -1
  105. package/src/cli/gallery-cli.ts +1 -1
  106. package/src/cli/gallery-fixtures/fs.ts +1 -1
  107. package/src/cli/gallery-fixtures/types.ts +5 -1
  108. package/src/cli/list-models.ts +7 -12
  109. package/src/cli/usage-cli.ts +603 -0
  110. package/src/cli-commands.ts +1 -0
  111. package/src/cli.ts +69 -5
  112. package/src/commands/complete.ts +1 -1
  113. package/src/commands/launch.ts +1 -1
  114. package/src/commands/read.ts +6 -3
  115. package/src/commands/usage.ts +35 -0
  116. package/src/commit/agentic/agent.ts +1 -1
  117. package/src/commit/model-selection.ts +1 -1
  118. package/src/config/append-only-context-mode.ts +6 -12
  119. package/src/config/model-discovery.ts +554 -0
  120. package/src/config/model-registry.ts +308 -1025
  121. package/src/config/model-resolver.ts +113 -156
  122. package/src/config/model-roles.ts +74 -0
  123. package/src/config/models-config-schema.ts +57 -8
  124. package/src/config/models-config.ts +129 -0
  125. package/src/config/settings-schema.ts +18 -14
  126. package/src/config/settings.ts +37 -1
  127. package/src/dap/client.ts +124 -37
  128. package/src/dap/session.ts +259 -158
  129. package/src/debug/log-viewer.ts +1 -1
  130. package/src/debug/raw-sse.ts +1 -1
  131. package/src/debug/terminal-info.ts +0 -3
  132. package/src/edit/diff.ts +95 -18
  133. package/src/edit/hashline/block-resolver.ts +20 -1
  134. package/src/edit/hashline/diff.ts +36 -1
  135. package/src/edit/hashline/execute.ts +8 -2
  136. package/src/edit/index.ts +16 -1
  137. package/src/edit/modes/patch.ts +52 -0
  138. package/src/edit/modes/replace.ts +56 -22
  139. package/src/edit/notebook.ts +22 -2
  140. package/src/edit/renderer.ts +36 -10
  141. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  142. package/src/eval/backend.ts +0 -2
  143. package/src/eval/completion-bridge.ts +2 -1
  144. package/src/eval/idle-timeout.ts +2 -9
  145. package/src/eval/js/context-manager.ts +6 -8
  146. package/src/eval/js/executor.ts +6 -2
  147. package/src/eval/js/index.ts +0 -2
  148. package/src/eval/js/shared/helpers.ts +5 -6
  149. package/src/eval/js/shared/local-module-loader.ts +1 -1
  150. package/src/eval/js/shared/prelude.txt +62 -1
  151. package/src/eval/js/shared/rewrite-imports.ts +49 -23
  152. package/src/eval/js/shared/runtime.ts +1 -1
  153. package/src/eval/py/index.ts +0 -2
  154. package/src/eval/py/kernel.ts +19 -0
  155. package/src/eval/py/runner.py +107 -3
  156. package/src/exec/bash-executor.ts +3 -1
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/export/html/template.js +3 -1
  159. package/src/extensibility/extensions/types.ts +3 -2
  160. package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
  161. package/src/hindsight/mental-models.ts +59 -12
  162. package/src/hindsight/state.ts +6 -1
  163. package/src/internal-urls/artifact-protocol.ts +11 -2
  164. package/src/internal-urls/docs-index.generated.ts +10 -10
  165. package/src/internal-urls/issue-pr-protocol.ts +12 -5
  166. package/src/internal-urls/router.ts +1 -1
  167. package/src/internal-urls/types.ts +1 -1
  168. package/src/lib/xai-http.ts +1 -1
  169. package/src/lsp/client.ts +118 -38
  170. package/src/lsp/clients/biome-client.ts +101 -39
  171. package/src/lsp/edits.ts +143 -95
  172. package/src/lsp/index.ts +31 -22
  173. package/src/lsp/render.ts +1 -1
  174. package/src/lsp/types.ts +2 -0
  175. package/src/lsp/utils.ts +28 -10
  176. package/src/main.ts +165 -17
  177. package/src/mcp/json-rpc.ts +35 -5
  178. package/src/mcp/transports/stdio.ts +7 -1
  179. package/src/memories/index.ts +2 -1
  180. package/src/mnemopi/backend.ts +25 -3
  181. package/src/mnemopi/state.ts +38 -2
  182. package/src/modes/components/agent-dashboard.ts +10 -7
  183. package/src/modes/components/assistant-message.ts +19 -13
  184. package/src/modes/components/bash-execution.ts +1 -1
  185. package/src/modes/components/copy-selector.ts +1 -1
  186. package/src/modes/components/diff.ts +13 -2
  187. package/src/modes/components/dynamic-border.ts +12 -3
  188. package/src/modes/components/extensions/extension-dashboard.ts +8 -5
  189. package/src/modes/components/extensions/extension-list.ts +1 -1
  190. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  191. package/src/modes/components/footer.ts +1 -1
  192. package/src/modes/components/history-search.ts +1 -1
  193. package/src/modes/components/hook-editor.ts +8 -0
  194. package/src/modes/components/hook-input.ts +8 -0
  195. package/src/modes/components/hook-selector.ts +2 -2
  196. package/src/modes/components/model-selector.ts +66 -54
  197. package/src/modes/components/plan-review-overlay.ts +1 -1
  198. package/src/modes/components/session-observer-overlay.ts +2 -2
  199. package/src/modes/components/session-selector.ts +1 -1
  200. package/src/modes/components/settings-selector.ts +5 -1
  201. package/src/modes/components/status-line/component.ts +1 -1
  202. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  203. package/src/modes/components/transcript-container.ts +373 -141
  204. package/src/modes/components/tree-selector.ts +3 -3
  205. package/src/modes/components/user-message-selector.ts +1 -1
  206. package/src/modes/components/user-message.ts +17 -5
  207. package/src/modes/components/visual-truncate.ts +1 -1
  208. package/src/modes/components/welcome.ts +108 -26
  209. package/src/modes/controllers/command-controller.ts +10 -3
  210. package/src/modes/controllers/event-controller.ts +73 -49
  211. package/src/modes/controllers/input-controller.ts +5 -5
  212. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  213. package/src/modes/controllers/selector-controller.ts +1 -5
  214. package/src/modes/controllers/streaming-reveal.ts +85 -18
  215. package/src/modes/interactive-mode.ts +5 -19
  216. package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
  217. package/src/modes/setup-wizard/scenes/providers.ts +1 -1
  218. package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
  219. package/src/modes/setup-wizard/scenes/theme.ts +1 -1
  220. package/src/modes/setup-wizard/scenes/types.ts +1 -1
  221. package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
  222. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  223. package/src/modes/types.ts +2 -1
  224. package/src/prompts/agents/explore.md +2 -2
  225. package/src/prompts/agents/librarian.md +1 -2
  226. package/src/prompts/agents/oracle.md +1 -1
  227. package/src/prompts/agents/plan.md +5 -5
  228. package/src/prompts/agents/task.md +5 -5
  229. package/src/prompts/ci-green-request.md +5 -7
  230. package/src/prompts/goals/goal-budget-limit.md +2 -2
  231. package/src/prompts/goals/goal-continuation.md +4 -4
  232. package/src/prompts/goals/goal-mode-active.md +1 -1
  233. package/src/prompts/memories/read-path.md +1 -1
  234. package/src/prompts/memories/stage_one_system.md +2 -2
  235. package/src/prompts/review-custom-request.md +1 -1
  236. package/src/prompts/system/agent-creation-architect.md +2 -2
  237. package/src/prompts/system/auto-continue.md +1 -1
  238. package/src/prompts/system/background-tan-dispatch.md +1 -1
  239. package/src/prompts/system/btw-user.md +2 -2
  240. package/src/prompts/system/commit-message-system.md +13 -1
  241. package/src/prompts/system/custom-system-prompt.md +1 -1
  242. package/src/prompts/system/eager-todo.md +2 -2
  243. package/src/prompts/system/irc-incoming.md +1 -1
  244. package/src/prompts/system/manual-continue.md +1 -1
  245. package/src/prompts/system/omfg-user.md +3 -4
  246. package/src/prompts/system/orchestrate-notice.md +9 -9
  247. package/src/prompts/system/plan-mode-active.md +4 -4
  248. package/src/prompts/system/plan-mode-subagent.md +4 -5
  249. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  250. package/src/prompts/system/project-prompt.md +2 -2
  251. package/src/prompts/system/subagent-system-prompt.md +4 -4
  252. package/src/prompts/system/system-prompt.md +15 -26
  253. package/src/prompts/system/title-system.md +2 -2
  254. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  255. package/src/prompts/system/workflow-notice.md +1 -1
  256. package/src/prompts/tools/ast-edit.md +1 -1
  257. package/src/prompts/tools/ast-grep.md +2 -2
  258. package/src/prompts/tools/bash.md +8 -10
  259. package/src/prompts/tools/browser.md +7 -7
  260. package/src/prompts/tools/debug.md +1 -1
  261. package/src/prompts/tools/eval.md +3 -3
  262. package/src/prompts/tools/find.md +0 -1
  263. package/src/prompts/tools/github.md +8 -7
  264. package/src/prompts/tools/goal.md +1 -1
  265. package/src/prompts/tools/image-gen.md +1 -1
  266. package/src/prompts/tools/inspect-image-system.md +1 -1
  267. package/src/prompts/tools/irc.md +15 -15
  268. package/src/prompts/tools/lsp.md +2 -2
  269. package/src/prompts/tools/patch.md +2 -2
  270. package/src/prompts/tools/read.md +3 -4
  271. package/src/prompts/tools/recall.md +1 -1
  272. package/src/prompts/tools/reflect.md +1 -1
  273. package/src/prompts/tools/render-mermaid.md +2 -2
  274. package/src/prompts/tools/replace.md +4 -10
  275. package/src/prompts/tools/rewind.md +2 -2
  276. package/src/prompts/tools/search-tool-bm25.md +1 -9
  277. package/src/prompts/tools/search.md +0 -1
  278. package/src/prompts/tools/ssh.md +0 -4
  279. package/src/prompts/tools/task.md +2 -3
  280. package/src/prompts/tools/todo.md +6 -2
  281. package/src/sdk.ts +23 -10
  282. package/src/session/agent-session.ts +44 -10
  283. package/src/session/auth-broker-config.ts +30 -1
  284. package/src/session/session-manager.ts +2 -2
  285. package/src/session/streaming-output.ts +23 -2
  286. package/src/slash-commands/builtin-registry.ts +20 -0
  287. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  288. package/src/ssh/connection-manager.ts +27 -0
  289. package/src/task/commands.ts +2 -1
  290. package/src/task/discovery.ts +17 -24
  291. package/src/task/executor.ts +61 -53
  292. package/src/task/index.ts +137 -60
  293. package/src/task/parallel.ts +3 -3
  294. package/src/task/render.ts +2 -2
  295. package/src/task/worktree.ts +64 -56
  296. package/src/thinking.ts +2 -1
  297. package/src/tiny/title-client.ts +32 -14
  298. package/src/tools/archive-reader.ts +30 -2
  299. package/src/tools/ask.ts +104 -21
  300. package/src/tools/ast-edit.ts +25 -5
  301. package/src/tools/auto-generated-guard.ts +20 -3
  302. package/src/tools/bash-interactive.ts +27 -7
  303. package/src/tools/bash.ts +54 -13
  304. package/src/tools/browser/launch.ts +11 -2
  305. package/src/tools/browser/readable.ts +19 -2
  306. package/src/tools/browser/registry.ts +4 -1
  307. package/src/tools/browser/render.ts +2 -2
  308. package/src/tools/browser/tab-supervisor.ts +55 -16
  309. package/src/tools/conflict-detect.ts +50 -4
  310. package/src/tools/debug.ts +1 -1
  311. package/src/tools/eval-render.ts +5 -5
  312. package/src/tools/eval.ts +0 -2
  313. package/src/tools/fetch.ts +33 -10
  314. package/src/tools/gh-cache-invalidation.ts +63 -8
  315. package/src/tools/gh-renderer.ts +1 -1
  316. package/src/tools/gh.ts +172 -29
  317. package/src/tools/github-cache.ts +70 -6
  318. package/src/tools/image-gen.ts +3 -9
  319. package/src/tools/irc.ts +5 -1
  320. package/src/tools/job.ts +1 -1
  321. package/src/tools/read.ts +202 -61
  322. package/src/tools/render-utils.ts +3 -3
  323. package/src/tools/resolve.ts +1 -1
  324. package/src/tools/search.ts +92 -29
  325. package/src/tools/sqlite-reader.ts +17 -5
  326. package/src/tools/ssh.ts +8 -8
  327. package/src/tools/todo.ts +51 -12
  328. package/src/tools/write.ts +118 -18
  329. package/src/tui/output-block.ts +4 -4
  330. package/src/utils/changelog.ts +27 -1
  331. package/src/utils/file-mentions.ts +2 -1
  332. package/src/web/scrapers/arxiv.ts +1 -1
  333. package/src/web/scrapers/go-pkg.ts +1 -1
  334. package/src/web/scrapers/iacr.ts +1 -1
  335. package/src/web/scrapers/readthedocs.ts +1 -1
  336. package/src/web/scrapers/twitter.ts +2 -1
  337. package/src/web/scrapers/types.ts +87 -8
  338. package/src/web/scrapers/wikipedia.ts +1 -1
  339. package/src/web/scrapers/youtube.ts +6 -1
  340. package/src/web/search/index.ts +1 -1
  341. package/src/web/search/providers/anthropic.ts +8 -2
  342. package/src/web/search/providers/codex.ts +2 -1
  343. package/src/web/search/providers/gemini.ts +2 -3
  344. package/src/web/search/render.ts +8 -6
  345. package/dist/types/config/model-equivalence.d.ts +0 -24
  346. package/dist/types/config/model-id-affixes.d.ts +0 -12
  347. package/dist/types/config/model-provider-priority.d.ts +0 -1
  348. package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
  349. package/src/config/model-equivalence.ts +0 -875
  350. package/src/config/model-id-affixes.ts +0 -81
  351. package/src/config/model-provider-priority.ts +0 -56
  352. package/src/exec/idle-timeout-watchdog.ts +0 -126
@@ -438,7 +438,7 @@ class TreeList implements Component {
438
438
  }
439
439
  }
440
440
 
441
- render(width: number): string[] {
441
+ render(width: number): readonly string[] {
442
442
  const lines: string[] = [];
443
443
 
444
444
  if (this.#filteredNodes.length === 0) {
@@ -835,7 +835,7 @@ class SearchLine implements Component {
835
835
 
836
836
  invalidate(): void {}
837
837
 
838
- render(width: number): string[] {
838
+ render(width: number): readonly string[] {
839
839
  const query = this.treeList.getSearchQuery();
840
840
  if (query) {
841
841
  return [truncateToWidth(` ${theme.fg("muted", "Search:")} ${theme.fg("accent", query)}`, width)];
@@ -864,7 +864,7 @@ class LabelInput implements Component {
864
864
 
865
865
  invalidate(): void {}
866
866
 
867
- render(width: number): string[] {
867
+ render(width: number): readonly string[] {
868
868
  const lines: string[] = [];
869
869
  const indent = " ";
870
870
  const availableWidth = width - indent.length;
@@ -82,7 +82,7 @@ class UserMessageList implements Component {
82
82
  return true;
83
83
  }
84
84
 
85
- render(width: number): string[] {
85
+ render(width: number): readonly string[] {
86
86
  const lines: string[] = [];
87
87
 
88
88
  if (this.messages.length === 0) {
@@ -12,6 +12,13 @@ const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
12
12
  * Component that renders a user message
13
13
  */
14
14
  export class UserMessageComponent extends Container {
15
+ // Memoized OSC 133 zone wrapping keyed on the underlying container render
16
+ // (same source ref ⇒ identical rows ⇒ reuse the wrapped copy). Keeps this
17
+ // component reference-stable for the transcript's incremental assembly and
18
+ // never mutates the container's cached array.
19
+ #zoneSource: readonly string[] | undefined;
20
+ #zoneLines: string[] | undefined;
21
+
15
22
  constructor(text: string, synthetic = false, imageLinks?: readonly (string | undefined)[]) {
16
23
  super();
17
24
  const bgColor = (value: string) => theme.bg("userMessageBg", value);
@@ -41,14 +48,19 @@ export class UserMessageComponent extends Container {
41
48
  );
42
49
  }
43
50
 
44
- override render(width: number): string[] {
51
+ override render(width: number): readonly string[] {
45
52
  const lines = super.render(width);
46
53
  if (lines.length === 0) {
47
54
  return lines;
48
55
  }
49
-
50
- lines[0] = OSC133_ZONE_START + lines[0];
51
- lines[lines.length - 1] = lines[lines.length - 1] + OSC133_ZONE_END + OSC133_ZONE_FINAL;
52
- return lines;
56
+ if (this.#zoneSource === lines && this.#zoneLines !== undefined) {
57
+ return this.#zoneLines;
58
+ }
59
+ const wrapped = lines.slice();
60
+ wrapped[0] = OSC133_ZONE_START + wrapped[0];
61
+ wrapped[wrapped.length - 1] = wrapped[wrapped.length - 1] + OSC133_ZONE_END + OSC133_ZONE_FINAL;
62
+ this.#zoneSource = lines;
63
+ this.#zoneLines = wrapped;
64
+ return wrapped;
53
65
  }
54
66
  }
@@ -6,7 +6,7 @@ import { Text } from "@oh-my-pi/pi-tui";
6
6
 
7
7
  export interface VisualTruncateResult {
8
8
  /** The visual lines to display */
9
- visualLines: string[];
9
+ visualLines: readonly string[];
10
10
  /** Number of visual lines that were skipped (hidden) */
11
11
  skippedCount: number;
12
12
  }
@@ -17,6 +17,24 @@ const TIPS: readonly string[] = tipsText
17
17
  .map(line => line.trim())
18
18
  .filter(line => line.length > 0);
19
19
 
20
+ /**
21
+ * Tip chosen once per process so the pre-TUI startup splash and the in-TUI
22
+ * welcome screen show the same tip instead of shuffling on the swap.
23
+ */
24
+ const PROCESS_TIP: string | undefined = TIPS.length > 0 ? TIPS[Math.floor(Math.random() * TIPS.length)] : undefined;
25
+
26
+ /**
27
+ * Fixed number of session rows in the welcome box so its height doesn't shift
28
+ * between the pre-TUI splash (loading placeholder) and the loaded state.
29
+ */
30
+ export const WELCOME_SESSION_SLOTS = 4;
31
+
32
+ /**
33
+ * Fixed number of LSP-server rows, for the same reason. Overflow is sliced so
34
+ * the box height is constant regardless of how many servers a project has.
35
+ */
36
+ export const WELCOME_LSP_SLOTS = 4;
37
+
20
38
  export function renderWelcomeTip(tip: string, boxWidth: number): string[] {
21
39
  const label = "Tip: ";
22
40
  const labelWidth = visibleWidth(label);
@@ -48,7 +66,7 @@ export interface RecentSession {
48
66
 
49
67
  export interface LspServerInfo {
50
68
  name: string;
51
- status: "ready" | "error" | "connecting";
69
+ status: "ready" | "error" | "connecting" | "available";
52
70
  fileTypes: string[];
53
71
  }
54
72
 
@@ -58,18 +76,38 @@ export interface LspServerInfo {
58
76
  export class WelcomeComponent implements Component {
59
77
  #animStart: number | null = null;
60
78
  #animTimer: ReturnType<typeof setInterval> | null = null;
61
- /** Tip chosen once per instance so re-renders (intro, LSP updates) don't shuffle it. */
62
- readonly #tip: string | undefined = TIPS.length > 0 ? TIPS[Math.floor(Math.random() * TIPS.length)] : undefined;
79
+ /** When set, a non-animating render shows the intro's first frame instead of the resting frame. */
80
+ #holdIntroFirstFrame = false;
81
+ /** Per-process tip so re-renders (intro, LSP updates, splash swap) don't shuffle it. */
82
+ readonly #tip: string | undefined = PROCESS_TIP;
83
+ // Render cache: the welcome box is the first transcript-area component, so
84
+ // returning a stable array reference keeps the whole frame prefix stable.
85
+ // Bypassed while the intro animation runs (every frame differs).
86
+ #cachedWidth = -1;
87
+ #cachedLines: string[] | undefined;
63
88
 
64
89
  constructor(
65
90
  private readonly version: string,
66
91
  private modelName: string,
67
92
  private providerName: string,
68
- private recentSessions: RecentSession[] = [],
93
+ private recentSessions: RecentSession[] | null = [],
69
94
  private lspServers: LspServerInfo[] = [],
70
95
  ) {}
71
96
 
72
- invalidate(): void {}
97
+ invalidate(): void {
98
+ this.#cachedWidth = -1;
99
+ this.#cachedLines = undefined;
100
+ }
101
+
102
+ /**
103
+ * Freeze the logo on the intro animation's first frame. The pre-TUI startup
104
+ * splash uses this so the in-TUI intro — which starts at that exact frame —
105
+ * picks up seamlessly from the splash's static box.
106
+ */
107
+ holdIntroFirstFrame(): void {
108
+ this.#holdIntroFirstFrame = true;
109
+ this.invalidate();
110
+ }
73
111
 
74
112
  /**
75
113
  * Play a one-shot intro that sweeps the gradient through every phase
@@ -78,6 +116,7 @@ export class WelcomeComponent implements Component {
78
116
  */
79
117
  playIntro(requestRender: () => void): void {
80
118
  this.#stopAnimation();
119
+ this.#holdIntroFirstFrame = false;
81
120
  this.#animStart = performance.now();
82
121
  requestRender();
83
122
  this.#animTimer = setInterval(() => {
@@ -95,22 +134,43 @@ export class WelcomeComponent implements Component {
95
134
  this.#animTimer = null;
96
135
  }
97
136
  this.#animStart = null;
137
+ // The settled (resting) frame differs from the last intro frame.
138
+ this.invalidate();
98
139
  }
99
140
 
100
141
  setModel(modelName: string, providerName: string): void {
101
142
  this.modelName = modelName;
102
143
  this.providerName = providerName;
144
+ this.invalidate();
103
145
  }
104
146
 
105
147
  setRecentSessions(sessions: RecentSession[]): void {
106
148
  this.recentSessions = sessions;
149
+ this.invalidate();
107
150
  }
108
151
 
109
152
  setLspServers(servers: LspServerInfo[]): void {
110
153
  this.lspServers = servers;
154
+ this.invalidate();
111
155
  }
112
156
 
113
- render(termWidth: number): string[] {
157
+ render(termWidth: number): readonly string[] {
158
+ const animating = this.#animStart != null;
159
+ if (!animating && this.#cachedLines && this.#cachedWidth === termWidth) {
160
+ return this.#cachedLines;
161
+ }
162
+ const lines = this.#renderLines(termWidth);
163
+ if (animating) {
164
+ this.#cachedLines = undefined;
165
+ this.#cachedWidth = -1;
166
+ } else {
167
+ this.#cachedLines = lines;
168
+ this.#cachedWidth = termWidth;
169
+ }
170
+ return lines;
171
+ }
172
+
173
+ #renderLines(termWidth: number): string[] {
114
174
  // Box dimensions - responsive with max width and small-terminal support
115
175
  const maxWidth = 100;
116
176
  const boxWidth = Math.min(maxWidth, Math.max(0, termWidth - 2));
@@ -157,7 +217,9 @@ export class WelcomeComponent implements Component {
157
217
 
158
218
  // Recent sessions content
159
219
  const sessionLines: string[] = [];
160
- if (this.recentSessions.length === 0) {
220
+ if (this.recentSessions === null) {
221
+ sessionLines.push(` ${theme.fg("dim", "Loading…")}`);
222
+ } else if (this.recentSessions.length === 0) {
161
223
  sessionLines.push(` ${theme.fg("dim", "No recent sessions")}`);
162
224
  } else {
163
225
  // Reserve width for the bullet prefix (" • ") and the trailing " (timeAgo)"
@@ -165,7 +227,7 @@ export class WelcomeComponent implements Component {
165
227
  // absorbs whatever space is left.
166
228
  const bulletPrefix = ` ${theme.md.bullet} `;
167
229
  const prefixWidth = visibleWidth(bulletPrefix);
168
- for (const session of this.recentSessions.slice(0, 3)) {
230
+ for (const session of this.recentSessions.slice(0, WELCOME_SESSION_SLOTS)) {
169
231
  const timeSuffixRaw = ` (${session.timeAgo})`;
170
232
  const timeWidth = visibleWidth(timeSuffixRaw);
171
233
  const nameBudget = Math.max(1, rightCol - prefixWidth - timeWidth);
@@ -176,23 +238,33 @@ export class WelcomeComponent implements Component {
176
238
  );
177
239
  }
178
240
  }
241
+ // Pad to the fixed slot count so the box doesn't grow when sessions load in.
242
+ while (sessionLines.length < WELCOME_SESSION_SLOTS) {
243
+ sessionLines.push("");
244
+ }
179
245
 
180
246
  // LSP servers content
181
247
  const lspLines: string[] = [];
182
248
  if (this.lspServers.length === 0) {
183
249
  lspLines.push(` ${theme.fg("dim", "No LSP servers")}`);
184
250
  } else {
185
- for (const server of this.lspServers) {
251
+ for (const server of this.lspServers.slice(0, WELCOME_LSP_SLOTS)) {
186
252
  const icon =
187
253
  server.status === "ready"
188
254
  ? theme.styledSymbol("status.enabled", "success")
189
- : server.status === "connecting"
190
- ? theme.styledSymbol("status.pending", "muted")
191
- : theme.styledSymbol("status.error", "error");
255
+ : server.status === "available"
256
+ ? theme.styledSymbol("status.enabled", "dim")
257
+ : server.status === "connecting"
258
+ ? theme.styledSymbol("status.pending", "muted")
259
+ : theme.styledSymbol("status.error", "error");
192
260
  const exts = server.fileTypes.slice(0, 3).join(" ");
193
261
  lspLines.push(` ${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", exts)}`);
194
262
  }
195
263
  }
264
+ // Pad to the fixed slot count so the box height doesn't depend on server count.
265
+ while (lspLines.length < WELCOME_LSP_SLOTS) {
266
+ lspLines.push("");
267
+ }
196
268
 
197
269
  // Right column
198
270
  const rightLines = [
@@ -305,23 +377,12 @@ export class WelcomeComponent implements Component {
305
377
  return str + padding(width - visLen);
306
378
  }
307
379
 
308
- /** Pick the logo frame for the current intro phase, or the resting frame. */
380
+ /** Pick the logo frame for the current intro phase, or the resting/held frame. */
309
381
  #currentLogoFrame(): readonly string[] {
310
- if (this.#animStart == null) return REST_FRAME;
382
+ if (this.#animStart == null) return this.#holdIntroFirstFrame ? INTRO_FIRST_FRAME : REST_FRAME;
311
383
  const elapsed = performance.now() - this.#animStart;
312
384
  if (elapsed >= INTRO_MS) return REST_FRAME;
313
- // Ease-out cubic so the spin decelerates into the resting state.
314
- const progress = elapsed / INTRO_MS;
315
- const eased = 1 - (1 - progress) ** 3;
316
- // Sweep backward through INTRO_SWEEPS full rotations so the gradient
317
- // visibly spins multiple times. `eased == 1` → phase = 0 = resting frame.
318
- const phase = ((((1 - eased) * INTRO_SWEEPS) % 1) + 1) % 1;
319
- // Shine traverses the diagonal at a steady pace, decoupled from the
320
- // gradient phase so the two layers parallax. Strength fades out with
321
- // the same ease-out curve so the highlight is gone by the resting frame.
322
- const shinePos = (((progress * INTRO_SHINE_TRAVERSALS) % 1) + 1) % 1;
323
- const shineStrength = (1 - eased) ** 1.5;
324
- return gradientLogo(PI_LOGO, phase, { strength: shineStrength, pos: shinePos });
385
+ return introLogoFrame(elapsed / INTRO_MS);
325
386
  }
326
387
  }
327
388
 
@@ -431,5 +492,26 @@ const INTRO_SWEEPS = 2.5;
431
492
  /** Number of times the shine highlight crosses the diagonal across the intro. */
432
493
  const INTRO_SHINE_TRAVERSALS = 3;
433
494
 
495
+ /**
496
+ * Logo frame for a normalized intro progress in [0, 1).
497
+ *
498
+ * Ease-out cubic so the spin decelerates into the resting state. The gradient
499
+ * sweeps backward through INTRO_SWEEPS full rotations (`eased == 1` → phase =
500
+ * 0 = resting frame) while the shine traverses the diagonal at a steady pace,
501
+ * decoupled from the gradient phase so the two layers parallax; its strength
502
+ * fades with the same ease-out curve so the highlight is gone by the resting
503
+ * frame.
504
+ */
505
+ function introLogoFrame(progress: number): string[] {
506
+ const eased = 1 - (1 - progress) ** 3;
507
+ const phase = ((((1 - eased) * INTRO_SWEEPS) % 1) + 1) % 1;
508
+ const shinePos = (((progress * INTRO_SHINE_TRAVERSALS) % 1) + 1) % 1;
509
+ const shineStrength = (1 - eased) ** 1.5;
510
+ return gradientLogo(PI_LOGO, phase, { strength: shineStrength, pos: shinePos });
511
+ }
512
+
513
+ /** First intro frame, cached for splash-held renders (resize re-renders reuse it). */
514
+ const INTRO_FIRST_FRAME = introLogoFrame(0);
515
+
434
516
  /** Resting gradient frame, cached for re-renders outside of the intro. */
435
517
  const REST_FRAME = gradientLogo(PI_LOGO, 0);
@@ -21,6 +21,7 @@ import {
21
21
  loadHindsightConfig,
22
22
  reloadMentalModelsForSession,
23
23
  resolveSeedsForScope,
24
+ seedAlreadyExists,
24
25
  summarizeMentalModel,
25
26
  } from "../../hindsight";
26
27
  import { resolveMemoryBackend } from "../../memory-backend";
@@ -314,7 +315,13 @@ export class CommandController {
314
315
  info += `\n${theme.bold("LSP Servers")}\n`;
315
316
  for (const server of this.ctx.lspServers) {
316
317
  const statusColor =
317
- server.status === "ready" ? "success" : server.status === "connecting" ? "warning" : "error";
318
+ server.status === "ready"
319
+ ? "success"
320
+ : server.status === "available"
321
+ ? "dim"
322
+ : server.status === "connecting"
323
+ ? "warning"
324
+ : "error";
318
325
  const statusText =
319
326
  server.status === "error" && server.error ? `${server.status}: ${server.error}` : server.status;
320
327
  info += `${theme.fg("dim", `${server.name}:`)} ${theme.fg(statusColor, statusText)} ${theme.fg("dim", `(${server.fileTypes.join(", ")})`)}\n`;
@@ -712,11 +719,11 @@ export class CommandController {
712
719
  return;
713
720
  }
714
721
  const list = await state.client.listMentalModels(state.bankId, { detail: "metadata" });
715
- const existing = new Set((list.items ?? []).map(m => m.id));
722
+ const existing = list.items ?? [];
716
723
  let created = 0;
717
724
  let skipped = 0;
718
725
  for (const seed of seeds) {
719
- if (existing.has(seed.id)) {
726
+ if (seedAlreadyExists(seed, existing)) {
720
727
  skipped++;
721
728
  continue;
722
729
  }
@@ -25,6 +25,16 @@ import { StreamingRevealController } from "./streaming-reveal";
25
25
  type AgentSessionEventKind = AgentSessionEvent["type"];
26
26
 
27
27
  const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
28
+ /**
29
+ * Concurrent IRC cards allowed in the transcript's live region. Cards land
30
+ * below a still-live block (a running task), where they cannot commit to
31
+ * native scrollback (commits are prefix-only) — every visible card inflates
32
+ * the live region and pushes the live block's uncommitted rows above the
33
+ * window top, where they are neither on screen nor in history. A swarm burst
34
+ * (several agents coordinating at once) must therefore stay bounded: the
35
+ * oldest live-region card retires as soon as a new one would exceed the cap.
36
+ */
37
+ const MAX_LIVE_IRC_CARDS = 4;
28
38
 
29
39
  /**
30
40
  * Loader label shown the instant a user interrupt (Esc) is requested, kept until
@@ -36,19 +46,6 @@ const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
36
46
  */
37
47
  export const INTERRUPTING_WORKING_MESSAGE = "Interrupting…";
38
48
 
39
- // Events that change foreground streaming state, or that reset a turn. The TUI
40
- // eager native-scrollback rebuild mode is recomputed only on these so unrelated
41
- // IRC/notices/status refreshes do not toggle scrollback replay policy.
42
- const STREAM_RENDER_MODE_EVENTS: Record<string, true> = {
43
- agent_start: true,
44
- agent_end: true,
45
- message_start: true,
46
- message_end: true,
47
- tool_execution_start: true,
48
- tool_execution_update: true,
49
- tool_execution_end: true,
50
- };
51
-
52
49
  type AgentSessionEventHandlers = {
53
50
  [E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
54
51
  };
@@ -65,7 +62,6 @@ export class EventController {
65
62
  #renderedCustomMessages = new Set<string>();
66
63
  #lastIntent: string | undefined = undefined;
67
64
  #backgroundToolCallIds = new Set<string>();
68
- #assistantMessageStreaming = false;
69
65
  #agentTurnActive = false;
70
66
  #interrupting = false;
71
67
  #readToolCallArgs = new Map<string, Record<string, unknown>>();
@@ -78,6 +74,9 @@ export class EventController {
78
74
  #pinnedErrorComponent: AssistantMessageComponent | undefined = undefined;
79
75
  #idleCompactionTimer?: NodeJS.Timeout;
80
76
  #ircExpiryTimers = new Map<string, NodeJS.Timeout>();
77
+ // Insertion-ordered IRC cards not yet retired; values are the transcript
78
+ // components each card contributed (see #retireIrcCard for the guard).
79
+ #liveIrcCards = new Map<string, Component[]>();
81
80
  #streamingReveal: StreamingRevealController;
82
81
  #handlers: AgentSessionEventHandlers;
83
82
 
@@ -125,6 +124,7 @@ export class EventController {
125
124
  clearTimeout(timer);
126
125
  }
127
126
  this.#ircExpiryTimers.clear();
127
+ this.#liveIrcCards.clear();
128
128
  }
129
129
 
130
130
  #resetReadGroup(): void {
@@ -217,30 +217,6 @@ export class EventController {
217
217
 
218
218
  const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
219
219
  await run(event);
220
- // While an assistant turn is active, visible status chrome and foreground
221
- // transcript blocks can re-render after rows have entered native scrollback
222
- // (idle Working loader, Markdown fences, wrapping, tool previews). Let the
223
- // TUI use its foreground live-region path instead of idle deferral, which
224
- // can otherwise leave the loader/status frame frozen until the next input.
225
- // Background-running tools after the turn ends are excluded so late async
226
- // updates keep the no-yank deferral; agent_start/agent_end bracket the
227
- // foreground turn.
228
- if (STREAM_RENDER_MODE_EVENTS[event.type]) {
229
- this.#refreshToolRenderMode();
230
- }
231
- }
232
-
233
- #refreshToolRenderMode(): void {
234
- let foregroundToolActive = this.#agentTurnActive || this.#assistantMessageStreaming;
235
- if (!foregroundToolActive) {
236
- for (const toolCallId of this.ctx.pendingTools.keys()) {
237
- if (!this.#backgroundToolCallIds.has(toolCallId)) {
238
- foregroundToolActive = true;
239
- break;
240
- }
241
- }
242
- }
243
- this.ctx.ui.setEagerNativeScrollbackRebuild(foregroundToolActive);
244
220
  }
245
221
 
246
222
  async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
@@ -250,7 +226,6 @@ export class EventController {
250
226
  this.#readToolCallArgs.clear();
251
227
  this.#readToolCallAssistantComponents.clear();
252
228
  this.#resetReadGroup();
253
- this.#assistantMessageStreaming = false;
254
229
  this.#lastAssistantComponent = undefined;
255
230
  // Restore the previous turn's inline error in the transcript before dropping
256
231
  // the banner, so the error stays in history once the banner is gone.
@@ -267,7 +242,6 @@ export class EventController {
267
242
  this.ctx.statusContainer.clear();
268
243
  }
269
244
  this.#cancelIdleCompaction();
270
- this.#refreshToolRenderMode();
271
245
  this.ctx.ensureLoadingAnimation();
272
246
  this.ctx.ui.requestRender();
273
247
  }
@@ -340,7 +314,6 @@ export class EventController {
340
314
  this.ctx.addMessageToChat(event.message);
341
315
  this.ctx.ui.requestRender();
342
316
  } else if (event.message.role === "assistant") {
343
- this.#assistantMessageStreaming = true;
344
317
  this.#lastVisibleBlockCount = 0;
345
318
  this.ctx.streamingComponent = new AssistantMessageComponent(
346
319
  undefined,
@@ -365,6 +338,7 @@ export class EventController {
365
338
  this.#resetReadGroup();
366
339
  const components = this.ctx.addMessageToChat(event.message);
367
340
  this.#scheduleIrcExpiry(signature, components);
341
+ this.#enforceIrcCardCap(signature);
368
342
  this.ctx.ui.requestRender();
369
343
  }
370
344
 
@@ -372,13 +346,47 @@ export class EventController {
372
346
  if (components.length === 0 || this.#ircExpiryTimers.has(signature)) return;
373
347
  const timer = setTimeout(() => {
374
348
  this.#ircExpiryTimers.delete(signature);
375
- for (const component of components) {
376
- this.ctx.chatContainer.removeChild(component);
377
- }
378
- this.ctx.ui.requestRender();
349
+ this.#retireIrcCard(signature);
379
350
  }, IRC_MESSAGE_VISIBLE_TTL_MS);
380
351
  timer.unref?.();
381
352
  this.#ircExpiryTimers.set(signature, timer);
353
+ this.#liveIrcCards.set(signature, components);
354
+ }
355
+
356
+ /**
357
+ * Remove an expired/evicted IRC card — but only while it still sits below a
358
+ * live block, where its rows cannot have entered native scrollback. Once
359
+ * everything above it has finalized, its rows may already be committed;
360
+ * removing them then is an interior deletion of the committed prefix, which
361
+ * the engine can only repair by recommitting every row below the gap —
362
+ * exactly the duplicated-block artifact this guard exists to prevent. Such
363
+ * a card simply stays: it is final history, and the window scrolls past it.
364
+ */
365
+ #retireIrcCard(signature: string): void {
366
+ const components = this.#liveIrcCards.get(signature);
367
+ this.#liveIrcCards.delete(signature);
368
+ if (!components) return;
369
+ let removed = false;
370
+ for (const component of components) {
371
+ if (!this.ctx.chatContainer.isWithinLiveRegion(component)) continue;
372
+ this.ctx.chatContainer.removeChild(component);
373
+ removed = true;
374
+ }
375
+ if (removed) this.ctx.ui.requestRender();
376
+ }
377
+
378
+ /** Evict oldest live-region cards beyond {@link MAX_LIVE_IRC_CARDS}. */
379
+ #enforceIrcCardCap(latestSignature: string): void {
380
+ while (this.#liveIrcCards.size > MAX_LIVE_IRC_CARDS) {
381
+ const oldest = this.#liveIrcCards.keys().next().value;
382
+ if (oldest === undefined || oldest === latestSignature) return;
383
+ const timer = this.#ircExpiryTimers.get(oldest);
384
+ if (timer) {
385
+ clearTimeout(timer);
386
+ this.#ircExpiryTimers.delete(oldest);
387
+ }
388
+ this.#retireIrcCard(oldest);
389
+ }
382
390
  }
383
391
 
384
392
  async #handleNotice(event: Extract<AgentSessionEvent, { type: "notice" }>): Promise<void> {
@@ -406,6 +414,26 @@ export class EventController {
406
414
  this.#resetReadGroup();
407
415
  this.#lastVisibleBlockCount = visibleBlockCount;
408
416
  }
417
+
418
+ // Content blocks stream sequentially: a toolCall block can only begin
419
+ // after every preceding thinking/text block has closed, and the
420
+ // reveal's setTarget above force-completes the visible text for
421
+ // toolCall messages. Finalize the assistant block now instead of at
422
+ // message_end so the transcript's commit-safe run can extend through
423
+ // it into the streaming tool preview below — otherwise a long args
424
+ // stream (a big write/edit/eval) sits below a still-live block and
425
+ // can never reach native scrollback: the head of the preview is
426
+ // neither committed nor on screen and the transcript reads as cut.
427
+ // Skipped when the per-turn usage row is enabled: that row is only
428
+ // known at message_end and appends to this block, which would shift
429
+ // committed tool rows below it every turn (audit recommit →
430
+ // duplicated preview copies in scrollback).
431
+ if (
432
+ this.ctx.streamingMessage.content.some(content => content.type === "toolCall") &&
433
+ !settings.get("display.showTokenUsage")
434
+ ) {
435
+ this.ctx.streamingComponent.markTranscriptBlockFinalized();
436
+ }
409
437
  for (const content of this.ctx.streamingMessage.content) {
410
438
  if (content.type !== "toolCall") continue;
411
439
  if (content.name === "read") {
@@ -491,9 +519,6 @@ export class EventController {
491
519
 
492
520
  async #handleMessageEnd(event: Extract<AgentSessionEvent, { type: "message_end" }>): Promise<void> {
493
521
  if (event.message.role === "user") return;
494
- if (event.message.role === "assistant") {
495
- this.#assistantMessageStreaming = false;
496
- }
497
522
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
498
523
  this.ctx.streamingMessage = event.message;
499
524
  this.#streamingReveal.stop();
@@ -701,7 +726,6 @@ export class EventController {
701
726
  }
702
727
  async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
703
728
  this.#agentTurnActive = false;
704
- this.#assistantMessageStreaming = false;
705
729
  this.#streamingReveal.stop();
706
730
  if (this.ctx.loadingAnimation) {
707
731
  this.ctx.loadingAnimation.stop();
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import type { ImageContent } from "@oh-my-pi/pi-ai";
3
3
  import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
4
4
  import { $env, logger, sanitizeText } from "@oh-my-pi/pi-utils";
5
- import { getRoleInfo } from "../../config/model-registry";
5
+ import { getRoleInfo } from "../../config/model-roles";
6
6
  import { isSettingsInitialized, settings } from "../../config/settings";
7
7
  import { renderSegmentTrack } from "../../modes/components/segment-track";
8
8
  import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
@@ -267,7 +267,7 @@ export class InputController {
267
267
  const focused = this.ctx.ui.getFocused();
268
268
  const target = focused && focused !== this.ctx.editor && hasPasteText(focused) ? focused : this.ctx.editor;
269
269
  target.pasteText(text);
270
- this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
270
+ this.ctx.ui.requestRender();
271
271
  },
272
272
  pasteImage: async image => {
273
273
  // Images can only land in the main editor — when a modal Input is
@@ -755,7 +755,7 @@ export class InputController {
755
755
  const dims = await this.#imageDimensions(imageData);
756
756
  const label = dims ? `[Image #${imageNum}, ${dims.width}x${dims.height}]` : `[Image #${imageNum}]`;
757
757
  this.ctx.editor.insertText(`${label} `);
758
- this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
758
+ this.ctx.ui.requestRender();
759
759
  }
760
760
 
761
761
  /** Probe pixel dimensions for the marker label (`[Image #N, WxH]`). Returns undefined when the
@@ -801,7 +801,7 @@ export class InputController {
801
801
  });
802
802
  if (!image) {
803
803
  this.ctx.editor.pasteText(path);
804
- this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
804
+ this.ctx.ui.requestRender();
805
805
  this.ctx.showStatus("Pasted path is not a supported image");
806
806
  return;
807
807
  }
@@ -811,7 +811,7 @@ export class InputController {
811
811
  );
812
812
  } catch (error) {
813
813
  this.ctx.editor.pasteText(path);
814
- this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
814
+ this.ctx.ui.requestRender();
815
815
  this.ctx.showStatus(
816
816
  error instanceof ImageInputTooLargeError ? error.message : "Failed to read pasted image path",
817
817
  );
@@ -62,7 +62,7 @@ export class MCPAuthorizationLinkPrompt implements Component {
62
62
 
63
63
  invalidate(): void {}
64
64
 
65
- render(_width: number): string[] {
65
+ render(_width: number): readonly string[] {
66
66
  const link = urlHyperlinkAlways(this.#url, "Click here to authorize");
67
67
  return [
68
68
  ` ${theme.fg("success", "Open authorization URL:")}`,
@@ -5,8 +5,8 @@ import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
5
5
  import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
6
6
  import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
7
7
  import { getAgentDbPath, getProjectDir, normalizePathForComparison } from "@oh-my-pi/pi-utils";
8
- import { getRoleInfo } from "../../config/model-registry";
9
8
  import { formatModelSelectorValue } from "../../config/model-resolver";
9
+ import { getRoleInfo } from "../../config/model-roles";
10
10
  import { settings } from "../../config/settings";
11
11
  import { disableProvider, enableProvider } from "../../discovery";
12
12
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
@@ -266,10 +266,6 @@ export class SelectorController {
266
266
  this.ctx.updateEditorBorderColor();
267
267
  break;
268
268
 
269
- case "clearOnShrink":
270
- this.ctx.ui.setClearOnShrink(value as boolean);
271
- break;
272
-
273
269
  case "autocompleteMaxVisible":
274
270
  this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));
275
271
  break;