aicodeman 1.35.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/README.md +33 -7
  2. package/dist/attachment-registry.d.ts +6 -0
  3. package/dist/attachment-registry.d.ts.map +1 -1
  4. package/dist/attachment-registry.js +9 -3
  5. package/dist/attachment-registry.js.map +1 -1
  6. package/dist/cli-agent.d.ts +225 -0
  7. package/dist/cli-agent.d.ts.map +1 -0
  8. package/dist/cli-agent.js +767 -0
  9. package/dist/cli-agent.js.map +1 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +8 -32
  12. package/dist/cli.js.map +1 -1
  13. package/dist/codeman-credentials.d.ts +27 -0
  14. package/dist/codeman-credentials.d.ts.map +1 -0
  15. package/dist/codeman-credentials.js +66 -0
  16. package/dist/codeman-credentials.js.map +1 -0
  17. package/dist/config/cli-registry/patterns.d.ts +6 -0
  18. package/dist/config/cli-registry/patterns.d.ts.map +1 -1
  19. package/dist/config/cli-registry/patterns.js +13 -0
  20. package/dist/config/cli-registry/patterns.js.map +1 -1
  21. package/dist/config/cli-registry/schema.d.ts +15 -0
  22. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  23. package/dist/config/cli-registry/schema.js +63 -2
  24. package/dist/config/cli-registry/schema.js.map +1 -1
  25. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  26. package/dist/config/cli-registry/stock.js +180 -7
  27. package/dist/config/cli-registry/stock.js.map +1 -1
  28. package/dist/config/cli-registry/types.d.ts +105 -7
  29. package/dist/config/cli-registry/types.d.ts.map +1 -1
  30. package/dist/deepseek-route-config.d.ts +110 -0
  31. package/dist/deepseek-route-config.d.ts.map +1 -0
  32. package/dist/deepseek-route-config.js +477 -0
  33. package/dist/deepseek-route-config.js.map +1 -0
  34. package/dist/git-clone.d.ts.map +1 -1
  35. package/dist/git-clone.js +4 -0
  36. package/dist/git-clone.js.map +1 -1
  37. package/dist/git-workspace-status.d.ts +31 -5
  38. package/dist/git-workspace-status.d.ts.map +1 -1
  39. package/dist/git-workspace-status.js +49 -29
  40. package/dist/git-workspace-status.js.map +1 -1
  41. package/dist/image-watcher.d.ts.map +1 -1
  42. package/dist/image-watcher.js +6 -2
  43. package/dist/image-watcher.js.map +1 -1
  44. package/dist/mcp-sync-targets.d.ts +34 -0
  45. package/dist/mcp-sync-targets.d.ts.map +1 -0
  46. package/dist/mcp-sync-targets.js +55 -0
  47. package/dist/mcp-sync-targets.js.map +1 -0
  48. package/dist/mcp-sync.d.ts +2 -1
  49. package/dist/mcp-sync.d.ts.map +1 -1
  50. package/dist/mcp-sync.js +62 -1
  51. package/dist/mcp-sync.js.map +1 -1
  52. package/dist/model-config-resolvers.d.ts +29 -0
  53. package/dist/model-config-resolvers.d.ts.map +1 -0
  54. package/dist/model-config-resolvers.js +34 -0
  55. package/dist/model-config-resolvers.js.map +1 -0
  56. package/dist/session-display-model.d.ts +110 -0
  57. package/dist/session-display-model.d.ts.map +1 -0
  58. package/dist/session-display-model.js +154 -0
  59. package/dist/session-display-model.js.map +1 -0
  60. package/dist/session.d.ts +145 -8
  61. package/dist/session.d.ts.map +1 -1
  62. package/dist/session.js +290 -39
  63. package/dist/session.js.map +1 -1
  64. package/dist/subagent-watcher.d.ts.map +1 -1
  65. package/dist/subagent-watcher.js +29 -6
  66. package/dist/subagent-watcher.js.map +1 -1
  67. package/dist/templates/case-template.md +2 -0
  68. package/dist/tmux-manager.d.ts +5 -0
  69. package/dist/tmux-manager.d.ts.map +1 -1
  70. package/dist/tmux-manager.js +47 -22
  71. package/dist/tmux-manager.js.map +1 -1
  72. package/dist/tui/tui-client.d.ts +3 -17
  73. package/dist/tui/tui-client.d.ts.map +1 -1
  74. package/dist/tui/tui-client.js +3 -44
  75. package/dist/tui/tui-client.js.map +1 -1
  76. package/dist/types/session.d.ts +28 -1
  77. package/dist/types/session.d.ts.map +1 -1
  78. package/dist/types/session.js.map +1 -1
  79. package/dist/types/tools.d.ts +1 -1
  80. package/dist/types/tools.d.ts.map +1 -1
  81. package/dist/utils/deepseek-cli-resolver.d.ts +8 -0
  82. package/dist/utils/deepseek-cli-resolver.d.ts.map +1 -1
  83. package/dist/utils/deepseek-cli-resolver.js +18 -2
  84. package/dist/utils/deepseek-cli-resolver.js.map +1 -1
  85. package/dist/utils/index.d.ts +1 -0
  86. package/dist/utils/index.d.ts.map +1 -1
  87. package/dist/utils/index.js +1 -0
  88. package/dist/utils/index.js.map +1 -1
  89. package/dist/utils/process-exit-wait.d.ts +40 -0
  90. package/dist/utils/process-exit-wait.d.ts.map +1 -0
  91. package/dist/utils/process-exit-wait.js +68 -0
  92. package/dist/utils/process-exit-wait.js.map +1 -0
  93. package/dist/web/launch-defaults.d.ts +22 -0
  94. package/dist/web/launch-defaults.d.ts.map +1 -0
  95. package/dist/web/launch-defaults.js +46 -0
  96. package/dist/web/launch-defaults.js.map +1 -0
  97. package/dist/web/paste-image-gc.d.ts +46 -6
  98. package/dist/web/paste-image-gc.d.ts.map +1 -1
  99. package/dist/web/paste-image-gc.js +106 -32
  100. package/dist/web/paste-image-gc.js.map +1 -1
  101. package/dist/web/public/admin-ui.js.gz +0 -0
  102. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  103. package/dist/web/public/app.6f8552e1.js +43 -0
  104. package/dist/web/public/app.6f8552e1.js.br +0 -0
  105. package/dist/web/public/app.6f8552e1.js.gz +0 -0
  106. package/dist/web/public/approvals-ui.js.gz +0 -0
  107. package/dist/web/public/{constants.d95fade9.js → constants.bc9c1f63.js} +1276 -127
  108. package/dist/web/public/constants.bc9c1f63.js.br +0 -0
  109. package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
  110. package/dist/web/public/cron-ui.js.gz +0 -0
  111. package/dist/web/public/entrance-animations.js +491 -43
  112. package/dist/web/public/entrance-animations.js.br +0 -0
  113. package/dist/web/public/entrance-animations.js.gz +0 -0
  114. package/dist/web/public/git-status-ui.js +55 -9
  115. package/dist/web/public/git-status-ui.js.br +0 -0
  116. package/dist/web/public/git-status-ui.js.gz +0 -0
  117. package/dist/web/public/home-sessions.js +13 -9
  118. package/dist/web/public/home-sessions.js.br +0 -0
  119. package/dist/web/public/home-sessions.js.gz +0 -0
  120. package/dist/web/public/host-wake-ui.js.gz +0 -0
  121. package/dist/web/public/i18n.a9840ea2.js +2 -0
  122. package/dist/web/public/i18n.a9840ea2.js.br +0 -0
  123. package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
  124. package/dist/web/public/{image-input.a216cf9e.js → image-input.e00471c6.js} +21 -9
  125. package/dist/web/public/image-input.e00471c6.js.br +0 -0
  126. package/dist/web/public/image-input.e00471c6.js.gz +0 -0
  127. package/dist/web/public/index.html +250 -59
  128. package/dist/web/public/index.html.br +0 -0
  129. package/dist/web/public/index.html.gz +0 -0
  130. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  131. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
  132. package/dist/web/public/{mobile-handlers.23a85db1.js → mobile-handlers.0ef9c311.js} +20 -0
  133. package/dist/web/public/mobile-handlers.0ef9c311.js.br +0 -0
  134. package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
  135. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  136. package/dist/web/public/mobile-overview.js.gz +0 -0
  137. package/dist/web/public/mobile.111b187d.css +1 -0
  138. package/dist/web/public/mobile.111b187d.css.br +0 -0
  139. package/dist/web/public/mobile.111b187d.css.gz +0 -0
  140. package/dist/web/public/{notification-manager.50bb87d8.js → notification-manager.feb82c04.js} +19 -2
  141. package/dist/web/public/notification-manager.feb82c04.js.br +0 -0
  142. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  143. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  144. package/dist/web/public/{panels-ui.6a5898d9.js → panels-ui.71846fcb.js} +4 -4
  145. package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
  146. package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
  147. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  148. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  149. package/dist/web/public/readmymind-ui.js.gz +0 -0
  150. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  151. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  152. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  153. package/dist/web/public/session-lineage.js +229 -86
  154. package/dist/web/public/session-lineage.js.br +0 -0
  155. package/dist/web/public/session-lineage.js.gz +0 -0
  156. package/dist/web/public/session-ui.f731602c.js +100 -0
  157. package/dist/web/public/session-ui.f731602c.js.br +0 -0
  158. package/dist/web/public/session-ui.f731602c.js.gz +0 -0
  159. package/dist/web/public/settings-ui.4e33390f.js +97 -0
  160. package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
  161. package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
  162. package/dist/web/public/spreadsheet-preview-worker.js +290 -0
  163. package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
  164. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  165. package/dist/web/public/spreadsheet-preview.af82fa2f.js +555 -0
  166. package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
  167. package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
  168. package/dist/web/public/spreadsheet-xlsx-core.js +1006 -0
  169. package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
  170. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  171. package/dist/web/public/styles.81a72906.css +1 -0
  172. package/dist/web/public/styles.81a72906.css.br +0 -0
  173. package/dist/web/public/styles.81a72906.css.gz +0 -0
  174. package/dist/web/public/{subagent-windows.e6ca799f.js → subagent-windows.07277a58.js} +7 -5
  175. package/dist/web/public/subagent-windows.07277a58.js.br +0 -0
  176. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  177. package/dist/web/public/sw.js +2 -2
  178. package/dist/web/public/sw.js.br +0 -0
  179. package/dist/web/public/sw.js.gz +0 -0
  180. package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
  181. package/dist/web/public/{tab-rail-resize.94cd59f0.js → tab-rail-resize.a5867793.js} +1 -1
  182. package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
  183. package/dist/web/public/{tab-rail-resize.94cd59f0.js.gz → tab-rail-resize.a5867793.js.gz} +0 -0
  184. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js +1 -0
  185. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.br +0 -0
  186. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  187. package/dist/web/public/terminal-split.js +146 -649
  188. package/dist/web/public/terminal-split.js.br +0 -0
  189. package/dist/web/public/terminal-split.js.gz +0 -0
  190. package/dist/web/public/terminal-tile.js +1688 -0
  191. package/dist/web/public/terminal-tile.js.br +0 -0
  192. package/dist/web/public/terminal-tile.js.gz +0 -0
  193. package/dist/web/public/terminal-ui.b7267005.js +2 -0
  194. package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
  195. package/dist/web/public/terminal-ui.b7267005.js.gz +0 -0
  196. package/dist/web/public/tile-grid.js +2895 -0
  197. package/dist/web/public/tile-grid.js.br +0 -0
  198. package/dist/web/public/tile-grid.js.gz +0 -0
  199. package/dist/web/public/ultracode-panel.js.gz +0 -0
  200. package/dist/web/public/ultracode-windows.js +9 -6
  201. package/dist/web/public/ultracode-windows.js.br +0 -0
  202. package/dist/web/public/ultracode-windows.js.gz +0 -0
  203. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  204. package/dist/web/public/vendor/exceljs.min.js +44 -0
  205. package/dist/web/public/vendor/exceljs.min.js.br +0 -0
  206. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  207. package/dist/web/public/vendor/fflate.min.js +1 -0
  208. package/dist/web/public/vendor/fflate.min.js.br +0 -0
  209. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  210. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  211. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  212. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  213. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  214. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  215. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  216. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js +4 -0
  217. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.br +0 -0
  218. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
  219. package/dist/web/public/vendor/xterm.css.gz +0 -0
  220. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  221. package/dist/web/public/{voice-input.c4b51eb6.js → voice-input.2342c37b.js} +64 -10
  222. package/dist/web/public/voice-input.2342c37b.js.br +0 -0
  223. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  224. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  225. package/dist/web/public/webview-tabs.js +9 -1
  226. package/dist/web/public/webview-tabs.js.br +0 -0
  227. package/dist/web/public/webview-tabs.js.gz +0 -0
  228. package/dist/web/routes/cli-registry-routes.d.ts +23 -4
  229. package/dist/web/routes/cli-registry-routes.d.ts.map +1 -1
  230. package/dist/web/routes/cli-registry-routes.js +95 -8
  231. package/dist/web/routes/cli-registry-routes.js.map +1 -1
  232. package/dist/web/routes/file-routes.d.ts +7 -0
  233. package/dist/web/routes/file-routes.d.ts.map +1 -1
  234. package/dist/web/routes/file-routes.js +34 -4
  235. package/dist/web/routes/file-routes.js.map +1 -1
  236. package/dist/web/routes/git-status-routes.d.ts.map +1 -1
  237. package/dist/web/routes/git-status-routes.js +33 -6
  238. package/dist/web/routes/git-status-routes.js.map +1 -1
  239. package/dist/web/routes/mcp-sync-routes.d.ts +7 -2
  240. package/dist/web/routes/mcp-sync-routes.d.ts.map +1 -1
  241. package/dist/web/routes/mcp-sync-routes.js +10 -3
  242. package/dist/web/routes/mcp-sync-routes.js.map +1 -1
  243. package/dist/web/routes/reboot-restore-routes.js +1 -1
  244. package/dist/web/routes/reboot-restore-routes.js.map +1 -1
  245. package/dist/web/routes/session-routes.d.ts +18 -0
  246. package/dist/web/routes/session-routes.d.ts.map +1 -1
  247. package/dist/web/routes/session-routes.js +154 -69
  248. package/dist/web/routes/session-routes.js.map +1 -1
  249. package/dist/web/routes/status-telemetry-routes.d.ts +3 -0
  250. package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
  251. package/dist/web/routes/status-telemetry-routes.js +12 -0
  252. package/dist/web/routes/status-telemetry-routes.js.map +1 -1
  253. package/dist/web/routes/system-routes.d.ts.map +1 -1
  254. package/dist/web/routes/system-routes.js +13 -0
  255. package/dist/web/routes/system-routes.js.map +1 -1
  256. package/dist/web/schemas.d.ts +30 -0
  257. package/dist/web/schemas.d.ts.map +1 -1
  258. package/dist/web/schemas.js +43 -0
  259. package/dist/web/schemas.js.map +1 -1
  260. package/dist/web/server.d.ts +6 -1
  261. package/dist/web/server.d.ts.map +1 -1
  262. package/dist/web/server.js +47 -21
  263. package/dist/web/server.js.map +1 -1
  264. package/dist/web/session-listener-wiring.d.ts +1 -0
  265. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  266. package/dist/web/session-listener-wiring.js +11 -0
  267. package/dist/web/session-listener-wiring.js.map +1 -1
  268. package/package.json +6 -4
  269. package/scripts/postinstall.js +16 -0
  270. package/skills/codeman/SKILL.md +6 -0
  271. package/README.zh-CN.md +0 -1139
  272. package/dist/web/public/app.b9516c7b.js +0 -43
  273. package/dist/web/public/app.b9516c7b.js.br +0 -0
  274. package/dist/web/public/app.b9516c7b.js.gz +0 -0
  275. package/dist/web/public/constants.d95fade9.js.br +0 -0
  276. package/dist/web/public/constants.d95fade9.js.gz +0 -0
  277. package/dist/web/public/i18n.52f5b7cc.js +0 -1
  278. package/dist/web/public/i18n.52f5b7cc.js.br +0 -0
  279. package/dist/web/public/i18n.52f5b7cc.js.gz +0 -0
  280. package/dist/web/public/image-input.a216cf9e.js.br +0 -0
  281. package/dist/web/public/image-input.a216cf9e.js.gz +0 -0
  282. package/dist/web/public/mobile-handlers.23a85db1.js.br +0 -0
  283. package/dist/web/public/mobile-handlers.23a85db1.js.gz +0 -0
  284. package/dist/web/public/mobile.1aa4952d.css +0 -1
  285. package/dist/web/public/mobile.1aa4952d.css.br +0 -0
  286. package/dist/web/public/mobile.1aa4952d.css.gz +0 -0
  287. package/dist/web/public/notification-manager.50bb87d8.js.br +0 -0
  288. package/dist/web/public/notification-manager.50bb87d8.js.gz +0 -0
  289. package/dist/web/public/panels-ui.6a5898d9.js.br +0 -0
  290. package/dist/web/public/panels-ui.6a5898d9.js.gz +0 -0
  291. package/dist/web/public/session-ui.6bddf916.js +0 -89
  292. package/dist/web/public/session-ui.6bddf916.js.br +0 -0
  293. package/dist/web/public/session-ui.6bddf916.js.gz +0 -0
  294. package/dist/web/public/settings-ui.6e798b7b.js +0 -97
  295. package/dist/web/public/settings-ui.6e798b7b.js.br +0 -0
  296. package/dist/web/public/settings-ui.6e798b7b.js.gz +0 -0
  297. package/dist/web/public/styles.2d175c60.css +0 -1
  298. package/dist/web/public/styles.2d175c60.css.br +0 -0
  299. package/dist/web/public/styles.2d175c60.css.gz +0 -0
  300. package/dist/web/public/subagent-windows.e6ca799f.js.br +0 -0
  301. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  302. package/dist/web/public/tab-rail-resize.94cd59f0.js.br +0 -0
  303. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js +0 -1
  304. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.br +0 -0
  305. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
  306. package/dist/web/public/terminal-ui.a4535311.js +0 -2
  307. package/dist/web/public/terminal-ui.a4535311.js.br +0 -0
  308. package/dist/web/public/terminal-ui.a4535311.js.gz +0 -0
  309. package/dist/web/public/upload.html +0 -155
  310. package/dist/web/public/upload.html.br +0 -0
  311. package/dist/web/public/upload.html.gz +0 -0
  312. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js +0 -4
  313. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.br +0 -0
  314. package/dist/web/public/vendor/xterm-zerolag-input.bbb5f84c.js.gz +0 -0
  315. package/dist/web/public/voice-input.c4b51eb6.js.br +0 -0
  316. package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
