aicodeman 1.47.0 → 1.49.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 (361) hide show
  1. package/README.md +15 -3
  2. package/dist/cli-notify.d.ts +34 -0
  3. package/dist/cli-notify.d.ts.map +1 -0
  4. package/dist/cli-notify.js +100 -0
  5. package/dist/cli-notify.js.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +16 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config/cli-registry/patterns.d.ts +1 -1
  10. package/dist/config/cli-registry/patterns.d.ts.map +1 -1
  11. package/dist/config/cli-registry/patterns.js +4 -0
  12. package/dist/config/cli-registry/patterns.js.map +1 -1
  13. package/dist/config/cli-registry/schema.d.ts +7 -0
  14. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  15. package/dist/config/cli-registry/schema.js +67 -3
  16. package/dist/config/cli-registry/schema.js.map +1 -1
  17. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  18. package/dist/config/cli-registry/stock.js +114 -31
  19. package/dist/config/cli-registry/stock.js.map +1 -1
  20. package/dist/config/cli-registry/types.d.ts +51 -21
  21. package/dist/config/cli-registry/types.d.ts.map +1 -1
  22. package/dist/config/foreign-tmux.d.ts +48 -0
  23. package/dist/config/foreign-tmux.d.ts.map +1 -0
  24. package/dist/config/foreign-tmux.js +48 -0
  25. package/dist/config/foreign-tmux.js.map +1 -0
  26. package/dist/config/path-probe.d.ts +6 -0
  27. package/dist/config/path-probe.d.ts.map +1 -1
  28. package/dist/config/path-probe.js +6 -0
  29. package/dist/config/path-probe.js.map +1 -1
  30. package/dist/config-backup.d.ts +1 -1
  31. package/dist/config-backup.d.ts.map +1 -1
  32. package/dist/config-backup.js +2 -0
  33. package/dist/config-backup.js.map +1 -1
  34. package/dist/custom-model-hosts.d.ts +32 -8
  35. package/dist/custom-model-hosts.d.ts.map +1 -1
  36. package/dist/custom-model-hosts.js +10 -0
  37. package/dist/custom-model-hosts.js.map +1 -1
  38. package/dist/custom-model-injection-apply.d.ts +21 -5
  39. package/dist/custom-model-injection-apply.d.ts.map +1 -1
  40. package/dist/custom-model-injection-apply.js +121 -27
  41. package/dist/custom-model-injection-apply.js.map +1 -1
  42. package/dist/custom-model-injection.d.ts +6 -0
  43. package/dist/custom-model-injection.d.ts.map +1 -1
  44. package/dist/custom-model-injection.js +92 -10
  45. package/dist/custom-model-injection.js.map +1 -1
  46. package/dist/discord-bridge.d.ts +338 -0
  47. package/dist/discord-bridge.d.ts.map +1 -0
  48. package/dist/discord-bridge.js +1256 -0
  49. package/dist/discord-bridge.js.map +1 -0
  50. package/dist/docker-hosts.d.ts +8 -1
  51. package/dist/docker-hosts.d.ts.map +1 -1
  52. package/dist/docker-hosts.js +5 -5
  53. package/dist/docker-hosts.js.map +1 -1
  54. package/dist/edit-history-local-source.d.ts +83 -0
  55. package/dist/edit-history-local-source.d.ts.map +1 -0
  56. package/dist/edit-history-local-source.js +198 -0
  57. package/dist/edit-history-local-source.js.map +1 -0
  58. package/dist/edit-history-parse.d.ts +114 -0
  59. package/dist/edit-history-parse.d.ts.map +1 -0
  60. package/dist/edit-history-parse.js +312 -0
  61. package/dist/edit-history-parse.js.map +1 -0
  62. package/dist/edit-history-store.d.ts +209 -0
  63. package/dist/edit-history-store.d.ts.map +1 -0
  64. package/dist/edit-history-store.js +599 -0
  65. package/dist/edit-history-store.js.map +1 -0
  66. package/dist/foreign-tmux-discovery.d.ts +140 -0
  67. package/dist/foreign-tmux-discovery.d.ts.map +1 -0
  68. package/dist/foreign-tmux-discovery.js +366 -0
  69. package/dist/foreign-tmux-discovery.js.map +1 -0
  70. package/dist/foreign-tmux.d.ts +381 -0
  71. package/dist/foreign-tmux.d.ts.map +1 -0
  72. package/dist/foreign-tmux.js +775 -0
  73. package/dist/foreign-tmux.js.map +1 -0
  74. package/dist/git-clone.d.ts +6 -1
  75. package/dist/git-clone.d.ts.map +1 -1
  76. package/dist/git-clone.js +79 -14
  77. package/dist/git-clone.js.map +1 -1
  78. package/dist/git-fetch-scheduler.d.ts +2 -0
  79. package/dist/git-fetch-scheduler.d.ts.map +1 -1
  80. package/dist/git-fetch-scheduler.js +1 -1
  81. package/dist/git-fetch-scheduler.js.map +1 -1
  82. package/dist/git-fetch.d.ts +1 -1
  83. package/dist/git-fetch.d.ts.map +1 -1
  84. package/dist/git-workspace-status.d.ts +2 -2
  85. package/dist/git-workspace-status.d.ts.map +1 -1
  86. package/dist/hooks-config.d.ts +2 -1
  87. package/dist/hooks-config.d.ts.map +1 -1
  88. package/dist/hooks-config.js +2 -1
  89. package/dist/hooks-config.js.map +1 -1
  90. package/dist/image-watcher.js +4 -4
  91. package/dist/image-watcher.js.map +1 -1
  92. package/dist/live-worktree.d.ts +53 -0
  93. package/dist/live-worktree.d.ts.map +1 -0
  94. package/dist/live-worktree.js +97 -0
  95. package/dist/live-worktree.js.map +1 -0
  96. package/dist/mux-interface.d.ts +22 -2
  97. package/dist/mux-interface.d.ts.map +1 -1
  98. package/dist/reboot-restore.d.ts +1 -1
  99. package/dist/reboot-restore.d.ts.map +1 -1
  100. package/dist/reboot-restore.js +9 -0
  101. package/dist/reboot-restore.js.map +1 -1
  102. package/dist/remote-files.d.ts +150 -5
  103. package/dist/remote-files.d.ts.map +1 -1
  104. package/dist/remote-files.js +505 -7
  105. package/dist/remote-files.js.map +1 -1
  106. package/dist/remote-hosts.d.ts +15 -9
  107. package/dist/remote-hosts.d.ts.map +1 -1
  108. package/dist/remote-hosts.js +42 -20
  109. package/dist/remote-hosts.js.map +1 -1
  110. package/dist/session-cli-registry-bridge.d.ts.map +1 -1
  111. package/dist/session-cli-registry-bridge.js +23 -8
  112. package/dist/session-cli-registry-bridge.js.map +1 -1
  113. package/dist/session-env-clamp.d.ts +1 -1
  114. package/dist/session-env-clamp.d.ts.map +1 -1
  115. package/dist/session-env-clamp.js +4 -3
  116. package/dist/session-env-clamp.js.map +1 -1
  117. package/dist/session.d.ts +161 -24
  118. package/dist/session.d.ts.map +1 -1
  119. package/dist/session.js +337 -37
  120. package/dist/session.js.map +1 -1
  121. package/dist/tmux-manager.d.ts +114 -12
  122. package/dist/tmux-manager.d.ts.map +1 -1
  123. package/dist/tmux-manager.js +318 -41
  124. package/dist/tmux-manager.js.map +1 -1
  125. package/dist/types/discord.d.ts +55 -0
  126. package/dist/types/discord.d.ts.map +1 -0
  127. package/dist/types/discord.js +18 -0
  128. package/dist/types/discord.js.map +1 -0
  129. package/dist/types/foreign-tmux.d.ts +101 -0
  130. package/dist/types/foreign-tmux.d.ts.map +1 -0
  131. package/dist/types/foreign-tmux.js +27 -0
  132. package/dist/types/foreign-tmux.js.map +1 -0
  133. package/dist/types/index.d.ts +3 -0
  134. package/dist/types/index.d.ts.map +1 -1
  135. package/dist/types/index.js +3 -0
  136. package/dist/types/index.js.map +1 -1
  137. package/dist/types/session.d.ts +119 -2
  138. package/dist/types/session.d.ts.map +1 -1
  139. package/dist/types/session.js.map +1 -1
  140. package/dist/types/update.d.ts +4 -1
  141. package/dist/types/update.d.ts.map +1 -1
  142. package/dist/web/case-path.d.ts +2 -0
  143. package/dist/web/case-path.d.ts.map +1 -1
  144. package/dist/web/case-path.js +3 -3
  145. package/dist/web/case-path.js.map +1 -1
  146. package/dist/web/edit-history-sync.d.ts +178 -0
  147. package/dist/web/edit-history-sync.d.ts.map +1 -0
  148. package/dist/web/edit-history-sync.js +461 -0
  149. package/dist/web/edit-history-sync.js.map +1 -0
  150. package/dist/web/launch-defaults.d.ts +1 -1
  151. package/dist/web/launch-defaults.js +2 -2
  152. package/dist/web/launch-defaults.js.map +1 -1
  153. package/dist/web/linked-cases-writer.d.ts +43 -0
  154. package/dist/web/linked-cases-writer.d.ts.map +1 -0
  155. package/dist/web/linked-cases-writer.js +107 -0
  156. package/dist/web/linked-cases-writer.js.map +1 -0
  157. package/dist/web/paste-image-gc.d.ts +10 -1
  158. package/dist/web/paste-image-gc.d.ts.map +1 -1
  159. package/dist/web/paste-image-gc.js +10 -2
  160. package/dist/web/paste-image-gc.js.map +1 -1
  161. package/dist/web/ports/session-port.d.ts +13 -0
  162. package/dist/web/ports/session-port.d.ts.map +1 -1
  163. package/dist/web/public/admin-ui.js.gz +0 -0
  164. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  165. package/dist/web/public/{app.fe8a6b4c.js → app.9ea37070.js} +6 -6
  166. package/dist/web/public/app.9ea37070.js.br +0 -0
  167. package/dist/web/public/app.9ea37070.js.gz +0 -0
  168. package/dist/web/public/approvals-ui.js.gz +0 -0
  169. package/dist/web/public/{constants.affcdde6.js → constants.782170e1.js} +114 -25
  170. package/dist/web/public/constants.782170e1.js.br +0 -0
  171. package/dist/web/public/constants.782170e1.js.gz +0 -0
  172. package/dist/web/public/cron-ui.js.gz +0 -0
  173. package/dist/web/public/edit-history-ui.js +643 -0
  174. package/dist/web/public/edit-history-ui.js.br +0 -0
  175. package/dist/web/public/edit-history-ui.js.gz +0 -0
  176. package/dist/web/public/entrance-animations.js.gz +0 -0
  177. package/dist/web/public/foreign-sessions.js +476 -0
  178. package/dist/web/public/foreign-sessions.js.br +0 -0
  179. package/dist/web/public/foreign-sessions.js.gz +0 -0
  180. package/dist/web/public/git-status-ui.js +5 -3
  181. package/dist/web/public/git-status-ui.js.br +0 -0
  182. package/dist/web/public/git-status-ui.js.gz +0 -0
  183. package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
  184. package/dist/web/public/home-sessions.js.gz +0 -0
  185. package/dist/web/public/host-wake-ui.js.gz +0 -0
  186. package/dist/web/public/i18n.a5e82c95.js +2 -0
  187. package/dist/web/public/i18n.a5e82c95.js.br +0 -0
  188. package/dist/web/public/i18n.a5e82c95.js.gz +0 -0
  189. package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
  190. package/dist/web/public/index.html +183 -18
  191. package/dist/web/public/index.html.br +0 -0
  192. package/dist/web/public/index.html.gz +0 -0
  193. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  194. package/dist/web/public/{keyboard-accessory.ff64fafc.js → keyboard-accessory.b941676c.js} +75 -2
  195. package/dist/web/public/keyboard-accessory.b941676c.js.br +0 -0
  196. package/dist/web/public/keyboard-accessory.b941676c.js.gz +0 -0
  197. package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
  198. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  199. package/dist/web/public/mobile-overview.js +10 -0
  200. package/dist/web/public/mobile-overview.js.br +0 -0
  201. package/dist/web/public/mobile-overview.js.gz +0 -0
  202. package/dist/web/public/mobile.67c8eb5b.css +1 -0
  203. package/dist/web/public/mobile.67c8eb5b.css.br +0 -0
  204. package/dist/web/public/mobile.67c8eb5b.css.gz +0 -0
  205. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  206. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  207. package/dist/web/public/{panels-ui.4a56ee63.js → panels-ui.8bcd4473.js} +5 -5
  208. package/dist/web/public/panels-ui.8bcd4473.js.br +0 -0
  209. package/dist/web/public/panels-ui.8bcd4473.js.gz +0 -0
  210. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  211. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  212. package/dist/web/public/readmymind-ui.js.gz +0 -0
  213. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  214. package/dist/web/public/{respawn-ui.d7adcf6a.js → respawn-ui.304ba0d4.js} +1 -1
  215. package/dist/web/public/respawn-ui.304ba0d4.js.br +0 -0
  216. package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
  217. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  218. package/dist/web/public/security-notices.json +1 -21
  219. package/dist/web/public/session-lineage.js.gz +0 -0
  220. package/dist/web/public/{session-ui.426fb671.js → session-ui.a7c8c095.js} +30 -30
  221. package/dist/web/public/session-ui.a7c8c095.js.br +0 -0
  222. package/dist/web/public/session-ui.a7c8c095.js.gz +0 -0
  223. package/dist/web/public/settings-ui.18f92052.js +106 -0
  224. package/dist/web/public/settings-ui.18f92052.js.br +0 -0
  225. package/dist/web/public/settings-ui.18f92052.js.gz +0 -0
  226. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  227. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  228. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  229. package/dist/web/public/styles.6a718331.css +1 -0
  230. package/dist/web/public/styles.6a718331.css.br +0 -0
  231. package/dist/web/public/styles.6a718331.css.gz +0 -0
  232. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  233. package/dist/web/public/sw.js +2 -2
  234. package/dist/web/public/sw.js.br +0 -0
  235. package/dist/web/public/sw.js.gz +0 -0
  236. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  237. package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
  238. package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
  239. package/dist/web/public/terminal-split.js.gz +0 -0
  240. package/dist/web/public/terminal-tile.js +2 -2
  241. package/dist/web/public/terminal-tile.js.br +0 -0
  242. package/dist/web/public/terminal-tile.js.gz +0 -0
  243. package/dist/web/public/terminal-ui.a7883640.js +4 -0
  244. package/dist/web/public/terminal-ui.a7883640.js.br +0 -0
  245. package/dist/web/public/terminal-ui.a7883640.js.gz +0 -0
  246. package/dist/web/public/tile-grid.js.gz +0 -0
  247. package/dist/web/public/ultracode-panel.js.gz +0 -0
  248. package/dist/web/public/ultracode-windows.js.gz +0 -0
  249. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  250. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  251. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  252. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  253. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  254. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  255. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  256. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  257. package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
  258. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
  259. package/dist/web/public/vendor/xterm.css.gz +0 -0
  260. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  261. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  262. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  263. package/dist/web/public/webview-tabs.js.gz +0 -0
  264. package/dist/web/public/whats-new.json +9 -8
  265. package/dist/web/routes/case-routes.d.ts.map +1 -1
  266. package/dist/web/routes/case-routes.js +165 -50
  267. package/dist/web/routes/case-routes.js.map +1 -1
  268. package/dist/web/routes/config-backup-routes.d.ts +5 -1
  269. package/dist/web/routes/config-backup-routes.d.ts.map +1 -1
  270. package/dist/web/routes/config-backup-routes.js +4 -1
  271. package/dist/web/routes/config-backup-routes.js.map +1 -1
  272. package/dist/web/routes/custom-model-routes.d.ts +7 -2
  273. package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
  274. package/dist/web/routes/custom-model-routes.js +51 -6
  275. package/dist/web/routes/custom-model-routes.js.map +1 -1
  276. package/dist/web/routes/discord-routes.d.ts +25 -0
  277. package/dist/web/routes/discord-routes.d.ts.map +1 -0
  278. package/dist/web/routes/discord-routes.js +206 -0
  279. package/dist/web/routes/discord-routes.js.map +1 -0
  280. package/dist/web/routes/edit-history-routes.d.ts +43 -0
  281. package/dist/web/routes/edit-history-routes.d.ts.map +1 -0
  282. package/dist/web/routes/edit-history-routes.js +107 -0
  283. package/dist/web/routes/edit-history-routes.js.map +1 -0
  284. package/dist/web/routes/file-routes.d.ts.map +1 -1
  285. package/dist/web/routes/file-routes.js +273 -102
  286. package/dist/web/routes/file-routes.js.map +1 -1
  287. package/dist/web/routes/git-status-routes.d.ts.map +1 -1
  288. package/dist/web/routes/git-status-routes.js +7 -2
  289. package/dist/web/routes/git-status-routes.js.map +1 -1
  290. package/dist/web/routes/index.d.ts +2 -0
  291. package/dist/web/routes/index.d.ts.map +1 -1
  292. package/dist/web/routes/index.js +2 -0
  293. package/dist/web/routes/index.js.map +1 -1
  294. package/dist/web/routes/mux-routes.d.ts +10 -3
  295. package/dist/web/routes/mux-routes.d.ts.map +1 -1
  296. package/dist/web/routes/mux-routes.js +59 -1
  297. package/dist/web/routes/mux-routes.js.map +1 -1
  298. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  299. package/dist/web/routes/ralph-routes.js +22 -0
  300. package/dist/web/routes/ralph-routes.js.map +1 -1
  301. package/dist/web/routes/readmymind-routes.d.ts.map +1 -1
  302. package/dist/web/routes/readmymind-routes.js +3 -2
  303. package/dist/web/routes/readmymind-routes.js.map +1 -1
  304. package/dist/web/routes/respawn-routes.d.ts.map +1 -1
  305. package/dist/web/routes/respawn-routes.js +24 -0
  306. package/dist/web/routes/respawn-routes.js.map +1 -1
  307. package/dist/web/routes/session-routes.d.ts +21 -1
  308. package/dist/web/routes/session-routes.d.ts.map +1 -1
  309. package/dist/web/routes/session-routes.js +338 -55
  310. package/dist/web/routes/session-routes.js.map +1 -1
  311. package/dist/web/routes/system-routes.d.ts.map +1 -1
  312. package/dist/web/routes/system-routes.js +4 -2
  313. package/dist/web/routes/system-routes.js.map +1 -1
  314. package/dist/web/schemas.d.ts +92 -2
  315. package/dist/web/schemas.d.ts.map +1 -1
  316. package/dist/web/schemas.js +111 -10
  317. package/dist/web/schemas.js.map +1 -1
  318. package/dist/web/self-update.d.ts +9 -0
  319. package/dist/web/self-update.d.ts.map +1 -1
  320. package/dist/web/self-update.js +19 -3
  321. package/dist/web/self-update.js.map +1 -1
  322. package/dist/web/sensitive-path.d.ts.map +1 -1
  323. package/dist/web/sensitive-path.js +15 -0
  324. package/dist/web/sensitive-path.js.map +1 -1
  325. package/dist/web/server.d.ts +56 -0
  326. package/dist/web/server.d.ts.map +1 -1
  327. package/dist/web/server.js +285 -17
  328. package/dist/web/server.js.map +1 -1
  329. package/dist/web/session-wait-registry.d.ts +13 -0
  330. package/dist/web/session-wait-registry.d.ts.map +1 -1
  331. package/dist/web/session-wait-registry.js +16 -8
  332. package/dist/web/session-wait-registry.js.map +1 -1
  333. package/package.json +2 -2
  334. package/skills/codeman/SKILL.md +20 -4
  335. package/dist/web/public/app.fe8a6b4c.js.br +0 -0
  336. package/dist/web/public/app.fe8a6b4c.js.gz +0 -0
  337. package/dist/web/public/constants.affcdde6.js.br +0 -0
  338. package/dist/web/public/constants.affcdde6.js.gz +0 -0
  339. package/dist/web/public/i18n.a618ef4e.js +0 -2
  340. package/dist/web/public/i18n.a618ef4e.js.br +0 -0
  341. package/dist/web/public/i18n.a618ef4e.js.gz +0 -0
  342. package/dist/web/public/keyboard-accessory.ff64fafc.js.br +0 -0
  343. package/dist/web/public/keyboard-accessory.ff64fafc.js.gz +0 -0
  344. package/dist/web/public/mobile.1bbd2d67.css +0 -1
  345. package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
  346. package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
  347. package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
  348. package/dist/web/public/panels-ui.4a56ee63.js.gz +0 -0
  349. package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
  350. package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
  351. package/dist/web/public/session-ui.426fb671.js.br +0 -0
  352. package/dist/web/public/session-ui.426fb671.js.gz +0 -0
  353. package/dist/web/public/settings-ui.a191154e.js +0 -106
  354. package/dist/web/public/settings-ui.a191154e.js.br +0 -0
  355. package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
  356. package/dist/web/public/styles.4f0c67a0.css +0 -1
  357. package/dist/web/public/styles.4f0c67a0.css.br +0 -0
  358. package/dist/web/public/styles.4f0c67a0.css.gz +0 -0
  359. package/dist/web/public/terminal-ui.7e787829.js +0 -4
  360. package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
  361. package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
