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
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import { EventEmitter } from 'node:events';
24
24
  import { collectDescendants } from './proc-tree.js';
25
+ import { foreignViewSessionName, buildForeignDockerViewKillCommand, buildForeignAttachCommand, buildForeignDockerAttachCommand, buildForeignRemoteAttachCommand, } from './foreign-tmux.js';
25
26
  import { execSync, exec, execFile } from 'node:child_process';
26
27
  import { promisify } from 'node:util';
27
28
  const execAsync = promisify(exec);
@@ -271,15 +272,54 @@ export function derivePaneExits(rows, now) {
271
272
  }
272
273
  return exits;
273
274
  }
275
+ /**
276
+ * Decide, from every pane tmux listed, which directory each tmux session's pane
277
+ * is in now (`#{pane_current_path}`). Exported for unit testing.
278
+ *
279
+ * This is what a relative path printed in the terminal is relative TO: a
280
+ * session's `workingDir` is fixed at create time and never follows a `cd`.
281
+ *
282
+ * - Only sessions with EXACTLY ONE pane answer, for the reason
283
+ * {@link derivePaneExits} gives: a hand-split session has no single "the"
284
+ * pane to speak for it.
285
+ * - Only absolute paths count. A blank field is a tmux that could not read the
286
+ * process cwd: usually a pane whose program exited under `remain-on-exit`
287
+ * (tmux 3.4 then reports it blank on every read), otherwise a read that
288
+ * failed that tick. Either way the directory is unknown, not gone, so a
289
+ * single-pane session with a blank field keeps its entry from `previous`.
290
+ * A session split into several panes, or one the read no longer lists, gets
291
+ * no entry: those are authoritative absences, not unanswered questions.
292
+ *
293
+ * The launch directory is NOT filtered here (unlike the rediscovery path): a
294
+ * remote or docker pane that never leaves it is refused by `Session.setPaneCwd()`,
295
+ * and a local pane reporting it really is there.
296
+ */
297
+ export function derivePaneCurrentPaths(rows, previous = new Map()) {
298
+ const panesPerSession = new Map();
299
+ for (const row of rows) {
300
+ panesPerSession.set(row.sessionName, (panesPerSession.get(row.sessionName) ?? 0) + 1);
301
+ }
302
+ const paths = new Map();
303
+ for (const row of rows) {
304
+ if (panesPerSession.get(row.sessionName) !== 1)
305
+ continue;
306
+ const path = row.currentPath ?? previous.get(row.sessionName);
307
+ if (!path || !path.startsWith('/'))
308
+ continue;
309
+ paths.set(row.sessionName, path);
310
+ }
311
+ return paths;
312
+ }
274
313
  /**
275
314
  * Could any of these tmux sessions ever produce a pane-exit answer? Exported
276
315
  * for unit testing.
277
316
  *
278
317
  * Mirrors `Session.paneExitApplies`, which is where the rule is enforced. A
279
318
  * remote session's local pane holds the ssh client, a docker case's holds a
280
- * `docker exec` into the container's own tmux, and a record rebuilt from the
281
- * socket carries no provenance at all, so the session end forces all three to
282
- * UNKNOWN whatever tmux reports. A tick that sees only those has nothing to
319
+ * `docker exec` into the container's own tmux, an ADOPTED session's holds a
320
+ * `tmux attach` to a session a human started rather than the agent itself, and
321
+ * a record rebuilt from the socket carries no provenance at all, so the session
322
+ * end forces all four to UNKNOWN whatever tmux reports. A tick that sees only those has nothing to
283
323
  * learn, and `refreshPaneExits()` skips its tmux read rather than paying for
284
324
  * the answer.
285
325
  *
@@ -294,12 +334,27 @@ export function hasObservablePaneSession(sessions) {
294
334
  continue;
295
335
  if (session.docker)
296
336
  continue;
337
+ if (session.adopt)
338
+ continue;
297
339
  if (session.discovered === true)
298
340
  continue;
299
341
  return true;
300
342
  }
301
343
  return false;
302
344
  }
345
+ /**
346
+ * Which adopted wrappers have a dead pane with a tmux client still attached, from rows of
347
+ * `#{pane_dead} #{session_attached} #{session_name}`. Exported for unit testing.
348
+ */
349
+ export function endedAdoptedAttaches(rows, adopted) {
350
+ const ended = [];
351
+ for (const row of rows.split('\n')) {
352
+ const [dead, attached, muxName] = row.trim().split(' ');
353
+ if (dead === '1' && Number(attached) > 0 && adopted.has(muxName))
354
+ ended.push(muxName);
355
+ }
356
+ return ended;
357
+ }
303
358
  /**
304
359
  * Resolve a target pane id from `tmux list-panes -F '#{pane_id}:#{pane_active}'`.
305
360
  * Prefers the active pane and falls back to the first valid pane.
@@ -988,12 +1043,15 @@ export function remoteTmuxLocation(remote, sessionId) {
988
1043
  *
989
1044
  * Emits:
990
1045
  * ssh -o BatchMode=yes -t [<COD-107 connection opts>] user@host \
991
- * 'tmux -L codeman-remote new-session -A -s codeman-ssh-<id> -c <path> "cd <path> && exec <cli>" \
1046
+ * 'tmux -L codeman-remote new-session -A -s codeman-ssh-<id> -c <path> /bin/sh -c <pane-command> \
992
1047
  * \; set -t codeman-ssh-<id> status off \; set -t codeman-ssh-<id> mouse off \
993
1048
  * \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0 \; \
994
1049
  * bind-key -T prefix C-q send-prefix \; unbind-key -a -T prefix \; \
995
1050
  * bind-key -T prefix C-q send-prefix \; set -t codeman-ssh-<id> remain-on-exit failed'
996
1051
  *
1052
+ * `<pane-command>` is `cd <path> && exec <cli>`, quoted as one token. A per-host `commands.*`
1053
+ * override is that one token alone, with no `/bin/sh -c` in front of it.
1054
+ *
997
1055
  * COD-107 — the connection options (`-p`, `-i`, `-J`, SOCKS `-o ProxyCommand`,
998
1056
  * arbitrary `-o`) come from the shared `buildSshConnectionArgs(remote)`, so the
999
1057
  * prereq tmux probe and this launch connect with identical options.
@@ -1007,10 +1065,12 @@ export function remoteTmuxLocation(remote, sessionId) {
1007
1065
  * - The `set` options are scoped per-session (`set -t <name>` / server-level
1008
1066
  * `set -s`), never `-g`, so they never mutate other sessions' prefix/mouse.
1009
1067
  * - The whole tmux invocation is a SINGLE ssh argument (the remote login shell
1010
- * runs it), so it is shell-quoted as one unit; the `cd && exec` command is in
1011
- * turn a single tmux argument (tmux runs it via `/bin/sh -c`), so the path is
1012
- * shell-quoted inside it too. This keeps escaping correct through every layer
1013
- * even when the remote path contains spaces.
1068
+ * runs it), so it is shell-quoted as one unit. The `cd && exec` command is the
1069
+ * pane command: Codeman's own text goes to `/bin/sh -c` as separate tmux
1070
+ * arguments, and a per-host `commands.*` override stays one argument, which
1071
+ * tmux hands to the remote default-shell (see the comment at `paneCommand`).
1072
+ * Each is shell-quoted, so escaping stays correct through every layer even
1073
+ * when the remote path contains spaces.
1014
1074
  */
