botmux 2.12.3 → 2.13.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 (108) hide show
  1. package/README.en.md +29 -0
  2. package/README.md +29 -0
  3. package/dist/adapters/cli/claude-code.d.ts +16 -0
  4. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  5. package/dist/adapters/cli/claude-code.js +54 -1
  6. package/dist/adapters/cli/claude-code.js.map +1 -1
  7. package/dist/cli.js +104 -8
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config.d.ts +6 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +8 -0
  12. package/dist/config.js.map +1 -1
  13. package/dist/core/dashboard-events.d.ts +57 -0
  14. package/dist/core/dashboard-events.d.ts.map +1 -0
  15. package/dist/core/dashboard-events.js +23 -0
  16. package/dist/core/dashboard-events.js.map +1 -0
  17. package/dist/core/dashboard-ipc-server.d.ts +43 -0
  18. package/dist/core/dashboard-ipc-server.d.ts.map +1 -0
  19. package/dist/core/dashboard-ipc-server.js +263 -0
  20. package/dist/core/dashboard-ipc-server.js.map +1 -0
  21. package/dist/core/dashboard-locate.d.ts +20 -0
  22. package/dist/core/dashboard-locate.d.ts.map +1 -0
  23. package/dist/core/dashboard-locate.js +26 -0
  24. package/dist/core/dashboard-locate.js.map +1 -0
  25. package/dist/core/dashboard-rows.d.ts +30 -0
  26. package/dist/core/dashboard-rows.d.ts.map +1 -0
  27. package/dist/core/dashboard-rows.js +48 -0
  28. package/dist/core/dashboard-rows.js.map +1 -0
  29. package/dist/core/scheduler.d.ts +20 -0
  30. package/dist/core/scheduler.d.ts.map +1 -1
  31. package/dist/core/scheduler.js +89 -2
  32. package/dist/core/scheduler.js.map +1 -1
  33. package/dist/core/session-manager.js +1 -1
  34. package/dist/core/session-manager.js.map +1 -1
  35. package/dist/core/types.d.ts +5 -0
  36. package/dist/core/types.d.ts.map +1 -1
  37. package/dist/core/types.js.map +1 -1
  38. package/dist/core/worker-pool.d.ts +55 -2
  39. package/dist/core/worker-pool.d.ts.map +1 -1
  40. package/dist/core/worker-pool.js +249 -5
  41. package/dist/core/worker-pool.js.map +1 -1
  42. package/dist/daemon.d.ts.map +1 -1
  43. package/dist/daemon.js +97 -4
  44. package/dist/daemon.js.map +1 -1
  45. package/dist/dashboard/aggregator.d.ts +41 -0
  46. package/dist/dashboard/aggregator.d.ts.map +1 -0
  47. package/dist/dashboard/aggregator.js +125 -0
  48. package/dist/dashboard/aggregator.js.map +1 -0
  49. package/dist/dashboard/auth.d.ts +23 -0
  50. package/dist/dashboard/auth.d.ts.map +1 -0
  51. package/dist/dashboard/auth.js +66 -0
  52. package/dist/dashboard/auth.js.map +1 -0
  53. package/dist/dashboard/registry.d.ts +28 -0
  54. package/dist/dashboard/registry.d.ts.map +1 -0
  55. package/dist/dashboard/registry.js +74 -0
  56. package/dist/dashboard/registry.js.map +1 -0
  57. package/dist/dashboard/web/app.d.ts +2 -0
  58. package/dist/dashboard/web/app.d.ts.map +1 -0
  59. package/dist/dashboard/web/app.js +42 -0
  60. package/dist/dashboard/web/app.js.map +1 -0
  61. package/dist/dashboard/web/groups.d.ts +2 -0
  62. package/dist/dashboard/web/groups.d.ts.map +1 -0
  63. package/dist/dashboard/web/groups.js +227 -0
  64. package/dist/dashboard/web/groups.js.map +1 -0
  65. package/dist/dashboard/web/schedules.d.ts +2 -0
  66. package/dist/dashboard/web/schedules.d.ts.map +1 -0
  67. package/dist/dashboard/web/schedules.js +105 -0
  68. package/dist/dashboard/web/schedules.js.map +1 -0
  69. package/dist/dashboard/web/sessions.d.ts +2 -0
  70. package/dist/dashboard/web/sessions.d.ts.map +1 -0
  71. package/dist/dashboard/web/sessions.js +187 -0
  72. package/dist/dashboard/web/sessions.js.map +1 -0
  73. package/dist/dashboard/web/store.d.ts +23 -0
  74. package/dist/dashboard/web/store.d.ts.map +1 -0
  75. package/dist/dashboard/web/store.js +82 -0
  76. package/dist/dashboard/web/store.js.map +1 -0
  77. package/dist/dashboard-web/app.js +157 -0
  78. package/dist/dashboard-web/index.html +22 -0
  79. package/dist/dashboard-web/style.css +57 -0
  80. package/dist/dashboard.d.ts +2 -0
  81. package/dist/dashboard.d.ts.map +1 -0
  82. package/dist/dashboard.js +334 -0
  83. package/dist/dashboard.js.map +1 -0
  84. package/dist/im/lark/card-handler.d.ts.map +1 -1
  85. package/dist/im/lark/card-handler.js +16 -2
  86. package/dist/im/lark/card-handler.js.map +1 -1
  87. package/dist/services/bridge-turn-queue.d.ts.map +1 -1
  88. package/dist/services/bridge-turn-queue.js +41 -9
  89. package/dist/services/bridge-turn-queue.js.map +1 -1
  90. package/dist/services/groups-store.d.ts +53 -0
  91. package/dist/services/groups-store.d.ts.map +1 -0
  92. package/dist/services/groups-store.js +134 -0
  93. package/dist/services/groups-store.js.map +1 -0
  94. package/dist/services/schedule-store.d.ts +1 -0
  95. package/dist/services/schedule-store.d.ts.map +1 -1
  96. package/dist/services/schedule-store.js +70 -1
  97. package/dist/services/schedule-store.js.map +1 -1
  98. package/dist/services/session-store.d.ts.map +1 -1
  99. package/dist/services/session-store.js +1 -0
  100. package/dist/services/session-store.js.map +1 -1
  101. package/dist/skills/definitions.d.ts.map +1 -1
  102. package/dist/skills/definitions.js +34 -0
  103. package/dist/skills/definitions.js.map +1 -1
  104. package/dist/types.d.ts +3 -0
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/worker.js +76 -15
  107. package/dist/worker.js.map +1 -1
  108. package/package.json +4 -2
