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