botmux 2.62.0 → 2.63.1

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 (137) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +2 -6
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +12 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +12 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/command-handler.d.ts.map +1 -1
  9. package/dist/core/command-handler.js +5 -1
  10. package/dist/core/command-handler.js.map +1 -1
  11. package/dist/core/dashboard-rows.d.ts +11 -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-activity.d.ts +16 -0
  16. package/dist/core/session-activity.d.ts.map +1 -1
  17. package/dist/core/session-activity.js +35 -0
  18. package/dist/core/session-activity.js.map +1 -1
  19. package/dist/core/trigger-session.d.ts.map +1 -1
  20. package/dist/core/trigger-session.js +9 -10
  21. package/dist/core/trigger-session.js.map +1 -1
  22. package/dist/core/worker-pool.d.ts.map +1 -1
  23. package/dist/core/worker-pool.js +3 -0
  24. package/dist/core/worker-pool.js.map +1 -1
  25. package/dist/daemon.d.ts.map +1 -1
  26. package/dist/daemon.js +8 -1
  27. package/dist/daemon.js.map +1 -1
  28. package/dist/dashboard/auth.d.ts +6 -0
  29. package/dist/dashboard/auth.d.ts.map +1 -1
  30. package/dist/dashboard/auth.js +33 -12
  31. package/dist/dashboard/auth.js.map +1 -1
  32. package/dist/dashboard/connector-api.d.ts.map +1 -1
  33. package/dist/dashboard/connector-api.js +22 -17
  34. package/dist/dashboard/connector-api.js.map +1 -1
  35. package/dist/dashboard/public-redact.d.ts +16 -0
  36. package/dist/dashboard/public-redact.d.ts.map +1 -0
  37. package/dist/dashboard/public-redact.js +62 -0
  38. package/dist/dashboard/public-redact.js.map +1 -0
  39. package/dist/dashboard/web/app.d.ts +1 -0
  40. package/dist/dashboard/web/app.d.ts.map +1 -1
  41. package/dist/dashboard/web/app.js +171 -25
  42. package/dist/dashboard/web/app.js.map +1 -1
  43. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  44. package/dist/dashboard/web/bot-defaults.js +136 -64
  45. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  46. package/dist/dashboard/web/connectors.d.ts.map +1 -1
  47. package/dist/dashboard/web/connectors.js +120 -25
  48. package/dist/dashboard/web/connectors.js.map +1 -1
  49. package/dist/dashboard/web/cyber-fx.d.ts +6 -0
  50. package/dist/dashboard/web/cyber-fx.d.ts.map +1 -0
  51. package/dist/dashboard/web/cyber-fx.js +289 -0
  52. package/dist/dashboard/web/cyber-fx.js.map +1 -0
  53. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  54. package/dist/dashboard/web/i18n.js +146 -8
  55. package/dist/dashboard/web/i18n.js.map +1 -1
  56. package/dist/dashboard/web/overview.d.ts.map +1 -1
  57. package/dist/dashboard/web/overview.js +206 -48
  58. package/dist/dashboard/web/overview.js.map +1 -1
  59. package/dist/dashboard/web/preferences.d.ts +16 -0
  60. package/dist/dashboard/web/preferences.d.ts.map +1 -1
  61. package/dist/dashboard/web/preferences.js +72 -1
  62. package/dist/dashboard/web/preferences.js.map +1 -1
  63. package/dist/dashboard/web/roles.js +3 -3
  64. package/dist/dashboard/web/roles.js.map +1 -1
  65. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  66. package/dist/dashboard/web/sessions.js +404 -64
  67. package/dist/dashboard/web/sessions.js.map +1 -1
  68. package/dist/dashboard/web/settings.d.ts +2 -0
  69. package/dist/dashboard/web/settings.d.ts.map +1 -0
  70. package/dist/dashboard/web/settings.js +135 -0
  71. package/dist/dashboard/web/settings.js.map +1 -0
  72. package/dist/dashboard/web/skin-intro.d.ts +4 -0
  73. package/dist/dashboard/web/skin-intro.d.ts.map +1 -0
  74. package/dist/dashboard/web/skin-intro.js +49 -0
  75. package/dist/dashboard/web/skin-intro.js.map +1 -0
  76. package/dist/dashboard/web/team-federation.js +4 -4
  77. package/dist/dashboard/web/team-federation.js.map +1 -1
  78. package/dist/dashboard/web/theme-menu.d.ts +3 -0
  79. package/dist/dashboard/web/theme-menu.d.ts.map +1 -0
  80. package/dist/dashboard/web/theme-menu.js +97 -0
  81. package/dist/dashboard/web/theme-menu.js.map +1 -0
  82. package/dist/dashboard/web/ui.d.ts +16 -2
  83. package/dist/dashboard/web/ui.d.ts.map +1 -1
  84. package/dist/dashboard/web/ui.js +137 -7
  85. package/dist/dashboard/web/ui.js.map +1 -1
  86. package/dist/dashboard/webhook-routes.d.ts +1 -5
  87. package/dist/dashboard/webhook-routes.d.ts.map +1 -1
  88. package/dist/dashboard/webhook-routes.js +127 -76
  89. package/dist/dashboard/webhook-routes.js.map +1 -1
  90. package/dist/dashboard-web/app.js +704 -512
  91. package/dist/dashboard-web/index.html +19 -14
  92. package/dist/dashboard-web/skins/bluearchive-hero.webp +0 -0
  93. package/dist/dashboard-web/skins/dragonball-goku.webp +0 -0
  94. package/dist/dashboard-web/skins/dragonball-wukong.webp +0 -0
  95. package/dist/dashboard-web/skins/fallout-vaultboy.webp +0 -0
  96. package/dist/dashboard-web/skins/genshin-breeze.webp +0 -0
  97. package/dist/dashboard-web/skins/ikun-hero.webp +0 -0
  98. package/dist/dashboard-web/skins/prts-priestess.webp +0 -0
  99. package/dist/dashboard-web/skins/zzz-hero.webp +0 -0
  100. package/dist/dashboard-web/skins/zzz-pattern.svg +25 -0
  101. package/dist/dashboard-web/style.css +2870 -16
  102. package/dist/dashboard.js +91 -31
  103. package/dist/dashboard.js.map +1 -1
  104. package/dist/global-config.d.ts +15 -0
  105. package/dist/global-config.d.ts.map +1 -1
  106. package/dist/global-config.js +51 -2
  107. package/dist/global-config.js.map +1 -1
  108. package/dist/im/lark/card-builder.d.ts.map +1 -1
  109. package/dist/im/lark/card-builder.js +17 -3
  110. package/dist/im/lark/card-builder.js.map +1 -1
  111. package/dist/im/lark/card-handler.d.ts.map +1 -1
  112. package/dist/im/lark/card-handler.js +5 -0
  113. package/dist/im/lark/card-handler.js.map +1 -1
  114. package/dist/services/bridge-fallback-gate.d.ts +1 -1
  115. package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
  116. package/dist/services/bridge-fallback-gate.js +26 -13
  117. package/dist/services/bridge-fallback-gate.js.map +1 -1
  118. package/dist/services/connector-store.d.ts +2 -3
  119. package/dist/services/connector-store.d.ts.map +1 -1
  120. package/dist/services/connector-store.js.map +1 -1
  121. package/dist/services/trigger-types.d.ts +1 -0
  122. package/dist/services/trigger-types.d.ts.map +1 -1
  123. package/dist/services/trigger-types.js.map +1 -1
  124. package/dist/services/webhook-lifecycle-extractors.d.ts +4 -13
  125. package/dist/services/webhook-lifecycle-extractors.d.ts.map +1 -1
  126. package/dist/services/webhook-lifecycle-extractors.js +8 -23
  127. package/dist/services/webhook-lifecycle-extractors.js.map +1 -1
  128. package/dist/utils/screen-analyzer.d.ts +8 -0
  129. package/dist/utils/screen-analyzer.d.ts.map +1 -1
  130. package/dist/utils/screen-analyzer.js +32 -11
  131. package/dist/utils/screen-analyzer.js.map +1 -1
  132. package/dist/worker.js +2 -2
  133. package/dist/worker.js.map +1 -1
  134. package/dist/workflows/trigger-from-envelope.d.ts.map +1 -1
  135. package/dist/workflows/trigger-from-envelope.js +1 -0
  136. package/dist/workflows/trigger-from-envelope.js.map +1 -1
  137. package/package.json +2 -2
