botmux 2.95.0 → 2.96.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 (156) hide show
  1. package/README.en.md +4 -4
  2. package/README.md +3 -3
  3. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  4. package/dist/adapters/backend/tmux-backend.js +1 -10
  5. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  6. package/dist/adapters/cli/kimi.d.ts +4 -0
  7. package/dist/adapters/cli/kimi.d.ts.map +1 -0
  8. package/dist/adapters/cli/kimi.js +54 -0
  9. package/dist/adapters/cli/kimi.js.map +1 -0
  10. package/dist/adapters/cli/registry.d.ts +2 -1
  11. package/dist/adapters/cli/registry.d.ts.map +1 -1
  12. package/dist/adapters/cli/registry.js +6 -20
  13. package/dist/adapters/cli/registry.js.map +1 -1
  14. package/dist/adapters/cli/types.d.ts +1 -1
  15. package/dist/adapters/cli/types.d.ts.map +1 -1
  16. package/dist/adapters/hook-installer.d.ts.map +1 -1
  17. package/dist/adapters/hook-installer.js +138 -28
  18. package/dist/adapters/hook-installer.js.map +1 -1
  19. package/dist/bot-registry.d.ts +61 -4
  20. package/dist/bot-registry.d.ts.map +1 -1
  21. package/dist/bot-registry.js +164 -3
  22. package/dist/bot-registry.js.map +1 -1
  23. package/dist/cli/stdin-encoding.d.ts +21 -0
  24. package/dist/cli/stdin-encoding.d.ts.map +1 -1
  25. package/dist/cli/stdin-encoding.js +37 -0
  26. package/dist/cli/stdin-encoding.js.map +1 -1
  27. package/dist/cli.d.ts.map +1 -1
  28. package/dist/cli.js +14 -4
  29. package/dist/cli.js.map +1 -1
  30. package/dist/core/ask-hook/opencode.d.ts +15 -17
  31. package/dist/core/ask-hook/opencode.d.ts.map +1 -1
  32. package/dist/core/ask-hook/opencode.js +17 -19
  33. package/dist/core/ask-hook/opencode.js.map +1 -1
  34. package/dist/core/command-handler.d.ts.map +1 -1
  35. package/dist/core/command-handler.js +1 -0
  36. package/dist/core/command-handler.js.map +1 -1
  37. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  38. package/dist/core/dashboard-ipc-server.js +193 -5
  39. package/dist/core/dashboard-ipc-server.js.map +1 -1
  40. package/dist/core/dashboard-rows.d.ts +2 -0
  41. package/dist/core/dashboard-rows.d.ts.map +1 -1
  42. package/dist/core/dashboard-rows.js +2 -0
  43. package/dist/core/dashboard-rows.js.map +1 -1
  44. package/dist/core/trigger-session.d.ts.map +1 -1
  45. package/dist/core/trigger-session.js +99 -7
  46. package/dist/core/trigger-session.js.map +1 -1
  47. package/dist/core/types.d.ts +16 -0
  48. package/dist/core/types.d.ts.map +1 -1
  49. package/dist/core/types.js.map +1 -1
  50. package/dist/core/worker-pool.d.ts +1 -1
  51. package/dist/core/worker-pool.d.ts.map +1 -1
  52. package/dist/core/worker-pool.js +68 -11
  53. package/dist/core/worker-pool.js.map +1 -1
  54. package/dist/dashboard/bot-payload.d.ts +7 -0
  55. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  56. package/dist/dashboard/bot-payload.js +7 -1
  57. package/dist/dashboard/bot-payload.js.map +1 -1
  58. package/dist/dashboard/session-cleanup.d.ts +1 -0
  59. package/dist/dashboard/session-cleanup.d.ts.map +1 -1
  60. package/dist/dashboard/session-cleanup.js +2 -0
  61. package/dist/dashboard/session-cleanup.js.map +1 -1
  62. package/dist/dashboard/trigger-api.d.ts +4 -0
  63. package/dist/dashboard/trigger-api.d.ts.map +1 -1
  64. package/dist/dashboard/trigger-api.js +20 -0
  65. package/dist/dashboard/trigger-api.js.map +1 -1
  66. package/dist/dashboard/web/bot-defaults.d.ts +1 -0
  67. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  68. package/dist/dashboard/web/bot-defaults.js +247 -4
  69. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  70. package/dist/dashboard/web/connectors.d.ts +11 -0
  71. package/dist/dashboard/web/connectors.d.ts.map +1 -1
  72. package/dist/dashboard/web/connectors.js +45 -1
  73. package/dist/dashboard/web/connectors.js.map +1 -1
  74. package/dist/dashboard/web/groups.d.ts +7 -0
  75. package/dist/dashboard/web/groups.d.ts.map +1 -1
  76. package/dist/dashboard/web/groups.js +62 -15
  77. package/dist/dashboard/web/groups.js.map +1 -1
  78. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  79. package/dist/dashboard/web/i18n.js +52 -2
  80. package/dist/dashboard/web/i18n.js.map +1 -1
  81. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  82. package/dist/dashboard/web/sessions.js +149 -5
  83. package/dist/dashboard/web/sessions.js.map +1 -1
  84. package/dist/dashboard/webhook-routes.d.ts.map +1 -1
  85. package/dist/dashboard/webhook-routes.js +126 -6
  86. package/dist/dashboard/webhook-routes.js.map +1 -1
  87. package/dist/dashboard-web/app.js +818 -751
  88. package/dist/dashboard-web/style.css +47 -0
  89. package/dist/dashboard.js +84 -6
  90. package/dist/dashboard.js.map +1 -1
  91. package/dist/i18n/en.d.ts.map +1 -1
  92. package/dist/i18n/en.js +1 -0
  93. package/dist/i18n/en.js.map +1 -1
  94. package/dist/i18n/zh.d.ts.map +1 -1
  95. package/dist/i18n/zh.js +1 -0
  96. package/dist/i18n/zh.js.map +1 -1
  97. package/dist/im/lark/card-builder.d.ts.map +1 -1
  98. package/dist/im/lark/card-builder.js +1 -0
  99. package/dist/im/lark/card-builder.js.map +1 -1
  100. package/dist/im/lark/client.d.ts +12 -0
  101. package/dist/im/lark/client.d.ts.map +1 -1
  102. package/dist/im/lark/client.js +45 -9
  103. package/dist/im/lark/client.js.map +1 -1
  104. package/dist/im/lark/event-dispatcher.d.ts +13 -0
  105. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  106. package/dist/im/lark/event-dispatcher.js +180 -11
  107. package/dist/im/lark/event-dispatcher.js.map +1 -1
  108. package/dist/im/lark/message-parser.d.ts +11 -0
  109. package/dist/im/lark/message-parser.d.ts.map +1 -1
  110. package/dist/im/lark/message-parser.js +2 -1
  111. package/dist/im/lark/message-parser.js.map +1 -1
  112. package/dist/im/lark/summary-command.d.ts +19 -0
  113. package/dist/im/lark/summary-command.d.ts.map +1 -0
  114. package/dist/im/lark/summary-command.js +276 -0
  115. package/dist/im/lark/summary-command.js.map +1 -0
  116. package/dist/mir-local-runtime.d.ts +40 -0
  117. package/dist/mir-local-runtime.d.ts.map +1 -1
  118. package/dist/mir-local-runtime.js +246 -52
  119. package/dist/mir-local-runtime.js.map +1 -1
  120. package/dist/mir-runner.js +26 -1
  121. package/dist/mir-runner.js.map +1 -1
  122. package/dist/services/card-prefs-store.d.ts +2 -2
  123. package/dist/services/card-prefs-store.d.ts.map +1 -1
  124. package/dist/services/card-prefs-store.js +5 -5
  125. package/dist/services/card-prefs-store.js.map +1 -1
  126. package/dist/services/chat-reply-mode-store.d.ts +6 -4
  127. package/dist/services/chat-reply-mode-store.d.ts.map +1 -1
  128. package/dist/services/chat-reply-mode-store.js +6 -4
  129. package/dist/services/chat-reply-mode-store.js.map +1 -1
  130. package/dist/services/pi-transcript.d.ts +19 -0
  131. package/dist/services/pi-transcript.d.ts.map +1 -0
  132. package/dist/services/pi-transcript.js +222 -0
  133. package/dist/services/pi-transcript.js.map +1 -0
  134. package/dist/services/summary-range-store.d.ts +21 -0
  135. package/dist/services/summary-range-store.d.ts.map +1 -0
  136. package/dist/services/summary-range-store.js +89 -0
  137. package/dist/services/summary-range-store.js.map +1 -0
  138. package/dist/services/trigger-types.d.ts +14 -2
  139. package/dist/services/trigger-types.d.ts.map +1 -1
  140. package/dist/services/trigger-types.js +15 -1
  141. package/dist/services/trigger-types.js.map +1 -1
  142. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  143. package/dist/setup/bot-config-editor.js +2 -0
  144. package/dist/setup/bot-config-editor.js.map +1 -1
  145. package/dist/types.d.ts +20 -1
  146. package/dist/types.d.ts.map +1 -1
  147. package/dist/utils/executable.d.ts +3 -0
  148. package/dist/utils/executable.d.ts.map +1 -0
  149. package/dist/utils/executable.js +24 -0
  150. package/dist/utils/executable.js.map +1 -0
  151. package/dist/worker.js +69 -5
  152. package/dist/worker.js.map +1 -1
  153. package/dist/workflows/definition.d.ts +8 -8
  154. package/dist/workflows/events/payloads.d.ts +10 -10
  155. package/dist/workflows/events/schema.d.ts +28 -28
  156. package/package.json +1 -1
