gsd-pi 2.73.0 → 2.73.1-dev.47ed32a

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 (259) hide show
  1. package/dist/cli.js +0 -47
  2. package/dist/help-text.js +1 -1
  3. package/dist/resource-loader.js +2 -2
  4. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +9 -3
  5. package/dist/resources/extensions/gsd/auto-dispatch.js +5 -3
  6. package/dist/resources/extensions/gsd/auto-model-selection.js +54 -11
  7. package/dist/resources/extensions/gsd/auto-prompts.js +9 -6
  8. package/dist/resources/extensions/gsd/auto-start.js +23 -6
  9. package/dist/resources/extensions/gsd/auto.js +13 -1
  10. package/dist/resources/extensions/gsd/bootstrap/crash-log.js +31 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -7
  12. package/dist/resources/extensions/gsd/bootstrap/system-context.js +6 -1
  13. package/dist/resources/extensions/gsd/commands-handlers.js +8 -2
  14. package/dist/resources/extensions/gsd/crash-recovery.js +51 -0
  15. package/dist/resources/extensions/gsd/gsd-db.js +36 -2
  16. package/dist/resources/extensions/gsd/milestone-actions.js +19 -1
  17. package/dist/resources/extensions/gsd/preferences-models.js +43 -0
  18. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  19. package/dist/resources/extensions/gsd/preferences-validation.js +22 -0
  20. package/dist/startup-model-validation.js +8 -5
  21. package/dist/update-check.d.ts +1 -0
  22. package/dist/update-check.js +13 -5
  23. package/dist/update-cmd.js +4 -3
  24. package/dist/web/standalone/.next/BUILD_ID +1 -1
  25. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  26. package/dist/web/standalone/.next/build-manifest.json +3 -3
  27. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  28. package/dist/web/standalone/.next/required-server-files.json +3 -3
  29. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  30. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  40. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  56. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  68. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  88. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  98. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  104. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  118. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  120. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  122. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  124. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  132. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/app/index.html +1 -1
  134. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  135. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  136. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  137. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  138. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  139. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  140. package/dist/web/standalone/.next/server/app/page.js +2 -2
  141. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  142. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  143. package/dist/web/standalone/.next/server/chunks/63.js +3 -3
  144. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  145. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  146. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  147. package/dist/web/standalone/.next/server/middleware.js +2 -2
  148. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  149. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  150. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  151. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  152. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  153. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  154. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  155. package/dist/web/standalone/.next/static/chunks/app/page-f1e30ab6bb269149.js +1 -0
  156. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  157. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  158. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  159. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  160. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  161. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  162. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  163. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  164. package/dist/web/standalone/server.js +1 -1
  165. package/package.json +1 -1
  166. package/packages/pi-ai/dist/index.d.ts +1 -0
  167. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  168. package/packages/pi-ai/dist/index.js +1 -0
  169. package/packages/pi-ai/dist/index.js.map +1 -1
  170. package/packages/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  171. package/packages/pi-ai/dist/utils/overflow.js +12 -0
  172. package/packages/pi-ai/dist/utils/overflow.js.map +1 -1
  173. package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts +2 -0
  174. package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts.map +1 -0
  175. package/packages/pi-ai/dist/utils/tests/overflow.test.js +50 -0
  176. package/packages/pi-ai/dist/utils/tests/overflow.test.js.map +1 -0
  177. package/packages/pi-ai/src/index.ts +4 -0
  178. package/packages/pi-ai/src/utils/overflow.ts +14 -1
  179. package/packages/pi-ai/src/utils/tests/overflow.test.ts +58 -0
  180. package/packages/pi-coding-agent/dist/core/auth-storage.js +1 -1
  181. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  182. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +27 -0
  183. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +313 -8
  185. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  186. package/packages/pi-coding-agent/dist/core/compaction/utils.js +5 -5
  187. package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
  188. package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts +2 -0
  189. package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts.map +1 -0
  190. package/packages/pi-coding-agent/dist/core/compaction-utils.test.js +45 -0
  191. package/packages/pi-coding-agent/dist/core/compaction-utils.test.js.map +1 -0
  192. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  193. package/packages/pi-coding-agent/dist/core/model-resolver.js +25 -68
  194. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  195. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +12 -2
  196. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  197. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +51 -26
  198. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  199. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +1 -0
  200. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  201. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +116 -25
  202. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  203. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts +2 -0
  204. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts.map +1 -0
  205. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +63 -0
  206. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -0
  207. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  208. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +6 -2
  209. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  210. package/packages/pi-coding-agent/package.json +1 -1
  211. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +38 -0
  212. package/packages/pi-coding-agent/src/core/auth-storage.ts +1 -1
  213. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +355 -8
  214. package/packages/pi-coding-agent/src/core/compaction/utils.ts +5 -5
  215. package/packages/pi-coding-agent/src/core/compaction-utils.test.ts +50 -0
  216. package/packages/pi-coding-agent/src/core/model-resolver.ts +26 -70
  217. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +62 -26
  218. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +71 -0
  219. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +136 -30
  220. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -2
  221. package/packages/pi-tui/dist/__tests__/tui.test.js +30 -1
  222. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
  223. package/packages/pi-tui/dist/tui.d.ts +7 -0
  224. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  225. package/packages/pi-tui/dist/tui.js +10 -0
  226. package/packages/pi-tui/dist/tui.js.map +1 -1
  227. package/packages/pi-tui/src/__tests__/tui.test.ts +38 -1
  228. package/packages/pi-tui/src/tui.ts +11 -0
  229. package/pkg/package.json +1 -1
  230. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +12 -4
  231. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +23 -2
  232. package/src/resources/extensions/gsd/auto-dispatch.ts +5 -0
  233. package/src/resources/extensions/gsd/auto-model-selection.ts +85 -11
  234. package/src/resources/extensions/gsd/auto-prompts.ts +9 -3
  235. package/src/resources/extensions/gsd/auto-start.ts +30 -6
  236. package/src/resources/extensions/gsd/auto.ts +10 -0
  237. package/src/resources/extensions/gsd/bootstrap/crash-log.ts +32 -0
  238. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -7
  239. package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -1
  240. package/src/resources/extensions/gsd/commands-handlers.ts +8 -2
  241. package/src/resources/extensions/gsd/crash-recovery.ts +59 -0
  242. package/src/resources/extensions/gsd/gsd-db.ts +52 -2
  243. package/src/resources/extensions/gsd/milestone-actions.ts +19 -1
  244. package/src/resources/extensions/gsd/preferences-models.ts +41 -0
  245. package/src/resources/extensions/gsd/preferences-types.ts +12 -0
  246. package/src/resources/extensions/gsd/preferences-validation.ts +23 -0
  247. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +51 -2
  248. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +235 -0
  249. package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +137 -1
  250. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +59 -1
  251. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +91 -2
  252. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +64 -0
  253. package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -0
  254. package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +267 -0
  255. package/dist/web/standalone/.next/static/chunks/app/page-7115e62689b5fd84.js +0 -1
  256. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  257. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  258. /package/dist/web/standalone/.next/static/{KSZ2dcC3p4z6lOmUpPpzr → WawMb3TMcYLkGJLTg_x1u}/_buildManifest.js +0 -0
  259. /package/dist/web/standalone/.next/static/{KSZ2dcC3p4z6lOmUpPpzr → WawMb3TMcYLkGJLTg_x1u}/_ssgManifest.js +0 -0