@@ -1,6 +1,7 @@
1
- // Sessions page: filter bar, table, detail drawer with locate/resume/close.
1
+ // Sessions page: filter bar, status board/table, detail drawer with locate/resume/close.
2
+ import { readStoredBoardOrder, readStoredSessionsViewMode, writeStoredBoardOrder, writeStoredSessionsViewMode, } from './preferences.js';
2
3
  import { store } from './store.js';
3
- import { escapeHtml, relTime, t } from './ui.js';
4
+ import { botDisplayName, botOrbStyle, chatDisplayTitle, escapeHtml, loadNameMaps, relTime, stripMentionPrefix, t, } from './ui.js';
4
5
  function th(sort, label) {
5
6
  return `<th data-sort="${sort}" data-label="${escapeHtml(label)}">${escapeHtml(label)}</th>`;
6
7
  }
@@ -21,16 +22,54 @@ const CLI_FILTER_OPTIONS = [
21
22
  'coco',
22
23
  'unknown',
23
24
  ];
25
+ const BOARD_COLUMNS = [
26
+ { id: 'needs-you', labelKey: 'sessions.board.needsYou', hintKey: 'sessions.board.needsYouHint' },
27
+ { id: 'starting', labelKey: 'sessions.board.starting', hintKey: 'sessions.board.startingHint' },
28
+ { id: 'working', labelKey: 'sessions.board.working', hintKey: 'sessions.board.workingHint' },
29
+ { id: 'idle', labelKey: 'sessions.board.idle', hintKey: 'sessions.board.idleHint' },
30
+ ];
31
+ function cssToken(value) {
32
+ return String(value ?? 'unknown').toLowerCase().replace(/[^a-z0-9_-]/g, '-');
33
+ }
34
+ function repoBasename(workingDir) {
35
+ const value = String(workingDir ?? '').trim();
36
+ if (!value)
37
+ return '-';
38
+ const parts = value.replace(/\\/g, '/').split('/').filter(Boolean);
39
+ return parts.at(-1) ?? value;
40
+ }
41
+ function terminalHref(s) {
42
+ if (!s.webPort)
43
+ return null;
44
+ const port = s.proxyPort ?? s.webPort;
45
+ const suffix = s.proxyPort ? `/s/${encodeURIComponent(s.sessionId)}` : '';
46
+ return `http://${location.hostname}:${port}${suffix}`;
47
+ }
48
+ function deriveSessionBoardColumn(s) {
49
+ if (s.status === 'closed')
50
+ return null;
51
+ if (s.pendingRepo || s.tuiPromptActive || s.status === 'limited')
52
+ return 'needs-you';
53
+ if (s.status === 'starting')
54
+ return 'starting';
55
+ if (s.status === 'working' || s.status === 'analyzing' || s.status === 'active')
56
+ return 'working';
57
+ return 'idle';
58
+ }
59
+ // CLI 多选收进一个下拉 chip:默认全选时只占一个胶囊位,点开才展开
60
+ // 全部选项,避免 15 个 checkbox 铺满一整行。
24
61
  export function renderCliFilterGroup() {
25
- return `<div class="filter-check-group" role="group" aria-label="${t('sessions.cli')}">
26
- <span class="filter-check-label">${t('sessions.cli')}</span>
27
- ${CLI_FILTER_OPTIONS.map(cli => `
28
- <label class="filter-check">
29
- <input type="checkbox" name="cli" value="${escapeHtml(cli)}" checked>
30
- <span>${escapeHtml(cli)}</span>
31
- </label>
32
- `).join('')}
33
- </div>`;
62
+ return `<details class="filter-cli">
63
+ <summary>${t('sessions.cli')} · <b id="cli-filter-count">${t('common.all')}</b></summary>
64
+ <div class="filter-cli-pop" role="group" aria-label="${t('sessions.cli')}">
65
+ ${CLI_FILTER_OPTIONS.map(cli => `
66
+ <label class="filter-check">
67
+ <input type="checkbox" name="cli" value="${escapeHtml(cli)}" checked>
68
+ <span>${escapeHtml(cli)}</span>
69
+ </label>
70
+ `).join('')}
71
+ </div>
72
+ </details>`;
34
73
  }
