botmux 2.71.5 → 2.73.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 (84) hide show
  1. package/dist/adapters/backend/tmux-pipe-backend.d.ts +6 -0
  2. package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
  3. package/dist/adapters/backend/tmux-pipe-backend.js +28 -0
  4. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +175 -121
  7. package/dist/cli.js.map +1 -1
  8. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  9. package/dist/core/dashboard-ipc-server.js +134 -2
  10. package/dist/core/dashboard-ipc-server.js.map +1 -1
  11. package/dist/core/dashboard-rows.d.ts +4 -0
  12. package/dist/core/dashboard-rows.d.ts.map +1 -1
  13. package/dist/core/dashboard-rows.js +4 -0
  14. package/dist/core/dashboard-rows.js.map +1 -1
  15. package/dist/core/session-board.d.ts +9 -0
  16. package/dist/core/session-board.d.ts.map +1 -0
  17. package/dist/core/session-board.js +24 -0
  18. package/dist/core/session-board.js.map +1 -0
  19. package/dist/daemon.d.ts.map +1 -1
  20. package/dist/daemon.js +6 -4
  21. package/dist/daemon.js.map +1 -1
  22. package/dist/dashboard/federated-group-core.d.ts.map +1 -1
  23. package/dist/dashboard/federated-group-core.js +5 -0
  24. package/dist/dashboard/federated-group-core.js.map +1 -1
  25. package/dist/dashboard/federation-api.d.ts.map +1 -1
  26. package/dist/dashboard/federation-api.js +70 -1
  27. package/dist/dashboard/federation-api.js.map +1 -1
  28. package/dist/dashboard/federation-spoke-api.d.ts +16 -2
  29. package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
  30. package/dist/dashboard/federation-spoke-api.js +96 -3
  31. package/dist/dashboard/federation-spoke-api.js.map +1 -1
  32. package/dist/dashboard/registry.d.ts +6 -1
  33. package/dist/dashboard/registry.d.ts.map +1 -1
  34. package/dist/dashboard/registry.js +13 -1
  35. package/dist/dashboard/registry.js.map +1 -1
  36. package/dist/dashboard/web/app.d.ts.map +1 -1
  37. package/dist/dashboard/web/app.js +52 -0
  38. package/dist/dashboard/web/app.js.map +1 -1
  39. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  40. package/dist/dashboard/web/i18n.js +64 -0
  41. package/dist/dashboard/web/i18n.js.map +1 -1
  42. package/dist/dashboard/web/kanban-model.d.ts +19 -0
  43. package/dist/dashboard/web/kanban-model.d.ts.map +1 -0
  44. package/dist/dashboard/web/kanban-model.js +40 -0
  45. package/dist/dashboard/web/kanban-model.js.map +1 -0
  46. package/dist/dashboard/web/preferences.d.ts +12 -1
  47. package/dist/dashboard/web/preferences.d.ts.map +1 -1
  48. package/dist/dashboard/web/preferences.js +32 -1
  49. package/dist/dashboard/web/preferences.js.map +1 -1
  50. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  51. package/dist/dashboard/web/sessions.js +1059 -10
  52. package/dist/dashboard/web/sessions.js.map +1 -1
  53. package/dist/dashboard-web/app.js +519 -433
  54. package/dist/dashboard-web/index.html +3 -0
  55. package/dist/dashboard-web/style.css +692 -1
  56. package/dist/dashboard.js +72 -3
  57. package/dist/dashboard.js.map +1 -1
  58. package/dist/im/lark/client.d.ts +8 -0
  59. package/dist/im/lark/client.d.ts.map +1 -1
  60. package/dist/im/lark/client.js +32 -0
  61. package/dist/im/lark/client.js.map +1 -1
  62. package/dist/index-daemon.js +9 -0
  63. package/dist/index-daemon.js.map +1 -1
  64. package/dist/services/hook-runner.d.ts +10 -0
  65. package/dist/services/hook-runner.d.ts.map +1 -1
  66. package/dist/services/hook-runner.js +53 -19
  67. package/dist/services/hook-runner.js.map +1 -1
  68. package/dist/services/team-board-store.d.ts +33 -0
  69. package/dist/services/team-board-store.d.ts.map +1 -0
  70. package/dist/services/team-board-store.js +88 -0
  71. package/dist/services/team-board-store.js.map +1 -0
  72. package/dist/services/team-groups-store.d.ts +8 -0
  73. package/dist/services/team-groups-store.d.ts.map +1 -0
  74. package/dist/services/team-groups-store.js +31 -0
  75. package/dist/services/team-groups-store.js.map +1 -0
  76. package/dist/setup/bot-config-editor.d.ts +3 -2
  77. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  78. package/dist/setup/bot-config-editor.js +1 -2
  79. package/dist/setup/bot-config-editor.js.map +1 -1
  80. package/dist/types.d.ts +5 -0
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/worker.js +1 -1
  83. package/dist/worker.js.map +1 -1
  84. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * botmux setup --no-open-platform-auto — skip Feishu Open Platform automation