package/dist/session.js CHANGED
@@ -28,13 +28,16 @@
28
28
  * @module session
29
29
  */
30
30
  import { EventEmitter } from 'node:events';
31
- import { basename } from 'node:path';
31
+ import { basename, isAbsolute } from 'node:path';
32
+ import { realpath } from 'node:fs/promises';
32
33
  import { execSync, execFileSync } from 'node:child_process';
33
34
  import { v4 as uuidv4 } from 'uuid';
34
35
  import * as pty from 'node-pty';
35
36
  import { DEFAULT_NICE_CONFIG, getErrorMessage, isAdvisorModel, isEffortLevel, } from './types.js';
36
37
  import { resolveAndClaimOmpSessionId } from './utils/omp-session-resolver.js';
37
38
  import { claudeTranscriptExists } from './utils/claude-transcript.js';
39
+ import { probePathKind } from './utils/bounded-path-probe.js';
40
+ import { PATH_PROBE_TIMEOUT_MS, WORKSPACE_REALPATH_RETRY_MS } from './config/path-probe.js';
38
41
  import { matchesPattern } from './config/cli-registry/patterns.js';
39
42
  import { probeDockerCliVersion } from './docker-hosts.js';
40
43
  import { probeRemoteCliVersion } from './remote-hosts.js';