35
74
  function pageHtml() {
36
75
  return `<section class="page">
@@ -40,6 +79,10 @@ function pageHtml() {
40
79
  <h1>${t('sessions.title')}</h1>
41
80
  <p>${t('sessions.subtitle')}</p>
42
81
  </div>
82
+ <div class="segmented sessions-view-toggle" role="group" aria-label="${t('sessions.viewMode')}">
83
+ <button type="button" data-view="board">${t('sessions.viewBoard')}</button>
84
+ <button type="button" data-view="table">${t('sessions.viewTable')}</button>
85
+ </div>
43
86
  </div>
44
87
  <form id="filters" class="filters sessions-filters">
45
88
  <input type="search" name="q" placeholder="${t('sessions.search')}" />
@@ -53,8 +96,8 @@ function pageHtml() {
53
96
  <option value="yes">${t('sessions.adoptYes')}</option>
54
97
  <option value="no">${t('sessions.adoptNo')}</option>
55
98
  </select>
56
- <label class="filter-toggle"><input type="checkbox" name="active" checked> ${t('sessions.activeOnly')}</label>
57
99
  ${renderCliFilterGroup()}
100
+ <label class="filter-toggle"><input type="checkbox" name="active" checked> <span>${t('sessions.activeOnly')}</span></label>
58
101
  </form>
59
102
  <div id="bulk-bar" class="bulk-bar" hidden>
60
103
  <span id="bulk-count"></span>
@@ -76,6 +119,7 @@ function pageHtml() {
76
119
  </tr></thead>
77
120
  <tbody></tbody>
78
121
  </table>
122
+ <div id="sessions-board" class="sessions-board" hidden></div>
79
123
  <dialog id="drawer"></dialog>
80
124
  </section>`;
81
125
  }