8
8
  * botmux start — start daemon (pm2)
9
9
  * botmux stop — stop daemon
10
- * botmux restart — restart daemon (auto-restores sessions)
10
+ * botmux restart [--include-pm2] — restart daemon (optionally restart PM2 God too)
11
11
  * botmux logs [--lines] — view daemon logs
12
12
  * botmux status — show daemon status
13
13
  * botmux upgrade — upgrade to latest version
@@ -18,7 +18,7 @@
18
18
  * botmux autostart enable|disable|status — manage boot-time autostart (launchd / user systemd)
19
19
  * botmux worker-budget status|set|unset — inspect/override idle worker suspension budget
20
20
  */
21
- import { execSync, spawnSync, spawn } from 'node:child_process';
21
+ import { execSync, execFileSync, spawnSync, spawn } from 'node:child_process';
22
22
  import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, renameSync, readdirSync, readlinkSync, appendFileSync, statSync, unlinkSync } from 'node:fs';
23
23
  import { atomicWriteFileSync } from './utils/atomic-write.js';
24
24
  import { join, dirname, basename } from 'node:path';
@@ -35,7 +35,6 @@ import { tmuxEnv } from './setup/ensure-tmux.js';
35
35
  import { writeBotsJsonAtomic as writeBotsAtomic } from './setup/bots-store.js';
36
36
  import { applyBotConfigEdits, assertUniqueBotProcessNames, botProcessName, normalizeBotConfig, parseBotConfigsJson, parseBotSelection, removeBotConfig, resolveCliId, assertOwnerWhenChatGroups, findInvalidAllowedUserEntries, hasOwnerEntry, } from './setup/bot-config-editor.js';
37
37
  import { buildPreset, serializePreset, presetFilename } from './setup/agent-preset.js';
38
- import { createCliAdapterSync } from './adapters/cli/registry.js';
39
38
  import { logger } from './utils/logger.js';
40
39
  import { invalidWorkingDirs } from './utils/working-dir.js';
41
40
  import { firstPositional } from './cli/arg-utils.js';
@@ -297,63 +296,6 @@ function printInputHelp(title, lines) {
297
296
  console.log(` ${line}`);
298
297
  }
299
298
  }
300
- /**
301
- * 读取指定 CLI 适配器声明的候选 model 列表 —— 不支持 model 配置的 CLI(aiden/
302
- * antigravity/mtr 等没声明 modelChoices)返回 null,promptModel 据此整段
303
- * 跳过提问。适配器解析失败也按"不支持"处理,避免在 setup 中冒出陌生堆栈。
304
- */
305
- function cliModelChoices(cliId) {
306
- try {
307
- const adapter = createCliAdapterSync(cliId);
308
- return adapter.modelChoices && adapter.modelChoices.length > 0
309
- ? adapter.modelChoices
310
- : null;
311
- }
312
- catch {
313
- return null;
314
- }
315
- }
316
- /**
317
- * 询问"指定 CLI 用哪个 model"。
318
- * - cliId 对应的 adapter 没声明 modelChoices → return undefined(跳过提问)
319
- * - 用户输入序号 → 取候选列表对应项
320
- * - 输入空 + 提供 current → 保留当前值(current 透传出去)
321
- * - 输入空 + 无 current → return undefined(用 CLI 默认)
322
- * - 输入 `-` → return null(语义:清空,调用方据此 delete model 字段)
323
- * - 输入自由文本 → 原样返回
324
- */
325
- async function promptModel(rl, cliId, current) {
326
- const choices = cliModelChoices(cliId);
327
- if (!choices)
328
- return undefined;
329
- const numbered = choices.map((m, i) => `${i + 1}) ${m}`).join(' ');
330
- printInputHelp(`CLI Model(${cliId})`, [
331
- '可选。在 spawn CLI 时注入 model 参数;同一 CLI 配多个 bot 可以跑不同 model。',
332
- `候选: ${numbered} ${choices.length + 1}) Other(自定义输入)`,
333
- current
334
- ? '留空保留当前值;输入 - 清空(回到 CLI 默认)。'
335
- : '留空 = 不设置(用 CLI 默认)。',
336
- ]);
337
- const label = current ? formatOptionalValue(current) : '未设置';
338
- const raw = (await ask(rl, `CLI Model [${label}]: `)).trim();
339
- if (!raw)
340
- return current ?? undefined;
341
- if (raw === '-')
342
- return null;
343
- const numIdx = Number(raw);
344
- if (Number.isInteger(numIdx) && numIdx >= 1 && numIdx <= choices.length) {
345
- return choices[numIdx - 1];
346
- }
347
- if (Number.isInteger(numIdx) && numIdx === choices.length + 1) {
348
- // 选了 Other,进一步要 free-form
349
- const customRaw = (await ask(rl, '请输入 model 名: ')).trim();
350
- if (!customRaw)
351
- return current ?? undefined;
352
- return customRaw;
353
- }
354
- // 直接当成自由输入
355
- return raw;
356
- }
357
299
  // Thin wrapper around setup/bots-store.writeBotsJsonAtomic so call-sites keep
