aicodeman 1.41.0 → 1.42.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 (339) hide show
  1. package/README.md +101 -21
  2. package/dist/attachment-registry.d.ts +2 -0
  3. package/dist/attachment-registry.d.ts.map +1 -1
  4. package/dist/attachment-registry.js +14 -3
  5. package/dist/attachment-registry.js.map +1 -1
  6. package/dist/cli-agent.d.ts +2 -0
  7. package/dist/cli-agent.d.ts.map +1 -1
  8. package/dist/cli-agent.js +13 -9
  9. package/dist/cli-agent.js.map +1 -1
  10. package/dist/cli.d.ts +25 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +97 -11
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config/buffer-limits.d.ts +9 -0
  15. package/dist/config/buffer-limits.d.ts.map +1 -1
  16. package/dist/config/buffer-limits.js +9 -0
  17. package/dist/config/buffer-limits.js.map +1 -1
  18. package/dist/config/cli-registry/schema.d.ts +10 -0
  19. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  20. package/dist/config/cli-registry/schema.js +26 -0
  21. package/dist/config/cli-registry/schema.js.map +1 -1
  22. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  23. package/dist/config/cli-registry/stock.js +89 -6
  24. package/dist/config/cli-registry/stock.js.map +1 -1
  25. package/dist/config/cli-registry/types.d.ts +61 -0
  26. package/dist/config/cli-registry/types.d.ts.map +1 -1
  27. package/dist/folder-listing.d.ts +94 -0
  28. package/dist/folder-listing.d.ts.map +1 -0
  29. package/dist/folder-listing.js +241 -0
  30. package/dist/folder-listing.js.map +1 -0
  31. package/dist/git-worktrees.d.ts +292 -0
  32. package/dist/git-worktrees.d.ts.map +1 -0
  33. package/dist/git-worktrees.js +791 -0
  34. package/dist/git-worktrees.js.map +1 -0
  35. package/dist/hooks-config.d.ts +36 -16
  36. package/dist/hooks-config.d.ts.map +1 -1
  37. package/dist/hooks-config.js +45 -20
  38. package/dist/hooks-config.js.map +1 -1
  39. package/dist/html-view-capabilities.d.ts +54 -0
  40. package/dist/html-view-capabilities.d.ts.map +1 -0
  41. package/dist/html-view-capabilities.js +103 -0
  42. package/dist/html-view-capabilities.js.map +1 -0
  43. package/dist/mcp-server.d.ts +114 -0
  44. package/dist/mcp-server.d.ts.map +1 -0
  45. package/dist/mcp-server.js +918 -0
  46. package/dist/mcp-server.js.map +1 -0
  47. package/dist/mcp-sync.d.ts +5 -0
  48. package/dist/mcp-sync.d.ts.map +1 -1
  49. package/dist/mcp-sync.js +51 -5
  50. package/dist/mcp-sync.js.map +1 -1
  51. package/dist/mux-interface.d.ts +36 -1
  52. package/dist/mux-interface.d.ts.map +1 -1
  53. package/dist/remote-files.d.ts +37 -0
  54. package/dist/remote-files.d.ts.map +1 -1
  55. package/dist/remote-files.js +103 -0
  56. package/dist/remote-files.js.map +1 -1
  57. package/dist/remote-pane-history.d.ts +103 -0
  58. package/dist/remote-pane-history.d.ts.map +1 -0
  59. package/dist/remote-pane-history.js +258 -0
  60. package/dist/remote-pane-history.js.map +1 -0
  61. package/dist/remote-ssh-limiter.d.ts +16 -3
  62. package/dist/remote-ssh-limiter.d.ts.map +1 -1
  63. package/dist/remote-ssh-limiter.js +37 -6
  64. package/dist/remote-ssh-limiter.js.map +1 -1
  65. package/dist/services/unified-session-service.d.ts +8 -0
  66. package/dist/services/unified-session-service.d.ts.map +1 -1
  67. package/dist/services/unified-session-service.js +21 -0
  68. package/dist/services/unified-session-service.js.map +1 -1
  69. package/dist/session-activity.d.ts +51 -0
  70. package/dist/session-activity.d.ts.map +1 -1
  71. package/dist/session-activity.js +62 -0
  72. package/dist/session-activity.js.map +1 -1
  73. package/dist/session-submit-verifier.js +1 -1
  74. package/dist/session.d.ts +90 -2
  75. package/dist/session.d.ts.map +1 -1
  76. package/dist/session.js +247 -9
  77. package/dist/session.js.map +1 -1
  78. package/dist/tab-layout-legacy-order.d.ts.map +1 -1
  79. package/dist/tab-layout-legacy-order.js +72 -13
  80. package/dist/tab-layout-legacy-order.js.map +1 -1
  81. package/dist/tab-layout-service.d.ts +33 -2
  82. package/dist/tab-layout-service.d.ts.map +1 -1
  83. package/dist/tab-layout-service.js +102 -7
  84. package/dist/tab-layout-service.js.map +1 -1
  85. package/dist/tab-layout.d.ts +9 -0
  86. package/dist/tab-layout.d.ts.map +1 -1
  87. package/dist/tab-layout.js +15 -2
  88. package/dist/tab-layout.js.map +1 -1
  89. package/dist/tmux-manager.d.ts +130 -7
  90. package/dist/tmux-manager.d.ts.map +1 -1
  91. package/dist/tmux-manager.js +263 -36
  92. package/dist/tmux-manager.js.map +1 -1
  93. package/dist/types/session.d.ts +53 -0
  94. package/dist/types/session.d.ts.map +1 -1
  95. package/dist/types/session.js.map +1 -1
  96. package/dist/usage-telemetry.d.ts +44 -7
  97. package/dist/usage-telemetry.d.ts.map +1 -1
  98. package/dist/usage-telemetry.js +72 -7
  99. package/dist/usage-telemetry.js.map +1 -1
  100. package/dist/utils/deepseek-cli-resolver.d.ts.map +1 -1
  101. package/dist/utils/deepseek-cli-resolver.js +1 -1
  102. package/dist/utils/deepseek-cli-resolver.js.map +1 -1
  103. package/dist/web/approval-inbox.d.ts +24 -2
  104. package/dist/web/approval-inbox.d.ts.map +1 -1
  105. package/dist/web/approval-inbox.js +8 -2
  106. package/dist/web/approval-inbox.js.map +1 -1
  107. package/dist/web/middleware/auth.d.ts.map +1 -1
  108. package/dist/web/middleware/auth.js +33 -0
  109. package/dist/web/middleware/auth.js.map +1 -1
  110. package/dist/web/ports/session-port.d.ts +8 -1
  111. package/dist/web/ports/session-port.d.ts.map +1 -1
  112. package/dist/web/public/admin-ui.js.gz +0 -0
  113. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  114. package/dist/web/public/app.f04bb2a5.js +43 -0
  115. package/dist/web/public/app.f04bb2a5.js.br +0 -0
  116. package/dist/web/public/app.f04bb2a5.js.gz +0 -0
  117. package/dist/web/public/approvals-ui.js +4 -0
  118. package/dist/web/public/approvals-ui.js.br +0 -0
  119. package/dist/web/public/approvals-ui.js.gz +0 -0
  120. package/dist/web/public/{constants.bc9c1f63.js → constants.12b0c247.js} +395 -7
  121. package/dist/web/public/constants.12b0c247.js.br +0 -0
  122. package/dist/web/public/constants.12b0c247.js.gz +0 -0
  123. package/dist/web/public/cron-ui.js.gz +0 -0
  124. package/dist/web/public/entrance-animations.js.gz +0 -0
  125. package/dist/web/public/gesture/gesture-codeman.js +112 -6
  126. package/dist/web/public/git-status-ui.js.gz +0 -0
  127. package/dist/web/public/home-sessions.js.gz +0 -0
  128. package/dist/web/public/host-wake-ui.js.gz +0 -0
  129. package/dist/web/public/i18n.06659676.js +2 -0
  130. package/dist/web/public/i18n.06659676.js.br +0 -0
  131. package/dist/web/public/i18n.06659676.js.gz +0 -0
  132. package/dist/web/public/image-input.fcdcb46c.js +462 -0
  133. package/dist/web/public/image-input.fcdcb46c.js.br +0 -0
  134. package/dist/web/public/image-input.fcdcb46c.js.gz +0 -0
  135. package/dist/web/public/index.html +169 -24
  136. package/dist/web/public/index.html.br +0 -0
  137. package/dist/web/public/index.html.gz +0 -0
  138. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  139. package/dist/web/public/{keyboard-accessory.c1e7c7b7.js → keyboard-accessory.b42f539b.js} +124 -44
  140. package/dist/web/public/keyboard-accessory.b42f539b.js.br +0 -0
  141. package/dist/web/public/keyboard-accessory.b42f539b.js.gz +0 -0
  142. package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
  143. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  144. package/dist/web/public/mobile-overview.js +11 -2
  145. package/dist/web/public/mobile-overview.js.br +0 -0
  146. package/dist/web/public/mobile-overview.js.gz +0 -0
  147. package/dist/web/public/mobile.2dff1580.css +1 -0
  148. package/dist/web/public/mobile.2dff1580.css.br +0 -0
  149. package/dist/web/public/mobile.2dff1580.css.gz +0 -0
  150. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  151. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  152. package/dist/web/public/{panels-ui.71846fcb.js → panels-ui.73ec0137.js} +87 -87
  153. package/dist/web/public/panels-ui.73ec0137.js.br +0 -0
  154. package/dist/web/public/panels-ui.73ec0137.js.gz +0 -0
  155. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  156. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  157. package/dist/web/public/readmymind-ui.js.gz +0 -0
  158. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  159. package/dist/web/public/{respawn-ui.ff0dae4c.js → respawn-ui.d7adcf6a.js} +1 -1
  160. package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
  161. package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
  162. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  163. package/dist/web/public/session-lineage.js.gz +0 -0
  164. package/dist/web/public/session-ui.d52ff6aa.js +100 -0
  165. package/dist/web/public/session-ui.d52ff6aa.js.br +0 -0
  166. package/dist/web/public/session-ui.d52ff6aa.js.gz +0 -0
  167. package/dist/web/public/settings-ui.3a22bd2a.js +97 -0
  168. package/dist/web/public/settings-ui.3a22bd2a.js.br +0 -0
  169. package/dist/web/public/settings-ui.3a22bd2a.js.gz +0 -0
  170. package/dist/web/public/spreadsheet-preview-worker.js +11 -0
  171. package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
  172. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  173. package/dist/web/public/{spreadsheet-preview.af82fa2f.js → spreadsheet-preview.75c751de.js} +1 -1
  174. package/dist/web/public/spreadsheet-preview.75c751de.js.br +0 -0
  175. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  176. package/dist/web/public/spreadsheet-xlsx-core.js +110 -0
  177. package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
  178. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  179. package/dist/web/public/styles.ba6a9b84.css +1 -0
  180. package/dist/web/public/styles.ba6a9b84.css.br +0 -0
  181. package/dist/web/public/styles.ba6a9b84.css.gz +0 -0
  182. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  183. package/dist/web/public/sw.js +6 -2
  184. package/dist/web/public/sw.js.br +0 -0
  185. package/dist/web/public/sw.js.gz +0 -0
  186. package/dist/web/public/tab-layout-browser.2d6189f7.js +1 -0
  187. package/dist/web/public/tab-layout-browser.2d6189f7.js.br +0 -0
  188. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  189. package/dist/web/public/{tab-rail-resize.a5867793.js → tab-rail-resize.7157e0bd.js} +1 -1
  190. package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
  191. package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
  192. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  193. package/dist/web/public/terminal-split.js.gz +0 -0
  194. package/dist/web/public/terminal-tile.js +45 -24
  195. package/dist/web/public/terminal-tile.js.br +0 -0
  196. package/dist/web/public/terminal-tile.js.gz +0 -0
  197. package/dist/web/public/terminal-ui.0a7ccc29.js +2 -0
  198. package/dist/web/public/terminal-ui.0a7ccc29.js.br +0 -0
  199. package/dist/web/public/terminal-ui.0a7ccc29.js.gz +0 -0
  200. package/dist/web/public/tile-grid.js +439 -18
  201. package/dist/web/public/tile-grid.js.br +0 -0
  202. package/dist/web/public/tile-grid.js.gz +0 -0
  203. package/dist/web/public/ultracode-panel.js.gz +0 -0
  204. package/dist/web/public/ultracode-windows.js.gz +0 -0
  205. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  206. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  207. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  208. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  209. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  210. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  211. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  212. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  213. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  214. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
  215. package/dist/web/public/vendor/xterm.css.gz +0 -0
  216. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  217. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  218. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  219. package/dist/web/public/webview-tabs.js +12 -0
  220. package/dist/web/public/webview-tabs.js.br +0 -0
  221. package/dist/web/public/webview-tabs.js.gz +0 -0
  222. package/dist/web/public/whats-new.json +13 -0
  223. package/dist/web/route-helpers.d.ts +8 -0
  224. package/dist/web/route-helpers.d.ts.map +1 -1
  225. package/dist/web/route-helpers.js +8 -0
  226. package/dist/web/route-helpers.js.map +1 -1
  227. package/dist/web/routes/admin-routes.d.ts.map +1 -1
  228. package/dist/web/routes/admin-routes.js +4 -1
  229. package/dist/web/routes/admin-routes.js.map +1 -1
  230. package/dist/web/routes/approval-routes.d.ts.map +1 -1
  231. package/dist/web/routes/approval-routes.js +12 -10
  232. package/dist/web/routes/approval-routes.js.map +1 -1
  233. package/dist/web/routes/case-routes.d.ts.map +1 -1
  234. package/dist/web/routes/case-routes.js +10 -3
  235. package/dist/web/routes/case-routes.js.map +1 -1
  236. package/dist/web/routes/file-routes.d.ts +46 -1
  237. package/dist/web/routes/file-routes.d.ts.map +1 -1
  238. package/dist/web/routes/file-routes.js +214 -27
  239. package/dist/web/routes/file-routes.js.map +1 -1
  240. package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
  241. package/dist/web/routes/hook-event-routes.js +8 -1
  242. package/dist/web/routes/hook-event-routes.js.map +1 -1
  243. package/dist/web/routes/html-view-routes.d.ts +60 -0
  244. package/dist/web/routes/html-view-routes.d.ts.map +1 -0
  245. package/dist/web/routes/html-view-routes.js +288 -0
  246. package/dist/web/routes/html-view-routes.js.map +1 -0
  247. package/dist/web/routes/index.d.ts +2 -0
  248. package/dist/web/routes/index.d.ts.map +1 -1
  249. package/dist/web/routes/index.js +2 -0
  250. package/dist/web/routes/index.js.map +1 -1
  251. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  252. package/dist/web/routes/ralph-routes.js +19 -14
  253. package/dist/web/routes/ralph-routes.js.map +1 -1
  254. package/dist/web/routes/reboot-restore-routes.js +2 -2
  255. package/dist/web/routes/reboot-restore-routes.js.map +1 -1
  256. package/dist/web/routes/session-routes.d.ts +8 -5
  257. package/dist/web/routes/session-routes.d.ts.map +1 -1
  258. package/dist/web/routes/session-routes.js +462 -109
  259. package/dist/web/routes/session-routes.js.map +1 -1
  260. package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
  261. package/dist/web/routes/status-telemetry-routes.js +24 -6
  262. package/dist/web/routes/status-telemetry-routes.js.map +1 -1
  263. package/dist/web/routes/system-routes.d.ts +9 -0
  264. package/dist/web/routes/system-routes.d.ts.map +1 -1
  265. package/dist/web/routes/system-routes.js +91 -12
  266. package/dist/web/routes/system-routes.js.map +1 -1
  267. package/dist/web/routes/worktree-routes.d.ts +52 -0
  268. package/dist/web/routes/worktree-routes.d.ts.map +1 -0
  269. package/dist/web/routes/worktree-routes.js +220 -0
  270. package/dist/web/routes/worktree-routes.js.map +1 -0
  271. package/dist/web/schemas.d.ts +45 -0
  272. package/dist/web/schemas.d.ts.map +1 -1
  273. package/dist/web/schemas.js +105 -0
  274. package/dist/web/schemas.js.map +1 -1
  275. package/dist/web/screen-blocked.d.ts +80 -0
  276. package/dist/web/screen-blocked.d.ts.map +1 -0
  277. package/dist/web/screen-blocked.js +106 -0
  278. package/dist/web/screen-blocked.js.map +1 -0
  279. package/dist/web/server.d.ts +6 -1
  280. package/dist/web/server.d.ts.map +1 -1
  281. package/dist/web/server.js +73 -14
  282. package/dist/web/server.js.map +1 -1
  283. package/dist/web/session-listener-wiring.d.ts +1 -0
  284. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  285. package/dist/web/session-listener-wiring.js +20 -0
  286. package/dist/web/session-listener-wiring.js.map +1 -1
  287. package/dist/web/session-wait-registry.d.ts +27 -9
  288. package/dist/web/session-wait-registry.d.ts.map +1 -1
  289. package/dist/web/session-wait-registry.js +36 -13
  290. package/dist/web/session-wait-registry.js.map +1 -1
  291. package/dist/web/sse-events.d.ts +12 -4
  292. package/dist/web/sse-events.d.ts.map +1 -1
  293. package/dist/web/sse-events.js +12 -4
  294. package/dist/web/sse-events.js.map +1 -1
  295. package/package.json +3 -2
  296. package/skills/codeman/SKILL.md +47 -27
  297. package/skills/codeman/preamble.sh +14 -11
  298. package/skills/codeman/reference/endpoints.md +1 -1
  299. package/skills/codeman/reference/recipes.md +12 -9
  300. package/skills/codeman/reference/verbs.md +3 -1
  301. package/dist/web/public/app.6f8552e1.js +0 -43
  302. package/dist/web/public/app.6f8552e1.js.br +0 -0
  303. package/dist/web/public/app.6f8552e1.js.gz +0 -0
  304. package/dist/web/public/constants.bc9c1f63.js.br +0 -0
  305. package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
  306. package/dist/web/public/i18n.a9840ea2.js +0 -2
  307. package/dist/web/public/i18n.a9840ea2.js.br +0 -0
  308. package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
  309. package/dist/web/public/image-input.e00471c6.js +0 -283
  310. package/dist/web/public/image-input.e00471c6.js.br +0 -0
  311. package/dist/web/public/image-input.e00471c6.js.gz +0 -0
  312. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.br +0 -0
  313. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
  314. package/dist/web/public/mobile.111b187d.css +0 -1
  315. package/dist/web/public/mobile.111b187d.css.br +0 -0
  316. package/dist/web/public/mobile.111b187d.css.gz +0 -0
  317. package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
  318. package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
  319. package/dist/web/public/respawn-ui.ff0dae4c.js.br +0 -0
  320. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  321. package/dist/web/public/session-ui.f731602c.js +0 -100
  322. package/dist/web/public/session-ui.f731602c.js.br +0 -0
  323. package/dist/web/public/session-ui.f731602c.js.gz +0 -0
  324. package/dist/web/public/settings-ui.4e33390f.js +0 -97
  325. package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
  326. package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
  327. package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
  328. package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
  329. package/dist/web/public/styles.81a72906.css +0 -1
  330. package/dist/web/public/styles.81a72906.css.br +0 -0
  331. package/dist/web/public/styles.81a72906.css.gz +0 -0
  332. package/dist/web/public/tab-layout-browser.065fcba4.js +0 -1
  333. package/dist/web/public/tab-layout-browser.065fcba4.js.br +0 -0
  334. package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
  335. package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
  336. package/dist/web/public/tab-rail-resize.a5867793.js.gz +0 -0
  337. package/dist/web/public/terminal-ui.b7267005.js +0 -2
  338. package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
  339. package/dist/web/public/terminal-ui.b7267005.js.gz +0 -0
