@code-yeongyu/senpi 2026.6.30 → 2026.7.3

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 (272) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/core/agent-session.d.ts +6 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +42 -25
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/auth-storage.d.ts.map +1 -1
  7. package/dist/core/auth-storage.js +13 -5
  8. package/dist/core/auth-storage.js.map +1 -1
  9. package/dist/core/compaction/compaction.d.ts.map +1 -1
  10. package/dist/core/compaction/compaction.js +5 -8
  11. package/dist/core/compaction/compaction.js.map +1 -1
  12. package/dist/core/dynamic-prompt/build.d.ts +13 -0
  13. package/dist/core/dynamic-prompt/build.d.ts.map +1 -1
  14. package/dist/core/dynamic-prompt/build.js +24 -21
  15. package/dist/core/dynamic-prompt/build.js.map +1 -1
  16. package/dist/core/dynamic-prompt/exploration.d.ts.map +1 -1
  17. package/dist/core/dynamic-prompt/exploration.js +1 -3
  18. package/dist/core/dynamic-prompt/exploration.js.map +1 -1
  19. package/dist/core/dynamic-prompt/index.d.ts +1 -1
  20. package/dist/core/dynamic-prompt/index.d.ts.map +1 -1
  21. package/dist/core/dynamic-prompt/index.js.map +1 -1
  22. package/dist/core/dynamic-prompt/intent-gate.d.ts.map +1 -1
  23. package/dist/core/dynamic-prompt/intent-gate.js +20 -35
  24. package/dist/core/dynamic-prompt/intent-gate.js.map +1 -1
  25. package/dist/core/dynamic-prompt/parallel-tools.d.ts.map +1 -1
  26. package/dist/core/dynamic-prompt/parallel-tools.js +2 -4
  27. package/dist/core/dynamic-prompt/parallel-tools.js.map +1 -1
  28. package/dist/core/dynamic-prompt/style.d.ts.map +1 -1
  29. package/dist/core/dynamic-prompt/style.js +7 -26
  30. package/dist/core/dynamic-prompt/style.js.map +1 -1
  31. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.d.ts +3 -0
  32. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.d.ts.map +1 -0
  33. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +16 -0
  34. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js.map +1 -0
  35. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.d.ts.map +1 -1
  36. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.js +3 -1
  37. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.js.map +1 -1
  38. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.d.ts.map +1 -1
  39. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.js +3 -1
  40. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.js.map +1 -1
  41. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.d.ts.map +1 -1
  42. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.js +6 -2
  43. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.js.map +1 -1
  44. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.d.ts.map +1 -1
  45. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.js +5 -3
  46. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.js.map +1 -1
  47. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.d.ts.map +1 -1
  48. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +68 -8
  49. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js.map +1 -1
  50. package/dist/core/extensions/builtin/prompt-preset/presets.d.ts.map +1 -1
  51. package/dist/core/extensions/builtin/prompt-preset/presets.js +9 -0
  52. package/dist/core/extensions/builtin/prompt-preset/presets.js.map +1 -1
  53. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  54. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts.map +1 -1
  55. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  56. package/dist/core/extensions/builtin/prompt-preset/settings.js.map +1 -1
  57. package/dist/core/extensions/index.d.ts +1 -1
  58. package/dist/core/extensions/index.d.ts.map +1 -1
  59. package/dist/core/extensions/index.js.map +1 -1
  60. package/dist/core/extensions/loader.d.ts.map +1 -1
  61. package/dist/core/extensions/loader.js +6 -0
  62. package/dist/core/extensions/loader.js.map +1 -1
  63. package/dist/core/extensions/runner.d.ts +2 -1
  64. package/dist/core/extensions/runner.d.ts.map +1 -1
  65. package/dist/core/extensions/runner.js +33 -1
  66. package/dist/core/extensions/runner.js.map +1 -1
  67. package/dist/core/extensions/types.d.ts +16 -2
  68. package/dist/core/extensions/types.d.ts.map +1 -1
  69. package/dist/core/extensions/types.js.map +1 -1
  70. package/dist/core/http-dispatcher.d.ts.map +1 -1
  71. package/dist/core/http-dispatcher.js +28 -1
  72. package/dist/core/http-dispatcher.js.map +1 -1
  73. package/dist/core/model-resolver.d.ts +10 -0
  74. package/dist/core/model-resolver.d.ts.map +1 -1
  75. package/dist/core/model-resolver.js +21 -27
  76. package/dist/core/model-resolver.js.map +1 -1
  77. package/dist/core/session-manager.d.ts +20 -1
  78. package/dist/core/session-manager.d.ts.map +1 -1
  79. package/dist/core/session-manager.js +87 -68
  80. package/dist/core/session-manager.js.map +1 -1
  81. package/dist/core/settings-manager.d.ts +3 -0
  82. package/dist/core/settings-manager.d.ts.map +1 -1
  83. package/dist/core/settings-manager.js +8 -0
  84. package/dist/core/settings-manager.js.map +1 -1
  85. package/dist/core/tools/bash.d.ts.map +1 -1
  86. package/dist/core/tools/bash.js +20 -5
  87. package/dist/core/tools/bash.js.map +1 -1
  88. package/dist/extension-system-migration.d.ts +2 -0
  89. package/dist/extension-system-migration.d.ts.map +1 -0
  90. package/dist/extension-system-migration.js +51 -0
  91. package/dist/extension-system-migration.js.map +1 -0
  92. package/dist/index.d.ts +3 -2
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +2 -1
  95. package/dist/index.js.map +1 -1
  96. package/dist/legacy-senpi-dir-migration.d.ts +2 -0
  97. package/dist/legacy-senpi-dir-migration.d.ts.map +1 -0
  98. package/dist/legacy-senpi-dir-migration.js +76 -0
  99. package/dist/legacy-senpi-dir-migration.js.map +1 -0
  100. package/dist/migrations.d.ts.map +1 -1
  101. package/dist/migrations.js +3 -95
  102. package/dist/migrations.js.map +1 -1
  103. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  104. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  105. package/dist/modes/interactive/components/assistant-message.js +15 -7
  106. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  107. package/dist/modes/interactive/components/custom-entry.d.ts +19 -0
  108. package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -0
  109. package/dist/modes/interactive/components/custom-entry.js +49 -0
  110. package/dist/modes/interactive/components/custom-entry.js.map +1 -0
  111. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  112. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  113. package/dist/modes/interactive/components/settings-selector.js +13 -1
  114. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  115. package/dist/modes/interactive/components/status-indicator.d.ts +27 -0
  116. package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -0
  117. package/dist/modes/interactive/components/status-indicator.js +64 -0
  118. package/dist/modes/interactive/components/status-indicator.js.map +1 -0
  119. package/dist/modes/interactive/components/user-message.d.ts +6 -2
  120. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  121. package/dist/modes/interactive/components/user-message.js +15 -4
  122. package/dist/modes/interactive/components/user-message.js.map +1 -1
  123. package/dist/modes/interactive/interactive-mode.d.ts +14 -15
  124. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  125. package/dist/modes/interactive/interactive-mode.js +177 -137
  126. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  127. package/dist/modes/rpc/rpc-client.d.ts +15 -0
  128. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  129. package/dist/modes/rpc/rpc-client.js +14 -0
  130. package/dist/modes/rpc/rpc-client.js.map +1 -1
  131. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  132. package/dist/modes/rpc/rpc-mode.js +16 -0
  133. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  134. package/dist/modes/rpc/rpc-types.d.ts +26 -0
  135. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  136. package/dist/modes/rpc/rpc-types.js.map +1 -1
  137. package/docs/extensions.md +55 -8
  138. package/docs/rpc.md +58 -0
  139. package/docs/sdk.md +28 -0
  140. package/docs/session-format.md +18 -8
  141. package/docs/settings.md +2 -1
  142. package/examples/extensions/README.md +1 -0
  143. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  144. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  145. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  146. package/examples/extensions/entry-renderer.ts +41 -0
  147. package/examples/extensions/gondolin/package-lock.json +2 -2
  148. package/examples/extensions/gondolin/package.json +1 -1
  149. package/examples/extensions/question.ts +1 -0
  150. package/examples/extensions/sandbox/package-lock.json +2 -2
  151. package/examples/extensions/sandbox/package.json +1 -1
  152. package/examples/extensions/with-deps/package-lock.json +2 -2
  153. package/examples/extensions/with-deps/package.json +1 -1
  154. package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts +3 -1
  155. package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts.map +1 -1
  156. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +9 -1
  157. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js.map +1 -1
  158. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.d.ts.map +1 -1
  159. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +4 -6
  160. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js.map +1 -1
  161. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.d.ts.map +1 -1
  162. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.js +20 -2
  163. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.js.map +1 -1
  164. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  165. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  166. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +6 -0
  167. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  168. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.d.ts.map +1 -1
  169. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js +2 -14
  170. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js.map +1 -1
  171. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  172. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +16 -5
  173. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js.map +1 -1
  174. package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.d.ts.map +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.js +2 -1
  176. package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.js.map +1 -1
  177. package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.d.ts.map +1 -1
  178. package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.js +2 -1
  179. package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.js.map +1 -1
  180. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.d.ts.map +1 -1
  181. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +56 -39
  182. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js.map +1 -1
  183. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
  184. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +8 -3
  185. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  186. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts.map +1 -1
  187. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +2 -14
  188. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js.map +1 -1
  189. package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.d.ts.map +1 -1
  190. package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.js +2 -1
  191. package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.js.map +1 -1
  192. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts +15 -45
  193. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts.map +1 -1
  194. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +15 -45
  195. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js.map +1 -1
  196. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +456 -37
  197. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  198. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.d.ts +123 -0
  199. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.d.ts.map +1 -1
  200. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js +124 -4
  201. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js.map +1 -1
  202. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.d.ts +20 -0
  203. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.d.ts.map +1 -1
  204. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js +18 -0
  205. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js.map +1 -1
  206. package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.d.ts +21 -0
  207. package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.d.ts.map +1 -1
  208. package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.js +18 -0
  209. package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.js.map +1 -1
  210. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.d.ts +21 -0
  211. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.d.ts.map +1 -1
  212. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js +18 -0
  213. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
  214. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.d.ts +10 -5
  215. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.d.ts.map +1 -1
  216. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js +4 -3
  217. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js.map +1 -1
  218. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.d.ts +82 -0
  219. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.d.ts.map +1 -1
  220. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js +57 -0
  221. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js.map +1 -1
  222. package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.d.ts +0 -28
  223. package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.d.ts.map +1 -1
  224. package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.js +0 -19
  225. package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.js.map +1 -1
  226. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js +5 -5
  227. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js.map +1 -1
  228. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.d.ts +89 -0
  229. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.d.ts.map +1 -1
  230. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js +75 -2
  231. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js.map +1 -1
  232. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts +46 -4
  233. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts.map +1 -1
  234. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +91 -55
  235. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js.map +1 -1
  236. package/node_modules/@earendil-works/pi-ai/dist/providers/together.models.js +2 -2
  237. package/node_modules/@earendil-works/pi-ai/dist/providers/together.models.js.map +1 -1
  238. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts +44 -0
  239. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
  240. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +41 -4
  241. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js.map +1 -1
  242. package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts +25 -0
  243. package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.d.ts.map +1 -0
  244. package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js +109 -0
  245. package/node_modules/@earendil-works/pi-ai/dist/utils/error-body.js.map +1 -0
  246. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.d.ts +1 -0
  247. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.d.ts.map +1 -1
  248. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js +6 -0
  249. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js.map +1 -1
  250. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  251. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.js +1 -0
  252. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  253. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts +1 -0
  254. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  255. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +3 -0
  256. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js.map +1 -1
  257. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  258. package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts +4 -0
  259. package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts.map +1 -0
  260. package/node_modules/@earendil-works/pi-tui/dist/mux.js +10 -0
  261. package/node_modules/@earendil-works/pi-tui/dist/mux.js.map +1 -0
  262. package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts +42 -1
  263. package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts.map +1 -1
  264. package/node_modules/@earendil-works/pi-tui/dist/tui.js +422 -101
  265. package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +1 -1
  266. package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts +10 -0
  267. package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts.map +1 -1
  268. package/node_modules/@earendil-works/pi-tui/dist/utils.js +53 -13
  269. package/node_modules/@earendil-works/pi-tui/dist/utils.js.map +1 -1
  270. package/node_modules/@earendil-works/pi-tui/package.json +2 -1
  271. package/npm-shrinkwrap.json +9 -9
  272. package/package.json +4 -4
