aicodeman 1.41.0 → 1.42.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 +101 -21
- package/dist/attachment-registry.d.ts +2 -0
- package/dist/attachment-registry.d.ts.map +1 -1
- package/dist/attachment-registry.js +14 -3
- package/dist/attachment-registry.js.map +1 -1
- package/dist/cli-agent.d.ts +2 -0
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +13 -9
- package/dist/cli-agent.js.map +1 -1
- package/dist/cli.d.ts +25 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +97 -11
- package/dist/cli.js.map +1 -1
- package/dist/config/buffer-limits.d.ts +9 -0
- package/dist/config/buffer-limits.d.ts.map +1 -1
- package/dist/config/buffer-limits.js +9 -0
- package/dist/config/buffer-limits.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +10 -0
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +26 -0
- 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 +89 -6
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +61 -0
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/folder-listing.d.ts +94 -0
- package/dist/folder-listing.d.ts.map +1 -0
- package/dist/folder-listing.js +241 -0
- package/dist/folder-listing.js.map +1 -0
- package/dist/git-worktrees.d.ts +292 -0
- package/dist/git-worktrees.d.ts.map +1 -0
- package/dist/git-worktrees.js +791 -0
- package/dist/git-worktrees.js.map +1 -0
- package/dist/hooks-config.d.ts +36 -16
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +45 -20
- package/dist/hooks-config.js.map +1 -1
- package/dist/html-view-capabilities.d.ts +54 -0
- package/dist/html-view-capabilities.d.ts.map +1 -0
- package/dist/html-view-capabilities.js +103 -0
- package/dist/html-view-capabilities.js.map +1 -0
- package/dist/mcp-server.d.ts +114 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +918 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp-sync.d.ts +5 -0
- package/dist/mcp-sync.d.ts.map +1 -1
- package/dist/mcp-sync.js +51 -5
- package/dist/mcp-sync.js.map +1 -1
- package/dist/mux-interface.d.ts +36 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/remote-files.d.ts +37 -0
- package/dist/remote-files.d.ts.map +1 -1
- package/dist/remote-files.js +103 -0
- package/dist/remote-files.js.map +1 -1
- package/dist/remote-pane-history.d.ts +103 -0
- package/dist/remote-pane-history.d.ts.map +1 -0
- package/dist/remote-pane-history.js +258 -0
- package/dist/remote-pane-history.js.map +1 -0
- package/dist/remote-ssh-limiter.d.ts +16 -3
- package/dist/remote-ssh-limiter.d.ts.map +1 -1
- package/dist/remote-ssh-limiter.js +37 -6
- package/dist/remote-ssh-limiter.js.map +1 -1
- package/dist/services/unified-session-service.d.ts +8 -0
- package/dist/services/unified-session-service.d.ts.map +1 -1
- package/dist/services/unified-session-service.js +21 -0
- package/dist/services/unified-session-service.js.map +1 -1
- package/dist/session-activity.d.ts +51 -0
- package/dist/session-activity.d.ts.map +1 -1
- package/dist/session-activity.js +62 -0
- package/dist/session-activity.js.map +1 -1
- package/dist/session-submit-verifier.js +1 -1
- package/dist/session.d.ts +90 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +247 -9
- package/dist/session.js.map +1 -1
- package/dist/tab-layout-legacy-order.d.ts.map +1 -1
- package/dist/tab-layout-legacy-order.js +72 -13
- package/dist/tab-layout-legacy-order.js.map +1 -1
- package/dist/tab-layout-service.d.ts +33 -2
- package/dist/tab-layout-service.d.ts.map +1 -1
- package/dist/tab-layout-service.js +102 -7
- package/dist/tab-layout-service.js.map +1 -1
- package/dist/tab-layout.d.ts +9 -0
- package/dist/tab-layout.d.ts.map +1 -1
- package/dist/tab-layout.js +15 -2
- package/dist/tab-layout.js.map +1 -1
- package/dist/tmux-manager.d.ts +130 -7
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +263 -36
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/session.d.ts +53 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/usage-telemetry.d.ts +44 -7
- package/dist/usage-telemetry.d.ts.map +1 -1
- package/dist/usage-telemetry.js +72 -7
- package/dist/usage-telemetry.js.map +1 -1
- package/dist/utils/deepseek-cli-resolver.d.ts.map +1 -1
- package/dist/utils/deepseek-cli-resolver.js +1 -1
- package/dist/utils/deepseek-cli-resolver.js.map +1 -1
- package/dist/web/approval-inbox.d.ts +24 -2
- package/dist/web/approval-inbox.d.ts.map +1 -1
- package/dist/web/approval-inbox.js +8 -2
- package/dist/web/approval-inbox.js.map +1 -1
- package/dist/web/middleware/auth.d.ts.map +1 -1
- package/dist/web/middleware/auth.js +33 -0
- package/dist/web/middleware/auth.js.map +1 -1
- package/dist/web/ports/session-port.d.ts +8 -1
- package/dist/web/ports/session-port.d.ts.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.f04bb2a5.js +43 -0
- package/dist/web/public/app.f04bb2a5.js.br +0 -0
- package/dist/web/public/app.f04bb2a5.js.gz +0 -0
- package/dist/web/public/approvals-ui.js +4 -0
- package/dist/web/public/approvals-ui.js.br +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.bc9c1f63.js → constants.12b0c247.js} +395 -7
- package/dist/web/public/constants.12b0c247.js.br +0 -0
- package/dist/web/public/constants.12b0c247.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/gesture/gesture-codeman.js +112 -6
- package/dist/web/public/git-status-ui.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.06659676.js +2 -0
- package/dist/web/public/i18n.06659676.js.br +0 -0
- package/dist/web/public/i18n.06659676.js.gz +0 -0
- package/dist/web/public/image-input.fcdcb46c.js +462 -0
- package/dist/web/public/image-input.fcdcb46c.js.br +0 -0
- package/dist/web/public/image-input.fcdcb46c.js.gz +0 -0
- package/dist/web/public/index.html +169 -24
- 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.c1e7c7b7.js → keyboard-accessory.b42f539b.js} +124 -44
- package/dist/web/public/keyboard-accessory.b42f539b.js.br +0 -0
- package/dist/web/public/keyboard-accessory.b42f539b.js.gz +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js +11 -2
- 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.2dff1580.css +1 -0
- package/dist/web/public/mobile.2dff1580.css.br +0 -0
- package/dist/web/public/mobile.2dff1580.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.71846fcb.js → panels-ui.73ec0137.js} +87 -87
- package/dist/web/public/panels-ui.73ec0137.js.br +0 -0
- package/dist/web/public/panels-ui.73ec0137.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.ff0dae4c.js → respawn-ui.d7adcf6a.js} +1 -1
- 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/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/session-ui.d52ff6aa.js +100 -0
- package/dist/web/public/session-ui.d52ff6aa.js.br +0 -0
- package/dist/web/public/session-ui.d52ff6aa.js.gz +0 -0
- package/dist/web/public/settings-ui.3a22bd2a.js +97 -0
- package/dist/web/public/settings-ui.3a22bd2a.js.br +0 -0
- package/dist/web/public/settings-ui.3a22bd2a.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js +11 -0
- package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/{spreadsheet-preview.af82fa2f.js → spreadsheet-preview.75c751de.js} +1 -1
- package/dist/web/public/spreadsheet-preview.75c751de.js.br +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js +110 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.ba6a9b84.css +1 -0
- package/dist/web/public/styles.ba6a9b84.css.br +0 -0
- package/dist/web/public/styles.ba6a9b84.css.gz +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +6 -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 +1 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.br +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/{tab-rail-resize.a5867793.js → tab-rail-resize.7157e0bd.js} +1 -1
- package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +45 -24
- 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.0a7ccc29.js +2 -0
- package/dist/web/public/terminal-ui.0a7ccc29.js.br +0 -0
- package/dist/web/public/terminal-ui.0a7ccc29.js.gz +0 -0
- package/dist/web/public/tile-grid.js +439 -18
- package/dist/web/public/tile-grid.js.br +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js +12 -0
- package/dist/web/public/webview-tabs.js.br +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/public/whats-new.json +13 -0
- package/dist/web/route-helpers.d.ts +8 -0
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +8 -0
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/admin-routes.d.ts.map +1 -1
- package/dist/web/routes/admin-routes.js +4 -1
- package/dist/web/routes/admin-routes.js.map +1 -1
- package/dist/web/routes/approval-routes.d.ts.map +1 -1
- package/dist/web/routes/approval-routes.js +12 -10
- package/dist/web/routes/approval-routes.js.map +1 -1
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +10 -3
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/file-routes.d.ts +46 -1
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +214 -27
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
- package/dist/web/routes/hook-event-routes.js +8 -1
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/html-view-routes.d.ts +60 -0
- package/dist/web/routes/html-view-routes.d.ts.map +1 -0
- package/dist/web/routes/html-view-routes.js +288 -0
- package/dist/web/routes/html-view-routes.js.map +1 -0
- package/dist/web/routes/index.d.ts +2 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +2 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +19 -14
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/reboot-restore-routes.js +2 -2
- package/dist/web/routes/reboot-restore-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +8 -5
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +462 -109
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
- package/dist/web/routes/status-telemetry-routes.js +24 -6
- package/dist/web/routes/status-telemetry-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts +9 -0
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +91 -12
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/routes/worktree-routes.d.ts +52 -0
- package/dist/web/routes/worktree-routes.d.ts.map +1 -0
- package/dist/web/routes/worktree-routes.js +220 -0
- package/dist/web/routes/worktree-routes.js.map +1 -0
- package/dist/web/schemas.d.ts +45 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +105 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/screen-blocked.d.ts +80 -0
- package/dist/web/screen-blocked.d.ts.map +1 -0
- package/dist/web/screen-blocked.js +106 -0
- package/dist/web/screen-blocked.js.map +1 -0
- package/dist/web/server.d.ts +6 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +73 -14
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-listener-wiring.d.ts +1 -0
- package/dist/web/session-listener-wiring.d.ts.map +1 -1
- package/dist/web/session-listener-wiring.js +20 -0
- package/dist/web/session-listener-wiring.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +27 -9
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +36 -13
- package/dist/web/session-wait-registry.js.map +1 -1
- package/dist/web/sse-events.d.ts +12 -4
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +12 -4
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +3 -2
- package/skills/codeman/SKILL.md +47 -27
- package/skills/codeman/preamble.sh +14 -11
- package/skills/codeman/reference/endpoints.md +1 -1
- package/skills/codeman/reference/recipes.md +12 -9
- package/skills/codeman/reference/verbs.md +3 -1
- package/dist/web/public/app.6f8552e1.js +0 -43
- package/dist/web/public/app.6f8552e1.js.br +0 -0
- package/dist/web/public/app.6f8552e1.js.gz +0 -0
- package/dist/web/public/constants.bc9c1f63.js.br +0 -0
- package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
- package/dist/web/public/i18n.a9840ea2.js +0 -2
- package/dist/web/public/i18n.a9840ea2.js.br +0 -0
- package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
- package/dist/web/public/image-input.e00471c6.js +0 -283
- package/dist/web/public/image-input.e00471c6.js.br +0 -0
- package/dist/web/public/image-input.e00471c6.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.br +0 -0
- package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
- package/dist/web/public/mobile.111b187d.css +0 -1
- package/dist/web/public/mobile.111b187d.css.br +0 -0
- package/dist/web/public/mobile.111b187d.css.gz +0 -0
- package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
- package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.br +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
- package/dist/web/public/session-ui.f731602c.js +0 -100
- package/dist/web/public/session-ui.f731602c.js.br +0 -0
- package/dist/web/public/session-ui.f731602c.js.gz +0 -0
- package/dist/web/public/settings-ui.4e33390f.js +0 -97
- package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
- package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
- package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
- package/dist/web/public/styles.81a72906.css +0 -1
- package/dist/web/public/styles.81a72906.css.br +0 -0
- package/dist/web/public/styles.81a72906.css.gz +0 -0
- package/dist/web/public/tab-layout-browser.065fcba4.js +0 -1
- package/dist/web/public/tab-layout-browser.065fcba4.js.br +0 -0
- package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
- package/dist/web/public/tab-rail-resize.a5867793.js.gz +0 -0
- package/dist/web/public/terminal-ui.b7267005.js +0 -2
- package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
- package/dist/web/public/terminal-ui.b7267005.js.gz +0 -0
package/dist/tmux-manager.js
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
* - **Persistence**: Sessions survive server restarts and disconnects
|
|
8
8
|
* - **Ghost recovery**: Orphaned sessions are discovered and reattached on startup
|
|
9
9
|
* - **Resource tracking**: Memory, CPU, and child process stats per session
|
|
10
|
-
* - **Reliable input**:
|
|
10
|
+
* - **Reliable input**: a submitted prompt goes in as one bracketed paste, then a separate Enter
|
|
11
11
|
* - **Teammate support**: Immutable pane IDs enable targeting individual teammates
|
|
12
12
|
*
|
|
13
13
|
* tmux sessions are named `codeman-{sessionId}` and stored in ~/.codeman/mux-sessions.json.
|
|
14
14
|
*
|
|
15
15
|
* Key features:
|
|
16
|
-
* - `send-keys
|
|
16
|
+
* - `paste-buffer -p` + a separate `send-keys Enter` submits a prompt (see sendInput)
|
|
17
17
|
* - `list-sessions -F` provides structured queries
|
|
18
18
|
* - `display-message -p '#{pane_pid}'` for reliable PID discovery
|
|
19
19
|
* - Single server architecture
|
|
@@ -292,6 +292,70 @@ export function resolveTmuxPaneTarget(muxName, paneTarget) {
|
|
|
292
292
|
}
|
|
293
293
|
return `${muxName}.${paneTarget}`;
|
|
294
294
|
}
|
|
295
|
+
let submitBufferSeq = 0;
|
|
296
|
+
/**
|
|
297
|
+
* Escape a final `;` for tmux. tmux splits commands on an argument that ENDS in
|
|
298
|
+
* `;` (`set-buffer -- 'a;'` stores `a`, `send-keys -l 'a;'` types `a`, a lone `;`
|
|
299
|
+
* is no argument at all); a final `\;` is its escape for a literal `;`. A `;`
|
|
300
|
+
* anywhere else in the argument is literal already. Exported for unit testing.
|
|
301
|
+
*/
|
|
302
|
+
export function escapeTmuxTrailingSemicolon(text) {
|
|
303
|
+
return text.endsWith(';') ? `${text.slice(0, -1)}\\;` : text;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Whether a text+Enter submit types `text` as keys instead of pasting it: true for
|
|
307
|
+
* a single character. Claude Code's and Codex's selection dialogs ignore a bracketed
|
|
308
|
+
* paste, so a pasted `3` + Enter confirms the highlighted option instead of option 3;
|
|
309
|
+
* a typed `3` selects it. One character cannot form the burst Codex reads as a paste,
|
|
310
|
+
* so typing it is safe there. Exported for unit testing.
|
|
311
|
+
*/
|
|
312
|
+
export function submitsAsKeys(text) {
|
|
313
|
+
return [...text].length === 1;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* tmux commands that type `text` into `target` as ONE bracketed paste, ahead of a
|
|
317
|
+
* separately sent `Enter` that submits it. Exported for unit testing.
|
|
318
|
+
*
|
|
319
|
+
* Why a paste and not `send-keys -l`: Codex detects a paste that arrives WITHOUT
|
|
320
|
+
* bracketed-paste markers heuristically, from characters arriving in a fast burst,
|
|
321
|
+
* and folds an Enter that lands inside that burst window into the paste as a
|
|
322
|
+
* newline. `send-keys -l` types exactly such an unmarked burst, so a text+Enter
|
|
323
|
+
* pair sat in Codex's composer unsubmitted: always for long text (Codex shows it as
|
|
324
|
+
* `[Pasted Content N chars]`, which SubmitVerifier cannot match against the prompt,
|
|
325
|
+
* so nothing re-presses Enter), intermittently for short text at the 50 ms gap.
|
|
326
|
+
* `paste-buffer -p` wraps the text in the markers (only when the pane asked for
|
|
327
|
+
* them, so an app without bracketed paste sees the same bytes as before) and the
|
|
328
|
+
* Enter after it is unambiguous.
|
|
329
|
+
*
|
|
330
|
+
* Each call gets its own named buffer so concurrent sends cannot paste each other's
|
|
331
|
+
* text. tmux keeps a named buffer until something deletes it (`buffer-limit` trims
|
|
332
|
+
* only automatic ones), hence `discard` for a paste that fails.
|
|
333
|
+
*
|
|
334
|
+
* A single character is typed instead (submitsAsKeys), so a dialog answer selects.
|
|
335
|
+
* A final `;` is escaped (escapeTmuxTrailingSemicolon).
|
|
336
|
+
*/
|
|
337
|
+
export function buildPasteTextCommands(tmux, target, text) {
|
|
338
|
+
const buffer = `codeman-submit-${process.pid}-${++submitBufferSeq}`;
|
|
339
|
+
return {
|
|
340
|
+
set: `${tmux} set-buffer -b ${buffer} -- ${shellescape(escapeTmuxTrailingSemicolon(text))}`,
|
|
341
|
+
paste: `${tmux} paste-buffer -p -d -b ${buffer} -t ${shellescape(target)}`,
|
|
342
|
+
discard: `${tmux} delete-buffer -b ${buffer}`,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Run a bracketed paste. A failed paste (the pane or the server is gone) deletes
|
|
347
|
+
* the buffer before the paste's own error propagates; a failing delete is ignored.
|
|
348
|
+
*/
|
|
349
|
+
export async function pasteText(run, cmds) {
|
|
350
|
+
await run(cmds.set);
|
|
351
|
+
try {
|
|
352
|
+
await run(cmds.paste);
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
await run(cmds.discard).catch(() => undefined);
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
295
359
|
/**
|
|
296
360
|
* Pick the active pane id from `tmux list-panes -F '#{pane_id}:#{pane_active}'`
|
|
297
361
|
* output (lines like `%0:1`). Returns the pane id whose active flag is 1.
|
|
@@ -534,11 +598,111 @@ function truncatePaneLineByVisibleColumns(line, maxColumns) {
|
|
|
534
598
|
export function normalizeScrollbackEol(buffer) {
|
|
535
599
|
return buffer.replace(/\r?\n/g, '\r\n');
|
|
536
600
|
}
|
|
601
|
+
/** `capture-pane -p` text as rows: tmux ends it with one `\n`, and '' has none. */
|
|
602
|
+
function captureRows(text) {
|
|
603
|
+
return text === '' ? [] : text.replace(/\n$/, '').split('\n');
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Least overlap that places a frame in a screen: rows compared, and how many
|
|
607
|
+
* of them hold text. A placement too LOW repeats the rows between the true one
|
|
608
|
+
* and it, so a short overlap (one `$` or `❯` row, a rule line) is not trusted:
|
|
609
|
+
* past this, the plain join, which can only lose rows, is the safer answer.
|
|
610
|
+
*/
|
|
611
|
+
const FRAME_MATCH_MIN_ROWS = 3;
|
|
612
|
+
const FRAME_MATCH_MIN_CONTENT_ROWS = 2;
|
|
613
|
+
/**
|
|
614
|
+
* A captured row as plain text: escape sequences (SGR, OSC 8 links) and
|
|
615
|
+
* trailing blanks dropped. Two tmuxes render the same cell content, but need
|
|
616
|
+
* not encode its attributes the same way.
|
|
617
|
+
*/
|
|
618
|
+
function capturedRowText(row) {
|
|
619
|
+
// eslint-disable-next-line no-control-regex
|
|
620
|
+
return row.replace(/\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]/g, '').trimEnd();
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Where a later frame of the same pane begins among the rows of an earlier
|
|
624
|
+
* screen capture, or -1 when that cannot be told.
|
|
625
|
+
*
|
|
626
|
+
* The frame's top rows must equal the screen's rows from that index on, as
|
|
627
|
+
* text. Two rows are left out of the comparison: the screen's trailing blank
|
|
628
|
+
* rows (output after the screen read fills them in the frame) and its last
|
|
629
|
+
* content row (output in flight may have finished it since). Every compared
|
|
630
|
+
* row must match, the overlap must reach FRAME_MATCH_MIN_ROWS rows with
|
|
631
|
+
* FRAME_MATCH_MIN_CONTENT_ROWS of them non-blank, and the SMALLEST index wins,
|
|
632
|
+
* so a block that merely repeats further down is only chosen when nothing
|
|
633
|
+
* above it matches. A row that changed between the two reads (a spinner, a
|
|
634
|
+
* clock) fails the true placement; the minimum keeps a lower one from being
|
|
635
|
+
* taken on a row or two that happen to be equal.
|
|
636
|
+
*
|
|
637
|
+
* An agent TUI mid-turn (Claude Code, codex, opencode) is not placed: it keeps
|
|
638
|
+
* a live region (spinner, composer, footer) pinned to the bottom and inserts new
|
|
639
|
+
* rows above it, so the frame is not a shifted copy of the screen and the
|
|
640
|
+
* comparison down to the screen's last content row fails. The caller then falls
|
|
641
|
+
* back to the plain join. A plain shell places, unless the frame starts on the
|
|
642
|
+
* second half of a wrapped line: -J holds that line as one screen row, so it
|
|
643
|
+
* matches nothing and the plain join applies too.
|
|
644
|
+
*/
|
|
645
|
+
export function findFrameInScreen(screenRows, frameRows) {
|
|
646
|
+
const screen = screenRows.map(capturedRowText);
|
|
647
|
+
const frame = frameRows.map(capturedRowText);
|
|
648
|
+
let lastContent = screen.length - 1;
|
|
649
|
+
while (lastContent >= 0 && screen[lastContent] === '')
|
|
650
|
+
lastContent--;
|
|
651
|
+
for (let start = 0; start < lastContent; start++) {
|
|
652
|
+
const overlap = Math.min(lastContent - start, frame.length);
|
|
653
|
+
if (overlap < FRAME_MATCH_MIN_ROWS)
|
|
654
|
+
break; // only shrinks from here on
|
|
655
|
+
let matches = true;
|
|
656
|
+
let contentRows = 0;
|
|
657
|
+
for (let k = 0; k < overlap && matches; k++) {
|
|
658
|
+
if (screen[start + k] !== frame[k])
|
|
659
|
+
matches = false;
|
|
660
|
+
else if (frame[k] !== '')
|
|
661
|
+
contentRows++;
|
|
662
|
+
}
|
|
663
|
+
if (matches && contentRows >= FRAME_MATCH_MIN_CONTENT_ROWS)
|
|
664
|
+
return start;
|
|
665
|
+
}
|
|
666
|
+
return -1;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Put scrollback rows read elsewhere (`PaneCaptureOptions.scrollbackOverride`)
|
|
670
|
+
* above a visible-frame capture, as one capture in tmux's own shape: rows joined
|
|
671
|
+
* by a bare `\n`. Undefined or empty scrollback leaves the frame as it is.
|
|
672
|
+
*
|
|
673
|
+
* `screen` is that other pane's screen, read right after its scrollback. The
|
|
674
|
+
* frame is captured later, so rows can have scrolled out of it in between, and
|
|
675
|
+
* a frame showing part of a taller screen starts below that screen's top.
|
|
676
|
+
* Either way the screen rows above the frame's first row are in neither the
|
|
677
|
+
* scrollback nor the frame, so they go between the two. When the frame cannot
|
|
678
|
+
* be placed in the screen ({@link findFrameInScreen}), the join is the plain
|
|
679
|
+
* scrollback-over-frame one.
|
|
680
|
+
*/
|
|
681
|
+
export function joinScrollbackAndFrame(scrollback, frame, screen) {
|
|
682
|
+
let above = scrollback ? captureRows(scrollback) : [];
|
|
683
|
+
if (screen) {
|
|
684
|
+
const screenRows = captureRows(screen);
|
|
685
|
+
const frameStart = findFrameInScreen(screenRows, captureRows(frame));
|
|
686
|
+
if (frameStart > 0) {
|
|
687
|
+
// The remote screen is a capture of its own too, encoded from the default
|
|
688
|
+
// state: the history's last row must not leave its attributes active.
|
|
689
|
+
if (above.length > 0)
|
|
690
|
+
above[above.length - 1] += '\x1b[0m';
|
|
691
|
+
above = above.concat(screenRows.slice(0, frameStart));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Separate captures: tmux encodes the frame relative to the DEFAULT state, so
|
|
695
|
+
// the rows above it must not leave their attributes active.
|
|
696
|
+
return above.length > 0 ? `${above.join('\n')}\x1b[0m\n${frame}` : frame;
|
|
697
|
+
}
|
|
537
698
|
/**
|
|
538
699
|
* Read the pane's cursor and size, or null when tmux cannot say.
|
|
539
700
|
*
|
|
540
|
-
*
|
|
541
|
-
* a caret with it, and one that gets null must not try.
|
|
701
|
+
* The four geometry fields are validated together: a caller that gets a value
|
|
702
|
+
* back can place a caret with it, and one that gets null must not try. The
|
|
703
|
+
* optional fifth and sixth fields, the history depth and the alternate-screen
|
|
704
|
+
* flag, are only informational, so a bad one is dropped rather than
|
|
705
|
+
* discarding a good caret.
|
|
542
706
|
*/
|
|
543
707
|
export function queryPaneCursor(run) {
|
|
544
708
|
let raw;
|
|
@@ -549,7 +713,9 @@ export function queryPaneCursor(run) {
|
|
|
549
713
|
console.error('[TmuxManager] Failed to query pane cursor after capture:', cursorErr);
|
|
550
714
|
return null;
|
|
551
715
|
}
|
|
552
|
-
const [cursorX, cursorY, cols, rows] = raw
|
|
716
|
+
const [cursorX, cursorY, cols, rows, historyLines, alternateOn] = raw
|
|
717
|
+
.split(/\s+/)
|
|
718
|
+
.map((value) => parseInt(value, 10));
|
|
553
719
|
if (!Number.isFinite(cursorX) ||
|
|
554
720
|
!Number.isFinite(cursorY) ||
|
|
555
721
|
!Number.isFinite(cols) ||
|
|
@@ -560,7 +726,12 @@ export function queryPaneCursor(run) {
|
|
|
560
726
|
rows <= 0) {
|
|
561
727
|
return null;
|
|
562
728
|
}
|
|
563
|
-
|
|
729
|
+
const geometry = { cols, rows, cursorX, cursorY };
|
|
730
|
+
if (Number.isFinite(historyLines) && historyLines >= 0)
|
|
731
|
+
geometry.historyLines = historyLines;
|
|
732
|
+
if (alternateOn === 0 || alternateOn === 1)
|
|
733
|
+
geometry.alternateOn = alternateOn === 1;
|
|
734
|
+
return geometry;
|
|
564
735
|
}
|
|
565
736
|
/** SGR attributes, which is all `capture-pane -e` emits. */
|
|
566
737
|
// eslint-disable-next-line no-control-regex
|
|
@@ -746,6 +917,18 @@ const REMOTE_TMUX_SOCKET = 'codeman-remote';
|
|
|
746
917
|
export function remoteTmuxSessionName(sessionId) {
|
|
747
918
|
return `codeman-ssh-${sessionId.slice(0, 8)}`;
|
|
748
919
|
}
|
|
920
|
+
/**
|
|
921
|
+
* The remote tmux socket and session a remote Codeman session's pane is attached
|
|
922
|
+
* to, matching the command that attached it: an owned session is the one
|
|
923
|
+
* `buildRemoteLaunchCommand` created on the private socket, a non-owned one is the
|
|
924
|
+
* discovered session `buildRemoteAttachCommand` joined on `-L codeman`.
|
|
925
|
+
*/
|
|
926
|
+
export function remoteTmuxLocation(remote, sessionId) {
|
|
927
|
+
if (remote.owned === false) {
|
|
928
|
+
return { socket: 'codeman', sessionName: remote.remoteSessionName || remoteTmuxSessionName(sessionId) };
|
|
929
|
+
}
|
|
930
|
+
return { socket: REMOTE_TMUX_SOCKET, sessionName: remoteTmuxSessionName(sessionId) };
|
|
931
|
+
}
|
|
749
932
|
/**
|
|
750
933
|
* COD-104 — build the SSH command that launches (or reattaches) a remote
|
|
751
934
|
* session INSIDE a tmux server on the remote host, so the remote agent survives
|
|
@@ -1733,6 +1916,24 @@ export class TmuxManager extends EventEmitter {
|
|
|
1733
1916
|
}
|
|
1734
1917
|
}
|
|
1735
1918
|
}
|
|
1919
|
+
/**
|
|
1920
|
+
* The statusLine exporter for one claude launch, and why it is missing when it is.
|
|
1921
|
+
* Registry-gated (capabilities.statusLineTelemetry, claude only today), local spawns
|
|
1922
|
+
* only (remote/docker have their own separate command builders). Resolving still runs
|
|
1923
|
+
* with collection off, because it also self-heals: it strips any legacy disk-written
|
|
1924
|
+
* exporter from an older Codeman build the first time a session starts in that
|
|
1925
|
+
* workspace again. `state` lands on the mux record so the Status row can
|
|
1926
|
+
* say why no cache report will come, instead of "not reported yet" forever.
|
|
1927
|
+
*/
|
|
1928
|
+
async _resolveStatusLine(mode, workingDir, remoteOrDocker) {
|
|
1929
|
+
if (!getCli(mode)?.capabilities.statusLineTelemetry)
|
|
1930
|
+
return {};
|
|
1931
|
+
if (remoteOrDocker)
|
|
1932
|
+
return { state: 'remote' };
|
|
1933
|
+
const enabled = await readPlanUsageTelemetryEnabled();
|
|
1934
|
+
const command = await resolveStatusLineCliCommand(workingDir, enabled);
|
|
1935
|
+
return { command, state: command ? 'injected' : enabled ? 'workspace-statusline' : 'collection-off' };
|
|
1936
|
+
}
|
|
1736
1937
|
/**
|
|
1737
1938
|
* Export the user's own REAL statusLine command (found by
|
|
1738
1939
|
* findEffectiveUserStatusLineCommand) via tmux setenv, so the shared
|
|
@@ -1820,13 +2021,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1820
2021
|
throw new Error(message);
|
|
1821
2022
|
}
|
|
1822
2023
|
const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
|
|
1823
|
-
|
|
1824
|
-
// spawns only (remote/docker have their own separate command builders — out of
|
|
1825
|
-
// scope here). Also self-heals: strips any legacy disk-written exporter from an
|
|
1826
|
-
// older Codeman build the first time a session starts in that workspace again.
|
|
1827
|
-
const statusLineCommand = getCli(mode)?.capabilities.statusLineTelemetry && !remote && !docker
|
|
1828
|
-
? await resolveStatusLineCliCommand(workingDir, await readPlanUsageTelemetryEnabled())
|
|
1829
|
-
: undefined;
|
|
2024
|
+
const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir, !!(remote || docker));
|
|
1830
2025
|
// The user's own REAL statusLine, if any (walked via Claude Code's own
|
|
1831
2026
|
// settings precedence) — exported below so the shared exporter script
|
|
1832
2027
|
// can wrap it. Only worth discovering when we're actually injecting.
|
|
@@ -1966,6 +2161,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
1966
2161
|
mode,
|
|
1967
2162
|
attached: false,
|
|
1968
2163
|
name,
|
|
2164
|
+
statusLine,
|
|
1969
2165
|
};
|
|
1970
2166
|
this.sessions.set(sessionId, session);
|
|
1971
2167
|
this.saveSessions();
|
|
@@ -2063,10 +2259,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
2063
2259
|
// Resolve CLI binary directory based on mode
|
|
2064
2260
|
const { pathExport } = this.buildPathExport(mode);
|
|
2065
2261
|
const envExportsStr = this.buildEnvExports(sessionId, muxName, mode).join(' && ');
|
|
2066
|
-
|
|
2067
|
-
const statusLineCommand = getCli(mode)?.capabilities.statusLineTelemetry && !remote && !docker
|
|
2068
|
-
? await resolveStatusLineCliCommand(workingDir, await readPlanUsageTelemetryEnabled())
|
|
2069
|
-
: undefined;
|
|
2262
|
+
const { command: statusLineCommand, state: statusLine } = await this._resolveStatusLine(mode, workingDir, !!(remote || docker));
|
|
2070
2263
|
const userStatusLineCommand = statusLineCommand ? await findEffectiveUserStatusLineCommand(workingDir) : undefined;
|
|
2071
2264
|
const baseCmd = buildSpawnCommand({
|
|
2072
2265
|
mode,
|
|
@@ -2118,6 +2311,10 @@ export class TmuxManager extends EventEmitter {
|
|
|
2118
2311
|
const pid = this.getPanePid(muxName);
|
|
2119
2312
|
if (pid)
|
|
2120
2313
|
session.pid = pid;
|
|
2314
|
+
if (session.statusLine !== statusLine) {
|
|
2315
|
+
session.statusLine = statusLine;
|
|
2316
|
+
this.saveSessions();
|
|
2317
|
+
}
|
|
2121
2318
|
return pid;
|
|
2122
2319
|
}
|
|
2123
2320
|
catch (err) {
|
|
@@ -3131,10 +3328,12 @@ export class TmuxManager extends EventEmitter {
|
|
|
3131
3328
|
});
|
|
3132
3329
|
}
|
|
3133
3330
|
/**
|
|
3134
|
-
* Send input directly to a tmux session
|
|
3331
|
+
* Send input directly to a tmux session.
|
|
3135
3332
|
*
|
|
3136
|
-
*
|
|
3137
|
-
*
|
|
3333
|
+
* - Text followed by `\r` goes in as ONE bracketed paste (buildPasteTextCommands),
|
|
3334
|
+
* so Codex cannot fold the Enter into a paste burst; a single character is typed
|
|
3335
|
+
* with `send-keys -l` instead, so a dialog answer selects (submitsAsKeys)
|
|
3336
|
+
* - Text without `\r` is typed literally with `send-keys -l` (no key interpretation)
|
|
3138
3337
|
* - `Enter` key is sent as a SEPARATE tmux invocation after a small delay
|
|
3139
3338
|
* - Ink (Claude CLI) needs text and Enter split to avoid treating Enter as a newline
|
|
3140
3339
|
*/
|
|
@@ -3160,10 +3359,17 @@ export class TmuxManager extends EventEmitter {
|
|
|
3160
3359
|
// Send text first, then Enter as a SEPARATE tmux command after a short delay.
|
|
3161
3360
|
// Ink (Claude CLI's terminal framework) needs them split — sending both in a
|
|
3162
3361
|
// single tmux invocation (via \;) causes Ink to interpret Enter as a newline
|
|
3163
|
-
// character in the input buffer rather than as form submission.
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3362
|
+
// character in the input buffer rather than as form submission. The text goes
|
|
3363
|
+
// in as a bracketed paste so Codex cannot fold the Enter into it either
|
|
3364
|
+
// (see buildPasteTextCommands). A single character is typed, because a
|
|
3365
|
+
// selection dialog ignores a pasted `3` and Enter then confirms the
|
|
3366
|
+
// highlighted option (see submitsAsKeys).
|
|
3367
|
+
if (submitsAsKeys(textPart)) {
|
|
3368
|
+
await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, { timeout: EXEC_TIMEOUT_MS });
|
|
3369
|
+
}
|
|
3370
|
+
else {
|
|
3371
|
+
await pasteText((cmd) => execAsync(cmd, { timeout: EXEC_TIMEOUT_MS }), buildPasteTextCommands(this.tmux(), session.muxName, textPart));
|
|
3372
|
+
}
|
|
3167
3373
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
3168
3374
|
await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" Enter`, {
|
|
3169
3375
|
timeout: EXEC_TIMEOUT_MS,
|
|
@@ -3171,9 +3377,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
3171
3377
|
}
|
|
3172
3378
|
else if (textPart) {
|
|
3173
3379
|
// Text only, no Enter
|
|
3174
|
-
await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(textPart)}`, {
|
|
3175
|
-
timeout: EXEC_TIMEOUT_MS,
|
|
3176
|
-
});
|
|
3380
|
+
await execAsync(`${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, { timeout: EXEC_TIMEOUT_MS });
|
|
3177
3381
|
}
|
|
3178
3382
|
else if (hasCarriageReturn) {
|
|
3179
3383
|
// Enter only
|
|
@@ -3307,7 +3511,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
3307
3511
|
const hasCarriageReturn = input.includes('\r');
|
|
3308
3512
|
const textPart = input.replace(/\r/g, '').replace(/\n/g, '').trimEnd();
|
|
3309
3513
|
if (textPart && hasCarriageReturn) {
|
|
3310
|
-
execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(textPart)}`, {
|
|
3514
|
+
execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
|
|
3311
3515
|
encoding: 'utf-8',
|
|
3312
3516
|
timeout: EXEC_TIMEOUT_MS,
|
|
3313
3517
|
});
|
|
@@ -3317,7 +3521,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
3317
3521
|
});
|
|
3318
3522
|
}
|
|
3319
3523
|
else if (textPart) {
|
|
3320
|
-
execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(textPart)}`, {
|
|
3524
|
+
execSync(`${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, {
|
|
3321
3525
|
encoding: 'utf-8',
|
|
3322
3526
|
timeout: EXEC_TIMEOUT_MS,
|
|
3323
3527
|
});
|
|
@@ -3397,7 +3601,14 @@ export class TmuxManager extends EventEmitter {
|
|
|
3397
3601
|
const historyLines = typeof requestedLines === 'number' && Number.isFinite(requestedLines) && requestedLines > 0
|
|
3398
3602
|
? Math.trunc(requestedLines)
|
|
3399
3603
|
: DEFAULT_TMUX_HISTORY_LIMIT;
|
|
3400
|
-
|
|
3604
|
+
// With a scrollback override this pane's own history is not read at all:
|
|
3605
|
+
// `-S 0` starts at the top of the visible frame.
|
|
3606
|
+
const scrollbackOverride = fullHistory ? opts?.scrollbackOverride : undefined;
|
|
3607
|
+
const captureFlags = !fullHistory
|
|
3608
|
+
? 'capture-pane -p -e'
|
|
3609
|
+
: scrollbackOverride !== undefined
|
|
3610
|
+
? 'capture-pane -p -e -J -S 0'
|
|
3611
|
+
: `capture-pane -p -e -J -S -${historyLines}`;
|
|
3401
3612
|
// execSync's default maxBuffer (1MB) kills multi-MB scrollback dumps
|
|
3402
3613
|
// (ENOBUFS) and would silently degrade full-history capture to the byte
|
|
3403
3614
|
// buffer for exactly the long sessions it exists for — size it from the
|
|
@@ -3410,12 +3621,12 @@ export class TmuxManager extends EventEmitter {
|
|
|
3410
3621
|
execOpts.maxBuffer =
|
|
3411
3622
|
(opts?.maxCaptureBytes ?? DEFAULT_TERMINAL_BUFFER_MAX_BYTES) + FULL_HISTORY_CAPTURE_SLACK_BYTES;
|
|
3412
3623
|
}
|
|
3413
|
-
const rawCapture = execSync(`${this.tmux()} ${captureFlags} -t ${shellescape(target)}`, execOpts);
|
|
3624
|
+
const rawCapture = joinScrollbackAndFrame(scrollbackOverride, execSync(`${this.tmux()} ${captureFlags} -t ${shellescape(target)}`, execOpts), scrollbackOverride !== undefined ? opts?.scrollbackOverrideScreen : undefined);
|
|
3414
3625
|
// Query the cursor BEFORE deciding anything else. On the full-history path
|
|
3415
3626
|
// it settles both how the capture is trimmed and whether a cursor move is
|
|
3416
3627
|
// appended, and those two have to agree: trailing blank rows are only safe
|
|
3417
3628
|
// to keep when a move follows to put the caret back above them.
|
|
3418
|
-
const geometry = queryPaneCursor(() => execSync(`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height}'`, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }));
|
|
3629
|
+
const geometry = queryPaneCursor(() => execSync(`${this.tmux()} display-message -p -t ${shellescape(target)} '#{cursor_x} #{cursor_y} #{pane_width} #{pane_height} #{history_size} #{alternate_on}'`, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }));
|
|
3419
3630
|
// Report the size the pane was really drawing at. The visible-frame path
|
|
3420
3631
|
// below addresses every row absolutely, so a consumer whose terminal is
|
|
3421
3632
|
// shorter than this piles the overflow rows onto its last line and loses
|
|
@@ -3425,12 +3636,25 @@ export class TmuxManager extends EventEmitter {
|
|
|
3425
3636
|
// the caller can see both sizes, so hand it this one.
|
|
3426
3637
|
if (opts && geometry)
|
|
3427
3638
|
opts.capturedGeometry = { cols: geometry.cols, rows: geometry.rows };
|
|
3639
|
+
// Same query, no extra tmux call: how much scrollback a full-history pull
|
|
3640
|
+
// could return. A pane in the alternate screen (fullscreen claude) holds
|
|
3641
|
+
// none, and the partial-history notice must not promise it. A caller that
|
|
3642
|
+
// bounded the capture with `historyLimitLines` also reads it to tell
|
|
3643
|
+
// whether the bound cut anything.
|
|
3644
|
+
if (opts && geometry?.historyLines !== undefined)
|
|
3645
|
+
opts.capturedHistoryLines = geometry.historyLines;
|
|
3646
|
+
// A pane in the alternate screen keeps no history (a nested tmux client
|
|
3647
|
+
// holds it there), so its capture is one screen however deep it reads.
|
|
3648
|
+
if (opts && geometry?.alternateOn !== undefined)
|
|
3649
|
+
opts.capturedAlternateOn = geometry.alternateOn;
|
|
3428
3650
|
if (fullHistory) {
|
|
3429
3651
|
// Without geometry there is no cursor move, so fall back to the old trim.
|
|
3430
3652
|
// Keeping the blank rows here would park the caret at the bottom of the
|
|
3431
3653
|
// pane with nothing to correct it — worse than not trying at all.
|
|
3432
|
-
if (!geometry)
|
|
3433
|
-
|
|
3654
|
+
if (!geometry) {
|
|
3655
|
+
const trimmed = rawCapture.replace(/\n+$/g, '');
|
|
3656
|
+
return trimmed ? `${normalizeScrollbackEol(trimmed)}\x1b[0m` : '';
|
|
3657
|
+
}
|
|
3434
3658
|
// Take the line terminator off and nothing else. The trailing blank rows
|
|
3435
3659
|
// that remain are the real bottom of the screen, and the cursor move
|
|
3436
3660
|
// below counts up from it. tmux joins rows with a bare `\n`; normalize to
|
|
@@ -3445,7 +3669,10 @@ export class TmuxManager extends EventEmitter {
|
|
|
3445
3669
|
// side where that guard cannot see it.
|
|
3446
3670
|
if (!hasVisibleContent(trimmed))
|
|
3447
3671
|
return '';
|
|
3448
|
-
|
|
3672
|
+
// capture-pane -e can leave the last row's SGR active (e.g. a nested
|
|
3673
|
+
// tmux status bar). Hand live output the same default style as a visible
|
|
3674
|
+
// snapshot, resetting before the cursor move so its position is kept.
|
|
3675
|
+
return `${normalizeScrollbackEol(trimmed)}\x1b[0m${formatCursorRestore(geometry)}`;
|
|
3449
3676
|
}
|
|
3450
3677
|
const buffer = rawCapture.replace(/\n+$/g, '');
|
|
3451
3678
|
if (geometry)
|
|
@@ -3454,7 +3681,7 @@ export class TmuxManager extends EventEmitter {
|
|
|
3454
3681
|
// positioned snapshot repaint and fall back to the raw capture. Normalize
|
|
3455
3682
|
// its bare `\n` line endings to `\r\n` so the replay doesn't staircase
|
|
3456
3683
|
// diagonally in a fresh xterm (COD-138, same reason as the fullHistory path).
|
|
3457
|
-
return normalizeScrollbackEol(buffer);
|
|
3684
|
+
return buffer ? `${normalizeScrollbackEol(buffer)}\x1b[0m` : '';
|
|
3458
3685
|
}
|
|
3459
3686
|
catch (err) {
|
|
3460
3687
|
// ENOBUFS carries the truncated multi-MB stdout on the error object —
|