1015
1075
  export function buildRemoteLaunchCommand(options) {
1016
1076
  const { mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId } = options;
@@ -1018,9 +1078,9 @@ export function buildRemoteLaunchCommand(options) {
1018
1078
  // hardcoding --dangerously-skip-permissions, so a non-granted multi-user user's
1019
1079
  // downgraded 'auto' actually reaches the remote agent (the default command otherwise
1020
1080
  // ignored claudeMode). A per-host `commands.claude` override stays authoritative
1021
- // (admin's explicit choice). Wrapped in `$SHELL -i -l -c` for the same reason as
1022
- // `defaultRemoteCommandForMode`: `claude` lives under a per-user PATH entry that
1023
- // only an interactive login shell resolves (see that function's comment).
1081
+ // (admin's explicit choice). Wrapped in the login shell (`remoteLoginShellCommand`) for
1082
+ // the same reason as `defaultRemoteCommandForMode`: `claude` lives under a per-user PATH
1083
+ // entry that only an interactive login shell resolves (see that function's comment).
1024
1084
  const override = remote.commands?.[mode];
1025
1085
  let modeCommand;
1026
1086
  if (override) {
@@ -1085,16 +1145,24 @@ export function buildRemoteLaunchCommand(options) {
1085
1145
  modeCommand = defaultRemoteCommandForMode(mode);
1086
1146
  }
1087
1147
  const remoteName = remoteTmuxSessionName(sessionId);
1088
- // Innermost: the command tmux runs in the new pane. Run via `/bin/sh -c` by
1089
- // tmux, so the path needs shell-quoting here. `exec` replaces the shell with
1090
- // the CLI so the pane PID is the agent itself.
1148
+ // Innermost: the command tmux runs in the new pane. `exec` replaces the shell
1149
+ // with the CLI so the pane PID is the agent itself.
1150
+ //
1151
+ // tmux runs a ONE-argument command through the remote default-shell, which is
1152
+ // the user's login shell, and bypasses it for two or more arguments (both
1153
+ // measured on tmux 3.4). Codeman's own text is POSIX (`"${SHELL:-/bin/sh}"` is
1154
+ // a parse error in fish, status 127, a dead pane on every new session), so it
1155
+ // goes to `/bin/sh -c` as separate arguments and no login shell has to parse it.
1156
+ // An override is the user's own text for THEIR shell: it stays one argument,
1157
+ // because `source` and `[[` work in bash and zsh and die in a dash `/bin/sh`.
1091
1158
  const paneCommand = `cd ${shellescape(remote.remotePath)} && ${modeCommand}`;
1159
+ const paneArgv = `${override ? '' : '/bin/sh -c '}${shellescape(paneCommand)}`;
1092
1160
  // The tmux command line, with `\;` separating commands so the config `set`s
1093
1161
  // apply on the SAME connection (and are idempotent on reattach). Options are
1094
1162
  // scoped per-session (`set -t <name>` / server `set -s`), NEVER `-g`, so a
1095
1163
  // shared remote tmux server's other sessions keep their own prefix/mouse.
1096
1164
  const tmuxInvocation = [
1097
- `tmux -L ${REMOTE_TMUX_SOCKET} new-session -A -s ${remoteName} -c ${shellescape(remote.remotePath)} ${shellescape(paneCommand)}`,
1165
+ `tmux -L ${REMOTE_TMUX_SOCKET} new-session -A -s ${remoteName} -c ${shellescape(remote.remotePath)} ${paneArgv}`,
1098
1166
  `set -t ${remoteName} status off`,
1099
1167
  `set -t ${remoteName} mouse off`,
1100
1168
  `set -t ${remoteName} prefix ${NESTED_TMUX_PREFIX}`,
@@ -1243,7 +1311,7 @@ function claudeDockerPaneCommand(modeCommand, sessionId, resumeId) {
1243
1311
  * Build the ONE `bash -c` launch string for a docker session: image-check ->
1244
1312
  * ensure (inspect-or-create) -> start -> `exec docker exec -it` into the durable
1245
1313
  * in-container tmux (resume-aware). PURE and unit-testable. The escaping survives
1246
- * four layers: outer `bash -c "…"` (JSON.stringify at respawn-pane) -> the joined
1314
+ * four layers: outer `bash -c '…'` (`bashCArg()` at respawn-pane) -> the joined
1247
1315
  * command -> `docker exec … sh -lc '<tmux>'` -> tmux `'<paneCommand>'`.
1248
1316
  */
1249
1317
  export function buildDockerLaunchCommand(opts) {
@@ -1270,8 +1338,8 @@ export function buildDockerLaunchCommand(opts) {
1270
1338
  else if (resumeSessionId) {
1271
1339
  modeCommand = appendResumeFlag(modeCommand, mode, resumeSessionId);
1272
1340
  }
1273
- // Run by tmux via /bin/sh -c, so the path is shell-quoted here. `exec` makes the
1274
- // pane PID the agent itself.
1341
+ // Run by tmux through the container's default-shell, so the path is shell-quoted
1342
+ // here. `exec` makes the pane PID the agent itself.
1275
1343
  const paneCommand = `cd ${workdir} && ${modeCommand}`;
1276
1344
  // `setenv -g` primes the session id so reattaches / newly-created panes inherit
1277
1345
  // it. `new-session -A` = attach-or-create (idempotent + resume-aware). Options
@@ -1325,11 +1393,10 @@ export function buildDockerLaunchCommand(opts) {
1325
1393
  const ensure = adopted
1326
1394
  ? `${base} inspect ${name} >/dev/null 2>&1 || { echo ${notFoundMsg}; exit 1; }`
1327
1395
  : `${base} inspect ${name} >/dev/null 2>&1 || ${createCommand}`;
1328
- // ⚠️ No double quotes and no `$(…)` in the ADOPTED arms. This whole chain is
1329
- // embedded in an outer `bash -c "…"`, so an unescaped `"` closes that string early,
1330
- // the rest is re-tokenized, and tmux fails to exec with a bare `execvp(3) failed`.
1331
- // A `grep -qx` pipeline reads the same answer using only the single-quoted form
1332
- // every other line in this builder already uses.
1396
+ // A `grep -qx` pipeline, in the single-quoted form every other line in this builder
1397
+ // uses. This chain reaches `bash -c` verbatim now (`bashCArg()`), but it went through
1398
+ // a double-quoted layer for a long time, where a `"` closed that string early and tmux
1399
+ // failed with a bare `execvp(3) failed`, so keep it free of double quotes and `$(…)`.
1333
1400
  const start = adopted
1334
1401
  ? `${base} inspect -f ${shellescape('{{.State.Running}}')} ${name} 2>/dev/null | grep -qx true || { echo ${notRunningMsg}; exit 1; }`
1335
1402
  : `${base} start ${name} >/dev/null 2>&1 || { echo ${startFailMsg}; exit 1; }`;
@@ -1524,6 +1591,44 @@ function buildRemoteSessionCommand(options) {
1524
1591
  }
1525
1592
  return buildRemoteLaunchCommand(options);
1526
1593
  }
1594
+ /**
1595
+ * ADOPTION: the pane command for a wrapper session around a tmux session a
1596
+ * human started. Sibling of `buildRemoteSessionCommand` and
1597
+ * `buildDockerLaunchCommand`, and the same shape of decision: the wrapper is an
1598
+ * ordinary `codeman-<8hex>` session on OUR socket, and only what runs INSIDE its
1599
+ * pane differs per location.
1600
+ *
1601
+ * That indirection is what makes adoption safe by construction rather than by
1602
+ * discipline: `killSession` only ever kills the wrapper on our own socket, so
1603
+ * there is no shape of caller bug that reaches the foreign server with a
1604
+ * `kill-session`. It is the same reasoning that keeps a non-owned REMOTE session
1605
+ * safe (see the detach-not-kill early return in `killSession`).
1606
+ */
1607
+ export function buildAdoptSessionCommand(adopt, sessionId) {
1608
+ const target = {
1609
+ socketPath: adopt.socketPath,
1610
+ targetSession: adopt.targetSession,
1611
+ // Derived rather than required: the view name is a function of the Codeman
1612
+ // session id, so a record that predates it (or one whose creator filled it in
1613
+ // after construction) still produces the right name instead of an empty `-s`.
1614
+ viewSession: adopt.viewSession || foreignViewSessionName(sessionId),
1615
+ };
1616
+ if (adopt.location === 'docker' && adopt.docker) {
1617
+ return buildForeignDockerAttachCommand({
1618
+ ...target,
1619
+ dockerBase: buildDockerBaseArgs(adopt.docker).join(' '),
1620
+ containerName: adopt.docker.containerName,
1621
+ });
1622
+ }
1623
+ if (adopt.location === 'remote' && adopt.remote) {
1624
+ return buildForeignRemoteAttachCommand({
1625
+ ...target,
1626
+ sshArgs: buildSshConnectionArgs(adopt.remote),
1627
+ sshTarget: remoteSshTarget(adopt.remote),
1628
+ });
1629
+ }
1630
+ return buildForeignAttachCommand(target);
1631
+ }
1527
1632
  /**
1528
1633
  * Push one environment variable into a tmux session with `setenv`.
1529
1634
  *
@@ -1674,6 +1779,13 @@ export class TmuxManager extends EventEmitter {
1674
1779
  * raw pane reading.
1675
1780
  */
1676
1781
  paneExits = new Map();
1782
+ /**
1783
+ * muxName -> `#{pane_current_path}` from the last authoritative pane read
1784
+ * ({@link derivePaneCurrentPaths}). Replaced wholesale by each non-empty read,
1785
+ * so it is bounded by the panes on the socket. Like {@link paneExits} it holds
1786
+ * the raw reading; `Session.setPaneCwd()` decides which sessions may use it.
1787
+ */
1788
+ paneCurrentPaths = new Map();
1677
1789
  /** The pane-exit watcher's own interval. Runs whether or not stats are on. */
1678
1790
  paneExitInterval = null;
1679
1791
  /**
@@ -2068,7 +2180,7 @@ export class TmuxManager extends EventEmitter {
2068
2180
  * In test mode: creates an in-memory session only (no real tmux session).
2069
2181
  */
2070
2182
  async createSession(options) {
2071
- const { sessionId, workingDir, mode, name, cliName, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig, copilotConfig, deepSeekConfig, ompConfig, resumeSessionId, envOverrides, effort, advisorModel, keepWarm, spawnedByAgent, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, docker, owner, } = options;
2183
+ const { sessionId, workingDir, mode, name, cliName, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig, copilotConfig, deepSeekConfig, ompConfig, resumeSessionId, envOverrides, effort, advisorModel, keepWarm, spawnedByAgent, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, docker, adopt, owner, } = options;
2072
2184
  const muxName = `codeman-${sessionId.slice(0, 8)}`;
2073
2185
  if (!isValidMuxName(muxName)) {
2074
2186
  throw new Error('Invalid session name: contains unsafe characters');
@@ -2086,6 +2198,7 @@ export class TmuxManager extends EventEmitter {
2086
2198
  workingDir,
2087
2199
  remote,
2088
2200
  docker,
2201
+ adopt,
2089
2202
  owner,
2090
2203
  mode,
2091
2204
  attached: false,
@@ -2111,14 +2224,22 @@ export class TmuxManager extends EventEmitter {
2111
2224
  // surfacing as a bare `execvp(3) failed: No such file or directory` with nothing
2112
2225
  // pointing at the real cause. The container's own CLIs are verified by the adoption
2113
2226
  // preflight / image gate before launch instead.
2114
- const cliRunsInContainer = !!docker;
2227
+ //
2228
+ // ⚠️ Skipped for an ADOPTED session for the mirror-image reason: its CLI was started by
2229
+ // a human in a process we did not spawn, and our pane only runs `tmux attach`. Requiring
2230
+ // the binary here would reject adopting a claude that lives in a container or on an ssh
2231
+ // host, and (worse) reject a LOCAL adoption whenever the agent is outside the server
2232
+ // process's PATH, which is the usual systemd/launchd situation.
2233
+ const cliRunsInContainer = !!docker || !!adopt;
2115
2234
  if (!cliRunsInContainer && !cliDir) {
2116
2235
  const message = missingCliMessage(mode);
2117
2236
  if (message)
2118
2237
  throw new Error(message);
2119
2238
  }
2120
2239
  const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
2121
- const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir, !!(remote || docker));
2240
+ const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir,
2241
+ // An adopted wrapper launches no agent, so nothing can carry the exporter.
2242
+ !!(remote || docker || adopt));
2122
2243
  const idleCompactionOff = await this._resolveIdleCompactionOptOut(mode, statusLine, keepWarm, spawnedByAgent);
2123
2244
  // The user's own REAL statusLine, if any (walked via Claude Code's own
2124
2245
  // settings precedence) — exported below so the shared exporter script
@@ -2151,11 +2272,21 @@ export class TmuxManager extends EventEmitter {
2151
2272
  try {
2152
2273
  // Build the full command to run inside tmux
2153
2274
  const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
2154
- const fullCmd = docker
2155
- ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
2156
- : remote
2157
- ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId })
2158
- : localFullCmd;
2275
+ const fullCmd = adopt
2276
+ ? buildAdoptSessionCommand(adopt, sessionId)
2277
+ : docker
2278
+ ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
2279
+ : remote
2280
+ ? buildRemoteSessionCommand({
2281
+ mode,
2282
+ remote,
2283
+ sessionId,
2284
+ claudeMode,
2285
+ allowedTools,
2286
+ ompConfig,
2287
+ resumeSessionId,
2288
+ })
2289
+ : localFullCmd;
2159
2290
  // Create tmux session in three steps to handle cold-start (no server running)
2160
2291
  // and avoid the race where the command exits before remain-on-exit is set:
2161
2292
  // 2. Set remain-on-exit (server now exists, session won't vanish on exit)
@@ -2205,8 +2336,10 @@ export class TmuxManager extends EventEmitter {
2205
2336
  this.applyEnvOverrides(muxName, envOverrides);
2206
2337
  // Replace the shell with the actual command (no echo in terminal). Keep
2207
2338
  // pane launch in /tmp, then cd inside bash against the current mount table.
2208
- const launchCmd = remote || docker ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
2209
- execSync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`, {
2339
+ // ⚠️ An adopted pane is never `cd`'d: it only attaches, and its `workingDir`
2340
+ // is the neutral wrapper folder, not where the foreign pane is.
2341
+ const launchCmd = remote || docker || adopt ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
2342
+ execSync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${bashCArg(launchCmd, !!(remote || docker || adopt))}`, {
2210
2343
  timeout: EXEC_TIMEOUT_MS,
2211
2344
  stdio: 'ignore',
2212
2345
  });
@@ -2260,6 +2393,7 @@ export class TmuxManager extends EventEmitter {
2260
2393
  workingDir,
2261
2394
  remote,
2262
2395
  docker,
2396
+ adopt,
2263
2397
  owner,
2264
2398
  mode,
2265
2399
  attached: false,
@@ -2357,13 +2491,20 @@ export class TmuxManager extends EventEmitter {
2357
2491
  const session = this.sessions.get(sessionId);
2358
2492
  if (!session)
2359
2493
  return null;
2494
+ // Adoption metadata comes from the tracked session, not the caller: a respawn
2495
+ // of an adopted wrapper RE-ATTACHES to the same foreign target. It must never
2496
+ // be able to become "launch a fresh agent here" because a caller forgot to
2497
+ // pass the field through.
2498
+ const adopt = options.adopt ?? session.adopt;
2360
2499
  const muxName = session.muxName;
2361
2500
  if (!isValidMuxName(muxName) || !isValidPath(workingDir))
2362
2501
  return null;
2363
2502
  // Resolve CLI binary directory based on mode
2364
2503
  const { pathExport } = this.buildPathExport(mode);
2365
2504
  const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
2366
- const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir, !!(remote || docker));
2505
+ const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir,
2506
+ // An adopted wrapper launches no agent, so nothing can carry the exporter.
2507
+ !!(remote || docker || adopt));
2367
2508
  const idleCompactionOff = await this._resolveIdleCompactionOptOut(mode, statusLine, keepWarm, spawnedByAgent);
2368
2509
  const userStatusLineCommand = statusLineCommand ? await findEffectiveUserStatusLineCommand(workingDir) : undefined;
2369
2510
  const baseCmd = buildSpawnCommand({
@@ -2391,11 +2532,13 @@ export class TmuxManager extends EventEmitter {
2391
2532
  const config = niceConfig || DEFAULT_NICE_CONFIG;
2392
2533
  const cmd = wrapWithNice(baseCmd, config);
2393
2534
  const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
2394
- const fullCmd = docker
2395
- ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
2396
- : remote
2397
- ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId })
2398
- : localFullCmd;
2535
+ const fullCmd = adopt
2536
+ ? buildAdoptSessionCommand(adopt, sessionId)
2537
+ : docker
2538
+ ? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
2539
+ : remote
2540
+ ? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId })
2541
+ : localFullCmd;
2399
2542
  try {
2400
2543
  // Same per-CLI env setup as createSession, re-applied so the respawned pane inherits it.
2401
2544
  this._configureCliEnv(muxName, sessionId, mode, legacyConfigForMode(mode, options));
@@ -2404,8 +2547,10 @@ export class TmuxManager extends EventEmitter {
2404
2547
  // dropping the ones the caller retired first (see applyEnvOverrides).
2405
2548
  this.applyEnvOverrides(muxName, envOverrides, unsetEnvKeys);
2406
2549
  // -c /tmp + cd bounce — see createSession() for rationale (stale FUSE state).
2407
- const launchCmd = remote || docker ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
2408
- await execAsync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`, {
2550
+ // ⚠️ An adopted pane is never `cd`'d: it only attaches, and its `workingDir`
2551
+ // is the neutral wrapper folder, not where the foreign pane is.
2552
+ const launchCmd = remote || docker || adopt ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
2553
+ await execAsync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${bashCArg(launchCmd, !!(remote || docker || adopt))}`, {
2409
2554
  timeout: EXEC_TIMEOUT_MS,
2410
2555
  });
2411
2556
  // Wait for the respawned process to start
@@ -2605,6 +2750,56 @@ export class TmuxManager extends EventEmitter {
2605
2750
  // for a non-owned session. The only `kill-session` we run is on OUR LOCAL
2606
2751
  // socket (`this.tmux()` = `tmux -L codeman` on THIS host), which kills the
2607
2752
  // local pane — it does NOT reach the REMOTE socket.
2753
+ // ADOPTION: DETACH-NOT-KILL, by construction rather than by discipline.
2754
+ //
2755
+ // An adopted session's wrapper is an ordinary session on OUR socket whose
2756
+ // pane runs `tmux attach` (or `docker exec …`/`ssh …` then attach) against a
2757
+ // FOREIGN server. Killing the wrapper kills our attach client, which is a
2758
+ // DETACH on the foreign side; the human's session keeps running, and our
2759
+ // grouped view session self-destroys via `destroy-unattached on`.
2760
+ //
2761
+ // The guard below is a formality more than a fence: `this.tmux()` only ever
2762
+ // addresses our own socket, so no code path here CAN reach the foreign
2763
+ // server. It is written out anyway so the intent survives future edits, the
2764
+ // same way the non-owned-remote early return does.
2765
+ if (session.adopt) {
2766
+ console.log(`[TmuxManager] DETACH (adopted): tearing down wrapper only for ${session.muxName}`);
2767
+ // A `docker exec` survives its client, so the in-container view would stay
2768
+ // attached forever and leak. Best-effort, and it can only reach OUR view.
2769
+ // See buildForeignDockerViewKillCommand for why local/remote need nothing.
2770
+ // Fire and forget like the owned docker branch below: a daemon that stopped
2771
+ // answering must not hold the event loop for the whole timeout.
2772
+ if (session.adopt.location === 'docker' && session.adopt.docker) {
2773
+ try {
2774
+ exec(buildForeignDockerViewKillCommand({
2775
+ dockerBase: buildDockerBaseArgs(session.adopt.docker).join(' '),
2776
+ containerName: session.adopt.docker.containerName,
2777
+ socketPath: session.adopt.socketPath,
2778
+ viewSession: session.adopt.viewSession || foreignViewSessionName(sessionId),
2779
+ }), { timeout: EXEC_TIMEOUT_MS }, () => { });
2780
+ }
2781
+ catch {
2782
+ // Best-effort: the container may be gone, or the view already collected.
2783
+ }
2784
+ }
2785
+ if (isValidMuxName(session.muxName)) {
2786
+ try {
2787
+ await execAsync(`${this.tmux()} kill-session -t "${session.muxName}" 2>/dev/null`, {
2788
+ timeout: EXEC_TIMEOUT_MS,
2789
+ });
2790
+ }
2791
+ catch {
2792
+ // Wrapper may already be gone.
2793
+ }
2794
+ }
2795
+ this.lastPaneCount.delete(session.muxName);
2796
+ this.clearPaneExit(session.muxName);
2797
+ this.sessions.delete(sessionId);
2798
+ this.clearRemoteReconnectState(sessionId);
2799
+ this.saveSessions();
2800
+ this.emit('sessionKilled', { sessionId });
2801
+ return true;
2802
+ }
2608
2803
  if (session.remote && session.remote.owned === false) {
2609
2804
  console.log(`[TmuxManager] DETACH (non-owned remote): tearing down local pane only for ${session.muxName}`);
2610
2805
  if (isValidMuxName(session.muxName)) {
@@ -3002,6 +3197,18 @@ export class TmuxManager extends EventEmitter {
3002
3197
  stats: statsMap.get(session.pid) || undefined,
3003
3198
  }));
3004
3199
  }
3200
+ /**
3201
+ * The directory the session's pane was in at the last authoritative pane
3202
+ * read, or `undefined` when unknown. Costs nothing: it reads the map the
3203
+ * pane-exit watcher's batched `list-panes -a` fills, so no tmux call is made
3204
+ * per click. A failed or empty read keeps the previous answers, and so does
3205
+ * a blank `#{pane_current_path}` on a single-pane session. After a successful
3206
+ * read, `undefined` means that read listed no single directory for the
3207
+ * session: it was split into several panes or is no longer on the socket.
3208
+ */
3209
+ getPaneCurrentPath(muxName) {
3210
+ return this.paneCurrentPaths.get(muxName);
3211
+ }
3005
3212
  /**
3006
3213
  * What the last pane read saw of this tmux session's agent. `undefined` is
3007
3214
  * the UNKNOWN answer and must never be rendered as "alive": it covers a pane
@@ -3064,6 +3271,11 @@ export class TmuxManager extends EventEmitter {
3064
3271
  }
3065
3272
  if (rows.length === 0)
3066
3273
  return;
3274
+ // The directory reading rides the same read. It is applied before the
3275
+ // generation check below: a respawn invalidates an exit observation, but the
3276
+ // pane's directory at read time is still the best answer there is. A blank
3277
+ // field on a single-pane session carries the previous answer forward.
3278
+ this.paneCurrentPaths = derivePaneCurrentPaths(rows, this.paneCurrentPaths);
3067
3279
  // A pane was respawned or killed while this read was out, so what it saw is
3068
3280
  // already history. Dropping it is what stops a freshly respawned pane from
3069
3281
  // being republished as exited.
@@ -3266,8 +3478,51 @@ export class TmuxManager extends EventEmitter {
3266
3478
  catch (err) {
3267
3479
  console.error('[TmuxManager] Remote reconnect watcher error:', err);
3268
3480
  }
3481
+ void this.detachEndedAdoptedAttaches().catch((err) => {
3482
+ console.error('[TmuxManager] Adopted attach watcher error:', err);
3483
+ });
3269
3484
  }, intervalMs);
3270
3485
  }
3486
+ /**
3487
+ * Let go of a wrapper whose pane died (#510). That pane holds the attach to someone
3488
+ * else's tmux session, so it dies when that session ends, the transport drops or the
3489
+ * view gets detached, and `paneExit` rightly reports none of them. But `remain-on-exit`
3490
+ * keeps Codeman's own tmux client on the dead pane, so the tab kept its pid and `busy`,
3491
+ * input went nowhere and both attach routes refused it.
3492
+ *
3493
+ * Detaching that client is an ordinary PTY exit with status 0: the pid goes null, the tab
3494
+ * stays open, and selecting it starts the wrapper again, which joins the target if it is
3495
+ * back and otherwise prints that it is gone. Only Codeman's own socket is touched.
3496
+ */
3497
+ async detachEndedAdoptedAttaches() {
3498
+ const adopted = new Set();
3499
+ for (const session of this.sessions.values()) {
3500
+ if (session.adopt)
3501
+ adopted.add(session.muxName);
3502
+ }
3503
+ if (adopted.size === 0)
3504
+ return;
3505
+ for (const muxName of endedAdoptedAttaches(await this.readAttachRows(), adopted)) {
3506
+ await this.detachClients(muxName);
3507
+ }
3508
+ }
3509
+ /** Rows for {@link endedAdoptedAttaches}. Empty when tmux did not answer. */
3510
+ async readAttachRows() {
3511
+ if (IS_TEST_MODE)
3512
+ return '';
3513
+ const format = '#{pane_dead} #{session_attached} #{session_name}';
3514
+ const { stdout } = await execAsync(`${this.tmux()} list-panes -a -F '${format}' 2>/dev/null || true`, {
3515
+ encoding: 'utf-8',
3516
+ timeout: EXEC_TIMEOUT_MS,
3517
+ });
3518
+ return stdout;
3519
+ }
3520
+ /** Detach every tmux client from one of Codeman's own sessions. */
3521
+ async detachClients(muxName) {
3522
+ if (IS_TEST_MODE || !isValidMuxName(muxName))
3523
+ return;
3524
+ await execAsync(`${this.tmux()} detach-client -s "${muxName}"`, { timeout: EXEC_TIMEOUT_MS });
3525
+ }
3271
3526
  stopRemoteReconnectWatcher() {
3272
3527
  if (this.remoteReconnectInterval) {
3273
3528
  clearInterval(this.remoteReconnectInterval);
@@ -4037,6 +4292,28 @@ export class TmuxManager extends EventEmitter {
4037
4292
  }
4038
4293
  }
4039
4294
  }
4295
+ /**
4296
+ * The `bash -c` argument of a pane launch (`respawn-pane … bash -c <arg>`, run through the
4297
+ * local sh). A remote, docker or adopted command is already quoted for the shell at the far
4298
+ * end and must reach bash verbatim, so it goes in single quotes. Double quotes
4299
+ * (JSON.stringify) let the local sh expand every `$` first: the PATH bootstrap of
4300
+ * `withRemoteTmuxPath` (`$HOME/.local/bin:…:$PATH`) arrived on the remote host as this
4301
+ * host's home and PATH, so a remote user with another home found no tmux and every new
4302
+ * remote pane exited 127. `${SHELL:-/bin/sh}` in the remote launch was the local shell for
4303
+ * the same reason, and an adopted ssh attach carries the same bootstrap, so a tmux in the
4304
+ * remote `~/.local/bin` was found by discovery and then missed by the attach. Single quotes
4305
+ * also keep a real newline, which JSON escaping left as the two characters `\n`.
4306
+ * An adopted attach goes verbatim at all three locations, the local one included: every
4307
+ * value in it is already quoted by `foreign-tmux.ts`, and nothing in it is for this shell.
4308
+ * An agent launched on this host keeps its double-quoted form.
4309
+ *
4310
+ * ⚠️ So nothing in a verbatim launch is expanded on this host any more, the docker client's
4311
+ * own arguments included: a bare `$VAR` in `extraCreateArgs`/`extraExecArgs` reaches docker
4312
+ * as those literal characters.
4313
+ */
4314
+ export function bashCArg(launchCmd, verbatim) {
4315
+ return verbatim ? shellescape(launchCmd) : JSON.stringify(launchCmd);
4316
+ }
4040
4317
  /**
4041
4318
  * Shell-escape a string for use as a single argument.
4042
4319
  * Wraps in single quotes, escaping any embedded single quotes.