package/dist/session.js CHANGED
@@ -28,6 +28,7 @@
28
28
  * @module session
29
29
  */
30
30
  import { EventEmitter } from 'node:events';
31
+ import { basename } from 'node:path';
31
32
  import { execSync, execFileSync } from 'node:child_process';
32
33
  import { v4 as uuidv4 } from 'uuid';
33
34
  import * as pty from 'node-pty';
@@ -42,7 +43,7 @@ import { RalphTracker } from './ralph-tracker.js';
42
43
  import { BashToolParser } from './bash-tool-parser.js';
43
44
  import { isTrustDialogScreen, trustDialogNextKey, TRUST_KEY_CONFIRM, TRUST_DIALOG_WINDOW_MS, TRUST_DIALOG_RETRY_MS, TRUST_DIALOG_MAX_ATTEMPTS, TRUST_DIALOG_SCAN_BYTES, } from './session-trust-dialog.js';
44
45
  import { trackActivityStreak, isSustainedActivity, isPaneQuiet, watchingLabel, isAwaitingWorkers, WATCHING_TAIL_LINES, IDLE_RECHECK_MS, PANE_PROBE_MIN_INTERVAL_MS, PANE_PROBE_RECHECK_MS, } from './session-activity.js';
45
- import { BufferAccumulator, ANSI_ESCAPE_PATTERN_FULL, TOKEN_PATTERN, SPINNER_PATTERN, CLAUDE_WORKING_LINE_PATTERN, MAX_SESSION_TOKENS, execPattern, getClaudeCliVersion, getClaudeBinaryPath, spawnPtyWithHelperRepair, resolveLocalShell, } from './utils/index.js';
46
+ import { BufferAccumulator, ANSI_ESCAPE_PATTERN_FULL, TOKEN_PATTERN, SPINNER_PATTERN, CLAUDE_WORKING_LINE_PATTERN, MAX_SESSION_TOKENS, execPattern, getClaudeCliVersion, getClaudeBinaryPath, spawnPtyWithHelperRepair, resolveLocalShell, waitForProcessesExit, } from './utils/index.js';
46
47
  import { MAX_TERMINAL_BUFFER_SIZE, TRIM_TERMINAL_TO as TERMINAL_BUFFER_TRIM_SIZE, MAX_TEXT_OUTPUT_SIZE, TRIM_TEXT_TO as TEXT_OUTPUT_TRIM_SIZE, MAX_MESSAGES, MAX_LINE_BUFFER_SIZE, } from './config/buffer-limits.js';
