claudeck 1.0.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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/cli.js +2 -0
  4. package/config/agent-chains.json +16 -0
  5. package/config/agent-dags.json +16 -0
  6. package/config/agents.json +46 -0
  7. package/config/bot-prompt.json +3 -0
  8. package/config/folders.json +66 -0
  9. package/config/prompts.json +92 -0
  10. package/config/repos.json +86 -0
  11. package/config/telegram-config.json +17 -0
  12. package/config/workflows.json +90 -0
  13. package/db.js +1198 -0
  14. package/package.json +55 -0
  15. package/plugins/claude-editor/client.css +171 -0
  16. package/plugins/claude-editor/client.js +183 -0
  17. package/plugins/event-stream/client.css +207 -0
  18. package/plugins/event-stream/client.js +271 -0
  19. package/plugins/linear/client.css +345 -0
  20. package/plugins/linear/client.js +380 -0
  21. package/plugins/linear/config.json +5 -0
  22. package/plugins/linear/server.js +312 -0
  23. package/plugins/repos/client.css +549 -0
  24. package/plugins/repos/client.js +663 -0
  25. package/plugins/repos/server.js +232 -0
  26. package/plugins/sudoku/client.css +196 -0
  27. package/plugins/sudoku/client.js +329 -0
  28. package/plugins/tasks/client.css +414 -0
  29. package/plugins/tasks/client.js +394 -0
  30. package/plugins/tasks/server.js +116 -0
  31. package/plugins/tic-tac-toe/client.css +167 -0
  32. package/plugins/tic-tac-toe/client.js +241 -0
  33. package/public/css/core/components.css +232 -0
  34. package/public/css/core/layout.css +330 -0
  35. package/public/css/core/print.css +18 -0
  36. package/public/css/core/reset.css +36 -0
  37. package/public/css/core/responsive.css +378 -0
  38. package/public/css/core/theme.css +116 -0
  39. package/public/css/core/variables.css +93 -0
  40. package/public/css/features/agent-monitor.css +297 -0
  41. package/public/css/features/agent-sidebar.css +525 -0
  42. package/public/css/features/agents.css +996 -0
  43. package/public/css/features/analytics.css +181 -0
  44. package/public/css/features/background-sessions.css +321 -0
  45. package/public/css/features/cost-dashboard.css +168 -0
  46. package/public/css/features/home.css +313 -0
  47. package/public/css/features/retro-terminal.css +88 -0
  48. package/public/css/features/telegram.css +127 -0
  49. package/public/css/features/tour.css +148 -0
  50. package/public/css/features/voice-input.css +60 -0
  51. package/public/css/features/welcome.css +241 -0
  52. package/public/css/panels/assistant-bot.css +442 -0
  53. package/public/css/panels/dev-docs.css +292 -0
  54. package/public/css/panels/file-explorer.css +322 -0
  55. package/public/css/panels/git-panel.css +221 -0
  56. package/public/css/panels/mcp-manager.css +199 -0
  57. package/public/css/panels/tips-feed.css +353 -0
  58. package/public/css/ui/commands.css +273 -0
  59. package/public/css/ui/context-gauge.css +76 -0
  60. package/public/css/ui/file-picker.css +69 -0
  61. package/public/css/ui/image-attachments.css +106 -0
  62. package/public/css/ui/messages.css +884 -0
  63. package/public/css/ui/modals.css +122 -0
  64. package/public/css/ui/parallel.css +217 -0
  65. package/public/css/ui/permissions.css +110 -0
  66. package/public/css/ui/right-panel.css +481 -0
  67. package/public/css/ui/sessions.css +689 -0
  68. package/public/css/ui/status-bar.css +425 -0
  69. package/public/css/ui/toolbox.css +206 -0
  70. package/public/data/tips.json +218 -0
  71. package/public/icons/favicon.png +0 -0
  72. package/public/icons/icon-192.png +0 -0
  73. package/public/icons/icon-512.png +0 -0
  74. package/public/icons/whaly.png +0 -0
  75. package/public/index.html +1140 -0
  76. package/public/js/core/api.js +591 -0
  77. package/public/js/core/constants.js +3 -0
  78. package/public/js/core/dom.js +270 -0
  79. package/public/js/core/events.js +10 -0
  80. package/public/js/core/plugin-loader.js +153 -0
  81. package/public/js/core/store.js +39 -0
  82. package/public/js/core/utils.js +25 -0
  83. package/public/js/core/ws.js +64 -0
  84. package/public/js/features/agent-monitor.js +222 -0
  85. package/public/js/features/agents.js +1209 -0
  86. package/public/js/features/analytics.js +397 -0
  87. package/public/js/features/attachments.js +251 -0
  88. package/public/js/features/background-sessions.js +475 -0
  89. package/public/js/features/chat.js +589 -0
  90. package/public/js/features/cost-dashboard.js +152 -0
  91. package/public/js/features/dag-editor.js +399 -0
  92. package/public/js/features/easter-egg.js +46 -0
  93. package/public/js/features/home.js +270 -0
  94. package/public/js/features/projects.js +372 -0
  95. package/public/js/features/prompts.js +228 -0
  96. package/public/js/features/sessions.js +332 -0
  97. package/public/js/features/telegram.js +131 -0
  98. package/public/js/features/tour.js +210 -0
  99. package/public/js/features/voice-input.js +185 -0
  100. package/public/js/features/welcome.js +43 -0
  101. package/public/js/features/workflows.js +277 -0
  102. package/public/js/main.js +51 -0
  103. package/public/js/panels/assistant-bot.js +445 -0
  104. package/public/js/panels/dev-docs.js +380 -0
  105. package/public/js/panels/file-explorer.js +486 -0
  106. package/public/js/panels/git-panel.js +285 -0
  107. package/public/js/panels/mcp-manager.js +311 -0
  108. package/public/js/panels/tips-feed.js +303 -0
  109. package/public/js/ui/commands.js +114 -0
  110. package/public/js/ui/context-gauge.js +100 -0
  111. package/public/js/ui/diff.js +124 -0
  112. package/public/js/ui/disabled-tools.js +36 -0
  113. package/public/js/ui/export.js +74 -0
  114. package/public/js/ui/formatting.js +206 -0
  115. package/public/js/ui/header-dropdowns.js +72 -0
  116. package/public/js/ui/input-meta.js +71 -0
  117. package/public/js/ui/max-turns.js +21 -0
  118. package/public/js/ui/messages.js +387 -0
  119. package/public/js/ui/model-selector.js +20 -0
  120. package/public/js/ui/notifications.js +232 -0
  121. package/public/js/ui/parallel.js +176 -0
  122. package/public/js/ui/permissions.js +168 -0
  123. package/public/js/ui/right-panel.js +173 -0
  124. package/public/js/ui/shortcuts.js +143 -0
  125. package/public/js/ui/sidebar-toggle.js +29 -0
  126. package/public/js/ui/status-bar.js +172 -0
  127. package/public/js/ui/tab-sdk.js +623 -0
  128. package/public/js/ui/theme.js +38 -0
  129. package/public/manifest.json +13 -0
  130. package/public/offline.html +190 -0
  131. package/public/style.css +42 -0
  132. package/public/sw.js +91 -0
  133. package/server/agent-loop.js +385 -0
  134. package/server/dag-executor.js +265 -0
  135. package/server/orchestrator.js +514 -0
  136. package/server/paths.js +61 -0
  137. package/server/plugin-mount.js +56 -0
  138. package/server/push-sender.js +31 -0
  139. package/server/routes/agents.js +294 -0
  140. package/server/routes/bot.js +45 -0
  141. package/server/routes/exec.js +35 -0
  142. package/server/routes/files.js +218 -0
  143. package/server/routes/mcp.js +82 -0
  144. package/server/routes/messages.js +36 -0
  145. package/server/routes/notifications.js +37 -0
  146. package/server/routes/projects.js +207 -0
  147. package/server/routes/prompts.js +53 -0
  148. package/server/routes/sessions.js +103 -0
  149. package/server/routes/stats.js +143 -0
  150. package/server/routes/telegram.js +71 -0
  151. package/server/routes/tips.js +135 -0
  152. package/server/routes/workflows.js +81 -0
  153. package/server/summarizer.js +55 -0
  154. package/server/telegram-poller.js +205 -0
  155. package/server/telegram-sender.js +304 -0
  156. package/server/ws-handler.js +926 -0
  157. package/server.js +179 -0
