botmux 2.51.1 → 2.53.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.en.md +22 -265
- package/README.md +21 -296
- package/dist/adapters/backend/session-backend-selector.d.ts +5 -1
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +15 -1
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +3 -0
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +22 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js +7 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zellij-backend.d.ts +132 -0
- package/dist/adapters/backend/zellij-backend.d.ts.map +1 -0
- package/dist/adapters/backend/zellij-backend.js +375 -0
- package/dist/adapters/backend/zellij-backend.js.map +1 -0
- package/dist/adapters/backend/zellij-observe-backend.d.ts +62 -0
- package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -0
- package/dist/adapters/backend/zellij-observe-backend.js +218 -0
- package/dist/adapters/backend/zellij-observe-backend.js.map +1 -0
- package/dist/adapters/cli/claude-code.d.ts +39 -5
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +53 -31
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/registry.d.ts +2 -1
- package/dist/adapters/cli/registry.d.ts.map +1 -1
- package/dist/adapters/cli/registry.js +3 -1
- package/dist/adapters/cli/registry.js.map +1 -1
- package/dist/adapters/cli/seed.d.ts +29 -0
- package/dist/adapters/cli/seed.d.ts.map +1 -0
- package/dist/adapters/cli/seed.js +63 -0
- package/dist/adapters/cli/seed.js.map +1 -0
- package/dist/adapters/cli/types.d.ts +17 -1
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +31 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +31 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +37 -27
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +7 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/core/ask-hook/registry.d.ts.map +1 -1
- package/dist/core/ask-hook/registry.js +4 -0
- package/dist/core/ask-hook/registry.js.map +1 -1
- package/dist/core/command-handler.d.ts +4 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +100 -8
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +37 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dispatch.d.ts +33 -0
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +26 -0
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/session-discovery.d.ts +13 -4
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +5 -5
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts +10 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +43 -18
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +5 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +22 -9
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/core/zellij-adopt-discovery.d.ts +28 -0
- package/dist/core/zellij-adopt-discovery.d.ts.map +1 -0
- package/dist/core/zellij-adopt-discovery.js +255 -0
- package/dist/core/zellij-adopt-discovery.js.map +1 -0
- package/dist/core/zellij-session-discovery.d.ts +73 -0
- package/dist/core/zellij-session-discovery.d.ts.map +1 -0
- package/dist/core/zellij-session-discovery.js +259 -0
- package/dist/core/zellij-session-discovery.js.map +1 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +145 -13
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +114 -0
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +23 -1
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +1 -0
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/workflows.js +1 -1
- package/dist/dashboard/web/workflows.js.map +1 -1
- package/dist/dashboard-web/app.js +449 -426
- package/dist/dashboard.js +20 -0
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +15 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +16 -2
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +8 -3
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +74 -5
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +72 -10
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +12 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +39 -31
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/grant-command.d.ts +26 -0
- package/dist/im/lark/grant-command.d.ts.map +1 -1
- package/dist/im/lark/grant-command.js +142 -3
- package/dist/im/lark/grant-command.js.map +1 -1
- package/dist/im/lark/grant-pending.d.ts +7 -4
- package/dist/im/lark/grant-pending.d.ts.map +1 -1
- package/dist/im/lark/grant-pending.js +12 -6
- package/dist/im/lark/grant-pending.js.map +1 -1
- package/dist/services/codex-app-threads.d.ts +20 -0
- package/dist/services/codex-app-threads.d.ts.map +1 -0
- package/dist/services/codex-app-threads.js +165 -0
- package/dist/services/codex-app-threads.js.map +1 -0
- package/dist/services/grant-prefs-store.d.ts +23 -0
- package/dist/services/grant-prefs-store.d.ts.map +1 -0
- package/dist/services/grant-prefs-store.js +94 -0
- package/dist/services/grant-prefs-store.js.map +1 -0
- package/dist/services/grant-store.d.ts +34 -2
- package/dist/services/grant-store.d.ts.map +1 -1
- package/dist/services/grant-store.js +160 -9
- package/dist/services/grant-store.js.map +1 -1
- package/dist/services/quota-dedup.d.ts +33 -0
- package/dist/services/quota-dedup.d.ts.map +1 -0
- package/dist/services/quota-dedup.js +67 -0
- package/dist/services/quota-dedup.js.map +1 -0
- package/dist/setup/bot-config-editor.d.ts +1 -1
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +5 -4
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-zellij.d.ts +48 -0
- package/dist/setup/ensure-zellij.d.ts.map +1 -0
- package/dist/setup/ensure-zellij.js +93 -0
- package/dist/setup/ensure-zellij.js.map +1 -0
- package/dist/types.d.ts +9 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/anchor-serializer.d.ts +3 -2
- package/dist/utils/anchor-serializer.d.ts.map +1 -1
- package/dist/utils/anchor-serializer.js +20 -5
- package/dist/utils/anchor-serializer.js.map +1 -1
- package/dist/utils/transient-snapshot.js +2 -2
- package/dist/utils/transient-snapshot.js.map +1 -1
- package/dist/worker.js +124 -30
- package/dist/worker.js.map +1 -1
- package/dist/workflows/attempt-resume.d.ts +1 -1
- package/dist/workflows/attempt-resume.d.ts.map +1 -1
- package/dist/workflows/attempt-resume.js +1 -1
- package/dist/workflows/attempt-resume.js.map +1 -1
- package/package.json +1 -1
package/dist/worker.js
CHANGED
|
@@ -32,10 +32,13 @@ import { WebSocketServer, WebSocket } from 'ws';
|
|
|
32
32
|
import { TerminalRenderer } from './utils/terminal-renderer.js';
|
|
33
33
|
import { DEFAULT_RENDER_COLS, DEFAULT_RENDER_ROWS, MAX_RENDER_COLS, MAX_RENDER_ROWS, MIN_RENDER_COLS, MIN_RENDER_ROWS, clamp, resolveRenderDimensions, } from './utils/render-dimensions.js';
|
|
34
34
|
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
35
|
-
import { claudeJsonlPathForSession, resolveJsonlFromPid, findOpenClaudeSessionIds } from './adapters/cli/claude-code.js';
|
|
35
|
+
import { claudeJsonlPathForSession, resolveJsonlFromPid, findOpenClaudeSessionIds, DEFAULT_CLAUDE_DATA_DIR } from './adapters/cli/claude-code.js';
|
|
36
36
|
import { mtrSessionIdForBotmuxSession } from './adapters/cli/mtr.js';
|
|
37
37
|
import { TmuxBackend } from './adapters/backend/tmux-backend.js';
|
|
38
38
|
import { TmuxPipeBackend } from './adapters/backend/tmux-pipe-backend.js';
|
|
39
|
+
import { ZellijBackend } from './adapters/backend/zellij-backend.js';
|
|
40
|
+
import { ZellijObserveBackend } from './adapters/backend/zellij-observe-backend.js';
|
|
41
|
+
import { isObserveBackend } from './adapters/backend/types.js';
|
|
39
42
|
import { selectSessionBackend } from './adapters/backend/session-backend-selector.js';
|
|
40
43
|
import { tmuxEnv } from './setup/ensure-tmux.js';
|
|
41
44
|
import { IdleDetector } from './utils/idle-detector.js';
|
|
@@ -60,6 +63,10 @@ let isTmuxMode = false;
|
|
|
60
63
|
* web-terminal updates flow through the shared scrollback fan-out instead
|
|
61
64
|
* of per-WS attach-session PTYs. Set in spawnCli's adopt branch. */
|
|
62
65
|
let isPipeMode = false;
|
|
66
|
+
/** pty-under-zellij backend (BACKEND_TYPE=zellij). Behaves like the non-tmux
|
|
67
|
+
* pty path for the worker (renderer screenshots, relay web terminal) but owns
|
|
68
|
+
* a persistent zellij session that survives daemon restart. */
|
|
69
|
+
let isZellijMode = false;
|
|
63
70
|
let httpServer = null;
|
|
64
71
|
let wss = null;
|
|
65
72
|
const wsClients = new Set();
|
|
@@ -69,7 +76,7 @@ const clientPtys = new Map();
|
|
|
69
76
|
const writeToken = randomBytes(16).toString('hex');
|
|
70
77
|
let sessionId = '';
|
|
71
78
|
let lastInitConfig = null;
|
|
72
|
-
const CLI_DISPLAY_NAMES = { 'claude-code': 'Claude', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', opencode: 'OpenCode', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira' };
|
|
79
|
+
const CLI_DISPLAY_NAMES = { 'claude-code': 'Claude', seed: 'Seed', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', opencode: 'OpenCode', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira' };
|
|
73
80
|
function cliName() { return CLI_DISPLAY_NAMES[lastInitConfig?.cliId ?? ''] ?? 'CLI'; }
|
|
74
81
|
let isPromptReady = false;
|
|
75
82
|
/** Mutex for async flushPending — prevents concurrent flush loops. */
|
|
@@ -147,6 +154,11 @@ let bridgeJsonlDir;
|
|
|
147
154
|
* follow the new jsonl. */
|
|
148
155
|
let bridgeCliPid;
|
|
149
156
|
let bridgeCliCwd;
|
|
157
|
+
/** Claude-family data root the bridge resolves JSONL / pid-state / tasks
|
|
158
|
+
* against. `~/.claude` for Claude Code; Seed CLI's `.claude-runtime`. Set at
|
|
159
|
+
* bridge start (from the adapter's claudeDataDir); defaults to `~/.claude` so
|
|
160
|
+
* the adopt path and any non-seed caller behave exactly as before. */
|
|
161
|
+
let bridgeDataDir = DEFAULT_CLAUDE_DATA_DIR;
|
|
150
162
|
/** Last sessionId we observed via the pid resolver — used to detect
|
|
151
163
|
* rotations cheaply (string compare instead of stat()ing every jsonl). */
|
|
152
164
|
let bridgeObservedCliSessionId;
|
|
@@ -425,7 +437,7 @@ function bridgeAbsorbBaseline() {
|
|
|
425
437
|
function bridgeMarkStalePidStateForAcceptedSid(acceptedSid) {
|
|
426
438
|
if (bridgeCliPid === undefined || bridgeCliCwd === undefined)
|
|
427
439
|
return;
|
|
428
|
-
const pidResolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd);
|
|
440
|
+
const pidResolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd, bridgeDataDir);
|
|
429
441
|
if (pidResolved && pidResolved.cliSessionId !== acceptedSid) {
|
|
430
442
|
bridgeStalePidStateSessionId = pidResolved.cliSessionId;
|
|
431
443
|
}
|
|
@@ -888,7 +900,7 @@ function maybeFollowQuietRotation() {
|
|
|
888
900
|
function maybeFollowSessionRotationViaPid() {
|
|
889
901
|
if (!bridgeCliPid || !bridgeCliCwd)
|
|
890
902
|
return 'unavailable';
|
|
891
|
-
const resolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd);
|
|
903
|
+
const resolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd, bridgeDataDir);
|
|
892
904
|
if (!resolved)
|
|
893
905
|
return 'unavailable';
|
|
894
906
|
if (bridgeObservedCliSessionId !== resolved.cliSessionId) {
|
|
@@ -1060,6 +1072,7 @@ function startBridgeWatcher(jsonlPath, opts) {
|
|
|
1060
1072
|
bridgeJsonlDir = dirname(jsonlPath);
|
|
1061
1073
|
bridgeCliPid = opts?.cliPid;
|
|
1062
1074
|
bridgeCliCwd = opts?.cliCwd;
|
|
1075
|
+
bridgeDataDir = opts?.dataDir ?? DEFAULT_CLAUDE_DATA_DIR;
|
|
1063
1076
|
const mode = opts?.mode ?? 'baseline-existing';
|
|
1064
1077
|
// Pid-state record ranks above the path the adopt scan computed. If
|
|
1065
1078
|
// Claude was launched with `--resume` (or the adopt scan picked a
|
|
@@ -1067,7 +1080,7 @@ function startBridgeWatcher(jsonlPath, opts) {
|
|
|
1067
1080
|
// and we swap to it before baseline so we don't waste a baseline on
|
|
1068
1081
|
// a frozen file.
|
|
1069
1082
|
if (bridgeCliPid && bridgeCliCwd) {
|
|
1070
|
-
const resolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd);
|
|
1083
|
+
const resolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd, bridgeDataDir);
|
|
1071
1084
|
if (resolved) {
|
|
1072
1085
|
bridgeObservedCliSessionId = resolved.cliSessionId;
|
|
1073
1086
|
bridgeRememberSessionIdForPath(resolved.path);
|
|
@@ -1092,10 +1105,10 @@ function startBridgeWatcher(jsonlPath, opts) {
|
|
|
1092
1105
|
// continuously for the active session, so this catches the rotation
|
|
1093
1106
|
// even between writes). `findOpenClaudeSessionIds` unions both.
|
|
1094
1107
|
if (bridgeCliPid !== undefined && bridgeJsonlDir && bridgeCliCwd) {
|
|
1095
|
-
const sids = findOpenClaudeSessionIds(bridgeCliPid);
|
|
1108
|
+
const sids = findOpenClaudeSessionIds(bridgeCliPid, bridgeDataDir);
|
|
1096
1109
|
const candidates = [];
|
|
1097
1110
|
for (const sid of sids) {
|
|
1098
|
-
const path = claudeJsonlPathForSession(sid, bridgeCliCwd);
|
|
1111
|
+
const path = claudeJsonlPathForSession(sid, bridgeCliCwd, bridgeDataDir);
|
|
1099
1112
|
bridgeRememberSessionIdForPath(path);
|
|
1100
1113
|
if (existsSyncSafe(path))
|
|
1101
1114
|
candidates.push(path);
|
|
@@ -2736,19 +2749,22 @@ function stopScreenUpdates() {
|
|
|
2736
2749
|
}
|
|
2737
2750
|
// ─── PTY Management ──────────────────────────────────────────────────────────
|
|
2738
2751
|
function spawnCli(cfg) {
|
|
2739
|
-
// ── Adopt mode:
|
|
2740
|
-
|
|
2752
|
+
// ── Adopt mode: observe the user's existing pane (no attach / non-invasive) ──
|
|
2753
|
+
// tmux: pipe-pane (raw stream). zellij: dump-screen poll + action drive.
|
|
2754
|
+
if (cfg.adoptMode && (cfg.adoptTmuxTarget || cfg.adoptZellijPaneId)) {
|
|
2741
2755
|
// We mark BOTH isTmuxMode and isPipeMode: the former keeps idle/spawn
|
|
2742
|
-
// logic on the
|
|
2756
|
+
// logic on the observe track; the latter tells the WS handler to route
|
|
2743
2757
|
// updates through the shared scrollback fan-out (because there is no
|
|
2744
2758
|
// PTY-per-WS — we don't attach to anything).
|
|
2745
2759
|
isTmuxMode = true;
|
|
2746
2760
|
isPipeMode = true;
|
|
2747
2761
|
const cols = cfg.adoptPaneCols ?? PTY_COLS;
|
|
2748
2762
|
const rows = cfg.adoptPaneRows ?? PTY_ROWS;
|
|
2749
|
-
const
|
|
2750
|
-
|
|
2751
|
-
|
|
2763
|
+
const observeBe = cfg.adoptZellijPaneId
|
|
2764
|
+
? new ZellijObserveBackend(cfg.adoptZellijSession ?? '', cfg.adoptZellijPaneId, { cliPid: cfg.adoptCliPid })
|
|
2765
|
+
: new TmuxPipeBackend(cfg.adoptTmuxTarget);
|
|
2766
|
+
backend = observeBe;
|
|
2767
|
+
observeBe.spawn('', [], {
|
|
2752
2768
|
cwd: cfg.workingDir,
|
|
2753
2769
|
cols,
|
|
2754
2770
|
rows,
|
|
@@ -2756,9 +2772,9 @@ function spawnCli(cfg) {
|
|
|
2756
2772
|
});
|
|
2757
2773
|
// Seed the shared scrollback with the pane's current screen so any
|
|
2758
2774
|
// already-connected (or future) WS clients render meaningful content
|
|
2759
|
-
// immediately, instead of waiting for the next
|
|
2775
|
+
// immediately, instead of waiting for the next observe tick.
|
|
2760
2776
|
try {
|
|
2761
|
-
const initial =
|
|
2777
|
+
const initial = observeBe.captureCurrentScreen();
|
|
2762
2778
|
if (initial.length > 0)
|
|
2763
2779
|
onPtyData(initial);
|
|
2764
2780
|
}
|
|
@@ -2932,14 +2948,19 @@ function spawnCli(cfg) {
|
|
|
2932
2948
|
// the worker re-probes here. If tmux can't start a server we silently
|
|
2933
2949
|
// fall back to PTY rather than letting attach-session / new-session spam
|
|
2934
2950
|
// the daemon error log every poll cycle.
|
|
2935
|
-
let
|
|
2936
|
-
if (
|
|
2951
|
+
let effectiveBackend = cfg.backendType;
|
|
2952
|
+
if (effectiveBackend === 'tmux' && !TmuxBackend.isAvailable()) {
|
|
2937
2953
|
log('tmux backend requested but functional probe failed — falling back to PTY backend');
|
|
2938
|
-
|
|
2954
|
+
effectiveBackend = 'pty';
|
|
2939
2955
|
}
|
|
2940
|
-
|
|
2956
|
+
if (effectiveBackend === 'zellij' && !ZellijBackend.isAvailable()) {
|
|
2957
|
+
log('zellij backend requested but functional probe failed (need zellij >= 0.44) — falling back to PTY backend');
|
|
2958
|
+
effectiveBackend = 'pty';
|
|
2959
|
+
}
|
|
2960
|
+
const selectedBackend = selectSessionBackend({ sessionId: cfg.sessionId, backendType: effectiveBackend });
|
|
2941
2961
|
isTmuxMode = selectedBackend.isTmuxMode;
|
|
2942
2962
|
isPipeMode = selectedBackend.isPipeMode;
|
|
2963
|
+
isZellijMode = selectedBackend.isZellijMode;
|
|
2943
2964
|
backend = selectedBackend.backend;
|
|
2944
2965
|
const adapterSessionId = cfg.resume
|
|
2945
2966
|
? (cfg.originalSessionId ?? cfg.sessionId)
|
|
@@ -2949,9 +2970,15 @@ function spawnCli(cfg) {
|
|
|
2949
2970
|
// actually committed (rather than trusting a fixed sleep), so wire it up now.
|
|
2950
2971
|
// Codex's adapter uses ~/.codex/history.jsonl (a fixed global path) directly,
|
|
2951
2972
|
// so it needs no per-session wiring here.
|
|
2952
|
-
|
|
2973
|
+
//
|
|
2974
|
+
// `claudeDataDir` is the Claude-family marker: set for claude-code AND its
|
|
2975
|
+
// forks (Seed → `.claude-runtime`), undefined for everything else. Every
|
|
2976
|
+
// JSONL/pid/bridge gate below keys off it instead of `cliId === 'claude-code'`,
|
|
2977
|
+
// so a fork inherits the whole submit-confirm + bridge-fallback machinery.
|
|
2978
|
+
const claudeDataDir = cliAdapter.claudeDataDir;
|
|
2979
|
+
if (claudeDataDir) {
|
|
2953
2980
|
backend.claudeJsonlPath =
|
|
2954
|
-
claudeJsonlPathForSession(cfg.cliSessionId ?? adapterSessionId, cfg.workingDir);
|
|
2981
|
+
claudeJsonlPathForSession(cfg.cliSessionId ?? adapterSessionId, cfg.workingDir, claudeDataDir);
|
|
2955
2982
|
}
|
|
2956
2983
|
const args = cliAdapter.buildArgs({
|
|
2957
2984
|
sessionId: adapterSessionId,
|
|
@@ -2971,12 +2998,13 @@ function spawnCli(cfg) {
|
|
|
2971
2998
|
args.push(...extra.split(/\s+/).filter(Boolean));
|
|
2972
2999
|
// Claude Code 在 root/sudo 下会拒绝 --dangerously-skip-permissions 并立即 exit。
|
|
2973
3000
|
// botmux 必须带这个 flag(话题里没法弹交互式审批),所以为 root 自动注入
|
|
2974
|
-
// IS_SANDBOX=1 走 Claude Code
|
|
2975
|
-
|
|
3001
|
+
// IS_SANDBOX=1 走 Claude Code 的受控环境逃生舱。Seed 是 Claude Code fork,同样
|
|
3002
|
+
// 受此限制 → 按 claude 家族判断。用户显式设了就尊重不覆盖。
|
|
3003
|
+
const injectClaudeSandbox = !!claudeDataDir &&
|
|
2976
3004
|
process.getuid?.() === 0 &&
|
|
2977
3005
|
!process.env.IS_SANDBOX;
|
|
2978
3006
|
if (injectClaudeSandbox) {
|
|
2979
|
-
log('Detected root user — injecting IS_SANDBOX=1 for Claude
|
|
3007
|
+
log('Detected root user — injecting IS_SANDBOX=1 for Claude-family CLI');
|
|
2980
3008
|
}
|
|
2981
3009
|
// Claude Code 2.1.x:`--resume` 一个「空闲 >70min 且累计 >10 万 token」的会话会弹
|
|
2982
3010
|
// 交互式菜单(Resume from summary / full / Don't ask again),botmux 无法导航 →
|
|
@@ -2984,7 +3012,8 @@ function spawnCli(cfg) {
|
|
|
2984
3012
|
// 而 return null → 菜单不弹、按 full session 原样续(走 summary 会触发 /compact,
|
|
2985
3013
|
// 破坏 bridge 的会话连续性追踪)。用户显式设了就尊重。注意:该 key 必须同时进
|
|
2986
3014
|
// BOTMUX_INJECTED_ENV_KEYS 白名单,否则 tmux backend 不会把它透传进 pane。
|
|
2987
|
-
|
|
3015
|
+
// Seed 是 Claude Code fork,同样有 resume-summary 菜单 → 按 claude 家族判断。
|
|
3016
|
+
const claudeResumeTokenThreshold = claudeDataDir
|
|
2988
3017
|
? process.env.CLAUDE_CODE_RESUME_TOKEN_THRESHOLD ?? '2147483647'
|
|
2989
3018
|
: undefined;
|
|
2990
3019
|
// Predict reattach vs fresh so the log line tells the truth. When a bmx-*
|
|
@@ -2993,9 +3022,13 @@ function spawnCli(cfg) {
|
|
|
2993
3022
|
// is misleading and has cost real debugging time. (CliId-mismatch reattach
|
|
2994
3023
|
// is now blocked upstream in restoreActiveSessions / killStalePids.)
|
|
2995
3024
|
const willReattachTmux = isTmuxMode && TmuxBackend.hasSession(TmuxBackend.sessionName(cfg.sessionId));
|
|
3025
|
+
const willReattachZellij = isZellijMode && ZellijBackend.hasSession(ZellijBackend.sessionName(cfg.sessionId));
|
|
2996
3026
|
if (willReattachTmux) {
|
|
2997
3027
|
log(`Re-attaching to existing tmux session: ${TmuxBackend.sessionName(cfg.sessionId)} (requested CLI: ${cliAdapter.resolvedBin})`);
|
|
2998
3028
|
}
|
|
3029
|
+
else if (willReattachZellij) {
|
|
3030
|
+
log(`Re-attaching to existing zellij session: ${ZellijBackend.sessionName(cfg.sessionId)} (requested CLI: ${cliAdapter.resolvedBin})`);
|
|
3031
|
+
}
|
|
2999
3032
|
else {
|
|
3000
3033
|
log(`Spawning fresh CLI: ${cliAdapter.resolvedBin} ${args.join(' ')} (cwd: ${cfg.workingDir})`);
|
|
3001
3034
|
}
|
|
@@ -3021,6 +3054,12 @@ function spawnCli(cfg) {
|
|
|
3021
3054
|
childEnv.IS_SANDBOX = '1';
|
|
3022
3055
|
if (claudeResumeTokenThreshold)
|
|
3023
3056
|
childEnv.CLAUDE_CODE_RESUME_TOKEN_THRESHOLD = claudeResumeTokenThreshold;
|
|
3057
|
+
// Adapter-supplied env: points Claude-family forks at their data root (Seed's
|
|
3058
|
+
// CLAUDE_CONFIG_DIR → `.claude-runtime`). Keys here are also in the tmux
|
|
3059
|
+
// passthrough whitelist (BOTMUX_INJECTED_ENV_KEYS) so the tmux backend forwards
|
|
3060
|
+
// them past the server's global env.
|
|
3061
|
+
if (cliAdapter.spawnEnv)
|
|
3062
|
+
Object.assign(childEnv, cliAdapter.spawnEnv);
|
|
3024
3063
|
backend.spawn(cliAdapter.resolvedBin, args, {
|
|
3025
3064
|
cwd: cfg.workingDir,
|
|
3026
3065
|
cols: PTY_COLS,
|
|
@@ -3051,10 +3090,47 @@ function spawnCli(cfg) {
|
|
|
3051
3090
|
// "no rotation at all". The pinned claudeJsonlPath above is still the
|
|
3052
3091
|
// initial guess; the resolver corrects it on first write when Claude was
|
|
3053
3092
|
// started with `--resume`.
|
|
3054
|
-
if (
|
|
3093
|
+
if (claudeDataDir && cliPid) {
|
|
3055
3094
|
backend.cliPid = cliPid;
|
|
3056
3095
|
backend.cliCwd = cfg.workingDir;
|
|
3057
3096
|
}
|
|
3097
|
+
// Async pid fallback: tmux/pty resolve the CLI pid synchronously above, but
|
|
3098
|
+
// zellij's CLI subprocess starts AFTER spawn() returns (the zellij server
|
|
3099
|
+
// forks the pane asynchronously), so getChildPid() is null right now. Without
|
|
3100
|
+
// the marker, an in-CLI `botmux send` walks ancestor pids, finds no match,
|
|
3101
|
+
// and reports "无法推断 session-id". Retry briefly (non-blocking — a sync wait
|
|
3102
|
+
// would lose zellij's initial render since node-pty doesn't buffer pre-listener
|
|
3103
|
+
// output) until the pid appears, then write the marker + wire claude-family pid.
|
|
3104
|
+
if (!cliPid) {
|
|
3105
|
+
let attempts = 0;
|
|
3106
|
+
const resolveCliPidLate = () => {
|
|
3107
|
+
if (!backend)
|
|
3108
|
+
return;
|
|
3109
|
+
const pid = backend.getChildPid?.();
|
|
3110
|
+
if (pid) {
|
|
3111
|
+
if (process.env.SESSION_DATA_DIR && !cliPidMarker) {
|
|
3112
|
+
try {
|
|
3113
|
+
const markersDir = join(process.env.SESSION_DATA_DIR, '.botmux-cli-pids');
|
|
3114
|
+
mkdirSync(markersDir, { recursive: true });
|
|
3115
|
+
cliPidMarker = join(markersDir, String(pid));
|
|
3116
|
+
writeFileSync(cliPidMarker, cfg.sessionId);
|
|
3117
|
+
log(`CLI PID marker written (async): ${pid}`);
|
|
3118
|
+
}
|
|
3119
|
+
catch (err) {
|
|
3120
|
+
log(`Failed to write CLI PID marker (async): ${err.message}`);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
if (claudeDataDir) {
|
|
3124
|
+
backend.cliPid = pid;
|
|
3125
|
+
backend.cliCwd = cfg.workingDir;
|
|
3126
|
+
}
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
if (++attempts < 25)
|
|
3130
|
+
setTimeout(resolveCliPidLate, 120); // ~3s budget
|
|
3131
|
+
};
|
|
3132
|
+
setTimeout(resolveCliPidLate, 120);
|
|
3133
|
+
}
|
|
3058
3134
|
// On tmux re-attach, keep awaitingFirstPrompt = true so screen updates are
|
|
3059
3135
|
// suppressed until the idle detector fires markNewTurn() — this prevents the
|
|
3060
3136
|
// full tmux scrollback history from leaking into the streaming card.
|
|
@@ -3066,13 +3142,14 @@ function spawnCli(cfg) {
|
|
|
3066
3142
|
// the file Claude creates on first submit isn't absorbed as history,
|
|
3067
3143
|
// and baseline-existing on resume so prior-run turns ARE absorbed (we
|
|
3068
3144
|
// don't want to re-emit yesterday's conversation as fresh turns).
|
|
3069
|
-
if (
|
|
3145
|
+
if (claudeDataDir && adapterSessionId) {
|
|
3070
3146
|
const claudeBridgeSessionId = cfg.cliSessionId ?? adapterSessionId;
|
|
3071
|
-
const claudeJsonl = claudeJsonlPathForSession(claudeBridgeSessionId, cfg.workingDir);
|
|
3147
|
+
const claudeJsonl = claudeJsonlPathForSession(claudeBridgeSessionId, cfg.workingDir, claudeDataDir);
|
|
3072
3148
|
startBridgeWatcher(claudeJsonl, {
|
|
3073
3149
|
cliPid: cliPid ?? undefined,
|
|
3074
3150
|
cliCwd: cfg.workingDir,
|
|
3075
3151
|
mode: cfg.resume ? 'baseline-existing' : 'fresh-empty',
|
|
3152
|
+
dataDir: claudeDataDir,
|
|
3076
3153
|
});
|
|
3077
3154
|
}
|
|
3078
3155
|
// Structured transcript bridge fallback: if the model finishes without
|
|
@@ -3308,8 +3385,20 @@ function startWebServer(host, preferredPort) {
|
|
|
3308
3385
|
// stream, which scrolls stale Ink redraw/spinner frames into scrollback
|
|
3309
3386
|
// at any size mismatch and produces the stacked-footer history garble.
|
|
3310
3387
|
// See chooseWebTerminalSeed for the full rationale.
|
|
3388
|
+
// Adopt observes a pane we CANNOT resize (tmux adopt has
|
|
3389
|
+
// ownsSession=false so resize() is a no-op; zellij drives via
|
|
3390
|
+
// dump-screen). The client's FitAddon sizes its xterm to the browser,
|
|
3391
|
+
// but the snapshot lines carry the PANE's width — any mismatch wraps the
|
|
3392
|
+
// full-width TUI box lines and garbles the layout (the misalignment 申晗
|
|
3393
|
+
// saw). Pin the client xterm to the pane's fixed size via a botmux OSC
|
|
3394
|
+
// (sent BEFORE the seed so the client resizes before rendering it).
|
|
3395
|
+
if (lastInitConfig?.adoptMode && isObserveBackend(backend)) {
|
|
3396
|
+
const sz = backend.getPaneSize();
|
|
3397
|
+
if (sz && sz.cols > 0 && sz.rows > 0)
|
|
3398
|
+
ws.send(`\x1b]1989;${sz.cols};${sz.rows}\x07`);
|
|
3399
|
+
}
|
|
3311
3400
|
const seed = chooseWebTerminalSeed({
|
|
3312
|
-
canCapture: isPipeMode && backend
|
|
3401
|
+
canCapture: isPipeMode && isObserveBackend(backend),
|
|
3313
3402
|
capture: () => backend.captureCurrentScreen(),
|
|
3314
3403
|
scrollback,
|
|
3315
3404
|
onError: log,
|
|
@@ -3502,8 +3591,9 @@ term.onData(function(d){
|
|
|
3502
3591
|
}
|
|
3503
3592
|
if(ws_&&ws_.readyState===1)ws_.send(JSON.stringify({type:'input',data:d}));
|
|
3504
3593
|
});
|
|
3594
|
+
var fixedSize=false;
|
|
3505
3595
|
function sendResize(){if(ws_&&ws_.readyState===1)ws_.send(JSON.stringify({type:'resize',cols:term.cols,rows:term.rows}))}
|
|
3506
|
-
window.addEventListener('resize',function(){fit.fit()
|
|
3596
|
+
window.addEventListener('resize',function(){if(!fixedSize){fit.fit()}sendResize()});
|
|
3507
3597
|
(function connect(){
|
|
3508
3598
|
var t=new URLSearchParams(location.search).get('token')||'';
|
|
3509
3599
|
// Derive base from the current path so the WS connects to the same prefix the
|
|
@@ -3516,6 +3606,10 @@ window.addEventListener('resize',function(){fit.fit();sendResize()});
|
|
|
3516
3606
|
ws.onopen=function(){el.textContent='connected';el.className='ok';sendResize()};
|
|
3517
3607
|
ws.onmessage=function(e){
|
|
3518
3608
|
var data=typeof e.data==='string'?e.data:new TextDecoder().decode(e.data);
|
|
3609
|
+
// botmux OSC 1989: pin the xterm to the adopted pane's fixed size (the pane
|
|
3610
|
+
// can't be resized, so FitAddon-to-browser would wrap the snapshot lines).
|
|
3611
|
+
var _fs=data.match(/\\x1b\\]1989;(\\d+);(\\d+)\\x07/);
|
|
3612
|
+
if(_fs){fixedSize=true;var _c=+_fs[1],_r=+_fs[2];if(_c>0&&_r>0){try{term.resize(_c,_r)}catch(ex){}}data=data.replace(_fs[0],'')}
|
|
3519
3613
|
// Intercept OSC 52 clipboard sequence from tmux (set-clipboard on)
|
|
3520
3614
|
var m=data.match(/\\x1b\\]52;[^;]*;([A-Za-z0-9+/=]+)(?:\\x07|\\x1b\\\\)/);
|
|
3521
3615
|
if(m){try{_clipBuf=new TextDecoder().decode(Uint8Array.from(atob(m[1]),function(c){return c.charCodeAt(0)}));_doCopy(_clipBuf);_showCopied()}catch(ex){}}
|