47
48
  import { DEFAULT_TMUX_HISTORY_LIMIT } from './config/terminal-history.js';
48
49
  import { EXEC_TIMEOUT_MS } from './config/exec-timeout.js';
@@ -58,6 +59,9 @@ import { InteractivePtyExitBreaker } from './session-pty-exit-breaker.js';
58
59
  import { isGeneratedSessionName, SubmittedPromptTracker } from './session-auto-name.js';
59
60
  import { parseTerminalAttachmentRequests } from './attachment-magic.js';
60
61
  import { sanitizeAttachmentHistory, upsertAttachmentHistory as upsertAttachmentHistoryList, } from './session-attachment-history.js';
62
+ import { resolveConfigModel } from './model-config-resolvers.js';
63
+ import { legacyConfigForMode } from './session-cli-registry-bridge.js';
64
+ import { launchModelFor, readScreenModel, resolveDisplayModel, restoredReportedModel, sanitizeModelName, } from './session-display-model.js';
61
65
  /** Line buffer flush interval (100ms) - forces processing of partial lines */
62
66
  const LINE_BUFFER_FLUSH_INTERVAL = 100;
63
67
  // ============================================================================
@@ -75,7 +79,10 @@ const IDLE_DETECTION_DELAY_MS = 2000;
75
79
  // (input, task assignment, respawn) always stamp through it.