@@ -0,0 +1,475 @@
1
+ // Background sessions — guardSwitch, toast notifications, header indicator, localStorage persistence
2
+ import { $ } from '../core/dom.js';
3
+ import { getState, setState } from '../core/store.js';
4
+ import { on } from '../core/events.js';
5
+ import { panes } from '../ui/parallel.js';
6
+ import { CHAT_IDS } from '../core/constants.js';
7
+ import { removeThinking } from '../ui/messages.js';
8
+ import { sendNotification } from '../ui/notifications.js';
9
+
10
+ const BG_STORAGE_KEY = "claudeck-bg-sessions";
11
+
12
+ // ── localStorage helpers ──
13
+
14
+ function persistBgSessions() {
15
+ const map = getBackgroundSessions();
16
+ const obj = {};
17
+ for (const [k, v] of map.entries()) {
18
+ obj[k] = v;
19
+ }
20
+ try {
21
+ localStorage.setItem(BG_STORAGE_KEY, JSON.stringify(obj));
22
+ } catch { /* quota exceeded — ignore */ }
23
+ }
24
+
25
+ function restoreBgSessions() {
26
+ try {
27
+ const raw = localStorage.getItem(BG_STORAGE_KEY);
28
+ if (!raw) return;
29
+ const obj = JSON.parse(raw);
30
+ const map = getBackgroundSessions();
31
+ for (const [k, v] of Object.entries(obj)) {
32
+ map.set(k, v);
33
+ }
34
+ updateHeaderIndicator();
35
+ } catch { /* corrupted data — ignore */ }
36
+ }
37
+
38
+ // ── Helpers ──
39
+
40
+ function getBackgroundSessions() {
41
+ return getState("backgroundSessions");
42
+ }
43
+
44
+ function isAnyPaneStreaming() {
45
+ for (const pane of panes.values()) {
46
+ if (pane.isStreaming) return true;
47
+ }
48
+ return false;
49
+ }
50
+
51
+ function updateHeaderIndicator() {
52
+ const map = getBackgroundSessions();
53
+ const count = map.size;
54
+ if (count > 0) {
55
+ $.bgSessionIndicator.classList.remove("hidden");
56
+ $.bgSessionBadge.textContent = count;
57
+ } else {
58
+ $.bgSessionIndicator.classList.add("hidden");
59
+ }
60
+ }
61
+
62
+ // ── Public API ──
63
+
64
+ export function addBackgroundSession(sessionId, title, projectName, projectPath) {
65
+ const map = getBackgroundSessions();
66
+ map.set(sessionId, { title, projectName, projectPath, startedAt: Date.now() });
67
+ updateHeaderIndicator();
68
+ persistBgSessions();
69
+ }
70
+
71
+ export function removeBackgroundSession(sessionId) {
72
+ const map = getBackgroundSessions();
73
+ map.delete(sessionId);
74
+ updateHeaderIndicator();
75
+ persistBgSessions();
76
+ }
77
+
78
+ export function isBackgroundSession(sessionId) {
79
+ return getBackgroundSessions().has(sessionId);
80
+ }
81
+
82
+ /**
83
+ * Reconcile background sessions against the server's active query list.
84
+ * Sessions NOT in activeSessionIds → show completion toast, remove from map.
85
+ * Sessions still active → keep in map.
86
+ */
87
+ export function reconcileBackgroundSessions(activeSessionIds) {
88
+ const activeSet = new Set(activeSessionIds);
89
+ const map = getBackgroundSessions();
90
+ for (const [sessionId, info] of [...map.entries()]) {
91
+ if (!activeSet.has(sessionId)) {
92
+ showCompletionToast(sessionId, info.title || "Background session", info.projectPath || "");
93
+ map.delete(sessionId);
94
+ }
95
+ }
96
+ updateHeaderIndicator();
97
+ persistBgSessions();
98
+ }
99
+
100
+ export function showErrorToast(sessionId, title, error) {
101
+ sendNotification('Session Error', `${title}: ${error}`, `error-${sessionId}`);
102
+
103
+ const container = document.getElementById("toast-container");
104
+ if (!container) return;
105
+
106
+ const toast = document.createElement("div");
107
+ toast.className = "bg-toast bg-toast-error";
108
+ toast.innerHTML = `
109
+ <span class="bg-toast-dot error"></span>
110
+ <div class="bg-toast-body">
111
+ <div class="bg-toast-label" style="color:var(--error)">Session error</div>
112
+ <div class="bg-toast-title">${escapeForHtml(title)}</div>
113
+ <div class="bg-toast-error-msg">${escapeForHtml(error.slice(0, 120))}</div>
114
+ </div>
115
+ <button class="bg-toast-close" title="Dismiss">&times;</button>
116
+ `;
117
+
118
+ toast.querySelector(".bg-toast-close").addEventListener("click", () => {
119
+ dismissToast(toast);
120
+ });
121
+
122
+ container.appendChild(toast);
123
+ }
124
+
125
+ export function showCompletionToast(sessionId, title, projectPath) {
126
+ sendNotification('Session Completed', title, `done-${sessionId}`);
127
+
128
+ const container = document.getElementById("toast-container");
129
+ if (!container) return;
130
+
131
+ const toast = document.createElement("div");
132
+ toast.className = "bg-toast";
133
+ toast.innerHTML = `
134
+ <span class="bg-toast-dot"></span>
135
+ <div class="bg-toast-body">
136
+ <div class="bg-toast-label">Session completed</div>
137
+ <div class="bg-toast-title">${escapeForHtml(title)}</div>
138
+ </div>
139
+ <button class="bg-toast-switch" title="Switch to session">&#8594;</button>
140
+ <button class="bg-toast-close" title="Dismiss">&times;</button>
141
+ `;
142
+
143
+ toast.querySelector(".bg-toast-switch").addEventListener("click", () => {
144
+ dismissToast(toast);
145
+ switchToSession(sessionId, projectPath);
146
+ });
147
+
148
+ toast.querySelector(".bg-toast-close").addEventListener("click", () => {
149
+ dismissToast(toast);
150
+ });
151
+
152
+ container.appendChild(toast);
153
+ }
154
+
155
+ export function showInputNeededToast(sessionId, title, projectPath) {
156
+ sendNotification('Input Needed', `${title} is waiting for your response`, `input-${sessionId}`);
157
+
158
+ const container = document.getElementById("toast-container");
159
+ if (!container) return;
160
+
161
+ const toast = document.createElement("div");
162
+ toast.className = "bg-toast bg-toast-input";
163
+ toast.innerHTML = `
164
+ <span class="bg-toast-dot input-needed"></span>
165
+ <div class="bg-toast-body">
166
+ <div class="bg-toast-label">Waiting for your input</div>
167
+ <div class="bg-toast-title">${escapeForHtml(title)}</div>
168
+ </div>
169
+ <button class="bg-toast-switch" title="Switch to session">&#8594;</button>
170
+ <button class="bg-toast-close" title="Dismiss">&times;</button>
171
+ `;
172
+
173
+ toast.querySelector(".bg-toast-switch").addEventListener("click", () => {
174
+ dismissToast(toast);
175
+ switchToSession(sessionId, projectPath);
176
+ });
177
+
178
+ toast.querySelector(".bg-toast-close").addEventListener("click", () => {
179
+ dismissToast(toast);
180
+ });
181
+
182
+ container.appendChild(toast);
183
+ }
184
+
185
+ function escapeForHtml(str) {
186
+ const div = document.createElement("div");
187
+ div.textContent = str;
188
+ return div.innerHTML;
189
+ }
190
+
191
+ function dismissToast(toast) {
192
+ toast.classList.add("toast-exit");
193
+ toast.addEventListener("animationend", () => toast.remove());
194
+ }
195
+
196
+ async function switchToSession(sessionId, projectPath) {
197
+ const { loadMessages, loadSessions } = await import('./sessions.js');
198
+ const { updateSystemPromptIndicator, updateHeaderProjectName, loadProjectCommands } = await import('./projects.js');
199
+
200
+ // Restore the project dropdown if the session belongs to a different project
201
+ if (projectPath && $.projectSelect.value !== projectPath) {
202
+ $.projectSelect.value = projectPath;
203
+ localStorage.setItem("claudeck-cwd", projectPath);
204
+ updateSystemPromptIndicator();
205
+ updateHeaderProjectName();
206
+ loadProjectCommands();
207
+ }
208
+
209
+ setState("sessionId", sessionId);
210
+
211
+ if (getState("parallelMode")) {
212
+ for (const chatId of CHAT_IDS) {
213
+ const pane = panes.get(chatId);
214
+ if (pane) pane.messagesDiv.innerHTML = "";
215
+ }
216
+ } else {
217
+ $.messagesDiv.innerHTML = "";
218
+ }
219
+
220
+ await loadMessages(sessionId);
221
+ loadSessions();
222
+ }
223
+
224
+ // ── Guard Switch ──
225
+
226
+ let pendingProceed = null;
227
+ let pendingSessionInfo = null;
228
+
229
+ export function guardSwitch(onProceed) {
230
+ if (!isAnyPaneStreaming()) {
231
+ onProceed();
232
+ return;
233
+ }
234
+
235
+ // Snapshot current session info before the modal.
236
+ // The dropdown value may already reflect the *new* project the user
237
+ // selected, so read the previous project path from localStorage which
238
+ // hasn't been updated yet (that happens inside onProceed).
239
+ const sessionId = getState("sessionId");
240
+ const prevPath = localStorage.getItem("claudeck-cwd") || "";
241
+ const prevOpt = [...$.projectSelect.options].find(o => o.value === prevPath);
242
+ pendingSessionInfo = {
243
+ sessionId,
244
+ projectName: prevOpt?.textContent || "Session",
245
+ projectPath: prevPath,
246
+ title: $.sessionList.querySelector("li.active .session-title")?.textContent || prevOpt?.textContent || "Session",
247
+ };
248
+
249
+ pendingProceed = onProceed;
250
+ $.bgConfirmModal.classList.remove("hidden");
251
+ }
252
+
253
+ function closeModal() {
254
+ $.bgConfirmModal.classList.add("hidden");
255
+ pendingProceed = null;
256
+ pendingSessionInfo = null;
257
+ }
258
+
259
+ function cancelModal() {
260
+ // Revert the project dropdown if the user changed it
261
+ if (pendingSessionInfo && pendingSessionInfo.projectPath) {
262
+ $.projectSelect.value = pendingSessionInfo.projectPath;
263
+ }
264
+ closeModal();
265
+ }
266
+
267
+ function initConfirmDialog() {
268
+ // Cancel — close modal, revert dropdown
269
+ $.bgConfirmCancel.addEventListener("click", cancelModal);
270
+
271
+ // Abort — stop generation, then proceed
272
+ $.bgConfirmAbort.addEventListener("click", async () => {
273
+ const { stopGeneration } = await import('./chat.js');
274
+ const { getPane } = await import('../ui/parallel.js');
275
+
276
+ // Abort all streaming panes
277
+ if (getState("parallelMode")) {
278
+ for (const chatId of CHAT_IDS) {
279
+ const pane = panes.get(chatId);
280
+ if (pane && pane.isStreaming) {
281
+ stopGeneration(pane);
282
+ pane.isStreaming = false;
283
+ pane.currentAssistantMsg = null;
284
+ removeThinking(pane);
285
+ }
286
+ }
287
+ } else {
288
+ const pane = getPane(null);
289
+ stopGeneration(pane);
290
+ pane.isStreaming = false;
291
+ pane.currentAssistantMsg = null;
292
+ removeThinking(pane);
293
+ }
294
+ // Reset Send/Stop buttons
295
+ if (getState("parallelMode")) {
296
+ for (const pane of panes.values()) {
297
+ if (pane.sendBtn) pane.sendBtn.classList.remove("hidden");
298
+ if (pane.stopBtn) pane.stopBtn.classList.add("hidden");
299
+ }
300
+ } else {
301
+ $.sendBtn.classList.remove("hidden");
302
+ $.stopBtn.classList.add("hidden");
303
+ $.sendBtn.disabled = false;
304
+ }
305
+ if ($.streamingTokens) $.streamingTokens.classList.add("hidden");
306
+ if ($.streamingTokensSep) $.streamingTokensSep.classList.add("hidden");
307
+
308
+ const proceed = pendingProceed;
309
+ closeModal();
310
+ if (proceed) proceed();
311
+ });
312
+
313
+ // Continue in Background — add current session to bg map, then proceed
314
+ $.bgConfirmBackground.addEventListener("click", () => {
315
+ if (pendingSessionInfo && pendingSessionInfo.sessionId) {
316
+ const { sessionId, title, projectName, projectPath } = pendingSessionInfo;
317
+ addBackgroundSession(sessionId, title, projectName, projectPath);
318
+ }
319
+
320
+ // Reset streaming UI — the stream continues server-side
321
+ // but the UI is no longer rendering it
322
+ for (const pane of panes.values()) {
323
+ if (pane.isStreaming) {
324
+ pane.isStreaming = false;
325
+ pane.currentAssistantMsg = null;
326
+ removeThinking(pane);
327
+ }
328
+ }
329
+ // Reset Send/Stop buttons and token counter
330
+ if (getState("parallelMode")) {
331
+ for (const pane of panes.values()) {
332
+ if (pane.sendBtn) pane.sendBtn.classList.remove("hidden");
333
+ if (pane.stopBtn) pane.stopBtn.classList.add("hidden");
334
+ }
335
+ } else {
336
+ $.sendBtn.classList.remove("hidden");
337
+ $.stopBtn.classList.add("hidden");
338
+ $.sendBtn.disabled = false;
339
+ }
340
+ if ($.streamingTokens) $.streamingTokens.classList.add("hidden");
341
+ if ($.streamingTokensSep) $.streamingTokensSep.classList.add("hidden");
342
+
343
+ const proceed = pendingProceed;
344
+ closeModal();
345
+ if (proceed) proceed();
346
+ });
347
+
348
+ // Prevent Escape from closing this modal (handled in capture phase before shortcuts.js)
349
+ document.addEventListener("keydown", (e) => {
350
+ if (e.key === "Escape" && !$.bgConfirmModal.classList.contains("hidden")) {
351
+ e.stopImmediatePropagation();
352
+ }
353
+ }, true);
354
+ }
355
+
356
+ // ── WS disconnect handler ──
357
+ // Non-destructive: bg sessions are persisted in localStorage and will be
358
+ // reconciled on reconnect instead of being cleared.
359
+
360
+ on("ws:disconnected", () => {
361
+ // No-op: bg sessions survive disconnects via localStorage persistence.
362
+ // reconcileBackgroundSessions() is called on ws:reconnected.
363
+ });
364
+
365
+ // ── Hover popup on background indicator ──
366
+
367
+ function initBgHoverPopup() {
368
+ const indicator = $.bgSessionIndicator;
369
+ if (!indicator) return;
370
+
371
+ let popup = null;
372
+
373
+ function formatElapsed(startedAt) {
374
+ if (!startedAt) return '';
375
+ const sec = Math.floor((Date.now() - startedAt) / 1000);
376
+ if (sec < 60) return `${sec}s`;
377
+ const min = Math.floor(sec / 60);
378
+ if (min < 60) return `${min}m ${sec % 60}s`;
379
+ return `${Math.floor(min / 60)}h ${min % 60}m`;
380
+ }
381
+
382
+ function show() {
383
+ const map = getBackgroundSessions();
384
+ if (map.size === 0) return;
385
+
386
+ if (popup) popup.remove();
387
+ popup = document.createElement('div');
388
+ popup.className = 'bg-popup';
389
+
390
+ let html = '<div class="bg-popup-header">Background Sessions</div>';
391
+ for (const [sid, info] of map.entries()) {
392
+ const shortId = sid.slice(0, 8);
393
+ const elapsed = formatElapsed(info.startedAt);
394
+ html += `
395
+ <div class="bg-popup-row" data-sid="${sid}">
396
+ <span class="bg-popup-dot"></span>
397
+ <div class="bg-popup-info">
398
+ <div class="bg-popup-title">${escapeForHtml(info.title || 'Untitled')}</div>
399
+ <div class="bg-popup-meta">
400
+ <span class="bg-popup-id">${shortId}</span>
401
+ ${info.projectName ? `<span class="bg-popup-sep">&middot;</span><span>${escapeForHtml(info.projectName)}</span>` : ''}
402
+ ${elapsed ? `<span class="bg-popup-sep">&middot;</span><span>${elapsed}</span>` : ''}
403
+ </div>
404
+ </div>
405
+ <button class="bg-popup-switch" title="Switch to session">&rarr;</button>
406
+ </div>
407
+ `;
408
+ }
409
+ popup.innerHTML = html;
410
+
411
+ // Switch buttons
412
+ popup.querySelectorAll('.bg-popup-switch').forEach(btn => {
413
+ btn.addEventListener('click', (e) => {
414
+ e.stopPropagation();
415
+ const sid = btn.closest('.bg-popup-row').dataset.sid;
416
+ const info = map.get(sid);
417
+ hide();
418
+ if (info) switchToSession(sid, info.projectPath || '');
419
+ });
420
+ });
421
+
422
+ // Rows clickable too
423
+ popup.querySelectorAll('.bg-popup-row').forEach(row => {
424
+ row.addEventListener('click', () => {
425
+ const sid = row.dataset.sid;
426
+ const info = map.get(sid);
427
+ hide();
428
+ if (info) switchToSession(sid, info.projectPath || '');
429
+ });
430
+ });
431
+
432
+ // Hide when mouse leaves the popup
433
+ popup.addEventListener('mouseleave', () => {
434
+ setTimeout(() => {
435
+ if (popup && !popup.matches(':hover') && !indicator.matches(':hover')) {
436
+ hide();
437
+ }
438
+ }, 100);
439
+ });
440
+
441
+ document.body.appendChild(popup);
442
+
443
+ // Position below indicator
444
+ const rect = indicator.getBoundingClientRect();
445
+ popup.style.top = (rect.bottom + 4) + 'px';
446
+ popup.style.left = Math.max(8, rect.left + rect.width / 2 - popup.offsetWidth / 2) + 'px';
447
+ }
448
+
449
+ function hide() {
450
+ if (popup) { popup.remove(); popup = null; }
451
+ }
452
+
453
+ indicator.addEventListener('mouseenter', show);
454
+ indicator.addEventListener('mouseleave', (e) => {
455
+ // Don't hide if moving into the popup
456
+ setTimeout(() => {
457
+ if (popup && !popup.matches(':hover') && !indicator.matches(':hover')) {
458
+ hide();
459
+ }
460
+ }, 100);
461
+ });
462
+
463
+ // Also hide when clicking outside
464
+ document.addEventListener('click', (e) => {
465
+ if (popup && !popup.contains(e.target) && !indicator.contains(e.target)) {
466
+ hide();
467
+ }
468
+ });
469
+ }
470
+
471
+ // ── Init ──
472
+
473
+ restoreBgSessions();
474
+ initConfirmDialog();
475
+ initBgHoverPopup();