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.
- 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 +2 -3
- package/dist/config/buffer-limits.d.ts.map +1 -1
- package/dist/config/buffer-limits.js +5 -5
- 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 +30 -0
- package/dist/config/terminal-history.d.ts.map +1 -0
- package/dist/config/terminal-history.js +43 -0
- package/dist/config/terminal-history.js.map +1 -0
- 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 +37 -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 +27 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +102 -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 +100 -8
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +249 -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/config-port.d.ts +2 -0
- package/dist/web/ports/config-port.d.ts.map +1 -1
- 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 +614 -68
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +5 -0
- package/dist/web/routes/system-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 +117 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +168 -1
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +9 -2
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +82 -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
|
*
|
|
@@ -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
|
|
330
|
+
* Capture a pane's text and SGR styles.
|
|
248
331
|
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
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
|
|
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":"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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3C,OAAO,EACL,YAAY,EACZ,sBAAsB,EAGtB,KAAK,QAAQ,EAEb,KAAK,WAAW,EAEhB,KAAK,WAAW,EAGhB,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAWpB,OAAO,KAAK,EACV,mBAAmB,EACnB,UAAU,EACV,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,oBAAoB,CAAC;AAoC5B,wFAAwF;AACxF,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAoDnD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,SAA2B,GAAG,MAAM,CAGtF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAajE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWzF;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASrE;AAkOD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACzE,MAAM,CAkBR;AAuGD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,MAAM,CAkB9D;AAsGD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IAChD,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,CAiCT;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMpG;AA+HD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,WAAY,SAAQ,YAAa,YAAW,mBAAmB;IAC1E,QAAQ,CAAC,OAAO,EAAG,MAAM,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiC;IAC5D,OAAO,CAAC,aAAa,CAA+B;IACpD,OAAO,CAAC,iBAAiB,CAA+B;IACxD,yFAAyF;IACzF,OAAO,CAAC,aAAa,CAAkC;IAEvD,OAAO,CAAC,mBAAmB,CAAS;;IAUpC,0GAA0G;IAC1G,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,OAAO,CAAC,IAAI;IAKZ,OAAO,CAAC,YAAY;IAkDpB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAsBpB;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAyBvB;;;;;;;;OAQG;IACH,OAAO,CAAC,iBAAiB;IAgCzB;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAoBvB;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAIvB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAIxB;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC;IA+NvE;;OAEG;IACH,OAAO,CAAC,UAAU;IAoBlB;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAI1C;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAcpC;;;;OAIG;IACG,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA0FtE,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,YAAY;IAuBpB,OAAO,CAAC,cAAc;YAUR,mBAAmB;IAmBjC;;;;OAIG;IACG,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAsHtD,WAAW,IAAI,UAAU,EAAE;IAI3B,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAIrD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAU3D;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAwFvF,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IA0ChE,oBAAoB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAiH5D,oBAAoB,CAAC,UAAU,GAAE,MAAkC,GAAG,IAAI;IAe1E,mBAAmB,IAAI,IAAI;IAO3B;;;;;OAKG;IACH,kBAAkB,CAAC,UAAU,GAAE,MAAa,GAAG,IAAI;IA+BnD,iBAAiB,IAAI,IAAI;IAQzB,OAAO,IAAI,IAAI;IAKf,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAK1C,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI;IAQvD,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,GAAG,SAAS,GAAG,IAAI;IAQxF,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAQ3C,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAQ7D;;;;OAIG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAenD;;;;;;;OAOG;IACG,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA4DnE;;;;OAIG;IACG,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBxD;;;OAGG;IACG,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBzD;;;;OAIG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUtD;;;OAGG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAiCrD;;;OAGG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IA+C5E;;;;;;;;;;;;;;;OAeG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,kBAAkB,GAAG,MAAM,GAAG,IAAI;IAqFjG;;;;;OAKG;IACH,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,kBAAkB,GAAG,MAAM,GAAG,IAAI;IAoBlF;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO;IA6B/E;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO;IAyB1D,gBAAgB,IAAI,MAAM;IAI1B,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE;IAIxC,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAkB7C,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAyBlE,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,MAAM,CAAC,eAAe,IAAI,OAAO;CAQlC"}
|
package/dist/tmux-manager.js
CHANGED
|
@@ -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
|
|
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.
|
|
1072
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2108
|
+
* Capture a pane's text and SGR styles.
|
|
1928
2109
|
*
|
|
1929
|
-
*
|
|
1930
|
-
*
|
|
1931
|
-
*
|
|
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
|
-
|
|
1937
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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);
|