@@ -150,8 +153,8 @@ function getModeLabel(mode) {
150
153
  *
151
154
  * Only such a CLI needs its version probed at session start — probing one with no gates
152
155
  * would spawn a `--version` subprocess whose answer nothing reads. Today that is claude
153
- * (the `--name` flag, gated at 2.1.224), which is why the probe used to be written as
154
- * `mode === 'claude'`.
156
+ * (the `--name` flag, gated at 2.1.224) and codex (`--no-daemon`, gated at 0.162.0). The
157
+ * probe used to be written as `mode === 'claude'`, when claude was the only one.
155
158
  */
156
159
  function cliNeedsVersionProbe(mode) {
157
160
  return Object.keys(getCli(mode)?.capabilities.gates ?? {}).length > 0;
@@ -175,23 +178,21 @@ function cliExportsTruecolor(mode) {
175
178
  * Codex, Claude Code, and Gemini are known, controlled (Ink/React) TUIs that
176
179
  * repaint via cursor positioning, so dropping the alt-screen switch is safe —
177
180
  * content stays in the normal buffer. Excluded: `shell` (arbitrary programs like
178
- * vim/less/htop legitimately need the alt screen), `opencode` (its own MIDDLE strip,
179
- * isMuxMouseStripMode), `pi` (below) and `grok` (a fullscreen alt-screen TUI with
181
+ * vim/less/htop legitimately need the alt screen), `opencode`, `copilot` and `pi` (their own
182
+ * MIDDLE strip, isMuxMouseStripMode) and `grok` (a fullscreen alt-screen TUI with
180
183
  * mouse support). Keep parity with the replay-side strip (`stripReplayBuffer` in
181
184
  * session-routes.ts); the table in `CliCapabilities.altScreen` is pinned for every
182
185
  * stock CLI in test/claude-scrollback-strip.test.ts.
183
186
  *
184
187
  * ⚠️ Being excluded here does NOT preserve the alt screen. Every excluded mode
185
- * falls through to isMuxAltScreenOnlyStripMode(), which strips the alt-screen
186
- * toggles too whenever the session is tmux-backed, and pi/opencode ALWAYS are
187
- * (both refuse the direct-PTY fallback). What exclusion actually buys is the rest
188
- * of the full strip: `\x1b[3J` and the mouse-tracking DECSETs survive. That is the
189
- * real reason pi is out: its default TUI renders into the MAIN screen with
190
- * terminal-owned scrollback and is mouse-aware, so it is a `3J`/mouse consumer in
191
- * a way an Ink TUI repainting in place is not. Consequence to know before
192
- * debugging it: pi's runtime-switchable fullscreen TUI (`/settings`, 0.84.0+)
193
- * still gets its `?1049h` stripped and paints into the main buffer, exactly like
194
- * vim inside a tmux `shell` session.
188
+ * falls through to isMuxMouseStripMode() or isMuxAltScreenOnlyStripMode(), which
189
+ * strip the alt-screen toggles too whenever the session is tmux-backed, and
190
+ * pi/opencode ALWAYS are (both refuse the direct-PTY fallback). What exclusion
191
+ * actually buys is the rest of the full strip: `\x1b[3J` survives, and for the
192
+ * narrow strip the mouse-tracking DECSETs too. pi (fullscreen by default since
193
+ * 1.0.0) is a mouse-aware TUI like opencode and copilot, so it takes the mouse
194
+ * strip (isMuxMouseStripMode): a drag selects locally and `cliMouseTracking`
195
+ * keeps the wheel working across a buffer replay.
195
196
  */
196
197
  export function isAltScreenStripMode(mode) {
197
198
  return getCli(mode)?.capabilities.altScreen === 'strip-full';
@@ -268,6 +269,21 @@ const TEST_PTY_SCRIPT = 'if (process.stdin.isTTY) process.stdin.setRawMode(true)
268
269
  const DOCKER_CLI_VERSION_PROBE_DELAY_MS = 3000;
269
270
  /** Delay before the over-ssh Claude CLI version probe (keeps session start off the ssh round-trip). */
270
271
  const REMOTE_CLI_VERSION_PROBE_DELAY_MS = 3000;
272
+ /** Whether absolute `path` is `root` or lies under it (a lexical compare). */
273
+ function isSameOrUnder(path, root) {
274
+ return path === root || path.startsWith(root === '/' ? '/' : `${root}/`);
275
+ }
276
+ /**
277
+ * Rewrite `path` from under `from` to the same place under `to`; a path not at or
278
+ * under `from` comes back unchanged. Exported for unit testing.
279
+ */
280
+ export function rebasePath(path, from, to) {
281
+ if (path === from)
282
+ return to;
283
+ if (path.startsWith(`${from}/`))
284
+ return to + path.slice(from.length);
285
+ return path;
286
+ }
271
287
  /**
272
288
  * Ask tmux for the current window geometry of `muxName` so a re-attaching PTY
273
289
  * client can spawn at the same size and avoid the resize-flicker / scrollback
@@ -297,10 +313,20 @@ export function queryTmuxWindowSize(muxName, socket) {
297
313
  }
298
314
  return { cols: DEFAULT_PTY_COLS, rows: DEFAULT_PTY_ROWS };
299
315
  }
300
- export function resolveMuxAttachCwd(workingDir, remote, docker) {
316
+ export function resolveMuxAttachCwd(workingDir, remote, docker, adopt) {
301
317
  // Remote and docker sessions run the CLI elsewhere (ssh / docker exec); the LOCAL
302
318
  // wrapper pane never needs the workspace as its cwd, so launch it in /tmp.
303
- return remote || docker ? '/tmp' : workingDir;
319
+ //
320
+ // ⚠️ An ADOPTED session needs the same treatment and does NOT get it from the
321
+ // two checks above: its connection facts live on `adopt.docker`/`adopt.remote`,
322
+ // so `session.docker` is undefined even for an in-container adoption. Its pane
323
+ // only attaches, so no folder on this host means anything to it. Measured against
324
+ // a real container, when `workingDir` still held the foreign pane's cwd: the local
325
+ // PTY spawn inherited `/workspace/pythonserver`, and the session came up with
326
+ // `pid: null` and a blank terminal while the wrapper tmux session and the
327
+ // in-container grouped view had both been created successfully, so it looked
328
+ // half-alive rather than failed.
329
+ return remote || docker || adopt ? '/tmp' : workingDir;
304
330
  }
305
331
  /**
306
332
  * A write that a guarded submit's lease could not hold, because the per-lease hold
@@ -564,6 +590,23 @@ export class Session extends EventEmitter {
564
590
  * rendered as "alive".
565
591
  */
566
592
  _paneExit = null;
593
+ /**
594
+ * The pane's live directory, from the pane-exit watcher's read. Runtime-only:
595
+ * serialized by toState() but never restored by the constructor.
596
+ */
597
+ _paneCwd;
598
+ /** See {@link liveWorktree}. */
599
+ _liveWorktree;
600
+ /**
601
+ * `workingDir` with symlinks resolved, so a pane reading (tmux reads the
602
+ * process cwd, always the resolved path) can be put back into `workingDir`'s
603
+ * spelling. `undefined` until the first resolution settles; `null` when there
604
+ * is nothing to translate (no symlink in the path) or it could not be resolved.
605
+ */
606
+ _workspaceRealPath;
607
+ _workspaceRealPathPending = false;
608
+ /** When a resolution that met an unreachable folder may be tried again (0 = never). */
609
+ _workspaceRealPathRetryAt = 0;
567
610
  /**
568
611
  * How many starts, attaches or relaunches are running for this session's
569
612
  * pane. While one is, a dead-pane reading may describe a pane that is being
@@ -651,6 +694,10 @@ export class Session extends EventEmitter {
651
694
  // Docker execution metadata, present when this session runs inside a container via
652
695
  // local tmux + `docker exec`. The container is per-CASE (shared by sibling sessions).
653
696
  _docker;
697
+ // Adoption metadata, present when this session is a WRAPPER around a tmux
698
+ // session a human started outside Codeman. Its presence is the single switch
699
+ // that disables everything Codeman can only do to a process it launched.
700
+ _adopt;
654
701
  // Owning username in multi-user mode (undefined in single-user). Stamped at create
655
702
  // from req.authUser and round-tripped through recovery like _remote/_docker.
656
703
  _owner;
@@ -826,6 +873,7 @@ export class Session extends EventEmitter {
826
873
  this._tmuxHistoryLimit = config.tmuxHistoryLimit ?? DEFAULT_TMUX_HISTORY_LIMIT;
827
874
  this._remote = config.remote;
828
875
  this._docker = config.docker;
876
+ this._adopt = config.adopt;
829
877
  this._owner = config.owner;
830
878
  this._discoveredMuxSession = config.discoveredMuxSession === true;
831
879
  // Restored so a record that says the agent exited survives a server restart
@@ -967,6 +1015,23 @@ export class Session extends EventEmitter {
967
1015
  get remote() {
968
1016
  return this._remote;
969
1017
  }
1018
+ /** Adoption metadata when this session wraps a human-started tmux session. */
1019
+ get adopt() {
1020
+ return this._adopt;
1021
+ }
1022
+ /**
1023
+ * Is this a wrapper around a tmux session Codeman did not start?
1024
+ *
1025
+ * ⚠️ Deliberately NOT folded into `isExternalCliMode()`. That predicate answers
1026
+ * "does this CLI render its own TUI", which is about the PROTOCOL. This one
1027
+ * answers "did we launch the process", which is about AUTHORITY: an adopted
1028
+ * session can be `mode: 'claude'` and still have no hooks installed, no
1029
+ * `envOverrides`, no effort and no `--session-id` we chose. Conflating the two
1030
+ * would silently grant claude-shaped automation over a process we do not own.
1031
+ */
1032
+ get isAdopted() {
1033
+ return this._adopt !== undefined;
1034
+ }
970
1035
  /**
971
1036
  * `deepSeekConfig.statusReporting` verbatim: `undefined` when the caller sent
972
1037
  * none (i.e. ON), `false` when the user disarmed the status bridge for this
@@ -993,6 +1058,26 @@ export class Session extends EventEmitter {
993
1058
  const value = this._envOverrides?.DSH_HOME;
994
1059
  return value && value.trim() ? value.trim() : undefined;
995
1060
  }
1061
+ /**
1062
+ * One env var from this session's overrides, for internal path resolution only
1063
+ * (custom-model injection linking an isolated config dir back to a path that must
1064
+ * survive lives in). Single-key lookup by design, like `deepSeekHomeOverride`: the
1065
+ * map can hold provider credentials and stays off the public `SessionState`.
1066
+ */
1067
+ envOverrideValue(key) {
1068
+ const value = this._envOverrides?.[key];
1069
+ return value && value.trim() ? value.trim() : undefined;
1070
+ }
1071
+ /**
1072
+ * What the SESSION itself set for `key`, same single-key rule. `envOverrideValue()` answers
1073
+ * with whatever the map holds, which under a selection is the injected value: a re-apply
1074
+ * resolving the CLI's real home must read the session's own home, or none.
1075
+ */
1076
+ ownEnvValue(key) {
1077
+ const selection = this._customModel;
1078
+ const value = selection?.envKeys.includes(key) ? selection.ownEnv?.[key] : this._envOverrides?.[key];
1079
+ return value?.trim() || undefined;
1080
+ }
996
1081
  /** Owning username in multi-user mode, else undefined. */
997
1082
  get owner() {
998
1083
  return this._owner;
@@ -1057,13 +1142,20 @@ export class Session extends EventEmitter {
1057
1142
  /**
1058
1143
  * True when a tmux pane's death would mean THIS session's agent has exited.
1059
1144
  *
1060
- * Four shapes fail the test, and each would otherwise publish a death that is
1145
+ * Five shapes fail the test, and each would otherwise publish a death that is
1061
1146
  * not the agent's. A direct-PTY session owns no pane at all. A remote SSH
1062
1147
  * session's local pane holds the ssh client, whose death means a transport
1063
1148
  * drop OR an exit, which is the ambiguity PR #355 was about. A docker case's
1064
1149
  * local pane holds a `docker exec` into the container's own tmux.
1065
1150
  *
1066
- * The fourth is a session rebuilt from the socket. Absent `remote`/`docker`
1151
+ * ⚠️ An ADOPTED session is the same shape for a different reason: its pane runs
1152
+ * `tmux attach` to a session a human started, never the agent. That attach exiting
1153
+ * with 0 says the connection ended or its owner closed the foreign session; neither
1154
+ * means this session finished, and neither is Codeman's to clear the tab for. Without
1155
+ * this, the clean-exit sweep (`pane-exit-sweep.ts`) would close someone else's live
1156
+ * session's tab the moment they detached from it themselves.
1157
+ *
1158
+ * The fifth is a session rebuilt from the socket. Absent `remote`/`docker`/`adopt`
1067
1159
  * normally means "this is local", but on a discovered record it only means
1068
1160
  * "Codeman never found the metadata": the synthetic `restored-<fragment>` id
1069
1161
  * matches no `state.json` entry, so a remote session rediscovered after
@@ -1073,7 +1165,7 @@ export class Session extends EventEmitter {
1073
1165
  get paneExitApplies() {
1074
1166
  if (this._discoveredMuxSession)
1075
1167
  return false;
1076
- return this._useMux && this._muxSession !== null && !this._remote && !this._docker;
1168
+ return this._useMux && this._muxSession !== null && !this._remote && !this._docker && !this._adopt;
1077
1169
  }
1078
1170
  /** What Codeman last observed of this pane's agent, or undefined for UNKNOWN. */
1079
1171
  get paneExit() {
@@ -1144,6 +1236,117 @@ export class Session extends EventEmitter {
1144
1236
  if (this._muxSession)
1145
1237
  this._mux?.clearPaneExit?.(this._muxSession.muxName);
1146
1238
  }
1239
+ /** The pane's live working directory, when the pane read has reported one. */
1240
+ get paneCwd() {
1241
+ return this._paneCwd;
1242
+ }
1243
+ /**
1244
+ * Record the pane's live directory. Returns whether it CHANGED, so the caller
1245
+ * broadcasts only on a real transition.
1246
+ *
1247
+ * Scoped by {@link paneExitApplies}, the same rule as the exit reading: a
1248
+ * remote session's local pane runs the ssh client and a docker case's runs a
1249
+ * `docker exec`, so their pane directory is a host directory the agent was
1250
+ * never in, and a direct-PTY or rediscovered session has no pane Codeman can
1251
+ * vouch for. A relative value is refused rather than stored.
1252
+ *
1253
+ * tmux reads the pane's directory from the process cwd, which is always the
1254
+ * resolved path, while `workingDir` keeps the spelling the case was created
1255
+ * with. A workspace reached through a symlink would otherwise read as outside
1256
+ * itself, and the browser's preview routing (a lexical compare against
1257
+ * `workingDir`) would send every relative link to the attachment route. So a
1258
+ * reading at or under the workspace's resolved path is written back in
1259
+ * `workingDir`'s spelling. Until that resolution has settled, only a reading
1260
+ * lexically inside `workingDir` is accepted.
1261
+ */
1262
+ setPaneCwd(next) {
1263
+ let resolved = this.paneExitApplies && next?.startsWith('/') ? next : undefined;
1264
+ if (resolved !== undefined) {
1265
+ if (this._workspaceRealPath === undefined ||
1266
+ (this._workspaceRealPathRetryAt !== 0 && Date.now() >= this._workspaceRealPathRetryAt)) {
1267
+ this._resolveWorkspaceRealPath();
1268
+ }
1269
+ if (this._workspaceRealPath === undefined) {
1270
+ // The workspace's resolved path is not known yet. A reading outside
1271
+ // `workingDir` could be the workspace itself seen through its symlink, so
1272
+ // it waits for the next tick rather than mis-route links for one.
1273
+ if (!isSameOrUnder(resolved, this._workspaceSpelling()))
1274
+ return false;
1275
+ }
1276
+ else if (this._workspaceRealPath !== null) {
1277
+ resolved = rebasePath(resolved, this._workspaceRealPath, this._workspaceSpelling());
1278
+ }
1279
+ }
1280
+ if (resolved === this._paneCwd)
1281
+ return false;
1282
+ this._paneCwd = resolved;
1283
+ return true;
1284
+ }
1285
+ /**
1286
+ * The linked git worktree inside `workingDir` that {@link paneCwd} is in, when there is one
1287
+ * (`live-worktree.ts`). The Files panel lists it instead of the main checkout. Runtime-only and
1288
+ * never a confinement root, like `paneCwd`: it is judged lexically when the pane moves, and the
1289
+ * listing route re-checks the real folder on every request.
1290
+ */
1291
+ get liveWorktree() {
1292
+ return this._liveWorktree;
1293
+ }
1294
+ /** Record the result of a worktree lookup. Returns whether it CHANGED, so the caller broadcasts only then. */
1295
+ setLiveWorktree(next) {
1296
+ if (next?.path === this._liveWorktree?.path)
1297
+ return false;
1298
+ this._liveWorktree = next;
1299
+ return true;
1300
+ }
1301
+ /** `workingDir` without a trailing separator (the root stays `/`). */
1302
+ _workspaceSpelling() {
1303
+ return this.workingDir.length > 1 ? this.workingDir.replace(/\/+$/, '') : this.workingDir;
1304
+ }
1305
+ /**
1306
+ * Resolve `workingDir`'s symlinks in the background for {@link setPaneCwd}.
1307
+ *
1308
+ * Bounded: the folder can sit on a network mount that stopped answering, so it
1309
+ * is probed with `probePathKind()` first and the `realpath()` is raced against
1310
+ * the same timeout. An `unknown` probe, a failed or slow `realpath()` leave no
1311
+ * translation and allow one more try after `WORKSPACE_REALPATH_RETRY_MS`; a
1312
+ * folder that is absent or not a directory is settled for good.
1313
+ */
1314
+ _resolveWorkspaceRealPath() {
1315
+ if (this._workspaceRealPathPending)
1316
+ return;
1317
+ this._workspaceRealPathPending = true;
1318
+ const spelling = this._workspaceSpelling();
1319
+ void (async () => {
1320
+ let real = null;
1321
+ let retry = false;
1322
+ try {
1323
+ const kind = await probePathKind(spelling);
1324
+ if (kind === 'unknown') {
1325
+ retry = true;
1326
+ }
1327
+ else if (kind === 'directory') {
1328
+ let timer;
1329
+ const answer = await Promise.race([
1330
+ realpath(spelling).catch(() => undefined),
1331
+ new Promise((resolveTimeout) => {
1332
+ timer = setTimeout(() => resolveTimeout(undefined), PATH_PROBE_TIMEOUT_MS);
1333
+ timer.unref?.();
1334
+ }),
1335
+ ]).finally(() => clearTimeout(timer));
1336
+ if (answer === undefined)
1337
+ retry = true;
1338
+ else if (answer !== spelling && answer !== '/')
1339
+ real = answer;
1340
+ }
1341
+ }
1342
+ catch {
1343
+ retry = true;
1344
+ }
1345
+ this._workspaceRealPath = real;
1346
+ this._workspaceRealPathRetryAt = retry ? Date.now() + WORKSPACE_REALPATH_RETRY_MS : 0;
1347
+ this._workspaceRealPathPending = false;
1348
+ })();
1349
+ }
1147
1350
  /**
1148
1351
  * Record what the mux layer observed of this pane's agent, and say whether
1149
1352
  * that changed the answer. The caller persists and broadcasts on a true.
@@ -1468,20 +1671,38 @@ export class Session extends EventEmitter {
1468
1671
  }
1469
1672
  /**
1470
1673
  * The full selection incl. bookkeeping, for state.json ONLY (`__customModel`, the
1471
- * same disk-only convention as `getEnvOverridesForPersist()`). Carries no env values,
1472
- * so nothing secret lands on disk; recovery re-derives them from the endpoint store.
1674
+ * same disk-only convention as `getEnvOverridesForPersist()`). Carries no injected env
1675
+ * values, so nothing secret lands on disk; recovery re-derives them from the endpoint store.
1473
1676
  * Without this a Codeman restart left the pane on the custom endpoint (tmux keeps
1474
1677
  * its `setenv`s) while `customModel` came back undefined, so the state was wrong and
1475
1678
  * clearing had nothing to unset. Must NOT be included in any API-bound serializer.
1679
+ *
1680
+ * The one value it does carry is the session's own home directory (`ownEnv`, narrowed to
1681
+ * `homeEnvVar`), a path like `configDir` beside it: the selection overwrote the tmux
1682
+ * `setenv` that held it, so without this a clear after a restart would lose it. Any other
1683
+ * entry of `ownEnv` may be a credential and stays in memory, and so does a `homeEnvVar`
1684
+ * value that is not an absolute path: the registry types that name as any env var, so a
1685
+ * `clis.json` override can point it at a credential.
1476
1686
  */
1477
1687
  getCustomModelForPersist() {
1478
- return this._customModel ? { ...this._customModel, envKeys: [...this._customModel.envKeys] } : undefined;
1688
+ if (!this._customModel)
1689
+ return undefined;
1690
+ const { ownEnv, ...selection } = this._customModel;
1691
+ const { homeEnvVar } = selection;
1692
+ const ownHome = homeEnvVar ? ownEnv?.[homeEnvVar] : undefined;
1693
+ return {
1694
+ ...selection,
1695
+ envKeys: [...selection.envKeys],
1696
+ ...(homeEnvVar && ownHome && isAbsolute(ownHome) ? { ownEnv: { [homeEnvVar]: ownHome } } : {}),
1697
+ };
1479
1698
  }
1480
1699
  /**
1481
1700
  * Update this session's custom-model selection and merge the endpoint's injected env
1482
1701
  * vars into `_envOverrides` — first UNDOING whatever the previous selection injected
1483
- * (removing exactly those env keys), so switching endpoints, or clearing back to the
1484
- * harness's native cloud default, never leaves a stale key behind. Synchronous and
1702
+ * (removing exactly those env keys, and putting back a value the session had set under
1703
+ * one itself), so switching endpoints, or clearing back to the harness's native cloud
1704
+ * default, never leaves a stale key behind and never loses the session's own `DSH_HOME`
1705
+ * or `CLAUDE_CONFIG_DIR`. Synchronous and
1485
1706
  * side-effect-free beyond mutating state, matching `setNice`/`setColor` above — this
1486
1707
  * class does no file IO, so it reports the PREVIOUS `configDir` (if any) for the
1487
1708
  * caller to clean up on disk (custom-model-injection.ts's configDir kind).
@@ -1491,20 +1712,44 @@ export class Session extends EventEmitter {
1491
1712
  * `_buildRespawnPaneOptions().unsetEnvKeys`): the tmux session inherits every
1492
1713
  * `setenv` into `respawn-pane`, so dropping them from the map alone would relaunch
1493
1714
  * the CLI still pointed at the old endpoint — and for the `configDir` kinds, at a
1494
- * `HOME`/`CODEX_HOME`/`GROK_HOME` the caller has just deleted.
1715
+ * `HOME`/`CODEX_HOME`/`GROK_HOME` the caller has just deleted. A key put back stays out
1716
+ * of that queue: `applyEnvOverrides()` writes its value over the injected one.
1717
+ *
1718
+ * `next.ownEnv` is for recovery, which states the session's own values itself: after a
1719
+ * restart `_envOverrides` holds what the previous run injected (`CLAUDE_CONFIG_DIR` is
1720
+ * persisted), and reading those as the session's own would pin it to a deleted directory.
1495
1721
  */
1496
1722
  setCustomModel(next, envOverrides) {
1497
1723
  const previousConfigDir = this._customModel?.configDir;
1498
1724
  const removedEnvKeys = [];
1499
1725
  if (this._customModel) {
1726
+ const previousOwnEnv = this._customModel.ownEnv ?? {};
1500
1727
  for (const key of this._customModel.envKeys) {
1501
1728
  if (this._envOverrides)
1502
1729
  delete this._envOverrides[key];
1503
1730
  removedEnvKeys.push(key);
1504
- this._pendingEnvUnsets.add(key);
1731
+ const own = previousOwnEnv[key];
1732
+ if (own) {
1733
+ this._envOverrides = { ...(this._envOverrides ?? {}), [key]: own };
1734
+ this._pendingEnvUnsets.delete(key);
1735
+ }
1736
+ else {
1737
+ this._pendingEnvUnsets.add(key);
1738
+ }
1739
+ }
1740
+ }
1741
+ // Read after the undo, so a switch between endpoints carries the own values across. An
1742
+ // empty value is skipped: `applyEnvOverrides()` never set one, so it has nothing to
1743
+ // restore, and the injected key still has to be unset.
1744
+ const ownEnv = { ...next?.ownEnv };
1745
+ if (!next?.ownEnv) {
1746
+ for (const key of Object.keys(envOverrides ?? {})) {
1747
+ const own = this._envOverrides?.[key];
1748
+ if (own)
1749
+ ownEnv[key] = own;
1505
1750
  }
1506
1751
  }
1507
- this._customModel = next ? { ...next, envKeys: [...next.envKeys] } : undefined;
1752
+ this._customModel = next ? { ...next, envKeys: [...next.envKeys], ownEnv } : undefined;
1508
1753
  if (envOverrides && Object.keys(envOverrides).length > 0) {
1509
1754
  this._envOverrides = { ...(this._envOverrides ?? {}), ...envOverrides };
1510
1755
  // A key the new selection sets again does not need an unset (applyEnvOverrides
@@ -1731,8 +1976,11 @@ export class Session extends EventEmitter {
1731
1976
  pid: this.pid,
1732
1977
  status: this._status,
1733
1978
  workingDir: this.workingDir,
1979
+ paneCwd: this._paneCwd,
1980
+ liveWorktree: this._liveWorktree,
1734
1981
  remote: this._remote,
1735
1982
  docker: this._docker,
1983
+ adopt: this._adopt,
1736
1984
  owner: this._owner,
1737
1985
  parentSessionId: this._parentSessionId,
1738
1986
  currentTaskId: this._currentTaskId,
@@ -2011,7 +2259,7 @@ export class Session extends EventEmitter {
2011
2259
  name: 'xterm-256color',
2012
2260
  cols: ptyCols,
2013
2261
  rows: ptyRows,
2014
- cwd: resolveMuxAttachCwd(this.workingDir, this._remote, this._docker),
2262
+ cwd: resolveMuxAttachCwd(this.workingDir, this._remote, this._docker, this._adopt),
2015
2263
  // COD-75: a CLI that declares `export COLORTERM=truecolor` gets it on the ATTACH
2016
2264
  // client too. Both sides read the same registry entry, which is what stops the
2017
2265
  // attach client and the tmux session from disagreeing — they used to be two
@@ -2158,6 +2406,7 @@ export class Session extends EventEmitter {
2158
2406
  historyLimit: this._tmuxHistoryLimit,
2159
2407
  remote: this._remote,
2160
2408
  docker: this._docker,
2409
+ adopt: this._adopt,
2161
2410
  owner: this._owner,
2162
2411
  };
2163
2412
  return this._withCustomModelLaunchModel(options);
@@ -2240,7 +2489,7 @@ export class Session extends EventEmitter {
2240
2489
  if (entry?.launch.chain !== 'fallback')
2241
2490
  return options;
2242
2491
  const resumeIdPattern = entry.launch.params?.resumeId;
2243
- const configDir = this._claudeConfigDir();
2492
+ const configDir = this.claudeConfigDir();
2244
2493
  const chainTail = this._claudeSessionChain[this._claudeSessionChain.length - 1];
2245
2494
  const candidates = [chainTail, options.resumeSessionId, this.id].filter((v) => !!v);
2246
2495
  for (const candidate of candidates) {
@@ -2262,8 +2511,11 @@ export class Session extends EventEmitter {
2262
2511
  // unpinned options already carry is the correct command.
2263
2512
  return options;
2264
2513
  }
2265
- /** The session's Claude config dir when it has been relocated (#255), else undefined. */
2266
- _claudeConfigDir() {
2514
+ /**
2515
+ * The session's Claude config dir when it has been relocated (#255), else undefined.
2516
+ * Public for the edit history reader, which looks for the transcript there.
2517
+ */
2518
+ claudeConfigDir() {
2267
2519
  // Trimmed like `claudeProjectsDir()` trims the process-wide override: the
2268
2520
  // envOverrides schema validates keys only, and a whitespace-only value would
2269
2521
  // otherwise resolve to a relative path and read "no transcript" for everything.
@@ -2271,7 +2523,8 @@ export class Session extends EventEmitter {
2271
2523
  }
2272
2524
  /**
2273
2525
  * Force the custom-model selection's `launchModel` (pi/omp `custom/<id>`, grok's
2274
- * `[model.<name>]` block name) onto the CLI's `model` launch param. Where that param
2526
+ * `[model.<name>]` block name, codex's bare selected id) onto the CLI's `model`
2527
+ * launch param. Where that param
2275
2528
  * lives is registry DATA — the entry's `legacyConfigField` (`piConfig`, `grokConfig`,
2276
2529
  * ...) or the top-level `model` for an entry that declares none — so this stays a
2277
2530
  * generic reader rather than a branch per CLI. Applied on the OPTIONS only: the stored
@@ -2411,7 +2664,7 @@ export class Session extends EventEmitter {
2411
2664
  // redraws overwrite only the cells they target, so non-erased rows keep
2412
2665
  // their content. Gated to Codex/Claude/Gemini (isAltScreenStripMode).
2413
2666
  //
2414
- // Every OTHER mode (shell/antigravity/pi/grok/deepseek/omp) gets the NARROW strip
2667
+ // Every OTHER mode (shell/antigravity/grok/deepseek/omp) gets the NARROW strip
2415
2668
  // when it is tmux-backed: alt-screen toggles only, because the sequence that breaks
2416
2669
  // scrollback there is tmux's own client-side smcup at attach, not anything the
2417
2670
  // program in the pane emitted (issue #205, see isMuxAltScreenOnlyStripMode).
@@ -2490,6 +2743,25 @@ export class Session extends EventEmitter {
2490
2743
  this.emit('terminal', data);
2491
2744
  this.emit('output', data);
2492
2745
  }
2746
+ /**
2747
+ * End a start that cannot reach an adopted session's wrapper.
2748
+ *
2749
+ * ⚠️ Never the direct-PTY fallback. What that starts is a NEW process on THIS host
2750
+ * (`claude --dangerously-skip-permissions`, or a server-account shell) in the neutral
2751
+ * `/tmp`, under a tab still carrying the foreign session's name, so someone who opened an
2752
+ * ssh or container session would end up typing into this machine. `_useMux` stays on, so
2753
+ * the next start, which is what selecting the tab sends, tries the wrapper again.
2754
+ *
2755
+ * The `error` event is what the tab shows: the browser's re-attach does not read the
2756
+ * route's answer.
2757
+ */
2758
+ _failAdoptedStart(err) {
2759
+ const message = `Could not attach to ${this._adopt?.targetSession}: ${getErrorMessage(err)}`;
2760
+ console.error(`[Session] ${message}`);
2761
+ this._status = 'stopped';
2762
+ this.emit('error', message);
2763
+ throw new Error(message);
2764
+ }
2493
2765
  async startInteractive() {
2494
2766
  if (this.ptyProcess) {
2495
2767
  throw new Error('Session already has a running process');
@@ -2525,8 +2797,10 @@ export class Session extends EventEmitter {
2525
2797
  // wheel-forwarding to Claude's own transcript (the only route to history in
2526
2798
  // repaint/alt-screen mode; issue #154). Remote sessions run claude on
2527
2799
  // another host, so a local probe wouldn't reflect their version; they get
2528
- // their own over-ssh probe below. Cached process-wide, best-effort.
2529
- if (cliNeedsVersionProbe(this.mode) && !this._remote && !this._docker && !this._cliVersion) {
2800
+ // their own over-ssh probe below. Cached process-wide, best-effort. An adopted
2801
+ // session's agent is a process someone else started, possibly elsewhere, so
2802
+ // the local binary says nothing about it either.
2803
+ if (cliNeedsVersionProbe(this.mode) && !this._remote && !this._docker && !this._adopt && !this._cliVersion) {
2530
2804
  const probedVersion = resolveSessionCliVersion(this.mode);
2531
2805
  if (probedVersion) {
2532
2806
  this._cliVersion = probedVersion;
@@ -2629,6 +2903,7 @@ export class Session extends EventEmitter {
2629
2903
  historyLimit: this._tmuxHistoryLimit,
2630
2904
  remote: this._remote,
2631
2905
  docker: this._docker,
2906
+ adopt: this._adopt,
2632
2907
  owner: this._owner,
2633
2908
  },
2634
2909
  spawnErrLabel: 'mux attachment',
@@ -2714,6 +2989,8 @@ export class Session extends EventEmitter {
2714
2989
  }
2715
2990
  }
2716
2991
  catch (err) {
2992
+ if (this._adopt)
2993
+ this._failAdoptedStart(err);
2717
2994
  console.error('[Session] Failed to create mux session, falling back to direct PTY:', err);
2718
2995
  this._useMux = false;
2719
2996
  this._muxSession = null;
@@ -2732,6 +3009,8 @@ export class Session extends EventEmitter {
2732
3009
  if (getCli(this.mode)?.capabilities.requiresMux) {
2733
3010
  throw new Error(`${getModeLabel(this.mode)} sessions require tmux. Direct PTY fallback is not supported.`);
2734
3011
  }
3012
+ if (this._adopt)
3013
+ this._failAdoptedStart(new Error('tmux is not available'));
2735
3014
  try {
2736
3015
  // Pass --session-id to use the SAME ID as the Codeman session
2737
3016
  // This ensures subagents can be directly matched to the correct tab.
@@ -2918,6 +3197,14 @@ export class Session extends EventEmitter {
2918
3197
  // dialog, so without this a deleted session kept "answering" it for several seconds.
2919
3198
  if (this._trustDialogAccepted || this._isStopped)
2920
3199
  return;
3200
+ // ⚠️ Never answer a dialog in someone else's pane. An adopted session wraps a
3201
+ // claude the human started, and this scan reads the pane and SENDS KEYS for 90
3202
+ // seconds after `startInteractive()`, which boot recovery re-runs for every
3203
+ // wrapper, so it is not a one-time window at adoption.
3204
+ if (this._adopt) {
3205
+ this._trustDialogAccepted = true;
3206
+ return;
3207
+ }
2921
3208
  const now = Date.now();
2922
3209
  if (now - this._interactiveStartedAt > TRUST_DIALOG_WINDOW_MS) {
2923
3210
  this._trustDialogAccepted = true; // window closed; anything matching now is not the dialog
@@ -3651,6 +3938,7 @@ export class Session extends EventEmitter {
3651
3938
  historyLimit: this._tmuxHistoryLimit,
3652
3939
  remote: this._remote,
3653
3940
  docker: this._docker,
3941
+ adopt: this._adopt,
3654
3942
  owner: this._owner,
3655
3943
  },
3656
3944
  createSessionOptions: {
@@ -3663,13 +3951,17 @@ export class Session extends EventEmitter {
3663
3951
  historyLimit: this._tmuxHistoryLimit,
3664
3952
  remote: this._remote,
3665
3953
  docker: this._docker,
3954
+ adopt: this._adopt,
3666
3955
  owner: this._owner,
3667
3956
  },
3668
3957
  spawnErrLabel: 'shell mux attachment',
3669
3958
  });
3670
3959
  // For NEW sessions: clear by sending 'clear' command to the shell
3671
3960
  // For RESTORED sessions: don't clear - we want to see the existing output
3672
- if (!isRestored) {
3961
+ // ⚠️ Never for an ADOPTED one: this pane is someone else's, so the write would land
3962
+ // on their half-typed command and submit it (measured), or in whatever is reading
3963
+ // stdin. `shell` is also the mode of anything the classifier cannot name.
3964
+ if (!isRestored && !this._adopt) {
3673
3965
  setTimeout(() => {
3674
3966
  if (this.ptyProcess) {
3675
3967
  this._terminalBuffer.clear();
@@ -3679,6 +3971,8 @@ export class Session extends EventEmitter {
3679
3971
  }
3680
3972
  }
3681
3973
  catch (err) {
3974
+ if (this._adopt)
3975
+ this._failAdoptedStart(err);
3682
3976
  console.error('[Session] Failed to create mux session, falling back to direct PTY:', err);
3683
3977
  this._useMux = false;
3684
3978
  this._muxSession = null;
@@ -3686,6 +3980,8 @@ export class Session extends EventEmitter {
3686
3980
  }
3687
3981
  // Fallback to direct PTY if mux is not used
3688
3982
  if (!this.ptyProcess) {
3983
+ if (this._adopt)
3984
+ this._failAdoptedStart(new Error('tmux is not available'));
3689
3985
  try {
3690
3986
  this.ptyProcess = spawnPtyWithHelperRepair(() => pty.spawn(shell, [], {
3691
3987
  name: 'xterm-256color',
@@ -3763,6 +4059,9 @@ export class Session extends EventEmitter {
3763
4059
  * ```
3764
4060
  */
3765
4061
  async runPrompt(prompt, options) {
4062
+ // ⚠️ A one-shot `claude -p` is a new local process in `/tmp`, never the agent in that pane.
4063
+ if (this._adopt)
4064
+ throw new Error('One-shot prompts are not available for adopted sessions');
3766
4065
  if (this.guardedInputHeld) {
3767
4066
  this._guardedInputRevision++;
3768
4067
  throw new Error('Guarded submission in progress');
@@ -4345,6 +4644,7 @@ export class Session extends EventEmitter {
4345
4644
  (!allowLease && this.guardedInputHeld) ||
4346
4645
  this.remote ||
4347
4646
  this.docker ||
4647
+ this._adopt ||
4348
4648
  this.paneExit ||
4349
4649
  !this._muxSession)
4350
4650
  return null;