@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
@@ -36,6 +36,9 @@ export class AssistantMessageComponent extends Container {
36
36
  * transcript keeps the error in history.
37
37
  */
38
38
  #errorPinned = false;
39
+ /** Whether the last updateContent carried an in-flight streaming partial; such
40
+ * renders bypass the markdown module LRU (see Markdown.transientRenderCache). */
41
+ #lastUpdateTransient = false;
39
42
 
40
43
  constructor(
41
44
  message?: AssistantMessage,
@@ -59,7 +62,7 @@ export class AssistantMessageComponent extends Container {
59
62
  override invalidate(): void {
60
63
  super.invalidate();
61
64
  if (this.#lastMessage) {
62
- this.updateContent(this.#lastMessage);
65
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
63
66
  }
64
67
  }
65
68
 
@@ -75,7 +78,7 @@ export class AssistantMessageComponent extends Container {
75
78
  if (this.#errorPinned === pinned) return;
76
79
  this.#errorPinned = pinned;
77
80
  if (this.#lastMessage) {
78
- this.updateContent(this.#lastMessage);
81
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
79
82
  }
80
83
  }
81
84
 
@@ -123,7 +126,7 @@ export class AssistantMessageComponent extends Container {
123
126
  this.#convertToolImagesForKitty(toolCallId, validImages);
124
127
  }
125
128
  if (this.#lastMessage) {
126
- this.updateContent(this.#lastMessage);
129
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
127
130
  }
128
131
  }
129
132
 
@@ -146,7 +149,7 @@ export class AssistantMessageComponent extends Container {
146
149
  mimeType: "image/png",
147
150
  });
148
151
  if (this.#lastMessage) {
149
- this.updateContent(this.#lastMessage);
152
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
150
153
  }
151
154
  this.onImageUpdate?.();
152
155
  })
@@ -159,7 +162,7 @@ export class AssistantMessageComponent extends Container {
159
162
  setUsageInfo(usage: Usage): void {
160
163
  this.#usageInfo = usage;
161
164
  if (this.#lastMessage) {
162
- this.updateContent(this.#lastMessage);
165
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
163
166
  }
164
167
  }
165
168
 
@@ -211,8 +214,9 @@ export class AssistantMessageComponent extends Container {
211
214
  }
212
215
  }
213
216
 
214
- updateContent(message: AssistantMessage): void {
217
+ updateContent(message: AssistantMessage, opts?: { transient?: boolean }): void {
215
218
  this.#lastMessage = message;
219
+ this.#lastUpdateTransient = opts?.transient === true;
216
220
 
217
221
  // Clear content container
218
222
  this.#contentContainer.clear();
@@ -228,7 +232,9 @@ export class AssistantMessageComponent extends Container {
228
232
  if (content.type === "text" && content.text.trim()) {
229
233
  // Assistant text messages with no background - trim the text
230
234
  // Set paddingY=0 to avoid extra spacing before tool executions
231
- this.#contentContainer.addChild(new Markdown(content.text.trim(), 1, 0, getMarkdownTheme()));
235
+ const markdown = new Markdown(content.text.trim(), 1, 0, getMarkdownTheme());
236
+ markdown.transientRenderCache = this.#lastUpdateTransient;
237
+ this.#contentContainer.addChild(markdown);
232
238
  } else if (content.type === "thinking" && content.thinking.trim()) {
233
239
  // Add spacing only when another visible assistant content block follows.
234
240
  // This avoids a superfluous blank line before separately-rendered tool execution blocks.
@@ -245,12 +251,12 @@ export class AssistantMessageComponent extends Container {
245
251
  } else {
246
252
  const thinkingText = content.thinking.trim();
247
253
  // Thinking traces in thinkingText color, italic
248
- this.#contentContainer.addChild(
249
- new Markdown(thinkingText, 1, 0, getMarkdownTheme(), {
250
- color: (text: string) => theme.fg("thinkingText", text),
251
- italic: true,
252
- }),
253
- );
254
+ const thinkingMarkdown = new Markdown(thinkingText, 1, 0, getMarkdownTheme(), {
255
+ color: (text: string) => theme.fg("thinkingText", text),
256
+ italic: true,
257
+ });
258
+ thinkingMarkdown.transientRenderCache = this.#lastUpdateTransient;
259
+ this.#contentContainer.addChild(thinkingMarkdown);
254
260
  this.#appendThinkingExtensions(i, thinkingIndex, thinkingText);
255
261
  thinkingIndex += 1;
256
262
  if (hasVisibleContentAfter) {
@@ -126,7 +126,7 @@ export class BashExecutionComponent extends Container {
126
126
  this.#updateDisplay();
127
127
  }
128
128
 
129
- override render(width: number): string[] {
129
+ override render(width: number): readonly string[] {
130
130
  if (this.#displayDirty) {
131
131
  this.#displayDirty = false;
132
132
  this.#updateDisplay();
@@ -173,7 +173,7 @@ export class CopySelectorComponent implements Component {
173
173
  return out;
174
174
  }
175
175
 
176
- render(width: number): string[] {
176
+ render(width: number): readonly string[] {
177
177
  const height = process.stdout.rows || 40;
178
178
  const flat = this.#flatten();
179
179
  const cursorIdx = Math.max(
@@ -109,10 +109,16 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
109
109
  const lines = sanitizeText(diffText).split("\n");
110
110
  const result: string[] = [];
111
111
  const parsedLines = lines.map(parseDiffLine);
112
+ // Reserve 3 gutter digits: a streaming preview re-renders this diff as it
113
+ // grows, and a width derived purely from the current max line number widens
114
+ // at the 100-line crossing — re-padding every already-rendered row, which
115
+ // breaks the transcript's append-only commit detection and forces a full
116
+ // recommit of the block into native scrollback. A constant gutter through
117
+ // 999 lines keeps streamed rows byte-identical to the final result render.
112
118
  const lineNumberWidth = parsedLines.reduce((width, parsed) => {
113
119
  const lineNumber = parsed?.lineNum.trim() ?? "";
114
120
  return Math.max(width, lineNumber.length);
115
- }, 0);
121
+ }, 3);
116
122
 
117
123
  // Batch-highlight context (unedited) lines so consecutive lines tokenize
118
124
  // with full multi-line context. Highlighting is a no-op when no language
@@ -142,7 +148,12 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
142
148
 
143
149
  if (!parsed) {
144
150
  prevLineNum = "";
145
- result.push(theme.fg("toolDiffContext", replaceTabs(line, options.filePath)));
151
+ // Blank gap rows (and legacy "..." markers from older transcripts)
152
+ // mark non-contiguous diff regions; display them as a single dim
153
+ // unicode ellipsis.
154
+ const trimmed = line.trim();
155
+ const isGapRow = trimmed.length === 0 || trimmed === "..." || trimmed === "…";
156
+ result.push(theme.fg("toolDiffContext", isGapRow ? "…" : replaceTabs(line, options.filePath)));
146
157
  i++;
147
158
  continue;
148
159
  }
@@ -10,16 +10,25 @@ import { theme } from "../../modes/theme/theme";
10
10
  */
11
11
  export class DynamicBorder implements Component {
12
12
  #color: (str: string) => string;
13
+ #cachedWidth = -1;
14
+ #cachedLines: string[] | undefined;
13
15
 
14
16
  constructor(color: (str: string) => string = str => theme.fg("border", str)) {
15
17
  this.#color = color;
16
18
  }
17
19
 
18
20
  invalidate(): void {
19
- // No cached state to invalidate currently
21
+ this.#cachedWidth = -1;
22
+ this.#cachedLines = undefined;
20
23
  }
21
24
 
22
- render(width: number): string[] {
23
- return [this.#color(theme.boxSharp.horizontal.repeat(Math.max(1, width)))];
25
+ render(width: number): readonly string[] {
26
+ if (this.#cachedLines && this.#cachedWidth === width) {
27
+ return this.#cachedLines;
28
+ }
29
+ const lines = [this.#color(theme.boxSharp.horizontal.repeat(Math.max(1, width)))];
30
+ this.#cachedWidth = width;
31
+ this.#cachedLines = lines;
32
+ return lines;
24
33
  }
25
34
  }
@@ -137,7 +137,7 @@ export class ExtensionDashboard extends Container {
137
137
  return Math.max(3, this.#computeBodyHeight() - 3);
138
138
  }
139
139
 
140
- override render(width: number): string[] {
140
+ override render(width: number): readonly string[] {
141
141
  // Rebuild when terminal geometry changes so the full-screen overlay
142
142
  // re-fits on resize.
143
143
  if (this.#terminalRows() !== this.#builtRows || this.#uiWidth() !== this.#builtCols) {
@@ -145,10 +145,13 @@ export class ExtensionDashboard extends Container {
145
145
  }
146
146
  const lines = super.render(width);
147
147
  // Pad to the full viewport so the dashboard covers the screen instead of
148
- // letting the transcript peek through below it.
148
+ // letting the transcript peek through below it. Copy before padding — the
149
+ // container's render result is component-owned and must not be mutated.
149
150
  const rows = this.#terminalRows();
150
- while (lines.length < rows) lines.push("");
151
- return lines;
151
+ if (lines.length >= rows) return lines;
152
+ const padded = lines.slice();
153
+ while (padded.length < rows) padded.push("");
154
+ return padded;
152
155
  }
153
156
 
154
157
  #buildLayout(): void {
@@ -367,7 +370,7 @@ class TwoColumnBody implements Component {
367
370
  private readonly maxHeight: number,
368
371
  ) {}
369
372
 
370
- render(width: number): string[] {
373
+ render(width: number): readonly string[] {
371
374
  const leftWidth = Math.floor(width * 0.5);
372
375
  const rightWidth = Math.max(0, width - leftWidth - 3);
373
376
 
@@ -113,7 +113,7 @@ export class ExtensionList implements Component {
113
113
 
114
114
  invalidate(): void {}
115
115
 
116
- render(width: number): string[] {
116
+ render(width: number): readonly string[] {
117
117
  const lines: string[] = [];
118
118
 
119
119
  // Search bar
@@ -18,7 +18,7 @@ export class InspectorPanel implements Component {
18
18
 
19
19
  invalidate(): void {}
20
20
 
21
- render(width: number): string[] {
21
+ render(width: number): readonly string[] {
22
22
  if (!this.#extension) {
23
23
  return [theme.fg("muted", "Select an extension"), theme.fg("dim", "to view details")];
24
24
  }
@@ -110,7 +110,7 @@ export class FooterComponent implements Component {
110
110
  return this.#cachedBranch;
111
111
  }
112
112
 
113
- render(width: number): string[] {
113
+ render(width: number): readonly string[] {
114
114
  const state = this.session.state;
115
115
 
116
116
  // Calculate cumulative usage from ALL session entries (not just post-compaction messages)
@@ -98,7 +98,7 @@ class HistoryResultsList implements Component {
98
98
  // No cached state to invalidate currently
99
99
  }
100
100
 
101
- render(width: number): string[] {
101
+ render(width: number): readonly string[] {
102
102
  const lines: string[] = [];
103
103
 
104
104
  if (this.#results.length === 0) {
@@ -86,6 +86,14 @@ export class HookEditorComponent extends Container {
86
86
  this.#onSubmitCallback(this.#editor.getExpandedText());
87
87
  }
88
88
 
89
+ /** Route non-bracketed paste transports (e.g. kitty's OSC 5522 enhanced clipboard)
90
+ * into the inner editor, mirroring bracketed-paste semantics. Without this hook,
91
+ * enhanced-paste routing falls back to the main prompt editor hidden behind the
92
+ * dialog (#2127 routing contract). */
93
+ pasteText(text: string): void {
94
+ this.#editor.pasteText(text);
95
+ }
96
+
89
97
  /** Prompt-style: raw Enter submits; Editor owns newline-producing sequences. */
90
98
  #handlePromptStyleInput(keyData: string): void {
91
99
  // Prompt-style keeps Escape as an explicit cancel key and also honors app.interrupt remaps.
@@ -73,6 +73,14 @@ export class HookInputComponent extends Container {
73
73
  }
74
74
  }
75
75
 
76
+ /** Route non-bracketed paste transports (e.g. kitty's OSC 5522 enhanced clipboard)
77
+ * into the inner input, mirroring bracketed-paste semantics. Pasting counts as
78
+ * interaction, so the timeout countdown resets like any keystroke. */
79
+ pasteText(text: string): void {
80
+ this.#countdown?.reset();
81
+ this.#input.pasteText(text);
82
+ }
83
+
76
84
  dispose(): void {
77
85
  this.#countdown?.dispose();
78
86
  }
@@ -122,7 +122,7 @@ class OutlinedList extends Container {
122
122
  this.invalidate();
123
123
  }
124
124
 
125
- render(width: number): string[] {
125
+ render(width: number): readonly string[] {
126
126
  const borderColor = (text: string) => theme.fg("border", text);
127
127
  const horizontal = borderColor(theme.boxSharp.horizontal.repeat(Math.max(1, width)));
128
128
  const innerWidth = Math.max(1, width - 2);
@@ -645,7 +645,7 @@ export class HookSelectorComponent extends Container {
645
645
  }
646
646
  }
647
647
 
648
- override render(width: number): string[] {
648
+ override render(width: number): readonly string[] {
649
649
  const renderWidth = Math.max(1, width);
650
650
  if (this.#lastRenderWidth !== renderWidth) {
651
651
  this.#lastRenderWidth = renderWidth;
@@ -1,5 +1,7 @@
1
1
  import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import { getSupportedEfforts, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
2
+ import type { Model } from "@oh-my-pi/pi-ai";
3
+ import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
4
+ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
3
5
  import {
4
6
  Container,
5
7
  fuzzyFilter,
@@ -16,8 +18,8 @@ import {
16
18
  } from "@oh-my-pi/pi-tui";
17
19
  import { formatNumber } from "@oh-my-pi/pi-utils";
18
20
  import type { ModelRegistry } from "../../config/model-registry";
19
- import { getKnownRoleIds, getRoleInfo, MODEL_ROLE_IDS, MODEL_ROLES } from "../../config/model-registry";
20
21
  import { getModelMatchPreferences, resolveModelRoleValue } from "../../config/model-resolver";
22
+ import { getKnownRoleIds, getRoleInfo, MODEL_ROLE_IDS, MODEL_ROLES } from "../../config/model-roles";
21
23
  import type { Settings } from "../../config/settings";
22
24
  import { type ThemeColor, theme } from "../../modes/theme/theme";
23
25
  import { matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
@@ -263,21 +265,26 @@ export class ModelSelectorComponent extends Container {
263
265
  // Add bottom border
264
266
  this.addChild(new DynamicBorder());
265
267
 
266
- // Load models and do initial render
267
- this.#loadModels().then(() => {
268
- this.#buildProviderTabs();
269
- this.#updateTabBar();
270
- // Always apply the current search query — the user may have typed
271
- // while models were loading asynchronously.
272
- const currentQuery = this.#searchInput.getValue();
273
- if (currentQuery) {
274
- this.#filterModels(currentQuery);
275
- } else {
276
- this.#updateList();
277
- }
278
- // Request re-render after models are loaded
279
- this.#tui.requestRender();
280
- });
268
+ // Hydrate synchronously from the current registry snapshot so the first
269
+ // Enter after opening the selector acts on cached models instead of being
270
+ // dropped while the offline refresh promise is still pending. This stays
271
+ // on the open path, so it must remain cheap — heavy lifting lives in the
272
+ // registry's one-pass getCanonicalModelSelections.
273
+ this.#syncFromRegistryState();
274
+
275
+ // Reconcile with cached discovery state in the background. A --models
276
+ // scope is registry-independent, so the offline reload would only repeat
277
+ // the synchronous hydration above.
278
+ if (this.#scopedModels.length === 0) {
279
+ this.#modelRegistry
280
+ .refresh("offline")
281
+ .then(() => this.#syncFromRegistryState())
282
+ .catch(error => {
283
+ this.#errorMessage = error instanceof Error ? error.message : String(error);
284
+ this.#updateList();
285
+ })
286
+ .finally(() => this.#tui.requestRender());
287
+ }
281
288
  }
282
289
 
283
290
  #buildMenuRoleActions(): void {
@@ -477,37 +484,30 @@ export class ModelSelectorComponent extends Container {
477
484
 
478
485
  const candidates = models.map(item => item.model);
479
486
  this.#loadRoleModels(candidates);
480
- const canonicalRecords = this.#modelRegistry.getCanonicalModels({
487
+ const canonicalSelections = this.#modelRegistry.getCanonicalModelSelections({
481
488
  availableOnly: this.#scopedModels.length === 0,
482
489
  candidates,
483
490
  });
484
- const canonicalModels = canonicalRecords
485
- .map(record => {
486
- const selectedModel = this.#modelRegistry.resolveCanonicalModel(record.id, {
487
- availableOnly: this.#scopedModels.length === 0,
488
- candidates,
489
- });
490
- if (!selectedModel) return undefined;
491
- const searchText = [
492
- record.id,
493
- record.name,
494
- selectedModel.provider,
495
- selectedModel.id,
496
- selectedModel.name,
497
- ...record.variants.flatMap(variant => [variant.selector, variant.model.name]),
498
- ].join(" ");
499
- return {
500
- kind: "canonical" as const,
501
- id: record.id,
502
- model: selectedModel,
503
- selector: record.id,
504
- variantCount: record.variants.length,
505
- searchText,
506
- normalizedSearchText: normalizeSearchText(searchText),
507
- compactSearchText: compactSearchText(searchText),
508
- };
509
- })
510
- .filter((item): item is CanonicalModelItem => item !== undefined);
491
+ const canonicalModels = canonicalSelections.map(({ record, model: selectedModel }): CanonicalModelItem => {
492
+ const searchText = [
493
+ record.id,
494
+ record.name,
495
+ selectedModel.provider,
496
+ selectedModel.id,
497
+ selectedModel.name,
498
+ ...record.variants.flatMap(variant => [variant.selector, variant.model.name]),
499
+ ].join(" ");
500
+ return {
501
+ kind: "canonical",
502
+ id: record.id,
503
+ model: selectedModel,
504
+ selector: record.id,
505
+ variantCount: record.variants.length,
506
+ searchText,
507
+ normalizedSearchText: normalizeSearchText(searchText),
508
+ compactSearchText: compactSearchText(searchText),
509
+ };
510
+ });
511
511
 
512
512
  this.#sortModels(models);
513
513
  this.#sortCanonicalModels(canonicalModels);
@@ -523,12 +523,27 @@ export class ModelSelectorComponent extends Container {
523
523
  );
524
524
  }
525
525
 
526
- async #loadModels(): Promise<void> {
527
- if (this.#scopedModels.length === 0) {
528
- // Reload config and cached discovery state without blocking on live provider refresh
529
- await this.#modelRegistry.refresh("offline");
530
- }
526
+ /**
527
+ * Rebuild the visible model lists from the registry's in-memory state.
528
+ * Re-entrant: runs once synchronously at construction and again whenever a
529
+ * background refresh lands, so it re-applies the live search query and pins
530
+ * the highlighted item by selector — a refresh that reorders or inserts
531
+ * models must not yank the user's selection out from under a pending Enter.
532
+ */
533
+ #syncFromRegistryState(): void {
534
+ const selectedKey = this.#getSelectedItem()?.selector;
531
535
  this.#loadModelsFromCurrentRegistryState();
536
+ this.#buildProviderTabs();
537
+ this.#updateTabBar();
538
+ this.#applyTabFilter();
539
+ if (selectedKey) {
540
+ const visibleItems = this.#getVisibleItems();
541
+ const restoredIndex = visibleItems.findIndex(item => item.selector === selectedKey);
542
+ if (restoredIndex >= 0 && restoredIndex !== this.#selectedIndex) {
543
+ this.#selectedIndex = this.#coerceSelectedIndex(restoredIndex, visibleItems);
544
+ this.#updateList();
545
+ }
546
+ }
532
547
  }
533
548
 
534
549
  #buildProviderTabs(): void {
@@ -631,10 +646,7 @@ export class ModelSelectorComponent extends Container {
631
646
  // here must stay purely in-memory — do not call modelRegistry.refresh()
632
647
  // again or tab switches will pay an extra whole-registry reload after the
633
648
  // network round-trip completes.
634
- this.#loadModelsFromCurrentRegistryState();
635
- this.#buildProviderTabs();
636
- this.#updateTabBar();
637
- this.#applyTabFilter();
649
+ this.#syncFromRegistryState();
638
650
  } catch (error) {
639
651
  this.#errorMessage = error instanceof Error ? error.message : String(error);
640
652
  this.#updateList();
@@ -754,7 +754,7 @@ export class PlanReviewOverlay implements Component {
754
754
  return [theme.fg("dim", this.#buildHelp())];
755
755
  }
756
756
 
757
- render(width: number): string[] {
757
+ render(width: number): readonly string[] {
758
758
  const termHeight = process.stdout.rows || 40;
759
759
  const sidebarShown = this.#sidebarVisible(width);
760
760
  this.#sidebarShown = sidebarShown;
@@ -118,12 +118,12 @@ export class SessionObserverOverlayComponent extends Container {
118
118
  return pool.sort((a, b) => b.lastUpdate - a.lastUpdate)[0];
119
119
  }
120
120
 
121
- override render(width: number): string[] {
121
+ override render(width: number): readonly string[] {
122
122
  return this.#renderViewer(width);
123
123
  }
124
124
 
125
125
  #setupViewer(): void {
126
- this.children = [];
126
+ this.clear();
127
127
  this.#scrollOffset = 0;
128
128
  this.#selectedEntryIndex = 0;
129
129
  this.#expandedEntries.clear();
@@ -255,7 +255,7 @@ class SessionList implements Component {
255
255
  // No cached state to invalidate currently
256
256
  }
257
257
 
258
- render(width: number): string[] {
258
+ render(width: number): readonly string[] {
259
259
  const lines: string[] = [];
260
260
 
261
261
  // Render search input
@@ -631,8 +631,12 @@ export class SettingsSelectorComponent extends Container {
631
631
  return;
632
632
  }
633
633
 
634
- // Escape at top level cancels
634
+ // Escape clears an active settings search before closing the panel.
635
635
  if (matchesAppInterrupt(data) && !this.#currentSubmenu) {
636
+ if (this.#currentList?.hasSearchQuery()) {
637
+ this.#currentList.clearSearch();
638
+ return;
639
+ }
636
640
  this.callbacks.onCancel();
637
641
  return;
638
642
  }
@@ -769,7 +769,7 @@ export class StatusLineComponent implements Component {
769
769
  };
770
770
  }
771
771
 
772
- render(width: number): string[] {
772
+ render(width: number): readonly string[] {
773
773
  // Only render hook statuses - main status is in editor's top border
774
774
  const showHooks = this.#settings.showHookStatus ?? true;
775
775
  if (!showHooks || this.#hookStatuses.size === 0) {
@@ -71,7 +71,7 @@ export class TinyTitleDownloadProgressComponent implements Component {
71
71
  // No cached state.
72
72
  }
73
73
 
74
- render(width: number): string[] {
74
+ render(width: number): readonly string[] {
75
75
  width = Math.max(1, width);
76
76
  const spec = getTinyTitleModelSpec(this.#modelKey);
77
77
  const border = theme.fg("border", theme.boxSharp.horizontal.repeat(width));