@@ -6,6 +6,7 @@ import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import { performance } from "node:perf_hooks";
8
8
  import { isKeyRelease, matchesKey } from "./keys.js";
9
+ import { isMultiplexerSession, useLegacyMuxRender, viewportRenderEnabled } from "./mux.js";
9
10
  import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
10
11
  import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
11
12
  import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
@@ -55,6 +56,103 @@ export function isFocusable(component) {
55
56
  */
56
57
  export const CURSOR_MARKER = "\x1b_pi:c\x07";
57
58
  export { visibleWidth };
59
+ const renderErrorLoggedClasses = new Set();
60
+ let renderErrorLogWrites = 0;
61
+ const DIAGNOSTIC_LOG_MODE = 0o600;
62
+ const VIEWPORT_RENDER_OVERSCAN = 16;
63
+ // Keep scroll-region wins cheap when a few visible rows mutate during append streaming.
64
+ const MAX_SCROLL_DIFF_ROWS = 4;
65
+ const viewportRenderStats = {
66
+ lastNormalizedLines: 0,
67
+ lastKittyImageScannedLines: 0,
68
+ totalNormalizedLines: 0,
69
+ totalKittyImageScannedLines: 0,
70
+ boundedFrames: 0,
71
+ escapedFrames: 0,
72
+ fullFrames: 0,
73
+ };
74
+ export function __renderErrorLogStats() {
75
+ if (process.env.PI_TUI_TEST_SEAMS !== "1") {
76
+ return undefined;
77
+ }
78
+ return { writes: renderErrorLogWrites };
79
+ }
80
+ export function __viewportRenderStats() {
81
+ if (process.env.PI_TUI_TEST_SEAMS !== "1") {
82
+ return undefined;
83
+ }
84
+ return { ...viewportRenderStats };
85
+ }
86
+ function recordViewportRenderStats(normalizedLines, mode) {
87
+ viewportRenderStats.lastNormalizedLines = normalizedLines;
88
+ viewportRenderStats.totalNormalizedLines += normalizedLines;
89
+ if (mode === "bounded") {
90
+ viewportRenderStats.boundedFrames += 1;
91
+ }
92
+ else if (mode === "escaped") {
93
+ viewportRenderStats.escapedFrames += 1;
94
+ }
95
+ else {
96
+ viewportRenderStats.fullFrames += 1;
97
+ }
98
+ }
99
+ function recordKittyImageScanStats(scannedLines) {
100
+ viewportRenderStats.lastKittyImageScannedLines = scannedLines;
101
+ viewportRenderStats.totalKittyImageScannedLines += scannedLines;
102
+ }
103
+ function componentRenderErrorName(component) {
104
+ return component.constructor.name || "AnonymousComponent";
105
+ }
106
+ function logRenderErrorOnce(component, error) {
107
+ const componentName = componentRenderErrorName(component);
108
+ if (renderErrorLoggedClasses.has(componentName)) {
109
+ return;
110
+ }
111
+ renderErrorLoggedClasses.add(componentName);
112
+ renderErrorLogWrites += 1;
113
+ const errorText = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
114
+ const logPath = path.join(os.homedir(), ".senpi", "agent", "senpi-debug.log");
115
+ const msg = `[${new Date().toISOString()}] render error: ${componentName}: ${errorText}\n`;
116
+ appendRenderErrorLogBestEffort(logPath, msg);
117
+ }
118
+ function appendRenderErrorLogBestEffort(logPath, msg) {
119
+ try {
120
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
121
+ fs.appendFileSync(logPath, msg, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
122
+ chmodDiagnosticLogBestEffort(logPath);
123
+ }
124
+ catch (error) {
125
+ if (error instanceof Error) {
126
+ return;
127
+ }
128
+ throw error;
129
+ }
130
+ }
131
+ function writeRenderDiagnosticBestEffort(logPath, data) {
132
+ try {
133
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
134
+ fs.writeFileSync(logPath, data, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
135
+ chmodDiagnosticLogBestEffort(logPath);
136
+ return true;
137
+ }
138
+ catch (error) {
139
+ if (error instanceof Error) {
140
+ return false;
141
+ }
142
+ throw error;
143
+ }
144
+ }
145
+ function chmodDiagnosticLogBestEffort(logPath) {
146
+ try {
147
+ fs.chmodSync(logPath, DIAGNOSTIC_LOG_MODE);
148
+ }
149
+ catch (error) {
150
+ if (error instanceof Error) {
151
+ return;
152
+ }
153
+ throw error;
154
+ }
155
+ }
58
156
  /** Parse a SizeValue into absolute value given a reference size */
59
157
  function parseSizeValue(value, referenceSize) {
60
158
  if (value === undefined)
@@ -120,7 +218,16 @@ export class Container {
120
218
  render(width) {
121
219
  const lines = [];
122
220
  for (const child of this.children) {
123
- const childLines = child.render(width);
221
+ let childLines;
222
+ try {
223
+ childLines = child.render(width);
224
+ }
225
+ catch (error) {
226
+ logRenderErrorOnce(child, error);
227
+ const componentName = componentRenderErrorName(child);
228
+ // Focus ownership stays unchanged; render containment must not steal or clear focus implicitly.
229
+ childLines = [`[render error: ${componentName}]`];
230
+ }
124
231
  for (const line of childLines) {
125
232
  lines.push(line);
126
233
  }
@@ -133,9 +240,12 @@ export class Container {
133
240
  */
134
241
  export class TUI extends Container {
135
242
  static { this.MIN_RENDER_INTERVAL_MS = 16; }
136
- constructor(terminal, showHardwareCursor) {
243
+ #lastCursorVisibility;
244
+ #muxDetector;
245
+ constructor(terminal, options) {
137
246
  super();
138
247
  this.previousLines = [];
248
+ this.previousRawLines = [];
139
249
  this.previousKittyImageIds = new Set();
140
250
  this.previousWidth = 0;
141
251
  this.previousHeight = 0;
@@ -151,6 +261,8 @@ export class TUI extends Container {
151
261
  this.maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered)
152
262
  this.previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
153
263
  this.fullRedrawCount = 0;
264
+ this.muxViewportRepaintCount = 0;
265
+ this.overWideCrashDumpWritten = false;
154
266
  this.stopped = false;
155
267
  this.pendingOsc11BackgroundReplies = 0;
156
268
  this.pendingOsc11BackgroundQueries = [];
@@ -161,13 +273,19 @@ export class TUI extends Container {
161
273
  this.overlayStack = [];
162
274
  this.overlayFocusRestore = { status: "inactive" };
163
275
  this.terminal = terminal;
164
- if (showHardwareCursor !== undefined) {
165
- this.showHardwareCursor = showHardwareCursor;
276
+ // Preserve existing positional boolean callers while allowing explicit render-policy overrides.
277
+ const normalizedOptions = typeof options === "boolean" ? { showHardwareCursor: options } : (options ?? {});
278
+ this.#muxDetector = normalizedOptions.muxDetector ?? isMultiplexerSession;
279
+ if (normalizedOptions.showHardwareCursor !== undefined) {
280
+ this.showHardwareCursor = normalizedOptions.showHardwareCursor;
166
281
  }
167
282
  }
168
283
  get fullRedraws() {
169
284
  return this.fullRedrawCount;
170
285
  }
286
+ get muxViewportRepaints() {
287
+ return this.muxViewportRepaintCount;
288
+ }
171
289
  getShowHardwareCursor() {
172
290
  return this.showHardwareCursor;
173
291
  }
@@ -176,7 +294,7 @@ export class TUI extends Container {
176
294
  return;
177
295
  this.showHardwareCursor = enabled;
178
296
  if (!enabled) {
179
- this.terminal.hideCursor();
297
+ this.#setCursorVisibility(false);
180
298
  }
181
299
  this.requestRender();
182
300
  }
@@ -318,7 +436,7 @@ export class TUI extends Container {
318
436
  if (!options?.nonCapturing && this.isOverlayVisible(entry)) {
319
437
  this.setFocus(component);
320
438
  }
321
- this.terminal.hideCursor();
439
+ this.#setCursorVisibility(false);
322
440
  this.requestRender();
323
441
  // Return handle for controlling this overlay
324
442
  return {
@@ -334,7 +452,7 @@ export class TUI extends Container {
334
452
  this.setFocus(topVisible?.component ?? entry.preFocus);
335
453
  }
336
454
  if (this.overlayStack.length === 0)
337
- this.terminal.hideCursor();
455
+ this.#setCursorVisibility(false);
338
456
  this.requestRender();
339
457
  }
340
458
  },
@@ -416,7 +534,7 @@ export class TUI extends Container {
416
534
  this.setFocus(topVisible?.component ?? overlay.preFocus);
417
535
  }
418
536
  if (this.overlayStack.length === 0)
419
- this.terminal.hideCursor();
537
+ this.#setCursorVisibility(false);
420
538
  this.requestRender();
421
539
  }
422
540
  /** Check if there are any visible overlays */
@@ -451,8 +569,9 @@ export class TUI extends Container {
451
569
  }
452
570
  start() {
453
571
  this.stopped = false;
572
+ this.#lastCursorVisibility = undefined;
454
573
  this.terminal.start((data) => this.handleInput(data), () => this.requestRender());
455
- this.terminal.hideCursor();
574
+ this.#setCursorVisibility(false);
456
575
  if (this.terminalColorSchemeNotificationsEnabled) {
457
576
  this.terminal.write("\x1b[?2031h");
458
577
  }
@@ -513,9 +632,12 @@ export class TUI extends Container {
513
632
  }
514
633
  this.terminal.write("\r\n");
515
634
  }
516
- this.terminal.showCursor();
635
+ this.#lastCursorVisibility = undefined;
636
+ this.#setCursorVisibility(true);
517
637
  this.terminal.stop();
638
+ this.#lastCursorVisibility = undefined;
518
639
  this.previousLines = [];
640
+ this.previousRawLines = [];
519
641
  this.previousKittyImageIds.clear();
520
642
  this.previousWidth = 0;
521
643
  this.previousHeight = 0;
@@ -524,10 +646,22 @@ export class TUI extends Container {
524
646
  this.maxLinesRendered = 0;
525
647
  this.previousViewportTop = 0;
526
648
  }
649
+ #setCursorVisibility(visible) {
650
+ if (this.#lastCursorVisibility === visible)
651
+ return;
652
+ if (visible) {
653
+ this.terminal.showCursor();
654
+ }
655
+ else {
656
+ this.terminal.hideCursor();
657
+ }
658
+ this.#lastCursorVisibility = visible;
659
+ }
527
660
  requestRender(force = false, source = "unknown") {
528
661
  if (force) {
529
662
  this.inputRenderPending = false;
530
663
  this.previousLines = [];
664
+ this.previousRawLines = [];
531
665
  this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
532
666
  this.previousHeight = -1; // -1 triggers heightChanged, forcing a full clear
533
667
  this.cursorRow = 0;
@@ -892,17 +1026,96 @@ export class TUI extends Container {
892
1026
  return result;
893
1027
  }
894
1028
  static { this.SEGMENT_RESET = "\x1b[0m\x1b]8;;\x07"; }
895
- applyLineResets(lines) {
896
- const reset = TUI.SEGMENT_RESET;
1029
+ /**
1030
+ * Every frame write is bracketed by synchronized output (DECSET 2026) and
1031
+ * disables autowrap (DECAWM, DECRST 7) while rows are painted. Differential
1032
+ * rendering tracks the cursor with relative moves only, so a row the
1033
+ * terminal draws wider than visibleWidth() measured (East-Asian-ambiguous
1034
+ * glyphs, emoji newer than the terminal's Unicode tables, decomposed jamo)
1035
+ * would wrap, silently shift the cursor one row down, and leave stale ghost
1036
+ * rows behind on every following diff. With autowrap off such rows clip at
1037
+ * the last column instead. Autowrap is restored at frame end so the shell
1038
+ * never observes the disabled state.
1039
+ */
1040
+ static { this.FRAME_BEGIN = "\x1b[?2026h\x1b[?7l"; }
1041
+ static { this.FRAME_END = "\x1b[?7h\x1b[?2026l"; }
1042
+ setPreviousLines(lines, rawLines) {
1043
+ this.previousLines = lines;
1044
+ this.previousRawLines = rawLines;
1045
+ }
1046
+ normalizeLine(line) {
1047
+ if (isImageLine(line)) {
1048
+ return { line, normalized: false };
1049
+ }
1050
+ return { line: normalizeTerminalOutput(line) + TUI.SEGMENT_RESET, normalized: true };
1051
+ }
1052
+ applyLineResets(lines, mode = "full") {
1053
+ const normalizedLines = [];
1054
+ let normalizedCount = 0;
897
1055
  for (let i = 0; i < lines.length; i++) {
898
1056
  const line = lines[i];
899
- if (!isImageLine(line)) {
900
- lines[i] = normalizeTerminalOutput(line) + reset;
1057
+ if (isImageLine(line)) {
1058
+ normalizedLines.push(line);
1059
+ }
1060
+ else {
1061
+ normalizedLines.push(normalizeTerminalOutput(line) + TUI.SEGMENT_RESET);
1062
+ normalizedCount += 1;
901
1063
  }
902
1064
  }
903
- return lines;
1065
+ recordViewportRenderStats(normalizedCount, mode);
1066
+ return {
1067
+ lines: normalizedLines,
1068
+ firstRawChanged: 0,
1069
+ compareEndExclusive: normalizedLines.length,
1070
+ bounded: false,
1071
+ };
1072
+ }
1073
+ applyViewportLineResets(rawLines, viewportTop, height, stableDimensions) {
1074
+ if (!viewportRenderEnabled() ||
1075
+ !stableDimensions ||
1076
+ this.previousLines.length === 0 ||
1077
+ this.previousLines.length !== rawLines.length ||
1078
+ this.previousRawLines.length !== rawLines.length) {
1079
+ return this.applyLineResets(rawLines);
1080
+ }
1081
+ const windowStart = Math.max(0, viewportTop - VIEWPORT_RENDER_OVERSCAN);
1082
+ const windowEnd = Math.min(rawLines.length, viewportTop + height + VIEWPORT_RENDER_OVERSCAN);
1083
+ let firstRawChanged = -1;
1084
+ for (let i = 0; i < rawLines.length; i++) {
1085
+ if (rawLines[i] === this.previousRawLines[i]) {
1086
+ continue;
1087
+ }
1088
+ if (firstRawChanged === -1) {
1089
+ firstRawChanged = i;
1090
+ }
1091
+ if (i < windowStart || i >= windowEnd) {
1092
+ return this.applyLineResets(rawLines, "escaped");
1093
+ }
1094
+ }
1095
+ const lines = this.previousLines.slice();
1096
+ let normalizedCount = 0;
1097
+ if (firstRawChanged !== -1) {
1098
+ for (let i = windowStart; i < windowEnd; i++) {
1099
+ if (rawLines[i] === this.previousRawLines[i]) {
1100
+ continue;
1101
+ }
1102
+ const normalized = this.normalizeLine(rawLines[i] ?? "");
1103
+ lines[i] = normalized.line;
1104
+ if (normalized.normalized) {
1105
+ normalizedCount += 1;
1106
+ }
1107
+ }
1108
+ }
1109
+ recordViewportRenderStats(normalizedCount, "bounded");
1110
+ return {
1111
+ lines,
1112
+ firstRawChanged,
1113
+ compareEndExclusive: windowEnd,
1114
+ bounded: true,
1115
+ };
904
1116
  }
905
1117
  collectKittyImageIds(lines) {
1118
+ recordKittyImageScanStats(lines.length);
906
1119
  const ids = new Set();
907
1120
  for (const line of lines) {
908
1121
  for (const id of extractKittyImageIds(line)) {
@@ -950,6 +1163,24 @@ export class TUI extends Container {
950
1163
  expandForLines(newLines);
951
1164
  return { firstChanged: expandedFirstChanged, lastChanged: expandedLastChanged };
952
1165
  }
1166
+ changedRangeNeedsKittyImageExpansion(lines, firstChanged, lastChanged) {
1167
+ if (this.previousKittyImageIds.size > 0) {
1168
+ return true;
1169
+ }
1170
+ const start = Math.max(0, firstChanged);
1171
+ const end = Math.min(lines.length - 1, lastChanged);
1172
+ let scannedLines = 0;
1173
+ for (let i = start; i <= end; i++) {
1174
+ scannedLines += 1;
1175
+ const line = lines[i] ?? "";
1176
+ if (isImageLine(line) || extractKittyImageIds(line).length > 0) {
1177
+ recordKittyImageScanStats(scannedLines);
1178
+ return true;
1179
+ }
1180
+ }
1181
+ recordKittyImageScanStats(scannedLines);
1182
+ return false;
1183
+ }
953
1184
  deleteChangedKittyImages(firstChanged, lastChanged) {
954
1185
  if (firstChanged < 0 || lastChanged < firstChanged)
955
1186
  return "";
@@ -965,6 +1196,9 @@ export class TUI extends Container {
965
1196
  getViewportRows(lines, viewportTop, height) {
966
1197
  return Array.from({ length: height }, (_, row) => lines[viewportTop + row] ?? "");
967
1198
  }
1199
+ shouldPreserveMuxScrollback() {
1200
+ return this.#muxDetector() && !useLegacyMuxRender();
1201
+ }
968
1202
  createViewportInsertScrollPlan(newLines, prevViewportTop, height, lineCountDelta) {
969
1203
  if (lineCountDelta <= 0 || lineCountDelta >= height || this.overlayStack.length > 0) {
970
1204
  return undefined;
@@ -988,19 +1222,24 @@ export class TUI extends Container {
988
1222
  if (regionHeight <= 0 || regionHeight < lineCountDelta) {
989
1223
  return undefined;
990
1224
  }
1225
+ const mutatedRows = [];
991
1226
  for (let row = 0; row < regionHeight - lineCountDelta; row++) {
992
1227
  if (previousVisible[row + lineCountDelta] !== nextVisible[row]) {
993
- return undefined;
1228
+ mutatedRows.push({ screenRow: row, content: nextVisible[row] });
1229
+ if (mutatedRows.length > MAX_SCROLL_DIFF_ROWS) {
1230
+ return undefined;
1231
+ }
994
1232
  }
995
1233
  }
996
1234
  return {
997
1235
  viewportTop,
998
1236
  regionBottom: regionHeight - 1,
999
1237
  insertedRows: nextVisible.slice(regionHeight - lineCountDelta, regionHeight),
1238
+ mutatedRows,
1000
1239
  };
1001
1240
  }
1002
- renderViewportInsertScroll(plan, newLines, cursorPos, width, height) {
1003
- let buffer = "\x1b[?2026h";
1241
+ renderViewportInsertScroll(plan, newLines, rawLines, cursorPos, width, height) {
1242
+ let buffer = TUI.FRAME_BEGIN;
1004
1243
  const regionTop = 0;
1005
1244
  const regionBottom = plan.regionBottom;
1006
1245
  buffer += `\x1b[${regionTop + 1};${regionBottom + 1}r`;
@@ -1008,27 +1247,35 @@ export class TUI extends Container {
1008
1247
  buffer += "\n".repeat(plan.insertedRows.length);
1009
1248
  buffer += "\x1b[r";
1010
1249
  const firstInsertedScreenRow = regionBottom - plan.insertedRows.length + 1;
1250
+ let finalPaintedScreenRow = firstInsertedScreenRow + plan.insertedRows.length - 1;
1011
1251
  for (let index = 0; index < plan.insertedRows.length; index++) {
1012
1252
  const screenRow = firstInsertedScreenRow + index;
1013
- buffer += `\x1b[${screenRow + 1};1H\x1b[2K`;
1253
+ buffer += `\x1b[${screenRow + 1};1H\x1b[2K${TUI.SEGMENT_RESET}`;
1014
1254
  buffer += plan.insertedRows[index] ?? "";
1015
1255
  }
1016
- buffer += "\x1b[?2026l";
1256
+ for (const row of plan.mutatedRows) {
1257
+ buffer += `\x1b[${row.screenRow + 1};1H\x1b[2K${TUI.SEGMENT_RESET}`;
1258
+ buffer += row.content;
1259
+ finalPaintedScreenRow = row.screenRow;
1260
+ }
1261
+ buffer += TUI.FRAME_END;
1017
1262
  this.terminal.write(buffer);
1018
1263
  this.cursorRow = Math.max(0, newLines.length - 1);
1019
- this.hardwareCursorRow = plan.viewportTop + firstInsertedScreenRow + plan.insertedRows.length - 1;
1264
+ this.hardwareCursorRow = plan.viewportTop + finalPaintedScreenRow;
1020
1265
  this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
1021
1266
  this.previousViewportTop = plan.viewportTop;
1022
1267
  this.positionHardwareCursor(cursorPos, newLines.length);
1023
- this.previousLines = newLines;
1268
+ this.setPreviousLines(newLines, rawLines);
1024
1269
  this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1025
1270
  this.previousWidth = width;
1026
1271
  this.previousHeight = height;
1027
1272
  }
1028
- renderScrollbackReplay(newLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow) {
1029
- let buffer = "\x1b[?2026h";
1273
+ renderScrollbackReplay(newLines, rawLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow) {
1274
+ let buffer = TUI.FRAME_BEGIN;
1030
1275
  buffer += this.deleteKittyImages(this.previousKittyImageIds);
1031
- buffer += "\x1b[3J";
1276
+ if (!this.shouldPreserveMuxScrollback()) {
1277
+ buffer += "\x1b[3J";
1278
+ }
1032
1279
  const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
1033
1280
  if (currentScreenRow > 0) {
1034
1281
  buffer += `\x1b[${currentScreenRow}A`;
@@ -1037,21 +1284,52 @@ export class TUI extends Container {
1037
1284
  for (let row = 0; row < bufferLength; row++) {
1038
1285
  if (row > 0)
1039
1286
  buffer += "\r\n";
1040
- buffer += "\r\x1b[2K";
1287
+ buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
1041
1288
  buffer += newLines[row] ?? "";
1042
1289
  }
1043
- buffer += "\x1b[?2026l";
1290
+ buffer += TUI.FRAME_END;
1044
1291
  this.terminal.write(buffer);
1045
1292
  this.cursorRow = Math.max(0, newLines.length - 1);
1046
1293
  this.hardwareCursorRow = bufferLength - 1;
1047
1294
  this.maxLinesRendered = newLines.length;
1048
1295
  this.previousViewportTop = Math.max(0, bufferLength - height);
1049
1296
  this.positionHardwareCursor(cursorPos, newLines.length);
1050
- this.previousLines = newLines;
1297
+ this.setPreviousLines(newLines, rawLines);
1051
1298
  this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1052
1299
  this.previousWidth = width;
1053
1300
  this.previousHeight = height;
1054
1301
  }
1302
+ renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop = Math.max(0, newLines.length - height)) {
1303
+ const previousVisible = this.getViewportRows(this.previousLines, this.previousViewportTop, height);
1304
+ const nextVisible = this.getViewportRows(newLines, viewportTop, height);
1305
+ if (previousVisible.some(isImageLine) || nextVisible.some(isImageLine)) {
1306
+ return false;
1307
+ }
1308
+ let buffer = TUI.FRAME_BEGIN;
1309
+ const currentScreenRow = Math.max(0, Math.min(height - 1, this.hardwareCursorRow - this.previousViewportTop));
1310
+ if (currentScreenRow > 0) {
1311
+ buffer += `\x1b[${currentScreenRow}A`;
1312
+ }
1313
+ for (let row = 0; row < height; row++) {
1314
+ if (row > 0)
1315
+ buffer += "\r\n";
1316
+ buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
1317
+ buffer += newLines[viewportTop + row] ?? "";
1318
+ }
1319
+ buffer += TUI.FRAME_END;
1320
+ this.terminal.write(buffer);
1321
+ this.muxViewportRepaintCount += 1;
1322
+ this.cursorRow = Math.max(0, newLines.length - 1);
1323
+ this.hardwareCursorRow = viewportTop + Math.max(0, height - 1);
1324
+ this.maxLinesRendered = newLines.length;
1325
+ this.previousViewportTop = viewportTop;
1326
+ this.positionHardwareCursor(cursorPos, newLines.length);
1327
+ this.setPreviousLines(newLines, rawLines);
1328
+ this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1329
+ this.previousWidth = width;
1330
+ this.previousHeight = height;
1331
+ return true;
1332
+ }
1055
1333
  /** Splice overlay content into a base line at a specific column. Single-pass optimized. */
1056
1334
  compositeLineAt(baseLine, overlayLine, startCol, overlayWidth, totalWidth) {
1057
1335
  if (isImageLine(baseLine))
@@ -1140,17 +1418,23 @@ export class TUI extends Container {
1140
1418
  }
1141
1419
  // Extract cursor position before applying line resets (marker must be found first)
1142
1420
  const cursorPos = this.extractCursorPosition(newLines, height);
1143
- newLines = this.applyLineResets(newLines);
1421
+ const rawLines = newLines;
1422
+ const normalizedLines = this.applyViewportLineResets(rawLines, prevViewportTop, height, !widthChanged && !heightChanged);
1423
+ newLines = normalizedLines.lines;
1424
+ const preserveMuxScrollback = this.shouldPreserveMuxScrollback();
1144
1425
  // Helper to clear scrollback and viewport and render all new lines
1145
- const fullRender = (clear) => {
1426
+ const fullRender = (clear, clearScrollback = clear) => {
1146
1427
  this.fullRedrawCount += 1;
1147
- let buffer = "\x1b[?2026h"; // Begin synchronized output
1428
+ let buffer = TUI.FRAME_BEGIN;
1148
1429
  if (clear) {
1149
1430
  buffer += this.deleteKittyImages(this.previousKittyImageIds);
1150
- buffer += "\x1b[2J\x1b[H\x1b[3J"; // Clear screen, home, then clear scrollback
1431
+ buffer += "\x1b[2J\x1b[H";
1432
+ if (clearScrollback && !preserveMuxScrollback) {
1433
+ buffer += "\x1b[3J";
1434
+ }
1151
1435
  }
1152
1436
  else {
1153
- buffer += "\r\x1b[2K";
1437
+ buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
1154
1438
  }
1155
1439
  for (let i = 0; i < newLines.length; i++) {
1156
1440
  if (i > 0)
@@ -1170,7 +1454,7 @@ export class TUI extends Container {
1170
1454
  }
1171
1455
  buffer += line;
1172
1456
  }
1173
- buffer += "\x1b[?2026l"; // End synchronized output
1457
+ buffer += TUI.FRAME_END;
1174
1458
  this.terminal.write(buffer);
1175
1459
  this.cursorRow = Math.max(0, newLines.length - 1);
1176
1460
  this.hardwareCursorRow = this.cursorRow;
@@ -1184,7 +1468,7 @@ export class TUI extends Container {
1184
1468
  const bufferLength = Math.max(height, newLines.length);
1185
1469
  this.previousViewportTop = Math.max(0, bufferLength - height);
1186
1470
  this.positionHardwareCursor(cursorPos, newLines.length);
1187
- this.previousLines = newLines;
1471
+ this.setPreviousLines(newLines, rawLines);
1188
1472
  this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1189
1473
  this.previousWidth = width;
1190
1474
  this.previousHeight = height;
@@ -1195,7 +1479,8 @@ export class TUI extends Container {
1195
1479
  return;
1196
1480
  const logPath = path.join(os.homedir(), ".senpi", "agent", "senpi-debug.log");
1197
1481
  const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.previousLines.length}, new=${newLines.length}, height=${height})\n`;
1198
- fs.appendFileSync(logPath, msg);
1482
+ fs.appendFileSync(logPath, msg, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
1483
+ chmodDiagnosticLogBestEffort(logPath);
1199
1484
  };
1200
1485
  // First render - just output everything without clearing (assumes clean screen)
1201
1486
  if (this.previousLines.length === 0 && !widthChanged && !heightChanged) {
@@ -1206,7 +1491,8 @@ export class TUI extends Container {
1206
1491
  // Width changes always need a full re-render because wrapping changes.
1207
1492
  if (widthChanged) {
1208
1493
  logRedraw(`terminal width changed (${this.previousWidth} -> ${width})`);
1209
- fullRender(true);
1494
+ // In multiplexers, re-emit the viewport without 3J so pane history survives; an older copy may remain above.
1495
+ fullRender(true, !preserveMuxScrollback);
1210
1496
  return;
1211
1497
  }
1212
1498
  // Height changes normally need a full re-render to keep the visible viewport aligned,
@@ -1214,7 +1500,14 @@ export class TUI extends Container {
1214
1500
  // In that environment, a full redraw causes the entire history to replay on every toggle.
1215
1501
  if (heightChanged && !isTermuxSession()) {
1216
1502
  logRedraw(`terminal height changed (${this.previousHeight} -> ${height})`);
1217
- fullRender(true);
1503
+ if (preserveMuxScrollback) {
1504
+ if (!this.renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height)) {
1505
+ fullRender(true, false);
1506
+ }
1507
+ }
1508
+ else {
1509
+ fullRender(true);
1510
+ }
1218
1511
  return;
1219
1512
  }
1220
1513
  // Content shrunk below the working area and no overlays - re-render to clear empty rows
@@ -1222,14 +1515,16 @@ export class TUI extends Container {
1222
1515
  // Configurable via setClearOnShrink() or PI_CLEAR_ON_SHRINK=0 env var
1223
1516
  if (this.clearOnShrink && newLines.length < this.maxLinesRendered && this.overlayStack.length === 0) {
1224
1517
  logRedraw(`clearOnShrink (maxLinesRendered=${this.maxLinesRendered})`);
1225
- fullRender(true);
1518
+ fullRender(true, !preserveMuxScrollback);
1226
1519
  return;
1227
1520
  }
1228
1521
  // Find first and last changed lines
1229
1522
  let firstChanged = -1;
1230
1523
  let lastChanged = -1;
1231
1524
  const maxLines = Math.max(newLines.length, this.previousLines.length);
1232
- for (let i = 0; i < maxLines; i++) {
1525
+ const diffScanStart = normalizedLines.bounded && normalizedLines.firstRawChanged !== -1 ? normalizedLines.firstRawChanged : 0;
1526
+ const diffScanEndExclusive = normalizedLines.bounded ? normalizedLines.compareEndExclusive : maxLines;
1527
+ for (let i = diffScanStart; i < diffScanEndExclusive; i++) {
1233
1528
  const oldLine = i < this.previousLines.length ? this.previousLines[i] : "";
1234
1529
  const newLine = i < newLines.length ? newLines[i] : "";
1235
1530
  if (oldLine !== newLine) {
@@ -1247,7 +1542,8 @@ export class TUI extends Container {
1247
1542
  }
1248
1543
  lastChanged = newLines.length - 1;
1249
1544
  }
1250
- if (firstChanged !== -1) {
1545
+ const needsKittyImageExpansion = firstChanged !== -1 && this.changedRangeNeedsKittyImageExpansion(newLines, firstChanged, lastChanged);
1546
+ if (needsKittyImageExpansion) {
1251
1547
  const expandedRange = this.expandChangedRangeForKittyImages(firstChanged, lastChanged, newLines);
1252
1548
  firstChanged = expandedRange.firstChanged;
1253
1549
  lastChanged = expandedRange.lastChanged;
@@ -1262,19 +1558,19 @@ export class TUI extends Container {
1262
1558
  return;
1263
1559
  }
1264
1560
  if (insertScrollPlan) {
1265
- this.renderViewportInsertScroll(insertScrollPlan, newLines, cursorPos, width, height);
1561
+ this.renderViewportInsertScroll(insertScrollPlan, newLines, rawLines, cursorPos, width, height);
1266
1562
  return;
1267
1563
  }
1268
1564
  // All changes are in deleted lines (nothing to render, just clear)
1269
1565
  if (firstChanged >= newLines.length) {
1270
1566
  if (this.previousLines.length > newLines.length) {
1271
- let buffer = "\x1b[?2026h";
1567
+ let buffer = TUI.FRAME_BEGIN;
1272
1568
  buffer += this.deleteChangedKittyImages(firstChanged, lastChanged);
1273
1569
  // Move to end of new content (clamp to 0 for empty content)
1274
1570
  const targetRow = Math.max(0, newLines.length - 1);
1275
1571
  if (targetRow < prevViewportTop) {
1276
1572
  logRedraw(`deleted lines moved viewport up (${targetRow} < ${prevViewportTop})`);
1277
- fullRender(true);
1573
+ fullRender(true, !preserveMuxScrollback);
1278
1574
  return;
1279
1575
  }
1280
1576
  const lineDiff = computeLineDiff(targetRow);
@@ -1287,7 +1583,7 @@ export class TUI extends Container {
1287
1583
  const extraLines = this.previousLines.length - newLines.length;
1288
1584
  if (extraLines > height) {
1289
1585
  logRedraw(`extraLines > height (${extraLines} > ${height})`);
1290
- fullRender(true);
1586
+ fullRender(true, !preserveMuxScrollback);
1291
1587
  return;
1292
1588
  }
1293
1589
  const clearStartOffset = newLines.length === 0 ? 0 : 1;
@@ -1295,7 +1591,7 @@ export class TUI extends Container {
1295
1591
  buffer += `\x1b[${clearStartOffset}B`;
1296
1592
  }
1297
1593
  for (let i = 0; i < extraLines; i++) {
1298
- buffer += "\r\x1b[2K";
1594
+ buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
1299
1595
  if (i < extraLines - 1)
1300
1596
  buffer += "\x1b[1B";
1301
1597
  }
@@ -1303,13 +1599,13 @@ export class TUI extends Container {
1303
1599
  if (moveBack > 0) {
1304
1600
  buffer += `\x1b[${moveBack}A`;
1305
1601
  }
1306
- buffer += "\x1b[?2026l";
1602
+ buffer += TUI.FRAME_END;
1307
1603
  this.terminal.write(buffer);
1308
1604
  this.cursorRow = targetRow;
1309
1605
  this.hardwareCursorRow = targetRow;
1310
1606
  }
1311
1607
  this.positionHardwareCursor(cursorPos, newLines.length);
1312
- this.previousLines = newLines;
1608
+ this.setPreviousLines(newLines, rawLines);
1313
1609
  this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1314
1610
  this.previousWidth = width;
1315
1611
  this.previousHeight = height;
@@ -1339,12 +1635,20 @@ export class TUI extends Container {
1339
1635
  }
1340
1636
  if (firstVisibleChanged === -1) {
1341
1637
  if (lineCountDelta !== 0) {
1342
- this.renderScrollbackReplay(newLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow);
1638
+ if (preserveMuxScrollback) {
1639
+ // Above-viewport scrollback may stay stale in mux panes; only the visible viewport is repainted.
1640
+ if (!this.renderMuxViewportRepaint(newLines, rawLines, cursorPos, width, height, viewportTop)) {
1641
+ fullRender(true, false);
1642
+ }
1643
+ }
1644
+ else {
1645
+ this.renderScrollbackReplay(newLines, rawLines, cursorPos, width, height, prevViewportTop, hardwareCursorRow);
1646
+ }
1343
1647
  return;
1344
1648
  }
1345
1649
  this.cursorRow = Math.max(0, newLines.length - 1);
1346
1650
  this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
1347
- this.previousLines = newLines;
1651
+ this.setPreviousLines(newLines, rawLines);
1348
1652
  this.previousWidth = width;
1349
1653
  this.previousHeight = height;
1350
1654
  this.previousViewportTop = viewportTop;
@@ -1352,7 +1656,7 @@ export class TUI extends Container {
1352
1656
  }
1353
1657
  if (viewportTop !== prevViewportTop) {
1354
1658
  const previousViewportBottom = Math.min(this.previousLines.length - 1, prevViewportTop + height - 1);
1355
- let buffer = "\x1b[?2026h";
1659
+ let buffer = TUI.FRAME_BEGIN;
1356
1660
  buffer += this.deleteChangedKittyImages(prevViewportTop, previousViewportBottom);
1357
1661
  const currentScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop));
1358
1662
  if (currentScreenRow > 0) {
@@ -1361,17 +1665,17 @@ export class TUI extends Container {
1361
1665
  for (let row = 0; row < height; row++) {
1362
1666
  if (row > 0)
1363
1667
  buffer += "\r\n";
1364
- buffer += "\r\x1b[2K";
1668
+ buffer += `\r\x1b[2K${TUI.SEGMENT_RESET}`;
1365
1669
  buffer += newLines[viewportTop + row] ?? "";
1366
1670
  }
1367
- buffer += "\x1b[?2026l";
1671
+ buffer += TUI.FRAME_END;
1368
1672
  this.terminal.write(buffer);
1369
1673
  this.cursorRow = Math.max(0, newLines.length - 1);
1370
1674
  this.hardwareCursorRow = viewportTop + Math.max(0, height - 1);
1371
1675
  this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
1372
1676
  this.previousViewportTop = viewportTop;
1373
1677
  this.positionHardwareCursor(cursorPos, newLines.length);
1374
- this.previousLines = newLines;
1678
+ this.setPreviousLines(newLines, rawLines);
1375
1679
  this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1376
1680
  this.previousWidth = width;
1377
1681
  this.previousHeight = height;
@@ -1382,7 +1686,7 @@ export class TUI extends Container {
1382
1686
  }
1383
1687
  // Render from first changed line to end
1384
1688
  // Build buffer with all updates wrapped in synchronized output
1385
- let buffer = "\x1b[?2026h"; // Begin synchronized output
1689
+ let buffer = TUI.FRAME_BEGIN;
1386
1690
  buffer += this.deleteChangedKittyImages(firstChanged, lastChanged);
1387
1691
  const prevViewportBottom = prevViewportTop + height - 1;
1388
1692
  const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged;
@@ -1420,12 +1724,12 @@ export class TUI extends Container {
1420
1724
  const imageStartScreenRow = i - viewportTop;
1421
1725
  if (imageStartScreenRow < 0 || imageStartScreenRow + imageReservedRows > height) {
1422
1726
  logRedraw(`kitty image pre-clear would scroll (${imageStartScreenRow} + ${imageReservedRows} > ${height})`);
1423
- fullRender(true);
1727
+ fullRender(true, !preserveMuxScrollback);
1424
1728
  return;
1425
1729
  }
1426
- buffer += "\x1b[2K";
1730
+ buffer += `\x1b[2K${TUI.SEGMENT_RESET}`;
1427
1731
  for (let row = 1; row < imageReservedRows; row++) {
1428
- buffer += "\r\n\x1b[2K";
1732
+ buffer += `\r\n\x1b[2K${TUI.SEGMENT_RESET}`;
1429
1733
  }
1430
1734
  buffer += `\x1b[${imageReservedRows - 1}A`;
1431
1735
  buffer += line;
@@ -1433,32 +1737,44 @@ export class TUI extends Container {
1433
1737
  i += imageReservedRows - 1;
1434
1738
  continue;
1435
1739
  }
1436
- buffer += "\x1b[2K"; // Clear current line
1437
- if (!isImage && visibleWidth(line) > width) {
1740
+ buffer += `\x1b[2K${TUI.SEGMENT_RESET}`; // Clear current line
1741
+ const lineWidth = visibleWidth(line);
1742
+ if (!isImage && lineWidth > width) {
1438
1743
  // Log all lines to crash file for debugging
1439
1744
  const crashLogPath = path.join(os.homedir(), ".senpi", "agent", "senpi-crash.log");
1440
1745
  const crashData = [
1441
1746
  `Crash at ${new Date().toISOString()}`,
1442
1747
  `Terminal width: ${width}`,
1443
- `Line ${i} visible width: ${visibleWidth(line)}`,
1748
+ `Line ${i} visible width: ${lineWidth}`,
1444
1749
  "",
1445
1750
  "=== All rendered lines ===",
1446
1751
  ...newLines.map((l, idx) => `[${idx}] (w=${visibleWidth(l)}) ${l}`),
1447
1752
  "",
1448
1753
  ].join("\n");
1449
- fs.mkdirSync(path.dirname(crashLogPath), { recursive: true });
1450
- fs.writeFileSync(crashLogPath, crashData);
1451
- // Clean up terminal state before throwing
1452
- this.stop();
1453
- const errorMsg = [
1454
- `Rendered line ${i} exceeds terminal width (${visibleWidth(line)} > ${width}).`,
1455
- "",
1456
- "This is likely caused by a custom TUI component not truncating its output.",
1457
- "Use visibleWidth() to measure and truncateToWidth() to truncate lines.",
1458
- "",
1459
- `Debug log written to: ${crashLogPath}`,
1460
- ].join("\n");
1461
- throw new Error(errorMsg);
1754
+ if (process.env.PI_TUI_STRICT_RENDER === "1") {
1755
+ const crashDumpWritten = writeRenderDiagnosticBestEffort(crashLogPath, crashData);
1756
+ // Clean up terminal state before throwing
1757
+ this.stop();
1758
+ const errorMsg = [
1759
+ `Rendered line ${i} exceeds terminal width (${lineWidth} > ${width}).`,
1760
+ "",
1761
+ "This is likely caused by a custom TUI component not truncating its output.",
1762
+ "Use visibleWidth() to measure and truncateToWidth() to truncate lines.",
1763
+ "",
1764
+ crashDumpWritten
1765
+ ? `Debug log written to: ${crashLogPath}`
1766
+ : `Debug log could not be written to: ${crashLogPath}`,
1767
+ ].join("\n");
1768
+ throw new Error(errorMsg);
1769
+ }
1770
+ if (!this.overWideCrashDumpWritten) {
1771
+ writeRenderDiagnosticBestEffort(crashLogPath, crashData);
1772
+ this.overWideCrashDumpWritten = true;
1773
+ }
1774
+ const truncatedLine = sliceByColumn(line, 0, width, true) + TUI.SEGMENT_RESET;
1775
+ newLines[i] = truncatedLine;
1776
+ buffer += truncatedLine;
1777
+ continue;
1462
1778
  }
1463
1779
  buffer += line;
1464
1780
  }
@@ -1474,12 +1790,12 @@ export class TUI extends Container {
1474
1790
  }
1475
1791
  const extraLines = this.previousLines.length - newLines.length;
1476
1792
  for (let i = newLines.length; i < this.previousLines.length; i++) {
1477
- buffer += "\r\n\x1b[2K";
1793
+ buffer += `\r\n\x1b[2K${TUI.SEGMENT_RESET}`;
1478
1794
  }
1479
1795
  // Move cursor back to end of new content
1480
1796
  buffer += `\x1b[${extraLines}A`;
1481
1797
  }
1482
- buffer += "\x1b[?2026l"; // End synchronized output
1798
+ buffer += TUI.FRAME_END;
1483
1799
  if (process.env.PI_TUI_DEBUG === "1") {
1484
1800
  const debugDir = "/tmp/tui";
1485
1801
  fs.mkdirSync(debugDir, { recursive: true });
@@ -1506,7 +1822,8 @@ export class TUI extends Container {
1506
1822
  "=== buffer ===",
1507
1823
  JSON.stringify(buffer),
1508
1824
  ].join("\n");
1509
- fs.writeFileSync(debugPath, debugData);
1825
+ fs.writeFileSync(debugPath, debugData, { encoding: "utf8", mode: DIAGNOSTIC_LOG_MODE });
1826
+ chmodDiagnosticLogBestEffort(debugPath);
1510
1827
  }
1511
1828
  // Write entire buffer at once
1512
1829
  this.terminal.write(buffer);
@@ -1520,8 +1837,10 @@ export class TUI extends Container {
1520
1837
  this.previousViewportTop = Math.max(prevViewportTop, finalCursorRow - height + 1);
1521
1838
  // Position hardware cursor for IME
1522
1839
  this.positionHardwareCursor(cursorPos, newLines.length);
1523
- this.previousLines = newLines;
1524
- this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1840
+ this.setPreviousLines(newLines, rawLines);
1841
+ if (needsKittyImageExpansion) {
1842
+ this.previousKittyImageIds = this.collectKittyImageIds(newLines);
1843
+ }
1525
1844
  this.previousWidth = width;
1526
1845
  this.previousHeight = height;
1527
1846
  }
@@ -1531,34 +1850,36 @@ export class TUI extends Container {
1531
1850
  * @param totalLines Total number of rendered lines
1532
1851
  */
1533
1852
  positionHardwareCursor(cursorPos, totalLines) {
1534
- if (!cursorPos || totalLines <= 0) {
1535
- this.terminal.hideCursor();
1536
- return;
1537
- }
1538
- // Clamp cursor position to valid range
1539
- const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
1540
- const targetCol = Math.max(0, cursorPos.col);
1541
- // Move cursor from current position to target
1542
- const rowDelta = targetRow - this.hardwareCursorRow;
1543
1853
  let buffer = "";
1544
- if (rowDelta > 0) {
1545
- buffer += `\x1b[${rowDelta}B`; // Move down
1854
+ if (!cursorPos || totalLines <= 0) {
1855
+ if (this.#lastCursorVisibility !== false) {
1856
+ buffer += "\x1b[?25l";
1857
+ this.#lastCursorVisibility = false;
1858
+ }
1546
1859
  }
1547
- else if (rowDelta < 0) {
1548
- buffer += `\x1b[${-rowDelta}A`; // Move up
1860
+ else {
1861
+ // Clamp cursor position to valid range
1862
+ const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
1863
+ const targetCol = Math.max(0, cursorPos.col);
1864
+ // Move cursor from current position to target
1865
+ const rowDelta = targetRow - this.hardwareCursorRow;
1866
+ if (rowDelta > 0) {
1867
+ buffer += `\x1b[${rowDelta}B`; // Move down
1868
+ }
1869
+ else if (rowDelta < 0) {
1870
+ buffer += `\x1b[${-rowDelta}A`; // Move up
1871
+ }
1872
+ // Move to absolute column (1-indexed)
1873
+ buffer += `\x1b[${targetCol + 1}G`;
1874
+ this.hardwareCursorRow = targetRow;
1875
+ if (this.#lastCursorVisibility !== this.showHardwareCursor) {
1876
+ buffer += this.showHardwareCursor ? "\x1b[?25h" : "\x1b[?25l";
1877
+ this.#lastCursorVisibility = this.showHardwareCursor;
1878
+ }
1549
1879
  }
1550
- // Move to absolute column (1-indexed)
1551
- buffer += `\x1b[${targetCol + 1}G`;
1552
1880
  if (buffer) {
1553
1881
  this.terminal.write(buffer);
1554
1882
  }
1555
- this.hardwareCursorRow = targetRow;
1556
- if (this.showHardwareCursor) {
1557
- this.terminal.showCursor();
1558
- }
1559
- else {
1560
- this.terminal.hideCursor();
1561
- }
1562
1883
  }
1563
1884
  /**
1564
1885
  * Query the terminal's default background color with OSC 11 (`ESC ] 11 ; ? BEL`).