@@ -7,13 +7,13 @@
7
7
  * - **Persistence**: Sessions survive server restarts and disconnects
8
8
  * - **Ghost recovery**: Orphaned sessions are discovered and reattached on startup
9
9
  * - **Resource tracking**: Memory, CPU, and child process stats per session
10
- * - **Reliable input**: `send-keys -l` sends literal text in a single command
10
+ * - **Reliable input**: a submitted prompt goes in as one bracketed paste, then a separate Enter
11
11
  * - **Teammate support**: Immutable pane IDs enable targeting individual teammates
12
12
  *
13
13
  * tmux sessions are named `codeman-{sessionId}` and stored in ~/.codeman/mux-sessions.json.
14
14
  *
15
15
  * Key features:
16
- * - `send-keys 'text' Enter` sends literal text in a single command
16
+ * - `paste-buffer -p` + a separate `send-keys Enter` submits a prompt (see sendInput)
17
17
  * - `list-sessions -F` provides structured queries
18
18
  * - `display-message -p '#{pane_pid}'` for reliable PID discovery
19
19
  * - Single server architecture
@@ -292,6 +292,70 @@ export function resolveTmuxPaneTarget(muxName, paneTarget) {
292
292
  }
293
293
  return `${muxName}.${paneTarget}`;
294
294
  }