76
80
  const WIRE_ACTIVITY_SETTLE_MS = 15_000;
77
81
  // Note: Auto-compact/clear timing constants moved to session-auto-ops.ts
78
- /** Graceful shutdown delay when stopping session (100ms) */
82
+ /**
83
+ * Longest the PTY process gets to exit on SIGTERM before SIGKILL when stopping a
84
+ * session. A deadline, not a sleep: stop() moves on as soon as it has exited.
85
+ */
79
86
  const GRACEFUL_SHUTDOWN_DELAY_MS = 100;
80
87
  // Conversations kept in a pane's chain. A pane that /clears repeatedly would
81
88
  // otherwise grow state.json without bound; 32 covers any real session's history
@@ -163,10 +170,11 @@ function cliExportsTruecolor(mode) {
163
170
  * Codex, Claude Code, and Gemini are known, controlled (Ink/React) TUIs that
164
171
  * repaint via cursor positioning, so dropping the alt-screen switch is safe —
165
172
  * content stays in the normal buffer. Excluded: `shell` (arbitrary programs like
166
- * vim/less/htop legitimately need the alt screen), `opencode` (renders its own
167
- * TUI that may rely on it), `pi` (below) and `grok` (a fullscreen alt-screen TUI
168
- * with mouse support, i.e. the opencode case, not the Ink case). Keep parity
169
- * with the replay-side strip in session-routes.ts.
173
+ * vim/less/htop legitimately need the alt screen), `opencode` (its own MIDDLE strip,
174
+ * isMuxMouseStripMode), `pi` (below) and `grok` (a fullscreen alt-screen TUI with
175
+ * mouse support). Keep parity with the replay-side strip (`stripReplayBuffer` in
176
+ * session-routes.ts); the table in `CliCapabilities.altScreen` is pinned for every
177
+ * stock CLI in test/claude-scrollback-strip.test.ts.
170
178
  *
171
179
  * ⚠️ Being excluded here does NOT preserve the alt screen. Every excluded mode
172
180
  * falls through to isMuxAltScreenOnlyStripMode(), which strips the alt-screen
@@ -185,8 +193,10 @@ export function isAltScreenStripMode(mode) {
185
193
  }