@@ -90,18 +134,60 @@ export function renderSessionsPage(root) {
90
134
  const bulkCloseBtn = root.querySelector('#bulk-close');
91
135
  const bulkClearBtn = root.querySelector('#bulk-clear');
92
136
  const table = root.querySelector('#sessions-table');
137
+ const board = root.querySelector('#sessions-board');
138
+ const viewButtons = root.querySelectorAll('[data-view]');
93
139
  const selected = new Set();
94
140
  let sortKey = 'lastMessageAt';
95
141
  let sortDir = 'desc';
142
+ let viewMode = readStoredSessionsViewMode(window.localStorage);
143
+ // 列顺序用户可调(拖列头 / ‹› 按钮),localStorage 持久化
144
+ let boardOrder = readStoredBoardOrder(window.localStorage);
145
+ let dragColId = null;
146
+ // 防闪烁:上次渲染的 HTML 快照(内容没变就跳过 innerHTML),以及
147
+ // 入场动画是否已播过(只在首次渲染播一轮)。
148
+ let lastBoardHtml = '';
149
+ let lastTableHtml = '';
150
+ let boardAnimated = false;
151
+ function orderedBoardColumns() {
152
+ return boardOrder
153
+ .map(id => BOARD_COLUMNS.find(c => c.id === id))
154
+ .filter((c) => !!c);
155
+ }
156
+ function moveColumn(id, delta) {
157
+ const from = boardOrder.indexOf(id);
158
+ const to = from + delta;
159
+ if (from < 0 || to < 0 || to >= boardOrder.length)
160
+ return;
161
+ const next = [...boardOrder];
162
+ next.splice(from, 1);
163
+ next.splice(to, 0, id);
164
+ boardOrder = next;
165
+ writeStoredBoardOrder(window.localStorage, boardOrder);
166
+ rerender();
167
+ }
168
+ function moveColumnTo(id, targetId) {
169
+ if (id === targetId)
170
+ return;
171
+ const from = boardOrder.indexOf(id);
172
+ const to = boardOrder.indexOf(targetId);
173
+ if (from < 0 || to < 0)
174
+ return;
175
+ const next = [...boardOrder];
176
+ next.splice(from, 1);
177
+ next.splice(to, 0, id);
178
+ boardOrder = next;
179
+ writeStoredBoardOrder(window.localStorage, boardOrder);
180
+ rerender();
181
+ }
96
182
  function rowHtml(s) {
97
183
  const closed = s.status === 'closed';
98
184
  const checked = selected.has(s.sessionId) ? 'checked' : '';
99
185
  return `<tr data-id="${escapeHtml(s.sessionId)}">
100
186
  <td><input type="checkbox" class="row-select" ${checked} ${closed ? 'disabled' : ''}></td>
101
- <td>${escapeHtml(s.botName ?? '')}</td>
102
- <td><span class="badge cli-${escapeHtml(s.cliId ?? 'unknown')}">${escapeHtml(s.cliId ?? 'unknown')}</span></td>
187
+ <td>${escapeHtml(botDisplayName(s))}</td>
188
+ <td><span class="badge cli-${cssToken(s.cliId)}">${escapeHtml(s.cliId ?? 'unknown')}</span></td>
103
189
  <td><span class="status status-${escapeHtml(s.status ?? 'unknown')}">${escapeHtml(s.status ?? 'unknown')}</span></td>
104
- <td>${escapeHtml((s.title ?? '').slice(0, 48))}</td>
190
+ <td title="${escapeHtml(String(s.title ?? ''))}">${escapeHtml(stripMentionPrefix(s.title ?? '').slice(0, 48))}</td>
105
191
  <td title="${escapeHtml(s.workingDir ?? '')}">${escapeHtml((s.workingDir ?? '').slice(-34))}</td>
106
192
  <td>${relTime(s.spawnedAt)}</td>
107
193
  <td>${relTime(s.lastMessageAt)}</td>
@@ -109,6 +195,105 @@ export function renderSessionsPage(root) {
109
195
  <td><button class="open" type="button">${t('sessions.details')}</button></td>
110
196
  </tr>`;
111
197
  }
198
+ // chat-scope(普通群/单聊平铺回复)没有话题可定位——「定位话题」换成直接
199
+ // 打开群聊的 applink。旧 daemon 上报的行没有 scope 字段,保持定位行为。
200
+ function chatScopeLink(s) {
201
+ if (s.scope !== 'chat' || !s.feishuChatLink)
202
+ return null;
203
+ return `<a class="btn-link" href="${escapeHtml(s.feishuChatLink)}" target="_blank" rel="noopener">${t('sessions.openChat')}</a>`;
204
+ }
205
+ function boardSignalLabel(s) {
206
+ if (s.pendingRepo)
207
+ return t('sessions.board.signalRepo');
208
+ if (s.tuiPromptActive)
209
+ return t('sessions.board.signalPrompt');
210
+ if (s.status === 'limited')
211
+ return t('sessions.board.signalLimited');
212
+ return '';
213
+ }
214
+ function boardCardHtml(s) {
215
+ const isSelected = selected.has(s.sessionId);
216
+ // title 剥掉开头的 "@bot" mention,只留消息内容;副行的群聊名比 cliId 更
217
+ // 有辨识度(同一 bot 可能在多个群干活),查不到群名时回退 cliId。
218
+ const title = stripMentionPrefix(s.title) || s.sessionId;
219
+ const botName = botDisplayName(s);
220
+ const chatTitle = chatDisplayTitle(s);
221
+ const terminal = terminalHref(s);
222
+ const signal = boardSignalLabel(s);
223
+ return `<article class="session-card${isSelected ? ' selected' : ''}" data-id="${escapeHtml(s.sessionId)}" aria-pressed="${isSelected}">
224
+ <div class="session-card-top">
225
+ <span class="orb-avatar orb-avatar-sm" style="${botOrbStyle(botName)}" aria-hidden="true"></span>
226
+ <div class="session-card-title">
227
+ <strong title="${escapeHtml(String(s.title ?? title))}">${escapeHtml(String(title).slice(0, 72))}</strong>
228
+ <span>${escapeHtml(botName)} · ${escapeHtml(chatTitle ?? s.cliId ?? 'unknown')}</span>
229
+ </div>
230
+ <span class="status status-${escapeHtml(s.status ?? 'unknown')}">${escapeHtml(s.status ?? 'unknown')}</span>
231
+ </div>
232
+ <div class="session-card-meta">
233
+ <span title="${escapeHtml(s.workingDir ?? '')}">${escapeHtml(repoBasename(s.workingDir))}</span>
234
+ ${s.adopt ? '<span class="badge">adopt</span>' : ''}
235
+ </div>
236
+ <div class="session-card-time">
237
+ <span>${escapeHtml(t('sessions.last'))}: ${relTime(s.lastMessageAt)}</span>
238
+ ${signal ? `<span class="session-signal">${escapeHtml(signal)} · ${relTime(s.lastMessageAt)}</span>` : ''}
239
+ </div>
240
+ <div class="session-card-actions">
241
+ ${chatScopeLink(s) ?? `<button type="button" data-action="locate">${t('sessions.locate')}</button>`}
242
+ <button type="button" data-action="details">${t('sessions.details')}</button>
243
+ ${terminal ? `<a class="btn-link primary" href="${escapeHtml(terminal)}" target="_blank" rel="noopener">${t('sessions.openTerminal')}</a>` : ''}
244
+ <button type="button" class="contrast" data-action="close">${t('sessions.close')}</button>
245
+ </div>
246
+ </article>`;
247
+ }
248
+ function compareBoardRows(a, b, column) {
249
+ const av = Number(a.lastMessageAt ?? 0);
250
+ const bv = Number(b.lastMessageAt ?? 0);
251
+ if (av !== bv)
252
+ return column === 'needs-you' ? av - bv : bv - av;
253
+ return String(a.title ?? a.sessionId).localeCompare(String(b.title ?? b.sessionId));
254
+ }
255
+ function renderBoard(rows) {
256
+ const groups = new Map(BOARD_COLUMNS.map(c => [c.id, []]));
257
+ for (const row of rows) {
258
+ const column = deriveSessionBoardColumn(row);
259
+ if (column)
260
+ groups.get(column).push(row);
261
+ }
262
+ const columns = orderedBoardColumns();
263
+ const html = columns.map((column, idx) => {
264
+ const columnRows = (groups.get(column.id) ?? []).sort((a, b) => compareBoardRows(a, b, column.id));
265
+ return `<section class="session-board-column session-board-${column.id}" data-col="${column.id}">
266
+ <header draggable="true" title="${escapeHtml(t('sessions.board.dragHint'))}">
267
+ <div>
268
+ <h2>${escapeHtml(t(column.labelKey))}</h2>
269
+ <p>${escapeHtml(t(column.hintKey))}</p>
270
+ </div>
271
+ <span class="session-board-head-right">
272
+ <span class="session-board-move">
273
+ <button type="button" data-move-col="${column.id}" data-dir="-1"
274
+ aria-label="${escapeHtml(t('sessions.board.moveLeft'))}" ${idx === 0 ? 'disabled' : ''}>‹</button>
275
+ <button type="button" data-move-col="${column.id}" data-dir="1"
276
+ aria-label="${escapeHtml(t('sessions.board.moveRight'))}" ${idx === columns.length - 1 ? 'disabled' : ''}>›</button>
277
+ </span>
278
+ <span class="session-board-count">${columnRows.length}</span>
279
+ </span>
280
+ </header>
281
+ <div class="session-board-list">
282
+ ${columnRows.length ? columnRows.map(boardCardHtml).join('') : `<div class="session-board-empty">${t('sessions.board.emptyColumn')}</div>`}
283
+ </div>
284
+ </section>`;
285
+ }).join('');
286
+ // SSE 事件(心跳/无关字段 patch)远多于真正的可见变化——内容没变就不碰
287
+ // DOM,避免 backdrop-blur 面板整板重排造成的闪烁。
288
+ if (html === lastBoardHtml)
289
+ return;
290
+ lastBoardHtml = html;
291
+ board.innerHTML = html;
292
+ // 入场动画只在看板第一次画出来时播一轮(.board-enter),之后的状态更新
293
+ // 重绘不再整板重播——那就是「每次状态更新页面闪一下」的来源。
294
+ board.classList.toggle('board-enter', !boardAnimated);
295
+ boardAnimated = true;
296
+ }
112
297
  function filtered() {
113
298
  const f = new FormData(filtersForm);
114
299
  const q = (f.get('q') ?? '').toLowerCase();
@@ -171,6 +356,23 @@ export function renderSessionsPage(root) {
171
356
  selectAllBox.checked = selectedInView === selectable.length;
172
357
  selectAllBox.indeterminate = selectedInView > 0 && selectedInView < selectable.length;
173
358
  }
359
+ function paintViewToggle() {
360
+ viewButtons.forEach(btn => {
361
+ const active = btn.dataset.view === viewMode;
362
+ btn.classList.toggle('active', active);
363
+ btn.setAttribute('aria-pressed', String(active));
364
+ });
365
+ }
366
+ // CLI 下拉 chip 上的已选计数:全选时显示「全部」,否则显示 N/总数
367
+ function paintCliFilterCount() {
368
+ const countEl = filtersForm.querySelector('#cli-filter-count');
369
+ if (!countEl)
370
+ return;
371
+ const boxes = [...filtersForm.querySelectorAll('input[name="cli"]')];
372
+ const checked = boxes.filter(b => b.checked).length;
373
+ countEl.textContent = checked === boxes.length ? t('common.all') : `${checked}/${boxes.length}`;
374
+ countEl.classList.toggle('cli-filter-active', checked !== boxes.length);
375
+ }
174
376
  function rerender() {
175
377
  const rows = filtered();
176
378
  for (const sid of [...selected]) {
@@ -178,28 +380,113 @@ export function renderSessionsPage(root) {
178
380
  if (!s || s.status === 'closed')
179
381
  selected.delete(sid);
180
382
  }
181
- tbody.innerHTML = rows.length
182
- ? rows.map(rowHtml).join('')
183
- : `<tr><td colspan="10" class="empty">${t('sessions.empty')}</td></tr>`;
383
+ const boardRows = rows.filter(r => r.status !== 'closed');
384
+ const visibleRows = viewMode === 'board' ? boardRows : rows;
385
+ table.hidden = viewMode !== 'table';
386
+ board.hidden = viewMode !== 'board';
387
+ if (viewMode === 'table') {
388
+ const tableHtml = rows.length
389
+ ? rows.map(rowHtml).join('')
390
+ : `<tr><td colspan="10" class="empty">${t('sessions.empty')}</td></tr>`;
391
+ if (tableHtml !== lastTableHtml) {
392
+ lastTableHtml = tableHtml;
393
+ tbody.innerHTML = tableHtml;
394
+ }
395
+ }
396
+ else {
397
+ renderBoard(boardRows);
398
+ }
399
+ paintViewToggle();
184
400
  paintSortHeaders();
185
- syncBulkUi(rows);
401
+ paintCliFilterCount();
402
+ syncBulkUi(visibleRows);
403
+ }
404
+ async function locateSession(s, locateBtn) {
405
+ if (locateBtn) {
406
+ locateBtn.disabled = true;
407
+ locateBtn.textContent = t('sessions.locating');
408
+ }
409
+ try {
410
+ const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/locate`, { method: 'POST' });
411
+ const body = await r.json();
412
+ if (body.ok) {
413
+ if (!locateBtn)
414
+ return;
415
+ let left = 30;
416
+ locateBtn.textContent = t('sessions.cooldown', { seconds: left });
417
+ const tick = setInterval(() => {
418
+ left -= 1;
419
+ if (left <= 0) {
420
+ clearInterval(tick);
421
+ locateBtn.disabled = false;
422
+ locateBtn.textContent = t('sessions.locate');
423
+ }
424
+ else {
425
+ locateBtn.textContent = t('sessions.cooldown', { seconds: left });
426
+ }
427
+ }, 1000);
428
+ }
429
+ else {
430
+ alert(`Locate failed: ${body.error ?? r.status}`);
431
+ if (locateBtn) {
432
+ locateBtn.disabled = false;
433
+ locateBtn.textContent = t('sessions.locate');
434
+ }
435
+ }
436
+ }
437
+ catch (e) {
438
+ alert(`Locate error: ${e}`);
439
+ if (locateBtn) {
440
+ locateBtn.disabled = false;
441
+ locateBtn.textContent = t('sessions.locate');
442
+ }
443
+ }
444
+ }
445
+ async function closeSession(s, closeBtn) {
446
+ if (!confirm(t('sessions.closeConfirm')))
447
+ return false;
448
+ if (closeBtn)
449
+ closeBtn.disabled = true;
450
+ try {
451
+ // 与批量关闭同口径:401(只读访客)/ 5xx 都不能当成功——否则抽屉
452
+ // 静默关闭、board 重绘,用户以为关掉了。401 的提示由全局 fetch
453
+ // patch 弹只读 toast,这里只负责不误报成功。
454
+ const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/close`, { method: 'POST' });
455
+ const body = await r.json().catch(() => ({}));
456
+ if (!r.ok || body?.ok === false) {
457
+ if (r.status !== 401)
458
+ alert(`Close failed: ${body?.error ?? r.status}`);
459
+ return false;
460
+ }
461
+ return true;
462
+ }
463
+ catch (e) {
464
+ alert(`Close error: ${e}`);
465
+ return false;
466
+ }
467
+ finally {
468
+ if (closeBtn)
469
+ closeBtn.disabled = false;
470
+ }
186
471
  }
187
472
  function openDrawer(s) {
188
473
  const closed = s.status === 'closed';
474
+ const terminal = terminalHref(s);
189
475
  drawer.innerHTML = `<article>
190
476
  <header>
191
- <h3>${escapeHtml(s.title ?? s.sessionId)}</h3>
477
+ <h3>${escapeHtml(stripMentionPrefix(s.title) || s.sessionId)}</h3>
192
478
  <span class="status status-${escapeHtml(s.status ?? 'unknown')}">${escapeHtml(s.status ?? 'unknown')}</span>
193
479
  <p><code>${escapeHtml(s.sessionId)}</code> <button data-copy="${escapeHtml(s.sessionId)}">${t('sessions.copy')}</button></p>
194
480
  </header>
195
- <p><b>${t('sessions.bot')}:</b> ${escapeHtml(s.botName ?? '-')} · <b>${t('sessions.cli')}:</b> ${escapeHtml(s.cliId ?? '?')}</p>
481
+ <p><b>${t('sessions.bot')}:</b> ${escapeHtml(botDisplayName(s))} · <b>${t('sessions.cli')}:</b> ${escapeHtml(s.cliId ?? '?')}</p>
482
+ ${chatDisplayTitle(s) ? `<p><b>${t('sessions.chat')}:</b> ${escapeHtml(chatDisplayTitle(s))}</p>` : ''}
196
483
  <p><b>chatId:</b> <code>${escapeHtml(s.chatId ?? '')}</code> <button data-copy="${escapeHtml(s.chatId ?? '')}">${t('sessions.copy')}</button></p>
197
484
  <p><b>rootMessageId:</b> <code>${escapeHtml(s.rootMessageId ?? '')}</code> <button data-copy="${escapeHtml(s.rootMessageId ?? '')}">${t('sessions.copy')}</button></p>
198
485
  ${s.threadId ? `<p><b>threadId:</b> <code>${escapeHtml(s.threadId)}</code></p>` : ''}
199
486
  <p><b>${t('sessions.workingDir')}:</b> ${escapeHtml(s.workingDir ?? '-')}</p>
200
487
  <div class="actions">
201
- <button id="locate-btn" type="button">${t('sessions.locate')}</button>
202
- ${s.webPort ? `<a class="btn-link primary" href="http://${escapeHtml(location.hostname)}:${s.proxyPort ?? s.webPort}${s.proxyPort ? `/s/${encodeURIComponent(s.sessionId)}` : ''}" target="_blank" rel="noopener">${t('sessions.openTerminal')}</a>` : ''}
488
+ ${chatScopeLink(s) ?? `<button id="locate-btn" type="button">${t('sessions.locate')}</button>`}
489
+ ${terminal ? `<a class="btn-link primary" href="${escapeHtml(terminal)}" target="_blank" rel="noopener">${t('sessions.openTerminal')}</a>` : ''}
203
490
  ${closed ? `<button id="resume-btn" type="button" class="primary">${t('sessions.resume')}</button>` : ''}
204
491
  ${!closed ? `<button id="close-btn" type="button" class="contrast">${t('sessions.close')}</button>` : ''}
205
492
  </div>
@@ -214,39 +501,7 @@ export function renderSessionsPage(root) {
214
501
  });
215
502
  const locateBtn = drawer.querySelector('#locate-btn');
216
503
  if (locateBtn) {
217
- locateBtn.onclick = async () => {
218
- locateBtn.disabled = true;
219
- locateBtn.textContent = t('sessions.locating');
220
- try {
221
- const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/locate`, { method: 'POST' });
222
- const body = await r.json();
223
- if (body.ok) {
224
- let left = 30;
225
- locateBtn.textContent = t('sessions.cooldown', { seconds: left });
226
- const tick = setInterval(() => {
227
- left -= 1;
228
- if (left <= 0) {
229
- clearInterval(tick);
230
- locateBtn.disabled = false;
231
- locateBtn.textContent = t('sessions.locate');
232
- }
233
- else {
234
- locateBtn.textContent = t('sessions.cooldown', { seconds: left });
235
- }
236
- }, 1000);
237
- }
238
- else {
239
- alert(`Locate failed: ${body.error ?? r.status}`);
240
- locateBtn.disabled = false;
241
- locateBtn.textContent = t('sessions.locate');
242
- }
243
- }
244
- catch (e) {
245
- alert(`Locate error: ${e}`);
246
- locateBtn.disabled = false;
247
- locateBtn.textContent = t('sessions.locate');
248
- }
249
- };
504
+ locateBtn.onclick = () => void locateSession(s, locateBtn);
250
505
  }
