aicodeman 1.2.1 → 1.3.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 (232) hide show
  1. package/README.md +264 -90
  2. package/dist/attachment-magic.d.ts +13 -0
  3. package/dist/attachment-magic.d.ts.map +1 -1
  4. package/dist/attachment-magic.js +47 -0
  5. package/dist/attachment-magic.js.map +1 -1
  6. package/dist/attachment-registry.d.ts.map +1 -1
  7. package/dist/attachment-registry.js +13 -2
  8. package/dist/attachment-registry.js.map +1 -1
  9. package/dist/config/buffer-limits.d.ts +2 -3
  10. package/dist/config/buffer-limits.d.ts.map +1 -1
  11. package/dist/config/buffer-limits.js +5 -5
  12. package/dist/config/buffer-limits.js.map +1 -1
  13. package/dist/config/map-limits.d.ts +11 -0
  14. package/dist/config/map-limits.d.ts.map +1 -1
  15. package/dist/config/map-limits.js +11 -0
  16. package/dist/config/map-limits.js.map +1 -1
  17. package/dist/config/server-timing.d.ts +6 -0
  18. package/dist/config/server-timing.d.ts.map +1 -1
  19. package/dist/config/server-timing.js +9 -0
  20. package/dist/config/server-timing.js.map +1 -1
  21. package/dist/config/terminal-history.d.ts +30 -0
  22. package/dist/config/terminal-history.d.ts.map +1 -0
  23. package/dist/config/terminal-history.js +43 -0
  24. package/dist/config/terminal-history.js.map +1 -0
  25. package/dist/cron/cron-input.d.ts +30 -0
  26. package/dist/cron/cron-input.d.ts.map +1 -0
  27. package/dist/cron/cron-input.js +7 -0
  28. package/dist/cron/cron-input.js.map +1 -0
  29. package/dist/cron/cron-service.d.ts +92 -0
  30. package/dist/cron/cron-service.d.ts.map +1 -0
  31. package/dist/cron/cron-service.js +606 -0
  32. package/dist/cron/cron-service.js.map +1 -0
  33. package/dist/cron/cron-time.d.ts +29 -0
  34. package/dist/cron/cron-time.d.ts.map +1 -0
  35. package/dist/cron/cron-time.js +87 -0
  36. package/dist/cron/cron-time.js.map +1 -0
  37. package/dist/document-thumbnailer.d.ts +1 -1
  38. package/dist/document-thumbnailer.d.ts.map +1 -1
  39. package/dist/document-thumbnailer.js +11 -2
  40. package/dist/document-thumbnailer.js.map +1 -1
  41. package/dist/generated-artifact-attachments.d.ts +26 -0
  42. package/dist/generated-artifact-attachments.d.ts.map +1 -0
  43. package/dist/generated-artifact-attachments.js +61 -0
  44. package/dist/generated-artifact-attachments.js.map +1 -0
  45. package/dist/index.js +11 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/mux-interface.d.ts +37 -5
  48. package/dist/mux-interface.d.ts.map +1 -1
  49. package/dist/remote-hosts.d.ts +66 -0
  50. package/dist/remote-hosts.d.ts.map +1 -0
  51. package/dist/remote-hosts.js +198 -0
  52. package/dist/remote-hosts.js.map +1 -0
  53. package/dist/services/unified-session-service.d.ts +115 -0
  54. package/dist/services/unified-session-service.d.ts.map +1 -0
  55. package/dist/services/unified-session-service.js +173 -0
  56. package/dist/services/unified-session-service.js.map +1 -0
  57. package/dist/session-cli-builder.d.ts +3 -1
  58. package/dist/session-cli-builder.d.ts.map +1 -1
  59. package/dist/session-cli-builder.js +26 -7
  60. package/dist/session-cli-builder.js.map +1 -1
  61. package/dist/session-pty-exit-breaker.d.ts +66 -0
  62. package/dist/session-pty-exit-breaker.d.ts.map +1 -0
  63. package/dist/session-pty-exit-breaker.js +77 -0
  64. package/dist/session-pty-exit-breaker.js.map +1 -0
  65. package/dist/session.d.ts +27 -1
  66. package/dist/session.d.ts.map +1 -1
  67. package/dist/session.js +102 -11
  68. package/dist/session.js.map +1 -1
  69. package/dist/state-store.d.ts +14 -0
  70. package/dist/state-store.d.ts.map +1 -1
  71. package/dist/state-store.js +49 -0
  72. package/dist/state-store.js.map +1 -1
  73. package/dist/tmux-manager.d.ts +100 -8
  74. package/dist/tmux-manager.d.ts.map +1 -1
  75. package/dist/tmux-manager.js +249 -26
  76. package/dist/tmux-manager.js.map +1 -1
  77. package/dist/types/api.d.ts +11 -0
  78. package/dist/types/api.d.ts.map +1 -1
  79. package/dist/types/api.js.map +1 -1
  80. package/dist/types/app-state.d.ts +5 -0
  81. package/dist/types/app-state.d.ts.map +1 -1
  82. package/dist/types/app-state.js.map +1 -1
  83. package/dist/types/cron.d.ts +88 -0
  84. package/dist/types/cron.d.ts.map +1 -0
  85. package/dist/types/cron.js +13 -0
  86. package/dist/types/cron.js.map +1 -0
  87. package/dist/types/session.d.ts +57 -0
  88. package/dist/types/session.d.ts.map +1 -1
  89. package/dist/types/session.js.map +1 -1
  90. package/dist/web/heic-jpeg-converter.d.ts +24 -0
  91. package/dist/web/heic-jpeg-converter.d.ts.map +1 -0
  92. package/dist/web/heic-jpeg-converter.js +76 -0
  93. package/dist/web/heic-jpeg-converter.js.map +1 -0
  94. package/dist/web/heic-jpeg-worker.d.ts +44 -0
  95. package/dist/web/heic-jpeg-worker.d.ts.map +1 -0
  96. package/dist/web/heic-jpeg-worker.js +78 -0
  97. package/dist/web/heic-jpeg-worker.js.map +1 -0
  98. package/dist/web/ports/config-port.d.ts +2 -0
  99. package/dist/web/ports/config-port.d.ts.map +1 -1
  100. package/dist/web/ports/cron-port.d.ts +9 -0
  101. package/dist/web/ports/cron-port.d.ts.map +1 -0
  102. package/dist/web/ports/cron-port.js +6 -0
  103. package/dist/web/ports/cron-port.js.map +1 -0
  104. package/dist/web/ports/index.d.ts +1 -0
  105. package/dist/web/ports/index.d.ts.map +1 -1
  106. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  107. package/dist/web/public/app.b0534652.js +38 -0
  108. package/dist/web/public/app.b0534652.js.br +0 -0
  109. package/dist/web/public/app.b0534652.js.gz +0 -0
  110. package/dist/web/public/{constants.1c779517.js → constants.5b18ed64.js} +65 -0
  111. package/dist/web/public/constants.5b18ed64.js.br +0 -0
  112. package/dist/web/public/constants.5b18ed64.js.gz +0 -0
  113. package/dist/web/public/cron-ui.js +328 -0
  114. package/dist/web/public/cron-ui.js.br +0 -0
  115. package/dist/web/public/cron-ui.js.gz +0 -0
  116. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  117. package/dist/web/public/index.html +296 -25
  118. package/dist/web/public/index.html.br +0 -0
  119. package/dist/web/public/index.html.gz +0 -0
  120. package/dist/web/public/input-cjk.63794d0b.js +1 -0
  121. package/dist/web/public/input-cjk.63794d0b.js.br +0 -0
  122. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  123. package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
  124. package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
  125. package/dist/web/public/mobile.0644e3ac.css +1 -0
  126. package/dist/web/public/mobile.0644e3ac.css.br +0 -0
  127. package/dist/web/public/mobile.0644e3ac.css.gz +0 -0
  128. package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
  129. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  130. package/dist/web/public/{panels-ui.772e92c9.js → panels-ui.ca03853f.js} +25 -17
  131. package/dist/web/public/panels-ui.ca03853f.js.br +0 -0
  132. package/dist/web/public/panels-ui.ca03853f.js.gz +0 -0
  133. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  134. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  135. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  136. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  137. package/dist/web/public/session-ui.b51814ef.js +49 -0
  138. package/dist/web/public/session-ui.b51814ef.js.br +0 -0
  139. package/dist/web/public/session-ui.b51814ef.js.gz +0 -0
  140. package/dist/web/public/settings-ui.6c67c125.js +67 -0
  141. package/dist/web/public/settings-ui.6c67c125.js.br +0 -0
  142. package/dist/web/public/settings-ui.6c67c125.js.gz +0 -0
  143. package/dist/web/public/styles.e1ba7849.css +1 -0
  144. package/dist/web/public/styles.e1ba7849.css.br +0 -0
  145. package/dist/web/public/styles.e1ba7849.css.gz +0 -0
  146. package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
  147. package/dist/web/public/sw.js.gz +0 -0
  148. package/dist/web/public/terminal-ui.dbf141fa.js +3 -0
  149. package/dist/web/public/terminal-ui.dbf141fa.js.br +0 -0
  150. package/dist/web/public/terminal-ui.dbf141fa.js.gz +0 -0
  151. package/dist/web/public/ultracode-panel.js.gz +0 -0
  152. package/dist/web/public/ultracode-windows.js.gz +0 -0
  153. package/dist/web/public/upload.html.gz +0 -0
  154. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  155. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  156. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  157. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  158. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  159. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  160. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  161. package/dist/web/public/vendor/xterm.css.gz +0 -0
  162. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  163. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  164. package/dist/web/routes/case-routes.d.ts.map +1 -1
  165. package/dist/web/routes/case-routes.js +115 -1
  166. package/dist/web/routes/case-routes.js.map +1 -1
  167. package/dist/web/routes/cron-routes.d.ts +11 -0
  168. package/dist/web/routes/cron-routes.d.ts.map +1 -0
  169. package/dist/web/routes/cron-routes.js +70 -0
  170. package/dist/web/routes/cron-routes.js.map +1 -0
  171. package/dist/web/routes/index.d.ts +1 -0
  172. package/dist/web/routes/index.d.ts.map +1 -1
  173. package/dist/web/routes/index.js +1 -0
  174. package/dist/web/routes/index.js.map +1 -1
  175. package/dist/web/routes/respawn-routes.d.ts.map +1 -1
  176. package/dist/web/routes/respawn-routes.js +3 -0
  177. package/dist/web/routes/respawn-routes.js.map +1 -1
  178. package/dist/web/routes/session-routes.d.ts.map +1 -1
  179. package/dist/web/routes/session-routes.js +614 -68
  180. package/dist/web/routes/session-routes.js.map +1 -1
  181. package/dist/web/routes/system-routes.d.ts.map +1 -1
  182. package/dist/web/routes/system-routes.js +5 -0
  183. package/dist/web/routes/system-routes.js.map +1 -1
  184. package/dist/web/routes/ws-routes.d.ts.map +1 -1
  185. package/dist/web/routes/ws-routes.js +56 -17
  186. package/dist/web/routes/ws-routes.js.map +1 -1
  187. package/dist/web/schemas.d.ts +117 -0
  188. package/dist/web/schemas.d.ts.map +1 -1
  189. package/dist/web/schemas.js +168 -1
  190. package/dist/web/schemas.js.map +1 -1
  191. package/dist/web/server.d.ts +9 -2
  192. package/dist/web/server.d.ts.map +1 -1
  193. package/dist/web/server.js +82 -13
  194. package/dist/web/server.js.map +1 -1
  195. package/dist/web/session-listener-wiring.d.ts +5 -1
  196. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  197. package/dist/web/session-listener-wiring.js +23 -1
  198. package/dist/web/session-listener-wiring.js.map +1 -1
  199. package/dist/web/sse-events.d.ts +15 -0
  200. package/dist/web/sse-events.d.ts.map +1 -1
  201. package/dist/web/sse-events.js +17 -0
  202. package/dist/web/sse-events.js.map +1 -1
  203. package/dist/web/ws-connection-registry.d.ts +77 -0
  204. package/dist/web/ws-connection-registry.d.ts.map +1 -0
  205. package/dist/web/ws-connection-registry.js +99 -0
  206. package/dist/web/ws-connection-registry.js.map +1 -0
  207. package/package.json +3 -1
  208. package/dist/web/public/app.a8d73c32.js +0 -38
  209. package/dist/web/public/app.a8d73c32.js.br +0 -0
  210. package/dist/web/public/app.a8d73c32.js.gz +0 -0
  211. package/dist/web/public/constants.1c779517.js.br +0 -0
  212. package/dist/web/public/constants.1c779517.js.gz +0 -0
  213. package/dist/web/public/input-cjk.4e950e0a.js +0 -1
  214. package/dist/web/public/input-cjk.4e950e0a.js.br +0 -0
  215. package/dist/web/public/input-cjk.4e950e0a.js.gz +0 -0
  216. package/dist/web/public/mobile.8a9aff7a.css +0 -1
  217. package/dist/web/public/mobile.8a9aff7a.css.br +0 -0
  218. package/dist/web/public/mobile.8a9aff7a.css.gz +0 -0
  219. package/dist/web/public/panels-ui.772e92c9.js.br +0 -0
  220. package/dist/web/public/panels-ui.772e92c9.js.gz +0 -0
  221. package/dist/web/public/session-ui.da3e53f2.js +0 -36
  222. package/dist/web/public/session-ui.da3e53f2.js.br +0 -0
  223. package/dist/web/public/session-ui.da3e53f2.js.gz +0 -0
  224. package/dist/web/public/settings-ui.38feb956.js +0 -61
  225. package/dist/web/public/settings-ui.38feb956.js.br +0 -0
  226. package/dist/web/public/settings-ui.38feb956.js.gz +0 -0
  227. package/dist/web/public/styles.1b3e6147.css +0 -1
  228. package/dist/web/public/styles.1b3e6147.css.br +0 -0
  229. package/dist/web/public/styles.1b3e6147.css.gz +0 -0
  230. package/dist/web/public/terminal-ui.865f21f6.js +0 -3
  231. package/dist/web/public/terminal-ui.865f21f6.js.br +0 -0
  232. package/dist/web/public/terminal-ui.865f21f6.js.gz +0 -0
