aicodeman 0.9.7 → 0.9.9
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/LICENSE +1 -1
- package/README.md +11 -3
- package/dist/file-stream-manager.d.ts.map +1 -1
- package/dist/file-stream-manager.js +6 -2
- package/dist/file-stream-manager.js.map +1 -1
- package/dist/mux-interface.d.ts +11 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/session.d.ts +39 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +75 -8
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +44 -3
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +446 -18
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/api.d.ts +10 -17
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/api.js +32 -3
- package/dist/types/api.js.map +1 -1
- package/dist/types/session.d.ts +17 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/utils/codex-cli-resolver.d.ts +21 -0
- package/dist/utils/codex-cli-resolver.d.ts.map +1 -0
- package/dist/utils/codex-cli-resolver.js +64 -0
- package/dist/utils/codex-cli-resolver.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/push-endpoint-validation.d.ts +6 -0
- package/dist/utils/push-endpoint-validation.d.ts.map +1 -0
- package/dist/utils/push-endpoint-validation.js +80 -0
- package/dist/utils/push-endpoint-validation.js.map +1 -0
- package/dist/web/public/{api-client.3adebdc2.js → api-client.c9b1cddc.js} +10 -1
- package/dist/web/public/api-client.c9b1cddc.js.br +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.a8663e79.js +35 -0
- package/dist/web/public/app.a8663e79.js.br +0 -0
- package/dist/web/public/app.a8663e79.js.gz +0 -0
- package/dist/web/public/{constants.5b68d2de.js → constants.74211deb.js} +1 -0
- package/dist/web/public/constants.74211deb.js.br +0 -0
- package/dist/web/public/constants.74211deb.js.gz +0 -0
- package/dist/web/public/{image-input.7cade6a8.js → image-input.0ea86695.js} +1 -1
- package/dist/web/public/{image-input.7cade6a8.js.br → image-input.0ea86695.js.br} +0 -0
- package/dist/web/public/image-input.0ea86695.js.gz +0 -0
- package/dist/web/public/index.html +66 -21
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.b8686b5e.js +1 -0
- package/dist/web/public/input-cjk.b8686b5e.js.br +0 -0
- package/dist/web/public/input-cjk.b8686b5e.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.cdfd8c04.js → keyboard-accessory.bc753cc7.js} +3 -2
- package/dist/web/public/keyboard-accessory.bc753cc7.js.br +0 -0
- package/dist/web/public/keyboard-accessory.bc753cc7.js.gz +0 -0
- package/dist/web/public/{mobile-handlers.1e2a8ef8.js → mobile-handlers.d54d97d6.js} +26 -10
- package/dist/web/public/mobile-handlers.d54d97d6.js.br +0 -0
- package/dist/web/public/mobile-handlers.d54d97d6.js.gz +0 -0
- package/dist/web/public/mobile.959f6fe2.css +1 -0
- package/dist/web/public/mobile.959f6fe2.css.br +0 -0
- package/dist/web/public/mobile.959f6fe2.css.gz +0 -0
- package/dist/web/public/notification-manager.9c984ac2.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js +3 -3
- package/dist/web/public/orchestrator-panel.js.br +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.5192a2c0.js → panels-ui.6bb3169f.js} +4 -4
- package/dist/web/public/panels-ui.6bb3169f.js.br +0 -0
- package/dist/web/public/panels-ui.6bb3169f.js.gz +0 -0
- package/dist/web/public/{ralph-panel.61076370.js → ralph-panel.6de2d0f8.js} +2 -2
- package/dist/web/public/{ralph-panel.61076370.js.br → ralph-panel.6de2d0f8.js.br} +0 -0
- package/dist/web/public/{ralph-panel.61076370.js.gz → ralph-panel.6de2d0f8.js.gz} +0 -0
- package/dist/web/public/{ralph-wizard.52d533d2.js → ralph-wizard.a6b2d36b.js} +6 -6
- package/dist/web/public/ralph-wizard.a6b2d36b.js.br +0 -0
- package/dist/web/public/ralph-wizard.a6b2d36b.js.gz +0 -0
- package/dist/web/public/{respawn-ui.5377f958.js → respawn-ui.2d249da9.js} +1 -1
- package/dist/web/public/{respawn-ui.5377f958.js.br → respawn-ui.2d249da9.js.br} +0 -0
- package/dist/web/public/{respawn-ui.5377f958.js.gz → respawn-ui.2d249da9.js.gz} +0 -0
- package/dist/web/public/session-ui.512816d8.js +36 -0
- package/dist/web/public/session-ui.512816d8.js.br +0 -0
- package/dist/web/public/session-ui.512816d8.js.gz +0 -0
- package/dist/web/public/settings-ui.21b009ca.js +55 -0
- package/dist/web/public/settings-ui.21b009ca.js.br +0 -0
- package/dist/web/public/settings-ui.21b009ca.js.gz +0 -0
- package/dist/web/public/styles.f3a0faa3.css +1 -0
- package/dist/web/public/styles.f3a0faa3.css.br +0 -0
- package/dist/web/public/styles.f3a0faa3.css.gz +0 -0
- package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.6ce91b0b.js +3 -0
- package/dist/web/public/terminal-ui.6ce91b0b.js.br +0 -0
- package/dist/web/public/terminal-ui.6ce91b0b.js.gz +0 -0
- package/dist/web/public/upload.html.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-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 +1 -2
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/clipboard-routes.d.ts.map +1 -1
- package/dist/web/routes/clipboard-routes.js +3 -2
- package/dist/web/routes/clipboard-routes.js.map +1 -1
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +5 -2
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.js +1 -1
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/mux-routes.js +3 -3
- package/dist/web/routes/mux-routes.js.map +1 -1
- package/dist/web/routes/plan-routes.js +1 -1
- package/dist/web/routes/plan-routes.js.map +1 -1
- package/dist/web/routes/push-routes.js +2 -2
- package/dist/web/routes/push-routes.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +6 -6
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/respawn-routes.d.ts.map +1 -1
- package/dist/web/routes/respawn-routes.js +17 -17
- package/dist/web/routes/respawn-routes.js.map +1 -1
- package/dist/web/routes/scheduled-routes.js +2 -2
- package/dist/web/routes/scheduled-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +55 -29
- 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 +20 -17
- 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 +21 -1
- package/dist/web/routes/ws-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +28 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +38 -6
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +72 -18
- package/dist/web/server.js.map +1 -1
- package/package.json +6 -3
- package/dist/web/public/api-client.3adebdc2.js.br +0 -0
- package/dist/web/public/api-client.3adebdc2.js.gz +0 -0
- package/dist/web/public/app.c860ea08.js +0 -34
- package/dist/web/public/app.c860ea08.js.br +0 -0
- package/dist/web/public/app.c860ea08.js.gz +0 -0
- package/dist/web/public/constants.5b68d2de.js.br +0 -0
- package/dist/web/public/constants.5b68d2de.js.gz +0 -0
- package/dist/web/public/image-input.7cade6a8.js.gz +0 -0
- package/dist/web/public/input-cjk.88082175.js +0 -1
- package/dist/web/public/input-cjk.88082175.js.br +0 -0
- package/dist/web/public/input-cjk.88082175.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.cdfd8c04.js.br +0 -0
- package/dist/web/public/keyboard-accessory.cdfd8c04.js.gz +0 -0
- package/dist/web/public/mobile-handlers.1e2a8ef8.js.br +0 -0
- package/dist/web/public/mobile-handlers.1e2a8ef8.js.gz +0 -0
- package/dist/web/public/mobile.26dc30d6.css +0 -1
- package/dist/web/public/mobile.26dc30d6.css.br +0 -0
- package/dist/web/public/mobile.26dc30d6.css.gz +0 -0
- package/dist/web/public/panels-ui.5192a2c0.js.br +0 -0
- package/dist/web/public/panels-ui.5192a2c0.js.gz +0 -0
- package/dist/web/public/ralph-wizard.52d533d2.js.br +0 -0
- package/dist/web/public/ralph-wizard.52d533d2.js.gz +0 -0
- package/dist/web/public/session-ui.3e0cf024.js +0 -36
- package/dist/web/public/session-ui.3e0cf024.js.br +0 -0
- package/dist/web/public/session-ui.3e0cf024.js.gz +0 -0
- package/dist/web/public/settings-ui.2b70e2c8.js +0 -55
- package/dist/web/public/settings-ui.2b70e2c8.js.br +0 -0
- package/dist/web/public/settings-ui.2b70e2c8.js.gz +0 -0
- package/dist/web/public/styles.e87cb785.css +0 -1
- package/dist/web/public/styles.e87cb785.css.br +0 -0
- package/dist/web/public/styles.e87cb785.css.gz +0 -0
- package/dist/web/public/terminal-ui.37caa926.js +0 -3
- package/dist/web/public/terminal-ui.37caa926.js.br +0 -0
- package/dist/web/public/terminal-ui.37caa926.js.gz +0 -0
package/dist/tmux-manager.js
CHANGED
|
@@ -30,7 +30,7 @@ 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 { wrapWithNice, SAFE_PATH_PATTERN, findClaudeDir, resolveOpenCodeDir } from './utils/index.js';
|
|
33
|
+
import { wrapWithNice, SAFE_PATH_PATTERN, findClaudeDir, resolveOpenCodeDir, resolveCodexDir } from './utils/index.js';
|
|
34
34
|
// ============================================================================
|
|
35
35
|
// Timing Constants
|
|
36
36
|
// ============================================================================
|
|
@@ -126,6 +126,259 @@ export function parsePaneList(output) {
|
|
|
126
126
|
}
|
|
127
127
|
return result;
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Resolve a target pane id from `tmux list-panes -F '#{pane_id}:#{pane_active}'`.
|
|
131
|
+
* Prefers the active pane and falls back to the first valid pane.
|
|
132
|
+
*/
|
|
133
|
+
export function resolveTmuxPaneTarget(muxName, paneTarget) {
|
|
134
|
+
if (!isValidMuxName(muxName)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (paneTarget === undefined || paneTarget === 'active') {
|
|
138
|
+
return muxName;
|
|
139
|
+
}
|
|
140
|
+
if (!SAFE_PANE_TARGET_PATTERN.test(paneTarget)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return `${muxName}.${paneTarget}`;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Pick the active pane id from `tmux list-panes -F '#{pane_id}:#{pane_active}'`
|
|
147
|
+
* output (lines like `%0:1`). Returns the pane id whose active flag is 1.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveActivePaneTarget(output) {
|
|
150
|
+
for (const line of output.split('\n')) {
|
|
151
|
+
const sep = line.indexOf(':');
|
|
152
|
+
if (sep === -1)
|
|
153
|
+
continue;
|
|
154
|
+
const paneId = line.slice(0, sep).trim();
|
|
155
|
+
const active = line.slice(sep + 1).trim();
|
|
156
|
+
if (paneId && active === '1')
|
|
157
|
+
return paneId;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const GRAPHEME_SEGMENTER = (() => {
|
|
162
|
+
try {
|
|
163
|
+
const Segmenter = Intl.Segmenter;
|
|
164
|
+
return Segmenter ? new Segmenter(undefined, { granularity: 'grapheme' }) : null;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
170
|
+
function findEscapeEnd(text, start) {
|
|
171
|
+
const type = text[start + 1];
|
|
172
|
+
if (type === '[') {
|
|
173
|
+
for (let i = start + 2; i < text.length; i++) {
|
|
174
|
+
const code = text.charCodeAt(i);
|
|
175
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
176
|
+
return i;
|
|
177
|
+
}
|
|
178
|
+
return text.length - 1;
|
|
179
|
+
}
|
|
180
|
+
if (type === ']') {
|
|
181
|
+
for (let i = start + 2; i < text.length; i++) {
|
|
182
|
+
if (text.charCodeAt(i) === 0x07)
|
|
183
|
+
return i;
|
|
184
|
+
if (text[i] === '\x1b' && text[i + 1] === '\\')
|
|
185
|
+
return i + 1;
|
|
186
|
+
}
|
|
187
|
+
return text.length - 1;
|
|
188
|
+
}
|
|
189
|
+
if (type === 'P' || type === '^' || type === '_' || type === 'X') {
|
|
190
|
+
for (let i = start + 2; i < text.length; i++) {
|
|
191
|
+
if (text.charCodeAt(i) === 0x07)
|
|
192
|
+
return i;
|
|
193
|
+
if (text[i] === '\x1b' && text[i + 1] === '\\')
|
|
194
|
+
return i + 1;
|
|
195
|
+
}
|
|
196
|
+
return text.length - 1;
|
|
197
|
+
}
|
|
198
|
+
return Math.min(start + 1, text.length - 1);
|
|
199
|
+
}
|
|
200
|
+
function sanitizePaneLineStyles(line) {
|
|
201
|
+
let result = '';
|
|
202
|
+
for (let i = 0; i < line.length; i++) {
|
|
203
|
+
if (line[i] !== '\x1b') {
|
|
204
|
+
result += line[i];
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const end = findEscapeEnd(line, i);
|
|
208
|
+
const sequence = line.slice(i, end + 1);
|
|
209
|
+
if (isSgrSequence(sequence)) {
|
|
210
|
+
result += sequence;
|
|
211
|
+
}
|
|
212
|
+
i = end;
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
function isSgrSequence(sequence) {
|
|
217
|
+
return (sequence.length >= 3 &&
|
|
218
|
+
sequence.charCodeAt(0) === 27 &&
|
|
219
|
+
sequence[1] === '[' &&
|
|
220
|
+
sequence.endsWith('m') &&
|
|
221
|
+
/^[0-9;:]*$/.test(sequence.slice(2, -1)));
|
|
222
|
+
}
|
|
223
|
+
function isZeroWidthCodePoint(codePoint) {
|
|
224
|
+
return (codePoint === 0x00ad ||
|
|
225
|
+
codePoint === 0x034f ||
|
|
226
|
+
codePoint === 0x061c ||
|
|
227
|
+
codePoint === 0x115f ||
|
|
228
|
+
codePoint === 0x1160 ||
|
|
229
|
+
codePoint === 0x17b4 ||
|
|
230
|
+
codePoint === 0x17b5 ||
|
|
231
|
+
codePoint === 0x180e ||
|
|
232
|
+
codePoint === 0x200b ||
|
|
233
|
+
codePoint === 0x200c ||
|
|
234
|
+
codePoint === 0x200d ||
|
|
235
|
+
codePoint === 0x2060 ||
|
|
236
|
+
codePoint === 0xfeff ||
|
|
237
|
+
(codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
238
|
+
(codePoint >= 0x0483 && codePoint <= 0x0489) ||
|
|
239
|
+
(codePoint >= 0x0591 && codePoint <= 0x05bd) ||
|
|
240
|
+
codePoint === 0x05bf ||
|
|
241
|
+
(codePoint >= 0x05c1 && codePoint <= 0x05c2) ||
|
|
242
|
+
(codePoint >= 0x05c4 && codePoint <= 0x05c5) ||
|
|
243
|
+
codePoint === 0x05c7 ||
|
|
244
|
+
(codePoint >= 0x0610 && codePoint <= 0x061a) ||
|
|
245
|
+
(codePoint >= 0x064b && codePoint <= 0x065f) ||
|
|
246
|
+
codePoint === 0x0670 ||
|
|
247
|
+
(codePoint >= 0x06d6 && codePoint <= 0x06dc) ||
|
|
248
|
+
(codePoint >= 0x06df && codePoint <= 0x06e4) ||
|
|
249
|
+
(codePoint >= 0x06e7 && codePoint <= 0x06e8) ||
|
|
250
|
+
(codePoint >= 0x06ea && codePoint <= 0x06ed) ||
|
|
251
|
+
codePoint === 0x0711 ||
|
|
252
|
+
(codePoint >= 0x0730 && codePoint <= 0x074a) ||
|
|
253
|
+
(codePoint >= 0x07a6 && codePoint <= 0x07b0) ||
|
|
254
|
+
(codePoint >= 0x07eb && codePoint <= 0x07f3) ||
|
|
255
|
+
(codePoint >= 0x0816 && codePoint <= 0x0819) ||
|
|
256
|
+
(codePoint >= 0x081b && codePoint <= 0x0823) ||
|
|
257
|
+
(codePoint >= 0x0825 && codePoint <= 0x0827) ||
|
|
258
|
+
(codePoint >= 0x0829 && codePoint <= 0x082d) ||
|
|
259
|
+
(codePoint >= 0x0859 && codePoint <= 0x085b) ||
|
|
260
|
+
(codePoint >= 0x08d3 && codePoint <= 0x08e1) ||
|
|
261
|
+
(codePoint >= 0x08e3 && codePoint <= 0x0902) ||
|
|
262
|
+
(codePoint >= 0x093a && codePoint <= 0x093c) ||
|
|
263
|
+
codePoint === 0x094d ||
|
|
264
|
+
(codePoint >= 0x0951 && codePoint <= 0x0957) ||
|
|
265
|
+
(codePoint >= 0x0962 && codePoint <= 0x0963) ||
|
|
266
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
267
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
|
|
268
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
|
|
269
|
+
(codePoint >= 0xfe00 && codePoint <= 0xfe0f) ||
|
|
270
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f) ||
|
|
271
|
+
(codePoint >= 0xe0100 && codePoint <= 0xe01ef));
|
|
272
|
+
}
|
|
273
|
+
function isWideCodePoint(codePoint) {
|
|
274
|
+
return (codePoint >= 0x1100 &&
|
|
275
|
+
(codePoint <= 0x115f ||
|
|
276
|
+
codePoint === 0x2329 ||
|
|
277
|
+
codePoint === 0x232a ||
|
|
278
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
279
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
280
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
281
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
282
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
283
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
284
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
285
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1faff) ||
|
|
286
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
287
|
+
}
|
|
288
|
+
function nextGrapheme(text, start) {
|
|
289
|
+
if (GRAPHEME_SEGMENTER) {
|
|
290
|
+
const iterator = GRAPHEME_SEGMENTER.segment(text.slice(start))[Symbol.iterator]();
|
|
291
|
+
const next = iterator.next();
|
|
292
|
+
if (!next.done && next.value.segment) {
|
|
293
|
+
return { value: next.value.segment, nextIndex: start + next.value.segment.length };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const first = text.codePointAt(start);
|
|
297
|
+
if (first === undefined)
|
|
298
|
+
return { value: '', nextIndex: start + 1 };
|
|
299
|
+
let value = String.fromCodePoint(first);
|
|
300
|
+
let nextIndex = start + value.length;
|
|
301
|
+
while (nextIndex < text.length) {
|
|
302
|
+
const codePoint = text.codePointAt(nextIndex);
|
|
303
|
+
if (codePoint === undefined || !isZeroWidthCodePoint(codePoint))
|
|
304
|
+
break;
|
|
305
|
+
const mark = String.fromCodePoint(codePoint);
|
|
306
|
+
value += mark;
|
|
307
|
+
nextIndex += mark.length;
|
|
308
|
+
}
|
|
309
|
+
return { value, nextIndex };
|
|
310
|
+
}
|
|
311
|
+
function terminalCellWidth(grapheme) {
|
|
312
|
+
let hasVisible = false;
|
|
313
|
+
let hasWide = false;
|
|
314
|
+
for (let i = 0; i < grapheme.length; i++) {
|
|
315
|
+
const codePoint = grapheme.codePointAt(i);
|
|
316
|
+
if (codePoint === undefined)
|
|
317
|
+
continue;
|
|
318
|
+
if (codePoint > 0xffff)
|
|
319
|
+
i++;
|
|
320
|
+
if (isZeroWidthCodePoint(codePoint) || codePoint < 0x20 || (codePoint >= 0x7f && codePoint < 0xa0)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
hasVisible = true;
|
|
324
|
+
if (isWideCodePoint(codePoint))
|
|
325
|
+
hasWide = true;
|
|
326
|
+
}
|
|
327
|
+
if (!hasVisible)
|
|
328
|
+
return 0;
|
|
329
|
+
return hasWide ? 2 : 1;
|
|
330
|
+
}
|
|
331
|
+
function truncatePaneLineByVisibleColumns(line, maxColumns) {
|
|
332
|
+
let result = '';
|
|
333
|
+
let visibleColumns = 0;
|
|
334
|
+
let sawSgr = false;
|
|
335
|
+
for (let i = 0; i < line.length; i++) {
|
|
336
|
+
if (line[i] === '\x1b') {
|
|
337
|
+
const end = findEscapeEnd(line, i);
|
|
338
|
+
const sequence = line.slice(i, end + 1);
|
|
339
|
+
if (isSgrSequence(sequence)) {
|
|
340
|
+
result += sequence;
|
|
341
|
+
sawSgr = true;
|
|
342
|
+
}
|
|
343
|
+
i = end;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const grapheme = nextGrapheme(line, i);
|
|
347
|
+
const width = terminalCellWidth(grapheme.value);
|
|
348
|
+
if (width === 0) {
|
|
349
|
+
result += grapheme.value;
|
|
350
|
+
}
|
|
351
|
+
else if (visibleColumns + width <= maxColumns) {
|
|
352
|
+
result += grapheme.value;
|
|
353
|
+
visibleColumns += width;
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
i = grapheme.nextIndex - 1;
|
|
359
|
+
if (visibleColumns >= maxColumns) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (sawSgr) {
|
|
364
|
+
result += '\x1b[0m';
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
export function formatPaneSnapshot(lines, geometry) {
|
|
369
|
+
const cols = Math.max(1, geometry.cols);
|
|
370
|
+
const paintCols = Math.max(1, cols - 1);
|
|
371
|
+
const rows = Math.max(1, geometry.rows);
|
|
372
|
+
const parts = [];
|
|
373
|
+
for (let row = 0; row < Math.min(lines.length, rows); row++) {
|
|
374
|
+
const safeLine = truncatePaneLineByVisibleColumns(sanitizePaneLineStyles(lines[row]), paintCols);
|
|
375
|
+
parts.push(`\x1b[${row + 1};1H${safeLine}`);
|
|
376
|
+
}
|
|
377
|
+
const cursorX = Math.max(0, Math.min(cols - 1, geometry.cursorX));
|
|
378
|
+
const cursorY = Math.max(0, Math.min(rows - 1, geometry.cursorY));
|
|
379
|
+
parts.push(`\x1b[${cursorY + 1};${cursorX + 1}H`);
|
|
380
|
+
return parts.join('');
|
|
381
|
+
}
|
|
129
382
|
/** Characters unsafe in paths — shell metacharacters, quotes, and control chars */
|
|
130
383
|
const UNSAFE_PATH_CHARS = /[;&|$`(){}<>'"\n\r]/;
|
|
131
384
|
/**
|
|
@@ -135,6 +388,9 @@ const UNSAFE_PATH_CHARS = /[;&|$`(){}<>'"\n\r]/;
|
|
|
135
388
|
function isValidMuxName(name) {
|
|
136
389
|
return SAFE_MUX_NAME_PATTERN.test(name) || LEGACY_MUX_NAME_PATTERN.test(name);
|
|
137
390
|
}
|
|
391
|
+
function isValidTerminalDimension(value) {
|
|
392
|
+
return Number.isSafeInteger(value) && value > 0 && value <= 1000;
|
|
393
|
+
}
|
|
138
394
|
/**
|
|
139
395
|
* Validates that a path contains only safe characters.
|
|
140
396
|
* Prevents command injection via malformed paths.
|
|
@@ -215,6 +471,29 @@ function buildOpenCodeCommand(config) {
|
|
|
215
471
|
}
|
|
216
472
|
return parts.join(' ');
|
|
217
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Build the codex CLI command with appropriate flags.
|
|
476
|
+
*
|
|
477
|
+
* Codeman launches Codex's native TUI and handles replay/scrollback by
|
|
478
|
+
* stripping destructive terminal sequences before xterm.js sees them.
|
|
479
|
+
*/
|
|
480
|
+
export function buildCodexCommand(config) {
|
|
481
|
+
const parts = ['codex'];
|
|
482
|
+
if (config?.dangerouslyBypassApprovals) {
|
|
483
|
+
parts.push('--dangerously-bypass-approvals-and-sandbox');
|
|
484
|
+
}
|
|
485
|
+
if (config?.model) {
|
|
486
|
+
const safeModel = /^[a-zA-Z0-9._\-/]+$/.test(config.model) ? config.model : undefined;
|
|
487
|
+
if (safeModel)
|
|
488
|
+
parts.push('--model', safeModel);
|
|
489
|
+
}
|
|
490
|
+
if (config?.resumeSessionId) {
|
|
491
|
+
const safeId = /^[a-zA-Z0-9_-]+$/.test(config.resumeSessionId) ? config.resumeSessionId : undefined;
|
|
492
|
+
if (safeId)
|
|
493
|
+
parts.push('resume', safeId);
|
|
494
|
+
}
|
|
495
|
+
return parts.join(' ');
|
|
496
|
+
}
|
|
218
497
|
/**
|
|
219
498
|
* Build the spawn command for any session mode.
|
|
220
499
|
* Shared by createSession() and respawnPane() to avoid duplication.
|
|
@@ -253,6 +532,9 @@ function buildSpawnCommand(options) {
|
|
|
253
532
|
if (options.mode === 'opencode') {
|
|
254
533
|
return buildOpenCodeCommand(options.openCodeConfig);
|
|
255
534
|
}
|
|
535
|
+
if (options.mode === 'codex') {
|
|
536
|
+
return buildCodexCommand(options.codexConfig);
|
|
537
|
+
}
|
|
256
538
|
return '$SHELL';
|
|
257
539
|
}
|
|
258
540
|
/**
|
|
@@ -279,6 +561,29 @@ function setOpenCodeEnvVars(tmuxCmd, muxName) {
|
|
|
279
561
|
}
|
|
280
562
|
}
|
|
281
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* Set sensitive environment variables for Codex on a tmux session via setenv.
|
|
566
|
+
* Codex (OpenAI CLI) needs OPENAI_API_KEY; we also forward CODEX_* keys.
|
|
567
|
+
*/
|
|
568
|
+
function setCodexEnvVars(tmuxCmd, muxName) {
|
|
569
|
+
const sensitiveVars = ['OPENAI_API_KEY', 'CODEX_API_KEY', 'CODEX_HOME'];
|
|
570
|
+
for (const key of sensitiveVars) {
|
|
571
|
+
const val = process.env[key];
|
|
572
|
+
if (val) {
|
|
573
|
+
const escaped = val.replace(/'/g, "'\\''");
|
|
574
|
+
try {
|
|
575
|
+
execSync(`${tmuxCmd} setenv -t '${muxName}' ${key} '${escaped}'`, {
|
|
576
|
+
encoding: 'utf8',
|
|
577
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
578
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
/* Non-critical — key may not be needed */
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
282
587
|
/**
|
|
283
588
|
* Set OPENCODE_CONFIG_CONTENT on a tmux session via setenv.
|
|
284
589
|
* Uses tmux setenv to avoid shell metacharacter injection from user-supplied JSON.
|
|
@@ -457,7 +762,8 @@ export class TmuxManager extends EventEmitter {
|
|
|
457
762
|
const exports = [
|
|
458
763
|
'export LANG=en_US.UTF-8',
|
|
459
764
|
'export LC_ALL=en_US.UTF-8',
|
|
460
|
-
'unset COLORTERM',
|
|
765
|
+
mode === 'codex' ? 'export COLORTERM=truecolor' : 'unset COLORTERM',
|
|
766
|
+
...(mode === 'codex' ? ['unset NO_COLOR'] : []),
|
|
461
767
|
'export CODEMAN_MUX=1',
|
|
462
768
|
`export CODEMAN_SESSION_ID=${sessionId}`,
|
|
463
769
|
`export CODEMAN_MUX_NAME=${muxName}`,
|
|
@@ -526,6 +832,10 @@ export class TmuxManager extends EventEmitter {
|
|
|
526
832
|
const dir = resolveOpenCodeDir();
|
|
527
833
|
return { pathExport: dir ? `export PATH="${dir}:$PATH" && ` : '', dir };
|
|
528
834
|
}
|
|
835
|
+
if (mode === 'codex') {
|
|
836
|
+
const dir = resolveCodexDir();
|
|
837
|
+
return { pathExport: dir ? `export PATH="${dir}:$PATH" && ` : '', dir };
|
|
838
|
+
}
|
|
529
839
|
return { pathExport: '', dir: null };
|
|
530
840
|
}
|
|
531
841
|
/**
|
|
@@ -538,12 +848,20 @@ export class TmuxManager extends EventEmitter {
|
|
|
538
848
|
setOpenCodeEnvVars(tmuxCmd, muxName);
|
|
539
849
|
setOpenCodeConfigContent(tmuxCmd, muxName, openCodeConfig);
|
|
540
850
|
}
|
|
851
|
+
/**
|
|
852
|
+
* Configure Codex-specific environment on a tmux session.
|
|
853
|
+
* Sets OPENAI_API_KEY (and related keys) via tmux setenv so secrets don't
|
|
854
|
+
* appear in the bash command line.
|
|
855
|
+
*/
|
|
856
|
+
_configureCodex(muxName) {
|
|
857
|
+
setCodexEnvVars(this.tmux(), muxName);
|
|
858
|
+
}
|
|
541
859
|
/**
|
|
542
860
|
* Creates a new tmux session wrapping Claude CLI or a shell.
|
|
543
861
|
* In test mode: creates an in-memory session only (no real tmux session).
|
|
544
862
|
*/
|
|
545
863
|
async createSession(options) {
|
|
546
|
-
const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig, resumeSessionId, envOverrides, effort, } = options;
|
|
864
|
+
const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, resumeSessionId, envOverrides, effort, } = options;
|
|
547
865
|
const muxName = `codeman-${sessionId.slice(0, 8)}`;
|
|
548
866
|
if (!isValidMuxName(muxName)) {
|
|
549
867
|
throw new Error('Invalid session name: contains unsafe characters');
|
|
@@ -583,6 +901,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
583
901
|
claudeMode,
|
|
584
902
|
allowedTools,
|
|
585
903
|
openCodeConfig,
|
|
904
|
+
codexConfig,
|
|
586
905
|
resumeSessionId,
|
|
587
906
|
effort,
|
|
588
907
|
});
|
|
@@ -600,14 +919,17 @@ export class TmuxManager extends EventEmitter {
|
|
|
600
919
|
// (Production uses systemd which has a clean env, but dev/test may be nested.)
|
|
601
920
|
const cleanEnv = { ...process.env };
|
|
602
921
|
delete cleanEnv.TMUX;
|
|
603
|
-
//
|
|
604
|
-
//
|
|
922
|
+
// Create the session on the dedicated socket (${this.tmux()} = `tmux -L <socket>`),
|
|
923
|
+
// launched in TMUX_LAUNCH_CWD (/tmp) rather than the real workingDir: a FUSE/rclone
|
|
924
|
+
// mount that isn't ready yet makes `getcwd` fail and breaks the spawn (see #110). The
|
|
925
|
+
// pane cd's into workingDir below via respawn-pane.
|
|
605
926
|
execSync(`${this.tmux()} new-session -ds "${muxName}" -c ${TMUX_LAUNCH_CWD}`, {
|
|
606
927
|
cwd: TMUX_LAUNCH_CWD,
|
|
607
928
|
timeout: EXEC_TIMEOUT_MS,
|
|
608
929
|
stdio: 'ignore',
|
|
609
930
|
env: cleanEnv,
|
|
610
931
|
});
|
|
932
|
+
this.resizeWindow(muxName, 120, 40);
|
|
611
933
|
// Set remain-on-exit now that the server is running — must be before respawn-pane
|
|
612
934
|
try {
|
|
613
935
|
execSync(`${this.tmux()} set-option -t "${muxName}" remain-on-exit on`, {
|
|
@@ -623,6 +945,9 @@ export class TmuxManager extends EventEmitter {
|
|
|
623
945
|
if (mode === 'opencode') {
|
|
624
946
|
this._configureOpenCode(muxName, openCodeConfig);
|
|
625
947
|
}
|
|
948
|
+
else if (mode === 'codex') {
|
|
949
|
+
this._configureCodex(muxName);
|
|
950
|
+
}
|
|
626
951
|
// Apply user-supplied env overrides (e.g., CLAUDE_CODE_EFFORT_LEVEL) via tmux setenv
|
|
627
952
|
// so secret values stay off the bash command line. Must run before respawn-pane.
|
|
628
953
|
this.applyEnvOverrides(muxName, envOverrides);
|
|
@@ -755,7 +1080,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
755
1080
|
* preserving the session and its scrollback buffer.
|
|
756
1081
|
*/
|
|
757
1082
|
async respawnPane(options) {
|
|
758
|
-
const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, resumeSessionId, envOverrides, effort, } = options;
|
|
1083
|
+
const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, resumeSessionId, envOverrides, effort, } = options;
|
|
759
1084
|
const session = this.sessions.get(sessionId);
|
|
760
1085
|
if (!session)
|
|
761
1086
|
return null;
|
|
@@ -772,6 +1097,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
772
1097
|
claudeMode,
|
|
773
1098
|
allowedTools,
|
|
774
1099
|
openCodeConfig,
|
|
1100
|
+
codexConfig,
|
|
775
1101
|
resumeSessionId,
|
|
776
1102
|
effort,
|
|
777
1103
|
});
|
|
@@ -783,6 +1109,9 @@ export class TmuxManager extends EventEmitter {
|
|
|
783
1109
|
if (mode === 'opencode') {
|
|
784
1110
|
this._configureOpenCode(muxName, openCodeConfig);
|
|
785
1111
|
}
|
|
1112
|
+
else if (mode === 'codex') {
|
|
1113
|
+
this._configureCodex(muxName);
|
|
1114
|
+
}
|
|
786
1115
|
// Re-apply user env overrides before respawn so the new shell inherits them.
|
|
787
1116
|
this.applyEnvOverrides(muxName, envOverrides);
|
|
788
1117
|
const launchCmd = `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
|
|
@@ -804,6 +1133,8 @@ export class TmuxManager extends EventEmitter {
|
|
|
804
1133
|
sessionExists(muxName) {
|
|
805
1134
|
if (IS_TEST_MODE)
|
|
806
1135
|
return false;
|
|
1136
|
+
if (!isValidMuxName(muxName))
|
|
1137
|
+
return false;
|
|
807
1138
|
try {
|
|
808
1139
|
execSync(`${this.tmux()} has-session -t "${muxName}" 2>/dev/null`, {
|
|
809
1140
|
encoding: 'utf-8',
|
|
@@ -932,14 +1263,16 @@ export class TmuxManager extends EventEmitter {
|
|
|
932
1263
|
// Process group may not exist or already terminated
|
|
933
1264
|
}
|
|
934
1265
|
}
|
|
935
|
-
// Strategy 3: Kill tmux session by name
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1266
|
+
// Strategy 3: Kill tmux session by name (guard the name before it reaches the shell)
|
|
1267
|
+
if (isValidMuxName(session.muxName)) {
|
|
1268
|
+
try {
|
|
1269
|
+
execSync(`${this.tmux()} kill-session -t "${session.muxName}" 2>/dev/null`, {
|
|
1270
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
// Session may already be dead
|
|
1275
|
+
}
|
|
943
1276
|
}
|
|
944
1277
|
// Strategy 4: Direct kill by PID as final fallback
|
|
945
1278
|
if (this.isProcessAlive(currentPid)) {
|
|
@@ -1031,6 +1364,14 @@ export class TmuxManager extends EventEmitter {
|
|
|
1031
1364
|
for (const [sessionName, pid] of active) {
|
|
1032
1365
|
if (!sessionName.startsWith('codeman-') && !sessionName.startsWith('claudeman-'))
|
|
1033
1366
|
continue;
|
|
1367
|
+
// Only admit names that pass the safe-name pattern. A foreign process on the
|
|
1368
|
+
// shared `tmux -L codeman` socket could create a `codeman-…` session whose name
|
|
1369
|
+
// contains shell metacharacters; rejecting it here keeps it out of this.sessions
|
|
1370
|
+
// and away from the name-interpolating tmux call sites (M1).
|
|
1371
|
+
if (!isValidMuxName(sessionName)) {
|
|
1372
|
+
console.warn(`[TmuxManager] Skipping discovered tmux session with unsafe name: ${sessionName}`);
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1034
1375
|
if (knownMuxNames.has(sessionName))
|
|
1035
1376
|
continue;
|
|
1036
1377
|
const fragment = sessionName.replace(/^(?:codeman|claudeman)-/, '');
|
|
@@ -1488,8 +1829,11 @@ export class TmuxManager extends EventEmitter {
|
|
|
1488
1829
|
}
|
|
1489
1830
|
}
|
|
1490
1831
|
/**
|
|
1491
|
-
* Capture the current
|
|
1492
|
-
*
|
|
1832
|
+
* Capture the current visible text and SGR styles of a specific pane.
|
|
1833
|
+
*
|
|
1834
|
+
* `capture-pane -e` is sanitized by `formatPaneSnapshot`: SGR color/style
|
|
1835
|
+
* codes are preserved, while cursor/erase/scroll-region controls are stripped
|
|
1836
|
+
* before rows are repainted at absolute positions in browser xterm.
|
|
1493
1837
|
*/
|
|
1494
1838
|
capturePaneBuffer(muxName, paneTarget) {
|
|
1495
1839
|
if (IS_TEST_MODE)
|
|
@@ -1504,16 +1848,63 @@ export class TmuxManager extends EventEmitter {
|
|
|
1504
1848
|
}
|
|
1505
1849
|
const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`;
|
|
1506
1850
|
try {
|
|
1507
|
-
|
|
1851
|
+
const buffer = execSync(`${this.tmux()} capture-pane -p -e -t ${shellescape(target)}`, {
|
|
1508
1852
|
encoding: 'utf-8',
|
|
1509
1853
|
timeout: EXEC_TIMEOUT_MS,
|
|
1510
|
-
});
|
|
1854
|
+
}).replace(/\n+$/g, '');
|
|
1855
|
+
try {
|
|
1856
|
+
const cursor = execSync(`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height}'`, {
|
|
1857
|
+
encoding: 'utf-8',
|
|
1858
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
1859
|
+
}).trim();
|
|
1860
|
+
const [cursorX, cursorY, cols, rows] = cursor.split(/\s+/).map((value) => parseInt(value, 10));
|
|
1861
|
+
if (Number.isFinite(cursorX) &&
|
|
1862
|
+
Number.isFinite(cursorY) &&
|
|
1863
|
+
Number.isFinite(cols) &&
|
|
1864
|
+
Number.isFinite(rows) &&
|
|
1865
|
+
cursorX >= 0 &&
|
|
1866
|
+
cursorY >= 0 &&
|
|
1867
|
+
cols > 0 &&
|
|
1868
|
+
rows > 0) {
|
|
1869
|
+
return formatPaneSnapshot(buffer.split('\n'), { cols, rows, cursorX, cursorY });
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
catch (cursorErr) {
|
|
1873
|
+
console.error('[TmuxManager] Failed to query pane cursor after capture:', cursorErr);
|
|
1874
|
+
}
|
|
1875
|
+
return buffer;
|
|
1511
1876
|
}
|
|
1512
1877
|
catch (err) {
|
|
1513
1878
|
console.error('[TmuxManager] Failed to capture pane buffer:', err);
|
|
1514
1879
|
return null;
|
|
1515
1880
|
}
|
|
1516
1881
|
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Capture the active pane for a tmux session.
|
|
1884
|
+
*
|
|
1885
|
+
* Pane ids are not stable across respawns or restores, so callers should not
|
|
1886
|
+
* assume the first pane remains `%0`.
|
|
1887
|
+
*/
|
|
1888
|
+
captureActivePaneBuffer(muxName) {
|
|
1889
|
+
if (IS_TEST_MODE)
|
|
1890
|
+
return '';
|
|
1891
|
+
if (!isValidMuxName(muxName)) {
|
|
1892
|
+
console.error('[TmuxManager] Invalid session name in captureActivePaneBuffer:', muxName);
|
|
1893
|
+
return null;
|
|
1894
|
+
}
|
|
1895
|
+
try {
|
|
1896
|
+
const output = execSync(`${this.tmux()} list-panes -t ${shellescape(muxName)} -F '#{pane_id}:#{pane_active}'`, {
|
|
1897
|
+
encoding: 'utf-8',
|
|
1898
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
1899
|
+
}).trim();
|
|
1900
|
+
const target = resolveActivePaneTarget(output);
|
|
1901
|
+
return target ? this.capturePaneBuffer(muxName, target) : null;
|
|
1902
|
+
}
|
|
1903
|
+
catch (err) {
|
|
1904
|
+
console.error('[TmuxManager] Failed to resolve active pane for capture:', err);
|
|
1905
|
+
return null;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1517
1908
|
/**
|
|
1518
1909
|
* Start piping pane output to a file using tmux pipe-pane.
|
|
1519
1910
|
* Only pipes output direction (-O) to avoid echoing input.
|
|
@@ -1579,6 +1970,43 @@ export class TmuxManager extends EventEmitter {
|
|
|
1579
1970
|
getAttachArgs(muxName) {
|
|
1580
1971
|
return ['-L', this.tmuxSocket, 'attach-session', '-t', muxName];
|
|
1581
1972
|
}
|
|
1973
|
+
setManualWindowSize(muxName) {
|
|
1974
|
+
if (!isValidMuxName(muxName)) {
|
|
1975
|
+
console.error('[TmuxManager] Invalid session name in setManualWindowSize:', muxName);
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1978
|
+
try {
|
|
1979
|
+
execSync(`${this.tmux()} set-window-option -t ${shellescape(muxName)} window-size manual`, {
|
|
1980
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
1981
|
+
stdio: 'ignore',
|
|
1982
|
+
});
|
|
1983
|
+
return true;
|
|
1984
|
+
}
|
|
1985
|
+
catch (err) {
|
|
1986
|
+
console.error('[TmuxManager] Failed to set manual window size:', err);
|
|
1987
|
+
return false;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
resizeWindow(muxName, cols, rows) {
|
|
1991
|
+
if (!isValidMuxName(muxName)) {
|
|
1992
|
+
console.error('[TmuxManager] Invalid session name in resizeWindow:', muxName);
|
|
1993
|
+
return false;
|
|
1994
|
+
}
|
|
1995
|
+
if (!isValidTerminalDimension(cols) || !isValidTerminalDimension(rows)) {
|
|
1996
|
+
console.error('[TmuxManager] Invalid resize dimensions:', { cols, rows });
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1999
|
+
// Fire-and-forget: this runs on the interactive resize path (WS {t:'z'} and
|
|
2000
|
+
// HTTP /resize), so use a non-blocking exec — a slow/hung tmux must not stall
|
|
2001
|
+
// the Fastify event loop while other sessions' input/SSE are served. The sole
|
|
2002
|
+
// caller (Session.resize) ignores the result, and under `window-size manual`
|
|
2003
|
+
// the subsequent ptyProcess.resize is subordinate to this authoritative size.
|
|
2004
|
+
exec(`${this.tmux()} resize-window -t ${shellescape(muxName)} -x ${cols} -y ${rows}`, { timeout: EXEC_TIMEOUT_MS }, (err) => {
|
|
2005
|
+
if (err)
|
|
2006
|
+
console.error('[TmuxManager] Failed to resize tmux window:', err);
|
|
2007
|
+
});
|
|
2008
|
+
return true;
|
|
2009
|
+
}
|
|
1582
2010
|
isAvailable() {
|
|
1583
2011
|
return TmuxManager.isTmuxAvailable();
|
|
1584
2012
|
}
|