aicodeman 1.43.0 → 1.45.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 (353) hide show
  1. package/README.md +9 -4
  2. package/dist/case-rename.d.ts +65 -0
  3. package/dist/case-rename.d.ts.map +1 -0
  4. package/dist/case-rename.js +198 -0
  5. package/dist/case-rename.js.map +1 -0
  6. package/dist/cli-agent.d.ts +63 -4
  7. package/dist/cli-agent.d.ts.map +1 -1
  8. package/dist/cli-agent.js +233 -19
  9. package/dist/cli-agent.js.map +1 -1
  10. package/dist/config/auth-config.d.ts +16 -1
  11. package/dist/config/auth-config.d.ts.map +1 -1
  12. package/dist/config/auth-config.js +16 -1
  13. package/dist/config/auth-config.js.map +1 -1
  14. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  15. package/dist/config/cli-registry/stock.js +8 -3
  16. package/dist/config/cli-registry/stock.js.map +1 -1
  17. package/dist/config/cli-registry/types.d.ts +2 -2
  18. package/dist/config/map-limits.d.ts +13 -1
  19. package/dist/config/map-limits.d.ts.map +1 -1
  20. package/dist/config/map-limits.js +28 -1
  21. package/dist/config/map-limits.js.map +1 -1
  22. package/dist/config-backup.d.ts +279 -0
  23. package/dist/config-backup.d.ts.map +1 -0
  24. package/dist/config-backup.js +794 -0
  25. package/dist/config-backup.js.map +1 -0
  26. package/dist/cron/cron-service.d.ts +5 -5
  27. package/dist/cron/cron-service.js +6 -6
  28. package/dist/cron/cron-service.js.map +1 -1
  29. package/dist/custom-model-hosts.d.ts +16 -0
  30. package/dist/custom-model-hosts.d.ts.map +1 -1
  31. package/dist/custom-model-hosts.js.map +1 -1
  32. package/dist/hooks-config.d.ts +15 -0
  33. package/dist/hooks-config.d.ts.map +1 -1
  34. package/dist/hooks-config.js +35 -0
  35. package/dist/hooks-config.js.map +1 -1
  36. package/dist/host-sensors.d.ts +115 -0
  37. package/dist/host-sensors.d.ts.map +1 -0
  38. package/dist/host-sensors.js +457 -0
  39. package/dist/host-sensors.js.map +1 -0
  40. package/dist/intent-store.d.ts +2 -0
  41. package/dist/intent-store.d.ts.map +1 -1
  42. package/dist/intent-store.js +9 -0
  43. package/dist/intent-store.js.map +1 -1
  44. package/dist/mux-interface.d.ts +21 -4
  45. package/dist/mux-interface.d.ts.map +1 -1
  46. package/dist/prompt-cache-keepwarm.d.ts +193 -0
  47. package/dist/prompt-cache-keepwarm.d.ts.map +1 -0
  48. package/dist/prompt-cache-keepwarm.js +359 -0
  49. package/dist/prompt-cache-keepwarm.js.map +1 -0
  50. package/dist/remote-hosts.d.ts +7 -0
  51. package/dist/remote-hosts.d.ts.map +1 -1
  52. package/dist/remote-hosts.js +17 -3
  53. package/dist/remote-hosts.js.map +1 -1
  54. package/dist/remote-pane-history.d.ts.map +1 -1
  55. package/dist/remote-pane-history.js +2 -2
  56. package/dist/remote-pane-history.js.map +1 -1
  57. package/dist/respawn-controller.d.ts.map +1 -1
  58. package/dist/respawn-controller.js +6 -5
  59. package/dist/respawn-controller.js.map +1 -1
  60. package/dist/session-auto-ops.d.ts.map +1 -1
  61. package/dist/session-auto-ops.js +6 -1
  62. package/dist/session-auto-ops.js.map +1 -1
  63. package/dist/session-cli-registry-bridge.d.ts +13 -0
  64. package/dist/session-cli-registry-bridge.d.ts.map +1 -1
  65. package/dist/session-cli-registry-bridge.js +20 -3
  66. package/dist/session-cli-registry-bridge.js.map +1 -1
  67. package/dist/session-submit-verifier.d.ts +19 -0
  68. package/dist/session-submit-verifier.d.ts.map +1 -1
  69. package/dist/session-submit-verifier.js +88 -0
  70. package/dist/session-submit-verifier.js.map +1 -1
  71. package/dist/session.d.ts +61 -5
  72. package/dist/session.d.ts.map +1 -1
  73. package/dist/session.js +125 -7
  74. package/dist/session.js.map +1 -1
  75. package/dist/state-store.d.ts +10 -0
  76. package/dist/state-store.d.ts.map +1 -1
  77. package/dist/state-store.js +35 -0
  78. package/dist/state-store.js.map +1 -1
  79. package/dist/tab-layout-service.d.ts +33 -0
  80. package/dist/tab-layout-service.d.ts.map +1 -1
  81. package/dist/tab-layout-service.js +75 -3
  82. package/dist/tab-layout-service.js.map +1 -1
  83. package/dist/tmux-manager.d.ts +57 -11
  84. package/dist/tmux-manager.d.ts.map +1 -1
  85. package/dist/tmux-manager.js +190 -42
  86. package/dist/tmux-manager.js.map +1 -1
  87. package/dist/tmux-prefix-table.d.ts +86 -0
  88. package/dist/tmux-prefix-table.d.ts.map +1 -0
  89. package/dist/tmux-prefix-table.js +172 -0
  90. package/dist/tmux-prefix-table.js.map +1 -0
  91. package/dist/tui/tui-app.d.ts +17 -2
  92. package/dist/tui/tui-app.d.ts.map +1 -1
  93. package/dist/tui/tui-app.js +70 -20
  94. package/dist/tui/tui-app.js.map +1 -1
  95. package/dist/tui/tui-client.d.ts +93 -5
  96. package/dist/tui/tui-client.d.ts.map +1 -1
  97. package/dist/tui/tui-client.js +198 -28
  98. package/dist/tui/tui-client.js.map +1 -1
  99. package/dist/types/app-state.d.ts +6 -0
  100. package/dist/types/app-state.d.ts.map +1 -1
  101. package/dist/types/app-state.js.map +1 -1
  102. package/dist/types/session.d.ts +40 -1
  103. package/dist/types/session.d.ts.map +1 -1
  104. package/dist/types/session.js.map +1 -1
  105. package/dist/usage-telemetry.d.ts +15 -0
  106. package/dist/usage-telemetry.d.ts.map +1 -1
  107. package/dist/usage-telemetry.js +45 -2
  108. package/dist/usage-telemetry.js.map +1 -1
  109. package/dist/utils/copilot-plan-usage.d.ts +46 -0
  110. package/dist/utils/copilot-plan-usage.d.ts.map +1 -0
  111. package/dist/utils/copilot-plan-usage.js +95 -0
  112. package/dist/utils/copilot-plan-usage.js.map +1 -0
  113. package/dist/utils/index.d.ts +2 -0
  114. package/dist/utils/index.d.ts.map +1 -1
  115. package/dist/utils/index.js +2 -0
  116. package/dist/utils/index.js.map +1 -1
  117. package/dist/utils/single-line.d.ts +22 -0
  118. package/dist/utils/single-line.d.ts.map +1 -0
  119. package/dist/utils/single-line.js +30 -0
  120. package/dist/utils/single-line.js.map +1 -0
  121. package/dist/web/agent-inbox-persistence.d.ts +52 -0
  122. package/dist/web/agent-inbox-persistence.d.ts.map +1 -0
  123. package/dist/web/agent-inbox-persistence.js +139 -0
  124. package/dist/web/agent-inbox-persistence.js.map +1 -0
  125. package/dist/web/agent-inbox.d.ts +140 -0
  126. package/dist/web/agent-inbox.d.ts.map +1 -0
  127. package/dist/web/agent-inbox.js +258 -0
  128. package/dist/web/agent-inbox.js.map +1 -0
  129. package/dist/web/approval-inbox.d.ts +6 -0
  130. package/dist/web/approval-inbox.d.ts.map +1 -1
  131. package/dist/web/approval-inbox.js +2 -2
  132. package/dist/web/approval-inbox.js.map +1 -1
  133. package/dist/web/middleware/auth.d.ts +2 -0
  134. package/dist/web/middleware/auth.d.ts.map +1 -1
  135. package/dist/web/middleware/auth.js +145 -52
  136. package/dist/web/middleware/auth.js.map +1 -1
  137. package/dist/web/plan-usage-latest.d.ts +2 -1
  138. package/dist/web/plan-usage-latest.d.ts.map +1 -1
  139. package/dist/web/plan-usage-latest.js +12 -2
  140. package/dist/web/plan-usage-latest.js.map +1 -1
  141. package/dist/web/ports/index.d.ts +1 -0
  142. package/dist/web/ports/index.d.ts.map +1 -1
  143. package/dist/web/ports/keep-warm-port.d.ts +9 -0
  144. package/dist/web/ports/keep-warm-port.d.ts.map +1 -0
  145. package/dist/web/ports/keep-warm-port.js +6 -0
  146. package/dist/web/ports/keep-warm-port.js.map +1 -0
  147. package/dist/web/ports/session-port.d.ts +3 -1
  148. package/dist/web/ports/session-port.d.ts.map +1 -1
  149. package/dist/web/public/admin-ui.js.gz +0 -0
  150. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  151. package/dist/web/public/app.42d0abae.js +43 -0
  152. package/dist/web/public/app.42d0abae.js.br +0 -0
  153. package/dist/web/public/app.42d0abae.js.gz +0 -0
  154. package/dist/web/public/approvals-ui.js.gz +0 -0
  155. package/dist/web/public/{constants.e5e588db.js → constants.0b3cdde9.js} +60 -1
  156. package/dist/web/public/constants.0b3cdde9.js.br +0 -0
  157. package/dist/web/public/constants.0b3cdde9.js.gz +0 -0
  158. package/dist/web/public/cron-ui.js.gz +0 -0
  159. package/dist/web/public/entrance-animations.js.gz +0 -0
  160. package/dist/web/public/git-status-ui.js.gz +0 -0
  161. package/dist/web/public/header-divider.eb2d2463.js +1 -0
  162. package/dist/web/public/header-divider.eb2d2463.js.br +0 -0
  163. package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
  164. package/dist/web/public/home-sessions.js.gz +0 -0
  165. package/dist/web/public/host-wake-ui.js.gz +0 -0
  166. package/dist/web/public/i18n.1e68220d.js +2 -0
  167. package/dist/web/public/i18n.1e68220d.js.br +0 -0
  168. package/dist/web/public/i18n.1e68220d.js.gz +0 -0
  169. package/dist/web/public/image-input.941386e7.js.gz +0 -0
  170. package/dist/web/public/index.html +162 -14
  171. package/dist/web/public/index.html.br +0 -0
  172. package/dist/web/public/index.html.gz +0 -0
  173. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  174. package/dist/web/public/{keyboard-accessory.c283e827.js → keyboard-accessory.4e8b20ac.js} +23 -5
  175. package/dist/web/public/keyboard-accessory.4e8b20ac.js.br +0 -0
  176. package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
  177. package/dist/web/public/{mobile-handlers.0ef9c311.js → mobile-handlers.1c0cbcd8.js} +50 -16
  178. package/dist/web/public/mobile-handlers.1c0cbcd8.js.br +0 -0
  179. package/dist/web/public/mobile-handlers.1c0cbcd8.js.gz +0 -0
  180. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  181. package/dist/web/public/mobile-overview.js.gz +0 -0
  182. package/dist/web/public/mobile.12fb18de.css +1 -0
  183. package/dist/web/public/mobile.12fb18de.css.br +0 -0
  184. package/dist/web/public/mobile.12fb18de.css.gz +0 -0
  185. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  186. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  187. package/dist/web/public/{panels-ui.b7bfb9da.js → panels-ui.78c3defb.js} +19 -13
  188. package/dist/web/public/panels-ui.78c3defb.js.br +0 -0
  189. package/dist/web/public/panels-ui.78c3defb.js.gz +0 -0
  190. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  191. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  192. package/dist/web/public/readmymind-ui.js.gz +0 -0
  193. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  194. package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
  195. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  196. package/dist/web/public/session-lineage.js.gz +0 -0
  197. package/dist/web/public/{session-ui.be076c88.js → session-ui.ea6b1994.js} +33 -27
  198. package/dist/web/public/session-ui.ea6b1994.js.br +0 -0
  199. package/dist/web/public/session-ui.ea6b1994.js.gz +0 -0
  200. package/dist/web/public/settings-ui.085dd202.js +106 -0
  201. package/dist/web/public/settings-ui.085dd202.js.br +0 -0
  202. package/dist/web/public/settings-ui.085dd202.js.gz +0 -0
  203. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  204. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  205. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  206. package/dist/web/public/styles.3b59b41d.css +1 -0
  207. package/dist/web/public/styles.3b59b41d.css.br +0 -0
  208. package/dist/web/public/styles.3b59b41d.css.gz +0 -0
  209. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  210. package/dist/web/public/sw.js +2 -2
  211. package/dist/web/public/sw.js.br +0 -0
  212. package/dist/web/public/sw.js.gz +0 -0
  213. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  214. package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
  215. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  216. package/dist/web/public/terminal-split.js.gz +0 -0
  217. package/dist/web/public/terminal-tile.js +49 -23
  218. package/dist/web/public/terminal-tile.js.br +0 -0
  219. package/dist/web/public/terminal-tile.js.gz +0 -0
  220. package/dist/web/public/terminal-ui.fb2a27f5.js +2 -0
  221. package/dist/web/public/terminal-ui.fb2a27f5.js.br +0 -0
  222. package/dist/web/public/terminal-ui.fb2a27f5.js.gz +0 -0
  223. package/dist/web/public/tile-grid.js.gz +0 -0
  224. package/dist/web/public/ultracode-panel.js.gz +0 -0
  225. package/dist/web/public/ultracode-windows.js.gz +0 -0
  226. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  227. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  228. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  229. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  230. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  231. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  232. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  233. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  234. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  235. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
  236. package/dist/web/public/vendor/xterm.css.gz +0 -0
  237. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  238. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  239. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  240. package/dist/web/public/webview-tabs.js.gz +0 -0
  241. package/dist/web/public/whats-new.json +9 -9
  242. package/dist/web/route-helpers.d.ts +57 -6
  243. package/dist/web/route-helpers.d.ts.map +1 -1
  244. package/dist/web/route-helpers.js +72 -4
  245. package/dist/web/route-helpers.js.map +1 -1
  246. package/dist/web/routes/admin-routes.d.ts +2 -1
  247. package/dist/web/routes/admin-routes.d.ts.map +1 -1
  248. package/dist/web/routes/admin-routes.js +3 -0
  249. package/dist/web/routes/admin-routes.js.map +1 -1
  250. package/dist/web/routes/case-routes.d.ts +2 -2
  251. package/dist/web/routes/case-routes.d.ts.map +1 -1
  252. package/dist/web/routes/case-routes.js +263 -4
  253. package/dist/web/routes/case-routes.js.map +1 -1
  254. package/dist/web/routes/config-backup-routes.d.ts +54 -0
  255. package/dist/web/routes/config-backup-routes.d.ts.map +1 -0
  256. package/dist/web/routes/config-backup-routes.js +268 -0
  257. package/dist/web/routes/config-backup-routes.js.map +1 -0
  258. package/dist/web/routes/custom-model-routes.d.ts +17 -1
  259. package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
  260. package/dist/web/routes/custom-model-routes.js +118 -13
  261. package/dist/web/routes/custom-model-routes.js.map +1 -1
  262. package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
  263. package/dist/web/routes/hook-event-routes.js +26 -5
  264. package/dist/web/routes/hook-event-routes.js.map +1 -1
  265. package/dist/web/routes/inbox-routes.d.ts +43 -0
  266. package/dist/web/routes/inbox-routes.d.ts.map +1 -0
  267. package/dist/web/routes/inbox-routes.js +148 -0
  268. package/dist/web/routes/inbox-routes.js.map +1 -0
  269. package/dist/web/routes/index.d.ts +2 -0
  270. package/dist/web/routes/index.d.ts.map +1 -1
  271. package/dist/web/routes/index.js +2 -0
  272. package/dist/web/routes/index.js.map +1 -1
  273. package/dist/web/routes/ralph-routes.js +2 -2
  274. package/dist/web/routes/ralph-routes.js.map +1 -1
  275. package/dist/web/routes/reboot-restore-routes.d.ts.map +1 -1
  276. package/dist/web/routes/reboot-restore-routes.js +2 -0
  277. package/dist/web/routes/reboot-restore-routes.js.map +1 -1
  278. package/dist/web/routes/session-routes.d.ts +2 -2
  279. package/dist/web/routes/session-routes.d.ts.map +1 -1
  280. package/dist/web/routes/session-routes.js +110 -61
  281. package/dist/web/routes/session-routes.js.map +1 -1
  282. package/dist/web/routes/status-telemetry-routes.d.ts +2 -2
  283. package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
  284. package/dist/web/routes/status-telemetry-routes.js +8 -4
  285. package/dist/web/routes/status-telemetry-routes.js.map +1 -1
  286. package/dist/web/routes/system-routes.d.ts +2 -2
  287. package/dist/web/routes/system-routes.d.ts.map +1 -1
  288. package/dist/web/routes/system-routes.js +30 -2
  289. package/dist/web/routes/system-routes.js.map +1 -1
  290. package/dist/web/routes/webview-routes.d.ts.map +1 -1
  291. package/dist/web/routes/webview-routes.js +1 -0
  292. package/dist/web/routes/webview-routes.js.map +1 -1
  293. package/dist/web/schemas.d.ts +45 -0
  294. package/dist/web/schemas.d.ts.map +1 -1
  295. package/dist/web/schemas.js +79 -2
  296. package/dist/web/schemas.js.map +1 -1
  297. package/dist/web/sensitive-path.d.ts.map +1 -1
  298. package/dist/web/sensitive-path.js +2 -0
  299. package/dist/web/sensitive-path.js.map +1 -1
  300. package/dist/web/server.d.ts +14 -2
  301. package/dist/web/server.d.ts.map +1 -1
  302. package/dist/web/server.js +176 -7
  303. package/dist/web/server.js.map +1 -1
  304. package/dist/web/session-listener-wiring.d.ts +6 -0
  305. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  306. package/dist/web/session-listener-wiring.js +10 -3
  307. package/dist/web/session-listener-wiring.js.map +1 -1
  308. package/dist/web/session-wait-registry.d.ts +48 -4
  309. package/dist/web/session-wait-registry.d.ts.map +1 -1
  310. package/dist/web/session-wait-registry.js +144 -6
  311. package/dist/web/session-wait-registry.js.map +1 -1
  312. package/dist/web/sse-events.d.ts +9 -3
  313. package/dist/web/sse-events.d.ts.map +1 -1
  314. package/dist/web/sse-events.js +9 -3
  315. package/dist/web/sse-events.js.map +1 -1
  316. package/dist/web/webview-proxy.d.ts +8 -3
  317. package/dist/web/webview-proxy.d.ts.map +1 -1
  318. package/dist/web/webview-proxy.js +23 -5
  319. package/dist/web/webview-proxy.js.map +1 -1
  320. package/package.json +1 -1
  321. package/skills/codeman/SKILL.md +27 -13
  322. package/skills/codeman/preamble.sh +15 -5
  323. package/skills/codeman/reference/endpoints.md +32 -10
  324. package/skills/codeman/reference/recipes.md +1 -1
  325. package/skills/codeman/reference/verbs.md +68 -13
  326. package/dist/web/public/app.ae50029a.js +0 -43
  327. package/dist/web/public/app.ae50029a.js.br +0 -0
  328. package/dist/web/public/app.ae50029a.js.gz +0 -0
  329. package/dist/web/public/constants.e5e588db.js.br +0 -0
  330. package/dist/web/public/constants.e5e588db.js.gz +0 -0
  331. package/dist/web/public/i18n.8f291f06.js +0 -2
  332. package/dist/web/public/i18n.8f291f06.js.br +0 -0
  333. package/dist/web/public/i18n.8f291f06.js.gz +0 -0
  334. package/dist/web/public/keyboard-accessory.c283e827.js.br +0 -0
  335. package/dist/web/public/keyboard-accessory.c283e827.js.gz +0 -0
  336. package/dist/web/public/mobile-handlers.0ef9c311.js.br +0 -0
  337. package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
  338. package/dist/web/public/mobile.13514863.css +0 -1
  339. package/dist/web/public/mobile.13514863.css.br +0 -0
  340. package/dist/web/public/mobile.13514863.css.gz +0 -0
  341. package/dist/web/public/panels-ui.b7bfb9da.js.br +0 -0
  342. package/dist/web/public/panels-ui.b7bfb9da.js.gz +0 -0
  343. package/dist/web/public/session-ui.be076c88.js.br +0 -0
  344. package/dist/web/public/session-ui.be076c88.js.gz +0 -0
  345. package/dist/web/public/settings-ui.fc4cc50a.js +0 -97
  346. package/dist/web/public/settings-ui.fc4cc50a.js.br +0 -0
  347. package/dist/web/public/settings-ui.fc4cc50a.js.gz +0 -0
  348. package/dist/web/public/styles.dcfa907f.css +0 -1
  349. package/dist/web/public/styles.dcfa907f.css.br +0 -0
  350. package/dist/web/public/styles.dcfa907f.css.gz +0 -0
  351. package/dist/web/public/terminal-ui.ae01a7d1.js +0 -2
  352. package/dist/web/public/terminal-ui.ae01a7d1.js.br +0 -0
  353. package/dist/web/public/terminal-ui.ae01a7d1.js.gz +0 -0