@@ -98,6 +98,8 @@ const ICON = {
98
98
  details: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2.2" y="2.9" width="11.6" height="10.2" rx="1.8"/><path d="M9.9 2.9v10.2"/></svg>',
99
99
  terminal: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="1.7" y="2.7" width="12.6" height="10.6" rx="2"/><path d="M4.4 6.3 6.4 8.1 4.4 9.9"/><path d="M8.2 10.2h3.4"/></svg>',
100
100
  key: '<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="6" cy="6.1" r="3"/><path d="M8.1 8.2 13 13.1"/><path d="M11.3 11.4 12.6 10.1"/><path d="M12.7 12.8 13.7 11.8"/></svg>',
101
+ lock: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="3.2" y="7" width="9.6" height="6.2" rx="1.4"/><path d="M5.2 7V5.2a2.8 2.8 0 0 1 5.6 0V7"/></svg>',
102
+ unlock: '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="3.2" y="7" width="9.6" height="6.2" rx="1.4"/><path d="M5.2 7V5.2a2.8 2.8 0 0 1 5.2-1.4"/></svg>',
101
103
  close: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4.2 4.2 11.8 11.8"/><path d="M11.8 4.2 4.2 11.8"/></svg>',
102
104
  edit: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M10.7 3.3 12.7 5.3 6.3 11.7 3.7 12.3 4.3 9.7 10.7 3.3z"/></svg>',
103
105
  history: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.2 4.8a2 2 0 0 1 2-2h7.6a2 2 0 0 1 2 2v4.6a2 2 0 0 1-2 2H6.6l-2.9 2.4v-2.4h-.5a2 2 0 0 1-2-2z"/><path d="M5.2 6.2h5.6M5.2 8.4h3.6"/></svg>',
@@ -110,6 +112,14 @@ const ICON = {
110
112
  function cardActBtn(action, icon, label, kind = '') {
111
113
  return `<button type="button" class="card-act${kind ? ' ' + kind : ''}" data-action="${action}" title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}">${icon}</button>`;
112
114
  }
115
+ function lockActionLabel(s) {
116
+ return s.locked ? t('sessions.unlock') : t('sessions.lock');
117
+ }
118
+ function lockChipHtml(s) {
119
+ return s.locked
120
+ ? `<span class="session-lock-badge" title="${escapeHtml(t('sessions.locked'))}">${escapeHtml(t('sessions.locked'))}</span>`
121
+ : '';
122
+ }
113
123
  // Terminal access pill for a live session: a read-only "open" segment (always
114
124
  // shown) plus — for authenticated users — a "key" segment that mints the
115
125
  // writable (token-bearing) link on demand. Icon-only + tooltip keeps it compact;
@@ -254,6 +264,8 @@ function pageHtml() {
254
264
  </div>
255
265
  <div id="bulk-bar" class="bulk-bar" hidden>
256
266
  <span id="bulk-count"></span>
267
+ <button type="button" id="bulk-lock">${t('sessions.lockSelected')}</button>
268
+ <button type="button" id="bulk-unlock">${t('sessions.unlockSelected')}</button>
257
269
  <button type="button" id="bulk-close" class="contrast">${t('sessions.closeSelected')}</button>
258
270
  <button type="button" id="bulk-clear">${t('sessions.clearSelection')}</button>
259
271
  </div>
@@ -489,6 +501,8 @@ export function renderSessionsPage(root) {
489
501
  const selectAllBox = root.querySelector('#select-all');
490
502
  const bulkBar = root.querySelector('#bulk-bar');
491
503
  const bulkCountSpan = root.querySelector('#bulk-count');
504
+ const bulkLockBtn = root.querySelector('#bulk-lock');
505
+ const bulkUnlockBtn = root.querySelector('#bulk-unlock');
492
506
  const bulkCloseBtn = root.querySelector('#bulk-close');
493
507
  const bulkClearBtn = root.querySelector('#bulk-clear');
494
508
  const idleCleanupBar = root.querySelector('#idle-cleanup-bar');
@@ -828,7 +842,7 @@ export function renderSessionsPage(root) {
828
842
  <td><input type="checkbox" class="row-select" ${checked} ${closed ? 'disabled' : ''}></td>
829
843
  <td>${escapeHtml(botDisplayName(s))}</td>
830
844
  <td><span class="badge cli-${cssToken(s.cliId)}">${escapeHtml(s.cliId ?? 'unknown')}</span></td>
831
- <td><span class="status status-${escapeHtml(s.status ?? 'unknown')}">${escapeHtml(s.status ?? 'unknown')}</span></td>
845
+ <td><span class="status status-${escapeHtml(s.status ?? 'unknown')}">${escapeHtml(s.status ?? 'unknown')}</span>${lockChipHtml(s)}</td>
832
846
  <td class="token-cell">${formatTokenCount(s.tokenUsage?.in)}</td>
833
847
  <td class="token-cell">${formatTokenCount(s.tokenUsage?.out)}</td>
834
848
  <td title="${escapeHtml(String(s.title ?? ''))}">${escapeHtml(stripMentionPrefix(s.title ?? '').slice(0, 48))}</td>
@@ -872,14 +886,17 @@ export function renderSessionsPage(root) {
872
886
  const terminal = terminalHref(s);
873
887
  const signal = boardSignalLabel(s);
874
888
  const repo = repoBasename(s.workingDir);
875
- return `<article class="session-card${isSelected ? ' selected' : ''}" data-id="${escapeHtml(s.sessionId)}" aria-pressed="${isSelected}">
889
+ return `<article class="session-card${isSelected ? ' selected' : ''}${s.locked ? ' locked' : ''}" data-id="${escapeHtml(s.sessionId)}" aria-pressed="${isSelected}">
876
890
  <div class="session-card-top">
877
891
  ${botAvatarHtml({ name: botName, larkAppId: s.larkAppId, size: 'sm' })}
878
892
  <div class="session-card-title">
879
893
  <strong title="${escapeHtml(String(s.title ?? title))}">${escapeHtml(String(title).slice(0, 72))}</strong>
880
894
  <span>${escapeHtml(botName)} · ${escapeHtml(chatTitle ?? s.cliId ?? 'unknown')}</span>
881
895
  </div>
882
- <span class="status status-${escapeHtml(s.status ?? 'unknown')}">${escapeHtml(s.status ?? 'unknown')}</span>
896
+ <span class="session-card-status-group">
897
+ <span class="status status-${escapeHtml(s.status ?? 'unknown')}">${escapeHtml(s.status ?? 'unknown')}</span>
898
+ ${lockChipHtml(s)}
899
+ </span>
883
900
  </div>
884
901
  ${repo !== '-' || s.adopt ? `<div class="session-card-meta">
885
902
  ${repo !== '-' ? `<span title="${escapeHtml(s.workingDir ?? '')}">${escapeHtml(repo)}</span>` : ''}
@@ -896,6 +913,7 @@ export function renderSessionsPage(root) {
896
913
  ${cardActBtn('details', ICON.details, t('sessions.details'))}
897
914
  ${canRestartSession(s) ? cardActBtn('restart', ICON.restart, t('sessions.restart')) : ''}
898
915
  ${terminalControlsHtml(terminal)}
916
+ ${cardActBtn('lock', s.locked ? ICON.unlock : ICON.lock, lockActionLabel(s), s.locked ? 'locked' : '')}
899
917
  ${cardActBtn('close', ICON.close, t('sessions.close'), 'danger')}
900
918
  </div>
901
919
  </article>`;
@@ -963,15 +981,17 @@ export function renderSessionsPage(root) {
963
981
  // 对方部署的会话:数据是 host 快照,终端/历史/改名都在对方机器上做不了——
964
982
  // 只保留状态点与部署来源徽章,卡片仍可拖(团队共享编排)。
965
983
  const remote = typeof s.remoteDeployment === 'string' ? s.remoteDeployment : '';
966
- return `<article class="kanban-card${remote ? ' kanban-card-remote' : ''}" data-id="${escapeHtml(s.sessionId)}" tabindex="0" role="button" draggable="true">
984
+ return `<article class="kanban-card${remote ? ' kanban-card-remote' : ''}${s.locked ? ' locked' : ''}" data-id="${escapeHtml(s.sessionId)}" tabindex="0" role="button" draggable="true">
967
985
  <div class="kanban-card-top">
968
986
  <span class="badge cli-${cssToken(s.cliId)}">${escapeHtml(s.cliId ?? 'unknown')}</span>
969
987
  ${s.adopt ? '<span class="badge">adopt</span>' : ''}
988
+ ${lockChipHtml(s)}
970
989
  ${remote ? `<span class="badge kanban-remote-badge" title="${escapeHtml(t('sessions.kanban.remoteHint', { name: remote }))}">${escapeHtml(remote)}</span>` : ''}
971
990
  <span class="kanban-card-top-right">
972
991
  <span class="kanban-card-dot" data-status="${escapeHtml(status)}" title="${escapeHtml(status)}"></span>
973
992
  ${remote ? '' : `<button type="button" class="card-act kanban-card-act" data-action="history" title="${escapeHtml(t('sessions.history.title'))}" aria-label="${escapeHtml(t('sessions.history.title'))}">${ICON.history}</button>
974
993
  ${s.feishuChatLink ? `<a class="card-act kanban-card-act" href="${escapeHtml(s.feishuChatLink)}" target="_blank" rel="noopener" title="${escapeHtml(t('sessions.kanban.openFeishu'))}" aria-label="${escapeHtml(t('sessions.kanban.openFeishu'))}">${ICON.feishu}</a>` : ''}
994
+ <button type="button" class="card-act kanban-card-act${s.locked ? ' locked' : ''}" data-action="lock" title="${escapeHtml(lockActionLabel(s))}" aria-label="${escapeHtml(lockActionLabel(s))}">${s.locked ? ICON.unlock : ICON.lock}</button>
975
995
  <button type="button" class="card-act kanban-card-act" data-action="details" title="${escapeHtml(t('sessions.details'))}" aria-label="${escapeHtml(t('sessions.details'))}">${ICON.details}</button>
976
996
  ${canRestartSession(s) ? `<button type="button" class="card-act kanban-card-act" data-action="restart" title="${escapeHtml(t('sessions.restart'))}" aria-label="${escapeHtml(t('sessions.restart'))}">${ICON.restart}</button>` : ''}`}
977
997
  </span>
@@ -1516,6 +1536,11 @@ export function renderSessionsPage(root) {
1516
1536
  function syncBulkUi(rows) {
1517
1537
  bulkBar.hidden = selected.size === 0;
1518
1538
  bulkCountSpan.textContent = t('sessions.selectedCount', { count: selected.size });
1539
+ const selectedRows = [...selected]
1540
+ .map(id => store.sessions.get(id))
1541
+ .filter((r) => !!r && r.status !== 'closed');
1542
+ bulkLockBtn.disabled = !selectedRows.some(r => !r.locked);
1543
+ bulkUnlockBtn.disabled = !selectedRows.some(r => !!r.locked);
1519
1544
  const selectable = rows.filter(r => r.status !== 'closed');
1520
1545
  if (selectable.length === 0) {
1521
1546
  selectAllBox.checked = false;
@@ -1664,6 +1689,52 @@ export function renderSessionsPage(root) {
1664
1689
  closeBtn.disabled = false;
1665
1690
  }
1666
1691
  }
1692
+ function invalidateSessionViews() {
1693
+ lastBoardHtml = '';
1694
+ lastTableHtml = '';
1695
+ lastKanbanHtml = '';
1696
+ }
1697
+ async function setSessionLocked(s, locked, btn) {
1698
+ const prev = !!s.locked;
1699
+ if (prev === locked)
1700
+ return true;
1701
+ s.locked = locked;
1702
+ invalidateSessionViews();
1703
+ rerender();
1704
+ if (btn)
1705
+ btn.disabled = true;
1706
+ try {
1707
+ const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/lock`, {
1708
+ method: 'POST',
1709
+ headers: { 'content-type': 'application/json' },
1710
+ body: JSON.stringify({ locked }),
1711
+ });
1712
+ const body = await r.json().catch(() => ({}));
1713
+ if (!r.ok || body?.ok === false) {
1714
+ s.locked = prev;
1715
+ invalidateSessionViews();
1716
+ rerender();
1717
+ if (r.status !== 401)
1718
+ alert(`${t('sessions.lockFailed')}: ${body?.error ?? r.status}`);
1719
+ return false;
1720
+ }
1721
+ s.locked = !!body.locked;
1722
+ invalidateSessionViews();
1723
+ rerender();
1724
+ return true;
1725
+ }
1726
+ catch (e) {
1727
+ s.locked = prev;
1728
+ invalidateSessionViews();
1729
+ rerender();
1730
+ alert(`${t('sessions.lockFailed')}: ${e}`);
1731
+ return false;
1732
+ }
1733
+ finally {
1734
+ if (btn)
1735
+ btn.disabled = false;
1736
+ }
1737
+ }
1667
1738
  // Per-session restart cooldown. The route returns 200 the instant the IPC is
1668
1739
  // sent — long before the worker's ~500ms+ respawn — so re-enabling the button
1669
1740
  // immediately let a second restart land inside the respawn window, which trips
@@ -1764,7 +1835,10 @@ export function renderSessionsPage(root) {
1764
1835
  drawer.innerHTML = `<article>
1765
1836
  <header>
1766
1837
  <h3>${escapeHtml(stripMentionPrefix(s.title) || s.sessionId)}</h3>
1767
- <span class="status status-${escapeHtml(s.status ?? 'unknown')}">${escapeHtml(s.status ?? 'unknown')}</span>
1838
+ <span class="drawer-status-line">
1839
+ <span class="status status-${escapeHtml(s.status ?? 'unknown')}">${escapeHtml(s.status ?? 'unknown')}</span>
1840
+ ${lockChipHtml(s)}
1841
+ </span>
1768
1842
  <p><code>${escapeHtml(s.sessionId)}</code> <button data-copy="${escapeHtml(s.sessionId)}">${t('sessions.copy')}</button></p>
1769
1843
  </header>
1770
1844
  <p><b>${t('sessions.bot')}:</b> ${escapeHtml(botDisplayName(s))} · <b>${t('sessions.cli')}:</b> ${escapeHtml(s.cliId ?? '?')}</p>
@@ -1778,6 +1852,7 @@ export function renderSessionsPage(root) {
1778
1852
  <button id="history-drawer-btn" type="button">${t('sessions.history.title')}</button>
1779
1853
  ${terminalControlsHtml(terminal)}
1780
1854
  ${canRestartSession(s) ? `<button id="restart-btn" type="button">${t('sessions.restart')}</button>` : ''}
1855
+ ${!closed ? `<button id="lock-btn" type="button">${escapeHtml(lockActionLabel(s))}</button>` : ''}
1781
1856
  ${s.queued && !closed ? `<button id="start-btn" type="button" class="primary">${t('sessions.create.start')}</button>` : ''}
1782
1857
  ${closed ? `<button id="resume-btn" type="button" class="primary">${t('sessions.resume')}</button>` : ''}
1783
1858
  ${!closed ? `<button id="close-btn" type="button" class="contrast">${t('sessions.close')}</button>` : ''}
@@ -1843,6 +1918,19 @@ export function renderSessionsPage(root) {
1843
1918
  drawer.close();
1844
1919
  };
1845
1920
  }
1921
+ const lockBtn = drawer.querySelector('#lock-btn');
1922
+ if (lockBtn) {
1923
+ lockBtn.onclick = async () => {
1924
+ const next = !s.locked;
1925
+ if (await setSessionLocked(s, next, lockBtn)) {
1926
+ lockBtn.textContent = lockActionLabel(s);
1927
+ const line = drawer.querySelector('.drawer-status-line');
1928
+ if (line) {
1929
+ line.innerHTML = `<span class="status status-${escapeHtml(s.status ?? 'unknown')}">${escapeHtml(s.status ?? 'unknown')}</span>${lockChipHtml(s)}`;
1930
+ }
1931
+ }
1932
+ };
1933
+ }
1846
1934
  // 待办池会话「开始」:激活 parked 会话(发首轮、起 CLI)。
1847
1935
  const startBtn = drawer.querySelector('#start-btn');
1848
1936
  if (startBtn) {
@@ -1991,6 +2079,8 @@ export function renderSessionsPage(root) {
1991
2079
  void locateSession(s, actionButton);
1992
2080
  else if (action === 'restart')
1993
2081
  void restartSession(s, actionButton);
2082
+ else if (action === 'lock')
2083
+ void setSessionLocked(s, !s.locked, actionButton);
1994
2084
  else if (action === 'close')
1995
2085
  void closeSession(s, actionButton).then(ok => {
1996
2086
  if (ok) {
@@ -2107,6 +2197,8 @@ export function renderSessionsPage(root) {
2107
2197
  void openHistoryModal(s);
2108
2198
  else if (actionButton.dataset.action === 'restart')
2109
2199
  void restartSession(s, actionButton);
2200
+ else if (actionButton.dataset.action === 'lock')
2201
+ void setSessionLocked(s, !s.locked, actionButton);
2110
2202
  return;
2111
2203
  }
2112
2204
  if (target.closest('a, button, input, label'))
@@ -2340,6 +2432,58 @@ export function renderSessionsPage(root) {
2340
2432
  if (failed > 0)
2341
2433
  alert(`Failed: ${failed}/${ids.length}`);
2342
2434
  });
2435
+ async function setSelectedLocked(locked) {
2436
+ const rows = [...selected]
2437
+ .map(id => store.sessions.get(id))
2438
+ .filter((s) => !!s && s.status !== 'closed' && !!s.locked !== locked);
2439
+ if (rows.length === 0)
2440
+ return;
2441
+ bulkLockBtn.disabled = true;
2442
+ bulkUnlockBtn.disabled = true;
2443
+ const original = locked ? bulkLockBtn.textContent : bulkUnlockBtn.textContent;
2444
+ const activeBtn = locked ? bulkLockBtn : bulkUnlockBtn;
2445
+ let done = 0;
2446
+ let failed = 0;
2447
+ const queue = [...rows];
2448
+ activeBtn.textContent = `0/${rows.length}`;
2449
+ async function worker() {
2450
+ while (queue.length) {
2451
+ const s = queue.shift();
2452
+ const prev = !!s.locked;
2453
+ try {
2454
+ const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/lock`, {
2455
+ method: 'POST',
2456
+ headers: { 'content-type': 'application/json' },
2457
+ body: JSON.stringify({ locked }),
2458
+ });
2459
+ const body = await r.json().catch(() => ({}));
2460
+ if (!r.ok || body?.ok === false) {
2461
+ failed += 1;
2462
+ s.locked = prev;
2463
+ }
2464
+ else {
2465
+ s.locked = !!body.locked;
2466
+ }
2467
+ }
2468
+ catch {
2469
+ failed += 1;
2470
+ s.locked = prev;
2471
+ }
2472
+ finally {
2473
+ done += 1;
2474
+ activeBtn.textContent = `${done}/${rows.length}`;
2475
+ }
2476
+ }
2477
+ }
2478
+ await Promise.all(Array.from({ length: Math.min(6, rows.length) }, () => worker()));
2479
+ activeBtn.textContent = original;
2480
+ invalidateSessionViews();
2481
+ rerender();
2482
+ if (failed > 0)
2483
+ alert(`${t('sessions.lockFailed')}: ${failed}/${rows.length}`);
2484
+ }
2485
+ bulkLockBtn.addEventListener('click', () => void setSelectedLocked(true));
2486
+ bulkUnlockBtn.addEventListener('click', () => void setSelectedLocked(false));
2343
2487
  idleCleanupThreshold.addEventListener('click', e => {
2344
2488
  // Don't let a threshold switch mid-cleanup repaint the count/pill against a
2345
2489
  // different threshold than the request that's already running.