186
194
  /**
187
195
  * Modes that need the NARROW strip: alt-screen toggles only, leaving `\x1b[3J`
188
- * and the mouse-tracking DECSETs alone. Applies to every mode `isAltScreenStripMode`
189
- * excludes, but ONLY when the session is tmux-backed (`useMux`).
196
+ * and the mouse-tracking DECSETs alone. Applies to every mode that is neither
197
+ * `strip-full` (isAltScreenStripMode) nor `strip-mux-and-mouse` (isMuxMouseStripMode),
198
+ * so `strip-mux-only` and `preserve` alike, but ONLY when the session is tmux-backed
199
+ * (`useMux`).
190
200
  *
191
201
  * The bug (issue #205): the tmux CLIENT emits `smcup` (`\x1b[?1049h`) as its first
192
202
  * bytes on attach, before any program has run. Unstripped, xterm.js parks in the
@@ -211,7 +221,31 @@ export function isAltScreenStripMode(mode) {
211
221
  * `\x1b[3J` from a user's own `clear` is a deliberate "wipe my scrollback".
212
222
  */
213
223
  export function isMuxAltScreenOnlyStripMode(mode, useMux) {
214
- return useMux && !isAltScreenStripMode(mode);
224
+ if (!useMux)
225
+ return false;
226
+ const altScreen = getCli(mode)?.capabilities.altScreen;
227
+ return altScreen !== 'strip-full' && altScreen !== 'strip-mux-and-mouse';
228
+ }
229
+ /**
230
+ * Modes whose mouse-tracking DECSETs must be stripped, leaving `3J` alone:
231
+ * `altScreen: 'strip-mux-and-mouse'`, i.e. a mouse-capable full-screen TUI.
232
+ *
233
+ * Why this exists (opencode, measured 2026-09-16): the TUI enables tracking
234
+ * DECSETs, tmux runs with `mouse off` and therefore passes the PANE's DECSETs
235
+ * straight through to the tmux client, and the browser's xterm obeyed them —
236
+ * `mouseTrackingMode` flipped to `'any'` and xterm then reported DRAGS to the TUI
237
+ * instead of selecting locally. "Mark text, copy on select" silently did nothing
238
+ * (measured 62 `none` / 18 `any` over 16s, and 5/5 dead drags while `any`), and
239
+ * the obvious fallback — Ctrl+C — is opencode's `app_exit`, so the failure also
240
+ * ended sessions. Stripping at the source keeps xterm in selection mode; clicks
241
+ * still reach the CLI through the browser's hand-encoded tap, which this strip
242
+ * publishes as `cliMouseTracking` (`_recordStrippedMouseMode`).
243
+ *
244
+ * Gated on `useMux` for the same reason as the narrow strip: on the direct-PTY
245
+ * fallback the program's own DECSETs really do reach xterm and must be honoured.
246
+ */
247
+ export function isMuxMouseStripMode(mode, useMux) {
248
+ return useMux && getCli(mode)?.capabilities.altScreen === 'strip-mux-and-mouse';
215
249
  }
216
250
  // Note: Claude CLI PATH resolution moved to session-cli-builder.ts (buildClaudeEnv)
217
251
  /** PTY fallback geometry when tmux can't be queried (matches pre-#80 hardcoded values). */
