aicodeman 1.43.0 → 1.45.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 +9 -4
- package/dist/case-rename.d.ts +65 -0
- package/dist/case-rename.d.ts.map +1 -0
- package/dist/case-rename.js +198 -0
- package/dist/case-rename.js.map +1 -0
- package/dist/cli-agent.d.ts +63 -4
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +233 -19
- package/dist/cli-agent.js.map +1 -1
- package/dist/config/auth-config.d.ts +16 -1
- package/dist/config/auth-config.d.ts.map +1 -1
- package/dist/config/auth-config.js +16 -1
- package/dist/config/auth-config.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +8 -3
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +2 -2
- package/dist/config/map-limits.d.ts +13 -1
- package/dist/config/map-limits.d.ts.map +1 -1
- package/dist/config/map-limits.js +28 -1
- package/dist/config/map-limits.js.map +1 -1
- package/dist/config-backup.d.ts +279 -0
- package/dist/config-backup.d.ts.map +1 -0
- package/dist/config-backup.js +794 -0
- package/dist/config-backup.js.map +1 -0
- package/dist/cron/cron-service.d.ts +5 -5
- package/dist/cron/cron-service.js +6 -6
- package/dist/cron/cron-service.js.map +1 -1
- package/dist/custom-model-hosts.d.ts +16 -0
- package/dist/custom-model-hosts.d.ts.map +1 -1
- package/dist/custom-model-hosts.js.map +1 -1
- package/dist/hooks-config.d.ts +15 -0
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +35 -0
- package/dist/hooks-config.js.map +1 -1
- package/dist/host-sensors.d.ts +115 -0
- package/dist/host-sensors.d.ts.map +1 -0
- package/dist/host-sensors.js +457 -0
- package/dist/host-sensors.js.map +1 -0
- package/dist/intent-store.d.ts +2 -0
- package/dist/intent-store.d.ts.map +1 -1
- package/dist/intent-store.js +9 -0
- package/dist/intent-store.js.map +1 -1
- package/dist/mux-interface.d.ts +21 -4
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/prompt-cache-keepwarm.d.ts +193 -0
- package/dist/prompt-cache-keepwarm.d.ts.map +1 -0
- package/dist/prompt-cache-keepwarm.js +359 -0
- package/dist/prompt-cache-keepwarm.js.map +1 -0
- package/dist/remote-hosts.d.ts +7 -0
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +17 -3
- package/dist/remote-hosts.js.map +1 -1
- package/dist/remote-pane-history.d.ts.map +1 -1
- package/dist/remote-pane-history.js +2 -2
- package/dist/remote-pane-history.js.map +1 -1
- package/dist/respawn-controller.d.ts.map +1 -1
- package/dist/respawn-controller.js +6 -5
- package/dist/respawn-controller.js.map +1 -1
- package/dist/session-auto-ops.d.ts.map +1 -1
- package/dist/session-auto-ops.js +6 -1
- package/dist/session-auto-ops.js.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts +13 -0
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +20 -3
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session-submit-verifier.d.ts +19 -0
- package/dist/session-submit-verifier.d.ts.map +1 -1
- package/dist/session-submit-verifier.js +88 -0
- package/dist/session-submit-verifier.js.map +1 -1
- package/dist/session.d.ts +61 -5
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +125 -7
- package/dist/session.js.map +1 -1
- package/dist/state-store.d.ts +10 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +35 -0
- package/dist/state-store.js.map +1 -1
- package/dist/tab-layout-service.d.ts +33 -0
- package/dist/tab-layout-service.d.ts.map +1 -1
- package/dist/tab-layout-service.js +75 -3
- package/dist/tab-layout-service.js.map +1 -1
- package/dist/tmux-manager.d.ts +57 -11
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +190 -42
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-prefix-table.d.ts +86 -0
- package/dist/tmux-prefix-table.d.ts.map +1 -0
- package/dist/tmux-prefix-table.js +172 -0
- package/dist/tmux-prefix-table.js.map +1 -0
- package/dist/tui/tui-app.d.ts +17 -2
- package/dist/tui/tui-app.d.ts.map +1 -1
- package/dist/tui/tui-app.js +70 -20
- package/dist/tui/tui-app.js.map +1 -1
- package/dist/tui/tui-client.d.ts +93 -5
- package/dist/tui/tui-client.d.ts.map +1 -1
- package/dist/tui/tui-client.js +198 -28
- package/dist/tui/tui-client.js.map +1 -1
- package/dist/types/app-state.d.ts +6 -0
- package/dist/types/app-state.d.ts.map +1 -1
- package/dist/types/app-state.js.map +1 -1
- package/dist/types/session.d.ts +40 -1
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/usage-telemetry.d.ts +15 -0
- package/dist/usage-telemetry.d.ts.map +1 -1
- package/dist/usage-telemetry.js +45 -2
- package/dist/usage-telemetry.js.map +1 -1
- package/dist/utils/copilot-plan-usage.d.ts +46 -0
- package/dist/utils/copilot-plan-usage.d.ts.map +1 -0
- package/dist/utils/copilot-plan-usage.js +95 -0
- package/dist/utils/copilot-plan-usage.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/single-line.d.ts +22 -0
- package/dist/utils/single-line.d.ts.map +1 -0
- package/dist/utils/single-line.js +30 -0
- package/dist/utils/single-line.js.map +1 -0
- package/dist/web/agent-inbox-persistence.d.ts +52 -0
- package/dist/web/agent-inbox-persistence.d.ts.map +1 -0
- package/dist/web/agent-inbox-persistence.js +139 -0
- package/dist/web/agent-inbox-persistence.js.map +1 -0
- package/dist/web/agent-inbox.d.ts +140 -0
- package/dist/web/agent-inbox.d.ts.map +1 -0
- package/dist/web/agent-inbox.js +258 -0
- package/dist/web/agent-inbox.js.map +1 -0
- package/dist/web/approval-inbox.d.ts +6 -0
- package/dist/web/approval-inbox.d.ts.map +1 -1
- package/dist/web/approval-inbox.js +2 -2
- package/dist/web/approval-inbox.js.map +1 -1
- package/dist/web/middleware/auth.d.ts +2 -0
- package/dist/web/middleware/auth.d.ts.map +1 -1
- package/dist/web/middleware/auth.js +145 -52
- package/dist/web/middleware/auth.js.map +1 -1
- package/dist/web/plan-usage-latest.d.ts +2 -1
- package/dist/web/plan-usage-latest.d.ts.map +1 -1
- package/dist/web/plan-usage-latest.js +12 -2
- package/dist/web/plan-usage-latest.js.map +1 -1
- package/dist/web/ports/index.d.ts +1 -0
- package/dist/web/ports/index.d.ts.map +1 -1
- package/dist/web/ports/keep-warm-port.d.ts +9 -0
- package/dist/web/ports/keep-warm-port.d.ts.map +1 -0
- package/dist/web/ports/keep-warm-port.js +6 -0
- package/dist/web/ports/keep-warm-port.js.map +1 -0
- package/dist/web/ports/session-port.d.ts +3 -1
- package/dist/web/ports/session-port.d.ts.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.42d0abae.js +43 -0
- package/dist/web/public/app.42d0abae.js.br +0 -0
- package/dist/web/public/app.42d0abae.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.e5e588db.js → constants.0b3cdde9.js} +60 -1
- package/dist/web/public/constants.0b3cdde9.js.br +0 -0
- package/dist/web/public/constants.0b3cdde9.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/header-divider.eb2d2463.js +1 -0
- package/dist/web/public/header-divider.eb2d2463.js.br +0 -0
- package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.1e68220d.js +2 -0
- package/dist/web/public/i18n.1e68220d.js.br +0 -0
- package/dist/web/public/i18n.1e68220d.js.gz +0 -0
- package/dist/web/public/image-input.941386e7.js.gz +0 -0
- package/dist/web/public/index.html +162 -14
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.c283e827.js → keyboard-accessory.4e8b20ac.js} +23 -5
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.br +0 -0
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
- package/dist/web/public/{mobile-handlers.0ef9c311.js → mobile-handlers.1c0cbcd8.js} +50 -16
- package/dist/web/public/mobile-handlers.1c0cbcd8.js.br +0 -0
- package/dist/web/public/mobile-handlers.1c0cbcd8.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.12fb18de.css +1 -0
- package/dist/web/public/mobile.12fb18de.css.br +0 -0
- package/dist/web/public/mobile.12fb18de.css.gz +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.b7bfb9da.js → panels-ui.78c3defb.js} +19 -13
- package/dist/web/public/panels-ui.78c3defb.js.br +0 -0
- package/dist/web/public/panels-ui.78c3defb.js.gz +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.be076c88.js → session-ui.ea6b1994.js} +33 -27
- package/dist/web/public/session-ui.ea6b1994.js.br +0 -0
- package/dist/web/public/session-ui.ea6b1994.js.gz +0 -0
- package/dist/web/public/settings-ui.085dd202.js +106 -0
- package/dist/web/public/settings-ui.085dd202.js.br +0 -0
- package/dist/web/public/settings-ui.085dd202.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.3b59b41d.css +1 -0
- package/dist/web/public/styles.3b59b41d.css.br +0 -0
- package/dist/web/public/styles.3b59b41d.css.gz +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +2 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +49 -23
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/terminal-ui.fb2a27f5.js +2 -0
- package/dist/web/public/terminal-ui.fb2a27f5.js.br +0 -0
- package/dist/web/public/terminal-ui.fb2a27f5.js.gz +0 -0
- package/dist/web/public/tile-grid.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/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.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-predictive-echo.ae5e3d5d.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.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.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/public/whats-new.json +9 -9
- package/dist/web/route-helpers.d.ts +57 -6
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +72 -4
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/admin-routes.d.ts +2 -1
- package/dist/web/routes/admin-routes.d.ts.map +1 -1
- package/dist/web/routes/admin-routes.js +3 -0
- package/dist/web/routes/admin-routes.js.map +1 -1
- package/dist/web/routes/case-routes.d.ts +2 -2
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +263 -4
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.d.ts +54 -0
- package/dist/web/routes/config-backup-routes.d.ts.map +1 -0
- package/dist/web/routes/config-backup-routes.js +268 -0
- package/dist/web/routes/config-backup-routes.js.map +1 -0
- package/dist/web/routes/custom-model-routes.d.ts +17 -1
- package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
- package/dist/web/routes/custom-model-routes.js +118 -13
- package/dist/web/routes/custom-model-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
- package/dist/web/routes/hook-event-routes.js +26 -5
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/inbox-routes.d.ts +43 -0
- package/dist/web/routes/inbox-routes.d.ts.map +1 -0
- package/dist/web/routes/inbox-routes.js +148 -0
- package/dist/web/routes/inbox-routes.js.map +1 -0
- package/dist/web/routes/index.d.ts +2 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +2 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/ralph-routes.js +2 -2
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/reboot-restore-routes.d.ts.map +1 -1
- package/dist/web/routes/reboot-restore-routes.js +2 -0
- package/dist/web/routes/reboot-restore-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +2 -2
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +110 -61
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/status-telemetry-routes.d.ts +2 -2
- package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
- package/dist/web/routes/status-telemetry-routes.js +8 -4
- package/dist/web/routes/status-telemetry-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts +2 -2
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +30 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/routes/webview-routes.d.ts.map +1 -1
- package/dist/web/routes/webview-routes.js +1 -0
- package/dist/web/routes/webview-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +45 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +79 -2
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/sensitive-path.d.ts.map +1 -1
- package/dist/web/sensitive-path.js +2 -0
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts +14 -2
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +176 -7
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-listener-wiring.d.ts +6 -0
- package/dist/web/session-listener-wiring.d.ts.map +1 -1
- package/dist/web/session-listener-wiring.js +10 -3
- package/dist/web/session-listener-wiring.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +48 -4
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +144 -6
- package/dist/web/session-wait-registry.js.map +1 -1
- package/dist/web/sse-events.d.ts +9 -3
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +9 -3
- package/dist/web/sse-events.js.map +1 -1
- package/dist/web/webview-proxy.d.ts +8 -3
- package/dist/web/webview-proxy.d.ts.map +1 -1
- package/dist/web/webview-proxy.js +23 -5
- package/dist/web/webview-proxy.js.map +1 -1
- package/package.json +1 -1
- package/skills/codeman/SKILL.md +27 -13
- package/skills/codeman/preamble.sh +15 -5
- package/skills/codeman/reference/endpoints.md +32 -10
- package/skills/codeman/reference/recipes.md +1 -1
- package/skills/codeman/reference/verbs.md +68 -13
- package/dist/web/public/app.ae50029a.js +0 -43
- package/dist/web/public/app.ae50029a.js.br +0 -0
- package/dist/web/public/app.ae50029a.js.gz +0 -0
- package/dist/web/public/constants.e5e588db.js.br +0 -0
- package/dist/web/public/constants.e5e588db.js.gz +0 -0
- package/dist/web/public/i18n.8f291f06.js +0 -2
- package/dist/web/public/i18n.8f291f06.js.br +0 -0
- package/dist/web/public/i18n.8f291f06.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.c283e827.js.br +0 -0
- package/dist/web/public/keyboard-accessory.c283e827.js.gz +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.br +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
- package/dist/web/public/mobile.13514863.css +0 -1
- package/dist/web/public/mobile.13514863.css.br +0 -0
- package/dist/web/public/mobile.13514863.css.gz +0 -0
- package/dist/web/public/panels-ui.b7bfb9da.js.br +0 -0
- package/dist/web/public/panels-ui.b7bfb9da.js.gz +0 -0
- package/dist/web/public/session-ui.be076c88.js.br +0 -0
- package/dist/web/public/session-ui.be076c88.js.gz +0 -0
- package/dist/web/public/settings-ui.fc4cc50a.js +0 -97
- package/dist/web/public/settings-ui.fc4cc50a.js.br +0 -0
- package/dist/web/public/settings-ui.fc4cc50a.js.gz +0 -0
- package/dist/web/public/styles.dcfa907f.css +0 -1
- package/dist/web/public/styles.dcfa907f.css.br +0 -0
- package/dist/web/public/styles.dcfa907f.css.gz +0 -0
- package/dist/web/public/terminal-ui.ae01a7d1.js +0 -2
- package/dist/web/public/terminal-ui.ae01a7d1.js.br +0 -0
- package/dist/web/public/terminal-ui.ae01a7d1.js.gz +0 -0
package/dist/tmux-manager.js
CHANGED
|
@@ -25,7 +25,7 @@ import { collectDescendants } from './proc-tree.js';
|
|
|
25
25
|
import { execSync, exec, execFile } from 'node:child_process';
|
|
26
26
|
import { promisify } from 'node:util';
|
|
27
27
|
const execAsync = promisify(exec);
|
|
28
|
-
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
28
|
+
import { existsSync, readFileSync, mkdirSync, realpathSync } from 'node:fs';
|
|
29
29
|
import { writeFile, rename } from 'node:fs/promises';
|
|
30
30
|
import { dirname } from 'node:path';
|
|
31
31
|
import { homedir } from 'node:os';
|
|
@@ -33,9 +33,10 @@ import { dataPath, DEFAULT_TMUX_SOCKET, CODEMAN_INSTANCE, SAFE_TMUX_SOCKET_PATTE
|
|
|
33
33
|
import { getErrorMessage, DEFAULT_NICE_CONFIG, } from './types.js';
|
|
34
34
|
import { getCli } from './config/cli-registry/registry.js';
|
|
35
35
|
import { missingCliMessage, resolveCliBinDir } from './utils/cli-resolver.js';
|
|
36
|
-
import { buildSpawnCommandFromRegistry, configSetenvValues, legacyConfigForMode, } from './session-cli-registry-bridge.js';
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
36
|
+
import { buildSpawnCommandFromRegistry, configSetenvValues, launchGatePasses, legacyConfigForMode, } from './session-cli-registry-bridge.js';
|
|
37
|
+
import { keepWarmOn } from './prompt-cache-keepwarm.js';
|
|
38
|
+
import { resolveStatusLineCliCommand, readPlanUsageTelemetryEnabled, readKeepWarmSettings, findEffectiveUserStatusLineCommand, } from './hooks-config.js';
|
|
39
|
+
import { buildSshConnectionArgs, defaultRemoteCommandForMode, remoteLoginShellCommand, remoteSshTarget, remoteTmuxSessionAlive, withRemoteTmuxPath, } from './remote-hosts.js';
|
|
39
40
|
import { buildDockerBaseArgs, buildDockerCreateArgs, containerApiUrl, CONTAINER_HOME, defaultDockerCommandForMode, hostGatewayAlias, resolveDockerClaudeArtifacts, resolveDockerCredentialArtifacts, resolveDockerDaemonMountSource, } from './docker-hosts.js';
|
|
40
41
|
import { wrapWithNice, SAFE_PATH_PATTERN, resolveLocalShell, loginShellArgs, waitForProcessesExit, } from './utils/index.js';
|
|
41
42
|
import { decideReconnect, advanceBackoff, freshReconnectState, resetReconnectState, } from './remote-reconnect.js';
|
|
@@ -44,6 +45,7 @@ import { decideReconnect, advanceBackoff, freshReconnectState, resetReconnectSta
|
|
|
44
45
|
// ============================================================================
|
|
45
46
|
import { EXEC_TIMEOUT_MS } from './config/exec-timeout.js';
|
|
46
47
|
import { ensureDeepSeekStatusShim } from './deepseek-status-shim.js';
|
|
48
|
+
import { NESTED_TMUX_PREFIX, nestedPrefixTableResetCommands, restrictTmuxPrefixTable } from './tmux-prefix-table.js';
|
|
47
49
|
/** How long a cached process snapshot stays usable. */
|
|
48
50
|
const PROC_SNAPSHOT_TTL_MS = 2000;
|
|
49
51
|
/**
|
|
@@ -89,6 +91,22 @@ const DEFAULT_PANE_EXIT_INTERVAL_MS = 2000;
|
|
|
89
91
|
const DEFAULT_REMOTE_RECONNECT_INTERVAL_MS = 5000;
|
|
90
92
|
/** Stable cwd for tmux server/pane launch; actual session cwd is reached inside the pane. */
|
|
91
93
|
const TMUX_LAUNCH_CWD = '/tmp';
|
|
94
|
+
/**
|
|
95
|
+
* TMUX_LAUNCH_CWD in every spelling a pane can report it. tmux reads a pane's
|
|
96
|
+
* directory from its process, which comes back resolved (`/private/tmp` on
|
|
97
|
+
* macOS, where `/tmp` is a symlink), so the literal and the resolved spelling
|
|
98
|
+
* both count. Computed once.
|
|
99
|
+
*/
|
|
100
|
+
const TMUX_LAUNCH_CWD_SPELLINGS = (() => {
|
|
101
|
+
const spellings = new Set([TMUX_LAUNCH_CWD]);
|
|
102
|
+
try {
|
|
103
|
+
spellings.add(realpathSync(TMUX_LAUNCH_CWD));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Unresolvable launch directory: the literal spelling is all there is.
|
|
107
|
+
}
|
|
108
|
+
return spellings;
|
|
109
|
+
})();
|
|
92
110
|
/** Claude Code native macOS recommendation for avoiding low nofile startup failures. */
|
|
93
111
|
export const CLAUDE_CODE_NOFILE_LIMIT = 2147483646;
|
|
94
112
|
/**
|
|
@@ -150,9 +168,13 @@ const PANE_LIST_SEP = '|';
|
|
|
150
168
|
* know a variable substitutes the empty string rather than failing, which is how
|
|
151
169
|
* tmux 3.2a answers `#{pane_dead_signal}` (added in 3.4), and the parser reads a
|
|
152
170
|
* short row as "pid known, deadness unknown" rather than discarding it.
|
|
171
|
+
*
|
|
172
|
+
* `#{pane_current_path}` comes last on purpose: a directory name may contain the
|
|
173
|
+
* separator, and only the final field can absorb it without ambiguity.
|
|
153
174
|
*/
|
|
154
175
|
const PANE_LIST_FORMAT = `#{session_name}${PANE_LIST_SEP}#{pane_pid}` +
|
|
155
|
-
`${PANE_LIST_SEP}#{pane_dead}${PANE_LIST_SEP}#{pane_dead_status}${PANE_LIST_SEP}#{pane_dead_signal}
|
|
176
|
+
`${PANE_LIST_SEP}#{pane_dead}${PANE_LIST_SEP}#{pane_dead_status}${PANE_LIST_SEP}#{pane_dead_signal}` +
|
|
177
|
+
`${PANE_LIST_SEP}#{pane_current_path}`;
|
|
156
178
|
/**
|
|
157
179
|
* 构建 pane 启动前的 nofile 修复命令。
|
|
158
180
|
*
|
|
@@ -201,6 +223,8 @@ export function parsePaneRows(output) {
|
|
|
201
223
|
dead: deadFlag === '1' ? true : deadFlag === '0' ? false : undefined,
|
|
202
224
|
exitStatus: paneField(fields, 3),
|
|
203
225
|
exitSignal: paneField(fields, 4),
|
|
226
|
+
// Everything after the fifth separator, so a path containing `|` stays whole.
|
|
227
|
+
currentPath: fields.slice(5).join(PANE_LIST_SEP) || undefined,
|
|
204
228
|
});
|
|
205
229
|
}
|
|
206
230
|
return rows;
|
|
@@ -295,9 +319,11 @@ export function resolveTmuxPaneTarget(muxName, paneTarget) {
|
|
|
295
319
|
let submitBufferSeq = 0;
|
|
296
320
|
/**
|
|
297
321
|
* Escape a final `;` for tmux. tmux splits commands on an argument that ENDS in
|
|
298
|
-
* `;` (`set-buffer -- 'a;'` stores `a`, `send-keys -l 'a;'` types `a`, a lone `;`
|
|
322
|
+
* `;` (`set-buffer -- 'a;'` stores `a`, `send-keys -l -- 'a;'` types `a`, a lone `;`
|
|
299
323
|
* is no argument at all); a final `\;` is its escape for a literal `;`. A `;`
|
|
300
|
-
* anywhere else in the argument is literal already.
|
|
324
|
+
* anywhere else in the argument is literal already. Every `send-keys -l` passes
|
|
325
|
+
* `--` before the text, like `set-buffer`, or tmux parses a text that starts with
|
|
326
|
+
* `-` as its own options. Exported for unit testing.
|
|
301
327
|
*/
|
|
302
328
|
export function escapeTmuxTrailingSemicolon(text) {
|
|
303
329
|
return text.endsWith(';') ? `${text.slice(0, -1)}\\;` : text;
|
|
@@ -864,6 +890,18 @@ export function buildCodexCommand(config) {
|
|
|
864
890
|
return 'codex';
|
|
865
891
|
return buildSpawnCommandFromRegistry(entry, { mode: 'codex', sessionId: '', codexConfig: config }) ?? 'codex';
|
|
866
892
|
}
|
|
893
|
+
/**
|
|
894
|
+
* Whether a launch carries `idleCompaction: false` in its one `--settings` JSON: keep-warm on
|
|
895
|
+
* at launch (`keepWarmOn`), the statusLine exporter injected (without its `prompt_cache` reports
|
|
896
|
+
* keep-warm cannot run, and the opt-out alone would only cost the user Claude Code's own idle
|
|
897
|
+
* compaction), and the CLI's `idleCompactionSetting` gate passed, fail-closed like `--name`:
|
|
898
|
+
* an unknown version means no key. Pure; `claudeCliVersion` as in buildSpawnCommand.
|
|
899
|
+
*/
|
|
900
|
+
export function decideIdleCompactionOptOut(o) {
|
|
901
|
+
return (o.statusLine === 'injected' &&
|
|
902
|
+
keepWarmOn(o.keepWarm, o.keepWarmDefault, !!o.spawnedByAgent) &&
|
|
903
|
+
launchGatePasses(o.mode, 'idleCompactionSetting', o.claudeCliVersion));
|
|
904
|
+
}
|
|
867
905
|
export function buildSpawnCommand(options) {
|
|
868
906
|
// Every CLI's command shape is registry DATA, rendered by the argv engine — see
|
|
869
907
|
// config/cli-registry/argv.ts for why config can never contain shell text. A `shell`-kind
|
|
@@ -938,7 +976,9 @@ export function remoteTmuxLocation(remote, sessionId) {
|
|
|
938
976
|
* ssh -o BatchMode=yes -t [<COD-107 connection opts>] user@host \
|
|
939
977
|
* 'tmux -L codeman-remote new-session -A -s codeman-ssh-<id> -c <path> "cd <path> && exec <cli>" \
|
|
940
978
|
* \; set -t codeman-ssh-<id> status off \; set -t codeman-ssh-<id> mouse off \
|
|
941
|
-
* \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0
|
|
979
|
+
* \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0 \; \
|
|
980
|
+
* bind-key -T prefix C-q send-prefix \; unbind-key -a -T prefix \; \
|
|
981
|
+
* bind-key -T prefix C-q send-prefix \; set -t codeman-ssh-<id> remain-on-exit failed'
|
|
942
982
|
*
|
|
943
983
|
* COD-107 — the connection options (`-p`, `-i`, `-J`, SOCKS `-o ProxyCommand`,
|
|
944
984
|
* arbitrary `-o`) come from the shared `buildSshConnectionArgs(remote)`, so the
|
|
@@ -1043,7 +1083,7 @@ export function buildRemoteLaunchCommand(options) {
|
|
|
1043
1083
|
`tmux -L ${REMOTE_TMUX_SOCKET} new-session -A -s ${remoteName} -c ${shellescape(remote.remotePath)} ${shellescape(paneCommand)}`,
|
|
1044
1084
|
`set -t ${remoteName} status off`,
|
|
1045
1085
|
`set -t ${remoteName} mouse off`,
|
|
1046
|
-
`set -t ${remoteName} prefix
|
|
1086
|
+
`set -t ${remoteName} prefix ${NESTED_TMUX_PREFIX}`,
|
|
1047
1087
|
'set -s escape-time 0',
|
|
1048
1088
|
// COD-106 — shared/collaborative sessions: tmux defaults to sizing a window
|
|
1049
1089
|
// to the SMALLEST attached client, so two Codemans at different viewports
|
|
@@ -1052,6 +1092,15 @@ export function buildRemoteLaunchCommand(options) {
|
|
|
1052
1092
|
// Per-session scoped (`set -t <name>`, matching #145's hardening) so a shared
|
|
1053
1093
|
// remote tmux server's other sessions keep their own sizing behavior.
|
|
1054
1094
|
`set -t ${remoteName} window-size latest`,
|
|
1095
|
+
// The prefix TABLE is server-global (unlike `prefix C-q`, which is per
|
|
1096
|
+
// session). This socket is dedicated to Codeman's owned sessions, so the
|
|
1097
|
+
// same reduction as the local socket applies: Ctrl+Q then )/(/s/w/L must
|
|
1098
|
+
// not switch-client / choose-tree onto a sibling. Bind, then unbind-all,
|
|
1099
|
+
// then bind again: a user config that left the prefix table empty makes
|
|
1100
|
+
// `unbind-key -a -T prefix` fail and abort the rest of this chain.
|
|
1101
|
+
// BEFORE remain-on-exit, which stays last because an old remote tmux
|
|
1102
|
+
// rejects `failed` and aborts the rest of the chain.
|
|
1103
|
+
...nestedPrefixTableResetCommands(),
|
|
1055
1104
|
// #210: keep a CRASHED pane so the failure is still on screen. Without this,
|
|
1056
1105
|
// tmux destroys the pane -> window -> session (and, being the only session,
|
|
1057
1106
|
// the whole remote server) the instant the pane command exits, which tears the
|
|
@@ -1068,7 +1117,8 @@ export function buildRemoteLaunchCommand(options) {
|
|
|
1068
1117
|
// LAST in the chain on purpose: tmux aborts the remaining commands of a `\;`
|
|
1069
1118
|
// sequence once one errors (also verified), and `failed` needs tmux >= 3.2 on
|
|
1070
1119
|
// the REMOTE host. Trailing, a rejection costs only this option; leading, it
|
|
1071
|
-
// would silently drop status/mouse/prefix/escape-time/window-size
|
|
1120
|
+
// would silently drop status/mouse/prefix/escape-time/window-size and the
|
|
1121
|
+
// prefix-table reduction with it.
|
|
1072
1122
|
`set -t ${remoteName} remain-on-exit failed`,
|
|
1073
1123
|
].join(' \\; ');
|
|
1074
1124
|
// ssh runs its trailing args through the remote login shell, so the entire
|
|
@@ -1080,7 +1130,14 @@ export function buildRemoteLaunchCommand(options) {
|
|
|
1080
1130
|
// `ssh -o BatchMode=yes` (preserving the historical token order), then the rest
|
|
1081
1131
|
// of the connection args, then the target and the quoted tmux invocation.
|
|
1082
1132
|
const [ssh, batchMode, ...connectionArgs] = buildSshConnectionArgs(remote);
|
|
1083
|
-
const sshParts = [
|
|
1133
|
+
const sshParts = [
|
|
1134
|
+
ssh,
|
|
1135
|
+
batchMode,
|
|
1136
|
+
'-t',
|
|
1137
|
+
...connectionArgs,
|
|
1138
|
+
remoteSshTarget(remote),
|
|
1139
|
+
shellescape(withRemoteTmuxPath(tmuxInvocation)),
|
|
1140
|
+
];
|
|
1084
1141
|
return sshParts.join(' ');
|
|
1085
1142
|
}
|
|
1086
1143
|
/**
|
|
@@ -1096,7 +1153,7 @@ export function buildRemoteKillCommand(options) {
|
|
|
1096
1153
|
const remoteName = remoteTmuxSessionName(sessionId);
|
|
1097
1154
|
const killCmd = `tmux -L ${REMOTE_TMUX_SOCKET} kill-session -t ${shellescape(remoteName)}`;
|
|
1098
1155
|
const [ssh, ...connectionArgs] = buildSshConnectionArgs(remote);
|
|
1099
|
-
return [ssh, ...connectionArgs, remoteSshTarget(remote), shellescape(killCmd)].join(' ');
|
|
1156
|
+
return [ssh, ...connectionArgs, remoteSshTarget(remote), shellescape(withRemoteTmuxPath(killCmd))].join(' ');
|
|
1100
1157
|
}
|
|
1101
1158
|
// ========== Docker cases (COD-Docker) ==========
|
|
1102
1159
|
//
|
|
@@ -1212,8 +1269,12 @@ export function buildDockerLaunchCommand(opts) {
|
|
|
1212
1269
|
`new-session -A -s ${dkrName} -c ${workdir} ${shellescape(paneCommand)}`,
|
|
1213
1270
|
`set -t ${dkrName} status off`,
|
|
1214
1271
|
`set -t ${dkrName} mouse off`,
|
|
1215
|
-
`set -t ${dkrName} prefix
|
|
1272
|
+
`set -t ${dkrName} prefix ${NESTED_TMUX_PREFIX}`,
|
|
1216
1273
|
'set -s escape-time 0',
|
|
1274
|
+
// Same reduction as the owned-remote chain: the in-container server is
|
|
1275
|
+
// Codeman's (`-L codeman-docker`), and its stock prefix table would move
|
|
1276
|
+
// the inner client to a sibling session on Ctrl+Q then )/(/s/w/L.
|
|
1277
|
+
...nestedPrefixTableResetCommands(),
|
|
1217
1278
|
].join(' \\; ');
|
|
1218
1279
|
const execEnvFlags = [];
|
|
1219
1280
|
for (const [k, v] of Object.entries(execEnv))
|
|
@@ -1423,7 +1484,14 @@ export function resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessio
|
|
|
1423
1484
|
export function buildRemoteAttachCommand(remote, remoteSessionName) {
|
|
1424
1485
|
const tmuxInvocation = `tmux -L codeman attach -t ${shellescape(remoteSessionName)}`;
|
|
1425
1486
|
const [ssh, batchMode, ...connectionArgs] = buildSshConnectionArgs(remote);
|
|
1426
|
-
const sshParts = [
|
|
1487
|
+
const sshParts = [
|
|
1488
|
+
ssh,
|
|
1489
|
+
batchMode,
|
|
1490
|
+
'-t',
|
|
1491
|
+
...connectionArgs,
|
|
1492
|
+
remoteSshTarget(remote),
|
|
1493
|
+
shellescape(withRemoteTmuxPath(tmuxInvocation)),
|
|
1494
|
+
];
|
|
1427
1495
|
return sshParts.join(' ');
|
|
1428
1496
|
}
|
|
1429
1497
|
/**
|
|
@@ -1934,6 +2002,21 @@ export class TmuxManager extends EventEmitter {
|
|
|
1934
2002
|
const command = await resolveStatusLineCliCommand(workingDir, enabled);
|
|
1935
2003
|
return { command, state: command ? 'injected' : enabled ? 'workspace-statusline' : 'collection-off' };
|
|
1936
2004
|
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Whether this launch carries `idleCompaction: false` (decideIdleCompactionOptOut), with the
|
|
2007
|
+
* keep-warm app default read fresh like the plan-usage switch above. Logged when keep-warm is
|
|
2008
|
+
* on but the version gate keeps the key off, so a session that may still idle-compact says why.
|
|
2009
|
+
*/
|
|
2010
|
+
async _resolveIdleCompactionOptOut(mode, statusLine, keepWarm, spawnedByAgent) {
|
|
2011
|
+
if (statusLine !== 'injected')
|
|
2012
|
+
return false; // no settings read for a launch that can never carry it
|
|
2013
|
+
const { keepWarmDefault } = await readKeepWarmSettings();
|
|
2014
|
+
const off = decideIdleCompactionOptOut({ mode, statusLine, keepWarm, keepWarmDefault, spawnedByAgent });
|
|
2015
|
+
if (!off && keepWarmOn(keepWarm, keepWarmDefault, !!spawnedByAgent)) {
|
|
2016
|
+
console.log(`[TmuxManager] Keep-warm is on but Claude Code's version is unknown or predates the idleCompaction setting; launching without the opt-out`);
|
|
2017
|
+
}
|
|
2018
|
+
return off;
|
|
2019
|
+
}
|
|
1937
2020
|
/**
|
|
1938
2021
|
* Export the user's own REAL statusLine command (found by
|
|
1939
2022
|
* findEffectiveUserStatusLineCommand) via tmux setenv, so the shared
|
|
@@ -1971,7 +2054,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1971
2054
|
* In test mode: creates an in-memory session only (no real tmux session).
|
|
1972
2055
|
*/
|
|
1973
2056
|
async createSession(options) {
|
|
1974
|
-
const { sessionId, workingDir, mode, name, cliName, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig, copilotConfig, deepSeekConfig, ompConfig, resumeSessionId, envOverrides, effort, advisorModel, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, docker, owner, } = options;
|
|
2057
|
+
const { sessionId, workingDir, mode, name, cliName, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig, copilotConfig, deepSeekConfig, ompConfig, resumeSessionId, envOverrides, effort, advisorModel, keepWarm, spawnedByAgent, historyLimit = DEFAULT_TMUX_HISTORY_LIMIT, remote, docker, owner, } = options;
|
|
1975
2058
|
const muxName = `codeman-${sessionId.slice(0, 8)}`;
|
|
1976
2059
|
if (!isValidMuxName(muxName)) {
|
|
1977
2060
|
throw new Error('Invalid session name: contains unsafe characters');
|
|
@@ -2022,6 +2105,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2022
2105
|
}
|
|
2023
2106
|
const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
|
|
2024
2107
|
const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir, !!(remote || docker));
|
|
2108
|
+
const idleCompactionOff = await this._resolveIdleCompactionOptOut(mode, statusLine, keepWarm, spawnedByAgent);
|
|
2025
2109
|
// The user's own REAL statusLine, if any (walked via Claude Code's own
|
|
2026
2110
|
// settings precedence) — exported below so the shared exporter script
|
|
2027
2111
|
// can wrap it. Only worth discovering when we're actually injecting.
|
|
@@ -2045,6 +2129,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2045
2129
|
effort,
|
|
2046
2130
|
advisorModel,
|
|
2047
2131
|
statusLineCommand,
|
|
2132
|
+
idleCompactionOff,
|
|
2048
2133
|
sessionName: cliName,
|
|
2049
2134
|
});
|
|
2050
2135
|
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
|
@@ -2094,6 +2179,9 @@ export class TmuxManager extends EventEmitter {
|
|
|
2094
2179
|
catch {
|
|
2095
2180
|
/* Non-critical */
|
|
2096
2181
|
}
|
|
2182
|
+
// Browser keystrokes reach the attach client, so tmux's stock prefix table would
|
|
2183
|
+
// let Ctrl+B ( or s move a tab onto another session. Before the agent starts.
|
|
2184
|
+
restrictTmuxPrefixTable(this.tmux());
|
|
2097
2185
|
// Per-CLI env: API keys, config blobs, config-sourced vars, status bridges. All of
|
|
2098
2186
|
// it is registry data, so this is one unconditional call rather than a per-mode ladder.
|
|
2099
2187
|
this._configureCliEnv(muxName, sessionId, mode, legacyConfigForMode(mode, options));
|
|
@@ -2163,6 +2251,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2163
2251
|
attached: false,
|
|
2164
2252
|
name,
|
|
2165
2253
|
statusLine,
|
|
2254
|
+
idleCompactionOff,
|
|
2166
2255
|
};
|
|
2167
2256
|
this.sessions.set(sessionId, session);
|
|
2168
2257
|
this.saveSessions();
|
|
@@ -2250,7 +2339,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2250
2339
|
* preserving the session and its scrollback buffer.
|
|
2251
2340
|
*/
|
|
2252
2341
|
async respawnPane(options) {
|
|
2253
|
-
const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig, copilotConfig, deepSeekConfig, ompConfig, resumeSessionId, envOverrides, unsetEnvKeys, effort, advisorModel, remote, docker, cliName, } = options;
|
|
2342
|
+
const { sessionId, workingDir, mode, niceConfig, model, claudeMode, allowedTools, openCodeConfig, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig, copilotConfig, deepSeekConfig, ompConfig, resumeSessionId, envOverrides, unsetEnvKeys, effort, advisorModel, keepWarm, spawnedByAgent, remote, docker, cliName, } = options;
|
|
2254
2343
|
const session = this.sessions.get(sessionId);
|
|
2255
2344
|
if (!session)
|
|
2256
2345
|
return null;
|
|
@@ -2261,6 +2350,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2261
2350
|
const { pathExport } = this.buildPathExport(mode);
|
|
2262
2351
|
const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
|
|
2263
2352
|
const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir, !!(remote || docker));
|
|
2353
|
+
const idleCompactionOff = await this._resolveIdleCompactionOptOut(mode, statusLine, keepWarm, spawnedByAgent);
|
|
2264
2354
|
const userStatusLineCommand = statusLineCommand ? await findEffectiveUserStatusLineCommand(workingDir) : undefined;
|
|
2265
2355
|
const baseCmd = buildSpawnCommand({
|
|
2266
2356
|
mode,
|
|
@@ -2281,6 +2371,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2281
2371
|
effort,
|
|
2282
2372
|
advisorModel,
|
|
2283
2373
|
statusLineCommand,
|
|
2374
|
+
idleCompactionOff,
|
|
2284
2375
|
sessionName: cliName,
|
|
2285
2376
|
});
|
|
2286
2377
|
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
|
@@ -2313,8 +2404,9 @@ export class TmuxManager extends EventEmitter {
|
|
|
2313
2404
|
const pid = this.getPanePid(muxName);
|
|
2314
2405
|
if (pid)
|
|
2315
2406
|
session.pid = pid;
|
|
2316
|
-
if (session.statusLine !== statusLine) {
|
|
2407
|
+
if (session.statusLine !== statusLine || session.idleCompactionOff !== idleCompactionOff) {
|
|
2317
2408
|
session.statusLine = statusLine;
|
|
2409
|
+
session.idleCompactionOff = idleCompactionOff;
|
|
2318
2410
|
this.saveSessions();
|
|
2319
2411
|
}
|
|
2320
2412
|
return pid;
|
|
@@ -2663,13 +2755,30 @@ export class TmuxManager extends EventEmitter {
|
|
|
2663
2755
|
// socket tag to reconcile and no cross-socket ambiguity that could mark a
|
|
2664
2756
|
// live session dead (the root cause of vanished/duplicate tabs).
|
|
2665
2757
|
let active;
|
|
2758
|
+
const paneDirs = new Map();
|
|
2666
2759
|
try {
|
|
2667
2760
|
const output = execSync(`${this.tmux()} list-panes -a -F '${PANE_LIST_FORMAT}' 2>/dev/null || true`, {
|
|
2668
2761
|
encoding: 'utf-8',
|
|
2669
2762
|
timeout: EXEC_TIMEOUT_MS,
|
|
2670
|
-
})
|
|
2763
|
+
});
|
|
2764
|
+
// Not trimmed: the directory is the last field, so trimming would cut a
|
|
2765
|
+
// trailing space off the last row's path. parsePaneRows skips empty lines.
|
|
2671
2766
|
const rows = parsePaneRows(output);
|
|
2672
2767
|
active = new Map(rows.map((row) => [row.sessionName, row.pid]));
|
|
2768
|
+
// A discovered session's directory, from its first pane with a usable path.
|
|
2769
|
+
// The path is admitted only if it passes the same check createSession and
|
|
2770
|
+
// respawnPane apply, since it was set by whoever owns the pane, not by us.
|
|
2771
|
+
// The launch directory counts as no answer: every pane starts there, and a
|
|
2772
|
+
// remote or docker pane never cds out of it, so for such a session it is
|
|
2773
|
+
// not the workspace — and a rediscovered record cannot tell it is one.
|
|
2774
|
+
for (const row of rows) {
|
|
2775
|
+
if (row.currentPath &&
|
|
2776
|
+
!TMUX_LAUNCH_CWD_SPELLINGS.has(row.currentPath) &&
|
|
2777
|
+
!paneDirs.has(row.sessionName) &&
|
|
2778
|
+
isValidPath(row.currentPath)) {
|
|
2779
|
+
paneDirs.set(row.sessionName, row.currentPath);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2673
2782
|
// The same read answers both questions, so recovery starts with a pane-exit
|
|
2674
2783
|
// reading rather than waiting for the first stats tick — which may never
|
|
2675
2784
|
// come, since the collector only starts when boot found a live session.
|
|
@@ -2681,6 +2790,11 @@ export class TmuxManager extends EventEmitter {
|
|
|
2681
2790
|
console.error('[TmuxManager] Failed to list tmux panes:', err);
|
|
2682
2791
|
active = new Map();
|
|
2683
2792
|
}
|
|
2793
|
+
// Sessions that survived an upgrade may sit on a server whose prefix table was never
|
|
2794
|
+
// reduced (createSession does it for new ones), so cover them at boot too. Only with
|
|
2795
|
+
// panes listed: on an empty socket `list-keys` spins up a throwaway server instead.
|
|
2796
|
+
if (active.size > 0)
|
|
2797
|
+
restrictTmuxPrefixTable(this.tmux());
|
|
2684
2798
|
// Check tracked sessions against the live pane list.
|
|
2685
2799
|
for (const [sessionId, session] of this.sessions) {
|
|
2686
2800
|
const pid = active.get(session.muxName);
|
|
@@ -2724,11 +2838,13 @@ export class TmuxManager extends EventEmitter {
|
|
|
2724
2838
|
muxName: sessionName,
|
|
2725
2839
|
pid,
|
|
2726
2840
|
createdAt: Date.now(),
|
|
2727
|
-
|
|
2841
|
+
// Where the pane actually works, when tmux said; the server's cwd is only a fallback.
|
|
2842
|
+
workingDir: paneDirs.get(sessionName) ?? process.cwd(),
|
|
2728
2843
|
mode: 'claude',
|
|
2729
2844
|
attached: false,
|
|
2730
2845
|
name: `Restored: ${sessionName}`,
|
|
2731
|
-
// Every field above except the name
|
|
2846
|
+
// Every field above except the name, the pid and, when tmux reported a
|
|
2847
|
+
// usable one, the directory is a guess: this record
|
|
2732
2848
|
// was rebuilt from the socket because Codeman's own bookkeeping did not
|
|
2733
2849
|
// have it. The synthetic id also cannot find the session's state.json
|
|
2734
2850
|
// entry, so a remote or docker session rediscovered this way arrives
|
|
@@ -3335,7 +3451,12 @@ export class TmuxManager extends EventEmitter {
|
|
|
3335
3451
|
* - Text followed by `\r` goes in as ONE bracketed paste (buildPasteTextCommands),
|
|
3336
3452
|
* so Codex cannot fold the Enter into a paste burst; a single character is typed
|
|
3337
3453
|
* with `send-keys -l` instead, so a dialog answer selects (submitsAsKeys)
|
|
3338
|
-
* -
|
|
3454
|
+
* - Single-line text without `\r` is typed literally with `send-keys -l` (no key
|
|
3455
|
+
* interpretation)
|
|
3456
|
+
* - A `\n` inside the text is kept (`\r\n` loses only its `\r`), and multi-line text
|
|
3457
|
+
* always goes in as ONE bracketed paste, with or without a following Enter: inside
|
|
3458
|
+
* the paste markers a CLI that enabled bracketed paste receives the line breaks as
|
|
3459
|
+
* pasted text, where typed as keys each one could submit a line on its own
|
|
3339
3460
|
* - `Enter` key is sent as a SEPARATE tmux invocation after a small delay
|
|
3340
3461
|
* - Ink (Claude CLI) needs text and Enter split to avoid treating Enter as a newline
|
|
3341
3462
|
*/
|
|
@@ -3345,8 +3466,10 @@ export class TmuxManager extends EventEmitter {
|
|
|
3345
3466
|
console.error(`[TmuxManager] sendInput failed: no session found for ${sessionId}. Known: ${Array.from(this.sessions.keys()).join(', ')}`);
|
|
3346
3467
|
return false;
|
|
3347
3468
|
}
|
|
3348
|
-
// TEST MODE: No-op — don't send input to real tmux sessions
|
|
3349
|
-
|
|
3469
|
+
// TEST MODE: No-op — don't send input to real tmux sessions. The gate lives in
|
|
3470
|
+
// inputRunner() so a test subclass can record what WOULD reach tmux.
|
|
3471
|
+
const run = this.inputRunner();
|
|
3472
|
+
if (!run) {
|
|
3350
3473
|
return true;
|
|
3351
3474
|
}
|
|
3352
3475
|
console.log(`[TmuxManager] sendInput to ${session.muxName}, input length: ${input.length}, hasCarriageReturn: ${input.includes('\r')}`);
|
|
@@ -3356,7 +3479,14 @@ export class TmuxManager extends EventEmitter {
|
|
|
3356
3479
|
}
|
|
3357
3480
|
try {
|
|
3358
3481
|
const hasCarriageReturn = input.includes('\r');
|
|
3359
|
-
|
|
3482
|
+
// Leading and trailing blank lines are dropped, so neither `"\nfix it\r"` nor
|
|
3483
|
+
// `" \nfix it\r"` pastes an empty first line (nor `"fix it\n\r"` an empty last
|
|
3484
|
+
// one). The leading match ends at a line break, so the first real line keeps its
|
|
3485
|
+
// indentation.
|
|
3486
|
+
const textPart = input
|
|
3487
|
+
.replace(/\r/g, '')
|
|
3488
|
+
.replace(/^\s*\n/, '')
|
|
3489
|
+
.trimEnd();
|
|
3360
3490
|
if (textPart && hasCarriageReturn) {
|
|
3361
3491
|
// Send text first, then Enter as a SEPARATE tmux command after a short delay.
|
|
3362
3492
|
// Ink (Claude CLI's terminal framework) needs them split — sending both in a
|
|
@@ -3367,25 +3497,29 @@ export class TmuxManager extends EventEmitter {
|
|
|
3367
3497
|
// selection dialog ignores a pasted `3` and Enter then confirms the
|
|
3368
3498
|
// highlighted option (see submitsAsKeys).
|
|
3369
3499
|
if (submitsAsKeys(textPart)) {
|
|
3370
|
-
await
|
|
3500
|
+
await run(`${this.tmux()} send-keys -t "${session.muxName}" -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`);
|
|
3371
3501
|
}
|
|
3372
3502
|
else {
|
|
3373
|
-
await pasteText(
|
|
3503
|
+
await pasteText(run, buildPasteTextCommands(this.tmux(), session.muxName, textPart));
|
|
3374
3504
|
}
|
|
3375
3505
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
3376
|
-
await
|
|
3377
|
-
|
|
3378
|
-
|
|
3506
|
+
await run(`${this.tmux()} send-keys -t "${session.muxName}" Enter`);
|
|
3507
|
+
}
|
|
3508
|
+
else if (textPart.includes('\n')) {
|
|
3509
|
+
// Multi-line text, no Enter: pasted, because typed as keys every `\n` would
|
|
3510
|
+
// reach the pane as a key the CLI may read as submit. A pane that has not
|
|
3511
|
+
// enabled bracketed paste (POSIX sh, macOS /bin/bash 3.2) still gets every line
|
|
3512
|
+
// break as a CR, so there every line but the last runs; before line breaks were
|
|
3513
|
+
// kept, the joined text was typed and nothing ran.
|
|
3514
|
+
await pasteText(run, buildPasteTextCommands(this.tmux(), session.muxName, textPart));
|
|
3379
3515
|
}
|
|
3380
3516
|
else if (textPart) {
|
|
3381
3517
|
// Text only, no Enter
|
|
3382
|
-
await
|
|
3518
|
+
await run(`${this.tmux()} send-keys -t "${session.muxName}" -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`);
|
|
3383
3519
|
}
|
|
3384
3520
|
else if (hasCarriageReturn) {
|
|
3385
3521
|
// Enter only
|
|
3386
|
-
await
|
|
3387
|
-
timeout: EXEC_TIMEOUT_MS,
|
|
3388
|
-
});
|
|
3522
|
+
await run(`${this.tmux()} send-keys -t "${session.muxName}" Enter`);
|
|
3389
3523
|
}
|
|
3390
3524
|
return true;
|
|
3391
3525
|
}
|
|
@@ -3394,6 +3528,16 @@ export class TmuxManager extends EventEmitter {
|
|
|
3394
3528
|
return false;
|
|
3395
3529
|
}
|
|
3396
3530
|
}
|
|
3531
|
+
/**
|
|
3532
|
+
* How {@link sendInput} reaches tmux, or null where it must not (vitest). Split
|
|
3533
|
+
* out like {@link readPaneRows}: with the test-mode gate at the top of sendInput,
|
|
3534
|
+
* no test could see which commands a submit actually sends.
|
|
3535
|
+
*/
|
|
3536
|
+
inputRunner() {
|
|
3537
|
+
if (IS_TEST_MODE)
|
|
3538
|
+
return null;
|
|
3539
|
+
return (cmd) => execAsync(cmd, { timeout: EXEC_TIMEOUT_MS });
|
|
3540
|
+
}
|
|
3397
3541
|
// ========== Pane Methods (for Agent Team teammate panes) ==========
|
|
3398
3542
|
/**
|
|
3399
3543
|
* Enable mouse mode for an existing tmux session.
|
|
@@ -3493,7 +3637,9 @@ export class TmuxManager extends EventEmitter {
|
|
|
3493
3637
|
}
|
|
3494
3638
|
/**
|
|
3495
3639
|
* Send input to a specific pane within a tmux session.
|
|
3496
|
-
*
|
|
3640
|
+
* Types the text literally with `send-keys -l`, line breaks stripped, then Enter
|
|
3641
|
+
* when the input carried a `\r`. Unlike sendInput(), it neither pastes nor keeps
|
|
3642
|
+
* the line breaks of multi-line text.
|
|
3497
3643
|
*/
|
|
3498
3644
|
sendInputToPane(muxName, paneTarget, input) {
|
|
3499
3645
|
if (IS_TEST_MODE)
|
|
@@ -3513,7 +3659,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
3513
3659
|
const hasCarriageReturn = input.includes('\r');
|
|
3514
3660
|
const textPart = input.replace(/\r/g, '').replace(/\n/g, '').trimEnd();
|
|
3515
3661
|
if (textPart && hasCarriageReturn) {
|
|
3516
|
-
execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
|
|
3662
|
+
execSync(`${tmux} send-keys -t ${shellescape(target)} -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
|
|
3517
3663
|
encoding: 'utf-8',
|
|
3518
3664
|
timeout: EXEC_TIMEOUT_MS,
|
|
3519
3665
|
});
|
|
@@ -3523,7 +3669,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
3523
3669
|
});
|
|
3524
3670
|
}
|
|
3525
3671
|
else if (textPart) {
|
|
3526
|
-
execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
|
|
3672
|
+
execSync(`${tmux} send-keys -t ${shellescape(target)} -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
|
|
3527
3673
|
encoding: 'utf-8',
|
|
3528
3674
|
timeout: EXEC_TIMEOUT_MS,
|
|
3529
3675
|
});
|
|
@@ -3563,19 +3709,21 @@ export class TmuxManager extends EventEmitter {
|
|
|
3563
3709
|
/**
|
|
3564
3710
|
* Plain visible-frame text for the working/idle probe (see `session.ts`).
|
|
3565
3711
|
*
|
|
3566
|
-
* One `capture-pane` and nothing else: no
|
|
3567
|
-
*
|
|
3568
|
-
*
|
|
3569
|
-
*
|
|
3712
|
+
* One `capture-pane` and nothing else: no `display-message` cursor query, no
|
|
3713
|
+
* repaint reconstruction: this feeds a regex, not a terminal. `styles` adds
|
|
3714
|
+
* `-e` for a reader that must tell faint text from typed text (the keep-warm
|
|
3715
|
+
* composer check); lines stay `\n`-separated either way. Returns null in tests
|
|
3716
|
+
* (no tmux) so callers fall back to their stream heuristics rather than
|
|
3717
|
+
* reading an empty screen as "not working".
|
|
3570
3718
|
*/
|
|
3571
|
-
capturePaneText(muxName, paneTarget) {
|
|
3719
|
+
capturePaneText(muxName, paneTarget, opts) {
|
|
3572
3720
|
if (IS_TEST_MODE)
|
|
3573
3721
|
return null;
|
|
3574
3722
|
const target = resolveTmuxPaneTarget(muxName, paneTarget);
|
|
3575
3723
|
if (!target)
|
|
3576
3724
|
return null;
|
|
3577
3725
|
try {
|
|
3578
|
-
return execSync(`${this.tmux()} capture-pane -p -t ${shellescape(target)}`, {
|
|
3726
|
+
return execSync(`${this.tmux()} capture-pane -p${opts?.styles ? ' -e' : ''} -t ${shellescape(target)}`, {
|
|
3579
3727
|
encoding: 'utf-8',
|
|
3580
3728
|
timeout: EXEC_TIMEOUT_MS,
|
|
3581
3729
|
});
|