295
+ let submitBufferSeq = 0;
296
+ /**
297
+ * 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 `;`
299
+ * 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.
301
+ */
302
+ export function escapeTmuxTrailingSemicolon(text) {
303
+ return text.endsWith(';') ? `${text.slice(0, -1)}\\;` : text;
304
+ }
305
+ /**
306
+ * Whether a text+Enter submit types `text` as keys instead of pasting it: true for
307
+ * a single character. Claude Code's and Codex's selection dialogs ignore a bracketed
308
+ * paste, so a pasted `3` + Enter confirms the highlighted option instead of option 3;
309
+ * a typed `3` selects it. One character cannot form the burst Codex reads as a paste,
310
+ * so typing it is safe there. Exported for unit testing.
311
+ */
312
+ export function submitsAsKeys(text) {
313
+ return [...text].length === 1;
314
+ }
315
+ /**
316
+ * tmux commands that type `text` into `target` as ONE bracketed paste, ahead of a
317
+ * separately sent `Enter` that submits it. Exported for unit testing.
318
+ *
319
+ * Why a paste and not `send-keys -l`: Codex detects a paste that arrives WITHOUT
320
+ * bracketed-paste markers heuristically, from characters arriving in a fast burst,
321
+ * and folds an Enter that lands inside that burst window into the paste as a
322
+ * newline. `send-keys -l` types exactly such an unmarked burst, so a text+Enter
323
+ * pair sat in Codex's composer unsubmitted: always for long text (Codex shows it as
324
+ * `[Pasted Content N chars]`, which SubmitVerifier cannot match against the prompt,
325
+ * so nothing re-presses Enter), intermittently for short text at the 50 ms gap.
326
+ * `paste-buffer -p` wraps the text in the markers (only when the pane asked for
327
+ * them, so an app without bracketed paste sees the same bytes as before) and the
328
+ * Enter after it is unambiguous.
329
+ *
330
+ * Each call gets its own named buffer so concurrent sends cannot paste each other's
331
+ * text. tmux keeps a named buffer until something deletes it (`buffer-limit` trims
332
+ * only automatic ones), hence `discard` for a paste that fails.
333
+ *
334
+ * A single character is typed instead (submitsAsKeys), so a dialog answer selects.
335
+ * A final `;` is escaped (escapeTmuxTrailingSemicolon).
336
+ */
337
+ export function buildPasteTextCommands(tmux, target, text) {
338
+ const buffer = `codeman-submit-${process.pid}-${++submitBufferSeq}`;
339
+ return {
340
+ set: `${tmux} set-buffer -b ${buffer} -- ${shellescape(escapeTmuxTrailingSemicolon(text))}`,
341
+ paste: `${tmux} paste-buffer -p -d -b ${buffer} -t ${shellescape(target)}`,
342
+ discard: `${tmux} delete-buffer -b ${buffer}`,
343
+ };
344
+ }
345
+ /**
346
+ * Run a bracketed paste. A failed paste (the pane or the server is gone) deletes
347
+ * the buffer before the paste's own error propagates; a failing delete is ignored.
348
+ */
349
+ export async function pasteText(run, cmds) {
350
+ await run(cmds.set);
351
+ try {
352
+ await run(cmds.paste);
353
+ }
354
+ catch (err) {
355
+ await run(cmds.discard).catch(() => undefined);
356
+ throw err;
357
+ }
358
+ }
295
359
  /**
296
360
  * Pick the active pane id from `tmux list-panes -F '#{pane_id}:#{pane_active}'`
297
361
  * output (lines like `%0:1`). Returns the pane id whose active flag is 1.
@@ -534,11 +598,111 @@ function truncatePaneLineByVisibleColumns(line, maxColumns) {
534
598
  export function normalizeScrollbackEol(buffer) {
535
599
  return buffer.replace(/\r?\n/g, '\r\n');
536
600
  }
601
+ /** `capture-pane -p` text as rows: tmux ends it with one `\n`, and '' has none. */
602
+ function captureRows(text) {
603
+ return text === '' ? [] : text.replace(/\n$/, '').split('\n');
604
+ }
605
+ /**
606
+ * Least overlap that places a frame in a screen: rows compared, and how many
607
+ * of them hold text. A placement too LOW repeats the rows between the true one
608
+ * and it, so a short overlap (one `$` or `❯` row, a rule line) is not trusted:
609
+ * past this, the plain join, which can only lose rows, is the safer answer.
610
+ */
611
+ const FRAME_MATCH_MIN_ROWS = 3;
612
+ const FRAME_MATCH_MIN_CONTENT_ROWS = 2;
613
+ /**
614
+ * A captured row as plain text: escape sequences (SGR, OSC 8 links) and
615
+ * trailing blanks dropped. Two tmuxes render the same cell content, but need
616
+ * not encode its attributes the same way.
617
+ */
618
+ function capturedRowText(row) {
619
+ // eslint-disable-next-line no-control-regex
620
+ return row.replace(/\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]/g, '').trimEnd();
621
+ }
622
+ /**
623
+ * Where a later frame of the same pane begins among the rows of an earlier
624
+ * screen capture, or -1 when that cannot be told.
625
+ *
626
+ * The frame's top rows must equal the screen's rows from that index on, as
627
+ * text. Two rows are left out of the comparison: the screen's trailing blank
628
+ * rows (output after the screen read fills them in the frame) and its last
629
+ * content row (output in flight may have finished it since). Every compared
630
+ * row must match, the overlap must reach FRAME_MATCH_MIN_ROWS rows with
631
+ * FRAME_MATCH_MIN_CONTENT_ROWS of them non-blank, and the SMALLEST index wins,
632
+ * so a block that merely repeats further down is only chosen when nothing
633
+ * above it matches. A row that changed between the two reads (a spinner, a
634
+ * clock) fails the true placement; the minimum keeps a lower one from being
635
+ * taken on a row or two that happen to be equal.
636
+ *
637
+ * An agent TUI mid-turn (Claude Code, codex, opencode) is not placed: it keeps
638
+ * a live region (spinner, composer, footer) pinned to the bottom and inserts new
639
+ * rows above it, so the frame is not a shifted copy of the screen and the
640
+ * comparison down to the screen's last content row fails. The caller then falls
641
+ * back to the plain join. A plain shell places, unless the frame starts on the
642
+ * second half of a wrapped line: -J holds that line as one screen row, so it
643
+ * matches nothing and the plain join applies too.
644
+ */
645
+ export function findFrameInScreen(screenRows, frameRows) {
646
+ const screen = screenRows.map(capturedRowText);
647
+ const frame = frameRows.map(capturedRowText);
648
+ let lastContent = screen.length - 1;
649
+ while (lastContent >= 0 && screen[lastContent] === '')
650
+ lastContent--;
651
+ for (let start = 0; start < lastContent; start++) {
652
+ const overlap = Math.min(lastContent - start, frame.length);
653
+ if (overlap < FRAME_MATCH_MIN_ROWS)
654
+ break; // only shrinks from here on
655
+ let matches = true;
656
+ let contentRows = 0;
657
+ for (let k = 0; k < overlap && matches; k++) {
658
+ if (screen[start + k] !== frame[k])
659
+ matches = false;
660
+ else if (frame[k] !== '')
661
+ contentRows++;
662
+ }
663
+ if (matches && contentRows >= FRAME_MATCH_MIN_CONTENT_ROWS)
664
+ return start;
665
+ }
666
+ return -1;
667
+ }
668
+ /**
669
+ * Put scrollback rows read elsewhere (`PaneCaptureOptions.scrollbackOverride`)
670
+ * above a visible-frame capture, as one capture in tmux's own shape: rows joined
671
+ * by a bare `\n`. Undefined or empty scrollback leaves the frame as it is.
672
+ *
673
+ * `screen` is that other pane's screen, read right after its scrollback. The
674
+ * frame is captured later, so rows can have scrolled out of it in between, and
675
+ * a frame showing part of a taller screen starts below that screen's top.
676
+ * Either way the screen rows above the frame's first row are in neither the
677
+ * scrollback nor the frame, so they go between the two. When the frame cannot
678
+ * be placed in the screen ({@link findFrameInScreen}), the join is the plain
679
+ * scrollback-over-frame one.
680
+ */
681
+ export function joinScrollbackAndFrame(scrollback, frame, screen) {
682
+ let above = scrollback ? captureRows(scrollback) : [];
683
+ if (screen) {
684
+ const screenRows = captureRows(screen);
685
+ const frameStart = findFrameInScreen(screenRows, captureRows(frame));
686
+ if (frameStart > 0) {
687
+ // The remote screen is a capture of its own too, encoded from the default
688
+ // state: the history's last row must not leave its attributes active.
689
+ if (above.length > 0)
690
+ above[above.length - 1] += '\x1b[0m';
691
+ above = above.concat(screenRows.slice(0, frameStart));
692
+ }
693
+ }
694
+ // Separate captures: tmux encodes the frame relative to the DEFAULT state, so
695
+ // the rows above it must not leave their attributes active.
696
+ return above.length > 0 ? `${above.join('\n')}\x1b[0m\n${frame}` : frame;
697
+ }
537
698
  /**
538
699
  * Read the pane's cursor and size, or null when tmux cannot say.
539
700
  *
540
- * Every field is validated together: a caller that gets a value back can place
541
- * a caret with it, and one that gets null must not try.
701
+ * The four geometry fields are validated together: a caller that gets a value
702
+ * back can place a caret with it, and one that gets null must not try. The
703
+ * optional fifth and sixth fields, the history depth and the alternate-screen
704
+ * flag, are only informational, so a bad one is dropped rather than
705
+ * discarding a good caret.
542
706
  */