@@ -405,6 +439,27 @@ export class Session extends EventEmitter {
405
439
  _watchingWindow = WATCHING_TAIL_LINES;
406
440
  /** Lazily compiled `capabilities.workDetect.awaitingLine`. See _awaitingLinePattern(). */
407
441
  _awaitingLineRe = undefined;
442
+ /**
443
+ * The newest model the running CLI reported for itself (its statusline, or its own
444
+ * footer read off the probe's capture), or null when none has. Feeds `displayModel`
445
+ * (src/session-display-model.ts). Persisted through `toState()` and restored after a
446
+ * restart, so an idle session keeps naming its model until the next report.
447
+ */
448
+ _reportedModel = null;
449
+ /**
450
+ * The model the CLI's own config pins for this session (`modelDetect.configResolver`),
451
+ * read at each pane start, attach or relaunch; null when it pins none. Below any
452
+ * report from the running CLI in `displayModel`. Not persisted: the next start reads it.
453
+ */
454
+ _configModel = null;
455
+ /** Bumped per config read, so a read that lands after a newer one is dropped. */
456
+ _configModelGen = 0;
457
+ /** Lazily compiled `capabilities.modelDetect.screenLine`. See _modelLinePattern(). */
458
+ _modelLineRe = undefined;
459
+ /** Resolved with the pattern above: how many rows at the foot of the screen it sees. */
460
+ _modelLineRows = 1;
461
+ /** Resolved with the pattern above: the fields it shows that are never the model. */
462
+ _modelRejectWords = [];
408
463
  _trustDialogAccepted = false; // Stops the trust-dialog scan (answered, or given up)
409
464
  _trustDialogAttempts = 0; // Keystrokes sent at the trust dialog
410
465
  _lastTrustDialogScanAt = 0; // Throttle for the trust-dialog screen read
@@ -697,6 +752,7 @@ export class Session extends EventEmitter {
697
752
  // replaces it with a first-hand reading. NOT the stats collector, which a
698
753
  // browser panel arms and disarms — see `startPaneExitWatcher`.
699
754
  this.setPaneExit(config.paneExit);
755
+ this._reportedModel = restoredReportedModel(config.displayModel) ?? null;
700
756
  // Never self-parent: a session pointing at itself would draw a zero-length
701
757
  // lineage arc under its own tab. Only reachable via the recovery path, where
702
758
  // both the id and the saved parent come from disk.
@@ -964,6 +1020,9 @@ export class Session extends EventEmitter {
964
1020
  finally {
965
1021
  this._paneLifecycleOps--;
966
1022
  this._paneStartedAt = Date.now();
1023
+ // A start, attach or relaunch is when the CLI read its config, so it is when
1024
+ // the model that config pins is read here too.
1025
+ this._refreshConfigModel();
967
1026
  }
968
1027
  }
969
1028
  /**
@@ -1010,7 +1069,7 @@ export class Session extends EventEmitter {
1010
1069
  /**
1011
1070
  * True when this session's PTY is a tmux client rather than the program itself.
1012
1071
  * Read by the replay-side alt-screen strip, which must apply the same
1013
- * `useMux` gate as the live strip (isMuxAltScreenOnlyStripMode).
1072
+ * `useMux` gate as the live strip (isMuxAltScreenOnlyStripMode, isMuxMouseStripMode).
1014
1073
  */
1015
1074
  get usesMux() {
1016
1075
  return this._useMux;
@@ -1482,6 +1541,7 @@ export class Session extends EventEmitter {
1482
1541
  model: cliTakesSessionModel(this.mode) ? this._model : undefined,
1483
1542
  advisorModel: this._advisorModel,
1484
1543
  customModel: this.customModel,
1544
+ displayModel: this.displayModel,
1485
1545
  // COD-118: runtime-only — surfaced so the frontend can require explicit user
1486
1546
  // intent before restarting a crash-looped session. Deliberately NOT restored
1487
1547
  // by the constructor: a Codeman restart starts with a fresh breaker so boot
@@ -2094,14 +2154,21 @@ export class Session extends EventEmitter {
2094
2154
  // redraws overwrite only the cells they target, so non-erased rows keep
2095
2155
  // their content. Gated to Codex/Claude/Gemini (isAltScreenStripMode).
2096
2156
  //
2097
- // Every OTHER mode (shell/opencode/antigravity) gets the NARROW strip when it
2098
- // is tmux-backed: alt-screen toggles only, because the sequence that breaks
2157
+ // Every OTHER mode (shell/antigravity/pi/grok/deepseek/omp) gets the NARROW strip
2158
+ // when it is tmux-backed: alt-screen toggles only, because the sequence that breaks
2099
2159
  // scrollback there is tmux's own client-side smcup at attach, not anything the
2100
2160
  // program in the pane emitted (issue #205, see isMuxAltScreenOnlyStripMode).
2101
2161
  // 3J and the mouse DECSETs stay, so `clear` and mouse-aware TUIs keep working.
2162
+ //
2163
+ // The MIDDLE case (isMuxMouseStripMode) is a mouse-capable full-screen TUI:
2164
+ // it needs smcup AND the mouse DECSETs gone — otherwise the pane's tracking
2165
+ // reaches xterm and every drag becomes a mouse report instead of a text
2166
+ // selection, which is what killed mark-and-copy in opencode — while 3J stays,
2167
+ // because a TUI is not a `clear` consumer.
2102
2168
  const fullStrip = isAltScreenStripMode(this.mode);
2103
- const altOnlyStrip = !fullStrip && isMuxAltScreenOnlyStripMode(this.mode, this._useMux);
2104
- if (fullStrip || altOnlyStrip) {
2169
+ const mouseStrip = isMuxMouseStripMode(this.mode, this._useMux);
2170
+ const altOnlyStrip = !fullStrip && !mouseStrip && isMuxAltScreenOnlyStripMode(this.mode, this._useMux);
2171
+ if (fullStrip || mouseStrip || altOnlyStrip) {
2105
2172
  // Reassemble sequences split across PTY chunk boundaries first: a chunk
2106
2173
  // ending mid-sequence ('\x1b[?104' now, '9h' next) would slip past the
2107
2174
  // strip below and leave xterm stuck in the scrollback-less alt buffer
@@ -2121,11 +2188,13 @@ export class Session extends EventEmitter {
2121
2188
  // eslint-disable-next-line no-control-regex
2122
2189
  data = data.replace(/\x1b\[\?(?:47|1047|1049)[hl]/g, '');
2123
2190
  if (fullStrip) {
2124
- data = data
2125
- // eslint-disable-next-line no-control-regex
2126
- .replace(/\x1b\[3J/g, '')
2127
- // eslint-disable-next-line no-control-regex
2128
- .replace(/\x1b\[\?(?:1000|1001|1002|1003|1005|1006|1007)[hl]/g, (seq) => {
2191
+ // eslint-disable-next-line no-control-regex
2192
+ data = data.replace(/\x1b\[3J/g, '');
2193
+ }
2194
+ if (fullStrip || mouseStrip) {
2195
+ data = data.replace(
2196
+ // eslint-disable-next-line no-control-regex
2197
+ /\x1b\[\?(?:1000|1001|1002|1003|1005|1006|1007)[hl]/g, (seq) => {
2129
2198
  this._recordStrippedMouseMode(seq);
2130
2199
  return '';
2131
2200
  });
@@ -2337,20 +2406,14 @@ export class Session extends EventEmitter {
2337
2406
  this._codexConfig?.resumeSessionId ||
2338
2407
  this.id;
2339
2408
  // For NEW mux sessions: wait for readiness then clean buffer
2340
- // For RESTORED mux sessions: don't do anything - client will fetch buffer on tab switch
2409
+ // For RESTORED mux sessions: leave the buffer alone - client will fetch it on tab switch
2341
2410
  if (!isRestored) {
2342
2411
  if (isExternalCliMode(this.mode)) {
2343
2412
  // External CLIs use custom TUIs — no ❯ prompt to detect.
2344
2413
  // Wait for TUI to stabilize (output stops changing), then mark ready.
2345
2414
  // Don't clear the buffer — the TUI's initial render IS the useful content.
2346
2415
  // Emit needsRefresh so the client fetches the full buffer once the TUI has rendered.
2347
- this._promptCheckTimeout = setTimeout(() => {
2348
- this._promptCheckTimeout = null;
2349
- if (this._isStopped)
2350
- return;
2351
- this._status = 'idle';
2352
- this.emit('needsRefresh');
2353
- }, 3000);
2416
+ this._armPaneSettle(false);
2354
2417
  }
2355
2418
  else {
2356
2419
  // Claude mode: wait for ❯ prompt
@@ -2384,6 +2447,9 @@ export class Session extends EventEmitter {
2384
2447
  }, 5000);
2385
2448
  }
2386
2449
  }
2450
+ else {
2451
+ this._armPaneSettle(true);
2452
+ }
2387
2453
  }
2388
2454
  catch (err) {
2389
2455
  console.error('[Session] Failed to create mux session, falling back to direct PTY:', err);
@@ -2707,8 +2773,128 @@ export class Session extends EventEmitter {
2707
2773
  this._lastPaneProbeWorking =
2708
2774
  text === null ? null : this._workingLinePattern().test(text) || this._paneAwaitsWorkers(text);
2709
2775
  this._readWatching(text);
2776
+ this._readScreenModel(text);
2710
2777
  return this._lastPaneProbeWorking;
2711
2778
  }
2779
+ /**
2780
+ * Read the model the CLI's own footer names off the same capture, for a CLI whose
2781
+ * registry entry declares `capabilities.modelDetect`.
2782
+ *
2783
+ * Unlike `_readWatching`, a capture that could not be read, or a footer the pattern
2784
+ * does not find (a popup covering it, a footer turned off), KEEPS the last model. The
2785
+ * two are not symmetric: background work ends and its badge must go, while a model does
2786
+ * not stop running because something was drawn over the row that names it.
2787
+ */
2788
+ _readScreenModel(paneText) {
2789
+ if (paneText === null)
2790
+ return;
2791
+ const pattern = this._modelLinePattern();
2792
+ if (!pattern)
2793
+ return;
2794
+ const model = readScreenModel(paneText, pattern, this._modelLineRows, {
2795
+ rejectWords: this._modelRejectWords,
2796
+ // A footer field equal to the folder this session runs in is the folder, never the
2797
+ // model: the generic half of the rule, for every CLI.
2798
+ cwdBasename: basename(this.workingDir),
2799
+ });
2800
+ if (model)
2801
+ this.noteReportedModel('screen', model);
2802
+ }
2803
+ /**
2804
+ * The regex reading this CLI's model off its footer, or null for a CLI that declares
2805
+ * none. Compiled once per session through `compileVersionRegex()` (null, never a throw,
2806
+ * for a pattern it refuses), like the working- and watching-line patterns.
2807
+ */
2808
+ _modelLinePattern() {
2809
+ if (this._modelLineRe === undefined) {
2810
+ const detect = getCli(this.mode)?.capabilities.modelDetect;
2811
+ this._modelLineRe = detect?.screenLine ? compileVersionRegex(detect.screenLine) : null;
2812
+ this._modelLineRows = detect?.screenLines ?? 1;
2813
+ this._modelRejectWords = detect?.rejectWords ?? [];
2814
+ }
2815
+ return this._modelLineRe;
2816
+ }
2817
+ /**
2818
+ * Record a model the running CLI reported for itself: its statusline (claude's
2819
+ * exporter, via `POST /api/status-telemetry`) or its own footer. The newest report
2820
+ * wins whatever its source. An empty or unprintable report changes nothing.
2821
+ *
2822
+ * @returns true when the reported model changed (and `displayModelChanged` was emitted)
2823
+ */
2824
+ noteReportedModel(source, raw) {
2825
+ const model = sanitizeModelName(raw);
2826
+ if (!model)
2827
+ return false;
2828
+ if (this._reportedModel?.model === model && this._reportedModel.source === source)
2829
+ return false;
2830
+ this._reportedModel = { model, source };
2831
+ // The status does not change with it, so it needs a broadcast (and a persist) of its own.
2832
+ this.emit('displayModelChanged');
2833
+ return true;
2834
+ }
2835
+ /**
2836
+ * The model this session runs as far as the server knows, and where that came from:
2837
+ * the custom endpoint's model, else the newest report from the CLI, else the launch
2838
+ * model (src/session-display-model.ts). Undefined when none is known.
2839
+ */
2840
+ get displayModel() {
2841
+ return resolveDisplayModel({
2842
+ customModelId: this._customModel?.modelId,
2843
+ reported: this._reportedModel,
2844
+ configModel: this._configModel,
2845
+ launchModel: launchModelFor(this.mode, this._launchOptionBag()),
2846
+ });
2847
+ }
2848
+ /**
2849
+ * The same option bag the spawn reads its launch params from: `model` at the top for
2850
+ * claude (the `--model` or app-wide default it was created with; inert for every other
2851
+ * CLI, which is why it is not handed over for them), each other CLI's own
2852
+ * `<Mode>Config`. Where a param lives is registry data (`legacyConfigForMode`).
2853
+ */
2854
+ _launchOptionBag() {
2855
+ return {
2856
+ model: cliTakesSessionModel(this.mode) ? this._model : undefined,
2857
+ openCodeConfig: this._openCodeConfig,
2858
+ codexConfig: this._codexConfig,
2859
+ geminiConfig: this._geminiConfig,
2860
+ antigravityConfig: this._antigravityConfig,
2861
+ piConfig: this._piConfig,
2862
+ grokConfig: this._grokConfig,
2863
+ deepSeekConfig: this._deepSeekConfig,
2864
+ ompConfig: this._ompConfig,
2865
+ };
2866
+ }
2867
+ /**
2868
+ * Read the model this session's CLI config pins, with the reader its registry entry
2869
+ * names (`capabilities.modelDetect.configResolver`), and announce a change. Async and
2870
+ * bounded (the reader probes before it reads); a read that lands after a newer one,
2871
+ * or after the session stopped, is dropped. A remote or docker session's CLI reads its
2872
+ * config on another machine or in its container, so nothing local is read for it.
2873
+ */
2874
+ _refreshConfigModel() {
2875
+ const name = getCli(this.mode)?.capabilities.modelDetect?.configResolver;
2876
+ if (!name || this._remote || this._docker)
2877
+ return;
2878
+ const gen = ++this._configModelGen;
2879
+ const overrides = this._envOverrides;
2880
+ resolveConfigModel(name, {
2881
+ config: legacyConfigForMode(this.mode, this._launchOptionBag()),
2882
+ // The session's own env first (already clamped for a non-granted owner), then the
2883
+ // server's: what the pane's CLI inherits.
2884
+ env: (key) => overrides?.[key] ?? process.env[key],
2885
+ }).then((model) => {
2886
+ if (gen !== this._configModelGen || this._isStopped)
2887
+ return;
2888
+ // Sanitized where it is published (resolveDisplayModel), like every source.
2889
+ const next = model || null;
2890
+ if (next === this._configModel)
2891
+ return;
2892
+ this._configModel = next;
2893
+ this.emit('displayModelChanged');
2894
+ }, () => {
2895
+ /* A reader answers null on doubt and never throws; a throw changes nothing. */
2896
+ });
2897
+ }
2712
2898
  /**
2713
2899
  * Read the background-work chip off the same capture the working probe just took.
2714
2900
  *
@@ -2843,15 +3029,77 @@ export class Session extends EventEmitter {
2843
3029
  // 1. Claude was working and is now at prompt (normal case)
2844
3030
  // 2. Session just started and is ready (status is 'busy' but _isWorking is false)
2845
3031
  const wasWorking = this._isWorking;
2846
- const isInitialReady = this._status === 'busy' && !this._isWorking;
2847
- if (wasWorking || isInitialReady) {
2848
- this._isWorking = false;
2849
- this._status = 'idle';
2850
- this._lastPromptTime = Date.now();
2851
- if (wasWorking)
2852
- this._maybeCaptureOmpSessionId();
2853
- this.emit('idle');
2854
- }
3032
+ if (wasWorking || this._status === 'busy')
3033
+ this._concludeIdle(wasWorking);
3034
+ }
3035
+ /**
3036
+ * The one place a pane is concluded idle: status, working flag and prompt stamp
3037
+ * change together, and the change is ANNOUNCED. The `idle` event is what the web
3038
+ * server turns into `session:idle` plus a state broadcast, so a path that flips
3039
+ * `_status` without it leaves every browser on the `busy` it was last sent. A fresh
3040
+ * codex pane used to stay "working" in the UI for its whole life that way.
3041
+ *
3042
+ * @param turnEnded a real turn just finished (not a pane becoming ready at launch)
3043
+ */
3044
+ _concludeIdle(turnEnded) {
3045
+ this._isWorking = false;
3046
+ this._status = 'idle';
3047
+ this._lastPromptTime = Date.now();
3048
+ // Only a finished turn proves omp has written its session file; a pane that is
3049
+ // merely ready has nothing to resolve yet and could claim a neighbour's file.
3050
+ if (turnEnded)
3051
+ this._maybeCaptureOmpSessionId();
3052
+ this.emit('idle');
3053
+ }
3054
+ /**
3055
+ * Arm the launch settle (`_settlePaneStartup`) for a pane `startInteractive()` just
3056
+ * started or re-attached, when one applies:
3057
+ * - a NEW pane of an external CLI, whose TUI has no ❯ for the Claude wait to find;
3058
+ * - a RESTORED pane (Codeman restart, auto-reattach, tile Attach) of a CLI that
3059
+ * declares no `capabilities.workDetect`. It is `busy` from `_resetBuffers()` like a
3060
+ * new pane, and with no composer glyph to arm `_confirmIdle()` nothing else would
3061
+ * ever settle it. A restored claude or codex pane is left to its glyph, which
3062
+ * reads the screen first, so a restart in mid-turn is never called idle.
3063
+ */
3064
+ _armPaneSettle(isRestored) {
3065
+ const applies = isRestored ? !getCli(this.mode)?.capabilities.workDetect : isExternalCliMode(this.mode);
3066
+ if (!applies)
3067
+ return;
3068
+ const armedAt = Date.now();
3069
+ this._promptCheckTimeout = setTimeout(() => this._settlePaneStartup(!isRestored, armedAt), 3000);
3070
+ }
3071
+ /**
3072
+ * The launch settle: 3 s after a NEW external-CLI pane spawned, or after ANY pane of a
3073
+ * CLI without work detection was re-attached, its TUI is taken to have rendered. A pane
3074
+ * still in its spawn-time `busy` is concluded idle (announced, see `_concludeIdle`),
3075
+ * then, for a new pane, the browser is told to refetch the rendered screen.
3076
+ *
3077
+ * ⚠️ This used to set `_status = 'idle'` without an event. When the launch paint
3078
+ * never tripped `_markWorking()`, the later `_confirmIdle()` found the status
3079
+ * already idle and emitted nothing, so no browser ever learned the pane was ready.
3080
+ *
3081
+ * A pane is left to `_confirmIdle()` only when its CLI declares
3082
+ * `capabilities.workDetect` (its composer glyph arms that confirmation, which reads
3083
+ * the screen first) AND it is already working, or a prompt was submitted since the
3084
+ * timer was armed: a turn started 2.9 s in is not marked working before the deferred
3085
+ * parsers run, and an idle edge here would end a send-and-wait registered for it.
3086
+ * For every other CLI this timer is the only thing that ever settles a fresh pane,
3087
+ * so it settles it even if a stray spinner glyph in the launch paint latched
3088
+ * `_isWorking`.
3089
+ *
3090
+ * @param refreshScreen emit `needsRefresh` (a new pane; an attach refetches by itself)
3091
+ * @param armedAt when the timer was armed (a submit at or after it means a prompt)
3092
+ */
3093
+ _settlePaneStartup(refreshScreen, armedAt) {
3094
+ this._promptCheckTimeout = null;
3095
+ if (this._isStopped)
3096
+ return;
3097
+ const busyWithTurn = this._isWorking || this.lastSubmitAt >= armedAt;
3098
+ const leaveToConfirm = busyWithTurn && !!getCli(this.mode)?.capabilities.workDetect;
3099
+ if (this._status === 'busy' && !leaveToConfirm)
3100
+ this._concludeIdle(false);
3101
+ if (refreshScreen)
3102
+ this.emit('needsRefresh');
2855
3103
  }
2856
3104
  /**
2857
3105
  * A brand-new omp session (never yet respawned, so
@@ -3736,7 +3984,7 @@ export class Session extends EventEmitter {
3736
3984
  : this._mux.capturePaneText?.(this._muxSession.muxName),
3737
3985
  sendEnter: () => this._mux?.sendInput(this.id, '\r'),
3738
3986
  // ⚠ NO fallback glyph here, unlike the screen-reading probe elsewhere in this file.
3739
- // Only claude and codex declare a promptGlyph; the other eight modes would fall back
3987
+ // Only claude, codex, pi, opencode, omp and gemini declare a promptGlyph; the other modes would fall back
3740
3988
  // to claude's `❯`, which is ALSO starship's default shell prompt (and pure's, and
3741
3989
  // spaceship's, and p10k lean's). On a shell session the line `❯ npm run build` sits
3742
3990
  // on screen for as long as the command runs, promptStillInComposer() reads that as
@@ -3990,8 +4238,11 @@ export class Session extends EventEmitter {
3990
4238
  catch (err) {
3991
4239
  console.warn('[Session] Failed to send SIGTERM to PTY process (may already be dead):', err);
3992
4240
  }
3993
- // Give it a moment to terminate gracefully
3994
- await new Promise((resolve) => setTimeout(resolve, GRACEFUL_SHUTDOWN_DELAY_MS));
4241
+ // Give it a moment to terminate gracefully. For a tmux-backed session this
4242
+ // is the attach client, gone within a few ms of SIGTERM, and this used to be
4243
+ // a fixed 100ms sleep on every close.
4244
+ if (pid)
4245
+ await waitForProcessesExit([pid], { timeoutMs: GRACEFUL_SHUTDOWN_DELAY_MS });
3995
4246
  // Force kill with SIGKILL if still alive
3996
4247
  try {
3997
4248
  if (pid) {