358
300
  // the same name without passing BOTS_JSON_FILE explicitly each time.
359
301
  function writeBotsJsonAtomic(bots) {
@@ -630,7 +572,6 @@ async function promptBotConfig(rl) {
630
572
  return null;
631
573
  }
632
574
  const workingDir = await ask(rl, '默认工作目录 [~]: ');
633
- const modelChoice = await promptModel(rl, cliId);
634
575
  const bot = {
635
576
  larkAppId: creds.appId,
636
577
  larkAppSecret: creds.appSecret,
@@ -645,10 +586,8 @@ async function promptBotConfig(rl) {
645
586
  if (creds.brand === 'lark') {
646
587
  bot.brand = 'lark';
647
588
  }
648
- // modelChoice === undefined CLI 没声明候选 / 用户跳过;不写 model 字段
649
- if (typeof modelChoice === 'string' && modelChoice) {
650
- bot.model = modelChoice;
651
- }
589
+ // setup 不再询问 model(用户常选到无权限的 model,setup 完一发消息就 spawn
590
+ // 报错,排查成本高)。需要指定 model /config 卡片或手动编辑 bots.json。
652
591
  // 扫码场景默认填扫码人自己 (registerApp 返回里有 open_id), 天然就是 owner.
653
592
  // 优先解析成 union_id (on_,跨应用稳定);失败则 fallback 到 open_id (ou_)。
654
593
  // 手动 fallback 场景没 open_id —— 必须显式指定 owner, 否则配置无 owner:
@@ -723,29 +662,14 @@ async function promptEditBotConfig(rl, bot) {
723
662
  '留空保留当前值;输入 - 清空覆盖,回到 PATH 查 cliId 对应的默认二进制。',
724
663
  ]);
725
664
  input.cliPathOverride = await ask(rl, `CLI 可执行文件路径覆盖 [${formatOptionalValue(bot.cliPathOverride)}]: `);
726
- // promptModel 返回 string | null | undefined,直接灌进 BotConfigEditInput.model:
727
- // undefined = 用户跳过 / adapter 不支持 applyBotConfigEdits 不改 model
728
- // null = 用户输 `-` 清空 delete model
729
- // string = 设值
730
- // 用本轮编辑后的 cliId 而非 bot.cliId —— 用户可能刚换了 CLI。
731
- const effectiveCliIdForModel = (resolveCliId(input.cliChoice) ?? bot.cliId ?? 'claude-code');
665
+ // setup 不再询问 model(同 promptBotConfig 的理由)。但切换 CLI 时旧 model
666
+ // 是上一个 CLI 的值,套到新 CLI 上没意义甚至直接 spawn 报错,必须强制清空;
667
+ // 未换 CLI input.model undefined,applyBotConfigEdits 保持原值不动。
732
668
  const cliChanged = !!resolveCliId(input.cliChoice) && resolveCliId(input.cliChoice) !== bot.cliId;
733
- if (cliChanged && !cliModelChoices(effectiveCliIdForModel)) {
734
- // 切到一个不支持 model adapter(例如 aiden / mtr / antigravity):
735
- // 即使原本配了 model 也要主动清空,避免 bots.json 里残留陈旧字段——
736
- // 否则用户后面再换回支持 model 的 adapter 一路回车时,旧 model
737
- // 会被当作"当前值"保留下来误套到新 CLI 上。
738
- console.log('\n⚠️ 新 CLI 不支持 --model 参数,已清空原 model 字段。');
669
+ if (cliChanged && bot.model) {
670
+ console.log('\n⚠️ 已切换 CLI,原 model 字段已清空(如需指定 model 请用 /config 卡片或编辑 bots.json)。');
739
671
  input.model = null;
740
672
  }
741
- else {
742
- const promptCurrent = cliChanged ? undefined : (typeof bot.model === 'string' ? bot.model : undefined);
743
- const result = await promptModel(rl, effectiveCliIdForModel, promptCurrent);
744
- // 切换 CLI 时哪怕用户留空也要清掉旧 model —— 旧值是上一个 CLI 的 model,
745
- // 套到新 CLI 上没意义。result === undefined 在"未变 CLI"分支等价于"保留旧值",
746
- // 但在 cliChanged 分支等价于"用户没指定,回到新 CLI 默认",必须 force null。
747
- input.model = cliChanged && result === undefined ? null : result;
748
- }
749
673
  printInputHelp('会话后端 backendType', [
750
674
  '可选。pty 更轻量;tmux 支持 adopt 和 Web Terminal 附着;herdr 支持托管持久会话;zellij 为实验后端(需 zellij >= 0.44)。',
751
675
  '留空保留当前值;输入 - 回到自动检测;接受 pty / tmux / herdr / zellij。',
@@ -1181,6 +1105,32 @@ function deleteAllBotmuxProcesses(home = PM2_HOME) {
1181
1105
  }
1182
1106
  catch { /* pm2 not running or no apps */ }
1183
1107
  }
1108
+ function killPm2GodDaemon(home = PM2_HOME) {
1109
+ try {
1110
+ execSync(`${pm2Bin()} kill`, {
1111
+ stdio: 'inherit',
1112
+ env: pm2Env(home),
1113
+ timeout: 15_000,
1114
+ });
1115
+ return;
1116
+ }
1117
+ catch {
1118
+ // Fall back to direct pid cleanup below.
1119
+ }
1120
+ for (const pid of listPm2GodDaemonPids(home)) {
1121
+ try {
1122
+ process.kill(pid, 'SIGTERM');
1123
+ }
1124
+ catch { /* already gone */ }
1125
+ }
1126
+ for (const pid of listPm2GodDaemonPids(home)) {
1127
+ try {
1128
+ process.kill(pid, 0);
1129
+ process.kill(pid, 'SIGKILL');
1130
+ }
1131
+ catch { /* already gone */ }
1132
+ }
1133
+ }
1184
1134
  /**
1185
1135
  * One-time migration for users upgrading from versions that used the default
1186
1136
  * ~/.pm2 directory. Removes any lingering botmux-* processes registered under
@@ -1249,6 +1199,7 @@ async function cmdRestart() {
1249
1199
  process.exit(1);
1250
1200
  }
1251
1201
  ensureConfigDir();
1202
+ const includePm2 = process.argv.includes('--include-pm2');
1252
1203
  // Drop a restart-intent breadcrumb so the fresh daemon knows this was an
1253
1204
  // intentional restart and DMs the owner a summary. `IfAbsent` preserves a
1254
1205
  // richer breadcrumb (update / auto-restart) already written by the
@@ -1266,6 +1217,9 @@ async function cmdRestart() {
1266
1217
  cleanupLegacyPm2();
1267
1218
  // Delete all botmux processes (handles both old single-process and new multi-process)
1268
1219
  deleteAllBotmuxProcesses();
1220
+ if (includePm2) {
1221
+ killPm2GodDaemon();
1222
+ }
1269
1223
  // Wipe abandoned dashboard-daemon descriptors left behind by killed daemons.
1270
1224
  cleanupStaleDaemonDescriptors();
1271
1225
  runPm2(['start', cfg]);
@@ -1727,11 +1681,13 @@ function formatSessionRow(s, multiBot, botLabels, cols) {
1727
1681
  }
1728
1682
  const title = padEndDisplay(truncate((s.title || '(untitled)').replace(/[\r\n]+/g, ' '), cols.title), cols.title);
1729
1683
  const dir = padEndDisplay(truncate(s.workingDir || '-', cols.dir), cols.dir);
1730
- const pid = s.pid ? String(s.pid).padEnd(cols.pid) : '-'.padEnd(cols.pid);
1684
+ const displayPid = sessionDisplayPid(s);
1685
+ const pid = displayPid ? String(displayPid).padEnd(cols.pid) : '-'.padEnd(cols.pid);
1731
1686
  const uptime = formatDuration(Date.now() - new Date(s.createdAt).getTime()).padEnd(cols.uptime);
1732
- const alive = !!(s.pid && isProcessAlive(s.pid));
1733
- const status = (alive ? 'online' : s.pid ? 'stopped' : 'idle').padEnd(cols.status);
1734
- parts.push(title, dir, pid, uptime, status);
1687
+ const alive = isSessionAliveForList(s);
1688
+ const status = padEndDisplay(sessionStatusLabel(s), cols.status);
1689
+ const target = padEndDisplay(truncate(sessionTargetLabel(s), cols.target), cols.target);
1690
+ parts.push(title, dir, pid, uptime, status, target);
1735
1691
  return { text: parts.join(' │ '), alive };
1736
1692
  }
1737
1693
  /** Print plain session table (non-interactive). */
@@ -1743,11 +1699,11 @@ function printSessionTable(active) {
1743
1699
  const b = botConfigs[i];
1744
1700
  botLabels.set(b.larkAppId, `bot${i + 1} (${b.cliId ?? 'claude-code'})`);
1745
1701
  }
1746
- const cols = { id: 10, ...(multiBot ? { bot: 22 } : {}), title: 28, dir: 28, pid: 8, uptime: 8, status: 8 };
1702
+ const cols = { id: 10, ...(multiBot ? { bot: 22 } : {}), title: 28, dir: 28, pid: 8, uptime: 8, status: 8, target: 26 };
1747
1703
  const headerParts = ['id'.padEnd(cols.id)];
1748
1704
  if (multiBot)
1749
1705
  headerParts.push('bot'.padEnd(cols.bot));
1750
- headerParts.push('title'.padEnd(cols.title), 'working dir'.padEnd(cols.dir), 'pid'.padEnd(cols.pid), 'uptime'.padEnd(cols.uptime), 'status'.padEnd(cols.status));
1706
+ headerParts.push('title'.padEnd(cols.title), 'working dir'.padEnd(cols.dir), 'pid'.padEnd(cols.pid), 'uptime'.padEnd(cols.uptime), 'status'.padEnd(cols.status), 'target'.padEnd(cols.target));
1751
1707
  const header = headerParts.join(' │ ');
1752
1708
  const separator = '─'.repeat(displayWidth(header));
1753
1709
  console.log(separator);
@@ -1770,6 +1726,66 @@ function tmuxSessionExists(name) {
1770
1726
  return false;
1771
1727
  }
1772
1728
  }
1729
+ function applyTmuxWindowSizeLargest(sessionName) {
1730
+ try {
1731
+ execFileSync('tmux', ['set-option', '-t', sessionName, 'window-size', 'largest'], {
1732
+ stdio: 'ignore',
1733
+ timeout: 3000,
1734
+ env: tmuxEnv(),
1735
+ });
1736
+ }
1737
+ catch { /* best-effort: attach can still proceed */ }
1738
+ }
1739
+ function isAdoptedSession(s) {
1740
+ return !!s.adoptedFrom && typeof s.adoptedFrom === 'object';
1741
+ }
1742
+ function adoptedCliPid(s) {
1743
+ const pid = isAdoptedSession(s) ? s.adoptedFrom.originalCliPid : undefined;
1744
+ return typeof pid === 'number' && pid > 0 ? pid : undefined;
1745
+ }
1746
+ function adoptTargetLabel(s) {
1747
+ if (!isAdoptedSession(s))
1748
+ return '';
1749
+ const a = s.adoptedFrom;
1750
+ if (a.source === 'zellij' || a.zellijPaneId) {
1751
+ const target = a.zellijSession && a.zellijPaneId
1752
+ ? `${a.zellijSession}/${a.zellijPaneId}`
1753
+ : a.zellijPaneId || a.zellijSession || '?';
1754
+ return `adopt: zellij ${target}`;
1755
+ }
1756
+ if (a.source === 'herdr' || a.herdrSessionName || a.herdrPaneId || a.herdrTarget) {
1757
+ const pane = a.herdrTarget ?? a.herdrPaneId ?? '?';
1758
+ const target = a.herdrSessionName ? `${a.herdrSessionName}:${pane}` : pane;
1759
+ return `adopt: herdr ${target}`;
1760
+ }
1761
+ return `adopt: tmux ${a.tmuxTarget ?? '?'}`;
1762
+ }
1763
+ function sessionDisplayPid(s) {
1764
+ return adoptedCliPid(s) ?? s.pid;
1765
+ }
1766
+ function isSessionAliveForList(s) {
1767
+ const pid = sessionDisplayPid(s);
1768
+ return !!(pid && isProcessAlive(pid));
1769
+ }
1770
+ function sessionStatusLabel(s) {
1771
+ if (isAdoptedSession(s)) {
1772
+ const pid = adoptedCliPid(s);
1773
+ if (pid)
1774
+ return isProcessAlive(pid) ? 'adopt' : 'stopped';
1775
+ return s.pid && isProcessAlive(s.pid) ? 'adopt' : 'idle';
1776
+ }
1777
+ return s.pid && isProcessAlive(s.pid) ? 'online' : s.pid ? 'stopped' : 'idle';
1778
+ }
1779
+ function sessionTargetLabel(s, tmuxName, hasTmux) {
1780
+ if (isAdoptedSession(s))
1781
+ return adoptTargetLabel(s);
1782
+ if (hasTmux === undefined) {
1783
+ const name = tmuxName ?? `bmx-${s.sessionId.substring(0, 8)}`;
1784
+ hasTmux = tmuxSessionExists(name);
1785
+ tmuxName = name;
1786
+ }
1787
+ return hasTmux ? `tmux: ${tmuxName}` : '-';
1788
+ }
1773
1789
  /** Shorten path for display: replace $HOME with ~. */
1774
1790
  function shortenPath(p) {
1775
1791
  const home = homedir();
@@ -1788,10 +1804,10 @@ function interactiveSessionPicker(active) {
1788
1804
  const termWidth = process.stdout.columns || 100;
1789
1805
  const PREFIX = 4; // " ❯ " or " "
1790
1806
  const SEP_W = 3; // " │ "
1791
- const fixedCols = { id: 10, pid: 8, uptime: 7, status: 7 };
1807
+ const fixedCols = { id: 10, pid: 8, uptime: 7, status: 7, target: 26 };
1792
1808
  const botW = multiBot ? 18 : 0;
1793
- const numSeps = (multiBot ? 7 : 6) - 1; // separators between columns
1794
- const fixedTotal = PREFIX + fixedCols.id + botW + fixedCols.pid + fixedCols.uptime + fixedCols.status + numSeps * SEP_W;
1809
+ const numSeps = (multiBot ? 8 : 7) - 1; // separators between columns
1810
+ const fixedTotal = PREFIX + fixedCols.id + botW + fixedCols.pid + fixedCols.uptime + fixedCols.status + fixedCols.target + numSeps * SEP_W;
1795
1811
  const flexTotal = Math.max(20, termWidth - fixedTotal);
1796
1812
  const titleW = Math.floor(flexTotal * 0.4);
1797
1813
  const dirW = flexTotal - titleW;
@@ -1803,10 +1819,15 @@ function interactiveSessionPicker(active) {
1803
1819
  pid: fixedCols.pid,
1804
1820
  uptime: fixedCols.uptime,
1805
1821
  status: fixedCols.status,
1822
+ target: fixedCols.target,
1806
1823
  };
1807
1824
  // Build row data — use shortened paths for TUI
1808
1825
  function buildRows() {
1809
1826
  return active.map(s => {
1827
+ const tmuxName = `bmx-${s.sessionId.substring(0, 8)}`;
1828
+ const isAdopt = isAdoptedSession(s);
1829
+ const hasTmux = !isAdopt && tmuxSessionExists(tmuxName);
1830
+ const targetLabel = sessionTargetLabel(s, tmuxName, hasTmux);
1810
1831
  // Build row text with shortened dir
1811
1832
  const id = padEndDisplay(s.sessionId.substring(0, 8), cols.id);
1812
1833
  const parts = [id];
@@ -1816,14 +1837,14 @@ function interactiveSessionPicker(active) {
1816
1837
  }
1817
1838
  const title = padEndDisplay(truncate((s.title || '(untitled)').replace(/[\r\n]+/g, ' '), cols.title), cols.title);
1818
1839
  const dir = padEndDisplay(truncate(shortenPath(s.workingDir || '-'), cols.dir), cols.dir);
1819
- const pid = s.pid ? String(s.pid).padEnd(cols.pid) : '-'.padEnd(cols.pid);
1840
+ const displayPid = sessionDisplayPid(s);
1841
+ const pid = displayPid ? String(displayPid).padEnd(cols.pid) : '-'.padEnd(cols.pid);
1820
1842
  const uptime = formatDuration(Date.now() - new Date(s.createdAt).getTime()).padEnd(cols.uptime);
1821
- const alive = !!(s.pid && isProcessAlive(s.pid));
1822
- const status = (alive ? 'online' : s.pid ? 'stopped' : 'idle').padEnd(cols.status);
1823
- parts.push(title, dir, pid, uptime, status);
1824
- const tmuxName = `bmx-${s.sessionId.substring(0, 8)}`;
1825
- const hasTmux = tmuxSessionExists(tmuxName);
1826
- return { session: s, text: parts.join(' │ '), alive, tmuxName, hasTmux };
1843
+ const alive = isSessionAliveForList(s);
1844
+ const status = padEndDisplay(sessionStatusLabel(s), cols.status);
1845
+ const target = padEndDisplay(truncate(targetLabel, cols.target), cols.target);
1846
+ parts.push(title, dir, pid, uptime, status, target);
1847
+ return { session: s, text: parts.join(' │ '), alive, tmuxName, hasTmux, isAdopt, targetLabel, canAttach: hasTmux && !isAdopt };
1827
1848
  });
1828
1849
  }
1829
1850
  let rows = buildRows();
@@ -1832,7 +1853,7 @@ function interactiveSessionPicker(active) {
1832
1853
  const hParts = ['id'.padEnd(cols.id)];
1833
1854
  if (multiBot)
1834
1855
  hParts.push('bot'.padEnd(cols.bot));
1835
- hParts.push('title'.padEnd(cols.title), 'working dir'.padEnd(cols.dir), 'pid'.padEnd(cols.pid), 'uptime'.padEnd(cols.uptime), 'status'.padEnd(cols.status));
1856
+ hParts.push('title'.padEnd(cols.title), 'working dir'.padEnd(cols.dir), 'pid'.padEnd(cols.pid), 'uptime'.padEnd(cols.uptime), 'status'.padEnd(cols.status), 'target'.padEnd(cols.target));
1836
1857
  return hParts.join(' │ ');
1837
1858
  }
1838
1859
  const header = buildHeader();
@@ -1866,10 +1887,12 @@ function interactiveSessionPicker(active) {
1866
1887
  process.stdout.write(` ${separator}\n`);
1867
1888
  // Footer info
1868
1889
  const selected = rows[cursor];
1869
- const tmuxHint = selected.hasTmux
1870
- ? `\x1b[32mtmux: ${selected.tmuxName}\x1b[0m`
1871
- : `\x1b[2mtmux: 无会话\x1b[0m`;
1872
- process.stdout.write(`\n ${tmuxHint}\n`);
1890
+ const targetHint = selected.isAdopt
1891
+ ? `\x1b[33m${selected.targetLabel}\x1b[0m \x1b[2mEnter 已禁用;请直接使用原 tmux/zellij/herdr 客户端。\x1b[0m`
1892
+ : selected.hasTmux
1893
+ ? `\x1b[32mtmux: ${selected.tmuxName}\x1b[0m`
1894
+ : `\x1b[2mtmux: 无会话\x1b[0m`;
1895
+ process.stdout.write(`\n ${targetHint}\n`);
1873
1896
  // Flash message or confirmation prompt
1874
1897
  if (confirmDelete) {
1875
1898
  const s = selected.session;
@@ -1882,7 +1905,7 @@ function interactiveSessionPicker(active) {
1882
1905
  process.stdout.write('\n');
1883
1906
  }
1884
1907
  // Keybinding hints
1885
- process.stdout.write(`\n \x1b[2m↑/↓ 选择 ⏎ 连接 d 删除 q 退出\x1b[0m\n`);
1908
+ process.stdout.write(`\n \x1b[2m↑/↓ 选择 ⏎ ${selected?.canAttach ? '连接' : '不可连接'} d 删除 q 退出\x1b[0m\n`);
1886
1909
  }
1887
1910
  return new Promise((resolve) => {
1888
1911
  process.stdin.setRawMode(true);
@@ -1900,12 +1923,14 @@ function interactiveSessionPicker(active) {
1900
1923
  function deleteSession(idx) {
1901
1924
  const r = rows[idx];
1902
1925
  const s = r.session;
1903
- // Kill CLI process
1904
- if (s.pid && isProcessAlive(s.pid)) {
1926
+ // Kill botmux's worker process. For adopted sessions, never kill the
1927
+ // user's original CLI pid if an old record stored it in `pid`.
1928
+ const originalPid = adoptedCliPid(s);
1929
+ if (s.pid && s.pid !== originalPid && isProcessAlive(s.pid)) {
1905
1930
  killProcess(s.pid);
1906
1931
  }
1907
- // Kill tmux session
1908
- if (r.hasTmux) {
1932
+ // Kill only botmux-owned tmux sessions. Adopted panes belong to the user.
1933
+ if (!r.isAdopt && r.hasTmux) {
1909
1934
  try {
1910
1935
  execSync(`tmux kill-session -t '${r.tmuxName}' 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
1911
1936
  }
@@ -1970,11 +1995,17 @@ function interactiveSessionPicker(active) {
1970
1995
  // Enter — attach to tmux
1971
1996
  if (key === '\r' || key === '\n') {
1972
1997
  const selected = rows[cursor];
1973
- if (!selected.hasTmux) {
1998
+ if (selected.isAdopt) {
1999
+ flashMsg = `\x1b[33m这是 adopt 会话;botmux 不 attach 用户 pane。目标: ${selected.targetLabel}\x1b[0m`;
2000
+ render();
2001
+ return;
2002
+ }
2003
+ if (!selected.canAttach) {
1974
2004
  flashMsg = '\x1b[33m该会话没有 tmux,无法连接\x1b[0m';
1975
2005
  render();
1976
2006
  return;
1977
2007
  }
2008
+ applyTmuxWindowSizeLargest(selected.tmuxName);
1978
2009
  cleanup();
1979
2010
  spawnSync('tmux', ['attach-session', '-t', selected.tmuxName], {
1980
2011
  stdio: 'inherit',
@@ -2001,6 +2032,20 @@ async function cmdList() {
2001
2032
  const prunedScratch = [];
2002
2033
  const live = [];
2003
2034
  for (const s of active) {
2035
+ if (isAdoptedSession(s)) {
2036
+ const pid = adoptedCliPid(s);
2037
+ if (pid && isProcessAlive(pid)) {
2038
+ live.push(s);
2039
+ }
2040
+ else if (pid) {
2041
+ pruned.push(s);
2042
+ }
2043
+ else {
2044
+ const hasPid = !!(s.pid && isProcessAlive(s.pid));
2045
+ hasPid ? live.push(s) : pruned.push(s);
2046
+ }
2047
+ continue;
2048
+ }
2004
2049
  const hasPid = !!(s.pid && isProcessAlive(s.pid));
2005
2050
  const hasTmux = tmuxSessionExists(`bmx-${s.sessionId.substring(0, 8)}`);
2006
2051
  if (!hasPid && !hasTmux) {
@@ -2022,7 +2067,7 @@ async function cmdList() {
2022
2067
  closeNow(prunedScratch);
2023
2068
  if (pruned.length > 0) {
2024
2069
  closeNow(pruned);
2025
- console.log(`已自动清理 ${pruned.length} 个不可恢复的会话(进程已死且无 tmux session)`);
2070
+ console.log(`已自动清理 ${pruned.length} 个不可恢复的会话(进程已退出或无可恢复后端)`);
2026
2071
  }
2027
2072
  // Sort by creation time, newest first
2028
2073
  live.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
@@ -2056,6 +2101,10 @@ function cmdDelete() {
2056
2101
  }
2057
2102
  else if (target === 'stopped') {
2058
2103
  toDelete = active.filter(s => {
2104
+ if (isAdoptedSession(s)) {
2105
+ const pid = adoptedCliPid(s);
2106
+ return pid ? !isProcessAlive(pid) : !(s.pid && isProcessAlive(s.pid));
2107
+ }
2059
2108
  const hasPid = !!(s.pid && isProcessAlive(s.pid));
2060
2109
  const hasTmux = tmuxSessionExists(`bmx-${s.sessionId.substring(0, 8)}`);
2061
2110
  return !hasPid && !hasTmux;
@@ -2082,18 +2131,23 @@ function cmdDelete() {
2082
2131
  }
2083
2132
  }
2084
2133
  for (const s of toDelete) {
2085
- // Kill CLI process if running
2086
- if (s.pid && isProcessAlive(s.pid)) {
2134
+ const originalPid = adoptedCliPid(s);
2135
+ // Kill botmux's worker process if running. For adopted sessions, never
2136
+ // kill the user's original CLI pid.
2137
+ if (s.pid && s.pid !== originalPid && isProcessAlive(s.pid)) {
2087
2138
  killProcess(s.pid);
2088
2139
  console.log(` killed pid ${s.pid}`);
2089
2140
  }
2090
- // Kill associated tmux session if it exists
2141
+ // Kill associated botmux-owned tmux session if it exists. Adopted panes
2142
+ // belong to the user and must be left untouched.
2091
2143
  const tmuxName = `bmx-${s.sessionId.substring(0, 8)}`;
2092
- try {
2093
- execSync(`tmux kill-session -t '${tmuxName}' 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
2094
- console.log(` killed tmux ${tmuxName}`);
2144
+ if (!isAdoptedSession(s)) {
2145
+ try {
2146
+ execSync(`tmux kill-session -t '${tmuxName}' 2>/dev/null`, { stdio: 'ignore', env: tmuxEnv() });
2147
+ console.log(` killed tmux ${tmuxName}`);
2148
+ }
2149
+ catch { /* no tmux session */ }
2095
2150
  }
2096
- catch { /* no tmux session */ }
2097
2151
  // Mark session as closed
2098
2152
  s.status = 'closed';
2099
2153
  s.closedAt = new Date().toISOString();
@@ -2360,7 +2414,7 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
2360
2414
  默认使用 botmux 内置 Feishu Web QR 登录尝试自动导入权限/redirect/发布版本;可加 --no-open-platform-auto 跳过
2361
2415
  start 启动 daemon
2362
2416
  stop 停止 daemon
2363
- restart 重启 daemon(自动恢复活跃会话)
2417
+ restart 重启 daemon(自动恢复活跃会话;--include-pm2 同时重启 PM2 God)
2364
2418
  logs 查看 daemon 日志(--lines N, --bot <0-based-index|pm2-name|appId>)
2365
2419
  status 查看 daemon 状态
2366
2420
  upgrade 升级到最新版本