543
707
  export function queryPaneCursor(run) {
544
708
  let raw;
@@ -549,7 +713,9 @@ export function queryPaneCursor(run) {
549
713
  console.error('[TmuxManager] Failed to query pane cursor after capture:', cursorErr);
550
714
  return null;
551
715
  }
552
- const [cursorX, cursorY, cols, rows] = raw.split(/\s+/).map((value) => parseInt(value, 10));
716
+ const [cursorX, cursorY, cols, rows, historyLines, alternateOn] = raw
717
+ .split(/\s+/)
718
+ .map((value) => parseInt(value, 10));
553
719
  if (!Number.isFinite(cursorX) ||
554
720
  !Number.isFinite(cursorY) ||
555
721
  !Number.isFinite(cols) ||
@@ -560,7 +726,12 @@ export function queryPaneCursor(run) {
560
726
  rows <= 0) {
561
727
  return null;
562
728
  }
563
- return { cols, rows, cursorX, cursorY };
729
+ const geometry = { cols, rows, cursorX, cursorY };
730
+ if (Number.isFinite(historyLines) && historyLines >= 0)
731
+ geometry.historyLines = historyLines;
732
+ if (alternateOn === 0 || alternateOn === 1)
733
+ geometry.alternateOn = alternateOn === 1;
734
+ return geometry;
564
735
  }
565
736
  /** SGR attributes, which is all `capture-pane -e` emits. */
566
737
  // eslint-disable-next-line no-control-regex
@@ -746,6 +917,18 @@ const REMOTE_TMUX_SOCKET = 'codeman-remote';
746
917
  export function remoteTmuxSessionName(sessionId) {
747
918
  return `codeman-ssh-${sessionId.slice(0, 8)}`;
748
919
  }
920
+ /**
921
+ * The remote tmux socket and session a remote Codeman session's pane is attached
922
+ * to, matching the command that attached it: an owned session is the one
923
+ * `buildRemoteLaunchCommand` created on the private socket, a non-owned one is the
924
+ * discovered session `buildRemoteAttachCommand` joined on `-L codeman`.
925
+ */
926
+ export function remoteTmuxLocation(remote, sessionId) {
927
+ if (remote.owned === false) {
928
+ return { socket: 'codeman', sessionName: remote.remoteSessionName || remoteTmuxSessionName(sessionId) };
929
+ }
930
+ return { socket: REMOTE_TMUX_SOCKET, sessionName: remoteTmuxSessionName(sessionId) };
931
+ }
749
932
  /**
750
933
  * COD-104 — build the SSH command that launches (or reattaches) a remote
751
934
  * session INSIDE a tmux server on the remote host, so the remote agent survives
@@ -1733,6 +1916,24 @@ export class TmuxManager extends EventEmitter {
1733
1916
  }
1734
1917
  }
1735
1918
  }
1919
+ /**
1920
+ * The statusLine exporter for one claude launch, and why it is missing when it is.
1921
+ * Registry-gated (capabilities.statusLineTelemetry, claude only today), local spawns
1922
+ * only (remote/docker have their own separate command builders). Resolving still runs
1923
+ * with collection off, because it also self-heals: it strips any legacy disk-written
1924
+ * exporter from an older Codeman build the first time a session starts in that
1925
+ * workspace again. `state` lands on the mux record so the Status row can
1926
+ * say why no cache report will come, instead of "not reported yet" forever.
1927
+ */
1928
+ async _resolveStatusLine(mode, workingDir, remoteOrDocker) {
1929
+ if (!getCli(mode)?.capabilities.statusLineTelemetry)
1930
+ return {};
1931
+ if (remoteOrDocker)
1932
+ return { state: 'remote' };
1933
+ const enabled = await readPlanUsageTelemetryEnabled();
1934
+ const command = await resolveStatusLineCliCommand(workingDir, enabled);
1935
+ return { command, state: command ? 'injected' : enabled ? 'workspace-statusline' : 'collection-off' };
1936
+ }
1736
1937
  /**
1737
1938
  * Export the user's own REAL statusLine command (found by
1738
1939
  * findEffectiveUserStatusLineCommand) via tmux setenv, so the shared
@@ -1820,13 +2021,7 @@ export class TmuxManager extends EventEmitter {
1820
2021
  throw new Error(message);
1821
2022
  }
1822
2023
  const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
1823
- // Registry-gated (capabilities.statusLineTelemetry — claude only today), local
1824
- // spawns only (remote/docker have their own separate command builders — out of
1825
- // scope here). Also self-heals: strips any legacy disk-written exporter from an
1826
- // older Codeman build the first time a session starts in that workspace again.
1827
- const statusLineCommand = getCli(mode)?.capabilities.statusLineTelemetry && !remote && !docker
1828
- ? await resolveStatusLineCliCommand(workingDir, await readPlanUsageTelemetryEnabled())
1829
- : undefined;
2024
+ const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir, !!(remote || docker));
1830
2025
  // The user's own REAL statusLine, if any (walked via Claude Code's own
1831
2026
  // settings precedence) — exported below so the shared exporter script
1832
2027
  // can wrap it. Only worth discovering when we're actually injecting.
@@ -1966,6 +2161,7 @@ export class TmuxManager extends EventEmitter {
1966
2161
  mode,
1967
2162
  attached: false,
1968
2163
  name,
2164
+ statusLine,
1969
2165
  };
1970
2166
  this.sessions.set(sessionId, session);
1971
2167
  this.saveSessions();
@@ -2063,10 +2259,7 @@ export class TmuxManager extends EventEmitter {
2063
2259
  // Resolve CLI binary directory based on mode
2064
2260
  const { pathExport } = this.buildPathExport(mode);
2065
2261
  const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
2066
- // See createSession()'s identical resolution for rationale.
2067
- const statusLineCommand = getCli(mode)?.capabilities.statusLineTelemetry && !remote && !docker
2068
- ? await resolveStatusLineCliCommand(workingDir, await readPlanUsageTelemetryEnabled())
2069
- : undefined;
2262
+ const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir, !!(remote || docker));
2070
2263
  const userStatusLineCommand = statusLineCommand ? await findEffectiveUserStatusLineCommand(workingDir) : undefined;
2071
2264
  const baseCmd = buildSpawnCommand({
2072
2265
  mode,
@@ -2118,6 +2311,10 @@ export class TmuxManager extends EventEmitter {
2118
2311
  const pid = this.getPanePid(muxName);
2119
2312
  if (pid)
2120
2313
  session.pid = pid;
2314
+ if (session.statusLine !== statusLine) {
2315
+ session.statusLine = statusLine;
2316
+ this.saveSessions();
2317
+ }
2121
2318
  return pid;
2122
2319
  }
2123
2320
  catch (err) {
@@ -3131,10 +3328,12 @@ export class TmuxManager extends EventEmitter {
3131
3328
  });
3132
3329
  }
3133
3330
  /**
3134
- * Send input directly to a tmux session using `send-keys`.
3331
+ * Send input directly to a tmux session.
3135
3332
  *
3136
- * Uses tmux send-keys for reliable input delivery:
3137
- * - `-l` flag sends literal text (no key interpretation)
3333
+ * - Text followed by `\r` goes in as ONE bracketed paste (buildPasteTextCommands),
3334
+ * so Codex cannot fold the Enter into a paste burst; a single character is typed
3335
+ * with `send-keys -l` instead, so a dialog answer selects (submitsAsKeys)
3336
+ * - Text without `\r` is typed literally with `send-keys -l` (no key interpretation)
3138
3337
  * - `Enter` key is sent as a SEPARATE tmux invocation after a small delay
3139
3338
  * - Ink (Claude CLI) needs text and Enter split to avoid treating Enter as a newline
3140
3339
  */
@@ -3160,10 +3359,17 @@ export class TmuxManager extends EventEmitter {
3160
3359
  // Send text first, then Enter as a SEPARATE tmux command after a short delay.
3161
3360
  // Ink (Claude CLI's terminal framework) needs them split — sending both in a
3162
3361
  // single tmux invocation (via \;) causes Ink to interpret Enter as a newline
3163
- // character in the input buffer rather than as form submission.
3164
- await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(textPart)}`, {
3165
- timeout: EXEC_TIMEOUT_MS,
3166
- });
3362
+ // character in the input buffer rather than as form submission. The text goes
3363
+ // in as a bracketed paste so Codex cannot fold the Enter into it either
3364
+ // (see buildPasteTextCommands). A single character is typed, because a
3365
+ // selection dialog ignores a pasted `3` and Enter then confirms the
3366
+ // highlighted option (see submitsAsKeys).
3367
+ if (submitsAsKeys(textPart)) {
3368
+ await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, { timeout: EXEC_TIMEOUT_MS });
3369
+ }
3370
+ else {
3371
+ await pasteText((cmd) => execAsync(cmd, { timeout: EXEC_TIMEOUT_MS }), buildPasteTextCommands(this.tmux(), session.muxName, textPart));
3372
+ }
3167
3373
  await new Promise((resolve) => setTimeout(resolve, 50));
3168
3374
  await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" Enter`, {
3169
3375
  timeout: EXEC_TIMEOUT_MS,
@@ -3171,9 +3377,7 @@ export class TmuxManager extends EventEmitter {
3171
3377
  }
3172
3378
  else if (textPart) {
3173
3379
  // Text only, no Enter
3174
- await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(textPart)}`, {
3175
- timeout: EXEC_TIMEOUT_MS,
3176
- });
3380
+ await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, { timeout: EXEC_TIMEOUT_MS });
3177
3381
  }
3178
3382
  else if (hasCarriageReturn) {
3179
3383
  // Enter only
@@ -3307,7 +3511,7 @@ export class TmuxManager extends EventEmitter {
3307
3511
  const hasCarriageReturn = input.includes('\r');
3308
3512
  const textPart = input.replace(/\r/g, '').replace(/\n/g, '').trimEnd();
3309
3513
  if (textPart && hasCarriageReturn) {
3310
- execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(textPart)}`, {
3514
+ execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
3311
3515
  encoding: 'utf-8',
3312
3516
  timeout: EXEC_TIMEOUT_MS,
3313
3517
  });
@@ -3317,7 +3521,7 @@ export class TmuxManager extends EventEmitter {
3317
3521
  });
