botmux 2.52.0 → 2.54.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 -269
- package/README.md +21 -300
- 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 +1 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +79 -49
- 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/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/pending-response.d.ts +31 -0
- package/dist/core/pending-response.d.ts.map +1 -0
- package/dist/core/pending-response.js +87 -0
- package/dist/core/pending-response.js.map +1 -0
- 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 +59 -19
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +8 -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 +2 -2
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +89 -15
- 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 +276 -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.map +1 -1
- package/dist/daemon.js +70 -4
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/web/i18n.js +1 -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 +3 -3
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +12 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +13 -1
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +7 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +92 -2
- 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 +78 -6
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +8 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +28 -0
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/grant-command.d.ts +13 -0
- package/dist/im/lark/grant-command.d.ts.map +1 -1
- package/dist/im/lark/grant-command.js +93 -0
- package/dist/im/lark/grant-command.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/pending-response-transaction-store.d.ts +12 -0
- package/dist/services/pending-response-transaction-store.d.ts.map +1 -0
- package/dist/services/pending-response-transaction-store.js +52 -0
- package/dist/services/pending-response-transaction-store.js.map +1 -0
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +15 -1
- package/dist/services/session-store.js.map +1 -1
- 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 +14 -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 +235 -32
- 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/dist/workflows/events/payloads.d.ts +20 -20
- package/dist/workflows/events/schema.d.ts +48 -48
- package/package.json +1 -1
package/dist/worker.js
CHANGED
|
@@ -32,10 +32,14 @@ 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, ZELLIJ_CONFIG_KDL } from './adapters/backend/zellij-backend.js';
|
|
40
|
+
import { ZellijObserveBackend } from './adapters/backend/zellij-observe-backend.js';
|
|
41
|
+
import { zellijEnv } from './setup/ensure-zellij.js';
|
|
42
|
+
import { isObserveBackend } from './adapters/backend/types.js';
|
|
39
43
|
import { selectSessionBackend } from './adapters/backend/session-backend-selector.js';
|
|
40
44
|
import { tmuxEnv } from './setup/ensure-tmux.js';
|
|
41
45
|
import { IdleDetector } from './utils/idle-detector.js';
|
|
@@ -60,16 +64,35 @@ let isTmuxMode = false;
|
|
|
60
64
|
* web-terminal updates flow through the shared scrollback fan-out instead
|
|
61
65
|
* of per-WS attach-session PTYs. Set in spawnCli's adopt branch. */
|
|
62
66
|
let isPipeMode = false;
|
|
67
|
+
/** pty-under-zellij backend (BACKEND_TYPE=zellij). Behaves like the non-tmux
|
|
68
|
+
* pty path for the worker (renderer screenshots, relay web terminal) but owns
|
|
69
|
+
* a persistent zellij session that survives daemon restart. */
|
|
70
|
+
let isZellijMode = false;
|
|
63
71
|
let httpServer = null;
|
|
64
72
|
let wss = null;
|
|
65
73
|
const wsClients = new Set();
|
|
66
74
|
const authedClients = new WeakSet();
|
|
67
|
-
/** Per-WS-client tmux attach PTYs
|
|
75
|
+
/** Per-WS-client tmux/zellij attach PTYs. */
|
|
68
76
|
const clientPtys = new Map();
|
|
69
77
|
const writeToken = randomBytes(16).toString('hex');
|
|
78
|
+
/** Lazily-written locked-mode zellij config for per-WS web-terminal attach
|
|
79
|
+
* clients: cleared keybinds + locked mode so every keystroke passes straight
|
|
80
|
+
* to the focused (codex) pane, never intercepted as a zellij shortcut. */
|
|
81
|
+
let zellijAttachCfgPath = null;
|
|
82
|
+
function ensureZellijAttachConfig() {
|
|
83
|
+
if (zellijAttachCfgPath)
|
|
84
|
+
return zellijAttachCfgPath;
|
|
85
|
+
const p = join(process.env.SESSION_DATA_DIR ?? '/tmp', '.zellij-web-attach.kdl');
|
|
86
|
+
try {
|
|
87
|
+
writeFileSync(p, ZELLIJ_CONFIG_KDL);
|
|
88
|
+
}
|
|
89
|
+
catch { /* best effort */ }
|
|
90
|
+
zellijAttachCfgPath = p;
|
|
91
|
+
return p;
|
|
92
|
+
}
|
|
70
93
|
let sessionId = '';
|
|
71
94
|
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' };
|
|
95
|
+
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
96
|
function cliName() { return CLI_DISPLAY_NAMES[lastInitConfig?.cliId ?? ''] ?? 'CLI'; }
|
|
74
97
|
let isPromptReady = false;
|
|
75
98
|
/** Mutex for async flushPending — prevents concurrent flush loops. */
|
|
@@ -147,6 +170,11 @@ let bridgeJsonlDir;
|
|
|
147
170
|
* follow the new jsonl. */
|
|
148
171
|
let bridgeCliPid;
|
|
149
172
|
let bridgeCliCwd;
|
|
173
|
+
/** Claude-family data root the bridge resolves JSONL / pid-state / tasks
|
|
174
|
+
* against. `~/.claude` for Claude Code; Seed CLI's `.claude-runtime`. Set at
|
|
175
|
+
* bridge start (from the adapter's claudeDataDir); defaults to `~/.claude` so
|
|
176
|
+
* the adopt path and any non-seed caller behave exactly as before. */
|
|
177
|
+
let bridgeDataDir = DEFAULT_CLAUDE_DATA_DIR;
|
|
150
178
|
/** Last sessionId we observed via the pid resolver — used to detect
|
|
151
179
|
* rotations cheaply (string compare instead of stat()ing every jsonl). */
|
|
152
180
|
let bridgeObservedCliSessionId;
|
|
@@ -425,7 +453,7 @@ function bridgeAbsorbBaseline() {
|
|
|
425
453
|
function bridgeMarkStalePidStateForAcceptedSid(acceptedSid) {
|
|
426
454
|
if (bridgeCliPid === undefined || bridgeCliCwd === undefined)
|
|
427
455
|
return;
|
|
428
|
-
const pidResolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd);
|
|
456
|
+
const pidResolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd, bridgeDataDir);
|
|
429
457
|
if (pidResolved && pidResolved.cliSessionId !== acceptedSid) {
|
|
430
458
|
bridgeStalePidStateSessionId = pidResolved.cliSessionId;
|
|
431
459
|
}
|
|
@@ -888,7 +916,7 @@ function maybeFollowQuietRotation() {
|
|
|
888
916
|
function maybeFollowSessionRotationViaPid() {
|
|
889
917
|
if (!bridgeCliPid || !bridgeCliCwd)
|
|
890
918
|
return 'unavailable';
|
|
891
|
-
const resolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd);
|
|
919
|
+
const resolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd, bridgeDataDir);
|
|
892
920
|
if (!resolved)
|
|
893
921
|
return 'unavailable';
|
|
894
922
|
if (bridgeObservedCliSessionId !== resolved.cliSessionId) {
|
|
@@ -1060,6 +1088,7 @@ function startBridgeWatcher(jsonlPath, opts) {
|
|
|
1060
1088
|
bridgeJsonlDir = dirname(jsonlPath);
|
|
1061
1089
|
bridgeCliPid = opts?.cliPid;
|
|
1062
1090
|
bridgeCliCwd = opts?.cliCwd;
|
|
1091
|
+
bridgeDataDir = opts?.dataDir ?? DEFAULT_CLAUDE_DATA_DIR;
|
|
1063
1092
|
const mode = opts?.mode ?? 'baseline-existing';
|
|
1064
1093
|
// Pid-state record ranks above the path the adopt scan computed. If
|
|
1065
1094
|
// Claude was launched with `--resume` (or the adopt scan picked a
|
|
@@ -1067,7 +1096,7 @@ function startBridgeWatcher(jsonlPath, opts) {
|
|
|
1067
1096
|
// and we swap to it before baseline so we don't waste a baseline on
|
|
1068
1097
|
// a frozen file.
|
|
1069
1098
|
if (bridgeCliPid && bridgeCliCwd) {
|
|
1070
|
-
const resolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd);
|
|
1099
|
+
const resolved = resolveJsonlFromPid(bridgeCliPid, bridgeCliCwd, bridgeDataDir);
|
|
1071
1100
|
if (resolved) {
|
|
1072
1101
|
bridgeObservedCliSessionId = resolved.cliSessionId;
|
|
1073
1102
|
bridgeRememberSessionIdForPath(resolved.path);
|
|
@@ -1092,10 +1121,10 @@ function startBridgeWatcher(jsonlPath, opts) {
|
|
|
1092
1121
|
// continuously for the active session, so this catches the rotation
|
|
1093
1122
|
// even between writes). `findOpenClaudeSessionIds` unions both.
|
|
1094
1123
|
if (bridgeCliPid !== undefined && bridgeJsonlDir && bridgeCliCwd) {
|
|
1095
|
-
const sids = findOpenClaudeSessionIds(bridgeCliPid);
|
|
1124
|
+
const sids = findOpenClaudeSessionIds(bridgeCliPid, bridgeDataDir);
|
|
1096
1125
|
const candidates = [];
|
|
1097
1126
|
for (const sid of sids) {
|
|
1098
|
-
const path = claudeJsonlPathForSession(sid, bridgeCliCwd);
|
|
1127
|
+
const path = claudeJsonlPathForSession(sid, bridgeCliCwd, bridgeDataDir);
|
|
1099
1128
|
bridgeRememberSessionIdForPath(path);
|
|
1100
1129
|
if (existsSyncSafe(path))
|
|
1101
1130
|
candidates.push(path);
|
|
@@ -2736,19 +2765,22 @@ function stopScreenUpdates() {
|
|
|
2736
2765
|
}
|
|
2737
2766
|
// ─── PTY Management ──────────────────────────────────────────────────────────
|
|
2738
2767
|
function spawnCli(cfg) {
|
|
2739
|
-
// ── Adopt mode:
|
|
2740
|
-
|
|
2768
|
+
// ── Adopt mode: observe the user's existing pane (no attach / non-invasive) ──
|
|
2769
|
+
// tmux: pipe-pane (raw stream). zellij: dump-screen poll + action drive.
|
|
2770
|
+
if (cfg.adoptMode && (cfg.adoptTmuxTarget || cfg.adoptZellijPaneId)) {
|
|
2741
2771
|
// We mark BOTH isTmuxMode and isPipeMode: the former keeps idle/spawn
|
|
2742
|
-
// logic on the
|
|
2772
|
+
// logic on the observe track; the latter tells the WS handler to route
|
|
2743
2773
|
// updates through the shared scrollback fan-out (because there is no
|
|
2744
2774
|
// PTY-per-WS — we don't attach to anything).
|
|
2745
2775
|
isTmuxMode = true;
|
|
2746
2776
|
isPipeMode = true;
|
|
2747
2777
|
const cols = cfg.adoptPaneCols ?? PTY_COLS;
|
|
2748
2778
|
const rows = cfg.adoptPaneRows ?? PTY_ROWS;
|
|
2749
|
-
const
|
|
2750
|
-
|
|
2751
|
-
|
|
2779
|
+
const observeBe = cfg.adoptZellijPaneId
|
|
2780
|
+
? new ZellijObserveBackend(cfg.adoptZellijSession ?? '', cfg.adoptZellijPaneId, { cliPid: cfg.adoptCliPid })
|
|
2781
|
+
: new TmuxPipeBackend(cfg.adoptTmuxTarget);
|
|
2782
|
+
backend = observeBe;
|
|
2783
|
+
observeBe.spawn('', [], {
|
|
2752
2784
|
cwd: cfg.workingDir,
|
|
2753
2785
|
cols,
|
|
2754
2786
|
rows,
|
|
@@ -2756,9 +2788,9 @@ function spawnCli(cfg) {
|
|
|
2756
2788
|
});
|
|
2757
2789
|
// Seed the shared scrollback with the pane's current screen so any
|
|
2758
2790
|
// already-connected (or future) WS clients render meaningful content
|
|
2759
|
-
// immediately, instead of waiting for the next
|
|
2791
|
+
// immediately, instead of waiting for the next observe tick.
|
|
2760
2792
|
try {
|
|
2761
|
-
const initial =
|
|
2793
|
+
const initial = observeBe.captureCurrentScreen();
|
|
2762
2794
|
if (initial.length > 0)
|
|
2763
2795
|
onPtyData(initial);
|
|
2764
2796
|
}
|
|
@@ -2932,14 +2964,19 @@ function spawnCli(cfg) {
|
|
|
2932
2964
|
// the worker re-probes here. If tmux can't start a server we silently
|
|
2933
2965
|
// fall back to PTY rather than letting attach-session / new-session spam
|
|
2934
2966
|
// the daemon error log every poll cycle.
|
|
2935
|
-
let
|
|
2936
|
-
if (
|
|
2967
|
+
let effectiveBackend = cfg.backendType;
|
|
2968
|
+
if (effectiveBackend === 'tmux' && !TmuxBackend.isAvailable()) {
|
|
2937
2969
|
log('tmux backend requested but functional probe failed — falling back to PTY backend');
|
|
2938
|
-
|
|
2970
|
+
effectiveBackend = 'pty';
|
|
2939
2971
|
}
|
|
2940
|
-
|
|
2972
|
+
if (effectiveBackend === 'zellij' && !ZellijBackend.isAvailable()) {
|
|
2973
|
+
log('zellij backend requested but functional probe failed (need zellij >= 0.44) — falling back to PTY backend');
|
|
2974
|
+
effectiveBackend = 'pty';
|
|
2975
|
+
}
|
|
2976
|
+
const selectedBackend = selectSessionBackend({ sessionId: cfg.sessionId, backendType: effectiveBackend });
|
|
2941
2977
|
isTmuxMode = selectedBackend.isTmuxMode;
|
|
2942
2978
|
isPipeMode = selectedBackend.isPipeMode;
|
|
2979
|
+
isZellijMode = selectedBackend.isZellijMode;
|
|
2943
2980
|
backend = selectedBackend.backend;
|
|
2944
2981
|
const adapterSessionId = cfg.resume
|
|
2945
2982
|
? (cfg.originalSessionId ?? cfg.sessionId)
|
|
@@ -2949,9 +2986,15 @@ function spawnCli(cfg) {
|
|
|
2949
2986
|
// actually committed (rather than trusting a fixed sleep), so wire it up now.
|
|
2950
2987
|
// Codex's adapter uses ~/.codex/history.jsonl (a fixed global path) directly,
|
|
2951
2988
|
// so it needs no per-session wiring here.
|
|
2952
|
-
|
|
2989
|
+
//
|
|
2990
|
+
// `claudeDataDir` is the Claude-family marker: set for claude-code AND its
|
|
2991
|
+
// forks (Seed → `.claude-runtime`), undefined for everything else. Every
|
|
2992
|
+
// JSONL/pid/bridge gate below keys off it instead of `cliId === 'claude-code'`,
|
|
2993
|
+
// so a fork inherits the whole submit-confirm + bridge-fallback machinery.
|
|
2994
|
+
const claudeDataDir = cliAdapter.claudeDataDir;
|
|
2995
|
+
if (claudeDataDir) {
|
|
2953
2996
|
backend.claudeJsonlPath =
|
|
2954
|
-
claudeJsonlPathForSession(cfg.cliSessionId ?? adapterSessionId, cfg.workingDir);
|
|
2997
|
+
claudeJsonlPathForSession(cfg.cliSessionId ?? adapterSessionId, cfg.workingDir, claudeDataDir);
|
|
2955
2998
|
}
|
|
2956
2999
|
const args = cliAdapter.buildArgs({
|
|
2957
3000
|
sessionId: adapterSessionId,
|
|
@@ -2971,12 +3014,13 @@ function spawnCli(cfg) {
|
|
|
2971
3014
|
args.push(...extra.split(/\s+/).filter(Boolean));
|
|
2972
3015
|
// Claude Code 在 root/sudo 下会拒绝 --dangerously-skip-permissions 并立即 exit。
|
|
2973
3016
|
// botmux 必须带这个 flag(话题里没法弹交互式审批),所以为 root 自动注入
|
|
2974
|
-
// IS_SANDBOX=1 走 Claude Code
|
|
2975
|
-
|
|
3017
|
+
// IS_SANDBOX=1 走 Claude Code 的受控环境逃生舱。Seed 是 Claude Code fork,同样
|
|
3018
|
+
// 受此限制 → 按 claude 家族判断。用户显式设了就尊重不覆盖。
|
|
3019
|
+
const injectClaudeSandbox = !!claudeDataDir &&
|
|
2976
3020
|
process.getuid?.() === 0 &&
|
|
2977
3021
|
!process.env.IS_SANDBOX;
|
|
2978
3022
|
if (injectClaudeSandbox) {
|
|
2979
|
-
log('Detected root user — injecting IS_SANDBOX=1 for Claude
|
|
3023
|
+
log('Detected root user — injecting IS_SANDBOX=1 for Claude-family CLI');
|
|
2980
3024
|
}
|
|
2981
3025
|
// Claude Code 2.1.x:`--resume` 一个「空闲 >70min 且累计 >10 万 token」的会话会弹
|
|
2982
3026
|
// 交互式菜单(Resume from summary / full / Don't ask again),botmux 无法导航 →
|
|
@@ -2984,7 +3028,8 @@ function spawnCli(cfg) {
|
|
|
2984
3028
|
// 而 return null → 菜单不弹、按 full session 原样续(走 summary 会触发 /compact,
|
|
2985
3029
|
// 破坏 bridge 的会话连续性追踪)。用户显式设了就尊重。注意:该 key 必须同时进
|
|
2986
3030
|
// BOTMUX_INJECTED_ENV_KEYS 白名单,否则 tmux backend 不会把它透传进 pane。
|
|
2987
|
-
|
|
3031
|
+
// Seed 是 Claude Code fork,同样有 resume-summary 菜单 → 按 claude 家族判断。
|
|
3032
|
+
const claudeResumeTokenThreshold = claudeDataDir
|
|
2988
3033
|
? process.env.CLAUDE_CODE_RESUME_TOKEN_THRESHOLD ?? '2147483647'
|
|
2989
3034
|
: undefined;
|
|
2990
3035
|
// Predict reattach vs fresh so the log line tells the truth. When a bmx-*
|
|
@@ -2993,9 +3038,13 @@ function spawnCli(cfg) {
|
|
|
2993
3038
|
// is misleading and has cost real debugging time. (CliId-mismatch reattach
|
|
2994
3039
|
// is now blocked upstream in restoreActiveSessions / killStalePids.)
|
|
2995
3040
|
const willReattachTmux = isTmuxMode && TmuxBackend.hasSession(TmuxBackend.sessionName(cfg.sessionId));
|
|
3041
|
+
const willReattachZellij = isZellijMode && ZellijBackend.hasSession(ZellijBackend.sessionName(cfg.sessionId));
|
|
2996
3042
|
if (willReattachTmux) {
|
|
2997
3043
|
log(`Re-attaching to existing tmux session: ${TmuxBackend.sessionName(cfg.sessionId)} (requested CLI: ${cliAdapter.resolvedBin})`);
|
|
2998
3044
|
}
|
|
3045
|
+
else if (willReattachZellij) {
|
|
3046
|
+
log(`Re-attaching to existing zellij session: ${ZellijBackend.sessionName(cfg.sessionId)} (requested CLI: ${cliAdapter.resolvedBin})`);
|
|
3047
|
+
}
|
|
2999
3048
|
else {
|
|
3000
3049
|
log(`Spawning fresh CLI: ${cliAdapter.resolvedBin} ${args.join(' ')} (cwd: ${cfg.workingDir})`);
|
|
3001
3050
|
}
|
|
@@ -3021,6 +3070,12 @@ function spawnCli(cfg) {
|
|
|
3021
3070
|
childEnv.IS_SANDBOX = '1';
|
|
3022
3071
|
if (claudeResumeTokenThreshold)
|
|
3023
3072
|
childEnv.CLAUDE_CODE_RESUME_TOKEN_THRESHOLD = claudeResumeTokenThreshold;
|
|
3073
|
+
// Adapter-supplied env: points Claude-family forks at their data root (Seed's
|
|
3074
|
+
// CLAUDE_CONFIG_DIR → `.claude-runtime`). Keys here are also in the tmux
|
|
3075
|
+
// passthrough whitelist (BOTMUX_INJECTED_ENV_KEYS) so the tmux backend forwards
|
|
3076
|
+
// them past the server's global env.
|
|
3077
|
+
if (cliAdapter.spawnEnv)
|
|
3078
|
+
Object.assign(childEnv, cliAdapter.spawnEnv);
|
|
3024
3079
|
backend.spawn(cliAdapter.resolvedBin, args, {
|
|
3025
3080
|
cwd: cfg.workingDir,
|
|
3026
3081
|
cols: PTY_COLS,
|
|
@@ -3051,10 +3106,47 @@ function spawnCli(cfg) {
|
|
|
3051
3106
|
// "no rotation at all". The pinned claudeJsonlPath above is still the
|
|
3052
3107
|
// initial guess; the resolver corrects it on first write when Claude was
|
|
3053
3108
|
// started with `--resume`.
|
|
3054
|
-
if (
|
|
3109
|
+
if (claudeDataDir && cliPid) {
|
|
3055
3110
|
backend.cliPid = cliPid;
|
|
3056
3111
|
backend.cliCwd = cfg.workingDir;
|
|
3057
3112
|
}
|
|
3113
|
+
// Async pid fallback: tmux/pty resolve the CLI pid synchronously above, but
|
|
3114
|
+
// zellij's CLI subprocess starts AFTER spawn() returns (the zellij server
|
|
3115
|
+
// forks the pane asynchronously), so getChildPid() is null right now. Without
|
|
3116
|
+
// the marker, an in-CLI `botmux send` walks ancestor pids, finds no match,
|
|
3117
|
+
// and reports "无法推断 session-id". Retry briefly (non-blocking — a sync wait
|
|
3118
|
+
// would lose zellij's initial render since node-pty doesn't buffer pre-listener
|
|
3119
|
+
// output) until the pid appears, then write the marker + wire claude-family pid.
|
|
3120
|
+
if (!cliPid) {
|
|
3121
|
+
let attempts = 0;
|
|
3122
|
+
const resolveCliPidLate = () => {
|
|
3123
|
+
if (!backend)
|
|
3124
|
+
return;
|
|
3125
|
+
const pid = backend.getChildPid?.();
|
|
3126
|
+
if (pid) {
|
|
3127
|
+
if (process.env.SESSION_DATA_DIR && !cliPidMarker) {
|
|
3128
|
+
try {
|
|
3129
|
+
const markersDir = join(process.env.SESSION_DATA_DIR, '.botmux-cli-pids');
|
|
3130
|
+
mkdirSync(markersDir, { recursive: true });
|
|
3131
|
+
cliPidMarker = join(markersDir, String(pid));
|
|
3132
|
+
writeFileSync(cliPidMarker, cfg.sessionId);
|
|
3133
|
+
log(`CLI PID marker written (async): ${pid}`);
|
|
3134
|
+
}
|
|
3135
|
+
catch (err) {
|
|
3136
|
+
log(`Failed to write CLI PID marker (async): ${err.message}`);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
if (claudeDataDir) {
|
|
3140
|
+
backend.cliPid = pid;
|
|
3141
|
+
backend.cliCwd = cfg.workingDir;
|
|
3142
|
+
}
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
if (++attempts < 25)
|
|
3146
|
+
setTimeout(resolveCliPidLate, 120); // ~3s budget
|
|
3147
|
+
};
|
|
3148
|
+
setTimeout(resolveCliPidLate, 120);
|
|
3149
|
+
}
|
|
3058
3150
|
// On tmux re-attach, keep awaitingFirstPrompt = true so screen updates are
|
|
3059
3151
|
// suppressed until the idle detector fires markNewTurn() — this prevents the
|
|
3060
3152
|
// full tmux scrollback history from leaking into the streaming card.
|
|
@@ -3066,13 +3158,14 @@ function spawnCli(cfg) {
|
|
|
3066
3158
|
// the file Claude creates on first submit isn't absorbed as history,
|
|
3067
3159
|
// and baseline-existing on resume so prior-run turns ARE absorbed (we
|
|
3068
3160
|
// don't want to re-emit yesterday's conversation as fresh turns).
|
|
3069
|
-
if (
|
|
3161
|
+
if (claudeDataDir && adapterSessionId) {
|
|
3070
3162
|
const claudeBridgeSessionId = cfg.cliSessionId ?? adapterSessionId;
|
|
3071
|
-
const claudeJsonl = claudeJsonlPathForSession(claudeBridgeSessionId, cfg.workingDir);
|
|
3163
|
+
const claudeJsonl = claudeJsonlPathForSession(claudeBridgeSessionId, cfg.workingDir, claudeDataDir);
|
|
3072
3164
|
startBridgeWatcher(claudeJsonl, {
|
|
3073
3165
|
cliPid: cliPid ?? undefined,
|
|
3074
3166
|
cliCwd: cfg.workingDir,
|
|
3075
3167
|
mode: cfg.resume ? 'baseline-existing' : 'fresh-empty',
|
|
3168
|
+
dataDir: claudeDataDir,
|
|
3076
3169
|
});
|
|
3077
3170
|
}
|
|
3078
3171
|
// Structured transcript bridge fallback: if the model finishes without
|
|
@@ -3301,6 +3394,84 @@ function startWebServer(host, preferredPort) {
|
|
|
3301
3394
|
}
|
|
3302
3395
|
});
|
|
3303
3396
|
}
|
|
3397
|
+
else if (lastInitConfig?.adoptMode && lastInitConfig?.adoptZellijPaneId) {
|
|
3398
|
+
// ── Zellij-adopt per-WS attach ──
|
|
3399
|
+
// Each WS client gets its own `zellij attach` PTY sized to the browser.
|
|
3400
|
+
// zellij sizes the (shared) pane to the SMALLEST attached client, so
|
|
3401
|
+
// when the user's terminal is detached the web client governs the size
|
|
3402
|
+
// → fully browser-responsive (申晗's insight, verified), never resizing
|
|
3403
|
+
// the user's terminal beyond min(theirs, browser). Locked-mode config
|
|
3404
|
+
// (cleared keybinds) makes every keystroke reach the codex pane instead
|
|
3405
|
+
// of being swallowed as a zellij shortcut. Bonus: raw byte stream — none
|
|
3406
|
+
// of the dump-screen snapshot / \r\n / fixed-width machinery the relay
|
|
3407
|
+
// needs. (The Lark screenshot card still uses the dump-screen
|
|
3408
|
+
// ObserveBackend; unaffected.) Deferred until first resize, same as tmux.
|
|
3409
|
+
const zSession = lastInitConfig.adoptZellijSession ?? '';
|
|
3410
|
+
const cfgPath = ensureZellijAttachConfig();
|
|
3411
|
+
let cp = null;
|
|
3412
|
+
const pendingInput = [];
|
|
3413
|
+
const startAttach = (cols, rows) => {
|
|
3414
|
+
if (cp)
|
|
3415
|
+
return;
|
|
3416
|
+
cp = pty.spawn('zellij', ['--config', cfgPath, 'attach', zSession], {
|
|
3417
|
+
name: 'xterm-256color',
|
|
3418
|
+
cols,
|
|
3419
|
+
rows,
|
|
3420
|
+
env: zellijEnv(),
|
|
3421
|
+
});
|
|
3422
|
+
clientPtys.set(ws, cp);
|
|
3423
|
+
cp.onData((d) => {
|
|
3424
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
3425
|
+
ws.send(d);
|
|
3426
|
+
});
|
|
3427
|
+
cp.onExit(() => {
|
|
3428
|
+
clientPtys.delete(ws);
|
|
3429
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
3430
|
+
ws.close();
|
|
3431
|
+
});
|
|
3432
|
+
for (const data of pendingInput)
|
|
3433
|
+
cp.write(data);
|
|
3434
|
+
pendingInput.length = 0;
|
|
3435
|
+
};
|
|
3436
|
+
const spawnTimer = setTimeout(() => startAttach(150, 40), 500);
|
|
3437
|
+
ws.on('message', (raw) => {
|
|
3438
|
+
try {
|
|
3439
|
+
const msg = JSON.parse(String(raw));
|
|
3440
|
+
if (msg.type === 'resize' && msg.cols > 0 && msg.rows > 0) {
|
|
3441
|
+
if (!cp) {
|
|
3442
|
+
clearTimeout(spawnTimer);
|
|
3443
|
+
startAttach(msg.cols, msg.rows);
|
|
3444
|
+
}
|
|
3445
|
+
else
|
|
3446
|
+
cp.resize(msg.cols, msg.rows);
|
|
3447
|
+
}
|
|
3448
|
+
else if (msg.type === 'input' && typeof msg.data === 'string') {
|
|
3449
|
+
if (!authedClients.has(ws)) {
|
|
3450
|
+
// Read-only: only let mouse events (scroll/select) through.
|
|
3451
|
+
if (!/^\x1b\[([<M])/.test(msg.data))
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
if (cp)
|
|
3455
|
+
cp.write(msg.data);
|
|
3456
|
+
else
|
|
3457
|
+
pendingInput.push(msg.data);
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
catch { /* ignore non-JSON or bad messages */ }
|
|
3461
|
+
});
|
|
3462
|
+
ws.on('close', () => {
|
|
3463
|
+
clearTimeout(spawnTimer);
|
|
3464
|
+
wsClients.delete(ws);
|
|
3465
|
+
const existing = clientPtys.get(ws);
|
|
3466
|
+
if (existing) {
|
|
3467
|
+
try {
|
|
3468
|
+
existing.kill();
|
|
3469
|
+
}
|
|
3470
|
+
catch { /* already dead */ }
|
|
3471
|
+
clientPtys.delete(ws);
|
|
3472
|
+
}
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3304
3475
|
else {
|
|
3305
3476
|
// ── Shared relay (PtyBackend OR tmux pipe mode) ──
|
|
3306
3477
|
// History seed: prefer tmux's authoritative capture-pane in pipe mode
|
|
@@ -3308,8 +3479,20 @@ function startWebServer(host, preferredPort) {
|
|
|
3308
3479
|
// stream, which scrolls stale Ink redraw/spinner frames into scrollback
|
|
3309
3480
|
// at any size mismatch and produces the stacked-footer history garble.
|
|
3310
3481
|
// See chooseWebTerminalSeed for the full rationale.
|
|
3482
|
+
// Adopt observes a pane we CANNOT resize (tmux adopt has
|
|
3483
|
+
// ownsSession=false so resize() is a no-op; zellij drives via
|
|
3484
|
+
// dump-screen). The client's FitAddon sizes its xterm to the browser,
|
|
3485
|
+
// but the snapshot lines carry the PANE's width — any mismatch wraps the
|
|
3486
|
+
// full-width TUI box lines and garbles the layout (the misalignment 申晗
|
|
3487
|
+
// saw). Pin the client xterm to the pane's fixed size via a botmux OSC
|
|
3488
|
+
// (sent BEFORE the seed so the client resizes before rendering it).
|
|
3489
|
+
if (lastInitConfig?.adoptMode && isObserveBackend(backend)) {
|
|
3490
|
+
const sz = backend.getPaneSize();
|
|
3491
|
+
if (sz && sz.cols > 0 && sz.rows > 0)
|
|
3492
|
+
ws.send(`\x1b]1989;${sz.cols};${sz.rows}\x07`);
|
|
3493
|
+
}
|
|
3311
3494
|
const seed = chooseWebTerminalSeed({
|
|
3312
|
-
canCapture: isPipeMode && backend
|
|
3495
|
+
canCapture: isPipeMode && isObserveBackend(backend),
|
|
3313
3496
|
capture: () => backend.captureCurrentScreen(),
|
|
3314
3497
|
scrollback,
|
|
3315
3498
|
onError: log,
|
|
@@ -3502,8 +3685,24 @@ term.onData(function(d){
|
|
|
3502
3685
|
}
|
|
3503
3686
|
if(ws_&&ws_.readyState===1)ws_.send(JSON.stringify({type:'input',data:d}));
|
|
3504
3687
|
});
|
|
3505
|
-
|
|
3506
|
-
|
|
3688
|
+
var fixedSize=false,_lastC=0,_lastR=0,_rzT=0;
|
|
3689
|
+
function sendResize(){
|
|
3690
|
+
if(!ws_||ws_.readyState!==1)return;
|
|
3691
|
+
// Dedup: a fit that lands on the same grid must NOT re-emit a resize — for a
|
|
3692
|
+
// zellij/tmux attach client that would reflow the shared pane for nothing.
|
|
3693
|
+
if(term.cols===_lastC&&term.rows===_lastR)return;
|
|
3694
|
+
_lastC=term.cols;_lastR=term.rows;
|
|
3695
|
+
ws_.send(JSON.stringify({type:'resize',cols:term.cols,rows:term.rows}));
|
|
3696
|
+
}
|
|
3697
|
+
// Debounce viewport resize: mobile fires a burst of window.resize as the address
|
|
3698
|
+
// bar / on-screen keyboard show & hide, and an un-debounced fit→resize on each
|
|
3699
|
+
// reflows the (shared) zellij pane every frame — the status bar toggles and the
|
|
3700
|
+
// text re-wraps, i.e. the flicker 申晗 saw. Coalesce to the settled size.
|
|
3701
|
+
function onViewportResize(){
|
|
3702
|
+
clearTimeout(_rzT);
|
|
3703
|
+
_rzT=setTimeout(function(){if(!fixedSize){try{fit.fit()}catch(e){}}sendResize()},250);
|
|
3704
|
+
}
|
|
3705
|
+
window.addEventListener('resize',onViewportResize);
|
|
3507
3706
|
(function connect(){
|
|
3508
3707
|
var t=new URLSearchParams(location.search).get('token')||'';
|
|
3509
3708
|
// Derive base from the current path so the WS connects to the same prefix the
|
|
@@ -3516,6 +3715,10 @@ window.addEventListener('resize',function(){fit.fit();sendResize()});
|
|
|
3516
3715
|
ws.onopen=function(){el.textContent='connected';el.className='ok';sendResize()};
|
|
3517
3716
|
ws.onmessage=function(e){
|
|
3518
3717
|
var data=typeof e.data==='string'?e.data:new TextDecoder().decode(e.data);
|
|
3718
|
+
// botmux OSC 1989: pin the xterm to the adopted pane's fixed size (the pane
|
|
3719
|
+
// can't be resized, so FitAddon-to-browser would wrap the snapshot lines).
|
|
3720
|
+
var _fs=data.match(/\\x1b\\]1989;(\\d+);(\\d+)\\x07/);
|
|
3721
|
+
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
3722
|
// Intercept OSC 52 clipboard sequence from tmux (set-clipboard on)
|
|
3520
3723
|
var m=data.match(/\\x1b\\]52;[^;]*;([A-Za-z0-9+/=]+)(?:\\x07|\\x1b\\\\)/);
|
|
3521
3724
|
if(m){try{_clipBuf=new TextDecoder().decode(Uint8Array.from(atob(m[1]),function(c){return c.charCodeAt(0)}));_doCopy(_clipBuf);_showCopied()}catch(ex){}}
|