aicodeman 1.2.2 → 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.
- package/README.md +264 -90
- package/dist/attachment-magic.d.ts +13 -0
- package/dist/attachment-magic.d.ts.map +1 -1
- package/dist/attachment-magic.js +47 -0
- package/dist/attachment-magic.js.map +1 -1
- package/dist/attachment-registry.d.ts.map +1 -1
- package/dist/attachment-registry.js +13 -2
- package/dist/attachment-registry.js.map +1 -1
- package/dist/config/buffer-limits.d.ts +1 -2
- package/dist/config/buffer-limits.d.ts.map +1 -1
- package/dist/config/buffer-limits.js +1 -2
- package/dist/config/buffer-limits.js.map +1 -1
- package/dist/config/map-limits.d.ts +11 -0
- package/dist/config/map-limits.d.ts.map +1 -1
- package/dist/config/map-limits.js +11 -0
- package/dist/config/map-limits.js.map +1 -1
- package/dist/config/server-timing.d.ts +6 -0
- package/dist/config/server-timing.d.ts.map +1 -1
- package/dist/config/server-timing.js +9 -0
- package/dist/config/server-timing.js.map +1 -1
- package/dist/config/terminal-history.d.ts +11 -6
- package/dist/config/terminal-history.d.ts.map +1 -1
- package/dist/config/terminal-history.js +18 -8
- package/dist/config/terminal-history.js.map +1 -1
- package/dist/cron/cron-input.d.ts +30 -0
- package/dist/cron/cron-input.d.ts.map +1 -0
- package/dist/cron/cron-input.js +7 -0
- package/dist/cron/cron-input.js.map +1 -0
- package/dist/cron/cron-service.d.ts +92 -0
- package/dist/cron/cron-service.d.ts.map +1 -0
- package/dist/cron/cron-service.js +606 -0
- package/dist/cron/cron-service.js.map +1 -0
- package/dist/cron/cron-time.d.ts +29 -0
- package/dist/cron/cron-time.d.ts.map +1 -0
- package/dist/cron/cron-time.js +87 -0
- package/dist/cron/cron-time.js.map +1 -0
- package/dist/document-thumbnailer.d.ts +1 -1
- package/dist/document-thumbnailer.d.ts.map +1 -1
- package/dist/document-thumbnailer.js +11 -2
- package/dist/document-thumbnailer.js.map +1 -1
- package/dist/generated-artifact-attachments.d.ts +26 -0
- package/dist/generated-artifact-attachments.d.ts.map +1 -0
- package/dist/generated-artifact-attachments.js +61 -0
- package/dist/generated-artifact-attachments.js.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/mux-interface.d.ts +31 -5
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/remote-hosts.d.ts +66 -0
- package/dist/remote-hosts.d.ts.map +1 -0
- package/dist/remote-hosts.js +198 -0
- package/dist/remote-hosts.js.map +1 -0
- package/dist/services/unified-session-service.d.ts +115 -0
- package/dist/services/unified-session-service.d.ts.map +1 -0
- package/dist/services/unified-session-service.js +173 -0
- package/dist/services/unified-session-service.js.map +1 -0
- package/dist/session-cli-builder.d.ts +3 -1
- package/dist/session-cli-builder.d.ts.map +1 -1
- package/dist/session-cli-builder.js +26 -7
- package/dist/session-cli-builder.js.map +1 -1
- package/dist/session-pty-exit-breaker.d.ts +66 -0
- package/dist/session-pty-exit-breaker.d.ts.map +1 -0
- package/dist/session-pty-exit-breaker.js +77 -0
- package/dist/session-pty-exit-breaker.js.map +1 -0
- package/dist/session.d.ts +24 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +94 -11
- package/dist/session.js.map +1 -1
- package/dist/state-store.d.ts +14 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +49 -0
- package/dist/state-store.js.map +1 -1
- package/dist/tmux-manager.d.ts +94 -8
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +222 -26
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/api.d.ts +11 -0
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/api.js.map +1 -1
- package/dist/types/app-state.d.ts +5 -0
- package/dist/types/app-state.d.ts.map +1 -1
- package/dist/types/app-state.js.map +1 -1
- package/dist/types/cron.d.ts +88 -0
- package/dist/types/cron.d.ts.map +1 -0
- package/dist/types/cron.js +13 -0
- package/dist/types/cron.js.map +1 -0
- package/dist/types/session.d.ts +57 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/heic-jpeg-converter.d.ts +24 -0
- package/dist/web/heic-jpeg-converter.d.ts.map +1 -0
- package/dist/web/heic-jpeg-converter.js +76 -0
- package/dist/web/heic-jpeg-converter.js.map +1 -0
- package/dist/web/heic-jpeg-worker.d.ts +44 -0
- package/dist/web/heic-jpeg-worker.d.ts.map +1 -0
- package/dist/web/heic-jpeg-worker.js +78 -0
- package/dist/web/heic-jpeg-worker.js.map +1 -0
- package/dist/web/ports/cron-port.d.ts +9 -0
- package/dist/web/ports/cron-port.d.ts.map +1 -0
- package/dist/web/ports/cron-port.js +6 -0
- package/dist/web/ports/cron-port.js.map +1 -0
- package/dist/web/ports/index.d.ts +1 -0
- package/dist/web/ports/index.d.ts.map +1 -1
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.b0534652.js +38 -0
- package/dist/web/public/app.b0534652.js.br +0 -0
- package/dist/web/public/app.b0534652.js.gz +0 -0
- package/dist/web/public/{constants.1c779517.js → constants.5b18ed64.js} +65 -0
- package/dist/web/public/constants.5b18ed64.js.br +0 -0
- package/dist/web/public/constants.5b18ed64.js.gz +0 -0
- package/dist/web/public/cron-ui.js +328 -0
- package/dist/web/public/cron-ui.js.br +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
- package/dist/web/public/index.html +296 -25
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.63794d0b.js +1 -0
- package/dist/web/public/input-cjk.63794d0b.js.br +0 -0
- package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
- package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
- package/dist/web/public/mobile.0644e3ac.css +1 -0
- package/dist/web/public/mobile.0644e3ac.css.br +0 -0
- package/dist/web/public/mobile.0644e3ac.css.gz +0 -0
- package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.772e92c9.js → panels-ui.ca03853f.js} +25 -17
- package/dist/web/public/panels-ui.ca03853f.js.br +0 -0
- package/dist/web/public/panels-ui.ca03853f.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-ui.b51814ef.js +49 -0
- package/dist/web/public/session-ui.b51814ef.js.br +0 -0
- package/dist/web/public/session-ui.b51814ef.js.gz +0 -0
- package/dist/web/public/settings-ui.6c67c125.js +67 -0
- package/dist/web/public/settings-ui.6c67c125.js.br +0 -0
- package/dist/web/public/settings-ui.6c67c125.js.gz +0 -0
- package/dist/web/public/styles.e1ba7849.css +1 -0
- package/dist/web/public/styles.e1ba7849.css.br +0 -0
- package/dist/web/public/styles.e1ba7849.css.gz +0 -0
- package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.dbf141fa.js +3 -0
- package/dist/web/public/terminal-ui.dbf141fa.js.br +0 -0
- package/dist/web/public/terminal-ui.dbf141fa.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +115 -1
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/cron-routes.d.ts +11 -0
- package/dist/web/routes/cron-routes.d.ts.map +1 -0
- package/dist/web/routes/cron-routes.js +70 -0
- package/dist/web/routes/cron-routes.js.map +1 -0
- package/dist/web/routes/index.d.ts +1 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +1 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/respawn-routes.d.ts.map +1 -1
- package/dist/web/routes/respawn-routes.js +3 -0
- package/dist/web/routes/respawn-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +610 -68
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/ws-routes.d.ts.map +1 -1
- package/dist/web/routes/ws-routes.js +56 -17
- package/dist/web/routes/ws-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +113 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +145 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +8 -2
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +75 -13
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-listener-wiring.d.ts +5 -1
- package/dist/web/session-listener-wiring.d.ts.map +1 -1
- package/dist/web/session-listener-wiring.js +23 -1
- package/dist/web/session-listener-wiring.js.map +1 -1
- package/dist/web/sse-events.d.ts +15 -0
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +17 -0
- package/dist/web/sse-events.js.map +1 -1
- package/dist/web/ws-connection-registry.d.ts +77 -0
- package/dist/web/ws-connection-registry.d.ts.map +1 -0
- package/dist/web/ws-connection-registry.js +99 -0
- package/dist/web/ws-connection-registry.js.map +1 -0
- package/package.json +3 -1
- package/dist/web/public/app.a8d73c32.js +0 -38
- package/dist/web/public/app.a8d73c32.js.br +0 -0
- package/dist/web/public/app.a8d73c32.js.gz +0 -0
- package/dist/web/public/constants.1c779517.js.br +0 -0
- package/dist/web/public/constants.1c779517.js.gz +0 -0
- package/dist/web/public/input-cjk.4e950e0a.js +0 -1
- package/dist/web/public/input-cjk.4e950e0a.js.br +0 -0
- package/dist/web/public/input-cjk.4e950e0a.js.gz +0 -0
- package/dist/web/public/mobile.8a9aff7a.css +0 -1
- package/dist/web/public/mobile.8a9aff7a.css.br +0 -0
- package/dist/web/public/mobile.8a9aff7a.css.gz +0 -0
- package/dist/web/public/panels-ui.772e92c9.js.br +0 -0
- package/dist/web/public/panels-ui.772e92c9.js.gz +0 -0
- package/dist/web/public/session-ui.da3e53f2.js +0 -36
- package/dist/web/public/session-ui.da3e53f2.js.br +0 -0
- package/dist/web/public/session-ui.da3e53f2.js.gz +0 -0
- package/dist/web/public/settings-ui.38feb956.js +0 -61
- package/dist/web/public/settings-ui.38feb956.js.br +0 -0
- package/dist/web/public/settings-ui.38feb956.js.gz +0 -0
- package/dist/web/public/styles.1b3e6147.css +0 -1
- package/dist/web/public/styles.1b3e6147.css.br +0 -0
- package/dist/web/public/styles.1b3e6147.css.gz +0 -0
- package/dist/web/public/terminal-ui.865f21f6.js +0 -3
- package/dist/web/public/terminal-ui.865f21f6.js.br +0 -0
- package/dist/web/public/terminal-ui.865f21f6.js.gz +0 -0
package/dist/tmux-manager.d.ts
CHANGED
|
@@ -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
|
*
|
|
@@ -250,20 +327,29 @@ export declare class TmuxManager extends EventEmitter implements TerminalMultipl
|
|
|
250
327
|
*/
|
|
251
328
|
sendInputToPane(muxName: string, paneTarget: string, input: string): boolean;
|
|
252
329
|
/**
|
|
253
|
-
* Capture
|
|
330
|
+
* Capture a pane's text and SGR styles.
|
|
254
331
|
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
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.
|
|
258
344
|
*/
|
|
259
|
-
capturePaneBuffer(muxName: string, paneTarget
|
|
345
|
+
capturePaneBuffer(muxName: string, paneTarget?: string, opts?: PaneCaptureOptions): string | null;
|
|
260
346
|
/**
|
|
261
347
|
* Capture the active pane for a tmux session.
|
|
262
348
|
*
|
|
263
349
|
* Pane ids are not stable across respawns or restores, so callers should not
|
|
264
350
|
* assume the first pane remains `%0`.
|
|
265
351
|
*/
|
|
266
|
-
captureActivePaneBuffer(muxName: string): string | null;
|
|
352
|
+
captureActivePaneBuffer(muxName: string, opts?: PaneCaptureOptions): string | null;
|
|
267
353
|
/**
|
|
268
354
|
* Start piping pane output to a file using tmux pipe-pane.
|
|
269
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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3C,OAAO,EACL,YAAY,EACZ,sBAAsB,EAGtB,KAAK,QAAQ,
|
|
1
|
+
{"version":3,"file":"tmux-manager.d.ts","sourceRoot":"","sources":["../src/tmux-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/tmux-manager.js
CHANGED
|
@@ -30,12 +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';
|
|
38
|
-
import { DEFAULT_TMUX_HISTORY_LIMIT } from './config/terminal-history.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;
|
|
39
47
|
/** Delay after tmux session creation — enough for detached tmux to be queryable */
|
|
40
48
|
const TMUX_CREATION_WAIT_MS = 100;
|
|
41
49
|
/** Max retries for getPanePid — tmux server cold-start (e.g. macOS) may need extra time */
|
|
@@ -366,6 +374,25 @@ function truncatePaneLineByVisibleColumns(line, maxColumns) {
|
|
|
366
374
|
}
|
|
367
375
|
return result;
|
|
368
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
|
+
}
|
|
369
396
|
export function formatPaneSnapshot(lines, geometry) {
|
|
370
397
|
const cols = Math.max(1, geometry.cols);
|
|
371
398
|
// Paint the full pane width. Earlier this dropped the rightmost column
|
|
@@ -572,6 +599,107 @@ function buildSpawnCommand(options) {
|
|
|
572
599
|
}
|
|
573
600
|
return '$SHELL';
|
|
574
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
|
+
}
|
|
575
703
|
/**
|
|
576
704
|
* Set sensitive environment variables on a tmux session via setenv.
|
|
577
705
|
* These are inherited by panes but not visible in ps output or tmux history.
|
|
@@ -831,6 +959,12 @@ export class TmuxManager extends EventEmitter {
|
|
|
831
959
|
'export LC_ALL=en_US.UTF-8',
|
|
832
960
|
mode === 'codex' || mode === 'gemini' ? 'export COLORTERM=truecolor' : 'unset COLORTERM',
|
|
833
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}`] : []),
|
|
834
968
|
'export CODEMAN_MUX=1',
|
|
835
969
|
`export CODEMAN_SESSION_ID=${sessionId}`,
|
|
836
970
|
`export CODEMAN_MUX_NAME=${muxName}`,
|
|
@@ -941,7 +1075,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
941
1075
|
* In test mode: creates an in-memory session only (no real tmux session).
|
|
942
1076
|
*/
|
|
943
1077
|
async createSession(options) {
|
|
944
|
-
const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, } = options;
|
|
1078
|
+
const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, } = options;
|
|
945
1079
|
const muxName = `codeman-${sessionId.slice(0, 8)}`;
|
|
946
1080
|
if (!isValidMuxName(muxName)) {
|
|
947
1081
|
throw new Error('Invalid session name: contains unsafe characters');
|
|
@@ -957,6 +1091,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
957
1091
|
pid: 99999,
|
|
958
1092
|
createdAt: Date.now(),
|
|
959
1093
|
workingDir,
|
|
1094
|
+
remote,
|
|
960
1095
|
mode,
|
|
961
1096
|
attached: false,
|
|
962
1097
|
name,
|
|
@@ -996,7 +1131,8 @@ export class TmuxManager extends EventEmitter {
|
|
|
996
1131
|
const cmd = wrapWithNice(baseCmd, config);
|
|
997
1132
|
try {
|
|
998
1133
|
// Build the full command to run inside tmux
|
|
999
|
-
const
|
|
1134
|
+
const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
|
|
1135
|
+
const fullCmd = remote ? buildRemoteLaunchCommand({ mode, remote, sessionId }) : localFullCmd;
|
|
1000
1136
|
// Create tmux session in three steps to handle cold-start (no server running)
|
|
1001
1137
|
// and avoid the race where the command exits before remain-on-exit is set:
|
|
1002
1138
|
// 1. Create session with default shell (starts tmux server, stays alive)
|
|
@@ -1044,7 +1180,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1044
1180
|
this.applyEnvOverrides(muxName, envOverrides);
|
|
1045
1181
|
// Replace the shell with the actual command (no echo in terminal). Keep
|
|
1046
1182
|
// pane launch in /tmp, then cd inside bash against the current mount table.
|
|
1047
|
-
const launchCmd = `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
|
|
1183
|
+
const launchCmd = remote ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
|
|
1048
1184
|
execSync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`, {
|
|
1049
1185
|
timeout: EXEC_TIMEOUT_MS,
|
|
1050
1186
|
stdio: 'ignore',
|
|
@@ -1069,7 +1205,8 @@ export class TmuxManager extends EventEmitter {
|
|
|
1069
1205
|
/* Already set globally as fallback */
|
|
1070
1206
|
}),
|
|
1071
1207
|
// Raise tmux scrollback from its 2000-line default so re-attach preserves
|
|
1072
|
-
// more context.
|
|
1208
|
+
// more context. Intentionally exceeds the xterm-side DEFAULT_SCROLLBACK (50k
|
|
1209
|
+
// in constants.js), which stays lower to protect browser/mobile memory.
|
|
1073
1210
|
execAsync(`${this.tmux()} set-option -t "${muxName}" history-limit ${historyLimit}`, {
|
|
1074
1211
|
timeout: EXEC_TIMEOUT_MS,
|
|
1075
1212
|
})
|
|
@@ -1106,6 +1243,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1106
1243
|
pid,
|
|
1107
1244
|
createdAt: Date.now(),
|
|
1108
1245
|
workingDir,
|
|
1246
|
+
remote,
|
|
1109
1247
|
mode,
|
|
1110
1248
|
attached: false,
|
|
1111
1249
|
name,
|
|
@@ -1173,7 +1311,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1173
1311
|
* preserving the session and its scrollback buffer.
|
|
1174
1312
|
*/
|
|
1175
1313
|
async respawnPane(options) {
|
|
1176
|
-
const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, } = options;
|
|
1314
|
+
const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, resumeSessionId, envOverrides, effort, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, } = options;
|
|
1177
1315
|
const session = this.sessions.get(sessionId);
|
|
1178
1316
|
if (!session)
|
|
1179
1317
|
return null;
|
|
@@ -1206,7 +1344,8 @@ export class TmuxManager extends EventEmitter {
|
|
|
1206
1344
|
});
|
|
1207
1345
|
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
|
1208
1346
|
const cmd = wrapWithNice(baseCmd, config);
|
|
1209
|
-
const
|
|
1347
|
+
const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
|
|
1348
|
+
const fullCmd = remote ? buildRemoteLaunchCommand({ mode, remote, sessionId }) : localFullCmd;
|
|
1210
1349
|
try {
|
|
1211
1350
|
// For OpenCode: set sensitive env vars via tmux setenv before respawn
|
|
1212
1351
|
if (mode === 'opencode') {
|
|
@@ -1221,7 +1360,8 @@ export class TmuxManager extends EventEmitter {
|
|
|
1221
1360
|
}
|
|
1222
1361
|
// Re-apply user env overrides before respawn so the new shell inherits them.
|
|
1223
1362
|
this.applyEnvOverrides(muxName, envOverrides);
|
|
1224
|
-
|
|
1363
|
+
// -c /tmp + cd bounce — see createSession() for rationale (stale FUSE state).
|
|
1364
|
+
const launchCmd = remote ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
|
|
1225
1365
|
await execAsync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`, {
|
|
1226
1366
|
timeout: EXEC_TIMEOUT_MS,
|
|
1227
1367
|
});
|
|
@@ -1381,6 +1521,20 @@ export class TmuxManager extends EventEmitter {
|
|
|
1381
1521
|
// Session may already be dead
|
|
1382
1522
|
}
|
|
1383
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
|
+
}
|
|
1384
1538
|
// Strategy 4: Direct kill by PID as final fallback
|
|
1385
1539
|
if (this.isProcessAlive(currentPid)) {
|
|
1386
1540
|
try {
|
|
@@ -1951,29 +2105,60 @@ export class TmuxManager extends EventEmitter {
|
|
|
1951
2105
|
}
|
|
1952
2106
|
}
|
|
1953
2107
|
/**
|
|
1954
|
-
* Capture
|
|
2108
|
+
* Capture a pane's text and SGR styles.
|
|
1955
2109
|
*
|
|
1956
|
-
*
|
|
1957
|
-
*
|
|
1958
|
-
*
|
|
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.
|
|
1959
2122
|
*/
|
|
1960
|
-
capturePaneBuffer(muxName, paneTarget) {
|
|
2123
|
+
capturePaneBuffer(muxName, paneTarget, opts) {
|
|
1961
2124
|
if (IS_TEST_MODE)
|
|
1962
2125
|
return '';
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
}
|
|
1967
|
-
if (!SAFE_PANE_TARGET_PATTERN.test(paneTarget)) {
|
|
1968
|
-
console.error('[TmuxManager] Invalid pane target:', paneTarget);
|
|
2126
|
+
const target = resolveTmuxPaneTarget(muxName, paneTarget);
|
|
2127
|
+
if (!target) {
|
|
2128
|
+
console.error('[TmuxManager] Invalid pane target in capturePaneBuffer:', { muxName, paneTarget });
|
|
1969
2129
|
return null;
|
|
1970
2130
|
}
|
|
1971
|
-
const
|
|
2131
|
+
const fullHistory = opts?.fullHistory === true;
|
|
1972
2132
|
try {
|
|
1973
|
-
|
|
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 = {
|
|
1974
2146
|
encoding: 'utf-8',
|
|
1975
2147
|
timeout: EXEC_TIMEOUT_MS,
|
|
1976
|
-
}
|
|
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
|
+
}
|
|
1977
2162
|
try {
|
|
1978
2163
|
const cursor = execSync(`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height}'`, {
|
|
1979
2164
|
encoding: 'utf-8',
|
|
@@ -1994,10 +2179,21 @@ export class TmuxManager extends EventEmitter {
|
|
|
1994
2179
|
catch (cursorErr) {
|
|
1995
2180
|
console.error('[TmuxManager] Failed to query pane cursor after capture:', cursorErr);
|
|
1996
2181
|
}
|
|
1997
|
-
|
|
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);
|
|
1998
2187
|
}
|
|
1999
2188
|
catch (err) {
|
|
2000
|
-
|
|
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
|
+
}
|
|
2001
2197
|
return null;
|
|
2002
2198
|
}
|
|
2003
2199
|
}
|
|
@@ -2007,7 +2203,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2007
2203
|
* Pane ids are not stable across respawns or restores, so callers should not
|
|
2008
2204
|
* assume the first pane remains `%0`.
|
|
2009
2205
|
*/
|
|
2010
|
-
captureActivePaneBuffer(muxName) {
|
|
2206
|
+
captureActivePaneBuffer(muxName, opts) {
|
|
2011
2207
|
if (IS_TEST_MODE)
|
|
2012
2208
|
return '';
|
|
2013
2209
|
if (!isValidMuxName(muxName)) {
|
|
@@ -2020,7 +2216,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2020
2216
|
timeout: EXEC_TIMEOUT_MS,
|
|
2021
2217
|
}).trim();
|
|
2022
2218
|
const target = resolveActivePaneTarget(output);
|
|
2023
|
-
return target ? this.capturePaneBuffer(muxName, target) : null;
|
|
2219
|
+
return target ? this.capturePaneBuffer(muxName, target, opts) : null;
|
|
2024
2220
|
}
|
|
2025
2221
|
catch (err) {
|
|
2026
2222
|
console.error('[TmuxManager] Failed to resolve active pane for capture:', err);
|