3318
3522
  }
3319
3523
  else if (textPart) {
3320
- execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(textPart)}`, {
3524
+ execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
3321
3525
  encoding: 'utf-8',
3322
3526
  timeout: EXEC_TIMEOUT_MS,
3323
3527
  });
@@ -3397,7 +3601,14 @@ export class TmuxManager extends EventEmitter {
3397
3601
  const historyLines = typeof requestedLines === 'number' && Number.isFinite(requestedLines) && requestedLines > 0
3398
3602
  ? Math.trunc(requestedLines)
3399
3603
  : DEFAULT_TMUX_HISTORY_LIMIT;
3400
- const captureFlags = fullHistory ? `capture-pane -p -e -J -S -${historyLines}` : 'capture-pane -p -e';
3604
+ // With a scrollback override this pane's own history is not read at all:
3605
+ // `-S 0` starts at the top of the visible frame.
3606
+ const scrollbackOverride = fullHistory ? opts?.scrollbackOverride : undefined;
3607
+ const captureFlags = !fullHistory
3608
+ ? 'capture-pane -p -e'
3609
+ : scrollbackOverride !== undefined
3610
+ ? 'capture-pane -p -e -J -S 0'
3611
+ : `capture-pane -p -e -J -S -${historyLines}`;
3401
3612
  // execSync's default maxBuffer (1MB) kills multi-MB scrollback dumps
3402
3613
  // (ENOBUFS) and would silently degrade full-history capture to the byte
3403
3614
  // buffer for exactly the long sessions it exists for — size it from the
@@ -3410,12 +3621,12 @@ export class TmuxManager extends EventEmitter {
3410
3621
  execOpts.maxBuffer =
3411
3622
  (opts?.maxCaptureBytes ?? DEFAULT_TERMINAL_BUFFER_MAX_BYTES) + FULL_HISTORY_CAPTURE_SLACK_BYTES;
3412
3623
  }
3413
- const rawCapture = execSync(`${this.tmux()} ${captureFlags} -t ${shellescape(target)}`, execOpts);
3624
+ const rawCapture = joinScrollbackAndFrame(scrollbackOverride, execSync(`${this.tmux()} ${captureFlags} -t ${shellescape(target)}`, execOpts), scrollbackOverride !== undefined ? opts?.scrollbackOverrideScreen : undefined);
3414
3625
  // Query the cursor BEFORE deciding anything else. On the full-history path
3415
3626
  // it settles both how the capture is trimmed and whether a cursor move is
3416
3627
  // appended, and those two have to agree: trailing blank rows are only safe
3417
3628
  // to keep when a move follows to put the caret back above them.
3418
- const geometry = queryPaneCursor(() => execSync(`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height}'`, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }));
3629
+ const geometry = queryPaneCursor(() => execSync(`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height} #{history_size} #{alternate_on}'`, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }));
3419
3630
  // Report the size the pane was really drawing at. The visible-frame path