@@ -21,8 +21,8 @@
21
21
  * @module tmux-manager
22
22
  */
23
23
  import { EventEmitter } from 'node:events';
24
- import { ProcessStats, PersistedRespawnConfig, type PaneInfo, type CodexConfig } from './types.js';
25
- import type { TerminalMultiplexer, MuxSession, MuxSessionWithStats, CreateSessionOptions, RespawnPaneOptions } from './mux-interface.js';
24
+ import { ProcessStats, PersistedRespawnConfig, type PaneInfo, type SessionMode, type CodexConfig, type SessionRemote } from './types.js';
25
+ import type { TerminalMultiplexer, MuxSession, MuxSessionWithStats, CreateSessionOptions, RespawnPaneOptions, PaneCaptureOptions } from './mux-interface.js';
26
26
  /** Claude Code native macOS recommendation for avoiding low nofile startup failures. */
27
27
  export declare const CLAUDE_CODE_NOFILE_LIMIT = 2147483646;
28
28
  /**
@@ -52,6 +52,23 @@ export declare function resolveTmuxPaneTarget(muxName: string, paneTarget?: stri
52
52
  * output (lines like `%0:1`). Returns the pane id whose active flag is 1.
53
53
  */
54
54
  export declare function resolveActivePaneTarget(output: string): string | null;