@@ -25,7 +25,7 @@ import { collectDescendants } from './proc-tree.js';
25
25
  import { execSync, exec, execFile } from 'node:child_process';
26
26
  import { promisify } from 'node:util';
27
27
  const execAsync = promisify(exec);
28
- import { existsSync, readFileSync, mkdirSync } from 'node:fs';
28
+ import { existsSync, readFileSync, mkdirSync, realpathSync } from 'node:fs';
29
29
  import { writeFile, rename } from 'node:fs/promises';
30
30
  import { dirname } from 'node:path';
31
31
  import { homedir } from 'node:os';
@@ -33,9 +33,10 @@ import { dataPath, DEFAULT_TMUX_SOCKET, CODEMAN_INSTANCE, SAFE_TMUX_SOCKET_PATTE
33
33
  import { getErrorMessage, DEFAULT_NICE_CONFIG, } from './types.js';
34
34
  import { getCli } from './config/cli-registry/registry.js';
35
35
  import { missingCliMessage, resolveCliBinDir } from './utils/cli-resolver.js';
36
- import { buildSpawnCommandFromRegistry, configSetenvValues, legacyConfigForMode, } from './session-cli-registry-bridge.js';
37
- import { resolveStatusLineCliCommand, readPlanUsageTelemetryEnabled, findEffectiveUserStatusLineCommand, } from './hooks-config.js';
38
- import { buildSshConnectionArgs, defaultRemoteCommandForMode, remoteLoginShellCommand, remoteSshTarget, remoteTmuxSessionAlive, } from './remote-hosts.js';
36
+ import { buildSpawnCommandFromRegistry, configSetenvValues, launchGatePasses, legacyConfigForMode, } from './session-cli-registry-bridge.js';
37
+ import { keepWarmOn } from './prompt-cache-keepwarm.js';
38
+ import { resolveStatusLineCliCommand, readPlanUsageTelemetryEnabled, readKeepWarmSettings, findEffectiveUserStatusLineCommand, } from './hooks-config.js';
39
+ import { buildSshConnectionArgs, defaultRemoteCommandForMode, remoteLoginShellCommand, remoteSshTarget, remoteTmuxSessionAlive, withRemoteTmuxPath, } from './remote-hosts.js';
39
40
  import { buildDockerBaseArgs, buildDockerCreateArgs, containerApiUrl, CONTAINER_HOME, defaultDockerCommandForMode, hostGatewayAlias, resolveDockerClaudeArtifacts, resolveDockerCredentialArtifacts, resolveDockerDaemonMountSource, } from './docker-hosts.js';
40
41
  import { wrapWithNice, SAFE_PATH_PATTERN, resolveLocalShell, loginShellArgs, waitForProcessesExit, } from './utils/index.js';
41
42
  import { decideReconnect, advanceBackoff, freshReconnectState, resetReconnectState, } from './remote-reconnect.js';
@@ -44,6 +45,7 @@ import { decideReconnect, advanceBackoff, freshReconnectState, resetReconnectSta
44
45
  // ============================================================================
45
46
  import { EXEC_TIMEOUT_MS } from './config/exec-timeout.js';
46
47
  import { ensureDeepSeekStatusShim } from './deepseek-status-shim.js';
48
+ import { NESTED_TMUX_PREFIX, nestedPrefixTableResetCommands, restrictTmuxPrefixTable } from './tmux-prefix-table.js';
47
49
  /** How long a cached process snapshot stays usable. */
48
50
  const PROC_SNAPSHOT_TTL_MS = 2000;
49
51
  /**
@@ -89,6 +91,22 @@ const DEFAULT_PANE_EXIT_INTERVAL_MS = 2000;
89
91
  const DEFAULT_REMOTE_RECONNECT_INTERVAL_MS = 5000;
90
92
  /** Stable cwd for tmux server/pane launch; actual session cwd is reached inside the pane. */
91
93
  const TMUX_LAUNCH_CWD = '/tmp';
94
+ /**
95
+ * TMUX_LAUNCH_CWD in every spelling a pane can report it. tmux reads a pane's
96
+ * directory from its process, which comes back resolved (`/private/tmp` on
97
+ * macOS, where `/tmp` is a symlink), so the literal and the resolved spelling
98
+ * both count. Computed once.
99
+ */
100
+ const TMUX_LAUNCH_CWD_SPELLINGS = (() => {
101
+ const spellings = new Set([TMUX_LAUNCH_CWD]);
102
+ try {
103
+ spellings.add(realpathSync(TMUX_LAUNCH_CWD));
104
+ }
105
+ catch {
106
+ // Unresolvable launch directory: the literal spelling is all there is.
107
+ }
108
+ return spellings;
109
+ })();
92
110
  /** Claude Code native macOS recommendation for avoiding low nofile startup failures. */
93
111
  export const CLAUDE_CODE_NOFILE_LIMIT = 2147483646;
94
112
  /**
@@ -150,9 +168,13 @@ const PANE_LIST_SEP = '|';
150
168
  * know a variable substitutes the empty string rather than failing, which is how
151
169
  * tmux 3.2a answers `#{pane_dead_signal}` (added in 3.4), and the parser reads a
152
170
  * short row as "pid known, deadness unknown" rather than discarding it.
171
+ *
172
+ * `#{pane_current_path}` comes last on purpose: a directory name may contain the
173
+ * separator, and only the final field can absorb it without ambiguity.
153
174
  */
154
175
  const PANE_LIST_FORMAT = `#{session_name}${PANE_LIST_SEP}#{pane_pid}` +
155
- `${PANE_LIST_SEP}#{pane_dead}${PANE_LIST_SEP}#{pane_dead_status}${PANE_LIST_SEP}#{pane_dead_signal}`;
176
+ `${PANE_LIST_SEP}#{pane_dead}${PANE_LIST_SEP}#{pane_dead_status}${PANE_LIST_SEP}#{pane_dead_signal}` +
177
+ `${PANE_LIST_SEP}#{pane_current_path}`;
156
178
  /**
157
179
  * 构建 pane 启动前的 nofile 修复命令。
158
180
  *
@@ -201,6 +223,8 @@ export function parsePaneRows(output) {
201
223
  dead: deadFlag === '1' ? true : deadFlag === '0' ? false : undefined,
202
224
  exitStatus: paneField(fields, 3),
203
225
  exitSignal: paneField(fields, 4),
226
+ // Everything after the fifth separator, so a path containing `|` stays whole.
227
+ currentPath: fields.slice(5).join(PANE_LIST_SEP) || undefined,
204
228
  });
205
229
  }
206
230
  return rows;
@@ -295,9 +319,11 @@ export function resolveTmuxPaneTarget(muxName, paneTarget) {
295
319
  let submitBufferSeq = 0;
296
320
  /**
297
321
  * Escape a final `;` for tmux. tmux splits commands on an argument that ENDS in
298
- * `;` (`set-buffer -- 'a;'` stores `a`, `send-keys -l 'a;'` types `a`, a lone `;`
322
+ * `;` (`set-buffer -- 'a;'` stores `a`, `send-keys -l -- 'a;'` types `a`, a lone `;`
299
323
  * is no argument at all); a final `\;` is its escape for a literal `;`. A `;`
300
- * anywhere else in the argument is literal already. Exported for unit testing.
324
+ * anywhere else in the argument is literal already. Every `send-keys -l` passes
325
+ * `--` before the text, like `set-buffer`, or tmux parses a text that starts with
326
+ * `-` as its own options. Exported for unit testing.
301
327
  */
302
328
  export function escapeTmuxTrailingSemicolon(text) {
303
329
  return text.endsWith(';') ? `${text.slice(0, -1)}\\;` : text;
@@ -864,6 +890,18 @@ export function buildCodexCommand(config) {
864
890
  return 'codex';
865
891
  return buildSpawnCommandFromRegistry(entry, { mode: 'codex', sessionId: '', codexConfig: config }) ?? 'codex';
866
892
  }
893
+ /**
894
+ * Whether a launch carries `idleCompaction: false` in its one `--settings` JSON: keep-warm on
895
+ * at launch (`keepWarmOn`), the statusLine exporter injected (without its `prompt_cache` reports
896
+ * keep-warm cannot run, and the opt-out alone would only cost the user Claude Code's own idle
897
+ * compaction), and the CLI's `idleCompactionSetting` gate passed, fail-closed like `--name`:
898
+ * an unknown version means no key. Pure; `claudeCliVersion` as in buildSpawnCommand.
899
+ */
900
+ export function decideIdleCompactionOptOut(o) {
901
+ return (o.statusLine === 'injected' &&
902
+ keepWarmOn(o.keepWarm, o.keepWarmDefault, !!o.spawnedByAgent) &&
903
+ launchGatePasses(o.mode, 'idleCompactionSetting', o.claudeCliVersion));
904
+ }
867
905
  export function buildSpawnCommand(options) {
868
906
  // Every CLI's command shape is registry DATA, rendered by the argv engine — see
869
907
  // config/cli-registry/argv.ts for why config can never contain shell text. A `shell`-kind
@@ -938,7 +976,9 @@ export function remoteTmuxLocation(remote, sessionId) {
938
976
  * ssh -o BatchMode=yes -t [<COD-107 connection opts>] user@host \
939
977
  * 'tmux -L codeman-remote new-session -A -s codeman-ssh-<id> -c <path> "cd <path> && exec <cli>" \
940
978
  * \; set -t codeman-ssh-<id> status off \; set -t codeman-ssh-<id> mouse off \
941
- * \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0'
979
+ * \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0 \; \
980
+ * bind-key -T prefix C-q send-prefix \; unbind-key -a -T prefix \; \
981
+ * bind-key -T prefix C-q send-prefix \; set -t codeman-ssh-<id> remain-on-exit failed'
942
982
  *
943
983
  * COD-107 — the connection options (`-p`, `-i`, `-J`, SOCKS `-o ProxyCommand`,
944
984
  * arbitrary `-o`) come from the shared `buildSshConnectionArgs(remote)`, so the
@@ -1043,7 +1083,7 @@ export function buildRemoteLaunchCommand(options) {
1043
1083
  `tmux -L ${REMOTE_TMUX_SOCKET} new-session -A -s ${remoteName} -c ${shellescape(remote.remotePath)} ${shellescape(paneCommand)}`,
1044
1084
  `set -t ${remoteName} status off`,
1045
1085
  `set -t ${remoteName} mouse off`,
1046
- `set -t ${remoteName} prefix C-q`,
1086
+ `set -t ${remoteName} prefix ${NESTED_TMUX_PREFIX}`,
1047
1087
  'set -s escape-time 0',
1048
1088
  // COD-106 — shared/collaborative sessions: tmux defaults to sizing a window
1049
1089
  // to the SMALLEST attached client, so two Codemans at different viewports
@@ -1052,6 +1092,15 @@ export function buildRemoteLaunchCommand(options) {
1052
1092
  // Per-session scoped (`set -t <name>`, matching #145's hardening) so a shared
1053
1093
  // remote tmux server's other sessions keep their own sizing behavior.
1054
1094
  `set -t ${remoteName} window-size latest`,
1095
+ // The prefix TABLE is server-global (unlike `prefix C-q`, which is per
1096
+ // session). This socket is dedicated to Codeman's owned sessions, so the
1097
+ // same reduction as the local socket applies: Ctrl+Q then )/(/s/w/L must
1098
+ // not switch-client / choose-tree onto a sibling. Bind, then unbind-all,
1099
+ // then bind again: a user config that left the prefix table empty makes
1100
+ // `unbind-key -a -T prefix` fail and abort the rest of this chain.
1101
+ // BEFORE remain-on-exit, which stays last because an old remote tmux
1102
+ // rejects `failed` and aborts the rest of the chain.
1103
+ ...nestedPrefixTableResetCommands(),
1055
1104
  // #210: keep a CRASHED pane so the failure is still on screen. Without this,
1056
1105
  // tmux destroys the pane -> window -> session (and, being the only session,
1057
1106
  // the whole remote server) the instant the pane command exits, which tears the
@@ -1068,7 +1117,8 @@ export function buildRemoteLaunchCommand(options) {
1068
1117
  // LAST in the chain on purpose: tmux aborts the remaining commands of a `\;`
1069
1118
  // sequence once one errors (also verified), and `failed` needs tmux >= 3.2 on
1070
1119
  // the REMOTE host. Trailing, a rejection costs only this option; leading, it
1071
- // would silently drop status/mouse/prefix/escape-time/window-size with it.
1120
+ // would silently drop status/mouse/prefix/escape-time/window-size and the
1121
+ // prefix-table reduction with it.
1072
1122
  `set -t ${remoteName} remain-on-exit failed`,
1073
1123
  ].join(' \\; ');
1074
1124
  // ssh runs its trailing args through the remote login shell, so the entire
@@ -1080,7 +1130,14 @@ export function buildRemoteLaunchCommand(options) {
1080
1130
  // `ssh -o BatchMode=yes` (preserving the historical token order), then the rest
1081
1131
  // of the connection args, then the target and the quoted tmux invocation.
1082
1132
  const [ssh, batchMode, ...connectionArgs] = buildSshConnectionArgs(remote);
1083
- const sshParts = [ssh, batchMode, '-t', ...connectionArgs, remoteSshTarget(remote), shellescape(tmuxInvocation)];
1133
+ const sshParts = [
1134
+ ssh,
1135
+ batchMode,
1136
+ '-t',
1137
+ ...connectionArgs,
1138
+ remoteSshTarget(remote),
1139
+ shellescape(withRemoteTmuxPath(tmuxInvocation)),
1140
+ ];
1084
1141
  return sshParts.join(' ');
1085
1142
  }
1086
1143
  /**
@@ -1096,7 +1153,7 @@ export function buildRemoteKillCommand(options) {
1096
1153
  const remoteName = remoteTmuxSessionName(sessionId);
1097
1154
  const killCmd = `tmux -L ${REMOTE_TMUX_SOCKET} kill-session -t ${shellescape(remoteName)}`;
1098
1155
  const [ssh, ...connectionArgs] = buildSshConnectionArgs(remote);
1099
- return [ssh, ...connectionArgs, remoteSshTarget(remote), shellescape(killCmd)].join(' ');
1156
+ return [ssh, ...connectionArgs, remoteSshTarget(remote), shellescape(withRemoteTmuxPath(killCmd))].join(' ');
1100
1157
  }
1101
1158
  // ========== Docker cases (COD-Docker) ==========
1102
1159
  //
@@ -1212,8 +1269,12 @@ export function buildDockerLaunchCommand(opts) {
1212
1269
  `new-session -A -s ${dkrName} -c ${workdir} ${shellescape(paneCommand)}`,
1213
1270
  `set -t ${dkrName} status off`,
1214
1271
  `set -t ${dkrName} mouse off`,
1215
- `set -t ${dkrName} prefix C-q`,
1272
+ `set -t ${dkrName} prefix ${NESTED_TMUX_PREFIX}`,
1216
1273
  'set -s escape-time 0',
1274
+ // Same reduction as the owned-remote chain: the in-container server is
1275
+ // Codeman's (`-L codeman-docker`), and its stock prefix table would move
1276
+ // the inner client to a sibling session on Ctrl+Q then )/(/s/w/L.
1277
+ ...nestedPrefixTableResetCommands(),
1217
1278
  ].join(' \\; ');
1218
1279
  const execEnvFlags = [];
1219
1280
  for (const [k, v] of Object.entries(execEnv))
@@ -1423,7 +1484,14 @@ export function resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessio
1423
1484
  export function buildRemoteAttachCommand(remote, remoteSessionName) {
1424
1485
  const tmuxInvocation = `tmux -L codeman attach -t ${shellescape(remoteSessionName)}`;
1425
1486
  const [ssh, batchMode, ...connectionArgs] = buildSshConnectionArgs(remote);
1426
- const sshParts = [ssh, batchMode, '-t', ...connectionArgs, remoteSshTarget(remote), shellescape(tmuxInvocation)];
1487
+ const sshParts = [
1488
+ ssh,
1489
+ batchMode,
1490
+ '-t',
1491
+ ...connectionArgs,
1492
+ remoteSshTarget(remote),
1493
+ shellescape(withRemoteTmuxPath(tmuxInvocation)),
1494
+ ];
1427
1495
  return sshParts.join(' ');
1428
1496
  }
1429
1497
  /**
@@ -1934,6 +2002,21 @@ export class TmuxManager extends EventEmitter {
1934
2002
  const command = await resolveStatusLineCliCommand(workingDir, enabled);
1935
2003
  return { command, state: command ? 'injected' : enabled ? 'workspace-statusline' : 'collection-off' };
1936
2004
  }
2005
+ /**
2006
+ * Whether this launch carries `idleCompaction: false` (decideIdleCompactionOptOut), with the
2007
+ * keep-warm app default read fresh like the plan-usage switch above. Logged when keep-warm is
2008
+ * on but the version gate keeps the key off, so a session that may still idle-compact says why.
2009
+ */
2010
+ async _resolveIdleCompactionOptOut(mode, statusLine, keepWarm, spawnedByAgent) {
2011
+ if (statusLine !== 'injected')
2012
+ return false; // no settings read for a launch that can never carry it
2013
+ const { keepWarmDefault } = await readKeepWarmSettings();
2014
+ const off = decideIdleCompactionOptOut({ mode, statusLine, keepWarm, keepWarmDefault, spawnedByAgent });
2015
+ if (!off && keepWarmOn(keepWarm, keepWarmDefault, !!spawnedByAgent)) {
2016
+ console.log(`[TmuxManager] Keep-warm is on but Claude Code's version is unknown or predates the idleCompaction setting; launching without the opt-out`);
2017
+ }
2018
+ return off;
2019
+ }
1937
2020
  /**
1938
2021
  * Export the user's own REAL statusLine command (found by
1939
2022
  * findEffectiveUserStatusLineCommand) via tmux setenv, so the shared
@@ -1971,7 +2054,7 @@ export class TmuxManager extends EventEmitter {
1971
2054
  * In test mode: creates an in-memory session only (no real tmux session).
1972
2055
  */
1973
2056
  async createSession(options) {
1974
- const { sessionId, workingDir, mode, name, cliName, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig, copilotConfig, deepSeekConfig, ompConfig, resumeSessionId, envOverrides, effort, advisorModel, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, docker, owner, } = options;
2057
+ 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;
1975
2058
  const muxName = `codeman-${sessionId.slice(0, 8)}`;
1976
2059
  if (!isValidMuxName(muxName)) {
1977
2060
  throw new Error('Invalid session name: contains unsafe characters');
@@ -2022,6 +2105,7 @@ export class TmuxManager extends EventEmitter {
2022
2105
  }
2023
2106
  const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
2024
2107
  const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir, !!(remote || docker));
2108
+ const idleCompactionOff = await this._resolveIdleCompactionOptOut(mode, statusLine, keepWarm, spawnedByAgent);
2025
2109
  // The user's own REAL statusLine, if any (walked via Claude Code's own
2026
2110
  // settings precedence) — exported below so the shared exporter script
2027
2111
  // can wrap it. Only worth discovering when we're actually injecting.
@@ -2045,6 +2129,7 @@ export class TmuxManager extends EventEmitter {
2045
2129
  effort,
2046
2130
  advisorModel,
2047
2131
  statusLineCommand,
2132
+ idleCompactionOff,
2048
2133
  sessionName: cliName,
2049
2134
  });
2050
2135
  const config = niceConfig || DEFAULT_NICE_CONFIG;
@@ -2094,6 +2179,9 @@ export class TmuxManager extends EventEmitter {
2094
2179
  catch {
2095
2180
  /* Non-critical */
2096
2181
  }
2182
+ // Browser keystrokes reach the attach client, so tmux's stock prefix table would
2183
+ // let Ctrl+B ( or s move a tab onto another session. Before the agent starts.
2184
+ restrictTmuxPrefixTable(this.tmux());
2097
2185
  // Per-CLI env: API keys, config blobs, config-sourced vars, status bridges. All of
2098
2186
  // it is registry data, so this is one unconditional call rather than a per-mode ladder.
2099
2187
  this._configureCliEnv(muxName, sessionId, mode, legacyConfigForMode(mode, options));
@@ -2163,6 +2251,7 @@ export class TmuxManager extends EventEmitter {
2163
2251
  attached: false,
2164
2252
  name,
2165
2253
  statusLine,
2254
+ idleCompactionOff,
2166
2255
  };
2167
2256
  this.sessions.set(sessionId, session);
2168
2257
  this.saveSessions();
@@ -2250,7 +2339,7 @@ export class TmuxManager extends EventEmitter {
2250
2339
  * preserving the session and its scrollback buffer.
2251
2340
  */
2252
2341
  async respawnPane(options) {
2253
- const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig, copilotConfig, deepSeekConfig, ompConfig, resumeSessionId, envOverrides, unsetEnvKeys, effort, advisorModel, remote, docker, cliName, } = options;
2342
+ const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig, copilotConfig, deepSeekConfig, ompConfig, resumeSessionId, envOverrides, unsetEnvKeys, effort, advisorModel, keepWarm, spawnedByAgent, remote, docker, cliName, } = options;
2254
2343
  const session = this.sessions.get(sessionId);
2255
2344
  if (!session)
2256
2345
  return null;
@@ -2261,6 +2350,7 @@ export class TmuxManager extends EventEmitter {
2261
2350
  const { pathExport } = this.buildPathExport(mode);
2262
2351
  const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
2263
2352
  const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir, !!(remote || docker));
2353
+ const idleCompactionOff = await this._resolveIdleCompactionOptOut(mode, statusLine, keepWarm, spawnedByAgent);
2264
2354
  const userStatusLineCommand = statusLineCommand ? await findEffectiveUserStatusLineCommand(workingDir) : undefined;
2265
2355
  const baseCmd = buildSpawnCommand({
2266
2356
  mode,
@@ -2281,6 +2371,7 @@ export class TmuxManager extends EventEmitter {
2281
2371
  effort,
2282
2372
  advisorModel,
2283
2373
  statusLineCommand,
2374
+ idleCompactionOff,
2284
2375
  sessionName: cliName,
2285
2376
  });
2286
2377
  const config = niceConfig || DEFAULT_NICE_CONFIG;
@@ -2313,8 +2404,9 @@ export class TmuxManager extends EventEmitter {
2313
2404
  const pid = this.getPanePid(muxName);
2314
2405
  if (pid)
2315
2406
  session.pid = pid;
2316
- if (session.statusLine !== statusLine) {
2407
+ if (session.statusLine !== statusLine || session.idleCompactionOff !== idleCompactionOff) {
2317
2408
  session.statusLine = statusLine;
2409
+ session.idleCompactionOff = idleCompactionOff;
2318
2410
  this.saveSessions();
2319
2411
  }
2320
2412
  return pid;
@@ -2663,13 +2755,30 @@ export class TmuxManager extends EventEmitter {
2663
2755
  // socket tag to reconcile and no cross-socket ambiguity that could mark a
2664
2756
  // live session dead (the root cause of vanished/duplicate tabs).
2665
2757
  let active;
2758
+ const paneDirs = new Map();
2666
2759
  try {
2667
2760
  const output = execSync(`${this.tmux()} list-panes -a -F '${PANE_LIST_FORMAT}' 2>/dev/null || true`, {
2668
2761
  encoding: 'utf-8',
2669
2762
  timeout: EXEC_TIMEOUT_MS,
2670
- }).trim();
2763
+ });
2764
+ // Not trimmed: the directory is the last field, so trimming would cut a
2765
+ // trailing space off the last row's path. parsePaneRows skips empty lines.
2671
2766
  const rows = parsePaneRows(output);
2672
2767
  active = new Map(rows.map((row) => [row.sessionName, row.pid]));
2768
+ // A discovered session's directory, from its first pane with a usable path.
2769
+ // The path is admitted only if it passes the same check createSession and
2770
+ // respawnPane apply, since it was set by whoever owns the pane, not by us.
2771
+ // The launch directory counts as no answer: every pane starts there, and a
2772
+ // remote or docker pane never cds out of it, so for such a session it is
2773
+ // not the workspace — and a rediscovered record cannot tell it is one.
2774
+ for (const row of rows) {
2775
+ if (row.currentPath &&
2776
+ !TMUX_LAUNCH_CWD_SPELLINGS.has(row.currentPath) &&
2777
+ !paneDirs.has(row.sessionName) &&
2778
+ isValidPath(row.currentPath)) {
2779
+ paneDirs.set(row.sessionName, row.currentPath);
2780
+ }
2781
+ }
2673
2782
  // The same read answers both questions, so recovery starts with a pane-exit
2674
2783
  // reading rather than waiting for the first stats tick — which may never
2675
2784
  // come, since the collector only starts when boot found a live session.
@@ -2681,6 +2790,11 @@ export class TmuxManager extends EventEmitter {
2681
2790
  console.error('[TmuxManager] Failed to list tmux panes:', err);
2682
2791
  active = new Map();
2683
2792
  }
2793
+ // Sessions that survived an upgrade may sit on a server whose prefix table was never
2794
+ // reduced (createSession does it for new ones), so cover them at boot too. Only with
2795
+ // panes listed: on an empty socket `list-keys` spins up a throwaway server instead.
2796
+ if (active.size > 0)
2797
+ restrictTmuxPrefixTable(this.tmux());
2684
2798
  // Check tracked sessions against the live pane list.
2685
2799
  for (const [sessionId, session] of this.sessions) {
2686
2800
  const pid = active.get(session.muxName);
@@ -2724,11 +2838,13 @@ export class TmuxManager extends EventEmitter {
2724
2838
  muxName: sessionName,
2725
2839
  pid,
2726
2840
  createdAt: Date.now(),
2727
- workingDir: process.cwd(),
2841
+ // Where the pane actually works, when tmux said; the server's cwd is only a fallback.
2842
+ workingDir: paneDirs.get(sessionName) ?? process.cwd(),
2728
2843
  mode: 'claude',
2729
2844
  attached: false,
2730
2845
  name: `Restored: ${sessionName}`,
2731
- // Every field above except the name and the pid is a guess: this record
2846
+ // Every field above except the name, the pid and, when tmux reported a
2847
+ // usable one, the directory is a guess: this record
2732
2848
  // was rebuilt from the socket because Codeman's own bookkeeping did not
2733
2849
  // have it. The synthetic id also cannot find the session's state.json
2734
2850
  // entry, so a remote or docker session rediscovered this way arrives
@@ -3335,7 +3451,12 @@ export class TmuxManager extends EventEmitter {
3335
3451
  * - Text followed by `\r` goes in as ONE bracketed paste (buildPasteTextCommands),
3336
3452
  * so Codex cannot fold the Enter into a paste burst; a single character is typed
3337
3453
  * with `send-keys -l` instead, so a dialog answer selects (submitsAsKeys)
3338
- * - Text without `\r` is typed literally with `send-keys -l` (no key interpretation)
3454
+ * - Single-line text without `\r` is typed literally with `send-keys -l` (no key
3455
+ * interpretation)
3456
+ * - A `\n` inside the text is kept (`\r\n` loses only its `\r`), and multi-line text
3457
+ * always goes in as ONE bracketed paste, with or without a following Enter: inside
3458
+ * the paste markers a CLI that enabled bracketed paste receives the line breaks as
3459
+ * pasted text, where typed as keys each one could submit a line on its own
3339
3460
  * - `Enter` key is sent as a SEPARATE tmux invocation after a small delay
3340
3461
  * - Ink (Claude CLI) needs text and Enter split to avoid treating Enter as a newline
3341
3462
  */
@@ -3345,8 +3466,10 @@ export class TmuxManager extends EventEmitter {
3345
3466
  console.error(`[TmuxManager] sendInput failed: no session found for ${sessionId}. Known: ${Array.from(this.sessions.keys()).join(', ')}`);
3346
3467
  return false;
3347
3468
  }
3348
- // TEST MODE: No-op — don't send input to real tmux sessions
3349
- if (IS_TEST_MODE) {
3469
+ // TEST MODE: No-op — don't send input to real tmux sessions. The gate lives in
3470
+ // inputRunner() so a test subclass can record what WOULD reach tmux.
3471
+ const run = this.inputRunner();
3472
+ if (!run) {
3350
3473
  return true;
3351
3474
  }
3352
3475
  console.log(`[TmuxManager] sendInput to ${session.muxName}, input length: ${input.length}, hasCarriageReturn: ${input.includes('\r')}`);
@@ -3356,7 +3479,14 @@ export class TmuxManager extends EventEmitter {
3356
3479
  }
3357
3480
  try {
3358
3481
  const hasCarriageReturn = input.includes('\r');
3359
- const textPart = input.replace(/\r/g, '').replace(/\n/g, '').trimEnd();
3482
+ // Leading and trailing blank lines are dropped, so neither `"\nfix it\r"` nor
3483
+ // `" \nfix it\r"` pastes an empty first line (nor `"fix it\n\r"` an empty last
3484
+ // one). The leading match ends at a line break, so the first real line keeps its
3485
+ // indentation.
3486
+ const textPart = input
3487
+ .replace(/\r/g, '')
3488
+ .replace(/^\s*\n/, '')
3489
+ .trimEnd();
3360
3490
  if (textPart && hasCarriageReturn) {
3361
3491
  // Send text first, then Enter as a SEPARATE tmux command after a short delay.
3362
3492
  // Ink (Claude CLI's terminal framework) needs them split — sending both in a
@@ -3367,25 +3497,29 @@ export class TmuxManager extends EventEmitter {
3367
3497
  // selection dialog ignores a pasted `3` and Enter then confirms the
3368
3498
  // highlighted option (see submitsAsKeys).
3369
3499
  if (submitsAsKeys(textPart)) {
3370
- await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, { timeout: EXEC_TIMEOUT_MS });
3500
+ await run(`${this.tmux()} send-keys -t "${session.muxName}" -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`);
3371
3501
  }
3372
3502
  else {
3373
- await pasteText((cmd) => execAsync(cmd, { timeout: EXEC_TIMEOUT_MS }), buildPasteTextCommands(this.tmux(), session.muxName, textPart));
3503
+ await pasteText(run, buildPasteTextCommands(this.tmux(), session.muxName, textPart));
3374
3504
  }
3375
3505
  await new Promise((resolve) => setTimeout(resolve, 50));
3376
- await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" Enter`, {
3377
- timeout: EXEC_TIMEOUT_MS,
3378
- });
3506
+ await run(`${this.tmux()} send-keys -t "${session.muxName}" Enter`);
3507
+ }
3508
+ else if (textPart.includes('\n')) {
3509
+ // Multi-line text, no Enter: pasted, because typed as keys every `\n` would
3510
+ // reach the pane as a key the CLI may read as submit. A pane that has not
3511
+ // enabled bracketed paste (POSIX sh, macOS /bin/bash 3.2) still gets every line
3512
+ // break as a CR, so there every line but the last runs; before line breaks were
3513
+ // kept, the joined text was typed and nothing ran.
3514
+ await pasteText(run, buildPasteTextCommands(this.tmux(), session.muxName, textPart));
3379
3515
  }
3380
3516
  else if (textPart) {
3381
3517
  // Text only, no Enter
3382
- await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, { timeout: EXEC_TIMEOUT_MS });
3518
+ await run(`${this.tmux()} send-keys -t "${session.muxName}" -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`);
3383
3519
  }
3384
3520
  else if (hasCarriageReturn) {
3385
3521
  // Enter only
3386
- await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" Enter`, {
3387
- timeout: EXEC_TIMEOUT_MS,
3388
- });
3522
+ await run(`${this.tmux()} send-keys -t "${session.muxName}" Enter`);
3389
3523
  }
3390
3524
  return true;
3391
3525
  }
@@ -3394,6 +3528,16 @@ export class TmuxManager extends EventEmitter {
3394
3528
  return false;
3395
3529
  }
3396
3530
  }
3531
+ /**
3532
+ * How {@link sendInput} reaches tmux, or null where it must not (vitest). Split
3533
+ * out like {@link readPaneRows}: with the test-mode gate at the top of sendInput,
3534
+ * no test could see which commands a submit actually sends.
3535
+ */
3536
+ inputRunner() {
3537
+ if (IS_TEST_MODE)
3538
+ return null;
3539
+ return (cmd) => execAsync(cmd, { timeout: EXEC_TIMEOUT_MS });
3540
+ }
3397
3541
  // ========== Pane Methods (for Agent Team teammate panes) ==========
3398
3542
  /**
3399
3543
  * Enable mouse mode for an existing tmux session.
@@ -3493,7 +3637,9 @@ export class TmuxManager extends EventEmitter {
3493
3637
  }
3494
3638
  /**
3495
3639
  * Send input to a specific pane within a tmux session.
3496
- * Uses the same literal text approach as sendInput() but targets a specific pane.
3640
+ * Types the text literally with `send-keys -l`, line breaks stripped, then Enter
3641
+ * when the input carried a `\r`. Unlike sendInput(), it neither pastes nor keeps
3642
+ * the line breaks of multi-line text.
3497
3643
  */
3498
3644
  sendInputToPane(muxName, paneTarget, input) {
3499
3645
  if (IS_TEST_MODE)
@@ -3513,7 +3659,7 @@ export class TmuxManager extends EventEmitter {
3513
3659
  const hasCarriageReturn = input.includes('\r');
3514
3660
  const textPart = input.replace(/\r/g, '').replace(/\n/g, '').trimEnd();
3515
3661
  if (textPart && hasCarriageReturn) {
3516
- execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
3662
+ execSync(`${tmux} send-keys -t ${shellescape(target)} -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
3517
3663
  encoding: 'utf-8',
3518
3664
  timeout: EXEC_TIMEOUT_MS,
3519
3665
  });
@@ -3523,7 +3669,7 @@ export class TmuxManager extends EventEmitter {
3523
3669
  });
3524
3670
  }
3525
3671
  else if (textPart) {
3526
- execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
3672
+ execSync(`${tmux} send-keys -t ${shellescape(target)} -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
3527
3673
  encoding: 'utf-8',
3528
3674
  timeout: EXEC_TIMEOUT_MS,
3529
3675
  });
@@ -3563,19 +3709,21 @@ export class TmuxManager extends EventEmitter {
3563
3709
  /**
3564
3710
  * Plain visible-frame text for the working/idle probe (see `session.ts`).
3565
3711
  *
3566
- * One `capture-pane` and nothing else: no `-e` styles, no `display-message`
3567
- * cursor query, no repaint reconstruction: this feeds a regex, not a
3568
- * terminal. Returns null in tests (no tmux) so callers fall back to their
3569
- * stream heuristics rather than reading an empty screen as "not working".
3712
+ * One `capture-pane` and nothing else: no `display-message` cursor query, no
3713
+ * repaint reconstruction: this feeds a regex, not a terminal. `styles` adds
3714
+ * `-e` for a reader that must tell faint text from typed text (the keep-warm
3715
+ * composer check); lines stay `\n`-separated either way. Returns null in tests
3716
+ * (no tmux) so callers fall back to their stream heuristics rather than
3717
+ * reading an empty screen as "not working".
3570
3718
  */
3571
- capturePaneText(muxName, paneTarget) {
3719
+ capturePaneText(muxName, paneTarget, opts) {
3572
3720
  if (IS_TEST_MODE)
3573
3721
  return null;
3574
3722
  const target = resolveTmuxPaneTarget(muxName, paneTarget);
3575
3723
  if (!target)
3576
3724
  return null;
3577
3725
  try {
3578
- return execSync(`${this.tmux()} capture-pane -p -t ${shellescape(target)}`, {
3726
+ return execSync(`${this.tmux()} capture-pane -p${opts?.styles ? ' -e' : ''} -t ${shellescape(target)}`, {
3579
3727
  encoding: 'utf-8',
3580
3728
  timeout: EXEC_TIMEOUT_MS,
3581
3729
  });