3420
3631
  // below addresses every row absolutely, so a consumer whose terminal is
3421
3632
  // shorter than this piles the overflow rows onto its last line and loses
@@ -3425,12 +3636,25 @@ export class TmuxManager extends EventEmitter {
3425
3636
  // the caller can see both sizes, so hand it this one.
3426
3637
  if (opts && geometry)
3427
3638
  opts.capturedGeometry = { cols: geometry.cols, rows: geometry.rows };
3639
+ // Same query, no extra tmux call: how much scrollback a full-history pull
3640
+ // could return. A pane in the alternate screen (fullscreen claude) holds
3641
+ // none, and the partial-history notice must not promise it. A caller that
3642
+ // bounded the capture with `historyLimitLines` also reads it to tell
3643
+ // whether the bound cut anything.
3644
+ if (opts && geometry?.historyLines !== undefined)
3645
+ opts.capturedHistoryLines = geometry.historyLines;
3646
+ // A pane in the alternate screen keeps no history (a nested tmux client
3647
+ // holds it there), so its capture is one screen however deep it reads.
3648
+ if (opts && geometry?.alternateOn !== undefined)
3649
+ opts.capturedAlternateOn = geometry.alternateOn;
3428
3650
  if (fullHistory) {
3429
3651
  // Without geometry there is no cursor move, so fall back to the old trim.
3430
3652
  // Keeping the blank rows here would park the caret at the bottom of the
3431
3653
  // pane with nothing to correct it — worse than not trying at all.
3432
- if (!geometry)
3433
- return normalizeScrollbackEol(rawCapture.replace(/\n+$/g, ''));
3654
+ if (!geometry) {
3655
+ const trimmed = rawCapture.replace(/\n+$/g, '');
3656
+ return trimmed ? `${normalizeScrollbackEol(trimmed)}\x1b[0m` : '';
3657
+ }
3434
3658
  // Take the line terminator off and nothing else. The trailing blank rows
3435
3659
  // that remain are the real bottom of the screen, and the cursor move
3436
3660
  // below counts up from it. tmux joins rows with a bare `\n`; normalize to
@@ -3445,7 +3669,10 @@ export class TmuxManager extends EventEmitter {
3445
3669
  // side where that guard cannot see it.
3446
3670
  if (!hasVisibleContent(trimmed))
3447
3671
  return '';
3448
- return `${normalizeScrollbackEol(trimmed)}${formatCursorRestore(geometry)}`;
3672
+ // capture-pane -e can leave the last row's SGR active (e.g. a nested
3673
+ // tmux status bar). Hand live output the same default style as a visible
3674
+ // snapshot, resetting before the cursor move so its position is kept.
3675
+ return `${normalizeScrollbackEol(trimmed)}\x1b[0m${formatCursorRestore(geometry)}`;
3449
3676
  }
3450
3677
  const buffer = rawCapture.replace(/\n+$/g, '');
3451
3678
  if (geometry)
@@ -3454,7 +3681,7 @@ export class TmuxManager extends EventEmitter {
3454
3681
  // positioned snapshot repaint and fall back to the raw capture. Normalize
3455
3682
  // its bare `\n` line endings to `\r\n` so the replay doesn't staircase
3456
3683
  // diagonally in a fresh xterm (COD-138, same reason as the fullHistory path).
3457
- return normalizeScrollbackEol(buffer);
3684
+ return buffer ? `${normalizeScrollbackEol(buffer)}\x1b[0m` : '';
3458
3685
  }
3459
3686
  catch (err) {
3460
3687
  // ENOBUFS carries the truncated multi-MB stdout on the error object —