@@ -3,45 +3,13 @@
3
3
  */
4
4
 
5
5
  import type { ThinkingLevel } from "@gsd/pi-agent-core";
6
- import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@gsd/pi-ai";
6
+ import { type Api, type Model, modelsAreEqual } from "@gsd/pi-ai";
7
7
  import chalk from "chalk";
8
8
  import { minimatch } from "minimatch";
9
9
  import { isValidThinkingLevel } from "../cli/args.js";
10
10
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
11
11
  import type { ModelRegistry } from "./model-registry.js";
12
12
 
13
- /** Default model IDs for each known provider */
14
- const defaultModelPerProvider: Record<KnownProvider, string> = {
15
- "amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
16
- anthropic: "claude-opus-4-6",
17
- "anthropic-vertex": "claude-sonnet-4-6",
18
- openai: "gpt-5.4",
19
- "azure-openai-responses": "gpt-5.2",
20
- "openai-codex": "gpt-5.4",
21
- google: "gemini-2.5-pro",
22
- "google-gemini-cli": "gemini-2.5-pro",
23
- "google-antigravity": "gemini-3.1-pro-high",
24
- "google-vertex": "gemini-3-pro-preview",
25
- "github-copilot": "gpt-4o",
26
- openrouter: "openai/gpt-5.1-codex",
27
- "vercel-ai-gateway": "anthropic/claude-opus-4-6",
28
- xai: "grok-4-fast-non-reasoning",
29
- groq: "openai/gpt-oss-120b",
30
- cerebras: "zai-glm-4.6",
31
- zai: "glm-4.6",
32
- mistral: "devstral-medium-latest",
33
- minimax: "MiniMax-M2.1",
34
- "minimax-cn": "MiniMax-M2.1",
35
- huggingface: "moonshotai/Kimi-K2.5",
36
- opencode: "claude-opus-4-6",
37
- "opencode-go": "kimi-k2.5",
38
- "kimi-coding": "kimi-k2-thinking",
39
- "alibaba-coding-plan": "qwen3.5-plus",
40
- "alibaba-dashscope": "qwen3.5-plus",
41
- ollama: "llama3.1:8b",
42
- "ollama-cloud": "qwen3:32b",
43
- };
44
-
45
13
  export interface ScopedModel {
46
14
  model: Model<Api>;
47
15
  /** Thinking level if explicitly specified in pattern (e.g., "model:high"), undefined otherwise */
@@ -123,10 +91,11 @@ function buildFallbackModel(provider: string, modelId: string, availableModels:
123
91
  const providerModels = availableModels.filter((m) => m.provider === provider);
124
92
  if (providerModels.length === 0) return undefined;
125
93
 
126
- const defaultId = defaultModelPerProvider[provider as KnownProvider];
127
- const baseModel = defaultId
128
- ? (providerModels.find((m) => m.id === defaultId) ?? providerModels[0])
129
- : providerModels[0];
94
+ // Use the first available model from this provider as a template for
95
+ // capabilities (context window, reasoning support, etc.). The user is
96
+ // explicitly providing a custom model id, so we just need any shape of
97
+ // model from the same provider to inherit from.
98
+ const baseModel = providerModels[0];
130
99
 
131
100
  return {
132
101
  ...baseModel,
@@ -503,33 +472,19 @@ export async function findInitialModel(options: {
503
472
  };
504
473
  }
505
474
 
506
- // 3. Try saved default from settings
507
- if (defaultProvider && defaultModelId) {
508
- // Guard against stale settings defaults: only use the saved provider/model
509
- // if the provider is actually request-ready (auth/OAuth/CLI ready).
510
- if (modelRegistry.isProviderRequestReady(defaultProvider)) {
511
- const found = modelRegistry.find(defaultProvider, defaultModelId);
512
- if (found) {
513
- // Check if the provider's recommended default is a higher-capability variant
514
- // of the saved model (e.g. saved "claude-opus-4-6" vs recommended "claude-opus-4-6-extended").
515
- // If so, prefer the recommended variant to avoid using a smaller context window (#1125).
516
- const recommendedId = defaultModelPerProvider[defaultProvider as KnownProvider];
517
- if (recommendedId && recommendedId !== defaultModelId && recommendedId.startsWith(defaultModelId)) {
518
- const recommended = modelRegistry.find(defaultProvider, recommendedId);
519
- if (recommended) {
520
- model = recommended;
521
- if (defaultThinkingLevel) {
522
- thinkingLevel = defaultThinkingLevel;
523
- }
524
- return { model, thinkingLevel, fallbackMessage: undefined };
525
- }
526
- }
527
- model = found;
528
- if (defaultThinkingLevel) {
529
- thinkingLevel = defaultThinkingLevel;
530
- }
531
- return { model, thinkingLevel, fallbackMessage: undefined };
475
+ // 3. Try saved default from settings — use it exactly as configured.
476
+ // Whatever the user chose is what gets used; no silent substitution.
477
+ // Skip the saved default if its provider is not request-ready (no auth
478
+ // available) so we fall through to an actually-usable model instead of
479
+ // returning a stale selection every selector surface would display.
480
+ if (defaultProvider && defaultModelId && modelRegistry.isProviderRequestReady(defaultProvider)) {
481
+ const found = modelRegistry.find(defaultProvider, defaultModelId);
482
+ if (found) {
483
+ model = found;
484
+ if (defaultThinkingLevel) {
485
+ thinkingLevel = defaultThinkingLevel;
532
486
  }
487
+ return { model, thinkingLevel, fallbackMessage: undefined };
533
488
  }
534
489
  }
535
490
 
@@ -537,16 +492,17 @@ export async function findInitialModel(options: {
537
492
  const availableModels = await modelRegistry.getAvailable();
538
493
 
539
494
  if (availableModels.length > 0) {
540
- // Try to find a default model from known providers
541
- for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {
542
- const defaultId = defaultModelPerProvider[provider];
543
- const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);
544
- if (match) {
545
- return { model: match, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
495
+ // Prefer a model from the user's saved provider if any is still available —
496
+ // provider stickiness, not a hard-coded Anthropic/OpenAI preference.
497
+ if (defaultProvider) {
498
+ const sameProvider = availableModels.find((m) => m.provider === defaultProvider);
499
+ if (sameProvider) {
500
+ return { model: sameProvider, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
546
501
  }
547
502
  }
548
503
 
549
- // If no default found, use first available
504
+ // Otherwise use the first available — registry order reflects models.json
505
+ // order, which the user controls.
550
506
  return { model: availableModels[0], thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };
551
507
  }
552
508
 
@@ -3,8 +3,15 @@ import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-t
3
3
  import { getMarkdownTheme, theme } from "../theme/theme.js";
4
4
  import { formatTimestamp, type TimestampFormat } from "./timestamp.js";
5
5
 
6
+ export interface ContentRange {
7
+ startIndex: number;
8
+ endIndex: number;
9
+ }
10
+
6
11
  /**
7
- * Component that renders a complete assistant message
12
+ * Component that renders a complete assistant message, or a sub-range of its content[].
13
+ * When `range` is provided, only content[startIndex..endIndex] (inclusive) is rendered.
14
+ * Non-text/thinking blocks within the range are silently skipped.
8
15
  */
9
16
  export class AssistantMessageComponent extends Container {
10
17
  private contentContainer: Container;
@@ -12,18 +19,26 @@ export class AssistantMessageComponent extends Container {
12
19
  private markdownTheme: MarkdownTheme;
13
20
  private lastMessage?: AssistantMessage;
14
21
  private timestampFormat: TimestampFormat;
22
+ private range?: ContentRange;
23
+ private showMetadata: boolean;
15
24
 
16
25
  constructor(
17
26
  message?: AssistantMessage,
18
27
  hideThinkingBlock = false,
19
28
  markdownTheme: MarkdownTheme = getMarkdownTheme(),
20
29
  timestampFormat: TimestampFormat = "date-time-iso",
30
+ range?: ContentRange,
21
31
  ) {
22
32
  super();
23
33
 
24
34
  this.hideThinkingBlock = hideThinkingBlock;
25
35
  this.markdownTheme = markdownTheme;
26
36
  this.timestampFormat = timestampFormat;
37
+ this.range = range;
38
+ // No range = legacy full-message rendering; show metadata by default.
39
+ // Ranged (interleaved) instances start with metadata hidden; chat-controller
40
+ // calls setShowMetadata(true) on the last segment at message_end.
41
+ this.showMetadata = !range;
27
42
 
28
43
  // Container for text/thinking content
29
44
  this.contentContainer = new Container();
@@ -34,6 +49,20 @@ export class AssistantMessageComponent extends Container {
34
49
  }
35
50
  }
36
51
 
52
+ setRange(range: ContentRange | undefined): void {
53
+ this.range = range;
54
+ if (this.lastMessage) {
55
+ this.updateContent(this.lastMessage);
56
+ }
57
+ }
58
+
59
+ setShowMetadata(show: boolean): void {
60
+ this.showMetadata = show;
61
+ if (this.lastMessage) {
62
+ this.updateContent(this.lastMessage);
63
+ }
64
+ }
65
+
37
66
  override invalidate(): void {
38
67
  super.invalidate();
39
68
  if (this.lastMessage) {
@@ -51,7 +80,11 @@ export class AssistantMessageComponent extends Container {
51
80
  // Clear content container
52
81
  this.contentContainer.clear();
53
82
 
54
- const hasVisibleContent = message.content.some(
83
+ const start = this.range?.startIndex ?? 0;
84
+ const end = this.range?.endIndex ?? message.content.length - 1;
85
+ const slice = message.content.slice(start, end + 1);
86
+
87
+ const hasVisibleContent = slice.some(
55
88
  (c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
56
89
  );
57
90
 
@@ -59,9 +92,9 @@ export class AssistantMessageComponent extends Container {
59
92
  this.contentContainer.addChild(new Spacer(1));
60
93
  }
61
94
 
62
- // Render content in order
63
- for (let i = 0; i < message.content.length; i++) {
64
- const content = message.content[i];
95
+ // Render content in order; non-text/thinking blocks are silently skipped
96
+ for (let i = 0; i < slice.length; i++) {
97
+ const content = slice[i];
65
98
  if (content.type === "text" && content.text.trim()) {
66
99
  // Assistant text messages with no background - trim the text
67
100
  // Set paddingY=0 to avoid extra spacing before tool executions
@@ -69,7 +102,7 @@ export class AssistantMessageComponent extends Container {
69
102
  } else if (content.type === "thinking" && content.thinking.trim()) {
70
103
  // Add spacing only when another visible assistant content block follows.
71
104
  // This avoids a superfluous blank line before separately-rendered tool execution blocks.
72
- const hasVisibleContentAfter = message.content
105
+ const hasVisibleContentAfter = slice
73
106
  .slice(i + 1)
74
107
  .some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
75
108
 
@@ -94,30 +127,33 @@ export class AssistantMessageComponent extends Container {
94
127
  }
95
128
  }
96
129
 
97
- // Check if aborted - show after partial content
98
- // But only if there are no tool calls (tool execution components will show the error)
99
- const hasToolCalls = message.content.some((c) => c.type === "toolCall");
100
- if (!hasToolCalls) {
101
- if (message.stopReason === "aborted") {
102
- const abortMessage =
103
- message.errorMessage && message.errorMessage !== "Request was aborted"
104
- ? message.errorMessage
105
- : "Operation aborted";
106
- if (hasVisibleContent) {
130
+ // Metadata (errors, timestamp): gated on showMetadata so ranged instances stay clean
131
+ // until chat-controller explicitly enables it on the last segment at message_end.
132
+ if (this.showMetadata) {
133
+ // Check if aborted - show after partial content
134
+ // But only if there are no tool calls (tool execution components will show the error)
135
+ const hasToolCalls = message.content.some((c) => c.type === "toolCall");
136
+ if (!hasToolCalls) {
137
+ if (message.stopReason === "aborted") {
138
+ const abortMessage =
139
+ message.errorMessage && message.errorMessage !== "Request was aborted"
140
+ ? message.errorMessage
141
+ : "Operation aborted";
142
+ if (hasVisibleContent) {
143
+ this.contentContainer.addChild(new Spacer(1));
144
+ }
145
+ this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
146
+ } else if (message.stopReason === "error") {
147
+ const errorMsg = message.errorMessage || "Unknown error";
107
148
  this.contentContainer.addChild(new Spacer(1));
149
+ this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
108
150
  }
109
- this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
110
- } else if (message.stopReason === "error") {
111
- const errorMsg = message.errorMessage || "Unknown error";
112
- this.contentContainer.addChild(new Spacer(1));
113
- this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
114
151
  }
115
- }
116
152
 
117
- // Show timestamp when the message is complete (has a stop reason)
118
- if (message.stopReason && message.timestamp) {
119
- const timeStr = formatTimestamp(message.timestamp, this.timestampFormat);
120
- this.contentContainer.addChild(new Text(theme.fg("dim", timeStr), 1, 0));
153
+ if (message.stopReason && message.timestamp) {
154
+ const timeStr = formatTimestamp(message.timestamp, this.timestampFormat);
155
+ this.contentContainer.addChild(new Text(theme.fg("dim", timeStr), 1, 0));
156
+ }
121
157
  }
122
158
  }
123
159
  }
@@ -0,0 +1,71 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { findLatestPinnableText } from "./chat-controller.js";
5
+
6
+ test("findLatestPinnableText: empty content returns empty string", () => {
7
+ assert.equal(findLatestPinnableText([]), "");
8
+ });
9
+
10
+ test("findLatestPinnableText: no tool calls returns empty string", () => {
11
+ const blocks = [
12
+ { type: "text", text: "hello" },
13
+ { type: "text", text: "world" },
14
+ ];
15
+ assert.equal(findLatestPinnableText(blocks), "");
16
+ });
17
+
18
+ test("findLatestPinnableText: returns text preceding a tool call", () => {
19
+ const blocks = [
20
+ { type: "text", text: "doing the thing" },
21
+ { type: "toolCall", id: "1", name: "Read" },
22
+ ];
23
+ assert.equal(findLatestPinnableText(blocks), "doing the thing");
24
+ });
25
+
26
+ test("findLatestPinnableText: ignores trailing streaming text after the last tool call (regression: pinned mirror duplicated chat-container tokens)", () => {
27
+ const blocks = [
28
+ { type: "text", text: "first prose" },
29
+ { type: "toolCall", id: "1", name: "Read" },
30
+ { type: "text", text: "second prose still streaming" },
31
+ ];
32
+ assert.equal(findLatestPinnableText(blocks), "first prose");
33
+ });
34
+
35
+ test("findLatestPinnableText: with multiple tools, picks text before the most recent tool call", () => {
36
+ const blocks = [
37
+ { type: "text", text: "first" },
38
+ { type: "toolCall", id: "1", name: "Read" },
39
+ { type: "text", text: "second" },
40
+ { type: "toolCall", id: "2", name: "Grep" },
41
+ { type: "text", text: "third streaming" },
42
+ ];
43
+ assert.equal(findLatestPinnableText(blocks), "second");
44
+ });
45
+
46
+ test("findLatestPinnableText: treats serverToolUse the same as toolCall", () => {
47
+ const blocks = [
48
+ { type: "text", text: "before web search" },
49
+ { type: "serverToolUse", id: "ws1", name: "web_search" },
50
+ { type: "text", text: "answer streaming" },
51
+ ];
52
+ assert.equal(findLatestPinnableText(blocks), "before web search");
53
+ });
54
+
55
+ test("findLatestPinnableText: skips empty/whitespace-only text blocks", () => {
56
+ const blocks = [
57
+ { type: "text", text: "real prose" },
58
+ { type: "text", text: " " },
59
+ { type: "text", text: "" },
60
+ { type: "toolCall", id: "1", name: "Read" },
61
+ ];
62
+ assert.equal(findLatestPinnableText(blocks), "real prose");
63
+ });
64
+
65
+ test("findLatestPinnableText: thinking blocks are not pinnable", () => {
66
+ const blocks = [
67
+ { type: "thinking", thinking: "internal" },
68
+ { type: "toolCall", id: "1", name: "Read" },
69
+ ];
70
+ assert.equal(findLatestPinnableText(blocks), "");
71
+ });
@@ -10,6 +10,17 @@ import { appKey } from "../components/keybinding-hints.js";
10
10
  // Tracks the last processed content index to avoid re-scanning all blocks on every message_update
11
11
  let lastProcessedContentIndex = 0;
12
12
 
13
+ // Tracks the previous content[] length so we can detect when an adapter resets
14
+ // the assistant content array for a new provider sub-turn within one lifecycle.
15
+ let lastContentLength = 0;
16
+
17
+ // --- Segment walker state (per streaming assistant turn) ---
18
+ type RenderedSegment =
19
+ | { kind: "text-run"; startIndex: number; endIndex: number; component: AssistantMessageComponent }
20
+ | { kind: "tool"; contentIndex: number; component: ToolExecutionComponent };
21
+
22
+ let renderedSegments: RenderedSegment[] = [];
23
+
13
24
  function hasVisibleAssistantContent(message: { content: Array<any> }): boolean {
14
25
  return message.content.some(
15
26
  (c) =>
@@ -22,6 +33,28 @@ function hasAssistantToolBlocks(message: { content: Array<any> }): boolean {
22
33
  return message.content.some((c) => c.type === "toolCall" || c.type === "serverToolUse");
23
34
  }
24
35
 
36
+ // Pick the latest non-empty text block that appears strictly before the most
37
+ // recent tool call. Text blocks that come after the last tool call are still
38
+ // streaming live into the chat container, so mirroring them into the pinned
39
+ // "Latest Output" zone would render the same tokens twice.
40
+ export function findLatestPinnableText(contentBlocks: Array<any>): string {
41
+ let lastToolIdx = -1;
42
+ for (let i = contentBlocks.length - 1; i >= 0; i--) {
43
+ const c = contentBlocks[i];
44
+ if (c?.type === "toolCall" || c?.type === "serverToolUse") {
45
+ lastToolIdx = i;
46
+ break;
47
+ }
48
+ }
49
+ for (let i = lastToolIdx - 1; i >= 0; i--) {
50
+ const c = contentBlocks[i];
51
+ if (c?.type === "text" && typeof c.text === "string" && c.text.trim()) {
52
+ return c.text.trim();
53
+ }
54
+ }
55
+ return "";
56
+ }
57
+
25
58
  // Tracks the latest assistant text for the pinned message zone
26
59
  let lastPinnedText = "";
27
60
  // Whether any tool execution has been added in this assistant turn (triggers pinned display)
@@ -56,8 +89,10 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
56
89
  // Reset content index tracker and pinned state when a new assistant message starts
57
90
  if (event.type === "message_start" && event.message.role === "assistant") {
58
91
  lastProcessedContentIndex = 0;
92
+ lastContentLength = 0;
59
93
  lastPinnedText = "";
60
94
  hasToolsInTurn = false;
95
+ renderedSegments = [];
61
96
  if (pinnedBorder) pinnedBorder.stopSpinner();
62
97
  pinnedBorder = undefined;
63
98
  pinnedTextComponent = undefined;
@@ -77,6 +112,8 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
77
112
  host.pinnedMessageContainer.clear();
78
113
  lastPinnedText = "";
79
114
  hasToolsInTurn = false;
115
+ renderedSegments = [];
116
+ lastContentLength = 0;
80
117
  if (pinnedBorder) pinnedBorder.stopSpinner();
81
118
  pinnedBorder = undefined;
82
119
  pinnedTextComponent = undefined;
@@ -180,12 +217,22 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
180
217
  }
181
218
 
182
219
  const contentBlocks = host.streamingMessage.content;
183
- // Some adapters reuse a single assistant lifecycle while internally
184
- // spanning multiple provider turns. When a new turn starts, content
185
- // length can shrink back to 0/1; reset scan index to avoid skipping.
186
- if (lastProcessedContentIndex >= contentBlocks.length) {
220
+ // Some adapters (notably claude-code) reuse a single assistant
221
+ // lifecycle while internally spanning multiple provider sub-turns.
222
+ // When a new sub-turn starts, content[] length shrinks back to 0/1.
223
+ // The scan loop needs its index reset, AND the segment walker's
224
+ // renderedSegments map must be cleared so existing text-run
225
+ // components don't get overwritten in place with new sub-turn
226
+ // content (#4144 regression). Prior sub-turn children stay in
227
+ // chatContainer as frozen history; new segments append after them.
228
+ if (contentBlocks.length < lastContentLength) {
229
+ renderedSegments = [];
230
+ lastPinnedText = "";
231
+ lastProcessedContentIndex = 0;
232
+ } else if (lastProcessedContentIndex >= contentBlocks.length) {
187
233
  lastProcessedContentIndex = 0;
188
234
  }
235
+ lastContentLength = contentBlocks.length;
189
236
  for (let i = lastProcessedContentIndex; i < contentBlocks.length; i++) {
190
237
  const content = contentBlocks[i];
191
238
  if (content.type === "toolCall") {
@@ -251,24 +298,88 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
251
298
  }
252
299
  }
253
300
 
254
- // Render assistant text/thinking after tool components so mixed
255
- // streams keep chronological ordering in the chat container.
256
- const hasToolBlocks = hasAssistantToolBlocks(host.streamingMessage);
257
- if (!host.streamingComponent && hasVisibleAssistantContent(host.streamingMessage)) {
258
- host.streamingComponent = new AssistantMessageComponent(
259
- undefined,
260
- host.hideThinkingBlock,
261
- host.getMarkdownThemeWithSettings(),
262
- host.settingsManager.getTimestampFormat(),
263
- );
264
- host.chatContainer.addChild(host.streamingComponent);
265
- }
266
- if (host.streamingComponent) {
267
- if (hasToolBlocks) {
268
- host.chatContainer.removeChild(host.streamingComponent);
269
- host.chatContainer.addChild(host.streamingComponent);
301
+ // Segment walker: render content blocks in stream order, append-only.
302
+ // Build desired segment plan from content[].
303
+ {
304
+ const blocks = host.streamingMessage.content;
305
+ type DesiredSegment =
306
+ | { kind: "text-run"; startIndex: number; endIndex: number }
307
+ | { kind: "tool"; contentIndex: number; toolId: string };
308
+ const desired: DesiredSegment[] = [];
309
+ let runStart = -1;
310
+ for (let i = 0; i < blocks.length; i++) {
311
+ const b = blocks[i];
312
+ const isText = b.type === "text" || b.type === "thinking";
313
+ const isTool = b.type === "toolCall" || b.type === "serverToolUse";
314
+ if (isText) {
315
+ if (runStart === -1) runStart = i;
316
+ } else {
317
+ if (runStart !== -1) {
318
+ desired.push({ kind: "text-run", startIndex: runStart, endIndex: i - 1 });
319
+ runStart = -1;
320
+ }
321
+ if (isTool) {
322
+ desired.push({ kind: "tool", contentIndex: i, toolId: b.id });
323
+ }
324
+ }
325
+ }
326
+ if (runStart !== -1) {
327
+ desired.push({ kind: "text-run", startIndex: runStart, endIndex: blocks.length - 1 });
328
+ }
329
+
330
+ // Append any newly needed segments (never reorder existing ones).
331
+ for (const seg of desired) {
332
+ if (seg.kind === "tool") {
333
+ // Tool segments are already handled above via pendingTools; just
334
+ // register them in renderedSegments if not yet tracked.
335
+ const existing = renderedSegments.find(
336
+ (s) => s.kind === "tool" && s.contentIndex === seg.contentIndex,
337
+ );
338
+ if (!existing) {
339
+ const comp = host.pendingTools.get(seg.toolId);
340
+ if (comp) {
341
+ renderedSegments.push({ kind: "tool", contentIndex: seg.contentIndex, component: comp });
342
+ }
343
+ }
344
+ } else {
345
+ // text-run segment
346
+ const existing = renderedSegments.find(
347
+ (s) => s.kind === "text-run" && s.startIndex === seg.startIndex,
348
+ );
349
+ if (!existing) {
350
+ const comp = new AssistantMessageComponent(
351
+ undefined,
352
+ host.hideThinkingBlock,
353
+ host.getMarkdownThemeWithSettings(),
354
+ host.settingsManager.getTimestampFormat(),
355
+ { startIndex: seg.startIndex, endIndex: seg.endIndex },
356
+ );
357
+ host.chatContainer.addChild(comp);
358
+ renderedSegments.push({ kind: "text-run", startIndex: seg.startIndex, endIndex: seg.endIndex, component: comp });
359
+ host.streamingComponent = comp;
360
+ }
361
+ }
362
+ }
363
+
364
+ // Update all trailing text-run segments with the latest message so
365
+ // streaming text grows in place.
366
+ for (const seg of renderedSegments) {
367
+ if (seg.kind === "text-run") {
368
+ // Find corresponding desired segment to get current endIndex
369
+ const d = desired.find((ds) => ds.kind === "text-run" && ds.startIndex === seg.startIndex);
370
+ if (d && d.kind === "text-run" && d.endIndex !== seg.endIndex) {
371
+ seg.endIndex = d.endIndex;
372
+ seg.component.setRange({ startIndex: seg.startIndex, endIndex: seg.endIndex });
373
+ }
374
+ seg.component.updateContent(host.streamingMessage);
375
+ }
376
+ }
377
+
378
+ // Keep streamingComponent pointing at the last text-run for message_end compatibility.
379
+ const lastTextSeg = [...renderedSegments].reverse().find((s) => s.kind === "text-run");
380
+ if (lastTextSeg && lastTextSeg.kind === "text-run") {
381
+ host.streamingComponent = lastTextSeg.component;
270
382
  }
271
- host.streamingComponent.updateContent(host.streamingMessage);
272
383
  }
273
384
 
274
385
  // Update index: fully processed blocks won't need re-scanning.
@@ -286,15 +397,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
286
397
  if (hasTools) hasToolsInTurn = true;
287
398
 
288
399
  if (hasToolsInTurn) {
289
- // Collect the latest text block(s) from the assistant message
290
- let latestText = "";
291
- for (let i = contentBlocks.length - 1; i >= 0; i--) {
292
- const c = contentBlocks[i] as any;
293
- if (c.type === "text" && c.text?.trim()) {
294
- latestText = c.text.trim();
295
- break;
296
- }
297
- }
400
+ const latestText = findLatestPinnableText(contentBlocks);
298
401
 
299
402
  if (latestText && latestText !== lastPinnedText) {
300
403
  lastPinnedText = latestText;
@@ -362,6 +465,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
362
465
  host.chatContainer.addChild(host.streamingComponent);
363
466
  }
364
467
  if (host.streamingComponent) {
468
+ host.streamingComponent.setShowMetadata(true);
365
469
  host.streamingComponent.updateContent(host.streamingMessage);
366
470
  }
367
471
 
@@ -385,6 +489,8 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
385
489
  }
386
490
  host.streamingComponent = undefined;
387
491
  host.streamingMessage = undefined;
492
+ renderedSegments = [];
493
+ lastContentLength = 0;
388
494
  // Clear pinned output once the message is finalized in the chat
389
495
  // container — prevents duplicate display when the agent continues
390
496
  // (e.g. form elicitation) after the assistant message ends.
@@ -1402,7 +1402,9 @@ export class InteractiveMode {
1402
1402
 
1403
1403
  // widgetContainerAbove: spacer collapses when pinned content is visible
1404
1404
  // so there's no extra blank line between pinned output and the editor border.
1405
- this.widgetContainerAbove.clear();
1405
+ // Use detachChildren() (not clear()) — the extensionWidgetsAbove map owns
1406
+ // disposal; clear() would dispose every mounted widget on every re-render.
1407
+ this.widgetContainerAbove.detachChildren();
1406
1408
  const pinned = this.pinnedMessageContainer;
1407
1409
  this.widgetContainerAbove.addChild({
1408
1410
  render: () => pinned.children.length > 0 ? [] : [""],
@@ -1422,7 +1424,9 @@ export class InteractiveMode {
1422
1424
  spacerWhenEmpty: boolean,
1423
1425
  leadingSpacer: boolean,
1424
1426
  ): void {
1425
- container.clear();
1427
+ // Detach without disposing — the widgets map owns lifecycle; disposing
1428
+ // here would kill refresh timers and subscriptions on every re-render.
1429
+ container.detachChildren();
1426
1430
 
1427
1431
  if (widgets.size === 0) {
1428
1432
  if (spacerWhenEmpty) {
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
- import { TUI } from "../tui.js";
3
+ import { Container, TUI } from "../tui.js";
4
4
  function makeTerminal() {
5
5
  return {
6
6
  isTTY: true,
@@ -40,4 +40,33 @@ describe("TUI", () => {
40
40
  assert.equal(anyTui.inputBuffer, "");
41
41
  });
42
42
  });
43
+ describe("Container", () => {
44
+ function makeDisposableChild(counter) {
45
+ return {
46
+ render: () => [],
47
+ invalidate() { },
48
+ dispose() {
49
+ counter.disposed++;
50
+ },
51
+ };
52
+ }
53
+ it("detachChildren() removes children without disposing them", () => {
54
+ const c = new Container();
55
+ const counter = { disposed: 0 };
56
+ c.addChild(makeDisposableChild(counter));
57
+ c.addChild(makeDisposableChild(counter));
58
+ c.detachChildren();
59
+ assert.equal(c.children.length, 0);
60
+ assert.equal(counter.disposed, 0);
61
+ });
62
+ it("clear() still disposes children (regression guard for detach/dispose split)", () => {
63
+ const c = new Container();
64
+ const counter = { disposed: 0 };
65
+ c.addChild(makeDisposableChild(counter));
66
+ c.addChild(makeDisposableChild(counter));
67
+ c.clear();
68
+ assert.equal(c.children.length, 0);
69
+ assert.equal(counter.disposed, 2);
70
+ });
71
+ });
43
72
  //# sourceMappingURL=tui.test.js.map