251
506
  const resumeBtn = drawer.querySelector('#resume-btn');
252
507
  if (resumeBtn) {
@@ -271,15 +526,8 @@ export function renderSessionsPage(root) {
271
526
  const closeBtn = drawer.querySelector('#close-btn');
272
527
  if (closeBtn) {
273
528
  closeBtn.onclick = async () => {
274
- if (!confirm(t('sessions.closeConfirm')))
275
- return;
276
- closeBtn.disabled = true;
277
- try {
278
- await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/close`, { method: 'POST' });
279
- }
280
- finally {
529
+ if (await closeSession(s, closeBtn))
281
530
  drawer.close();
282
- }
283
531
  };
284
532
  }
285
533
  drawer.showModal();
@@ -308,6 +556,96 @@ export function renderSessionsPage(root) {
308
556
  if (s)
309
557
  openDrawer(s);
310
558
  });
559
+ board.addEventListener('click', e => {
560
+ const target = e.target;
561
+ // 列头 ‹/› 按钮:交换列顺序(拖拽的键盘/触屏兜底)
562
+ const moveBtn = target.closest('button[data-move-col]');
563
+ if (moveBtn) {
564
+ moveColumn(moveBtn.dataset.moveCol, Number(moveBtn.dataset.dir));
565
+ return;
566
+ }
567
+ const card = target.closest('.session-card[data-id]');
568
+ if (!card)
569
+ return;
570
+ const s = store.sessions.get(card.dataset.id);
571
+ if (!s)
572
+ return;
573
+ const actionButton = target.closest('button[data-action]');
574
+ if (actionButton) {
575
+ const action = actionButton.dataset.action;
576
+ if (action === 'details')
577
+ openDrawer(s);
578
+ else if (action === 'locate')
579
+ void locateSession(s, actionButton);
580
+ else if (action === 'close')
581
+ void closeSession(s, actionButton).then(ok => {
582
+ if (ok) {
583
+ selected.delete(s.sessionId);
584
+ rerender();
585
+ }
586
+ });
587
+ return;
588
+ }
589
+ // Clicking the card body toggles selection (the Feishu topic link lives
590
+ // behind the 定位 button) — no checkbox, the whole card is the target.
591
+ if (target.closest('a, button, input, label'))
592
+ return;
593
+ if (selected.has(s.sessionId))
594
+ selected.delete(s.sessionId);
595
+ else
596
+ selected.add(s.sessionId);
597
+ card.classList.toggle('selected', selected.has(s.sessionId));
598
+ card.setAttribute('aria-pressed', String(selected.has(s.sessionId)));
599
+ syncBulkUi(filtered().filter(r => r.status !== 'closed'));
600
+ });
601
+ // ── 列头拖拽排序:拖起一列,放到目标列的位置 ─────────────────────────────
602
+ board.addEventListener('dragstart', e => {
603
+ const header = e.target.closest('.session-board-column > header[draggable]');
604
+ const col = header?.closest('.session-board-column');
605
+ if (!col?.dataset.col)
606
+ return;
607
+ dragColId = col.dataset.col;
608
+ col.classList.add('dragging');
609
+ if (e.dataTransfer) {
610
+ e.dataTransfer.effectAllowed = 'move';
611
+ e.dataTransfer.setData('text/plain', dragColId);
612
+ }
613
+ });
614
+ board.addEventListener('dragover', e => {
615
+ if (!dragColId)
616
+ return;
617
+ e.preventDefault();
618
+ if (e.dataTransfer)
619
+ e.dataTransfer.dropEffect = 'move';
620
+ const target = e.target.closest('.session-board-column');
621
+ board.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
622
+ if (target && target.dataset.col !== dragColId)
623
+ target.classList.add('drag-over');
624
+ });
625
+ board.addEventListener('drop', e => {
626
+ if (!dragColId)
627
+ return;
628
+ e.preventDefault();
629
+ const target = e.target.closest('.session-board-column');
630
+ const from = dragColId;
631
+ dragColId = null;
632
+ if (target?.dataset.col)
633
+ moveColumnTo(from, target.dataset.col);
634
+ });
635
+ board.addEventListener('dragend', () => {
636
+ dragColId = null;
637
+ board.querySelectorAll('.drag-over, .dragging').forEach(el => el.classList.remove('drag-over', 'dragging'));
638
+ });
639
+ viewButtons.forEach(btn => {
640
+ btn.addEventListener('click', () => {
641
+ const next = btn.dataset.view === 'table' ? 'table' : 'board';
642
+ if (next === viewMode)
643
+ return;
644
+ viewMode = next;
645
+ writeStoredSessionsViewMode(window.localStorage, viewMode);
646
+ rerender();
647
+ });
648
+ });
311
649
  selectAllBox.addEventListener('change', () => {
312
650
  const rows = filtered().filter(r => r.status !== 'closed');
313
651
  for (const row of rows) {
@@ -377,5 +715,7 @@ export function renderSessionsPage(root) {
377
715
  filtersForm.addEventListener('input', rerender);
378
716
  store.on(rerender);
379
717
  rerender();
718
+ // bot 友好名 / 群聊标题异步解析,回来后补一次重绘(首帧先显示原值)
719
+ void loadNameMaps().then(rerender);
380
720
  }
381
721
  //# sourceMappingURL=sessions.js.map