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.
Files changed (165) hide show
  1. package/README.en.md +22 -265
  2. package/README.md +21 -296
  3. package/dist/adapters/backend/session-backend-selector.d.ts +5 -1
  4. package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
  5. package/dist/adapters/backend/session-backend-selector.js +15 -1
  6. package/dist/adapters/backend/session-backend-selector.js.map +1 -1
  7. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  8. package/dist/adapters/backend/tmux-backend.js +3 -0
  9. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  10. package/dist/adapters/backend/types.d.ts +22 -0
  11. package/dist/adapters/backend/types.d.ts.map +1 -1
  12. package/dist/adapters/backend/types.js +7 -1
  13. package/dist/adapters/backend/types.js.map +1 -1
  14. package/dist/adapters/backend/zellij-backend.d.ts +132 -0
  15. package/dist/adapters/backend/zellij-backend.d.ts.map +1 -0
  16. package/dist/adapters/backend/zellij-backend.js +375 -0
  17. package/dist/adapters/backend/zellij-backend.js.map +1 -0
  18. package/dist/adapters/backend/zellij-observe-backend.d.ts +62 -0
  19. package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -0
  20. package/dist/adapters/backend/zellij-observe-backend.js +218 -0
  21. package/dist/adapters/backend/zellij-observe-backend.js.map +1 -0
  22. package/dist/adapters/cli/claude-code.d.ts +39 -5
  23. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  24. package/dist/adapters/cli/claude-code.js +53 -31
  25. package/dist/adapters/cli/claude-code.js.map +1 -1
  26. package/dist/adapters/cli/registry.d.ts +2 -1
  27. package/dist/adapters/cli/registry.d.ts.map +1 -1
  28. package/dist/adapters/cli/registry.js +3 -1
  29. package/dist/adapters/cli/registry.js.map +1 -1
  30. package/dist/adapters/cli/seed.d.ts +29 -0
  31. package/dist/adapters/cli/seed.d.ts.map +1 -0
  32. package/dist/adapters/cli/seed.js +63 -0
  33. package/dist/adapters/cli/seed.js.map +1 -0
  34. package/dist/adapters/cli/types.d.ts +17 -1
  35. package/dist/adapters/cli/types.d.ts.map +1 -1
  36. package/dist/bot-registry.d.ts +31 -1
  37. package/dist/bot-registry.d.ts.map +1 -1
  38. package/dist/bot-registry.js +31 -0
  39. package/dist/bot-registry.js.map +1 -1
  40. package/dist/cli.d.ts.map +1 -1
  41. package/dist/cli.js +37 -27
  42. package/dist/cli.js.map +1 -1
  43. package/dist/config.d.ts +7 -1
  44. package/dist/config.d.ts.map +1 -1
  45. package/dist/config.js +8 -0
  46. package/dist/config.js.map +1 -1
  47. package/dist/core/ask-hook/registry.d.ts.map +1 -1
  48. package/dist/core/ask-hook/registry.js +4 -0
  49. package/dist/core/ask-hook/registry.js.map +1 -1
  50. package/dist/core/command-handler.d.ts +4 -1
  51. package/dist/core/command-handler.d.ts.map +1 -1
  52. package/dist/core/command-handler.js +100 -8
  53. package/dist/core/command-handler.js.map +1 -1
  54. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  55. package/dist/core/dashboard-ipc-server.js +37 -0
  56. package/dist/core/dashboard-ipc-server.js.map +1 -1
  57. package/dist/core/dispatch.d.ts +33 -0
  58. package/dist/core/dispatch.d.ts.map +1 -1
  59. package/dist/core/dispatch.js +26 -0
  60. package/dist/core/dispatch.js.map +1 -1
  61. package/dist/core/session-discovery.d.ts +13 -4
  62. package/dist/core/session-discovery.d.ts.map +1 -1
  63. package/dist/core/session-discovery.js +5 -5
  64. package/dist/core/session-discovery.js.map +1 -1
  65. package/dist/core/session-manager.d.ts +10 -0
  66. package/dist/core/session-manager.d.ts.map +1 -1
  67. package/dist/core/session-manager.js +43 -18
  68. package/dist/core/session-manager.js.map +1 -1
  69. package/dist/core/types.d.ts +5 -2
  70. package/dist/core/types.d.ts.map +1 -1
  71. package/dist/core/types.js.map +1 -1
  72. package/dist/core/worker-pool.d.ts +1 -1
  73. package/dist/core/worker-pool.d.ts.map +1 -1
  74. package/dist/core/worker-pool.js +22 -9
  75. package/dist/core/worker-pool.js.map +1 -1
  76. package/dist/core/zellij-adopt-discovery.d.ts +28 -0
  77. package/dist/core/zellij-adopt-discovery.d.ts.map +1 -0
  78. package/dist/core/zellij-adopt-discovery.js +255 -0
  79. package/dist/core/zellij-adopt-discovery.js.map +1 -0
  80. package/dist/core/zellij-session-discovery.d.ts +73 -0
  81. package/dist/core/zellij-session-discovery.d.ts.map +1 -0
  82. package/dist/core/zellij-session-discovery.js +259 -0
  83. package/dist/core/zellij-session-discovery.js.map +1 -0
  84. package/dist/daemon.d.ts +3 -0
  85. package/dist/daemon.d.ts.map +1 -1
  86. package/dist/daemon.js +145 -13
  87. package/dist/daemon.js.map +1 -1
  88. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  89. package/dist/dashboard/web/bot-defaults.js +114 -0
  90. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  91. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  92. package/dist/dashboard/web/i18n.js +23 -1
  93. package/dist/dashboard/web/i18n.js.map +1 -1
  94. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  95. package/dist/dashboard/web/sessions.js +1 -0
  96. package/dist/dashboard/web/sessions.js.map +1 -1
  97. package/dist/dashboard/web/workflows.js +1 -1
  98. package/dist/dashboard/web/workflows.js.map +1 -1
  99. package/dist/dashboard-web/app.js +449 -426
  100. package/dist/dashboard.js +20 -0
  101. package/dist/dashboard.js.map +1 -1
  102. package/dist/i18n/en.d.ts.map +1 -1
  103. package/dist/i18n/en.js +15 -1
  104. package/dist/i18n/en.js.map +1 -1
  105. package/dist/i18n/zh.d.ts.map +1 -1
  106. package/dist/i18n/zh.js +16 -2
  107. package/dist/i18n/zh.js.map +1 -1
  108. package/dist/im/lark/card-builder.d.ts +8 -3
  109. package/dist/im/lark/card-builder.d.ts.map +1 -1
  110. package/dist/im/lark/card-builder.js +74 -5
  111. package/dist/im/lark/card-builder.js.map +1 -1
  112. package/dist/im/lark/card-handler.d.ts.map +1 -1
  113. package/dist/im/lark/card-handler.js +72 -10
  114. package/dist/im/lark/card-handler.js.map +1 -1
  115. package/dist/im/lark/event-dispatcher.d.ts +12 -0
  116. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  117. package/dist/im/lark/event-dispatcher.js +39 -31
  118. package/dist/im/lark/event-dispatcher.js.map +1 -1
  119. package/dist/im/lark/grant-command.d.ts +26 -0
  120. package/dist/im/lark/grant-command.d.ts.map +1 -1
  121. package/dist/im/lark/grant-command.js +142 -3
  122. package/dist/im/lark/grant-command.js.map +1 -1
  123. package/dist/im/lark/grant-pending.d.ts +7 -4
  124. package/dist/im/lark/grant-pending.d.ts.map +1 -1
  125. package/dist/im/lark/grant-pending.js +12 -6
  126. package/dist/im/lark/grant-pending.js.map +1 -1
  127. package/dist/services/codex-app-threads.d.ts +20 -0
  128. package/dist/services/codex-app-threads.d.ts.map +1 -0
  129. package/dist/services/codex-app-threads.js +165 -0
  130. package/dist/services/codex-app-threads.js.map +1 -0
  131. package/dist/services/grant-prefs-store.d.ts +23 -0
  132. package/dist/services/grant-prefs-store.d.ts.map +1 -0
  133. package/dist/services/grant-prefs-store.js +94 -0
  134. package/dist/services/grant-prefs-store.js.map +1 -0
  135. package/dist/services/grant-store.d.ts +34 -2
  136. package/dist/services/grant-store.d.ts.map +1 -1
  137. package/dist/services/grant-store.js +160 -9
  138. package/dist/services/grant-store.js.map +1 -1
  139. package/dist/services/quota-dedup.d.ts +33 -0
  140. package/dist/services/quota-dedup.d.ts.map +1 -0
  141. package/dist/services/quota-dedup.js +67 -0
  142. package/dist/services/quota-dedup.js.map +1 -0
  143. package/dist/setup/bot-config-editor.d.ts +1 -1
  144. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  145. package/dist/setup/bot-config-editor.js +5 -4
  146. package/dist/setup/bot-config-editor.js.map +1 -1
  147. package/dist/setup/ensure-zellij.d.ts +48 -0
  148. package/dist/setup/ensure-zellij.d.ts.map +1 -0
  149. package/dist/setup/ensure-zellij.js +93 -0
  150. package/dist/setup/ensure-zellij.js.map +1 -0
  151. package/dist/types.d.ts +9 -3
  152. package/dist/types.d.ts.map +1 -1
  153. package/dist/utils/anchor-serializer.d.ts +3 -2
  154. package/dist/utils/anchor-serializer.d.ts.map +1 -1
  155. package/dist/utils/anchor-serializer.js +20 -5
  156. package/dist/utils/anchor-serializer.js.map +1 -1
  157. package/dist/utils/transient-snapshot.js +2 -2
  158. package/dist/utils/transient-snapshot.js.map +1 -1
  159. package/dist/worker.js +124 -30
  160. package/dist/worker.js.map +1 -1
  161. package/dist/workflows/attempt-resume.d.ts +1 -1
  162. package/dist/workflows/attempt-resume.d.ts.map +1 -1
  163. package/dist/workflows/attempt-resume.js +1 -1
  164. package/dist/workflows/attempt-resume.js.map +1 -1
  165. 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: pipe-pane the user's existing tmux pane (no attach) ──
2740
- if (cfg.adoptMode && cfg.adoptTmuxTarget) {
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 tmux track; the latter tells the WS handler to route
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 pipeBe = new TmuxPipeBackend(cfg.adoptTmuxTarget);
2750
- backend = pipeBe;
2751
- pipeBe.spawn('', [], {
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 byte tmux pipes through.
2775
+ // immediately, instead of waiting for the next observe tick.
2760
2776
  try {
2761
- const initial = pipeBe.captureCurrentScreen();
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 useTmux = cfg.backendType === 'tmux';
2936
- if (useTmux && !TmuxBackend.isAvailable()) {
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
- useTmux = false;
2954
+ effectiveBackend = 'pty';
2939
2955
  }
2940
- const selectedBackend = selectSessionBackend({ sessionId: cfg.sessionId, useTmux });
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
- if (cfg.cliId === 'claude-code') {
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
- const injectClaudeSandbox = cfg.cliId === 'claude-code' &&
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 Code');
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
- const claudeResumeTokenThreshold = cfg.cliId === 'claude-code'
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 (cfg.cliId === 'claude-code' && cliPid) {
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 (cfg.cliId === 'claude-code' && adapterSessionId) {
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 instanceof TmuxPipeBackend,
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();sendResize()});
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){}}