aicodeman 1.47.0 → 1.49.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 +15 -3
- package/dist/cli-notify.d.ts +34 -0
- package/dist/cli-notify.d.ts.map +1 -0
- package/dist/cli-notify.js +100 -0
- package/dist/cli-notify.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/cli-registry/patterns.d.ts +1 -1
- package/dist/config/cli-registry/patterns.d.ts.map +1 -1
- package/dist/config/cli-registry/patterns.js +4 -0
- package/dist/config/cli-registry/patterns.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +7 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +67 -3
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +114 -31
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +51 -21
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/foreign-tmux.d.ts +48 -0
- package/dist/config/foreign-tmux.d.ts.map +1 -0
- package/dist/config/foreign-tmux.js +48 -0
- package/dist/config/foreign-tmux.js.map +1 -0
- package/dist/config/path-probe.d.ts +6 -0
- package/dist/config/path-probe.d.ts.map +1 -1
- package/dist/config/path-probe.js +6 -0
- package/dist/config/path-probe.js.map +1 -1
- package/dist/config-backup.d.ts +1 -1
- package/dist/config-backup.d.ts.map +1 -1
- package/dist/config-backup.js +2 -0
- package/dist/config-backup.js.map +1 -1
- package/dist/custom-model-hosts.d.ts +32 -8
- package/dist/custom-model-hosts.d.ts.map +1 -1
- package/dist/custom-model-hosts.js +10 -0
- package/dist/custom-model-hosts.js.map +1 -1
- package/dist/custom-model-injection-apply.d.ts +21 -5
- package/dist/custom-model-injection-apply.d.ts.map +1 -1
- package/dist/custom-model-injection-apply.js +121 -27
- package/dist/custom-model-injection-apply.js.map +1 -1
- package/dist/custom-model-injection.d.ts +6 -0
- package/dist/custom-model-injection.d.ts.map +1 -1
- package/dist/custom-model-injection.js +92 -10
- package/dist/custom-model-injection.js.map +1 -1
- package/dist/discord-bridge.d.ts +338 -0
- package/dist/discord-bridge.d.ts.map +1 -0
- package/dist/discord-bridge.js +1256 -0
- package/dist/discord-bridge.js.map +1 -0
- package/dist/docker-hosts.d.ts +8 -1
- package/dist/docker-hosts.d.ts.map +1 -1
- package/dist/docker-hosts.js +5 -5
- package/dist/docker-hosts.js.map +1 -1
- package/dist/edit-history-local-source.d.ts +83 -0
- package/dist/edit-history-local-source.d.ts.map +1 -0
- package/dist/edit-history-local-source.js +198 -0
- package/dist/edit-history-local-source.js.map +1 -0
- package/dist/edit-history-parse.d.ts +114 -0
- package/dist/edit-history-parse.d.ts.map +1 -0
- package/dist/edit-history-parse.js +312 -0
- package/dist/edit-history-parse.js.map +1 -0
- package/dist/edit-history-store.d.ts +209 -0
- package/dist/edit-history-store.d.ts.map +1 -0
- package/dist/edit-history-store.js +599 -0
- package/dist/edit-history-store.js.map +1 -0
- package/dist/foreign-tmux-discovery.d.ts +140 -0
- package/dist/foreign-tmux-discovery.d.ts.map +1 -0
- package/dist/foreign-tmux-discovery.js +366 -0
- package/dist/foreign-tmux-discovery.js.map +1 -0
- package/dist/foreign-tmux.d.ts +381 -0
- package/dist/foreign-tmux.d.ts.map +1 -0
- package/dist/foreign-tmux.js +775 -0
- package/dist/foreign-tmux.js.map +1 -0
- package/dist/git-clone.d.ts +6 -1
- package/dist/git-clone.d.ts.map +1 -1
- package/dist/git-clone.js +79 -14
- package/dist/git-clone.js.map +1 -1
- package/dist/git-fetch-scheduler.d.ts +2 -0
- package/dist/git-fetch-scheduler.d.ts.map +1 -1
- package/dist/git-fetch-scheduler.js +1 -1
- package/dist/git-fetch-scheduler.js.map +1 -1
- package/dist/git-fetch.d.ts +1 -1
- package/dist/git-fetch.d.ts.map +1 -1
- package/dist/git-workspace-status.d.ts +2 -2
- package/dist/git-workspace-status.d.ts.map +1 -1
- package/dist/hooks-config.d.ts +2 -1
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +2 -1
- package/dist/hooks-config.js.map +1 -1
- package/dist/image-watcher.js +4 -4
- package/dist/image-watcher.js.map +1 -1
- package/dist/live-worktree.d.ts +53 -0
- package/dist/live-worktree.d.ts.map +1 -0
- package/dist/live-worktree.js +97 -0
- package/dist/live-worktree.js.map +1 -0
- package/dist/mux-interface.d.ts +22 -2
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/reboot-restore.d.ts +1 -1
- package/dist/reboot-restore.d.ts.map +1 -1
- package/dist/reboot-restore.js +9 -0
- package/dist/reboot-restore.js.map +1 -1
- package/dist/remote-files.d.ts +150 -5
- package/dist/remote-files.d.ts.map +1 -1
- package/dist/remote-files.js +505 -7
- package/dist/remote-files.js.map +1 -1
- package/dist/remote-hosts.d.ts +15 -9
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +42 -20
- package/dist/remote-hosts.js.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +23 -8
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session-env-clamp.d.ts +1 -1
- package/dist/session-env-clamp.d.ts.map +1 -1
- package/dist/session-env-clamp.js +4 -3
- package/dist/session-env-clamp.js.map +1 -1
- package/dist/session.d.ts +161 -24
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +337 -37
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +114 -12
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +318 -41
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/discord.d.ts +55 -0
- package/dist/types/discord.d.ts.map +1 -0
- package/dist/types/discord.js +18 -0
- package/dist/types/discord.js.map +1 -0
- package/dist/types/foreign-tmux.d.ts +101 -0
- package/dist/types/foreign-tmux.d.ts.map +1 -0
- package/dist/types/foreign-tmux.js +27 -0
- package/dist/types/foreign-tmux.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/session.d.ts +119 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/types/update.d.ts +4 -1
- package/dist/types/update.d.ts.map +1 -1
- package/dist/web/case-path.d.ts +2 -0
- package/dist/web/case-path.d.ts.map +1 -1
- package/dist/web/case-path.js +3 -3
- package/dist/web/case-path.js.map +1 -1
- package/dist/web/edit-history-sync.d.ts +178 -0
- package/dist/web/edit-history-sync.d.ts.map +1 -0
- package/dist/web/edit-history-sync.js +461 -0
- package/dist/web/edit-history-sync.js.map +1 -0
- package/dist/web/launch-defaults.d.ts +1 -1
- package/dist/web/launch-defaults.js +2 -2
- package/dist/web/launch-defaults.js.map +1 -1
- package/dist/web/linked-cases-writer.d.ts +43 -0
- package/dist/web/linked-cases-writer.d.ts.map +1 -0
- package/dist/web/linked-cases-writer.js +107 -0
- package/dist/web/linked-cases-writer.js.map +1 -0
- package/dist/web/paste-image-gc.d.ts +10 -1
- package/dist/web/paste-image-gc.d.ts.map +1 -1
- package/dist/web/paste-image-gc.js +10 -2
- package/dist/web/paste-image-gc.js.map +1 -1
- package/dist/web/ports/session-port.d.ts +13 -0
- 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.fe8a6b4c.js → app.9ea37070.js} +6 -6
- package/dist/web/public/app.9ea37070.js.br +0 -0
- package/dist/web/public/app.9ea37070.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.affcdde6.js → constants.782170e1.js} +114 -25
- package/dist/web/public/constants.782170e1.js.br +0 -0
- package/dist/web/public/constants.782170e1.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/edit-history-ui.js +643 -0
- package/dist/web/public/edit-history-ui.js.br +0 -0
- package/dist/web/public/edit-history-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/foreign-sessions.js +476 -0
- package/dist/web/public/foreign-sessions.js.br +0 -0
- package/dist/web/public/foreign-sessions.js.gz +0 -0
- package/dist/web/public/git-status-ui.js +5 -3
- package/dist/web/public/git-status-ui.js.br +0 -0
- package/dist/web/public/git-status-ui.js.gz +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.a5e82c95.js +2 -0
- package/dist/web/public/i18n.a5e82c95.js.br +0 -0
- package/dist/web/public/i18n.a5e82c95.js.gz +0 -0
- package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
- package/dist/web/public/index.html +183 -18
- 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.ff64fafc.js → keyboard-accessory.b941676c.js} +75 -2
- package/dist/web/public/keyboard-accessory.b941676c.js.br +0 -0
- package/dist/web/public/keyboard-accessory.b941676c.js.gz +0 -0
- package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js +10 -0
- package/dist/web/public/mobile-overview.js.br +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.67c8eb5b.css +1 -0
- package/dist/web/public/mobile.67c8eb5b.css.br +0 -0
- package/dist/web/public/mobile.67c8eb5b.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.4a56ee63.js → panels-ui.8bcd4473.js} +5 -5
- package/dist/web/public/panels-ui.8bcd4473.js.br +0 -0
- package/dist/web/public/panels-ui.8bcd4473.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 → respawn-ui.304ba0d4.js} +1 -1
- package/dist/web/public/respawn-ui.304ba0d4.js.br +0 -0
- package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/security-notices.json +1 -21
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.426fb671.js → session-ui.a7c8c095.js} +30 -30
- package/dist/web/public/session-ui.a7c8c095.js.br +0 -0
- package/dist/web/public/session-ui.a7c8c095.js.gz +0 -0
- package/dist/web/public/settings-ui.18f92052.js +106 -0
- package/dist/web/public/settings-ui.18f92052.js.br +0 -0
- package/dist/web/public/settings-ui.18f92052.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.6a718331.css +1 -0
- package/dist/web/public/styles.6a718331.css.br +0 -0
- package/dist/web/public/styles.6a718331.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.b5cdceaf.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +2 -2
- 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.a7883640.js +4 -0
- package/dist/web/public/terminal-ui.a7883640.js.br +0 -0
- package/dist/web/public/terminal-ui.a7883640.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.276b11d9.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.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 -8
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +165 -50
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.d.ts +5 -1
- package/dist/web/routes/config-backup-routes.d.ts.map +1 -1
- package/dist/web/routes/config-backup-routes.js +4 -1
- package/dist/web/routes/config-backup-routes.js.map +1 -1
- package/dist/web/routes/custom-model-routes.d.ts +7 -2
- package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
- package/dist/web/routes/custom-model-routes.js +51 -6
- package/dist/web/routes/custom-model-routes.js.map +1 -1
- package/dist/web/routes/discord-routes.d.ts +25 -0
- package/dist/web/routes/discord-routes.d.ts.map +1 -0
- package/dist/web/routes/discord-routes.js +206 -0
- package/dist/web/routes/discord-routes.js.map +1 -0
- package/dist/web/routes/edit-history-routes.d.ts +43 -0
- package/dist/web/routes/edit-history-routes.d.ts.map +1 -0
- package/dist/web/routes/edit-history-routes.js +107 -0
- package/dist/web/routes/edit-history-routes.js.map +1 -0
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +273 -102
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/git-status-routes.d.ts.map +1 -1
- package/dist/web/routes/git-status-routes.js +7 -2
- package/dist/web/routes/git-status-routes.js.map +1 -1
- 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/mux-routes.d.ts +10 -3
- package/dist/web/routes/mux-routes.d.ts.map +1 -1
- package/dist/web/routes/mux-routes.js +59 -1
- package/dist/web/routes/mux-routes.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +22 -0
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/readmymind-routes.d.ts.map +1 -1
- package/dist/web/routes/readmymind-routes.js +3 -2
- package/dist/web/routes/readmymind-routes.js.map +1 -1
- package/dist/web/routes/respawn-routes.d.ts.map +1 -1
- package/dist/web/routes/respawn-routes.js +24 -0
- package/dist/web/routes/respawn-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +21 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +338 -55
- 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 +4 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +92 -2
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +111 -10
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/self-update.d.ts +9 -0
- package/dist/web/self-update.d.ts.map +1 -1
- package/dist/web/self-update.js +19 -3
- package/dist/web/self-update.js.map +1 -1
- package/dist/web/sensitive-path.d.ts.map +1 -1
- package/dist/web/sensitive-path.js +15 -0
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts +56 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +285 -17
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +13 -0
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +16 -8
- package/dist/web/session-wait-registry.js.map +1 -1
- package/package.json +2 -2
- package/skills/codeman/SKILL.md +20 -4
- package/dist/web/public/app.fe8a6b4c.js.br +0 -0
- package/dist/web/public/app.fe8a6b4c.js.gz +0 -0
- package/dist/web/public/constants.affcdde6.js.br +0 -0
- package/dist/web/public/constants.affcdde6.js.gz +0 -0
- package/dist/web/public/i18n.a618ef4e.js +0 -2
- package/dist/web/public/i18n.a618ef4e.js.br +0 -0
- package/dist/web/public/i18n.a618ef4e.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.ff64fafc.js.br +0 -0
- package/dist/web/public/keyboard-accessory.ff64fafc.js.gz +0 -0
- package/dist/web/public/mobile.1bbd2d67.css +0 -1
- package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
- package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
- package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
- package/dist/web/public/panels-ui.4a56ee63.js.gz +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
- package/dist/web/public/session-ui.426fb671.js.br +0 -0
- package/dist/web/public/session-ui.426fb671.js.gz +0 -0
- package/dist/web/public/settings-ui.a191154e.js +0 -106
- package/dist/web/public/settings-ui.a191154e.js.br +0 -0
- package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
- package/dist/web/public/styles.4f0c67a0.css +0 -1
- package/dist/web/public/styles.4f0c67a0.css.br +0 -0
- package/dist/web/public/styles.4f0c67a0.css.gz +0 -0
- package/dist/web/public/terminal-ui.7e787829.js +0 -4
- package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
- package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
package/dist/tmux-manager.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { EventEmitter } from 'node:events';
|
|
24
24
|
import { collectDescendants } from './proc-tree.js';
|
|
25
|
+
import { foreignViewSessionName, buildForeignDockerViewKillCommand, buildForeignAttachCommand, buildForeignDockerAttachCommand, buildForeignRemoteAttachCommand, } from './foreign-tmux.js';
|
|
25
26
|
import { execSync, exec, execFile } from 'node:child_process';
|
|
26
27
|
import { promisify } from 'node:util';
|
|
27
28
|
const execAsync = promisify(exec);
|
|
@@ -271,15 +272,54 @@ export function derivePaneExits(rows, now) {
|
|
|
271
272
|
}
|
|
272
273
|
return exits;
|
|
273
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Decide, from every pane tmux listed, which directory each tmux session's pane
|
|
277
|
+
* is in now (`#{pane_current_path}`). Exported for unit testing.
|
|
278
|
+
*
|
|
279
|
+
* This is what a relative path printed in the terminal is relative TO: a
|
|
280
|
+
* session's `workingDir` is fixed at create time and never follows a `cd`.
|
|
281
|
+
*
|
|
282
|
+
* - Only sessions with EXACTLY ONE pane answer, for the reason
|
|
283
|
+
* {@link derivePaneExits} gives: a hand-split session has no single "the"
|
|
284
|
+
* pane to speak for it.
|
|
285
|
+
* - Only absolute paths count. A blank field is a tmux that could not read the
|
|
286
|
+
* process cwd: usually a pane whose program exited under `remain-on-exit`
|
|
287
|
+
* (tmux 3.4 then reports it blank on every read), otherwise a read that
|
|
288
|
+
* failed that tick. Either way the directory is unknown, not gone, so a
|
|
289
|
+
* single-pane session with a blank field keeps its entry from `previous`.
|
|
290
|
+
* A session split into several panes, or one the read no longer lists, gets
|
|
291
|
+
* no entry: those are authoritative absences, not unanswered questions.
|
|
292
|
+
*
|
|
293
|
+
* The launch directory is NOT filtered here (unlike the rediscovery path): a
|
|
294
|
+
* remote or docker pane that never leaves it is refused by `Session.setPaneCwd()`,
|
|
295
|
+
* and a local pane reporting it really is there.
|
|
296
|
+
*/
|
|
297
|
+
export function derivePaneCurrentPaths(rows, previous = new Map()) {
|
|
298
|
+
const panesPerSession = new Map();
|
|
299
|
+
for (const row of rows) {
|
|
300
|
+
panesPerSession.set(row.sessionName, (panesPerSession.get(row.sessionName) ?? 0) + 1);
|
|
301
|
+
}
|
|
302
|
+
const paths = new Map();
|
|
303
|
+
for (const row of rows) {
|
|
304
|
+
if (panesPerSession.get(row.sessionName) !== 1)
|
|
305
|
+
continue;
|
|
306
|
+
const path = row.currentPath ?? previous.get(row.sessionName);
|
|
307
|
+
if (!path || !path.startsWith('/'))
|
|
308
|
+
continue;
|
|
309
|
+
paths.set(row.sessionName, path);
|
|
310
|
+
}
|
|
311
|
+
return paths;
|
|
312
|
+
}
|
|
274
313
|
/**
|
|
275
314
|
* Could any of these tmux sessions ever produce a pane-exit answer? Exported
|
|
276
315
|
* for unit testing.
|
|
277
316
|
*
|
|
278
317
|
* Mirrors `Session.paneExitApplies`, which is where the rule is enforced. A
|
|
279
318
|
* remote session's local pane holds the ssh client, a docker case's holds a
|
|
280
|
-
* `docker exec` into the container's own tmux,
|
|
281
|
-
*
|
|
282
|
-
*
|
|
319
|
+
* `docker exec` into the container's own tmux, an ADOPTED session's holds a
|
|
320
|
+
* `tmux attach` to a session a human started rather than the agent itself, and
|
|
321
|
+
* a record rebuilt from the socket carries no provenance at all, so the session
|
|
322
|
+
* end forces all four to UNKNOWN whatever tmux reports. A tick that sees only those has nothing to
|
|
283
323
|
* learn, and `refreshPaneExits()` skips its tmux read rather than paying for
|
|
284
324
|
* the answer.
|
|
285
325
|
*
|
|
@@ -294,12 +334,27 @@ export function hasObservablePaneSession(sessions) {
|
|
|
294
334
|
continue;
|
|
295
335
|
if (session.docker)
|
|
296
336
|
continue;
|
|
337
|
+
if (session.adopt)
|
|
338
|
+
continue;
|
|
297
339
|
if (session.discovered === true)
|
|
298
340
|
continue;
|
|
299
341
|
return true;
|
|
300
342
|
}
|
|
301
343
|
return false;
|
|
302
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Which adopted wrappers have a dead pane with a tmux client still attached, from rows of
|
|
347
|
+
* `#{pane_dead} #{session_attached} #{session_name}`. Exported for unit testing.
|
|
348
|
+
*/
|
|
349
|
+
export function endedAdoptedAttaches(rows, adopted) {
|
|
350
|
+
const ended = [];
|
|
351
|
+
for (const row of rows.split('\n')) {
|
|
352
|
+
const [dead, attached, muxName] = row.trim().split(' ');
|
|
353
|
+
if (dead === '1' && Number(attached) > 0 && adopted.has(muxName))
|
|
354
|
+
ended.push(muxName);
|
|
355
|
+
}
|
|
356
|
+
return ended;
|
|
357
|
+
}
|
|
303
358
|
/**
|
|
304
359
|
* Resolve a target pane id from `tmux list-panes -F '#{pane_id}:#{pane_active}'`.
|
|
305
360
|
* Prefers the active pane and falls back to the first valid pane.
|
|
@@ -988,12 +1043,15 @@ export function remoteTmuxLocation(remote, sessionId) {
|
|
|
988
1043
|
*
|
|
989
1044
|
* Emits:
|
|
990
1045
|
* ssh -o BatchMode=yes -t [<COD-107 connection opts>] user@host \
|
|
991
|
-
* 'tmux -L codeman-remote new-session -A -s codeman-ssh-<id> -c <path>
|
|
1046
|
+
* 'tmux -L codeman-remote new-session -A -s codeman-ssh-<id> -c <path> /bin/sh -c <pane-command> \
|
|
992
1047
|
* \; set -t codeman-ssh-<id> status off \; set -t codeman-ssh-<id> mouse off \
|
|
993
1048
|
* \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0 \; \
|
|
994
1049
|
* bind-key -T prefix C-q send-prefix \; unbind-key -a -T prefix \; \
|
|
995
1050
|
* bind-key -T prefix C-q send-prefix \; set -t codeman-ssh-<id> remain-on-exit failed'
|
|
996
1051
|
*
|
|
1052
|
+
* `<pane-command>` is `cd <path> && exec <cli>`, quoted as one token. A per-host `commands.*`
|
|
1053
|
+
* override is that one token alone, with no `/bin/sh -c` in front of it.
|
|
1054
|
+
*
|
|
997
1055
|
* COD-107 — the connection options (`-p`, `-i`, `-J`, SOCKS `-o ProxyCommand`,
|
|
998
1056
|
* arbitrary `-o`) come from the shared `buildSshConnectionArgs(remote)`, so the
|
|
999
1057
|
* prereq tmux probe and this launch connect with identical options.
|
|
@@ -1007,10 +1065,12 @@ export function remoteTmuxLocation(remote, sessionId) {
|
|
|
1007
1065
|
* - The `set` options are scoped per-session (`set -t <name>` / server-level
|
|
1008
1066
|
* `set -s`), never `-g`, so they never mutate other sessions' prefix/mouse.
|
|
1009
1067
|
* - The whole tmux invocation is a SINGLE ssh argument (the remote login shell
|
|
1010
|
-
* runs it), so it is shell-quoted as one unit
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1068
|
+
* runs it), so it is shell-quoted as one unit. The `cd && exec` command is the
|
|
1069
|
+
* pane command: Codeman's own text goes to `/bin/sh -c` as separate tmux
|
|
1070
|
+
* arguments, and a per-host `commands.*` override stays one argument, which
|
|
1071
|
+
* tmux hands to the remote default-shell (see the comment at `paneCommand`).
|
|
1072
|
+
* Each is shell-quoted, so escaping stays correct through every layer even
|
|
1073
|
+
* when the remote path contains spaces.
|
|
1014
1074
|
*/
|
|
1015
1075
|
export function buildRemoteLaunchCommand(options) {
|
|
1016
1076
|
const { mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId } = options;
|
|
@@ -1018,9 +1078,9 @@ export function buildRemoteLaunchCommand(options) {
|
|
|
1018
1078
|
// hardcoding --dangerously-skip-permissions, so a non-granted multi-user user's
|
|
1019
1079
|
// downgraded 'auto' actually reaches the remote agent (the default command otherwise
|
|
1020
1080
|
// ignored claudeMode). A per-host `commands.claude` override stays authoritative
|
|
1021
|
-
// (admin's explicit choice). Wrapped in
|
|
1022
|
-
// `defaultRemoteCommandForMode`: `claude` lives under a per-user PATH
|
|
1023
|
-
// only an interactive login shell resolves (see that function's comment).
|
|
1081
|
+
// (admin's explicit choice). Wrapped in the login shell (`remoteLoginShellCommand`) for
|
|
1082
|
+
// the same reason as `defaultRemoteCommandForMode`: `claude` lives under a per-user PATH
|
|
1083
|
+
// entry that only an interactive login shell resolves (see that function's comment).
|
|
1024
1084
|
const override = remote.commands?.[mode];
|
|
1025
1085
|
let modeCommand;
|
|
1026
1086
|
if (override) {
|
|
@@ -1085,16 +1145,24 @@ export function buildRemoteLaunchCommand(options) {
|
|
|
1085
1145
|
modeCommand = defaultRemoteCommandForMode(mode);
|
|
1086
1146
|
}
|
|
1087
1147
|
const remoteName = remoteTmuxSessionName(sessionId);
|
|
1088
|
-
// Innermost: the command tmux runs in the new pane.
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1148
|
+
// Innermost: the command tmux runs in the new pane. `exec` replaces the shell
|
|
1149
|
+
// with the CLI so the pane PID is the agent itself.
|
|
1150
|
+
//
|
|
1151
|
+
// tmux runs a ONE-argument command through the remote default-shell, which is
|
|
1152
|
+
// the user's login shell, and bypasses it for two or more arguments (both
|
|
1153
|
+
// measured on tmux 3.4). Codeman's own text is POSIX (`"${SHELL:-/bin/sh}"` is
|
|
1154
|
+
// a parse error in fish, status 127, a dead pane on every new session), so it
|
|
1155
|
+
// goes to `/bin/sh -c` as separate arguments and no login shell has to parse it.
|
|
1156
|
+
// An override is the user's own text for THEIR shell: it stays one argument,
|
|
1157
|
+
// because `source` and `[[` work in bash and zsh and die in a dash `/bin/sh`.
|
|
1091
1158
|
const paneCommand = `cd ${shellescape(remote.remotePath)} && ${modeCommand}`;
|
|
1159
|
+
const paneArgv = `${override ? '' : '/bin/sh -c '}${shellescape(paneCommand)}`;
|
|
1092
1160
|
// The tmux command line, with `\;` separating commands so the config `set`s
|
|
1093
1161
|
// apply on the SAME connection (and are idempotent on reattach). Options are
|
|
1094
1162
|
// scoped per-session (`set -t <name>` / server `set -s`), NEVER `-g`, so a
|
|
1095
1163
|
// shared remote tmux server's other sessions keep their own prefix/mouse.
|
|
1096
1164
|
const tmuxInvocation = [
|
|
1097
|
-
`tmux -L ${REMOTE_TMUX_SOCKET} new-session -A -s ${remoteName} -c ${shellescape(remote.remotePath)} ${
|
|
1165
|
+
`tmux -L ${REMOTE_TMUX_SOCKET} new-session -A -s ${remoteName} -c ${shellescape(remote.remotePath)} ${paneArgv}`,
|
|
1098
1166
|
`set -t ${remoteName} status off`,
|
|
1099
1167
|
`set -t ${remoteName} mouse off`,
|
|
1100
1168
|
`set -t ${remoteName} prefix ${NESTED_TMUX_PREFIX}`,
|
|
@@ -1243,7 +1311,7 @@ function claudeDockerPaneCommand(modeCommand, sessionId, resumeId) {
|
|
|
1243
1311
|
* Build the ONE `bash -c` launch string for a docker session: image-check ->
|
|
1244
1312
|
* ensure (inspect-or-create) -> start -> `exec docker exec -it` into the durable
|
|
1245
1313
|
* in-container tmux (resume-aware). PURE and unit-testable. The escaping survives
|
|
1246
|
-
* four layers: outer `bash -c
|
|
1314
|
+
* four layers: outer `bash -c '…'` (`bashCArg()` at respawn-pane) -> the joined
|
|
1247
1315
|
* command -> `docker exec … sh -lc '<tmux>'` -> tmux `'<paneCommand>'`.
|
|
1248
1316
|
*/
|
|
1249
1317
|
export function buildDockerLaunchCommand(opts) {
|
|
@@ -1270,8 +1338,8 @@ export function buildDockerLaunchCommand(opts) {
|
|
|
1270
1338
|
else if (resumeSessionId) {
|
|
1271
1339
|
modeCommand = appendResumeFlag(modeCommand, mode, resumeSessionId);
|
|
1272
1340
|
}
|
|
1273
|
-
// Run by tmux
|
|
1274
|
-
// pane PID the agent itself.
|
|
1341
|
+
// Run by tmux through the container's default-shell, so the path is shell-quoted
|
|
1342
|
+
// here. `exec` makes the pane PID the agent itself.
|
|
1275
1343
|
const paneCommand = `cd ${workdir} && ${modeCommand}`;
|
|
1276
1344
|
// `setenv -g` primes the session id so reattaches / newly-created panes inherit
|
|
1277
1345
|
// it. `new-session -A` = attach-or-create (idempotent + resume-aware). Options
|
|
@@ -1325,11 +1393,10 @@ export function buildDockerLaunchCommand(opts) {
|
|
|
1325
1393
|
const ensure = adopted
|
|
1326
1394
|
? `${base} inspect ${name} >/dev/null 2>&1 || { echo ${notFoundMsg}; exit 1; }`
|
|
1327
1395
|
: `${base} inspect ${name} >/dev/null 2>&1 || ${createCommand}`;
|
|
1328
|
-
//
|
|
1329
|
-
//
|
|
1330
|
-
//
|
|
1331
|
-
//
|
|
1332
|
-
// every other line in this builder already uses.
|
|
1396
|
+
// A `grep -qx` pipeline, in the single-quoted form every other line in this builder
|
|
1397
|
+
// uses. This chain reaches `bash -c` verbatim now (`bashCArg()`), but it went through
|
|
1398
|
+
// a double-quoted layer for a long time, where a `"` closed that string early and tmux
|
|
1399
|
+
// failed with a bare `execvp(3) failed`, so keep it free of double quotes and `$(…)`.
|
|
1333
1400
|
const start = adopted
|
|
1334
1401
|
? `${base} inspect -f ${shellescape('{{.State.Running}}')} ${name} 2>/dev/null | grep -qx true || { echo ${notRunningMsg}; exit 1; }`
|
|
1335
1402
|
: `${base} start ${name} >/dev/null 2>&1 || { echo ${startFailMsg}; exit 1; }`;
|
|
@@ -1524,6 +1591,44 @@ function buildRemoteSessionCommand(options) {
|
|
|
1524
1591
|
}
|
|
1525
1592
|
return buildRemoteLaunchCommand(options);
|
|
1526
1593
|
}
|
|
1594
|
+
/**
|
|
1595
|
+
* ADOPTION: the pane command for a wrapper session around a tmux session a
|
|
1596
|
+
* human started. Sibling of `buildRemoteSessionCommand` and
|
|
1597
|
+
* `buildDockerLaunchCommand`, and the same shape of decision: the wrapper is an
|
|
1598
|
+
* ordinary `codeman-<8hex>` session on OUR socket, and only what runs INSIDE its
|
|
1599
|
+
* pane differs per location.
|
|
1600
|
+
*
|
|
1601
|
+
* That indirection is what makes adoption safe by construction rather than by
|
|
1602
|
+
* discipline: `killSession` only ever kills the wrapper on our own socket, so
|
|
1603
|
+
* there is no shape of caller bug that reaches the foreign server with a
|
|
1604
|
+
* `kill-session`. It is the same reasoning that keeps a non-owned REMOTE session
|
|
1605
|
+
* safe (see the detach-not-kill early return in `killSession`).
|
|
1606
|
+
*/
|
|
1607
|
+
export function buildAdoptSessionCommand(adopt, sessionId) {
|
|
1608
|
+
const target = {
|
|
1609
|
+
socketPath: adopt.socketPath,
|
|
1610
|
+
targetSession: adopt.targetSession,
|
|
1611
|
+
// Derived rather than required: the view name is a function of the Codeman
|
|
1612
|
+
// session id, so a record that predates it (or one whose creator filled it in
|
|
1613
|
+
// after construction) still produces the right name instead of an empty `-s`.
|
|
1614
|
+
viewSession: adopt.viewSession || foreignViewSessionName(sessionId),
|
|
1615
|
+
};
|
|
1616
|
+
if (adopt.location === 'docker' && adopt.docker) {
|
|
1617
|
+
return buildForeignDockerAttachCommand({
|
|
1618
|
+
...target,
|
|
1619
|
+
dockerBase: buildDockerBaseArgs(adopt.docker).join(' '),
|
|
1620
|
+
containerName: adopt.docker.containerName,
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
if (adopt.location === 'remote' && adopt.remote) {
|
|
1624
|
+
return buildForeignRemoteAttachCommand({
|
|
1625
|
+
...target,
|
|
1626
|
+
sshArgs: buildSshConnectionArgs(adopt.remote),
|
|
1627
|
+
sshTarget: remoteSshTarget(adopt.remote),
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
return buildForeignAttachCommand(target);
|
|
1631
|
+
}
|
|
1527
1632
|
/**
|
|
1528
1633
|
* Push one environment variable into a tmux session with `setenv`.
|
|
1529
1634
|
*
|
|
@@ -1674,6 +1779,13 @@ export class TmuxManager extends EventEmitter {
|
|
|
1674
1779
|
* raw pane reading.
|
|
1675
1780
|
*/
|
|
1676
1781
|
paneExits = new Map();
|
|
1782
|
+
/**
|
|
1783
|
+
* muxName -> `#{pane_current_path}` from the last authoritative pane read
|
|
1784
|
+
* ({@link derivePaneCurrentPaths}). Replaced wholesale by each non-empty read,
|
|
1785
|
+
* so it is bounded by the panes on the socket. Like {@link paneExits} it holds
|
|
1786
|
+
* the raw reading; `Session.setPaneCwd()` decides which sessions may use it.
|
|
1787
|
+
*/
|
|
1788
|
+
paneCurrentPaths = new Map();
|
|
1677
1789
|
/** The pane-exit watcher's own interval. Runs whether or not stats are on. */
|
|
1678
1790
|
paneExitInterval = null;
|
|
1679
1791
|
/**
|
|
@@ -2068,7 +2180,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2068
2180
|
* In test mode: creates an in-memory session only (no real tmux session).
|
|
2069
2181
|
*/
|
|
2070
2182
|
async createSession(options) {
|
|
2071
|
-
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;
|
|
2183
|
+
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, adopt, owner, } = options;
|
|
2072
2184
|
const muxName = `codeman-${sessionId.slice(0, 8)}`;
|
|
2073
2185
|
if (!isValidMuxName(muxName)) {
|
|
2074
2186
|
throw new Error('Invalid session name: contains unsafe characters');
|
|
@@ -2086,6 +2198,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2086
2198
|
workingDir,
|
|
2087
2199
|
remote,
|
|
2088
2200
|
docker,
|
|
2201
|
+
adopt,
|
|
2089
2202
|
owner,
|
|
2090
2203
|
mode,
|
|
2091
2204
|
attached: false,
|
|
@@ -2111,14 +2224,22 @@ export class TmuxManager extends EventEmitter {
|
|
|
2111
2224
|
// surfacing as a bare `execvp(3) failed: No such file or directory` with nothing
|
|
2112
2225
|
// pointing at the real cause. The container's own CLIs are verified by the adoption
|
|
2113
2226
|
// preflight / image gate before launch instead.
|
|
2114
|
-
|
|
2227
|
+
//
|
|
2228
|
+
// ⚠️ Skipped for an ADOPTED session for the mirror-image reason: its CLI was started by
|
|
2229
|
+
// a human in a process we did not spawn, and our pane only runs `tmux attach`. Requiring
|
|
2230
|
+
// the binary here would reject adopting a claude that lives in a container or on an ssh
|
|
2231
|
+
// host, and (worse) reject a LOCAL adoption whenever the agent is outside the server
|
|
2232
|
+
// process's PATH, which is the usual systemd/launchd situation.
|
|
2233
|
+
const cliRunsInContainer = !!docker || !!adopt;
|
|
2115
2234
|
if (!cliRunsInContainer && !cliDir) {
|
|
2116
2235
|
const message = missingCliMessage(mode);
|
|
2117
2236
|
if (message)
|
|
2118
2237
|
throw new Error(message);
|
|
2119
2238
|
}
|
|
2120
2239
|
const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
|
|
2121
|
-
const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir,
|
|
2240
|
+
const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir,
|
|
2241
|
+
// An adopted wrapper launches no agent, so nothing can carry the exporter.
|
|
2242
|
+
!!(remote || docker || adopt));
|
|
2122
2243
|
const idleCompactionOff = await this._resolveIdleCompactionOptOut(mode, statusLine, keepWarm, spawnedByAgent);
|
|
2123
2244
|
// The user's own REAL statusLine, if any (walked via Claude Code's own
|
|
2124
2245
|
// settings precedence) — exported below so the shared exporter script
|
|
@@ -2151,11 +2272,21 @@ export class TmuxManager extends EventEmitter {
|
|
|
2151
2272
|
try {
|
|
2152
2273
|
// Build the full command to run inside tmux
|
|
2153
2274
|
const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
|
|
2154
|
-
const fullCmd =
|
|
2155
|
-
?
|
|
2156
|
-
:
|
|
2157
|
-
?
|
|
2158
|
-
:
|
|
2275
|
+
const fullCmd = adopt
|
|
2276
|
+
? buildAdoptSessionCommand(adopt, sessionId)
|
|
2277
|
+
: docker
|
|
2278
|
+
? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
|
|
2279
|
+
: remote
|
|
2280
|
+
? buildRemoteSessionCommand({
|
|
2281
|
+
mode,
|
|
2282
|
+
remote,
|
|
2283
|
+
sessionId,
|
|
2284
|
+
claudeMode,
|
|
2285
|
+
allowedTools,
|
|
2286
|
+
ompConfig,
|
|
2287
|
+
resumeSessionId,
|
|
2288
|
+
})
|
|
2289
|
+
: localFullCmd;
|
|
2159
2290
|
// Create tmux session in three steps to handle cold-start (no server running)
|
|
2160
2291
|
// and avoid the race where the command exits before remain-on-exit is set:
|
|
2161
2292
|
// 2. Set remain-on-exit (server now exists, session won't vanish on exit)
|
|
@@ -2205,8 +2336,10 @@ export class TmuxManager extends EventEmitter {
|
|
|
2205
2336
|
this.applyEnvOverrides(muxName, envOverrides);
|
|
2206
2337
|
// Replace the shell with the actual command (no echo in terminal). Keep
|
|
2207
2338
|
// pane launch in /tmp, then cd inside bash against the current mount table.
|
|
2208
|
-
|
|
2209
|
-
|
|
2339
|
+
// ⚠️ An adopted pane is never `cd`'d: it only attaches, and its `workingDir`
|
|
2340
|
+
// is the neutral wrapper folder, not where the foreign pane is.
|
|
2341
|
+
const launchCmd = remote || docker || adopt ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
|
|
2342
|
+
execSync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${bashCArg(launchCmd, !!(remote || docker || adopt))}`, {
|
|
2210
2343
|
timeout: EXEC_TIMEOUT_MS,
|
|
2211
2344
|
stdio: 'ignore',
|
|
2212
2345
|
});
|
|
@@ -2260,6 +2393,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2260
2393
|
workingDir,
|
|
2261
2394
|
remote,
|
|
2262
2395
|
docker,
|
|
2396
|
+
adopt,
|
|
2263
2397
|
owner,
|
|
2264
2398
|
mode,
|
|
2265
2399
|
attached: false,
|
|
@@ -2357,13 +2491,20 @@ export class TmuxManager extends EventEmitter {
|
|
|
2357
2491
|
const session = this.sessions.get(sessionId);
|
|
2358
2492
|
if (!session)
|
|
2359
2493
|
return null;
|
|
2494
|
+
// Adoption metadata comes from the tracked session, not the caller: a respawn
|
|
2495
|
+
// of an adopted wrapper RE-ATTACHES to the same foreign target. It must never
|
|
2496
|
+
// be able to become "launch a fresh agent here" because a caller forgot to
|
|
2497
|
+
// pass the field through.
|
|
2498
|
+
const adopt = options.adopt ?? session.adopt;
|
|
2360
2499
|
const muxName = session.muxName;
|
|
2361
2500
|
if (!isValidMuxName(muxName) || !isValidPath(workingDir))
|
|
2362
2501
|
return null;
|
|
2363
2502
|
// Resolve CLI binary directory based on mode
|
|
2364
2503
|
const { pathExport } = this.buildPathExport(mode);
|
|
2365
2504
|
const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
|
|
2366
|
-
const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir,
|
|
2505
|
+
const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir,
|
|
2506
|
+
// An adopted wrapper launches no agent, so nothing can carry the exporter.
|
|
2507
|
+
!!(remote || docker || adopt));
|
|
2367
2508
|
const idleCompactionOff = await this._resolveIdleCompactionOptOut(mode, statusLine, keepWarm, spawnedByAgent);
|
|
2368
2509
|
const userStatusLineCommand = statusLineCommand ? await findEffectiveUserStatusLineCommand(workingDir) : undefined;
|
|
2369
2510
|
const baseCmd = buildSpawnCommand({
|
|
@@ -2391,11 +2532,13 @@ export class TmuxManager extends EventEmitter {
|
|
|
2391
2532
|
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
|
2392
2533
|
const cmd = wrapWithNice(baseCmd, config);
|
|
2393
2534
|
const localFullCmd = `${buildNofileLimitCommand()} && ${pathExport}${envExportsStr} && ${cmd}`;
|
|
2394
|
-
const fullCmd =
|
|
2395
|
-
?
|
|
2396
|
-
:
|
|
2397
|
-
?
|
|
2398
|
-
:
|
|
2535
|
+
const fullCmd = adopt
|
|
2536
|
+
? buildAdoptSessionCommand(adopt, sessionId)
|
|
2537
|
+
: docker
|
|
2538
|
+
? buildDockerLaunchCommand(resolveDockerLaunchOptions(mode, docker, sessionId, resumeSessionId))
|
|
2539
|
+
: remote
|
|
2540
|
+
? buildRemoteSessionCommand({ mode, remote, sessionId, claudeMode, allowedTools, ompConfig, resumeSessionId })
|
|
2541
|
+
: localFullCmd;
|
|
2399
2542
|
try {
|
|
2400
2543
|
// Same per-CLI env setup as createSession, re-applied so the respawned pane inherits it.
|
|
2401
2544
|
this._configureCliEnv(muxName, sessionId, mode, legacyConfigForMode(mode, options));
|
|
@@ -2404,8 +2547,10 @@ export class TmuxManager extends EventEmitter {
|
|
|
2404
2547
|
// dropping the ones the caller retired first (see applyEnvOverrides).
|
|
2405
2548
|
this.applyEnvOverrides(muxName, envOverrides, unsetEnvKeys);
|
|
2406
2549
|
// -c /tmp + cd bounce — see createSession() for rationale (stale FUSE state).
|
|
2407
|
-
|
|
2408
|
-
|
|
2550
|
+
// ⚠️ An adopted pane is never `cd`'d: it only attaches, and its `workingDir`
|
|
2551
|
+
// is the neutral wrapper folder, not where the foreign pane is.
|
|
2552
|
+
const launchCmd = remote || docker || adopt ? fullCmd : `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
|
|
2553
|
+
await execAsync(`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${bashCArg(launchCmd, !!(remote || docker || adopt))}`, {
|
|
2409
2554
|
timeout: EXEC_TIMEOUT_MS,
|
|
2410
2555
|
});
|
|
2411
2556
|
// Wait for the respawned process to start
|
|
@@ -2605,6 +2750,56 @@ export class TmuxManager extends EventEmitter {
|
|
|
2605
2750
|
// for a non-owned session. The only `kill-session` we run is on OUR LOCAL
|
|
2606
2751
|
// socket (`this.tmux()` = `tmux -L codeman` on THIS host), which kills the
|
|
2607
2752
|
// local pane — it does NOT reach the REMOTE socket.
|
|
2753
|
+
// ADOPTION: DETACH-NOT-KILL, by construction rather than by discipline.
|
|
2754
|
+
//
|
|
2755
|
+
// An adopted session's wrapper is an ordinary session on OUR socket whose
|
|
2756
|
+
// pane runs `tmux attach` (or `docker exec …`/`ssh …` then attach) against a
|
|
2757
|
+
// FOREIGN server. Killing the wrapper kills our attach client, which is a
|
|
2758
|
+
// DETACH on the foreign side; the human's session keeps running, and our
|
|
2759
|
+
// grouped view session self-destroys via `destroy-unattached on`.
|
|
2760
|
+
//
|
|
2761
|
+
// The guard below is a formality more than a fence: `this.tmux()` only ever
|
|
2762
|
+
// addresses our own socket, so no code path here CAN reach the foreign
|
|
2763
|
+
// server. It is written out anyway so the intent survives future edits, the
|
|
2764
|
+
// same way the non-owned-remote early return does.
|
|
2765
|
+
if (session.adopt) {
|
|
2766
|
+
console.log(`[TmuxManager] DETACH (adopted): tearing down wrapper only for ${session.muxName}`);
|
|
2767
|
+
// A `docker exec` survives its client, so the in-container view would stay
|
|
2768
|
+
// attached forever and leak. Best-effort, and it can only reach OUR view.
|
|
2769
|
+
// See buildForeignDockerViewKillCommand for why local/remote need nothing.
|
|
2770
|
+
// Fire and forget like the owned docker branch below: a daemon that stopped
|
|
2771
|
+
// answering must not hold the event loop for the whole timeout.
|
|
2772
|
+
if (session.adopt.location === 'docker' && session.adopt.docker) {
|
|
2773
|
+
try {
|
|
2774
|
+
exec(buildForeignDockerViewKillCommand({
|
|
2775
|
+
dockerBase: buildDockerBaseArgs(session.adopt.docker).join(' '),
|
|
2776
|
+
containerName: session.adopt.docker.containerName,
|
|
2777
|
+
socketPath: session.adopt.socketPath,
|
|
2778
|
+
viewSession: session.adopt.viewSession || foreignViewSessionName(sessionId),
|
|
2779
|
+
}), { timeout: EXEC_TIMEOUT_MS }, () => { });
|
|
2780
|
+
}
|
|
2781
|
+
catch {
|
|
2782
|
+
// Best-effort: the container may be gone, or the view already collected.
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
if (isValidMuxName(session.muxName)) {
|
|
2786
|
+
try {
|
|
2787
|
+
await execAsync(`${this.tmux()} kill-session -t "${session.muxName}" 2>/dev/null`, {
|
|
2788
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
catch {
|
|
2792
|
+
// Wrapper may already be gone.
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
this.lastPaneCount.delete(session.muxName);
|
|
2796
|
+
this.clearPaneExit(session.muxName);
|
|
2797
|
+
this.sessions.delete(sessionId);
|
|
2798
|
+
this.clearRemoteReconnectState(sessionId);
|
|
2799
|
+
this.saveSessions();
|
|
2800
|
+
this.emit('sessionKilled', { sessionId });
|
|
2801
|
+
return true;
|
|
2802
|
+
}
|
|
2608
2803
|
if (session.remote && session.remote.owned === false) {
|
|
2609
2804
|
console.log(`[TmuxManager] DETACH (non-owned remote): tearing down local pane only for ${session.muxName}`);
|
|
2610
2805
|
if (isValidMuxName(session.muxName)) {
|
|
@@ -3002,6 +3197,18 @@ export class TmuxManager extends EventEmitter {
|
|
|
3002
3197
|
stats: statsMap.get(session.pid) || undefined,
|
|
3003
3198
|
}));
|
|
3004
3199
|
}
|
|
3200
|
+
/**
|
|
3201
|
+
* The directory the session's pane was in at the last authoritative pane
|
|
3202
|
+
* read, or `undefined` when unknown. Costs nothing: it reads the map the
|
|
3203
|
+
* pane-exit watcher's batched `list-panes -a` fills, so no tmux call is made
|
|
3204
|
+
* per click. A failed or empty read keeps the previous answers, and so does
|
|
3205
|
+
* a blank `#{pane_current_path}` on a single-pane session. After a successful
|
|
3206
|
+
* read, `undefined` means that read listed no single directory for the
|
|
3207
|
+
* session: it was split into several panes or is no longer on the socket.
|
|
3208
|
+
*/
|
|
3209
|
+
getPaneCurrentPath(muxName) {
|
|
3210
|
+
return this.paneCurrentPaths.get(muxName);
|
|
3211
|
+
}
|
|
3005
3212
|
/**
|
|
3006
3213
|
* What the last pane read saw of this tmux session's agent. `undefined` is
|
|
3007
3214
|
* the UNKNOWN answer and must never be rendered as "alive": it covers a pane
|
|
@@ -3064,6 +3271,11 @@ export class TmuxManager extends EventEmitter {
|
|
|
3064
3271
|
}
|
|
3065
3272
|
if (rows.length === 0)
|
|
3066
3273
|
return;
|
|
3274
|
+
// The directory reading rides the same read. It is applied before the
|
|
3275
|
+
// generation check below: a respawn invalidates an exit observation, but the
|
|
3276
|
+
// pane's directory at read time is still the best answer there is. A blank
|
|
3277
|
+
// field on a single-pane session carries the previous answer forward.
|
|
3278
|
+
this.paneCurrentPaths = derivePaneCurrentPaths(rows, this.paneCurrentPaths);
|
|
3067
3279
|
// A pane was respawned or killed while this read was out, so what it saw is
|
|
3068
3280
|
// already history. Dropping it is what stops a freshly respawned pane from
|
|
3069
3281
|
// being republished as exited.
|
|
@@ -3266,8 +3478,51 @@ export class TmuxManager extends EventEmitter {
|
|
|
3266
3478
|
catch (err) {
|
|
3267
3479
|
console.error('[TmuxManager] Remote reconnect watcher error:', err);
|
|
3268
3480
|
}
|
|
3481
|
+
void this.detachEndedAdoptedAttaches().catch((err) => {
|
|
3482
|
+
console.error('[TmuxManager] Adopted attach watcher error:', err);
|
|
3483
|
+
});
|
|
3269
3484
|
}, intervalMs);
|
|
3270
3485
|
}
|
|
3486
|
+
/**
|
|
3487
|
+
* Let go of a wrapper whose pane died (#510). That pane holds the attach to someone
|
|
3488
|
+
* else's tmux session, so it dies when that session ends, the transport drops or the
|
|
3489
|
+
* view gets detached, and `paneExit` rightly reports none of them. But `remain-on-exit`
|
|
3490
|
+
* keeps Codeman's own tmux client on the dead pane, so the tab kept its pid and `busy`,
|
|
3491
|
+
* input went nowhere and both attach routes refused it.
|
|
3492
|
+
*
|
|
3493
|
+
* Detaching that client is an ordinary PTY exit with status 0: the pid goes null, the tab
|
|
3494
|
+
* stays open, and selecting it starts the wrapper again, which joins the target if it is
|
|
3495
|
+
* back and otherwise prints that it is gone. Only Codeman's own socket is touched.
|
|
3496
|
+
*/
|
|
3497
|
+
async detachEndedAdoptedAttaches() {
|
|
3498
|
+
const adopted = new Set();
|
|
3499
|
+
for (const session of this.sessions.values()) {
|
|
3500
|
+
if (session.adopt)
|
|
3501
|
+
adopted.add(session.muxName);
|
|
3502
|
+
}
|
|
3503
|
+
if (adopted.size === 0)
|
|
3504
|
+
return;
|
|
3505
|
+
for (const muxName of endedAdoptedAttaches(await this.readAttachRows(), adopted)) {
|
|
3506
|
+
await this.detachClients(muxName);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
/** Rows for {@link endedAdoptedAttaches}. Empty when tmux did not answer. */
|
|
3510
|
+
async readAttachRows() {
|
|
3511
|
+
if (IS_TEST_MODE)
|
|
3512
|
+
return '';
|
|
3513
|
+
const format = '#{pane_dead} #{session_attached} #{session_name}';
|
|
3514
|
+
const { stdout } = await execAsync(`${this.tmux()} list-panes -a -F '${format}' 2>/dev/null || true`, {
|
|
3515
|
+
encoding: 'utf-8',
|
|
3516
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
3517
|
+
});
|
|
3518
|
+
return stdout;
|
|
3519
|
+
}
|
|
3520
|
+
/** Detach every tmux client from one of Codeman's own sessions. */
|
|
3521
|
+
async detachClients(muxName) {
|
|
3522
|
+
if (IS_TEST_MODE || !isValidMuxName(muxName))
|
|
3523
|
+
return;
|
|
3524
|
+
await execAsync(`${this.tmux()} detach-client -s "${muxName}"`, { timeout: EXEC_TIMEOUT_MS });
|
|
3525
|
+
}
|
|
3271
3526
|
stopRemoteReconnectWatcher() {
|
|
3272
3527
|
if (this.remoteReconnectInterval) {
|
|
3273
3528
|
clearInterval(this.remoteReconnectInterval);
|
|
@@ -4037,6 +4292,28 @@ export class TmuxManager extends EventEmitter {
|
|
|
4037
4292
|
}
|
|
4038
4293
|
}
|
|
4039
4294
|
}
|
|
4295
|
+
/**
|
|
4296
|
+
* The `bash -c` argument of a pane launch (`respawn-pane … bash -c <arg>`, run through the
|
|
4297
|
+
* local sh). A remote, docker or adopted command is already quoted for the shell at the far
|
|
4298
|
+
* end and must reach bash verbatim, so it goes in single quotes. Double quotes
|
|
4299
|
+
* (JSON.stringify) let the local sh expand every `$` first: the PATH bootstrap of
|
|
4300
|
+
* `withRemoteTmuxPath` (`$HOME/.local/bin:…:$PATH`) arrived on the remote host as this
|
|
4301
|
+
* host's home and PATH, so a remote user with another home found no tmux and every new
|
|
4302
|
+
* remote pane exited 127. `${SHELL:-/bin/sh}` in the remote launch was the local shell for
|
|
4303
|
+
* the same reason, and an adopted ssh attach carries the same bootstrap, so a tmux in the
|
|
4304
|
+
* remote `~/.local/bin` was found by discovery and then missed by the attach. Single quotes
|
|
4305
|
+
* also keep a real newline, which JSON escaping left as the two characters `\n`.
|
|
4306
|
+
* An adopted attach goes verbatim at all three locations, the local one included: every
|
|
4307
|
+
* value in it is already quoted by `foreign-tmux.ts`, and nothing in it is for this shell.
|
|
4308
|
+
* An agent launched on this host keeps its double-quoted form.
|
|
4309
|
+
*
|
|
4310
|
+
* ⚠️ So nothing in a verbatim launch is expanded on this host any more, the docker client's
|
|
4311
|
+
* own arguments included: a bare `$VAR` in `extraCreateArgs`/`extraExecArgs` reaches docker
|
|
4312
|
+
* as those literal characters.
|
|
4313
|
+
*/
|
|
4314
|
+
export function bashCArg(launchCmd, verbatim) {
|
|
4315
|
+
return verbatim ? shellescape(launchCmd) : JSON.stringify(launchCmd);
|
|
4316
|
+
}
|
|
4040
4317
|
/**
|
|
4041
4318
|
* Shell-escape a string for use as a single argument.
|
|
4042
4319
|
* Wraps in single quotes, escaping any embedded single quotes.
|