@@ -0,0 +1,187 @@
1
+ // Sessions page: filter bar, table, detail drawer with locate (30s cooldown) + close.
2
+ import { store } from './store.js';
3
+ const PAGE_HTML = `
4
+ <form id="filters" class="filters">
5
+ <input type="search" name="q" placeholder="search workingDir / title / ids" />
6
+ <select name="cli" multiple size="4">
7
+ <option value="claude-code">claude-code</option>
8
+ <option value="codex">codex</option>
9
+ <option value="gemini">gemini</option>
10
+ <option value="opencode">opencode</option>
11
+ <option value="aiden">aiden</option>
12
+ <option value="coco">coco</option>
13
+ <option value="unknown">unknown</option>
14
+ </select>
15
+ <select name="status">
16
+ <option value="">any status</option>
17
+ <option>starting</option><option>working</option>
18
+ <option>idle</option><option>analyzing</option><option>closed</option>
19
+ </select>
20
+ <select name="adopt">
21
+ <option value="">adopt: any</option>
22
+ <option value="yes">adopt: yes</option>
23
+ <option value="no">adopt: no</option>
24
+ </select>
25
+ <label><input type="checkbox" name="active" checked> active only</label>
26
+ </form>
27
+ <table id="sessions-table">
28
+ <thead><tr>
29
+ <th>bot</th><th>cli</th><th>status</th><th>title</th><th>workingDir</th>
30
+ <th>spawned</th><th>last</th><th>adopt</th><th></th>
31
+ </tr></thead>
32
+ <tbody></tbody>
33
+ </table>
34
+ <dialog id="drawer"></dialog>
35
+ `;
36
+ function relTime(ms) {
37
+ if (!ms)
38
+ return '-';
39
+ const diff = Date.now() - ms;
40
+ if (diff < 60_000)
41
+ return 'now';
42
+ if (diff < 3_600_000)
43
+ return Math.floor(diff / 60_000) + 'm';
44
+ if (diff < 86_400_000)
45
+ return Math.floor(diff / 3_600_000) + 'h';
46
+ return Math.floor(diff / 86_400_000) + 'd';
47
+ }
48
+ function escapeHtml(s) {
49
+ return s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
50
+ }
51
+ const ICON_MIRROR = '\u{1FA9E}';
52
+ const ICON_PIN = '\u{1F4CD}';
53
+ const ICON_SCREEN = '\u{1F5A5}️';
54
+ export function renderSessionsPage(root) {
55
+ root.innerHTML = PAGE_HTML;
56
+ const tbody = root.querySelector('#sessions-table tbody');
57
+ const filtersForm = root.querySelector('#filters');
58
+ const drawer = root.querySelector('#drawer');
59
+ function rowHtml(s) {
60
+ return `<tr data-id="${escapeHtml(s.sessionId)}">
61
+ <td>${escapeHtml(s.botName ?? '')}</td>
62
+ <td><span class="badge cli-${escapeHtml(s.cliId ?? 'unknown')}">${escapeHtml(s.cliId ?? 'unknown')}</span></td>
63
+ <td><span class="status status-${escapeHtml(s.status)}">${escapeHtml(s.status)}</span></td>
64
+ <td>${escapeHtml((s.title ?? '').slice(0, 40))}</td>
65
+ <td title="${escapeHtml(s.workingDir ?? '')}">${escapeHtml((s.workingDir ?? '').slice(-30))}</td>
66
+ <td>${relTime(s.spawnedAt)}</td>
67
+ <td>${relTime(s.lastMessageAt)}</td>
68
+ <td>${s.adopt ? ICON_MIRROR : ''}</td>
69
+ <td><button class="open">⋯</button></td>
70
+ </tr>`;
71
+ }
72
+ function filtered() {
73
+ const f = new FormData(filtersForm);
74
+ const q = (f.get('q') ?? '').toLowerCase();
75
+ const cli = f.getAll('cli');
76
+ const status = f.get('status');
77
+ const adopt = f.get('adopt');
78
+ const active = !!f.get('active');
79
+ return [...store.sessions.values()]
80
+ .filter(s => !cli.length || cli.includes(s.cliId ?? 'unknown'))
81
+ .filter(s => !status || s.status === status)
82
+ .filter(s => !adopt || (adopt === 'yes') === !!s.adopt)
83
+ .filter(s => !active || s.status !== 'closed')
84
+ .filter(s => !q || JSON.stringify(s).toLowerCase().includes(q))
85
+ .sort((a, b) => (b.lastMessageAt ?? 0) - (a.lastMessageAt ?? 0));
86
+ }
87
+ function rerender() {
88
+ tbody.innerHTML = filtered().map(rowHtml).join('');
89
+ }
90
+ function openDrawer(s) {
91
+ const closed = s.status === 'closed';
92
+ drawer.innerHTML = `
93
+ <article>
94
+ <header>
95
+ <h3>${escapeHtml(s.title ?? s.sessionId)}</h3>
96
+ <code>${escapeHtml(s.sessionId)}</code> <button data-copy="${escapeHtml(s.sessionId)}">copy</button>
97
+ </header>
98
+ <p><b>bot:</b> ${escapeHtml(s.botName ?? '-')} · <b>cli:</b> ${escapeHtml(s.cliId ?? '?')} · <b>status:</b> ${escapeHtml(s.status)}</p>
99
+ <p><b>chatId:</b> <code>${escapeHtml(s.chatId)}</code> <button data-copy="${escapeHtml(s.chatId)}">copy</button></p>
100
+ <p><b>rootMessageId:</b> <code>${escapeHtml(s.rootMessageId ?? '')}</code> <button data-copy="${escapeHtml(s.rootMessageId ?? '')}">copy</button></p>
101
+ ${s.threadId ? `<p><b>threadId:</b> <code>${escapeHtml(s.threadId)}</code></p>` : ''}
102
+ <p><b>workingDir:</b> ${escapeHtml(s.workingDir ?? '-')}</p>
103
+ <div class="actions">
104
+ <button id="locate-btn" type="button">${ICON_PIN} 定位到飞书话题</button>
105
+ ${s.webPort ? `<a class="btn-link" href="http://${escapeHtml(location.hostname)}:${s.webPort}" target="_blank">${ICON_SCREEN} 打开 xterm</a>` : ''}
106
+ ${!closed ? `<button id="close-btn" type="button" class="contrast">关闭会话</button>` : ''}
107
+ </div>
108
+ <form method="dialog"><button>关闭</button></form>
109
+ </article>`;
110
+ drawer.querySelectorAll('[data-copy]').forEach(b => {
111
+ b.onclick = () => {
112
+ navigator.clipboard.writeText(b.dataset.copy ?? '');
113
+ b.textContent = 'copied';
114
+ setTimeout(() => { b.textContent = 'copy'; }, 800);
115
+ };
116
+ });
117
+ const locateBtn = drawer.querySelector('#locate-btn');
118
+ if (locateBtn) {
119
+ locateBtn.onclick = async () => {
120
+ locateBtn.disabled = true;
121
+ locateBtn.textContent = `${ICON_PIN} 发送中...`;
122
+ try {
123
+ const r = await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/locate`, { method: 'POST' });
124
+ const body = await r.json();
125
+ if (body.ok) {
126
+ // Daemon posted the @mention into the original thread. The
127
+ // notification is what the user navigates from — no AppLink
128
+ // redirect (the previous "open chat in new tab" behavior was
129
+ // explicitly removed by the user as intrusive).
130
+ let left = 30;
131
+ locateBtn.textContent = `${ICON_PIN} (冷却 ${left}s)`;
132
+ const tick = setInterval(() => {
133
+ left -= 1;
134
+ if (left <= 0) {
135
+ clearInterval(tick);
136
+ locateBtn.disabled = false;
137
+ locateBtn.textContent = `${ICON_PIN} 定位到飞书话题`;
138
+ }
139
+ else {
140
+ locateBtn.textContent = `${ICON_PIN} (冷却 ${left}s)`;
141
+ }
142
+ }, 1000);
143
+ }
144
+ else {
145
+ const reason = body.error ?? r.status;
146
+ alert('Locate failed: ' + reason);
147
+ locateBtn.disabled = false;
148
+ locateBtn.textContent = `${ICON_PIN} 定位到飞书话题`;
149
+ }
150
+ }
151
+ catch (e) {
152
+ alert('Locate error: ' + e);
153
+ locateBtn.disabled = false;
154
+ locateBtn.textContent = `${ICON_PIN} 定位到飞书话题`;
155
+ }
156
+ };
157
+ }
158
+ const closeBtn = drawer.querySelector('#close-btn');
159
+ if (closeBtn) {
160
+ closeBtn.onclick = async () => {
161
+ if (!confirm('关闭这个会话?'))
162
+ return;
163
+ closeBtn.disabled = true;
164
+ try {
165
+ await fetch(`/api/sessions/${encodeURIComponent(s.sessionId)}/close`, { method: 'POST' });
166
+ }
167
+ finally {
168
+ drawer.close();
169
+ }
170
+ };
171
+ }
172
+ drawer.showModal();
173
+ }
174
+ tbody.addEventListener('click', e => {
175
+ const tr = e.target.closest('tr[data-id]');
176
+ if (!tr)
177
+ return;
178
+ const sid = tr.dataset.id;
179
+ const s = store.sessions.get(sid);
180
+ if (s)
181
+ openDrawer(s);
182
+ });
183
+ filtersForm.addEventListener('input', rerender);
184
+ store.on(rerender);
185
+ rerender();
186
+ }
187
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../../src/dashboard/web/sessions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ type Session = Record<string, any> & {
2
+ sessionId: string;
3
+ status: string;
4
+ };
5
+ type Schedule = Record<string, any> & {
6
+ id: string;
7
+ };
8
+ declare class Store {
9
+ sessions: Map<string, Session>;
10
+ schedules: Map<string, Schedule>;
11
+ online: boolean;
12
+ private listeners;
13
+ upsertSessions(rows: Session[]): void;
14
+ upsertSchedules(rows: Schedule[]): void;
15
+ applySse(type: string, body: any): void;
16
+ setOnline(v: boolean): void;
17
+ on(fn: () => void): () => boolean;
18
+ private emit;
19
+ }
20
+ export declare const store: Store;
21
+ export declare function bootstrap(): Promise<void>;
22
+ export {};
23
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/dashboard/web/store.ts"],"names":[],"mappings":"AACA,KAAK,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAC3E,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD,cAAM,KAAK;IACT,QAAQ,uBAA8B;IACtC,SAAS,wBAA+B;IACxC,MAAM,UAAQ;IACd,OAAO,CAAC,SAAS,CAAyB;IAE1C,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE;IAI9B,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE;IAIhC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;IAqBhC,SAAS,CAAC,CAAC,EAAE,OAAO;IAGpB,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI;IACjB,OAAO,CAAC,IAAI;CACb;AAED,eAAO,MAAM,KAAK,OAAc,CAAC;AAEjC,wBAAsB,SAAS,kBAwB9B"}
@@ -0,0 +1,82 @@
1
+ class Store {
2
+ sessions = new Map();
3
+ schedules = new Map();
4
+ online = true;
5
+ listeners = new Set();
6
+ upsertSessions(rows) {
7
+ for (const r of rows)
8
+ this.sessions.set(r.sessionId, r);
9
+ this.emit();
10
+ }
11
+ upsertSchedules(rows) {
12
+ for (const r of rows)
13
+ this.schedules.set(r.id, r);
14
+ this.emit();
15
+ }
16
+ applySse(type, body) {
17
+ if (type === 'session.spawned') {
18
+ this.sessions.set(body.session.sessionId, body.session);
19
+ }
20
+ else if (type === 'session.update') {
21
+ const cur = this.sessions.get(body.sessionId);
22
+ if (cur)
23
+ this.sessions.set(body.sessionId, { ...cur, ...body.patch });
24
+ }
25
+ else if (type === 'session.exited') {
26
+ const cur = this.sessions.get(body.sessionId);
27
+ if (cur)
28
+ this.sessions.set(body.sessionId, { ...cur, status: 'closed' });
29
+ }
30
+ else if (type === 'schedule.created') {
31
+ this.schedules.set(body.schedule.id, body.schedule);
32
+ }
33
+ else if (type === 'schedule.updated') {
34
+ const cur = this.schedules.get(body.id);
35
+ if (cur)
36
+ this.schedules.set(body.id, { ...cur, ...body.patch });
37
+ }
38
+ else if (type === 'schedule.deleted') {
39
+ this.schedules.delete(body.id);
40
+ }
41
+ else {
42
+ return; // heartbeat / schedule.fired — no cache mutation
43
+ }
44
+ this.emit();
45
+ }
46
+ setOnline(v) {
47
+ if (this.online !== v) {
48
+ this.online = v;
49
+ this.emit();
50
+ }
51
+ }
52
+ on(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); }
53
+ emit() { for (const fn of this.listeners)
54
+ fn(); }
55
+ }
56
+ export const store = new Store();
57
+ export async function bootstrap() {
58
+ const [s, sch] = await Promise.all([
59
+ fetch('/api/sessions').then(r => r.json()),
60
+ fetch('/api/schedules').then(r => r.json()),
61
+ ]);
62
+ store.upsertSessions(s.sessions ?? []);
63
+ store.upsertSchedules(sch.schedules ?? []);
64
+ const es = new EventSource('/events');
65
+ const types = [
66
+ 'session.spawned', 'session.update', 'session.exited',
67
+ 'schedule.created', 'schedule.updated', 'schedule.deleted',
68
+ 'schedule.fired', 'heartbeat',
69
+ ];
70
+ for (const t of types) {
71
+ es.addEventListener(t, e => {
72
+ try {
73
+ const data = JSON.parse(e.data);
74
+ store.applySse(t, data.body ?? data);
75
+ }
76
+ catch { /* skip malformed */ }
77
+ });
78
+ }
79
+ es.onerror = () => store.setOnline(false);
80
+ es.onopen = () => store.setOnline(true);
81
+ }
82
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/dashboard/web/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,157 @@
1
+ "use strict";(()=>{var v=class{sessions=new Map;schedules=new Map;online=!0;listeners=new Set;upsertSessions(t){for(let i of t)this.sessions.set(i.sessionId,i);this.emit()}upsertSchedules(t){for(let i of t)this.schedules.set(i.id,i);this.emit()}applySse(t,i){if(t==="session.spawned")this.sessions.set(i.session.sessionId,i.session);else if(t==="session.update"){let u=this.sessions.get(i.sessionId);u&&this.sessions.set(i.sessionId,{...u,...i.patch})}else if(t==="session.exited"){let u=this.sessions.get(i.sessionId);u&&this.sessions.set(i.sessionId,{...u,status:"closed"})}else if(t==="schedule.created")this.schedules.set(i.schedule.id,i.schedule);else if(t==="schedule.updated"){let u=this.schedules.get(i.id);u&&this.schedules.set(i.id,{...u,...i.patch})}else if(t==="schedule.deleted")this.schedules.delete(i.id);else return;this.emit()}setOnline(t){this.online!==t&&(this.online=t,this.emit())}on(t){return this.listeners.add(t),()=>this.listeners.delete(t)}emit(){for(let t of this.listeners)t()}},m=new v;async function A(){let[n,t]=await Promise.all([fetch("/api/sessions").then(f=>f.json()),fetch("/api/schedules").then(f=>f.json())]);m.upsertSessions(n.sessions??[]),m.upsertSchedules(t.schedules??[]);let i=new EventSource("/events"),u=["session.spawned","session.update","session.exited","schedule.created","schedule.updated","schedule.deleted","schedule.fired","heartbeat"];for(let f of u)i.addEventListener(f,s=>{try{let h=JSON.parse(s.data);m.applySse(f,h.body??h)}catch{}});i.onerror=()=>m.setOnline(!1),i.onopen=()=>m.setOnline(!0)}var _=`
2
+ <form id="filters" class="filters">
3
+ <input type="search" name="q" placeholder="search workingDir / title / ids" />
4
+ <select name="cli" multiple size="4">
5
+ <option value="claude-code">claude-code</option>
6
+ <option value="codex">codex</option>
7
+ <option value="gemini">gemini</option>
8
+ <option value="opencode">opencode</option>
9
+ <option value="aiden">aiden</option>
10
+ <option value="coco">coco</option>
11
+ <option value="unknown">unknown</option>
12
+ </select>
13
+ <select name="status">
14
+ <option value="">any status</option>
15
+ <option>starting</option><option>working</option>
16
+ <option>idle</option><option>analyzing</option><option>closed</option>
17
+ </select>
18
+ <select name="adopt">
19
+ <option value="">adopt: any</option>
20
+ <option value="yes">adopt: yes</option>
21
+ <option value="no">adopt: no</option>
22
+ </select>
23
+ <label><input type="checkbox" name="active" checked> active only</label>
24
+ </form>
25
+ <table id="sessions-table">
26
+ <thead><tr>
27
+ <th>bot</th><th>cli</th><th>status</th><th>title</th><th>workingDir</th>
28
+ <th>spawned</th><th>last</th><th>adopt</th><th></th>
29
+ </tr></thead>
30
+ <tbody></tbody>
31
+ </table>
32
+ <dialog id="drawer"></dialog>
33
+ `;function q(n){if(!n)return"-";let t=Date.now()-n;return t<6e4?"now":t<36e5?Math.floor(t/6e4)+"m":t<864e5?Math.floor(t/36e5)+"h":Math.floor(t/864e5)+"d"}function p(n){return n.replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t])}var j="\u{1FA9E}",L="\u{1F4CD}",G="\u{1F5A5}\uFE0F";function N(n){n.innerHTML=_;let t=n.querySelector("#sessions-table tbody"),i=n.querySelector("#filters"),u=n.querySelector("#drawer");function f(e){return`<tr data-id="${p(e.sessionId)}">
34
+ <td>${p(e.botName??"")}</td>
35
+ <td><span class="badge cli-${p(e.cliId??"unknown")}">${p(e.cliId??"unknown")}</span></td>
36
+ <td><span class="status status-${p(e.status)}">${p(e.status)}</span></td>
37
+ <td>${p((e.title??"").slice(0,40))}</td>
38
+ <td title="${p(e.workingDir??"")}">${p((e.workingDir??"").slice(-30))}</td>
39
+ <td>${q(e.spawnedAt)}</td>
40
+ <td>${q(e.lastMessageAt)}</td>
41
+ <td>${e.adopt?j:""}</td>
42
+ <td><button class="open">\u22EF</button></td>
43
+ </tr>`}function s(){let e=new FormData(i),l=(e.get("q")??"").toLowerCase(),a=e.getAll("cli"),r=e.get("status"),d=e.get("adopt"),g=!!e.get("active");return[...m.sessions.values()].filter(o=>!a.length||a.includes(o.cliId??"unknown")).filter(o=>!r||o.status===r).filter(o=>!d||d==="yes"==!!o.adopt).filter(o=>!g||o.status!=="closed").filter(o=>!l||JSON.stringify(o).toLowerCase().includes(l)).sort((o,b)=>(b.lastMessageAt??0)-(o.lastMessageAt??0))}function h(){t.innerHTML=s().map(f).join("")}function w(e){let l=e.status==="closed";u.innerHTML=`
44
+ <article>
45
+ <header>
46
+ <h3>${p(e.title??e.sessionId)}</h3>
47
+ <code>${p(e.sessionId)}</code> <button data-copy="${p(e.sessionId)}">copy</button>
48
+ </header>
49
+ <p><b>bot:</b> ${p(e.botName??"-")} \xB7 <b>cli:</b> ${p(e.cliId??"?")} \xB7 <b>status:</b> ${p(e.status)}</p>
50
+ <p><b>chatId:</b> <code>${p(e.chatId)}</code> <button data-copy="${p(e.chatId)}">copy</button></p>
51
+ <p><b>rootMessageId:</b> <code>${p(e.rootMessageId??"")}</code> <button data-copy="${p(e.rootMessageId??"")}">copy</button></p>
52
+ ${e.threadId?`<p><b>threadId:</b> <code>${p(e.threadId)}</code></p>`:""}
53
+ <p><b>workingDir:</b> ${p(e.workingDir??"-")}</p>
54
+ <div class="actions">
55
+ <button id="locate-btn" type="button">${L} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898</button>
56
+ ${e.webPort?`<a class="btn-link" href="http://${p(location.hostname)}:${e.webPort}" target="_blank">${G} \u6253\u5F00 xterm</a>`:""}
57
+ ${l?"":'<button id="close-btn" type="button" class="contrast">\u5173\u95ED\u4F1A\u8BDD</button>'}
58
+ </div>
59
+ <form method="dialog"><button>\u5173\u95ED</button></form>
60
+ </article>`,u.querySelectorAll("[data-copy]").forEach(d=>{d.onclick=()=>{navigator.clipboard.writeText(d.dataset.copy??""),d.textContent="copied",setTimeout(()=>{d.textContent="copy"},800)}});let a=u.querySelector("#locate-btn");a&&(a.onclick=async()=>{a.disabled=!0,a.textContent=`${L} \u53D1\u9001\u4E2D...`;try{let d=await fetch(`/api/sessions/${encodeURIComponent(e.sessionId)}/locate`,{method:"POST"}),g=await d.json();if(g.ok){let o=30;a.textContent=`${L} (\u51B7\u5374 ${o}s)`;let b=setInterval(()=>{o-=1,o<=0?(clearInterval(b),a.disabled=!1,a.textContent=`${L} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`):a.textContent=`${L} (\u51B7\u5374 ${o}s)`},1e3)}else{let o=g.error??d.status;alert("Locate failed: "+o),a.disabled=!1,a.textContent=`${L} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`}}catch(d){alert("Locate error: "+d),a.disabled=!1,a.textContent=`${L} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`}});let r=u.querySelector("#close-btn");r&&(r.onclick=async()=>{if(confirm("\u5173\u95ED\u8FD9\u4E2A\u4F1A\u8BDD?")){r.disabled=!0;try{await fetch(`/api/sessions/${encodeURIComponent(e.sessionId)}/close`,{method:"POST"})}finally{u.close()}}}),u.showModal()}t.addEventListener("click",e=>{let l=e.target.closest("tr[data-id]");if(!l)return;let a=l.dataset.id,r=m.sessions.get(a);r&&w(r)}),i.addEventListener("input",h),m.on(h),h()}var J=`
61
+ <form id="sched-filters" class="filters">
62
+ <input type="search" name="q" placeholder="search name / prompt / workingDir" />
63
+ <select name="kind">
64
+ <option value="">any kind</option>
65
+ <option>cron</option>
66
+ <option>interval</option>
67
+ <option>once</option>
68
+ </select>
69
+ <label><input type="checkbox" name="enabled"> enabled only</label>
70
+ </form>
71
+ <table>
72
+ <thead><tr>
73
+ <th>name</th><th>bot</th><th>schedule</th><th>next</th><th>last</th>
74
+ <th>repeat</th><th>enabled</th><th>actions</th>
75
+ </tr></thead>
76
+ <tbody id="schedules-tbody"></tbody>
77
+ </table>
78
+ `;function E(n){return n.replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t])}function B(n){if(!n)return"\u2014";try{return new Date(n).toLocaleString()}catch{return n}}function D(n){n.innerHTML=J;let t=n.querySelector("#schedules-tbody"),i=n.querySelector("#sched-filters");function u(){let s=new FormData(i),h=(s.get("q")??"").toLowerCase(),w=s.get("kind"),e=!!s.get("enabled");return[...m.schedules.values()].filter(l=>!w||l.parsed?.kind===w).filter(l=>!e||l.enabled).filter(l=>!h||JSON.stringify(l).toLowerCase().includes(h)).sort((l,a)=>{if(l.enabled!==a.enabled)return l.enabled?-1:1;let r=l.nextRunAt?Date.parse(l.nextRunAt):1/0,d=a.nextRunAt?Date.parse(a.nextRunAt):1/0;return r-d})}function f(){t.innerHTML=u().map(s=>`<tr data-id="${E(s.id)}">
79
+ <td>${E(s.name??s.id)}</td>
80
+ <td>${E(s.botName??s.larkAppId??"-")}</td>
81
+ <td><code>${E(s.parsed?.display??"?")}</code></td>
82
+ <td>${B(s.nextRunAt)}</td>
83
+ <td>${B(s.lastRunAt)} ${s.lastStatus==="error"?"\u26A0\uFE0F":""}</td>
84
+ <td>${s.repeat?`${s.repeat.completed}/${s.repeat.times??"\u221E"}`:"\u2014"}</td>
85
+ <td>${s.enabled?"\u2713":"\u2717"}</td>
86
+ <td class="actions-cell">
87
+ <button data-op="run" type="button">Run now</button>
88
+ ${s.enabled?'<button data-op="pause" type="button">Pause</button>':'<button data-op="resume" type="button">Resume</button>'}
89
+ </td>
90
+ </tr>`).join("")||'<tr><td colspan="8" class="empty">No schedules.</td></tr>'}t.addEventListener("click",async s=>{let h=s.target.closest("button[data-op]");if(!h)return;let w=h.closest("tr[data-id]");if(!w)return;let e=w.dataset.id,l=h.dataset.op;h.disabled=!0;let a=h.textContent;h.textContent="...";try{let r=await fetch(`/api/schedules/${encodeURIComponent(e)}/${l}`,{method:"POST"}),d=await r.json().catch(()=>({}));(!r.ok||d.ok===!1)&&alert(`Failed: ${r.status} ${d?.error??""}`.trim())}catch(r){alert("Network error: "+r)}finally{h.disabled=!1,h.textContent=a}}),i.addEventListener("input",f),m.on(f),f()}var I={chats:[],bots:[]},U=`
91
+ <form id="g-filters" class="filters">
92
+ <input type="search" name="q" placeholder="search chat name / id / owner" />
93
+ <label><input type="checkbox" name="missing"> missing-bot only</label>
94
+ <button type="button" id="g-refresh">Refresh</button>
95
+ <button type="button" id="g-create">+ Create new group</button>
96
+ </form>
97
+ <table>
98
+ <thead id="g-head"></thead>
99
+ <tbody id="g-body"></tbody>
100
+ </table>
101
+ <dialog id="g-drawer"></dialog>
102
+ `;function y(n){return n.replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t])}async function H(){I=await(await fetch("/api/groups")).json()}async function P(n){n.innerHTML=U;let t=n.querySelector("#g-head"),i=n.querySelector("#g-body"),u=n.querySelector("#g-filters"),f=n.querySelector("#g-refresh"),s=n.querySelector("#g-drawer");f.onclick=async()=>{f.disabled=!0;try{await H(),l()}finally{f.disabled=!1}};let h=n.querySelector("#g-create");h.onclick=()=>w(),await H();function w(){let a=I.bots;if(a.length===0){alert("No bots online. Restart the daemon first.");return}s.innerHTML=`
103
+ <article>
104
+ <header><h3>Create new group</h3></header>
105
+ <p>Pick bots to invite. The dashboard auto-selects an online daemon as the chat creator/owner; the rest are added as members in the same call.</p>
106
+ <form id="g-createform">
107
+ <label class="form-row">
108
+ <span>Group name <small>(optional)</small></span>
109
+ <input type="text" name="name" placeholder="e.g. AI ChangeLog" maxlength="60">
110
+ </label>
111
+ <fieldset>
112
+ <legend>Bots</legend>
113
+ ${a.map(r=>`
114
+ <label class="checkbox-row">
115
+ <input type="checkbox" name="bot" value="${y(r.larkAppId)}">
116
+ ${y(r.botName??r.larkAppId)} <small>(${y(r.larkAppId)})</small>
117
+ </label>
118
+ `).join("")}
119
+ </fieldset>
120
+ <div class="actions">
121
+ <button type="submit">Create</button>
122
+ <button type="button" id="g-create-cancel">Cancel</button>
123
+ </div>
124
+ </form>
125
+ </article>`,s.showModal(),s.querySelector("#g-create-cancel").onclick=()=>s.close(),s.querySelector("#g-createform").onsubmit=async r=>{r.preventDefault();let d=new FormData(r.target),g=(d.get("name")??"").trim(),o=d.getAll("bot");if(o.length===0){alert("Pick at least one bot.");return}let b=r.target.querySelector("button[type=submit]");b&&(b.disabled=!0,b.textContent="Creating...");try{let c=await fetch("/api/groups/create",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({name:g||void 0,larkAppIds:o})}),$=await c.json();if($.ok&&$.chatId){let M=$.invalidBotIds??[],k=M.length?`
126
+
127
+ Invalid bot ids: ${M.join(", ")}`:"";alert(`Group created.
128
+ chatId: ${$.chatId}
129
+ creator: ${$.creator??"?"}${k}`),await H(),l()}else alert(`Failed: ${$.error??c.status}`)}catch(c){alert("Network error: "+c)}finally{s.close()}}}function e(){t.innerHTML=`<tr>
130
+ <th>chat</th>
131
+ ${I.bots.map(a=>`<th>${y(a.botName??a.larkAppId)}</th>`).join("")}
132
+ <th>actions</th>
133
+ </tr>`}function l(){e();let a=new FormData(u),r=(a.get("q")??"").toLowerCase(),d=!!a.get("missing"),g=I.chats.filter(o=>!r||(o.name??"").toLowerCase().includes(r)||o.chatId.toLowerCase().includes(r)||(o.ownerId??"").toLowerCase().includes(r)).filter(o=>!d||o.memberBots.some(b=>!b.inChat));if(g.length===0){i.innerHTML=`<tr><td colspan="${I.bots.length+2}" class="empty">No chats match the filter.</td></tr>`;return}i.innerHTML=g.map(o=>`<tr data-chat="${y(o.chatId)}">
134
+ <td>
135
+ <strong>${y(o.name??o.chatId)}</strong><br>
136
+ <small><code>${y(o.chatId)}</code></small>
137
+ </td>
138
+ ${I.bots.map(b=>{let c=o.memberBots.find(k=>k.larkAppId===b.larkAppId),$=c?c.error?"!":c.inChat?"\u2713":"\u2717":"?";return`<td class="${c?c.error?"cell-error":c.inChat?"cell-in":"cell-out":"cell-unknown"}" title="${y(c?.error??"")}">${$}</td>`}).join("")}
139
+ <td><button class="add-bots" type="button">Add bots</button></td>
140
+ </tr>`).join("")}l(),i.addEventListener("click",async a=>{let r=a.target.closest("button.add-bots");if(!r)return;let g=r.closest("tr[data-chat]").dataset.chat,o=I.chats.find(c=>c.chatId===g);if(!o)return;let b=o.memberBots.filter(c=>!c.inChat);if(!b.length){alert("All configured bots are already in this chat.");return}s.innerHTML=`
141
+ <article>
142
+ <header><h3>Add bots to ${y(o.name??o.chatId)}</h3></header>
143
+ <p>Select bots to add. The dashboard will pick a bot that's already in the chat as the proxy.</p>
144
+ <form id="g-addform">
145
+ ${b.map(c=>`
146
+ <label class="checkbox-row">
147
+ <input type="checkbox" name="bot" value="${y(c.larkAppId)}">
148
+ ${y(c.botName??c.larkAppId)} <small>(${y(c.larkAppId)})</small>
149
+ </label>
150
+ `).join("")}
151
+ <div class="actions">
152
+ <button type="submit">Confirm add</button>
153
+ <button type="button" id="g-cancel">Cancel</button>
154
+ </div>
155
+ </form>
156
+ </article>`,s.showModal(),s.querySelector("#g-cancel").onclick=()=>s.close(),s.querySelector("#g-addform").onsubmit=async c=>{c.preventDefault();let M=new FormData(c.target).getAll("bot");if(M.length===0){alert("Pick at least one bot.");return}try{let T=await(await fetch(`/api/groups/${encodeURIComponent(g)}/add-bots`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({larkAppIds:M})})).json();if(T.error==="no_proxy_bot")alert("No bot is currently in this chat \u2014 add one manually in Feishu first, then retry.");else if(T.result){let F=T.result.map(S=>`${S.id}: ${S.ok?"OK":`failed (${S.error??"unknown"})`}`).join(`
157
+ `);alert(F),await H(),l()}else alert(`Unexpected response: ${JSON.stringify(T)}`)}catch(k){alert("Network error: "+k)}finally{s.close()}}}),u.addEventListener("input",l)}var C=document.getElementById("root");function R(){let n=location.hash||"#/";n.startsWith("#/groups")?P(C):n.startsWith("#/schedules")?D(C):N(C);for(let t of document.querySelectorAll("header nav a"))t.classList.toggle("active",t.getAttribute("href")===(n||"#/")||n==="#/"&&t.dataset.route==="sessions")}var x=document.getElementById("status");function O(){x&&(x.textContent=m.online?"\u25CF live":"\u25CF disconnected",x.className="status "+(m.online?"online":"offline"))}m.on(O);O();(async()=>{try{await A()}catch(n){console.error("botmux dashboard bootstrap failed",n),m.setOnline(!1)}window.addEventListener("hashchange",R),R()})();})();
@@ -0,0 +1,22 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>botmux dashboard</title>
7
+ <link rel="stylesheet" href="/assets/style.css">
8
+ </head>
9
+ <body>
10
+ <header class="topbar">
11
+ <strong>botmux</strong>
12
+ <nav>
13
+ <a href="#/" data-route="sessions">Sessions</a>
14
+ <a href="#/schedules" data-route="schedules">Schedules</a>
15
+ <a href="#/groups" data-route="groups">Groups &amp; Bots</a>
16
+ </nav>
17
+ <span id="status" class="status"></span>
18
+ </header>
19
+ <main id="root"></main>
20
+ <script src="/assets/app.js"></script>
21
+ </body>
22
+ </html>
@@ -0,0 +1,57 @@
1
+ :root {
2
+ --fg: #1f2328;
3
+ --muted: #57606a;
4
+ --border: #d0d7de;
5
+ --bg: #ffffff;
6
+ --header-bg: #f6f8fa;
7
+ }
8
+ * { box-sizing: border-box; }
9
+ body { margin: 0; font: 14px/1.4 system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--fg); background: var(--bg); }
10
+ .topbar { display: flex; align-items: center; gap: 1.5rem; padding: 0.6rem 1rem; background: var(--header-bg); border-bottom: 1px solid var(--border); }
11
+ .topbar nav a { margin-right: 0.6rem; color: var(--muted); text-decoration: none; }
12
+ .topbar nav a.active { color: var(--fg); font-weight: 600; }
13
+ .status { margin-left: auto; font-size: 12px; color: var(--muted); }
14
+ .status.online { color: #1a7f37; }
15
+ .status.offline { color: #cf222e; }
16
+ main { padding: 1rem; }
17
+ .filters { display: flex; gap: 0.6rem; margin-bottom: 0.8rem; flex-wrap: wrap; align-items: center; }
18
+ .filters input[type=search] { min-width: 240px; padding: 0.3rem 0.5rem; border: 1px solid var(--border); border-radius: 4px; }
19
+ .filters select { padding: 0.2rem; border: 1px solid var(--border); border-radius: 4px; }
20
+ table { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
21
+ th, td { padding: 0.4rem 0.6rem; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
22
+ th { background: var(--header-bg); font-weight: 600; }
23
+ tr[data-id]:hover { background: #f6f8fa; cursor: pointer; }
24
+ button { padding: 0.25rem 0.6rem; border: 1px solid var(--border); background: white; border-radius: 4px; cursor: pointer; }
25
+ button:disabled { opacity: 0.5; cursor: default; }
26
+ button.contrast { background: #cf222e; color: white; border-color: #cf222e; }
27
+ .btn-link { display: inline-block; padding: 0.25rem 0.6rem; border: 1px solid var(--border); border-radius: 4px; text-decoration: none; color: var(--fg); }
28
+ .badge { padding: 0.05rem 0.4rem; border-radius: 999px; font-size: 11px; font-variant: small-caps; }
29
+ .cli-claude-code { background: #d6e4ff; }
30
+ .cli-codex { background: #ffe5cc; }
31
+ .cli-gemini { background: #ffd6e7; }
32
+ .cli-opencode { background: #d4f4dd; }
33
+ .cli-aiden { background: #fff3a8; }
34
+ .cli-coco { background: #c8e6c9; }
35
+ .cli-unknown { background: #eee; color: var(--muted); }
36
+ .status-working { color: #0969da; font-weight: 600; }
37
+ .status-idle { color: var(--muted); }
38
+ .status-closed { color: #999; text-decoration: line-through; }
39
+ .status-analyzing { color: #bf8700; }
40
+ .status-starting { color: #8250df; }
41
+ dialog#drawer { border: none; border-radius: 8px; padding: 0; max-width: 600px; }
42
+ dialog#drawer article { padding: 1rem 1.5rem; }
43
+ dialog#drawer code { background: var(--header-bg); padding: 0.05rem 0.3rem; border-radius: 3px; font-size: 12px; }
44
+ .actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 0.8rem 0; }
45
+ .cell-in { color: #1a7f37; text-align: center; }
46
+ .cell-out { color: #999; text-align: center; }
47
+ .cell-error { color: #cf222e; text-align: center; }
48
+ .cell-unknown { color: #bf8700; text-align: center; }
49
+ .checkbox-row { display: block; padding: 0.3rem 0; }
50
+ .empty { color: var(--muted); padding: 1rem; text-align: center; }
51
+ .form-row { display: block; margin: 0.5rem 0; }
52
+ .form-row span { display: block; margin-bottom: 0.2rem; color: var(--muted); font-size: 12px; }
53
+ .form-row input[type=text] { width: 100%; padding: 0.3rem 0.5rem; border: 1px solid var(--border); border-radius: 4px; }
54
+ dialog#g-drawer { border: none; border-radius: 8px; padding: 0; max-width: 600px; }
55
+ dialog#g-drawer article { padding: 1rem 1.5rem; }
56
+ dialog#g-drawer fieldset { margin: 0.5rem 0; padding: 0.4rem 0.8rem; border: 1px solid var(--border); border-radius: 4px; }
57
+ dialog#g-drawer fieldset legend { padding: 0 0.3rem; color: var(--muted); font-size: 12px; }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":""}