55
+ /**
56
+ * Normalize scrollback line endings to `\r\n` so a fresh xterm replays each line
57
+ * at column 0 (COD-138).
58
+ *
59
+ * `capture-pane -p -e -S -` (full-history capture) joins scrollback rows with a
60
+ * BARE `\n`. The browser xterm is created with the default `convertEol: false`
61
+ * (correct for the live PTY stream, which already carries real `\r\n`), so a bare
62
+ * `\n` drops a row without returning the cursor to column 0. Replaying that raw
63
+ * buffer on a full page reload makes every line start one column further right —
64
+ * the diagonal "staircase". The visible/tab-switch path avoids this by repainting
65
+ * each row with an absolute cursor CSI (`formatPaneSnapshot`); the full-history
66
+ * path returns raw scrollback, so it must be CRLF-normalized here.
67
+ *
68
+ * `\r?\n → \r\n` is idempotent on already-CRLF input and leaves a lone `\r` (an
69
+ * intentional in-line column reset / overwrite) untouched.
70
+ */
71
+ export declare function normalizeScrollbackEol(buffer: string): string;
55
72
  export declare function formatPaneSnapshot(lines: string[], geometry: {
56
73
  cols: number;
57
74
  rows: number;
@@ -65,6 +82,66 @@ export declare function formatPaneSnapshot(lines: string[], geometry: {
65
82
  * stripping destructive terminal sequences before xterm.js sees them.
66
83
  */
67
84
  export declare function buildCodexCommand(config?: CodexConfig): string;
85
+ /**
86
+ * Deterministic, reattach-stable remote tmux session name for a Codeman session.
87
+ *
88
+ * Derived from the same stable field the LOCAL muxName uses (the first 8 chars of
89
+ * the sessionId), so reconnecting (which re-issues the exact same
90
+ * `ssh … new-session -A`) lands back in the SAME remote session. Must NOT be
91
+ * random/time-based — it has to be stable across reconnects.
92
+ *
93
+ * The `codeman-ssh-` prefix is deliberately chosen to FAIL a remote Codeman's
94
+ * `SAFE_MUX_NAME_PATTERN` (`^codeman-[a-f0-9-]+$`) — the `s`/`h` letters mean a
95
+ * remote instance's discovery never treats this as one of its own sessions (belt
96
+ * to the dedicated-socket suspenders above).
97
+ */
98
+ export declare function remoteTmuxSessionName(sessionId: string): string;
99
+ /**
100
+ * COD-104 — build the SSH command that launches (or reattaches) a remote
101
+ * session INSIDE a tmux server on the remote host, so the remote agent survives
102
+ * an SSH drop.
103
+ *
104
+ * Emits:
105
+ * ssh -o BatchMode=yes -t [<COD-107 connection opts>] user@host \
106
+ * 'tmux -L codeman-remote new-session -A -s codeman-ssh-<id> -c <path> "cd <path> && exec <cli>" \
107
+ * \; set -t codeman-ssh-<id> status off \; set -t codeman-ssh-<id> mouse off \
108
+ * \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0'
109
+ *
110
+ * COD-107 — the connection options (`-p`, `-i`, `-J`, SOCKS `-o ProxyCommand`,
111
+ * arbitrary `-o`) come from the shared `buildSshConnectionArgs(remote)`, so the
112
+ * prereq tmux probe and this launch connect with identical options.
113
+ *
114
+ * - `new-session -A -s codeman-ssh-<id>` = attach-if-exists-else-create
115
+ * (idempotent), so reconnect re-runs the same command and reattaches the
116
+ * still-running agent.
117
+ * - `-L codeman-remote` = a DEDICATED socket, NOT the canonical `-L codeman` a
118
+ * remote Codeman would use, so our session never collides with / gets adopted by
119
+ * an instance running on the remote host.
120
+ * - The `set` options are scoped per-session (`set -t <name>` / server-level
121
+ * `set -s`), never `-g`, so they never mutate other sessions' prefix/mouse.
122
+ * - The whole tmux invocation is a SINGLE ssh argument (the remote login shell
123
+ * runs it), so it is shell-quoted as one unit; the `cd && exec` command is in
124
+ * turn a single tmux argument (tmux runs it via `/bin/sh -c`), so the path is
125
+ * shell-quoted inside it too. This keeps escaping correct through every layer
126
+ * even when the remote path contains spaces.
127
+ */
128
+ export declare function buildRemoteLaunchCommand(options: {
129
+ mode: SessionMode;
130
+ remote: SessionRemote;
131
+ sessionId: string;
132
+ }): string;
133
+ /**
134
+ * Build the SSH command that kills the durable remote tmux session created by
135
+ * `buildRemoteLaunchCommand`. Because that session lives on a private socket
136
+ * (`-L codeman-remote`) under a stable name, killing the LOCAL ssh wrapper alone
137
+ * would orphan the remote agent forever (invisible to Codeman, still burning plan
138
+ * quota). This is fired best-effort on session kill; the shared connection args
139
+ * carry the default `-o ConnectTimeout=10` so an unreachable host fails fast.
140
+ */
141
+ export declare function buildRemoteKillCommand(options: {
142
+ remote: SessionRemote;
143
+ sessionId: string;
144
+ }): string;
68
145
  /**
69
146
  * Manages tmux sessions that wrap Claude CLI or shell processes.
70
147
  *
@@ -207,6 +284,12 @@ export declare class TmuxManager extends EventEmitter implements TerminalMultipl
207
284
  updateRespawnConfig(sessionId: string, config: PersistedRespawnConfig | undefined): void;
208
285
  clearRespawnConfig(sessionId: string): void;
209
286
  updateRalphEnabled(sessionId: string, enabled: boolean): void;
287
+ /**
288
+ * Apply a tmux history-limit to all tracked sessions (e.g. when the user
289
+ * changes the terminal-history setting). Invalid limits fall back to the
290
+ * default. Best-effort per session.
291
+ */
292
+ setHistoryLimit(limit: number): Promise<void>;
210
293
  /**
211
294
  * Send input directly to a tmux session using `send-keys`.
212
295
  *
@@ -244,20 +327,29 @@ export declare class TmuxManager extends EventEmitter implements TerminalMultipl
244
327
  */
245
328
  sendInputToPane(muxName: string, paneTarget: string, input: string): boolean;
246
329
  /**
247
- * Capture the current visible text and SGR styles of a specific pane.
330
+ * Capture a pane's text and SGR styles.
248
331
  *
249
- * `capture-pane -e` is sanitized by `formatPaneSnapshot`: SGR color/style
250
- * codes are preserved, while cursor/erase/scroll-region controls are stripped
251
- * before rows are repainted at absolute positions in browser xterm.
332
+ * Two modes:
333
+ * - Visible (default): `capture-pane -p -e` grabs only the on-screen frame,
334
+ * then `formatPaneSnapshot` repaints each row at its absolute position so
335
+ * the browser xterm reproduces the live frame. Used for fast tab switches.
336
+ * - Full history (`opts.fullHistory`): `capture-pane -p -e -J -S -<N>` grabs
337
+ * the tmux scrollback (COD-47, bounded to the configured history limit),
338
+ * returned as linear scrollback text with SGR codes preserved (NOT
339
+ * repositioned — a multi-screen history can't be painted into a single
340
+ * visible frame, so the snapshot repaint is skipped). `-J` re-joins lines
341
+ * hard-wrapped at the pane width so they reflow in the browser xterm.
342
+ * Used for full page reloads so the user gets back their scroll history.
343
+ * Caveat: lines tmux has already evicted past its history-limit are gone.
252
344
  */
253
- capturePaneBuffer(muxName: string, paneTarget: string): string | null;
345
+ capturePaneBuffer(muxName: string, paneTarget?: string, opts?: PaneCaptureOptions): string | null;
254
346
  /**
255
347
  * Capture the active pane for a tmux session.
256
348
  *
257
349
  * Pane ids are not stable across respawns or restores, so callers should not
258
350
  * assume the first pane remains `%0`.
259
351
  */
260
- captureActivePaneBuffer(muxName: string): string | null;
352
+ captureActivePaneBuffer(muxName: string, opts?: PaneCaptureOptions): string | null;
261
353
  /**
262
354
  * Start piping pane output to a file using tmux pipe-pane.
263
355
  * Only pipes output direction (-O) to avoid echoing input.
@@ -1 +1 @@
1
- {"version":3,"file":"tmux-manager.d.ts","sourceRoot":"","sources":["../src/tmux-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tmux-manager.d.ts","sourceRoot":"","sources":["../src/tmux-manager.ts"],"names":[],"mappings":"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"}
@@ -30,11 +30,20 @@ import { dirname } from 'node:path';
30
30
  import { dataPath, DEFAULT_TMUX_SOCKET } from './config/instance.js';
31
31
  import { getErrorMessage, DEFAULT_NICE_CONFIG, } from './types.js';
32
32
  import { buildEffortCliArgs } from './session-cli-builder.js';
33
+ import { buildSshConnectionArgs, defaultRemoteCommandForMode, remoteSshTarget } from './remote-hosts.js';
33
34
  import { wrapWithNice, SAFE_PATH_PATTERN, findClaudeDir, resolveOpenCodeDir, resolveCodexDir, resolveGeminiDir, } from './utils/index.js';
34
35
  // ============================================================================
35
36
  // Timing Constants
36
37
  // ============================================================================
37
38
  import { EXEC_TIMEOUT_MS } from './config/exec-timeout.js';
39
+ import { DEFAULT_TMUX_HISTORY_LIMIT, DEFAULT_TERMINAL_BUFFER_MAX_BYTES } from './config/terminal-history.js';
40
+ /**
41
+ * Extra stdout headroom for the full-history `capture-pane` child process on
42
+ * top of the consumer's byte cap: raw scrollback carries per-line SGR/ANSI
43
+ * overhead that the route pipeline strips before applying its cap, so the
44
+ * capture must be allowed to exceed the final payload size.
45
+ */
46
+ const FULL_HISTORY_CAPTURE_SLACK_BYTES = 8 * 1024 * 1024;
38
47
  /** Delay after tmux session creation — enough for detached tmux to be queryable */
39
48
  const TMUX_CREATION_WAIT_MS = 100;
40
49
  /** Max retries for getPanePid — tmux server cold-start (e.g. macOS) may need extra time */
@@ -365,6 +374,25 @@ function truncatePaneLineByVisibleColumns(line, maxColumns) {
365
374
  }
366
375
  return result;
367
376
  }
377
+ /**
378
+ * Normalize scrollback line endings to `\r\n` so a fresh xterm replays each line
379
+ * at column 0 (COD-138).
380
+ *
381
+ * `capture-pane -p -e -S -` (full-history capture) joins scrollback rows with a
382
+ * BARE `\n`. The browser xterm is created with the default `convertEol: false`
383
+ * (correct for the live PTY stream, which already carries real `\r\n`), so a bare
384
+ * `\n` drops a row without returning the cursor to column 0. Replaying that raw
385
+ * buffer on a full page reload makes every line start one column further right —
386
+ * the diagonal "staircase". The visible/tab-switch path avoids this by repainting
387
+ * each row with an absolute cursor CSI (`formatPaneSnapshot`); the full-history
388
+ * path returns raw scrollback, so it must be CRLF-normalized here.
389
+ *
390
+ * `\r?\n → \r\n` is idempotent on already-CRLF input and leaves a lone `\r` (an
391
+ * intentional in-line column reset / overwrite) untouched.
392
+ */
393
+ export function normalizeScrollbackEol(buffer) {
394
+ return buffer.replace(/\r?\n/g, '\r\n');
395
+ }
368
396
  export function formatPaneSnapshot(lines, geometry) {
369
397
  const cols = Math.max(1, geometry.cols);
370
398
  // Paint the full pane width. Earlier this dropped the rightmost column
@@ -571,6 +599,107 @@ function buildSpawnCommand(options) {
571
599
  }
572
600
  return '$SHELL';
573
601
  }
602
+ /**
603
+ * Dedicated socket for Codeman-launched REMOTE tmux servers, distinct from the
604
+ * canonical local `-L codeman` socket. A remote host that runs its OWN Codeman
605
+ * would otherwise share the `-L codeman` socket AND the `codeman-<hex>` discovery
606
+ * name, so its `reconcileSessions()` would ADOPT our session (attach a PTY,
607
+ * resize, respawn-pane it locally) — the cross-machine form of the "2nd instance
608
+ * attaches live sessions" hazard. A private socket keeps our remote sessions off
609
+ * that instance's radar entirely.
610
+ */
611
+ const REMOTE_TMUX_SOCKET = 'codeman-remote';
612
+ /**
613
+ * Deterministic, reattach-stable remote tmux session name for a Codeman session.
614
+ *
615
+ * Derived from the same stable field the LOCAL muxName uses (the first 8 chars of
616
+ * the sessionId), so reconnecting (which re-issues the exact same
617
+ * `ssh … new-session -A`) lands back in the SAME remote session. Must NOT be
618
+ * random/time-based — it has to be stable across reconnects.
619
+ *
620
+ * The `codeman-ssh-` prefix is deliberately chosen to FAIL a remote Codeman's
621
+ * `SAFE_MUX_NAME_PATTERN` (`^codeman-[a-f0-9-]+$`) — the `s`/`h` letters mean a
622
+ * remote instance's discovery never treats this as one of its own sessions (belt
623
+ * to the dedicated-socket suspenders above).
624
+ */
625
+ export function remoteTmuxSessionName(sessionId) {
626
+ return `codeman-ssh-${sessionId.slice(0, 8)}`;
627
+ }
628
+ /**
629
+ * COD-104 — build the SSH command that launches (or reattaches) a remote
630
+ * session INSIDE a tmux server on the remote host, so the remote agent survives
631
+ * an SSH drop.
632
+ *
633
+ * Emits:
634
+ * ssh -o BatchMode=yes -t [<COD-107 connection opts>] user@host \
635
+ * 'tmux -L codeman-remote new-session -A -s codeman-ssh-<id> -c <path> "cd <path> && exec <cli>" \
636
+ * \; set -t codeman-ssh-<id> status off \; set -t codeman-ssh-<id> mouse off \
637
+ * \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0'
638
+ *
639
+ * COD-107 — the connection options (`-p`, `-i`, `-J`, SOCKS `-o ProxyCommand`,
640
+ * arbitrary `-o`) come from the shared `buildSshConnectionArgs(remote)`, so the
641
+ * prereq tmux probe and this launch connect with identical options.
642
+ *
643
+ * - `new-session -A -s codeman-ssh-<id>` = attach-if-exists-else-create
644
+ * (idempotent), so reconnect re-runs the same command and reattaches the
645
+ * still-running agent.
646
+ * - `-L codeman-remote` = a DEDICATED socket, NOT the canonical `-L codeman` a
647
+ * remote Codeman would use, so our session never collides with / gets adopted by
648
+ * an instance running on the remote host.
649
+ * - The `set` options are scoped per-session (`set -t <name>` / server-level
650
+ * `set -s`), never `-g`, so they never mutate other sessions' prefix/mouse.
651
+ * - The whole tmux invocation is a SINGLE ssh argument (the remote login shell
652
+ * runs it), so it is shell-quoted as one unit; the `cd && exec` command is in
653
+ * turn a single tmux argument (tmux runs it via `/bin/sh -c`), so the path is
654
+ * shell-quoted inside it too. This keeps escaping correct through every layer
655
+ * even when the remote path contains spaces.
656
+ */
657
+ export function buildRemoteLaunchCommand(options) {
658
+ const { mode, remote, sessionId } = options;
659
+ const modeCommand = remote.commands?.[mode] || defaultRemoteCommandForMode(mode);
660
+ const remoteName = remoteTmuxSessionName(sessionId);
661
+ // Innermost: the command tmux runs in the new pane. Run via `/bin/sh -c` by
662
+ // tmux, so the path needs shell-quoting here. `exec` replaces the shell with
663
+ // the CLI so the pane PID is the agent itself.
664
+ const paneCommand = `cd ${shellescape(remote.remotePath)} && ${modeCommand}`;
665
+ // The tmux command line, with `\;` separating commands so the config `set`s
666
+ // apply on the SAME connection (and are idempotent on reattach). Options are
667
+ // scoped per-session (`set -t <name>` / server `set -s`), NEVER `-g`, so a
668
+ // shared remote tmux server's other sessions keep their own prefix/mouse.
669
+ const tmuxInvocation = [
670
+ `tmux -L ${REMOTE_TMUX_SOCKET} new-session -A -s ${remoteName} -c ${shellescape(remote.remotePath)} ${shellescape(paneCommand)}`,
671
+ `set -t ${remoteName} status off`,
672
+ `set -t ${remoteName} mouse off`,
673
+ `set -t ${remoteName} prefix C-q`,
674
+ 'set -s escape-time 0',
675
+ ].join(' \\; ');
676
+ // ssh runs its trailing args through the remote login shell, so the entire
677
+ // tmux invocation is passed as one shell-quoted argument.
678
+ //
679
+ // COD-107 — connection options (port, identity, SOCKS ProxyCommand, jump host,
680
+ // arbitrary -o) come from the shared `buildSshConnectionArgs` so the launch and
681
+ // the tmux-prereq probe connect IDENTICALLY. `-t` is inserted right after
682
+ // `ssh -o BatchMode=yes` (preserving the historical token order), then the rest
683
+ // of the connection args, then the target and the quoted tmux invocation.
684
+ const [ssh, batchMode, ...connectionArgs] = buildSshConnectionArgs(remote);
685
+ const sshParts = [ssh, batchMode, '-t', ...connectionArgs, remoteSshTarget(remote), shellescape(tmuxInvocation)];
686
+ return sshParts.join(' ');
687
+ }
688
+ /**
689
+ * Build the SSH command that kills the durable remote tmux session created by
690
+ * `buildRemoteLaunchCommand`. Because that session lives on a private socket
691
+ * (`-L codeman-remote`) under a stable name, killing the LOCAL ssh wrapper alone
692
+ * would orphan the remote agent forever (invisible to Codeman, still burning plan
693
+ * quota). This is fired best-effort on session kill; the shared connection args
694
+ * carry the default `-o ConnectTimeout=10` so an unreachable host fails fast.
695
+ */
696
+ export function buildRemoteKillCommand(options) {
697
+ const { remote, sessionId } = options;
698
+ const remoteName = remoteTmuxSessionName(sessionId);
699
+ const killCmd = `tmux -L ${REMOTE_TMUX_SOCKET} kill-session -t ${shellescape(remoteName)}`;
700
+ const [ssh, ...connectionArgs] = buildSshConnectionArgs(remote);
701
+ return [ssh, ...connectionArgs, remoteSshTarget(remote), shellescape(killCmd)].join(' ');
702
+ }
574
703
  /**
575
704
  * Set sensitive environment variables on a tmux session via setenv.
576
705
  * These are inherited by panes but not visible in ps output or tmux history.
@@ -830,6 +959,12 @@ export class TmuxManager extends EventEmitter {
830
959
  'export LC_ALL=en_US.UTF-8',
831
960
  mode === 'codex' || mode === 'gemini' ? 'export COLORTERM=truecolor' : 'unset COLORTERM',
832
961
  ...(mode === 'codex' || mode === 'gemini' ? ['unset NO_COLOR'] : []),
962
+ // Stamp each Codex pane with a unique originator so the response-viewer
963
+ // can locate THIS pane's rollout exactly — codex writes the value into
964
+ // session_meta.originator of every rollout it creates. Without it,
965
+ // rollouts are matched by cwd+mtime and two panes in the same directory
966
+ // bleed into each other.
967
+ ...(mode === 'codex' ? [`export CODEX_INTERNAL_ORIGINATOR_OVERRIDE=codeman_${sessionId}`] : []),
833
968
  'export CODEMAN_MUX=1',
834
969
  `export CODEMAN_SESSION_ID=${sessionId}`,
835
970
  `export CODEMAN_MUX_NAME=${muxName}`,
@@ -940,7 +1075,7 @@ export class TmuxManager extends EventEmitter {
940
1075
  * In test mode: creates an in-memory session only (no real tmux session).
941
1076
  */
942
1077
  async createSession(options) {
943
- const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, } = options;
1078
+ const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, } = options;
944
1079
  const muxName = `codeman-${sessionId.slice(0, 8)}`;
945
1080
  if (!isValidMuxName(muxName)) {
946
1081
  throw new Error('Invalid session name: contains unsafe characters');
@@ -956,6 +1091,7 @@ export class TmuxManager extends EventEmitter {
956
1091
  pid: 99999,
957
1092
  createdAt: Date.now(),
958
1093
  workingDir,
1094
+ remote,
959
1095
  mode,
960
1096
  attached: false,
961
1097
  name,
@@ -995,7 +1131,8 @@ export class TmuxManager extends EventEmitter {
995
1131
  const cmd = wrapWithNice(baseCmd, config);
996
1132
  try {
997
1133
  // Build the full command to run inside tmux
998
- const fullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
1134
+ const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
1135
+ const fullCmd = remote ? buildRemoteLaunchCommand({ mode, remote, sessionId }) : localFullCmd;
999
1136
  // Create tmux session in three steps to handle cold-start (no server running)
1000
1137
  // and avoid the race where the command exits before remain-on-exit is set:
1001
1138
  // 1. Create session with default shell (starts tmux server, stays alive)
@@ -1043,7 +1180,7 @@ export class TmuxManager extends EventEmitter {
1043
1180
  this.applyEnvOverrides(muxName, envOverrides);
1044
1181
  // Replace the shell with the actual command (no echo in terminal). Keep
1045
1182
  // pane launch in /tmp, then cd inside bash against the current mount table.
1046
- const launchCmd = `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
1183
+ const launchCmd = remote ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
1047
1184
  execSync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`, {
1048
1185
  timeout: EXEC_TIMEOUT_MS,
1049
1186
  stdio: 'ignore',
@@ -1068,8 +1205,11 @@ export class TmuxManager extends EventEmitter {
1068
1205
  /* Already set globally as fallback */
1069
1206
  }),
1070
1207
  // Raise tmux scrollback from its 2000-line default so re-attach preserves
1071
- // more context. Matches the xterm-side default in constants.js.
1072
- execAsync(`${this.tmux()} set-option -t "${muxName}" history-limit 50000`, { timeout: EXEC_TIMEOUT_MS })
1208
+ // more context. Intentionally exceeds the xterm-side DEFAULT_SCROLLBACK (50k
1209
+ // in constants.js), which stays lower to protect browser/mobile memory.
1210
+ execAsync(`${this.tmux()} set-option -t "${muxName}" history-limit ${historyLimit}`, {
1211
+ timeout: EXEC_TIMEOUT_MS,
1212
+ })
1073
1213
  .then(() => { })
1074
1214
  .catch(() => {
1075
1215
  /* Non-critical — falls back to tmux default */
@@ -1103,6 +1243,7 @@ export class TmuxManager extends EventEmitter {
1103
1243
  pid,
1104
1244
  createdAt: Date.now(),
1105
1245
  workingDir,
1246
+ remote,
1106
1247
  mode,
1107
1248
  attached: false,
1108
1249
  name,
@@ -1170,13 +1311,22 @@ export class TmuxManager extends EventEmitter {
1170
1311
  * preserving the session and its scrollback buffer.
1171
1312
  */
1172
1313
  async respawnPane(options) {
1173
- const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, } = options;
1314
+ const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, } = options;
1174
1315
  const session = this.sessions.get(sessionId);
1175
1316
  if (!session)
1176
1317
  return null;
1177
1318
  const muxName = session.muxName;
1178
1319
  if (!isValidMuxName(muxName) || !isValidPath(workingDir))
1179
1320
  return null;
1321
+ // Re-apply the configured tmux history-limit after respawn (kept in sync
1322
+ // with the live setting via setHistoryLimit()).
1323
+ if (!IS_TEST_MODE) {
1324
+ await execAsync(`${this.tmux()} set-option -t ${shellescape(muxName)} history-limit ${historyLimit}`, {
1325
+ timeout: EXEC_TIMEOUT_MS,
1326
+ }).catch(() => {
1327
+ /* Non-critical — keeps existing tmux history-limit */
1328
+ });
1329
+ }
1180
1330
  // Resolve CLI binary directory based on mode
1181
1331
  const { pathExport } = this.buildPathExport(mode);
1182
1332
  const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
@@ -1194,7 +1344,8 @@ export class TmuxManager extends EventEmitter {
1194
1344
  });
1195
1345
  const config = niceConfig || DEFAULT_NICE_CONFIG;
1196
1346
  const cmd = wrapWithNice(baseCmd, config);
1197
- const fullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
1347
+ const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
1348
+ const fullCmd = remote ? buildRemoteLaunchCommand({ mode, remote, sessionId }) : localFullCmd;
1198
1349
  try {
1199
1350
  // For OpenCode: set sensitive env vars via tmux setenv before respawn
1200
1351
  if (mode === 'opencode') {
@@ -1209,7 +1360,8 @@ export class TmuxManager extends EventEmitter {
1209
1360
  }
1210
1361
  // Re-apply user env overrides before respawn so the new shell inherits them.
1211
1362
  this.applyEnvOverrides(muxName, envOverrides);
1212
- const launchCmd = `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
1363
+ // -c /tmp + cd bounce — see createSession() for rationale (stale FUSE state).
1364
+ const launchCmd = remote ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
1213
1365
  await execAsync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`, {
1214
1366
  timeout: EXEC_TIMEOUT_MS,
1215
1367
  });
@@ -1369,6 +1521,20 @@ export class TmuxManager extends EventEmitter {
1369
1521
  // Session may already be dead
1370
1522
  }
1371
1523
  }
1524
+ // Strategy 3b: Remote sessions run a DURABLE tmux server on the remote host
1525
+ // (survives ssh drops), so killing only the local ssh wrapper above would
1526
+ // orphan the remote agent forever. Fire a best-effort `ssh … tmux kill-session`
1527
+ // — fire-and-forget so it NEVER blocks or throws the local kill (bounded by the
1528
+ // shared ConnectTimeout on an unreachable host).
1529
+ if (session.remote) {
1530
+ try {
1531
+ const remoteKillCmd = buildRemoteKillCommand({ remote: session.remote, sessionId });
1532
+ exec(remoteKillCmd, { timeout: EXEC_TIMEOUT_MS }, () => { });
1533
+ }
1534
+ catch {
1535
+ // Best-effort — a failure here must not affect the local kill result.
1536
+ }
1537
+ }
1372
1538
  // Strategy 4: Direct kill by PID as final fallback
1373
1539
  if (this.isProcessAlive(currentPid)) {
1374
1540
  try {
@@ -1718,6 +1884,21 @@ export class TmuxManager extends EventEmitter {
1718
1884
  this.saveSessions();
1719
1885
  }
1720
1886
  }
1887
+ /**
1888
+ * Apply a tmux history-limit to all tracked sessions (e.g. when the user
1889
+ * changes the terminal-history setting). Invalid limits fall back to the
1890
+ * default. Best-effort per session.
1891
+ */
1892
+ async setHistoryLimit(limit) {
1893
+ const safeLimit = Number.isSafeInteger(limit) && limit > 0 ? Math.trunc(limit) : DEFAULT_TMUX_HISTORY_LIMIT;
1894
+ if (IS_TEST_MODE) {
1895
+ return;
1896
+ }
1897
+ const updates = Array.from(this.sessions.values()).map((session) => execAsync(`${this.tmux()} set-option -t ${shellescape(session.muxName)} history-limit ${safeLimit}`, {
1898
+ timeout: EXEC_TIMEOUT_MS,
1899
+ }));
1900
+ await Promise.allSettled(updates);
1901
+ }
1721
1902
  /**
1722
1903
  * Send input directly to a tmux session using `send-keys`.
1723
1904
  *
@@ -1924,29 +2105,60 @@ export class TmuxManager extends EventEmitter {
1924
2105
  }
1925
2106
  }
1926
2107
  /**
1927
- * Capture the current visible text and SGR styles of a specific pane.
2108
+ * Capture a pane's text and SGR styles.
1928
2109
  *
1929
- * `capture-pane -e` is sanitized by `formatPaneSnapshot`: SGR color/style
1930
- * codes are preserved, while cursor/erase/scroll-region controls are stripped
1931
- * before rows are repainted at absolute positions in browser xterm.
2110
+ * Two modes:
2111
+ * - Visible (default): `capture-pane -p -e` grabs only the on-screen frame,
2112
+ * then `formatPaneSnapshot` repaints each row at its absolute position so
2113
+ * the browser xterm reproduces the live frame. Used for fast tab switches.
2114
+ * - Full history (`opts.fullHistory`): `capture-pane -p -e -J -S -<N>` grabs
2115
+ * the tmux scrollback (COD-47, bounded to the configured history limit),
2116
+ * returned as linear scrollback text with SGR codes preserved (NOT
2117
+ * repositioned — a multi-screen history can't be painted into a single
2118
+ * visible frame, so the snapshot repaint is skipped). `-J` re-joins lines
2119
+ * hard-wrapped at the pane width so they reflow in the browser xterm.
2120
+ * Used for full page reloads so the user gets back their scroll history.
2121
+ * Caveat: lines tmux has already evicted past its history-limit are gone.
1932
2122
  */
1933
- capturePaneBuffer(muxName, paneTarget) {
2123
+ capturePaneBuffer(muxName, paneTarget, opts) {
1934
2124
  if (IS_TEST_MODE)
1935
2125
  return '';
1936
- if (!isValidMuxName(muxName)) {
1937
- console.error('[TmuxManager] Invalid session name in capturePaneBuffer:', muxName);
2126
+ const target = resolveTmuxPaneTarget(muxName, paneTarget);
2127
+ if (!target) {
2128
+ console.error('[TmuxManager] Invalid pane target in capturePaneBuffer:', { muxName, paneTarget });
1938
2129
  return null;
1939
2130
  }
1940
- if (!SAFE_PANE_TARGET_PATTERN.test(paneTarget)) {
1941
- console.error('[TmuxManager] Invalid pane target:', paneTarget);
1942
- return null;
1943
- }
1944
- const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`;
2131
+ const fullHistory = opts?.fullHistory === true;
1945
2132
  try {
1946
- const buffer = execSync(`${this.tmux()} capture-pane -p -e -t ${shellescape(target)}`, {
2133
+ // `-S -<N>` starts the capture N lines above the visible frame (tmux
2134
+ // clamps to the top of history), so tmux never serializes more scrollback
2135
+ // than the configured history limit retains.
2136
+ const requestedLines = opts?.historyLimitLines;
2137
+ const historyLines = typeof requestedLines === 'number' && Number.isFinite(requestedLines) && requestedLines > 0
2138
+ ? Math.trunc(requestedLines)
2139
+ : DEFAULT_TMUX_HISTORY_LIMIT;
2140
+ const captureFlags = fullHistory ? `capture-pane -p -e -J -S -${historyLines}` : 'capture-pane -p -e';
2141
+ // execSync's default maxBuffer (1MB) kills multi-MB scrollback dumps
2142
+ // (ENOBUFS) and would silently degrade full-history capture to the byte
2143
+ // buffer for exactly the long sessions it exists for — size it from the
2144
+ // consumer's byte cap plus ANSI-overhead slack instead.
2145
+ const execOpts = {
1947
2146
  encoding: 'utf-8',
1948
2147
  timeout: EXEC_TIMEOUT_MS,
1949
- }).replace(/\n+$/g, '');
2148
+ };
2149
+ if (fullHistory) {
2150
+ execOpts.maxBuffer =
2151
+ (opts?.maxCaptureBytes ?? DEFAULT_TERMINAL_BUFFER_MAX_BYTES) + FULL_HISTORY_CAPTURE_SLACK_BYTES;
2152
+ }
2153
+ const buffer = execSync(`${this.tmux()} ${captureFlags} -t ${shellescape(target)}`, execOpts).replace(/\n+$/g, '');
2154
+ // Full-history spans many screens — return it as raw linear scrollback
2155
+ // rather than repainting rows at single-screen absolute positions. tmux
2156
+ // joins scrollback rows with a bare `\n`; normalize to `\r\n` so a fresh
2157
+ // xterm (convertEol:false) starts each replayed line at column 0 instead
2158
+ // of staircasing diagonally (COD-138).
2159
+ if (fullHistory) {
2160
+ return normalizeScrollbackEol(buffer);
2161
+ }
1950
2162
  try {
1951
2163
  const cursor = execSync(`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height}'`, {
1952
2164
  encoding: 'utf-8',
@@ -1967,10 +2179,21 @@ export class TmuxManager extends EventEmitter {
1967
2179
  catch (cursorErr) {
1968
2180
  console.error('[TmuxManager] Failed to query pane cursor after capture:', cursorErr);
1969
2181
  }
1970
- return buffer;
2182
+ // Cursor query failed or geometry was invalid, so we skip the absolute-
2183
+ // positioned snapshot repaint and fall back to the raw capture. Normalize
2184
+ // its bare `\n` line endings to `\r\n` so the replay doesn't staircase
2185
+ // diagonally in a fresh xterm (COD-138, same reason as the fullHistory path).
2186
+ return normalizeScrollbackEol(buffer);
1971
2187
  }
1972
2188
  catch (err) {
1973
- console.error('[TmuxManager] Failed to capture pane buffer:', err);
2189
+ // ENOBUFS carries the truncated multi-MB stdout on the error object —
2190
+ // log a concise line instead of dumping it into the journal.
2191
+ if (err?.code === 'ENOBUFS') {
2192
+ console.error('[TmuxManager] Pane capture exceeded maxBuffer (ENOBUFS); falling back to byte history');
2193
+ }
2194
+ else {
2195
+ console.error('[TmuxManager] Failed to capture pane buffer:', err);
2196
+ }
1974
2197
  return null;
1975
2198
  }
1976
2199
  }
@@ -1980,7 +2203,7 @@ export class TmuxManager extends EventEmitter {
1980
2203
  * Pane ids are not stable across respawns or restores, so callers should not
1981
2204
  * assume the first pane remains `%0`.
1982
2205
  */
1983
- captureActivePaneBuffer(muxName) {
2206
+ captureActivePaneBuffer(muxName, opts) {
1984
2207
  if (IS_TEST_MODE)
1985
2208
  return '';
1986
2209
  if (!isValidMuxName(muxName)) {
@@ -1993,7 +2216,7 @@ export class TmuxManager extends EventEmitter {
1993
2216
  timeout: EXEC_TIMEOUT_MS,
1994
2217
  }).trim();
1995
2218
  const target = resolveActivePaneTarget(output);
1996
- return target ? this.capturePaneBuffer(muxName, target) : null;
2219
+ return target ? this.capturePaneBuffer(muxName, target, opts) : null;
1997
2220
  }
1998
2221
  catch (err) {
1999
2222
  console.error('[TmuxManager] Failed to resolve active pane for capture:', err);