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/session.js
CHANGED
|
@@ -28,13 +28,16 @@
|
|
|
28
28
|
* @module session
|
|
29
29
|
*/
|
|
30
30
|
import { EventEmitter } from 'node:events';
|
|
31
|
-
import { basename } from 'node:path';
|
|
31
|
+
import { basename, isAbsolute } from 'node:path';
|
|
32
|
+
import { realpath } from 'node:fs/promises';
|
|
32
33
|
import { execSync, execFileSync } from 'node:child_process';
|
|
33
34
|
import { v4 as uuidv4 } from 'uuid';
|
|
34
35
|
import * as pty from 'node-pty';
|
|
35
36
|
import { DEFAULT_NICE_CONFIG, getErrorMessage, isAdvisorModel, isEffortLevel, } from './types.js';
|
|
36
37
|
import { resolveAndClaimOmpSessionId } from './utils/omp-session-resolver.js';
|
|
37
38
|
import { claudeTranscriptExists } from './utils/claude-transcript.js';
|
|
39
|
+
import { probePathKind } from './utils/bounded-path-probe.js';
|
|
40
|
+
import { PATH_PROBE_TIMEOUT_MS, WORKSPACE_REALPATH_RETRY_MS } from './config/path-probe.js';
|
|
38
41
|
import { matchesPattern } from './config/cli-registry/patterns.js';
|
|
39
42
|
import { probeDockerCliVersion } from './docker-hosts.js';
|
|
40
43
|
import { probeRemoteCliVersion } from './remote-hosts.js';
|
|
@@ -150,8 +153,8 @@ function getModeLabel(mode) {
|
|
|
150
153
|
*
|
|
151
154
|
* Only such a CLI needs its version probed at session start — probing one with no gates
|
|
152
155
|
* would spawn a `--version` subprocess whose answer nothing reads. Today that is claude
|
|
153
|
-
* (the `--name` flag, gated at 2.1.224)
|
|
154
|
-
* `mode === 'claude'
|
|
156
|
+
* (the `--name` flag, gated at 2.1.224) and codex (`--no-daemon`, gated at 0.162.0). The
|
|
157
|
+
* probe used to be written as `mode === 'claude'`, when claude was the only one.
|
|
155
158
|
*/
|
|
156
159
|
function cliNeedsVersionProbe(mode) {
|
|
157
160
|
return Object.keys(getCli(mode)?.capabilities.gates ?? {}).length > 0;
|
|
@@ -175,23 +178,21 @@ function cliExportsTruecolor(mode) {
|
|
|
175
178
|
* Codex, Claude Code, and Gemini are known, controlled (Ink/React) TUIs that
|
|
176
179
|
* repaint via cursor positioning, so dropping the alt-screen switch is safe —
|
|
177
180
|
* content stays in the normal buffer. Excluded: `shell` (arbitrary programs like
|
|
178
|
-
* vim/less/htop legitimately need the alt screen), `opencode` (
|
|
179
|
-
*
|
|
181
|
+
* vim/less/htop legitimately need the alt screen), `opencode`, `copilot` and `pi` (their own
|
|
182
|
+
* MIDDLE strip, isMuxMouseStripMode) and `grok` (a fullscreen alt-screen TUI with
|
|
180
183
|
* mouse support). Keep parity with the replay-side strip (`stripReplayBuffer` in
|
|
181
184
|
* session-routes.ts); the table in `CliCapabilities.altScreen` is pinned for every
|
|
182
185
|
* stock CLI in test/claude-scrollback-strip.test.ts.
|
|
183
186
|
*
|
|
184
187
|
* ⚠️ Being excluded here does NOT preserve the alt screen. Every excluded mode
|
|
185
|
-
* falls through to isMuxAltScreenOnlyStripMode(), which
|
|
186
|
-
* toggles too whenever the session is tmux-backed, and
|
|
187
|
-
* (both refuse the direct-PTY fallback). What exclusion
|
|
188
|
-
* of the full strip: `\x1b[3J` and
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* still gets its `?1049h` stripped and paints into the main buffer, exactly like
|
|
194
|
-
* vim inside a tmux `shell` session.
|
|
188
|
+
* falls through to isMuxMouseStripMode() or isMuxAltScreenOnlyStripMode(), which
|
|
189
|
+
* strip the alt-screen toggles too whenever the session is tmux-backed, and
|
|
190
|
+
* pi/opencode ALWAYS are (both refuse the direct-PTY fallback). What exclusion
|
|
191
|
+
* actually buys is the rest of the full strip: `\x1b[3J` survives, and for the
|
|
192
|
+
* narrow strip the mouse-tracking DECSETs too. pi (fullscreen by default since
|
|
193
|
+
* 1.0.0) is a mouse-aware TUI like opencode and copilot, so it takes the mouse
|
|
194
|
+
* strip (isMuxMouseStripMode): a drag selects locally and `cliMouseTracking`
|
|
195
|
+
* keeps the wheel working across a buffer replay.
|
|
195
196
|
*/
|
|
196
197
|
export function isAltScreenStripMode(mode) {
|
|
197
198
|
return getCli(mode)?.capabilities.altScreen === 'strip-full';
|
|
@@ -268,6 +269,21 @@ const TEST_PTY_SCRIPT = 'if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
|
268
269
|
const DOCKER_CLI_VERSION_PROBE_DELAY_MS = 3000;
|
|
269
270
|
/** Delay before the over-ssh Claude CLI version probe (keeps session start off the ssh round-trip). */
|
|
270
271
|
const REMOTE_CLI_VERSION_PROBE_DELAY_MS = 3000;
|
|
272
|
+
/** Whether absolute `path` is `root` or lies under it (a lexical compare). */
|
|
273
|
+
function isSameOrUnder(path, root) {
|
|
274
|
+
return path === root || path.startsWith(root === '/' ? '/' : `${root}/`);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Rewrite `path` from under `from` to the same place under `to`; a path not at or
|
|
278
|
+
* under `from` comes back unchanged. Exported for unit testing.
|
|
279
|
+
*/
|
|
280
|
+
export function rebasePath(path, from, to) {
|
|
281
|
+
if (path === from)
|
|
282
|
+
return to;
|
|
283
|
+
if (path.startsWith(`${from}/`))
|
|
284
|
+
return to + path.slice(from.length);
|
|
285
|
+
return path;
|
|
286
|
+
}
|
|
271
287
|
/**
|
|
272
288
|
* Ask tmux for the current window geometry of `muxName` so a re-attaching PTY
|
|
273
289
|
* client can spawn at the same size and avoid the resize-flicker / scrollback
|
|
@@ -297,10 +313,20 @@ export function queryTmuxWindowSize(muxName, socket) {
|
|
|
297
313
|
}
|
|
298
314
|
return { cols: DEFAULT_PTY_COLS, rows: DEFAULT_PTY_ROWS };
|
|
299
315
|
}
|
|
300
|
-
export function resolveMuxAttachCwd(workingDir, remote, docker) {
|
|
316
|
+
export function resolveMuxAttachCwd(workingDir, remote, docker, adopt) {
|
|
301
317
|
// Remote and docker sessions run the CLI elsewhere (ssh / docker exec); the LOCAL
|
|
302
318
|
// wrapper pane never needs the workspace as its cwd, so launch it in /tmp.
|
|
303
|
-
|
|
319
|
+
//
|
|
320
|
+
// ⚠️ An ADOPTED session needs the same treatment and does NOT get it from the
|
|
321
|
+
// two checks above: its connection facts live on `adopt.docker`/`adopt.remote`,
|
|
322
|
+
// so `session.docker` is undefined even for an in-container adoption. Its pane
|
|
323
|
+
// only attaches, so no folder on this host means anything to it. Measured against
|
|
324
|
+
// a real container, when `workingDir` still held the foreign pane's cwd: the local
|
|
325
|
+
// PTY spawn inherited `/workspace/pythonserver`, and the session came up with
|
|
326
|
+
// `pid: null` and a blank terminal while the wrapper tmux session and the
|
|
327
|
+
// in-container grouped view had both been created successfully, so it looked
|
|
328
|
+
// half-alive rather than failed.
|
|
329
|
+
return remote || docker || adopt ? '/tmp' : workingDir;
|
|
304
330
|
}
|
|
305
331
|
/**
|
|
306
332
|
* A write that a guarded submit's lease could not hold, because the per-lease hold
|
|
@@ -564,6 +590,23 @@ export class Session extends EventEmitter {
|
|
|
564
590
|
* rendered as "alive".
|
|
565
591
|
*/
|
|
566
592
|
_paneExit = null;
|
|
593
|
+
/**
|
|
594
|
+
* The pane's live directory, from the pane-exit watcher's read. Runtime-only:
|
|
595
|
+
* serialized by toState() but never restored by the constructor.
|
|
596
|
+
*/
|
|
597
|
+
_paneCwd;
|
|
598
|
+
/** See {@link liveWorktree}. */
|
|
599
|
+
_liveWorktree;
|
|
600
|
+
/**
|
|
601
|
+
* `workingDir` with symlinks resolved, so a pane reading (tmux reads the
|
|
602
|
+
* process cwd, always the resolved path) can be put back into `workingDir`'s
|
|
603
|
+
* spelling. `undefined` until the first resolution settles; `null` when there
|
|
604
|
+
* is nothing to translate (no symlink in the path) or it could not be resolved.
|
|
605
|
+
*/
|
|
606
|
+
_workspaceRealPath;
|
|
607
|
+
_workspaceRealPathPending = false;
|
|
608
|
+
/** When a resolution that met an unreachable folder may be tried again (0 = never). */
|
|
609
|
+
_workspaceRealPathRetryAt = 0;
|
|
567
610
|
/**
|
|
568
611
|
* How many starts, attaches or relaunches are running for this session's
|
|
569
612
|
* pane. While one is, a dead-pane reading may describe a pane that is being
|
|
@@ -651,6 +694,10 @@ export class Session extends EventEmitter {
|
|
|
651
694
|
// Docker execution metadata, present when this session runs inside a container via
|
|
652
695
|
// local tmux + `docker exec`. The container is per-CASE (shared by sibling sessions).
|
|
653
696
|
_docker;
|
|
697
|
+
// Adoption metadata, present when this session is a WRAPPER around a tmux
|
|
698
|
+
// session a human started outside Codeman. Its presence is the single switch
|
|
699
|
+
// that disables everything Codeman can only do to a process it launched.
|
|
700
|
+
_adopt;
|
|
654
701
|
// Owning username in multi-user mode (undefined in single-user). Stamped at create
|
|
655
702
|
// from req.authUser and round-tripped through recovery like _remote/_docker.
|
|
656
703
|
_owner;
|
|
@@ -826,6 +873,7 @@ export class Session extends EventEmitter {
|
|
|
826
873
|
this._tmuxHistoryLimit = config.tmuxHistoryLimit ?? DEFAULT_TMUX_HISTORY_LIMIT;
|
|
827
874
|
this._remote = config.remote;
|
|
828
875
|
this._docker = config.docker;
|
|
876
|
+
this._adopt = config.adopt;
|
|
829
877
|
this._owner = config.owner;
|
|
830
878
|
this._discoveredMuxSession = config.discoveredMuxSession === true;
|
|
831
879
|
// Restored so a record that says the agent exited survives a server restart
|
|
@@ -967,6 +1015,23 @@ export class Session extends EventEmitter {
|
|
|
967
1015
|
get remote() {
|
|
968
1016
|
return this._remote;
|
|
969
1017
|
}
|
|
1018
|
+
/** Adoption metadata when this session wraps a human-started tmux session. */
|
|
1019
|
+
get adopt() {
|
|
1020
|
+
return this._adopt;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Is this a wrapper around a tmux session Codeman did not start?
|
|
1024
|
+
*
|
|
1025
|
+
* ⚠️ Deliberately NOT folded into `isExternalCliMode()`. That predicate answers
|
|
1026
|
+
* "does this CLI render its own TUI", which is about the PROTOCOL. This one
|
|
1027
|
+
* answers "did we launch the process", which is about AUTHORITY: an adopted
|
|
1028
|
+
* session can be `mode: 'claude'` and still have no hooks installed, no
|
|
1029
|
+
* `envOverrides`, no effort and no `--session-id` we chose. Conflating the two
|
|
1030
|
+
* would silently grant claude-shaped automation over a process we do not own.
|
|
1031
|
+
*/
|
|
1032
|
+
get isAdopted() {
|
|
1033
|
+
return this._adopt !== undefined;
|
|
1034
|
+
}
|
|
970
1035
|
/**
|
|
971
1036
|
* `deepSeekConfig.statusReporting` verbatim: `undefined` when the caller sent
|
|
972
1037
|
* none (i.e. ON), `false` when the user disarmed the status bridge for this
|
|
@@ -993,6 +1058,26 @@ export class Session extends EventEmitter {
|
|
|
993
1058
|
const value = this._envOverrides?.DSH_HOME;
|
|
994
1059
|
return value && value.trim() ? value.trim() : undefined;
|
|
995
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* One env var from this session's overrides, for internal path resolution only
|
|
1063
|
+
* (custom-model injection linking an isolated config dir back to a path that must
|
|
1064
|
+
* survive lives in). Single-key lookup by design, like `deepSeekHomeOverride`: the
|
|
1065
|
+
* map can hold provider credentials and stays off the public `SessionState`.
|
|
1066
|
+
*/
|
|
1067
|
+
envOverrideValue(key) {
|
|
1068
|
+
const value = this._envOverrides?.[key];
|
|
1069
|
+
return value && value.trim() ? value.trim() : undefined;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* What the SESSION itself set for `key`, same single-key rule. `envOverrideValue()` answers
|
|
1073
|
+
* with whatever the map holds, which under a selection is the injected value: a re-apply
|
|
1074
|
+
* resolving the CLI's real home must read the session's own home, or none.
|
|
1075
|
+
*/
|
|
1076
|
+
ownEnvValue(key) {
|
|
1077
|
+
const selection = this._customModel;
|
|
1078
|
+
const value = selection?.envKeys.includes(key) ? selection.ownEnv?.[key] : this._envOverrides?.[key];
|
|
1079
|
+
return value?.trim() || undefined;
|
|
1080
|
+
}
|
|
996
1081
|
/** Owning username in multi-user mode, else undefined. */
|
|
997
1082
|
get owner() {
|
|
998
1083
|
return this._owner;
|
|
@@ -1057,13 +1142,20 @@ export class Session extends EventEmitter {
|
|
|
1057
1142
|
/**
|
|
1058
1143
|
* True when a tmux pane's death would mean THIS session's agent has exited.
|
|
1059
1144
|
*
|
|
1060
|
-
*
|
|
1145
|
+
* Five shapes fail the test, and each would otherwise publish a death that is
|
|
1061
1146
|
* not the agent's. A direct-PTY session owns no pane at all. A remote SSH
|
|
1062
1147
|
* session's local pane holds the ssh client, whose death means a transport
|
|
1063
1148
|
* drop OR an exit, which is the ambiguity PR #355 was about. A docker case's
|
|
1064
1149
|
* local pane holds a `docker exec` into the container's own tmux.
|
|
1065
1150
|
*
|
|
1066
|
-
*
|
|
1151
|
+
* ⚠️ An ADOPTED session is the same shape for a different reason: its pane runs
|
|
1152
|
+
* `tmux attach` to a session a human started, never the agent. That attach exiting
|
|
1153
|
+
* with 0 says the connection ended or its owner closed the foreign session; neither
|
|
1154
|
+
* means this session finished, and neither is Codeman's to clear the tab for. Without
|
|
1155
|
+
* this, the clean-exit sweep (`pane-exit-sweep.ts`) would close someone else's live
|
|
1156
|
+
* session's tab the moment they detached from it themselves.
|
|
1157
|
+
*
|
|
1158
|
+
* The fifth is a session rebuilt from the socket. Absent `remote`/`docker`/`adopt`
|
|
1067
1159
|
* normally means "this is local", but on a discovered record it only means
|
|
1068
1160
|
* "Codeman never found the metadata": the synthetic `restored-<fragment>` id
|
|
1069
1161
|
* matches no `state.json` entry, so a remote session rediscovered after
|
|
@@ -1073,7 +1165,7 @@ export class Session extends EventEmitter {
|
|
|
1073
1165
|
get paneExitApplies() {
|
|
1074
1166
|
if (this._discoveredMuxSession)
|
|
1075
1167
|
return false;
|
|
1076
|
-
return this._useMux && this._muxSession !== null && !this._remote && !this._docker;
|
|
1168
|
+
return this._useMux && this._muxSession !== null && !this._remote && !this._docker && !this._adopt;
|
|
1077
1169
|
}
|
|
1078
1170
|
/** What Codeman last observed of this pane's agent, or undefined for UNKNOWN. */
|
|
1079
1171
|
get paneExit() {
|
|
@@ -1144,6 +1236,117 @@ export class Session extends EventEmitter {
|
|
|
1144
1236
|
if (this._muxSession)
|
|
1145
1237
|
this._mux?.clearPaneExit?.(this._muxSession.muxName);
|
|
1146
1238
|
}
|
|
1239
|
+
/** The pane's live working directory, when the pane read has reported one. */
|
|
1240
|
+
get paneCwd() {
|
|
1241
|
+
return this._paneCwd;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Record the pane's live directory. Returns whether it CHANGED, so the caller
|
|
1245
|
+
* broadcasts only on a real transition.
|
|
1246
|
+
*
|
|
1247
|
+
* Scoped by {@link paneExitApplies}, the same rule as the exit reading: a
|
|
1248
|
+
* remote session's local pane runs the ssh client and a docker case's runs a
|
|
1249
|
+
* `docker exec`, so their pane directory is a host directory the agent was
|
|
1250
|
+
* never in, and a direct-PTY or rediscovered session has no pane Codeman can
|
|
1251
|
+
* vouch for. A relative value is refused rather than stored.
|
|
1252
|
+
*
|
|
1253
|
+
* tmux reads the pane's directory from the process cwd, which is always the
|
|
1254
|
+
* resolved path, while `workingDir` keeps the spelling the case was created
|
|
1255
|
+
* with. A workspace reached through a symlink would otherwise read as outside
|
|
1256
|
+
* itself, and the browser's preview routing (a lexical compare against
|
|
1257
|
+
* `workingDir`) would send every relative link to the attachment route. So a
|
|
1258
|
+
* reading at or under the workspace's resolved path is written back in
|
|
1259
|
+
* `workingDir`'s spelling. Until that resolution has settled, only a reading
|
|
1260
|
+
* lexically inside `workingDir` is accepted.
|
|
1261
|
+
*/
|
|
1262
|
+
setPaneCwd(next) {
|
|
1263
|
+
let resolved = this.paneExitApplies && next?.startsWith('/') ? next : undefined;
|
|
1264
|
+
if (resolved !== undefined) {
|
|
1265
|
+
if (this._workspaceRealPath === undefined ||
|
|
1266
|
+
(this._workspaceRealPathRetryAt !== 0 && Date.now() >= this._workspaceRealPathRetryAt)) {
|
|
1267
|
+
this._resolveWorkspaceRealPath();
|
|
1268
|
+
}
|
|
1269
|
+
if (this._workspaceRealPath === undefined) {
|
|
1270
|
+
// The workspace's resolved path is not known yet. A reading outside
|
|
1271
|
+
// `workingDir` could be the workspace itself seen through its symlink, so
|
|
1272
|
+
// it waits for the next tick rather than mis-route links for one.
|
|
1273
|
+
if (!isSameOrUnder(resolved, this._workspaceSpelling()))
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
else if (this._workspaceRealPath !== null) {
|
|
1277
|
+
resolved = rebasePath(resolved, this._workspaceRealPath, this._workspaceSpelling());
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (resolved === this._paneCwd)
|
|
1281
|
+
return false;
|
|
1282
|
+
this._paneCwd = resolved;
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* The linked git worktree inside `workingDir` that {@link paneCwd} is in, when there is one
|
|
1287
|
+
* (`live-worktree.ts`). The Files panel lists it instead of the main checkout. Runtime-only and
|
|
1288
|
+
* never a confinement root, like `paneCwd`: it is judged lexically when the pane moves, and the
|
|
1289
|
+
* listing route re-checks the real folder on every request.
|
|
1290
|
+
*/
|
|
1291
|
+
get liveWorktree() {
|
|
1292
|
+
return this._liveWorktree;
|
|
1293
|
+
}
|
|
1294
|
+
/** Record the result of a worktree lookup. Returns whether it CHANGED, so the caller broadcasts only then. */
|
|
1295
|
+
setLiveWorktree(next) {
|
|
1296
|
+
if (next?.path === this._liveWorktree?.path)
|
|
1297
|
+
return false;
|
|
1298
|
+
this._liveWorktree = next;
|
|
1299
|
+
return true;
|
|
1300
|
+
}
|
|
1301
|
+
/** `workingDir` without a trailing separator (the root stays `/`). */
|
|
1302
|
+
_workspaceSpelling() {
|
|
1303
|
+
return this.workingDir.length > 1 ? this.workingDir.replace(/\/+$/, '') : this.workingDir;
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Resolve `workingDir`'s symlinks in the background for {@link setPaneCwd}.
|
|
1307
|
+
*
|
|
1308
|
+
* Bounded: the folder can sit on a network mount that stopped answering, so it
|
|
1309
|
+
* is probed with `probePathKind()` first and the `realpath()` is raced against
|
|
1310
|
+
* the same timeout. An `unknown` probe, a failed or slow `realpath()` leave no
|
|
1311
|
+
* translation and allow one more try after `WORKSPACE_REALPATH_RETRY_MS`; a
|
|
1312
|
+
* folder that is absent or not a directory is settled for good.
|
|
1313
|
+
*/
|
|
1314
|
+
_resolveWorkspaceRealPath() {
|
|
1315
|
+
if (this._workspaceRealPathPending)
|
|
1316
|
+
return;
|
|
1317
|
+
this._workspaceRealPathPending = true;
|
|
1318
|
+
const spelling = this._workspaceSpelling();
|
|
1319
|
+
void (async () => {
|
|
1320
|
+
let real = null;
|
|
1321
|
+
let retry = false;
|
|
1322
|
+
try {
|
|
1323
|
+
const kind = await probePathKind(spelling);
|
|
1324
|
+
if (kind === 'unknown') {
|
|
1325
|
+
retry = true;
|
|
1326
|
+
}
|
|
1327
|
+
else if (kind === 'directory') {
|
|
1328
|
+
let timer;
|
|
1329
|
+
const answer = await Promise.race([
|
|
1330
|
+
realpath(spelling).catch(() => undefined),
|
|
1331
|
+
new Promise((resolveTimeout) => {
|
|
1332
|
+
timer = setTimeout(() => resolveTimeout(undefined), PATH_PROBE_TIMEOUT_MS);
|
|
1333
|
+
timer.unref?.();
|
|
1334
|
+
}),
|
|
1335
|
+
]).finally(() => clearTimeout(timer));
|
|
1336
|
+
if (answer === undefined)
|
|
1337
|
+
retry = true;
|
|
1338
|
+
else if (answer !== spelling && answer !== '/')
|
|
1339
|
+
real = answer;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
catch {
|
|
1343
|
+
retry = true;
|
|
1344
|
+
}
|
|
1345
|
+
this._workspaceRealPath = real;
|
|
1346
|
+
this._workspaceRealPathRetryAt = retry ? Date.now() + WORKSPACE_REALPATH_RETRY_MS : 0;
|
|
1347
|
+
this._workspaceRealPathPending = false;
|
|
1348
|
+
})();
|
|
1349
|
+
}
|
|
1147
1350
|
/**
|
|
1148
1351
|
* Record what the mux layer observed of this pane's agent, and say whether
|
|
1149
1352
|
* that changed the answer. The caller persists and broadcasts on a true.
|
|
@@ -1468,20 +1671,38 @@ export class Session extends EventEmitter {
|
|
|
1468
1671
|
}
|
|
1469
1672
|
/**
|
|
1470
1673
|
* The full selection incl. bookkeeping, for state.json ONLY (`__customModel`, the
|
|
1471
|
-
* same disk-only convention as `getEnvOverridesForPersist()`). Carries no env
|
|
1472
|
-
* so nothing secret lands on disk; recovery re-derives them from the endpoint store.
|
|
1674
|
+
* same disk-only convention as `getEnvOverridesForPersist()`). Carries no injected env
|
|
1675
|
+
* values, so nothing secret lands on disk; recovery re-derives them from the endpoint store.
|
|
1473
1676
|
* Without this a Codeman restart left the pane on the custom endpoint (tmux keeps
|
|
1474
1677
|
* its `setenv`s) while `customModel` came back undefined, so the state was wrong and
|
|
1475
1678
|
* clearing had nothing to unset. Must NOT be included in any API-bound serializer.
|
|
1679
|
+
*
|
|
1680
|
+
* The one value it does carry is the session's own home directory (`ownEnv`, narrowed to
|
|
1681
|
+
* `homeEnvVar`), a path like `configDir` beside it: the selection overwrote the tmux
|
|
1682
|
+
* `setenv` that held it, so without this a clear after a restart would lose it. Any other
|
|
1683
|
+
* entry of `ownEnv` may be a credential and stays in memory, and so does a `homeEnvVar`
|
|
1684
|
+
* value that is not an absolute path: the registry types that name as any env var, so a
|
|
1685
|
+
* `clis.json` override can point it at a credential.
|
|
1476
1686
|
*/
|
|
1477
1687
|
getCustomModelForPersist() {
|
|
1478
|
-
|
|
1688
|
+
if (!this._customModel)
|
|
1689
|
+
return undefined;
|
|
1690
|
+
const { ownEnv, ...selection } = this._customModel;
|
|
1691
|
+
const { homeEnvVar } = selection;
|
|
1692
|
+
const ownHome = homeEnvVar ? ownEnv?.[homeEnvVar] : undefined;
|
|
1693
|
+
return {
|
|
1694
|
+
...selection,
|
|
1695
|
+
envKeys: [...selection.envKeys],
|
|
1696
|
+
...(homeEnvVar && ownHome && isAbsolute(ownHome) ? { ownEnv: { [homeEnvVar]: ownHome } } : {}),
|
|
1697
|
+
};
|
|
1479
1698
|
}
|
|
1480
1699
|
/**
|
|
1481
1700
|
* Update this session's custom-model selection and merge the endpoint's injected env
|
|
1482
1701
|
* vars into `_envOverrides` — first UNDOING whatever the previous selection injected
|
|
1483
|
-
* (removing exactly those env keys
|
|
1484
|
-
*
|
|
1702
|
+
* (removing exactly those env keys, and putting back a value the session had set under
|
|
1703
|
+
* one itself), so switching endpoints, or clearing back to the harness's native cloud
|
|
1704
|
+
* default, never leaves a stale key behind and never loses the session's own `DSH_HOME`
|
|
1705
|
+
* or `CLAUDE_CONFIG_DIR`. Synchronous and
|
|
1485
1706
|
* side-effect-free beyond mutating state, matching `setNice`/`setColor` above — this
|
|
1486
1707
|
* class does no file IO, so it reports the PREVIOUS `configDir` (if any) for the
|
|
1487
1708
|
* caller to clean up on disk (custom-model-injection.ts's configDir kind).
|
|
@@ -1491,20 +1712,44 @@ export class Session extends EventEmitter {
|
|
|
1491
1712
|
* `_buildRespawnPaneOptions().unsetEnvKeys`): the tmux session inherits every
|
|
1492
1713
|
* `setenv` into `respawn-pane`, so dropping them from the map alone would relaunch
|
|
1493
1714
|
* the CLI still pointed at the old endpoint — and for the `configDir` kinds, at a
|
|
1494
|
-
* `HOME`/`CODEX_HOME`/`GROK_HOME` the caller has just deleted.
|
|
1715
|
+
* `HOME`/`CODEX_HOME`/`GROK_HOME` the caller has just deleted. A key put back stays out
|
|
1716
|
+
* of that queue: `applyEnvOverrides()` writes its value over the injected one.
|
|
1717
|
+
*
|
|
1718
|
+
* `next.ownEnv` is for recovery, which states the session's own values itself: after a
|
|
1719
|
+
* restart `_envOverrides` holds what the previous run injected (`CLAUDE_CONFIG_DIR` is
|
|
1720
|
+
* persisted), and reading those as the session's own would pin it to a deleted directory.
|
|
1495
1721
|
*/
|
|
1496
1722
|
setCustomModel(next, envOverrides) {
|
|
1497
1723
|
const previousConfigDir = this._customModel?.configDir;
|
|
1498
1724
|
const removedEnvKeys = [];
|
|
1499
1725
|
if (this._customModel) {
|
|
1726
|
+
const previousOwnEnv = this._customModel.ownEnv ?? {};
|
|
1500
1727
|
for (const key of this._customModel.envKeys) {
|
|
1501
1728
|
if (this._envOverrides)
|
|
1502
1729
|
delete this._envOverrides[key];
|
|
1503
1730
|
removedEnvKeys.push(key);
|
|
1504
|
-
|
|
1731
|
+
const own = previousOwnEnv[key];
|
|
1732
|
+
if (own) {
|
|
1733
|
+
this._envOverrides = { ...(this._envOverrides ?? {}), [key]: own };
|
|
1734
|
+
this._pendingEnvUnsets.delete(key);
|
|
1735
|
+
}
|
|
1736
|
+
else {
|
|
1737
|
+
this._pendingEnvUnsets.add(key);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
// Read after the undo, so a switch between endpoints carries the own values across. An
|
|
1742
|
+
// empty value is skipped: `applyEnvOverrides()` never set one, so it has nothing to
|
|
1743
|
+
// restore, and the injected key still has to be unset.
|
|
1744
|
+
const ownEnv = { ...next?.ownEnv };
|
|
1745
|
+
if (!next?.ownEnv) {
|
|
1746
|
+
for (const key of Object.keys(envOverrides ?? {})) {
|
|
1747
|
+
const own = this._envOverrides?.[key];
|
|
1748
|
+
if (own)
|
|
1749
|
+
ownEnv[key] = own;
|
|
1505
1750
|
}
|
|
1506
1751
|
}
|
|
1507
|
-
this._customModel = next ? { ...next, envKeys: [...next.envKeys] } : undefined;
|
|
1752
|
+
this._customModel = next ? { ...next, envKeys: [...next.envKeys], ownEnv } : undefined;
|
|
1508
1753
|
if (envOverrides && Object.keys(envOverrides).length > 0) {
|
|
1509
1754
|
this._envOverrides = { ...(this._envOverrides ?? {}), ...envOverrides };
|
|
1510
1755
|
// A key the new selection sets again does not need an unset (applyEnvOverrides
|
|
@@ -1731,8 +1976,11 @@ export class Session extends EventEmitter {
|
|
|
1731
1976
|
pid: this.pid,
|
|
1732
1977
|
status: this._status,
|
|
1733
1978
|
workingDir: this.workingDir,
|
|
1979
|
+
paneCwd: this._paneCwd,
|
|
1980
|
+
liveWorktree: this._liveWorktree,
|
|
1734
1981
|
remote: this._remote,
|
|
1735
1982
|
docker: this._docker,
|
|
1983
|
+
adopt: this._adopt,
|
|
1736
1984
|
owner: this._owner,
|
|
1737
1985
|
parentSessionId: this._parentSessionId,
|
|
1738
1986
|
currentTaskId: this._currentTaskId,
|
|
@@ -2011,7 +2259,7 @@ export class Session extends EventEmitter {
|
|
|
2011
2259
|
name: 'xterm-256color',
|
|
2012
2260
|
cols: ptyCols,
|
|
2013
2261
|
rows: ptyRows,
|
|
2014
|
-
cwd: resolveMuxAttachCwd(this.workingDir, this._remote, this._docker),
|
|
2262
|
+
cwd: resolveMuxAttachCwd(this.workingDir, this._remote, this._docker, this._adopt),
|
|
2015
2263
|
// COD-75: a CLI that declares `export COLORTERM=truecolor` gets it on the ATTACH
|
|
2016
2264
|
// client too. Both sides read the same registry entry, which is what stops the
|
|
2017
2265
|
// attach client and the tmux session from disagreeing — they used to be two
|
|
@@ -2158,6 +2406,7 @@ export class Session extends EventEmitter {
|
|
|
2158
2406
|
historyLimit: this._tmuxHistoryLimit,
|
|
2159
2407
|
remote: this._remote,
|
|
2160
2408
|
docker: this._docker,
|
|
2409
|
+
adopt: this._adopt,
|
|
2161
2410
|
owner: this._owner,
|
|
2162
2411
|
};
|
|
2163
2412
|
return this._withCustomModelLaunchModel(options);
|
|
@@ -2240,7 +2489,7 @@ export class Session extends EventEmitter {
|
|
|
2240
2489
|
if (entry?.launch.chain !== 'fallback')
|
|
2241
2490
|
return options;
|
|
2242
2491
|
const resumeIdPattern = entry.launch.params?.resumeId;
|
|
2243
|
-
const configDir = this.
|
|
2492
|
+
const configDir = this.claudeConfigDir();
|
|
2244
2493
|
const chainTail = this._claudeSessionChain[this._claudeSessionChain.length - 1];
|
|
2245
2494
|
const candidates = [chainTail, options.resumeSessionId, this.id].filter((v) => !!v);
|
|
2246
2495
|
for (const candidate of candidates) {
|
|
@@ -2262,8 +2511,11 @@ export class Session extends EventEmitter {
|
|
|
2262
2511
|
// unpinned options already carry is the correct command.
|
|
2263
2512
|
return options;
|
|
2264
2513
|
}
|
|
2265
|
-
/**
|
|
2266
|
-
|
|
2514
|
+
/**
|
|
2515
|
+
* The session's Claude config dir when it has been relocated (#255), else undefined.
|
|
2516
|
+
* Public for the edit history reader, which looks for the transcript there.
|
|
2517
|
+
*/
|
|
2518
|
+
claudeConfigDir() {
|
|
2267
2519
|
// Trimmed like `claudeProjectsDir()` trims the process-wide override: the
|
|
2268
2520
|
// envOverrides schema validates keys only, and a whitespace-only value would
|
|
2269
2521
|
// otherwise resolve to a relative path and read "no transcript" for everything.
|
|
@@ -2271,7 +2523,8 @@ export class Session extends EventEmitter {
|
|
|
2271
2523
|
}
|
|
2272
2524
|
/**
|
|
2273
2525
|
* Force the custom-model selection's `launchModel` (pi/omp `custom/<id>`, grok's
|
|
2274
|
-
* `[model.<name>]` block name) onto the CLI's `model`
|
|
2526
|
+
* `[model.<name>]` block name, codex's bare selected id) onto the CLI's `model`
|
|
2527
|
+
* launch param. Where that param
|
|
2275
2528
|
* lives is registry DATA — the entry's `legacyConfigField` (`piConfig`, `grokConfig`,
|
|
2276
2529
|
* ...) or the top-level `model` for an entry that declares none — so this stays a
|
|
2277
2530
|
* generic reader rather than a branch per CLI. Applied on the OPTIONS only: the stored
|
|
@@ -2411,7 +2664,7 @@ export class Session extends EventEmitter {
|
|
|
2411
2664
|
// redraws overwrite only the cells they target, so non-erased rows keep
|
|
2412
2665
|
// their content. Gated to Codex/Claude/Gemini (isAltScreenStripMode).
|
|
2413
2666
|
//
|
|
2414
|
-
// Every OTHER mode (shell/antigravity/
|
|
2667
|
+
// Every OTHER mode (shell/antigravity/grok/deepseek/omp) gets the NARROW strip
|
|
2415
2668
|
// when it is tmux-backed: alt-screen toggles only, because the sequence that breaks
|
|
2416
2669
|
// scrollback there is tmux's own client-side smcup at attach, not anything the
|
|
2417
2670
|
// program in the pane emitted (issue #205, see isMuxAltScreenOnlyStripMode).
|
|
@@ -2490,6 +2743,25 @@ export class Session extends EventEmitter {
|
|
|
2490
2743
|
this.emit('terminal', data);
|
|
2491
2744
|
this.emit('output', data);
|
|
2492
2745
|
}
|
|
2746
|
+
/**
|
|
2747
|
+
* End a start that cannot reach an adopted session's wrapper.
|
|
2748
|
+
*
|
|
2749
|
+
* ⚠️ Never the direct-PTY fallback. What that starts is a NEW process on THIS host
|
|
2750
|
+
* (`claude --dangerously-skip-permissions`, or a server-account shell) in the neutral
|
|
2751
|
+
* `/tmp`, under a tab still carrying the foreign session's name, so someone who opened an
|
|
2752
|
+
* ssh or container session would end up typing into this machine. `_useMux` stays on, so
|
|
2753
|
+
* the next start, which is what selecting the tab sends, tries the wrapper again.
|
|
2754
|
+
*
|
|
2755
|
+
* The `error` event is what the tab shows: the browser's re-attach does not read the
|
|
2756
|
+
* route's answer.
|
|
2757
|
+
*/
|
|
2758
|
+
_failAdoptedStart(err) {
|
|
2759
|
+
const message = `Could not attach to ${this._adopt?.targetSession}: ${getErrorMessage(err)}`;
|
|
2760
|
+
console.error(`[Session] ${message}`);
|
|
2761
|
+
this._status = 'stopped';
|
|
2762
|
+
this.emit('error', message);
|
|
2763
|
+
throw new Error(message);
|
|
2764
|
+
}
|
|
2493
2765
|
async startInteractive() {
|
|
2494
2766
|
if (this.ptyProcess) {
|
|
2495
2767
|
throw new Error('Session already has a running process');
|
|
@@ -2525,8 +2797,10 @@ export class Session extends EventEmitter {
|
|
|
2525
2797
|
// wheel-forwarding to Claude's own transcript (the only route to history in
|
|
2526
2798
|
// repaint/alt-screen mode; issue #154). Remote sessions run claude on
|
|
2527
2799
|
// another host, so a local probe wouldn't reflect their version; they get
|
|
2528
|
-
// their own over-ssh probe below. Cached process-wide, best-effort.
|
|
2529
|
-
|
|
2800
|
+
// their own over-ssh probe below. Cached process-wide, best-effort. An adopted
|
|
2801
|
+
// session's agent is a process someone else started, possibly elsewhere, so
|
|
2802
|
+
// the local binary says nothing about it either.
|
|
2803
|
+
if (cliNeedsVersionProbe(this.mode) && !this._remote && !this._docker && !this._adopt && !this._cliVersion) {
|
|
2530
2804
|
const probedVersion = resolveSessionCliVersion(this.mode);
|
|
2531
2805
|
if (probedVersion) {
|
|
2532
2806
|
this._cliVersion = probedVersion;
|
|
@@ -2629,6 +2903,7 @@ export class Session extends EventEmitter {
|
|
|
2629
2903
|
historyLimit: this._tmuxHistoryLimit,
|
|
2630
2904
|
remote: this._remote,
|
|
2631
2905
|
docker: this._docker,
|
|
2906
|
+
adopt: this._adopt,
|
|
2632
2907
|
owner: this._owner,
|
|
2633
2908
|
},
|
|
2634
2909
|
spawnErrLabel: 'mux attachment',
|
|
@@ -2714,6 +2989,8 @@ export class Session extends EventEmitter {
|
|
|
2714
2989
|
}
|
|
2715
2990
|
}
|
|
2716
2991
|
catch (err) {
|
|
2992
|
+
if (this._adopt)
|
|
2993
|
+
this._failAdoptedStart(err);
|
|
2717
2994
|
console.error('[Session] Failed to create mux session, falling back to direct PTY:', err);
|
|
2718
2995
|
this._useMux = false;
|
|
2719
2996
|
this._muxSession = null;
|
|
@@ -2732,6 +3009,8 @@ export class Session extends EventEmitter {
|
|
|
2732
3009
|
if (getCli(this.mode)?.capabilities.requiresMux) {
|
|
2733
3010
|
throw new Error(`${getModeLabel(this.mode)} sessions require tmux. Direct PTY fallback is not supported.`);
|
|
2734
3011
|
}
|
|
3012
|
+
if (this._adopt)
|
|
3013
|
+
this._failAdoptedStart(new Error('tmux is not available'));
|
|
2735
3014
|
try {
|
|
2736
3015
|
// Pass --session-id to use the SAME ID as the Codeman session
|
|
2737
3016
|
// This ensures subagents can be directly matched to the correct tab.
|
|
@@ -2918,6 +3197,14 @@ export class Session extends EventEmitter {
|
|
|
2918
3197
|
// dialog, so without this a deleted session kept "answering" it for several seconds.
|
|
2919
3198
|
if (this._trustDialogAccepted || this._isStopped)
|
|
2920
3199
|
return;
|
|
3200
|
+
// ⚠️ Never answer a dialog in someone else's pane. An adopted session wraps a
|
|
3201
|
+
// claude the human started, and this scan reads the pane and SENDS KEYS for 90
|
|
3202
|
+
// seconds after `startInteractive()`, which boot recovery re-runs for every
|
|
3203
|
+
// wrapper, so it is not a one-time window at adoption.
|
|
3204
|
+
if (this._adopt) {
|
|
3205
|
+
this._trustDialogAccepted = true;
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
2921
3208
|
const now = Date.now();
|
|
2922
3209
|
if (now - this._interactiveStartedAt > TRUST_DIALOG_WINDOW_MS) {
|
|
2923
3210
|
this._trustDialogAccepted = true; // window closed; anything matching now is not the dialog
|
|
@@ -3651,6 +3938,7 @@ export class Session extends EventEmitter {
|
|
|
3651
3938
|
historyLimit: this._tmuxHistoryLimit,
|
|
3652
3939
|
remote: this._remote,
|
|
3653
3940
|
docker: this._docker,
|
|
3941
|
+
adopt: this._adopt,
|
|
3654
3942
|
owner: this._owner,
|
|
3655
3943
|
},
|
|
3656
3944
|
createSessionOptions: {
|
|
@@ -3663,13 +3951,17 @@ export class Session extends EventEmitter {
|
|
|
3663
3951
|
historyLimit: this._tmuxHistoryLimit,
|
|
3664
3952
|
remote: this._remote,
|
|
3665
3953
|
docker: this._docker,
|
|
3954
|
+
adopt: this._adopt,
|
|
3666
3955
|
owner: this._owner,
|
|
3667
3956
|
},
|
|
3668
3957
|
spawnErrLabel: 'shell mux attachment',
|
|
3669
3958
|
});
|
|
3670
3959
|
// For NEW sessions: clear by sending 'clear' command to the shell
|
|
3671
3960
|
// For RESTORED sessions: don't clear - we want to see the existing output
|
|
3672
|
-
|
|
3961
|
+
// ⚠️ Never for an ADOPTED one: this pane is someone else's, so the write would land
|
|
3962
|
+
// on their half-typed command and submit it (measured), or in whatever is reading
|
|
3963
|
+
// stdin. `shell` is also the mode of anything the classifier cannot name.
|
|
3964
|
+
if (!isRestored && !this._adopt) {
|
|
3673
3965
|
setTimeout(() => {
|
|
3674
3966
|
if (this.ptyProcess) {
|
|
3675
3967
|
this._terminalBuffer.clear();
|
|
@@ -3679,6 +3971,8 @@ export class Session extends EventEmitter {
|
|
|
3679
3971
|
}
|
|
3680
3972
|
}
|
|
3681
3973
|
catch (err) {
|
|
3974
|
+
if (this._adopt)
|
|
3975
|
+
this._failAdoptedStart(err);
|
|
3682
3976
|
console.error('[Session] Failed to create mux session, falling back to direct PTY:', err);
|
|
3683
3977
|
this._useMux = false;
|
|
3684
3978
|
this._muxSession = null;
|
|
@@ -3686,6 +3980,8 @@ export class Session extends EventEmitter {
|
|
|
3686
3980
|
}
|
|
3687
3981
|
// Fallback to direct PTY if mux is not used
|
|
3688
3982
|
if (!this.ptyProcess) {
|
|
3983
|
+
if (this._adopt)
|
|
3984
|
+
this._failAdoptedStart(new Error('tmux is not available'));
|
|
3689
3985
|
try {
|
|
3690
3986
|
this.ptyProcess = spawnPtyWithHelperRepair(() => pty.spawn(shell, [], {
|
|
3691
3987
|
name: 'xterm-256color',
|
|
@@ -3763,6 +4059,9 @@ export class Session extends EventEmitter {
|
|
|
3763
4059
|
* ```
|
|
3764
4060
|
*/
|
|
3765
4061
|
async runPrompt(prompt, options) {
|
|
4062
|
+
// ⚠️ A one-shot `claude -p` is a new local process in `/tmp`, never the agent in that pane.
|
|
4063
|
+
if (this._adopt)
|
|
4064
|
+
throw new Error('One-shot prompts are not available for adopted sessions');
|
|
3766
4065
|
if (this.guardedInputHeld) {
|
|
3767
4066
|
this._guardedInputRevision++;
|
|
3768
4067
|
throw new Error('Guarded submission in progress');
|
|
@@ -4345,6 +4644,7 @@ export class Session extends EventEmitter {
|
|
|
4345
4644
|
(!allowLease && this.guardedInputHeld) ||
|
|
4346
4645
|
this.remote ||
|
|
4347
4646
|
this.docker ||
|
|
4647
|
+
this._adopt ||
|
|
4348
4648
|
this.paneExit ||
|
|
4349
4649
|
!this._muxSession)
|
|
4350
4650
|
return null;
|