claude-relay 2.4.2 → 2.5.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 (75) hide show
  1. package/bin/cli.js +1 -2350
  2. package/package.json +7 -42
  3. package/LICENSE +0 -21
  4. package/README.md +0 -281
  5. package/lib/cli-sessions.js +0 -270
  6. package/lib/config.js +0 -222
  7. package/lib/daemon.js +0 -423
  8. package/lib/ipc.js +0 -112
  9. package/lib/pages.js +0 -714
  10. package/lib/project.js +0 -1224
  11. package/lib/public/app.js +0 -2157
  12. package/lib/public/apple-touch-icon.png +0 -0
  13. package/lib/public/css/base.css +0 -145
  14. package/lib/public/css/diff.css +0 -128
  15. package/lib/public/css/filebrowser.css +0 -1076
  16. package/lib/public/css/highlight.css +0 -144
  17. package/lib/public/css/input.css +0 -512
  18. package/lib/public/css/menus.css +0 -683
  19. package/lib/public/css/messages.css +0 -1159
  20. package/lib/public/css/overlays.css +0 -731
  21. package/lib/public/css/rewind.css +0 -529
  22. package/lib/public/css/sidebar.css +0 -794
  23. package/lib/public/favicon.svg +0 -26
  24. package/lib/public/icon-192.png +0 -0
  25. package/lib/public/icon-512.png +0 -0
  26. package/lib/public/icon-mono.svg +0 -19
  27. package/lib/public/index.html +0 -460
  28. package/lib/public/manifest.json +0 -27
  29. package/lib/public/modules/diff.js +0 -398
  30. package/lib/public/modules/events.js +0 -21
  31. package/lib/public/modules/filebrowser.js +0 -1375
  32. package/lib/public/modules/fileicons.js +0 -172
  33. package/lib/public/modules/icons.js +0 -54
  34. package/lib/public/modules/input.js +0 -578
  35. package/lib/public/modules/markdown.js +0 -149
  36. package/lib/public/modules/notifications.js +0 -643
  37. package/lib/public/modules/qrcode.js +0 -70
  38. package/lib/public/modules/rewind.js +0 -334
  39. package/lib/public/modules/sidebar.js +0 -628
  40. package/lib/public/modules/state.js +0 -3
  41. package/lib/public/modules/terminal.js +0 -658
  42. package/lib/public/modules/theme.js +0 -622
  43. package/lib/public/modules/tools.js +0 -1410
  44. package/lib/public/modules/utils.js +0 -56
  45. package/lib/public/style.css +0 -10
  46. package/lib/public/sw.js +0 -75
  47. package/lib/push.js +0 -125
  48. package/lib/sdk-bridge.js +0 -771
  49. package/lib/server.js +0 -577
  50. package/lib/sessions.js +0 -402
  51. package/lib/terminal-manager.js +0 -187
  52. package/lib/terminal.js +0 -24
  53. package/lib/themes/ayu-light.json +0 -9
  54. package/lib/themes/catppuccin-latte.json +0 -9
  55. package/lib/themes/catppuccin-mocha.json +0 -9
  56. package/lib/themes/claude-light.json +0 -9
  57. package/lib/themes/claude.json +0 -9
  58. package/lib/themes/dracula.json +0 -9
  59. package/lib/themes/everforest-light.json +0 -9
  60. package/lib/themes/everforest.json +0 -9
  61. package/lib/themes/github-light.json +0 -9
  62. package/lib/themes/gruvbox-dark.json +0 -9
  63. package/lib/themes/gruvbox-light.json +0 -9
  64. package/lib/themes/monokai.json +0 -9
  65. package/lib/themes/nord-light.json +0 -9
  66. package/lib/themes/nord.json +0 -9
  67. package/lib/themes/one-dark.json +0 -9
  68. package/lib/themes/one-light.json +0 -9
  69. package/lib/themes/rose-pine-dawn.json +0 -9
  70. package/lib/themes/rose-pine.json +0 -9
  71. package/lib/themes/solarized-dark.json +0 -9
  72. package/lib/themes/solarized-light.json +0 -9
  73. package/lib/themes/tokyo-night-light.json +0 -9
  74. package/lib/themes/tokyo-night.json +0 -9
  75. package/lib/updater.js +0 -96
package/lib/public/app.js DELETED
@@ -1,2157 +0,0 @@
1
- import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
2
- import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
3
- import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal } from './modules/markdown.js';
4
- import { initSidebar, renderSessionList, handleSearchResults, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, populateCliSessionList } from './modules/sidebar.js';
5
- import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid } from './modules/rewind.js';
6
- import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
7
- import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands } from './modules/input.js';
8
- import { initQrCode } from './modules/qrcode.js';
9
- import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer } from './modules/filebrowser.js';
10
- import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed } from './modules/terminal.js';
11
- import { initTheme, getThemeColor, getComputedVar, onThemeChange } from './modules/theme.js';
12
- import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
13
-
14
- // --- Base path for multi-project routing ---
15
- var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
16
- var basePath = slugMatch ? "/p/" + slugMatch[1] + "/" : "/";
17
- var wsPath = slugMatch ? "/p/" + slugMatch[1] + "/ws" : "/ws";
18
-
19
- // --- DOM refs ---
20
- var $ = function (id) { return document.getElementById(id); };
21
- var messagesEl = $("messages");
22
- var inputEl = $("input");
23
- var sendBtn = $("send-btn");
24
- var statusDot = $("status-dot");
25
- var headerTitleEl = $("header-title");
26
- var headerRenameBtn = $("header-rename-btn");
27
- var slashMenu = $("slash-menu");
28
- var sidebar = $("sidebar");
29
- var sidebarOverlay = $("sidebar-overlay");
30
- var sessionListEl = $("session-list");
31
- var newSessionBtn = $("new-session-btn");
32
- var hamburgerBtn = $("hamburger-btn");
33
- var sidebarToggleBtn = $("sidebar-toggle-btn");
34
- var sidebarExpandBtn = $("sidebar-expand-btn");
35
- var resumeSessionBtn = $("resume-session-btn");
36
- var imagePreviewBar = $("image-preview-bar");
37
- var connectOverlay = $("connect-overlay");
38
- var connectVerbEl = $("connect-verb");
39
- var connectStatusEl = $("connect-status");
40
-
41
- // --- Project List ---
42
- var projectListSection = $("project-list-section");
43
- var projectListEl = $("project-list");
44
- var projectListAddBtn = $("project-list-add");
45
- var projectHint = $("project-hint");
46
- var projectHintDismiss = $("project-hint-dismiss");
47
- var cachedProjects = [];
48
- var cachedProjectCount = 0;
49
- var currentSlug = slugMatch ? slugMatch[1] : null;
50
-
51
- function updateProjectList(msg) {
52
- if (typeof msg.projectCount === "number") cachedProjectCount = msg.projectCount;
53
- if (msg.projects) cachedProjects = msg.projects;
54
- var count = cachedProjectCount || 0;
55
- renderProjectList();
56
- if (count === 1 && projectHint) {
57
- try {
58
- if (!localStorage.getItem("claude-relay-project-hint-dismissed")) {
59
- projectHint.classList.remove("hidden");
60
- }
61
- } catch (e) {}
62
- } else if (projectHint) {
63
- projectHint.classList.add("hidden");
64
- }
65
- }
66
-
67
- function renderProjectList() {
68
- if (!projectListEl) return;
69
- projectListEl.innerHTML = "";
70
- for (var i = 0; i < cachedProjects.length; i++) {
71
- var p = cachedProjects[i];
72
- var isCurrent = p.slug === currentSlug;
73
- var displayName = p.title || p.project;
74
- var item = document.createElement("a");
75
- item.className = "project-list-item" + (isCurrent ? " current" : "");
76
- item.href = "/p/" + p.slug + "/";
77
-
78
- var indicator = document.createElement("span");
79
- indicator.className = "pd-indicator " + (isCurrent ? "active" : "inactive");
80
- item.appendChild(indicator);
81
-
82
- var name = document.createElement("span");
83
- name.className = "pd-name";
84
- name.textContent = displayName;
85
- item.appendChild(name);
86
-
87
- var removeBtn = document.createElement("button");
88
- removeBtn.className = "pd-remove";
89
- removeBtn.type = "button";
90
- removeBtn.title = "Remove project";
91
- removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
92
- removeBtn.dataset.slug = p.slug;
93
- removeBtn.dataset.name = displayName;
94
- removeBtn.addEventListener("click", function (e) {
95
- e.preventDefault();
96
- e.stopPropagation();
97
- var s = this.dataset.slug;
98
- var n = this.dataset.name;
99
- confirmRemoveProject(s, n);
100
- });
101
- item.appendChild(removeBtn);
102
-
103
- projectListEl.appendChild(item);
104
- }
105
- refreshIcons();
106
- }
107
-
108
- if (projectListAddBtn) {
109
- projectListAddBtn.addEventListener("click", function () {
110
- openAddProjectModal();
111
- });
112
- }
113
-
114
- document.addEventListener("keydown", function (e) {
115
- if (e.key === "Escape") {
116
- closeImageModal();
117
- }
118
- });
119
-
120
- if (projectHintDismiss) {
121
- projectHintDismiss.addEventListener("click", function () {
122
- projectHint.classList.add("hidden");
123
- try { localStorage.setItem("claude-relay-project-hint-dismissed", "1"); } catch (e) {}
124
- });
125
- }
126
-
127
- // Modal close handlers (replaces inline onclick)
128
- $("paste-modal").querySelector(".confirm-backdrop").addEventListener("click", function() {
129
- $("paste-modal").classList.add("hidden");
130
- });
131
- $("paste-modal").querySelector(".paste-modal-close").addEventListener("click", function() {
132
- $("paste-modal").classList.add("hidden");
133
- });
134
- $("mermaid-modal").querySelector(".confirm-backdrop").addEventListener("click", closeMermaidModal);
135
- $("mermaid-modal").querySelector(".mermaid-modal-btn[title='Close']").addEventListener("click", closeMermaidModal);
136
- $("image-modal").querySelector(".confirm-backdrop").addEventListener("click", closeImageModal);
137
- $("image-modal").querySelector(".image-modal-close").addEventListener("click", closeImageModal);
138
-
139
- function showImageModal(src) {
140
- var modal = $("image-modal");
141
- var img = $("image-modal-img");
142
- if (!modal || !img) return;
143
- img.src = src;
144
- modal.classList.remove("hidden");
145
- refreshIcons(modal);
146
- }
147
-
148
- function closeImageModal() {
149
- var modal = $("image-modal");
150
- if (modal) modal.classList.add("hidden");
151
- }
152
-
153
- // --- State ---
154
- var ws = null;
155
- var connected = false;
156
- var wasConnected = false;
157
- var verbCycleTimer = null;
158
- var processing = false;
159
- // isComposing -> modules/input.js
160
- var reconnectTimer = null;
161
- var reconnectDelay = 1000;
162
- var disconnectNotifTimer = null;
163
- var disconnectNotifShown = false;
164
- var activityEl = null;
165
- var currentMsgEl = null;
166
- var currentFullText = "";
167
- // tools, currentThinking -> modules/tools.js
168
- var highlightTimer = null;
169
- var activeSessionId = null;
170
- var sessionDrafts = {};
171
- var slashCommands = [];
172
- // slashActiveIdx, slashFiltered, pendingImages, pendingPastes -> modules/input.js
173
- // pendingPermissions -> modules/tools.js
174
- var cliSessionId = null;
175
- var projectName = "";
176
- var turnCounter = 0;
177
- var messageUuidMap = [];
178
- // pendingRewindUuid is now in modules/rewind.js
179
- // rewindMode is now in modules/rewind.js
180
-
181
- // --- Progressive history loading ---
182
- var historyFrom = 0;
183
- var historyTotal = 0;
184
- var prependAnchor = null;
185
- var loadingMore = false;
186
- var historySentinelObserver = null;
187
-
188
- // --- Scroll lock ---
189
- var isUserScrolledUp = false;
190
- var scrollThreshold = 150;
191
-
192
- // builtinCommands -> modules/input.js
193
-
194
- // --- Header session rename ---
195
- if (headerRenameBtn) {
196
- headerRenameBtn.addEventListener("click", function () {
197
- if (!activeSessionId) return;
198
- var currentText = headerTitleEl.textContent;
199
- var input = document.createElement("input");
200
- input.type = "text";
201
- input.className = "header-rename-input";
202
- input.value = currentText;
203
- headerTitleEl.style.display = "none";
204
- headerRenameBtn.style.display = "none";
205
- headerTitleEl.parentNode.insertBefore(input, headerTitleEl.nextSibling);
206
- input.focus();
207
- input.select();
208
-
209
- function commit() {
210
- var newTitle = input.value.trim();
211
- if (newTitle && newTitle !== currentText && ws && ws.readyState === 1) {
212
- ws.send(JSON.stringify({ type: "rename_session", id: activeSessionId, title: newTitle }));
213
- headerTitleEl.textContent = newTitle;
214
- }
215
- input.remove();
216
- headerTitleEl.style.display = "";
217
- headerRenameBtn.style.display = "";
218
- }
219
-
220
- input.addEventListener("keydown", function (e) {
221
- if (e.key === "Enter") { e.preventDefault(); commit(); }
222
- if (e.key === "Escape") {
223
- e.preventDefault();
224
- input.remove();
225
- headerTitleEl.style.display = "";
226
- headerRenameBtn.style.display = "";
227
- }
228
- });
229
- input.addEventListener("blur", commit);
230
- });
231
- }
232
-
233
- // --- Confirm modal ---
234
- var confirmModal = $("confirm-modal");
235
- var confirmText = $("confirm-text");
236
- var confirmOk = $("confirm-ok");
237
- var confirmCancel = $("confirm-cancel");
238
- // --- Paste content viewer modal ---
239
- function showPasteModal(text) {
240
- var modal = $("paste-modal");
241
- var body = $("paste-modal-body");
242
- if (!modal || !body) return;
243
- body.textContent = text;
244
- modal.classList.remove("hidden");
245
- }
246
-
247
- function closePasteModal() {
248
- var modal = $("paste-modal");
249
- if (modal) modal.classList.add("hidden");
250
- }
251
-
252
- var confirmCallback = null;
253
-
254
- function showConfirm(text, onConfirm) {
255
- confirmText.textContent = text;
256
- confirmCallback = onConfirm;
257
- confirmModal.classList.remove("hidden");
258
- }
259
-
260
- function hideConfirm() {
261
- confirmModal.classList.add("hidden");
262
- confirmCallback = null;
263
- }
264
-
265
- confirmOk.addEventListener("click", function () {
266
- if (confirmCallback) confirmCallback();
267
- hideConfirm();
268
- });
269
-
270
- confirmCancel.addEventListener("click", hideConfirm);
271
- confirmModal.querySelector(".confirm-backdrop").addEventListener("click", hideConfirm);
272
-
273
- // --- Rewind (module) ---
274
- initRewind({
275
- $: $,
276
- get ws() { return ws; },
277
- get connected() { return connected; },
278
- get processing() { return processing; },
279
- messagesEl: messagesEl,
280
- addSystemMessage: addSystemMessage,
281
- });
282
-
283
- // --- Theme (module) ---
284
- initTheme();
285
-
286
- // --- Sidebar (module) ---
287
- initSidebar({
288
- $: $,
289
- get ws() { return ws; },
290
- get connected() { return connected; },
291
- get projectName() { return projectName; },
292
- messagesEl: messagesEl,
293
- sessionListEl: sessionListEl,
294
- sidebar: sidebar,
295
- sidebarOverlay: sidebarOverlay,
296
- sidebarToggleBtn: sidebarToggleBtn,
297
- sidebarExpandBtn: sidebarExpandBtn,
298
- hamburgerBtn: hamburgerBtn,
299
- newSessionBtn: newSessionBtn,
300
- resumeSessionBtn: resumeSessionBtn,
301
- headerTitleEl: headerTitleEl,
302
- showConfirm: showConfirm,
303
- onFilesTabOpen: function () { loadRootDirectory(); },
304
- });
305
-
306
- // --- Connect overlay verb cycling ---
307
- function startVerbCycle() {
308
- if (verbCycleTimer) return;
309
- connectVerbEl.textContent = randomThinkingVerb() + "...";
310
- connectVerbEl.classList.remove("fade-out");
311
- connectVerbEl.classList.add("fade-in");
312
- verbCycleTimer = setInterval(function () {
313
- connectVerbEl.classList.remove("fade-in");
314
- connectVerbEl.classList.add("fade-out");
315
- setTimeout(function () {
316
- connectVerbEl.textContent = randomThinkingVerb() + "...";
317
- connectVerbEl.classList.remove("fade-out");
318
- connectVerbEl.classList.add("fade-in");
319
- }, 400);
320
- }, 10000);
321
- }
322
-
323
- function stopVerbCycle() {
324
- if (verbCycleTimer) {
325
- clearInterval(verbCycleTimer);
326
- verbCycleTimer = null;
327
- }
328
- stopPixelAnim();
329
- }
330
-
331
- // --- Pixel character animation ---
332
- var pixelAnimTimer = null;
333
- var pixelBlocks = [];
334
- var antennaBlocks = [];
335
-
336
- (function initPixelAnim() {
337
- var canvas = document.getElementById("pixel-canvas");
338
- if (!canvas) return;
339
-
340
- // Character grid: 1 = body, 2 = eye, 0 = empty
341
- // 12 cols x 9 rows
342
- // 0=empty, 1=body, 2=eye, 3=antenna
343
- var grid = [
344
- [0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0],
345
- [0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0],
346
- [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
347
- [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
348
- [0, 0, 1, 2, 1, 1, 1, 1, 2, 1, 0, 0],
349
- [0, 0, 1, 2, 1, 1, 1, 1, 2, 1, 0, 0],
350
- [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
351
- [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
352
- [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
353
- [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
354
- [0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0],
355
- [0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0],
356
- ];
357
-
358
- var CELL = 12;
359
- var accent = getThemeColor("base09");
360
- var eye = getThemeColor("base00");
361
- var antenna = getThemeColor("base06");
362
-
363
- for (var r = 0; r < grid.length; r++) {
364
- for (var c = 0; c < grid[r].length; c++) {
365
- if (grid[r][c] === 0) continue;
366
- var el = document.createElement("div");
367
- el.className = "px";
368
- var v = grid[r][c];
369
- el.style.background = v === 2 ? eye : v === 3 ? antenna : accent;
370
- el.style.left = c * CELL + "px";
371
- el.style.top = r * CELL + "px";
372
- if (v === 3) antennaBlocks.push(el);
373
- canvas.appendChild(el);
374
- pixelBlocks.push(el);
375
- }
376
- }
377
- })();
378
-
379
- // Update pixel mascot colors when theme changes
380
- onThemeChange(function () {
381
- var newAccent = getThemeColor("base09");
382
- var newEye = getThemeColor("base00");
383
- var newAntenna = getThemeColor("base06");
384
- for (var i = 0; i < pixelBlocks.length; i++) {
385
- var el = pixelBlocks[i];
386
- var bg = el.style.background;
387
- if (bg === accent || bg === newAccent) el.style.background = newAccent;
388
- else if (bg === eye || bg === newEye) el.style.background = newEye;
389
- else if (bg === antenna || bg === newAntenna) el.style.background = newAntenna;
390
- }
391
- accent = newAccent;
392
- eye = newEye;
393
- antenna = newAntenna;
394
- });
395
-
396
- function pixelScatter() {
397
- stopSpark();
398
- for (var i = 0; i < pixelBlocks.length; i++) {
399
- var el = pixelBlocks[i];
400
- var angle = Math.random() * Math.PI * 2;
401
- var dist = 80 + Math.random() * 120;
402
- var dx = Math.cos(angle) * dist;
403
- var dy = Math.sin(angle) * dist;
404
- var rot = (Math.random() - 0.5) * 360;
405
- el.className = "px scatter";
406
- el.style.transform = "translate(" + dx + "px," + dy + "px) rotate(" + rot + "deg)";
407
- el.style.opacity = "0";
408
- }
409
- }
410
-
411
- var sparkTimer = null;
412
-
413
- function pixelAssemble() {
414
- for (var i = 0; i < pixelBlocks.length; i++) {
415
- (function (el, delay) {
416
- setTimeout(function () {
417
- el.className = "px settle";
418
- el.style.transform = "translate(0,0) rotate(0deg)";
419
- el.style.opacity = "1";
420
- }, delay);
421
- })(pixelBlocks[i], Math.random() * 300);
422
- }
423
- startSpark();
424
- }
425
-
426
- function startSpark() {
427
- stopSpark();
428
- var count = 0;
429
- sparkTimer = setInterval(function () {
430
- for (var i = 0; i < antennaBlocks.length; i++) {
431
- if (Math.random() < 0.4) {
432
- antennaBlocks[i].style.background = "#FFF";
433
- antennaBlocks[i].style.boxShadow = "0 0 6px 2px rgba(255,255,255,0.6)";
434
- } else {
435
- antennaBlocks[i].style.background = antenna;
436
- antennaBlocks[i].style.boxShadow = "none";
437
- }
438
- }
439
- count++;
440
- if (count > 20) stopSpark();
441
- }, 80);
442
- }
443
-
444
- function stopSpark() {
445
- if (sparkTimer) {
446
- clearInterval(sparkTimer);
447
- sparkTimer = null;
448
- }
449
- for (var i = 0; i < antennaBlocks.length; i++) {
450
- antennaBlocks[i].style.background = antenna;
451
- antennaBlocks[i].style.boxShadow = "none";
452
- }
453
- }
454
-
455
- function startPixelAnim() {
456
- if (pixelAnimTimer) return;
457
- // Start scattered
458
- for (var i = 0; i < pixelBlocks.length; i++) {
459
- var angle = Math.random() * Math.PI * 2;
460
- var dist = 80 + Math.random() * 120;
461
- pixelBlocks[i].className = "px";
462
- pixelBlocks[i].style.transform = "translate(" + (Math.cos(angle) * dist) + "px," + (Math.sin(angle) * dist) + "px) rotate(" + ((Math.random() - 0.5) * 360) + "deg)";
463
- pixelBlocks[i].style.opacity = "0";
464
- }
465
- function cycle() {
466
- pixelAssemble();
467
- pixelAnimTimer = setTimeout(function () {
468
- pixelScatter();
469
- pixelAnimTimer = setTimeout(cycle, 800);
470
- }, 2200);
471
- }
472
- pixelAnimTimer = setTimeout(cycle, 300);
473
- }
474
-
475
- function stopPixelAnim() {
476
- if (pixelAnimTimer) {
477
- clearTimeout(pixelAnimTimer);
478
- pixelAnimTimer = null;
479
- }
480
- }
481
-
482
- // --- Dynamic favicon ---
483
- var faviconSvg = null;
484
- var faviconLink = document.querySelector('link[rel="icon"]');
485
-
486
- function updateFavicon(bgColor) {
487
- if (!faviconLink) return;
488
- if (!faviconSvg) {
489
- var xhr = new XMLHttpRequest();
490
- xhr.open("GET", basePath + "favicon.svg", false);
491
- xhr.send();
492
- if (xhr.status === 200) faviconSvg = xhr.responseText;
493
- else return;
494
- }
495
- var svg = faviconSvg.replace(/fill="#57AB5A"/g, 'fill="' + bgColor + '"');
496
- faviconLink.href = "data:image/svg+xml," + encodeURIComponent(svg);
497
- }
498
-
499
- // --- Status & Activity ---
500
- function setSendBtnMode(mode) {
501
- if (mode === "stop") {
502
- sendBtn.disabled = false;
503
- sendBtn.classList.add("stop");
504
- sendBtn.innerHTML = '<i data-lucide="square"></i>';
505
- } else {
506
- sendBtn.classList.remove("stop");
507
- sendBtn.innerHTML = '<i data-lucide="arrow-up"></i>';
508
- }
509
- refreshIcons();
510
- }
511
-
512
- var ioTimer = null;
513
- var faviconIoTimer = null;
514
- function blinkIO() {
515
- if (!processing) return;
516
- statusDot.classList.add("io");
517
- clearTimeout(ioTimer);
518
- ioTimer = setTimeout(function () { statusDot.classList.remove("io"); }, 60);
519
-
520
- // Blink favicon: dim then restore
521
- updateFavicon(getComputedVar("--sidebar-bg"));
522
- clearTimeout(faviconIoTimer);
523
- faviconIoTimer = setTimeout(function () { updateFavicon(getComputedVar("--success")); }, 60);
524
- }
525
-
526
- // --- Urgent favicon blink (permission / ask user) ---
527
- var urgentBlinkTimer = null;
528
- var savedTitle = null;
529
- function startUrgentBlink() {
530
- if (urgentBlinkTimer) return;
531
- savedTitle = document.title;
532
- var colors = [getComputedVar("--accent"), getComputedVar("--success"), getComputedVar("--accent"), getComputedVar("--text"), getComputedVar("--accent"), getComputedVar("--success")];
533
- var tick = 0;
534
- urgentBlinkTimer = setInterval(function () {
535
- updateFavicon(colors[tick % colors.length]);
536
- document.title = tick % 2 === 0 ? "\u26A0 Input needed" : savedTitle;
537
- tick++;
538
- }, 250);
539
- }
540
- function stopUrgentBlink() {
541
- if (!urgentBlinkTimer) return;
542
- clearInterval(urgentBlinkTimer);
543
- urgentBlinkTimer = null;
544
- updateFavicon(getComputedVar("--success"));
545
- if (savedTitle) document.title = savedTitle;
546
- savedTitle = null;
547
- }
548
-
549
- function setStatus(status) {
550
- statusDot.className = "status-dot";
551
- if (status === "connected") {
552
- statusDot.classList.add("connected");
553
- connected = true;
554
- processing = false;
555
- sendBtn.disabled = false;
556
- setSendBtnMode("send");
557
- connectOverlay.classList.add("hidden");
558
- stopVerbCycle();
559
- updateFavicon(getComputedVar("--success"));
560
- } else if (status === "processing") {
561
- statusDot.classList.add("processing");
562
- processing = true;
563
- setSendBtnMode("stop");
564
- updateFavicon(getComputedVar("--success"));
565
- } else {
566
- connected = false;
567
- sendBtn.disabled = true;
568
- connectOverlay.classList.remove("hidden");
569
- connectStatusEl.textContent = "Reconnecting...";
570
- startVerbCycle();
571
- startPixelAnim();
572
- updateFavicon(getComputedVar("--error"));
573
- }
574
- }
575
-
576
- function setActivity(text) {
577
- if (text) {
578
- if (!activityEl) {
579
- activityEl = document.createElement("div");
580
- activityEl.className = "activity-inline";
581
- activityEl.innerHTML =
582
- '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
583
- '<span class="activity-text"></span>';
584
- addToMessages(activityEl);
585
- refreshIcons();
586
- }
587
- activityEl.querySelector(".activity-text").textContent = text;
588
- scrollToBottom();
589
- } else {
590
- if (activityEl) {
591
- activityEl.remove();
592
- activityEl = null;
593
- }
594
- }
595
- }
596
-
597
- // --- Model selector ---
598
- var modelMenuWrap = $("model-menu-wrap");
599
- var modelBtn = $("model-btn");
600
- var modelLabel = $("model-label");
601
- var modelMenu = $("model-menu");
602
-
603
- function modelDisplayName(value, models) {
604
- if (!value) return "";
605
- // Look up displayName from models list
606
- if (models) {
607
- for (var i = 0; i < models.length; i++) {
608
- if (models[i].value === value && models[i].displayName) return models[i].displayName;
609
- }
610
- }
611
- return value;
612
- }
613
-
614
- var currentModels = [];
615
-
616
- function updateModelSelector(current, models) {
617
- if (!modelMenuWrap || !modelBtn || !modelMenu) return;
618
- currentModels = models;
619
- modelLabel.textContent = modelDisplayName(current, models);
620
- modelMenuWrap.classList.remove("hidden");
621
-
622
- modelMenu.innerHTML = "";
623
- var list = models.length > 0 ? models : (current ? [{ value: current, displayName: current }] : []);
624
- for (var i = 0; i < list.length; i++) {
625
- var item = list[i];
626
- var value = item.value || "";
627
- var label = item.displayName || value;
628
- var btn = document.createElement("button");
629
- btn.className = "model-menu-item";
630
- if (value === current) btn.classList.add("active");
631
- btn.dataset.model = value;
632
- btn.textContent = label;
633
- btn.addEventListener("click", function () {
634
- var model = this.dataset.model;
635
- if (ws && ws.readyState === 1) {
636
- ws.send(JSON.stringify({ type: "set_model", model: model }));
637
- }
638
- modelMenu.classList.add("hidden");
639
- modelBtn.classList.remove("active");
640
- });
641
- modelMenu.appendChild(btn);
642
- }
643
- }
644
-
645
- modelBtn.addEventListener("click", function (e) {
646
- e.stopPropagation();
647
- var open = modelMenu.classList.toggle("hidden");
648
- modelBtn.classList.toggle("active", !open);
649
- });
650
-
651
- document.addEventListener("click", function (e) {
652
- if (!modelMenu.contains(e.target) && e.target !== modelBtn) {
653
- modelMenu.classList.add("hidden");
654
- modelBtn.classList.remove("active");
655
- }
656
- });
657
-
658
- // --- Usage panel ---
659
- var usagePanel = $("usage-panel");
660
- var usagePanelClose = $("usage-panel-close");
661
- var usageCostEl = $("usage-cost");
662
- var usageInputEl = $("usage-input");
663
- var usageOutputEl = $("usage-output");
664
- var usageCacheReadEl = $("usage-cache-read");
665
- var usageCacheWriteEl = $("usage-cache-write");
666
- var usageTurnsEl = $("usage-turns");
667
- var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
668
-
669
- function formatTokens(n) {
670
- if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
671
- if (n >= 1000) return (n / 1000).toFixed(1) + "K";
672
- return String(n);
673
- }
674
-
675
- function updateUsagePanel() {
676
- if (!usageCostEl) return;
677
- usageCostEl.textContent = "$" + sessionUsage.cost.toFixed(4);
678
- usageInputEl.textContent = formatTokens(sessionUsage.input);
679
- usageOutputEl.textContent = formatTokens(sessionUsage.output);
680
- usageCacheReadEl.textContent = formatTokens(sessionUsage.cacheRead);
681
- usageCacheWriteEl.textContent = formatTokens(sessionUsage.cacheWrite);
682
- usageTurnsEl.textContent = String(sessionUsage.turns);
683
- }
684
-
685
- function accumulateUsage(cost, usage) {
686
- if (cost != null) sessionUsage.cost += cost;
687
- if (usage) {
688
- sessionUsage.input += usage.input_tokens || usage.inputTokens || 0;
689
- sessionUsage.output += usage.output_tokens || usage.outputTokens || 0;
690
- sessionUsage.cacheRead += usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
691
- sessionUsage.cacheWrite += usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
692
- }
693
- sessionUsage.turns++;
694
- updateUsagePanel();
695
- }
696
-
697
- function resetUsage() {
698
- sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
699
- updateUsagePanel();
700
- if (usagePanel) usagePanel.classList.add("hidden");
701
- }
702
-
703
- function toggleUsagePanel() {
704
- if (!usagePanel) return;
705
- usagePanel.classList.toggle("hidden");
706
- refreshIcons();
707
- }
708
-
709
- if (usagePanelClose) {
710
- usagePanelClose.addEventListener("click", function () {
711
- usagePanel.classList.add("hidden");
712
- });
713
- }
714
-
715
- // --- Status panel ---
716
- var statusPanel = $("status-panel");
717
- var statusPanelClose = $("status-panel-close");
718
- var statusPidEl = $("status-pid");
719
- var statusUptimeEl = $("status-uptime");
720
- var statusRssEl = $("status-rss");
721
- var statusHeapUsedEl = $("status-heap-used");
722
- var statusHeapTotalEl = $("status-heap-total");
723
- var statusExternalEl = $("status-external");
724
- var statusSessionsEl = $("status-sessions");
725
- var statusProcessingEl = $("status-processing");
726
- var statusClientsEl = $("status-clients");
727
- var statusTerminalsEl = $("status-terminals");
728
- var statusRefreshTimer = null;
729
-
730
- function formatBytes(n) {
731
- if (n >= 1073741824) return (n / 1073741824).toFixed(1) + " GB";
732
- if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
733
- if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
734
- return n + " B";
735
- }
736
-
737
- function formatUptime(seconds) {
738
- var d = Math.floor(seconds / 86400);
739
- var h = Math.floor((seconds % 86400) / 3600);
740
- var m = Math.floor((seconds % 3600) / 60);
741
- var s = Math.floor(seconds % 60);
742
- if (d > 0) return d + "d " + h + "h " + m + "m";
743
- if (h > 0) return h + "h " + m + "m " + s + "s";
744
- return m + "m " + s + "s";
745
- }
746
-
747
- function updateStatusPanel(data) {
748
- if (!statusPidEl) return;
749
- statusPidEl.textContent = String(data.pid);
750
- statusUptimeEl.textContent = formatUptime(data.uptime);
751
- statusRssEl.textContent = formatBytes(data.memory.rss);
752
- statusHeapUsedEl.textContent = formatBytes(data.memory.heapUsed);
753
- statusHeapTotalEl.textContent = formatBytes(data.memory.heapTotal);
754
- statusExternalEl.textContent = formatBytes(data.memory.external);
755
- statusSessionsEl.textContent = String(data.sessions);
756
- statusProcessingEl.textContent = String(data.processing);
757
- statusClientsEl.textContent = String(data.clients);
758
- statusTerminalsEl.textContent = String(data.terminals);
759
- }
760
-
761
- function requestProcessStats() {
762
- if (ws && ws.readyState === 1) {
763
- ws.send(JSON.stringify({ type: "process_stats" }));
764
- }
765
- }
766
-
767
- function toggleStatusPanel() {
768
- if (!statusPanel) return;
769
- var opening = statusPanel.classList.contains("hidden");
770
- statusPanel.classList.toggle("hidden");
771
- if (opening) {
772
- requestProcessStats();
773
- statusRefreshTimer = setInterval(requestProcessStats, 5000);
774
- } else {
775
- if (statusRefreshTimer) {
776
- clearInterval(statusRefreshTimer);
777
- statusRefreshTimer = null;
778
- }
779
- }
780
- refreshIcons();
781
- }
782
-
783
- if (statusPanelClose) {
784
- statusPanelClose.addEventListener("click", function () {
785
- statusPanel.classList.add("hidden");
786
- if (statusRefreshTimer) {
787
- clearInterval(statusRefreshTimer);
788
- statusRefreshTimer = null;
789
- }
790
- });
791
- }
792
-
793
- // --- Context panel ---
794
- var contextPanel = $("context-panel");
795
- var contextPanelClose = $("context-panel-close");
796
- var contextPanelMinimize = $("context-panel-minimize");
797
- var contextBarFill = $("context-bar-fill");
798
- var contextBarPct = $("context-bar-pct");
799
- var contextUsedEl = $("context-used");
800
- var contextWindowEl = $("context-window");
801
- var contextMaxOutputEl = $("context-max-output");
802
- var contextInputEl = $("context-input");
803
- var contextOutputEl = $("context-output");
804
- var contextCacheReadEl = $("context-cache-read");
805
- var contextCacheWriteEl = $("context-cache-write");
806
- var contextModelEl = $("context-model");
807
- var contextCostEl = $("context-cost");
808
- var contextTurnsEl = $("context-turns");
809
- var contextMini = $("context-mini");
810
- var contextMiniFill = $("context-mini-fill");
811
- var contextMiniLabel = $("context-mini-label");
812
- var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
813
-
814
- function contextPctClass(pct) {
815
- return pct >= 85 ? " danger" : pct >= 60 ? " warn" : "";
816
- }
817
-
818
- function updateContextPanel() {
819
- if (!contextUsedEl) return;
820
- // Context window usage = input tokens (includes cache read/write) + output tokens
821
- var used = contextData.input + contextData.output;
822
- var win = contextData.contextWindow;
823
- var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
824
- var cls = contextPctClass(pct);
825
- // Panel bar
826
- contextBarFill.style.width = pct.toFixed(1) + "%";
827
- contextBarFill.className = "context-bar-fill" + cls;
828
- contextBarPct.textContent = pct.toFixed(0) + "%";
829
- // Mini bar
830
- if (contextMiniFill) {
831
- contextMiniFill.style.width = pct.toFixed(1) + "%";
832
- contextMiniFill.className = "context-mini-fill" + cls;
833
- }
834
- if (contextMiniLabel) {
835
- contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "0%");
836
- }
837
- contextUsedEl.textContent = formatTokens(used);
838
- contextWindowEl.textContent = win > 0 ? formatTokens(win) : "-";
839
- contextMaxOutputEl.textContent = contextData.maxOutputTokens > 0 ? formatTokens(contextData.maxOutputTokens) : "-";
840
- contextInputEl.textContent = formatTokens(contextData.input);
841
- contextOutputEl.textContent = formatTokens(contextData.output);
842
- contextCacheReadEl.textContent = formatTokens(contextData.cacheRead);
843
- contextCacheWriteEl.textContent = formatTokens(contextData.cacheWrite);
844
- contextModelEl.textContent = contextData.model;
845
- contextCostEl.textContent = "$" + contextData.cost.toFixed(4);
846
- contextTurnsEl.textContent = String(contextData.turns);
847
- }
848
-
849
- function accumulateContext(cost, usage, modelUsage) {
850
- if (cost != null) contextData.cost += cost;
851
- // Use latest turn values (not cumulative) since each turn's input_tokens
852
- // already includes the full conversation context up to that point
853
- if (usage) {
854
- contextData.input = usage.input_tokens || usage.inputTokens || 0;
855
- contextData.output = usage.output_tokens || usage.outputTokens || 0;
856
- contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
857
- contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
858
- }
859
- contextData.turns++;
860
- if (modelUsage) {
861
- var models = Object.keys(modelUsage);
862
- if (models.length > 0) {
863
- var m = models[0];
864
- var mu = modelUsage[m];
865
- contextData.model = m;
866
- if (mu.contextWindow) contextData.contextWindow = mu.contextWindow;
867
- if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
868
- }
869
- }
870
- updateContextPanel();
871
- }
872
-
873
- // contextView: "off" | "mini" | "panel"
874
- function getContextView() {
875
- try { return localStorage.getItem("claude-relay-context-view") || "off"; } catch (e) { return "off"; }
876
- }
877
- function setContextView(v) {
878
- try { localStorage.setItem("claude-relay-context-view", v); } catch (e) {}
879
- }
880
-
881
- function applyContextView(view) {
882
- if (contextPanel) contextPanel.classList.toggle("hidden", view !== "panel");
883
- if (contextMini) contextMini.classList.toggle("hidden", view !== "mini");
884
- if (view === "panel") refreshIcons();
885
- }
886
-
887
- function resetContextData() {
888
- contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
889
- updateContextPanel();
890
- }
891
-
892
- function resetContext() {
893
- resetContextData();
894
- // Keep view state, just reset data
895
- applyContextView(getContextView());
896
- }
897
-
898
- function minimizeContext() {
899
- setContextView("mini");
900
- applyContextView("mini");
901
- }
902
-
903
- function expandContext() {
904
- setContextView("panel");
905
- applyContextView("panel");
906
- }
907
-
908
- function toggleContextPanel() {
909
- if (!contextPanel) return;
910
- var view = getContextView();
911
- if (view === "panel") {
912
- setContextView("mini");
913
- applyContextView("mini");
914
- } else {
915
- setContextView("panel");
916
- applyContextView("panel");
917
- }
918
- }
919
-
920
- if (contextPanelClose) {
921
- contextPanelClose.addEventListener("click", function () {
922
- setContextView("off");
923
- applyContextView("off");
924
- });
925
- }
926
-
927
- if (contextPanelMinimize) {
928
- contextPanelMinimize.addEventListener("click", minimizeContext);
929
- }
930
-
931
- // Restore context view on load
932
- applyContextView(getContextView());
933
-
934
- if (contextMini) {
935
- contextMini.addEventListener("click", expandContext);
936
- }
937
-
938
- function addToMessages(el) {
939
- if (prependAnchor) messagesEl.insertBefore(el, prependAnchor);
940
- else messagesEl.appendChild(el);
941
- }
942
-
943
- var newMsgBtn = $("new-msg-btn");
944
- var newMsgBtnDefault = "\u2193 Latest";
945
- var newMsgBtnActivity = "\u2193 New activity";
946
-
947
- messagesEl.addEventListener("scroll", function () {
948
- var distFromBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
949
- isUserScrolledUp = distFromBottom > scrollThreshold;
950
- if (isUserScrolledUp) {
951
- if (newMsgBtn.classList.contains("hidden")) {
952
- newMsgBtn.textContent = newMsgBtnDefault;
953
- }
954
- newMsgBtn.classList.remove("hidden");
955
- } else {
956
- newMsgBtn.classList.add("hidden");
957
- newMsgBtn.textContent = newMsgBtnDefault;
958
- }
959
- });
960
-
961
- newMsgBtn.addEventListener("click", function () {
962
- forceScrollToBottom();
963
- });
964
-
965
- function scrollToBottom() {
966
- if (prependAnchor) return;
967
- if (isUserScrolledUp) {
968
- newMsgBtn.textContent = newMsgBtnActivity;
969
- newMsgBtn.classList.remove("hidden");
970
- return;
971
- }
972
- requestAnimationFrame(function () {
973
- messagesEl.scrollTop = messagesEl.scrollHeight;
974
- });
975
- }
976
-
977
- function forceScrollToBottom() {
978
- if (prependAnchor) return;
979
- isUserScrolledUp = false;
980
- newMsgBtn.classList.add("hidden");
981
- newMsgBtn.textContent = newMsgBtnDefault;
982
- requestAnimationFrame(function () {
983
- messagesEl.scrollTop = messagesEl.scrollHeight;
984
- });
985
- }
986
-
987
- // --- Tools module ---
988
- initTools({
989
- $: $,
990
- get ws() { return ws; },
991
- get connected() { return connected; },
992
- get turnCounter() { return turnCounter; },
993
- messagesEl: messagesEl,
994
- inputEl: inputEl,
995
- finalizeAssistantBlock: function() { finalizeAssistantBlock(); },
996
- addToMessages: function(el) { addToMessages(el); },
997
- scrollToBottom: function() { scrollToBottom(); },
998
- setActivity: function(text) { setActivity(text); },
999
- stopUrgentBlink: function() { stopUrgentBlink(); },
1000
- });
1001
-
1002
- // isPlanFile, toolSummary, toolActivityText, shortPath -> modules/tools.js
1003
-
1004
- // AskUserQuestion, PermissionRequest, Plan, Todo, Thinking, Tool items -> modules/tools.js
1005
-
1006
- // --- DOM: Messages ---
1007
- function addUserMessage(text, images, pastes) {
1008
- var div = document.createElement("div");
1009
- div.className = "msg-user";
1010
- div.dataset.turn = ++turnCounter;
1011
- var bubble = document.createElement("div");
1012
- bubble.className = "bubble";
1013
- bubble.dir = "auto";
1014
-
1015
- if (images && images.length > 0) {
1016
- var imgRow = document.createElement("div");
1017
- imgRow.className = "bubble-images";
1018
- for (var i = 0; i < images.length; i++) {
1019
- var img = document.createElement("img");
1020
- img.src = "data:" + images[i].mediaType + ";base64," + images[i].data;
1021
- img.className = "bubble-img";
1022
- img.addEventListener("click", function () { showImageModal(this.src); });
1023
- imgRow.appendChild(img);
1024
- }
1025
- bubble.appendChild(imgRow);
1026
- }
1027
-
1028
- if (pastes && pastes.length > 0) {
1029
- var pasteRow = document.createElement("div");
1030
- pasteRow.className = "bubble-pastes";
1031
- for (var p = 0; p < pastes.length; p++) {
1032
- (function (pasteText) {
1033
- var chip = document.createElement("div");
1034
- chip.className = "bubble-paste";
1035
- var preview = pasteText.substring(0, 60).replace(/\n/g, " ");
1036
- if (pasteText.length > 60) preview += "...";
1037
- chip.innerHTML = '<span class="bubble-paste-preview">' + escapeHtml(preview) + '</span><span class="bubble-paste-label">PASTED</span>';
1038
- chip.addEventListener("click", function (e) {
1039
- e.stopPropagation();
1040
- showPasteModal(pasteText);
1041
- });
1042
- pasteRow.appendChild(chip);
1043
- })(pastes[p]);
1044
- }
1045
- bubble.appendChild(pasteRow);
1046
- }
1047
-
1048
- if (text) {
1049
- var textEl = document.createElement("span");
1050
- textEl.textContent = text;
1051
- bubble.appendChild(textEl);
1052
- }
1053
-
1054
- div.appendChild(bubble);
1055
- addToMessages(div);
1056
- forceScrollToBottom();
1057
- }
1058
-
1059
- function ensureAssistantBlock() {
1060
- if (!currentMsgEl) {
1061
- currentMsgEl = document.createElement("div");
1062
- currentMsgEl.className = "msg-assistant";
1063
- currentMsgEl.dataset.turn = turnCounter;
1064
- currentMsgEl.innerHTML = '<div class="md-content" dir="auto"></div>';
1065
- addToMessages(currentMsgEl);
1066
- currentFullText = "";
1067
- }
1068
- return currentMsgEl;
1069
- }
1070
-
1071
- function addCopyHandler(msgEl, rawText) {
1072
- var primed = false;
1073
- var resetTimer = null;
1074
-
1075
- var isTouchDevice = "ontouchstart" in window;
1076
-
1077
- var hint = document.createElement("div");
1078
- hint.className = "msg-copy-hint";
1079
- hint.textContent = (isTouchDevice ? "Tap" : "Click") + " to grab this";
1080
- msgEl.appendChild(hint);
1081
-
1082
- function reset() {
1083
- primed = false;
1084
- msgEl.classList.remove("copy-primed", "copy-done");
1085
- hint.textContent = (isTouchDevice ? "Tap" : "Click") + " to grab this";
1086
- }
1087
-
1088
- msgEl.addEventListener("click", function (e) {
1089
- // Don't intercept clicks on links or code blocks
1090
- if (e.target.closest("a, pre, code")) return;
1091
- // Don't intercept text selection
1092
- var sel = window.getSelection();
1093
- if (sel && sel.toString().length > 0) return;
1094
-
1095
- if (!primed) {
1096
- primed = true;
1097
- msgEl.classList.add("copy-primed");
1098
- hint.textContent = isTouchDevice ? "Tap again to grab" : "Click again to grab";
1099
- clearTimeout(resetTimer);
1100
- resetTimer = setTimeout(reset, 3000);
1101
- } else {
1102
- clearTimeout(resetTimer);
1103
- copyToClipboard(rawText).then(function () {
1104
- msgEl.classList.remove("copy-primed");
1105
- msgEl.classList.add("copy-done");
1106
- hint.textContent = "Grabbed!";
1107
- resetTimer = setTimeout(reset, 1500);
1108
- });
1109
- }
1110
- });
1111
-
1112
- document.addEventListener("click", function (e) {
1113
- if (primed && !msgEl.contains(e.target)) reset();
1114
- });
1115
- }
1116
-
1117
- function appendDelta(text) {
1118
- ensureAssistantBlock();
1119
- currentFullText += text;
1120
- var contentEl = currentMsgEl.querySelector(".md-content");
1121
- contentEl.innerHTML = renderMarkdown(currentFullText);
1122
-
1123
- if (highlightTimer) clearTimeout(highlightTimer);
1124
- highlightTimer = setTimeout(function () {
1125
- highlightCodeBlocks(contentEl);
1126
- }, 150);
1127
-
1128
- scrollToBottom();
1129
- }
1130
-
1131
- function finalizeAssistantBlock() {
1132
- if (currentMsgEl) {
1133
- var contentEl = currentMsgEl.querySelector(".md-content");
1134
- if (contentEl) {
1135
- highlightCodeBlocks(contentEl);
1136
- renderMermaidBlocks(contentEl);
1137
- }
1138
- if (currentFullText) {
1139
- addCopyHandler(currentMsgEl, currentFullText);
1140
- }
1141
- // Assistant text appeared, so break the current tool group
1142
- closeToolGroup();
1143
- }
1144
- currentMsgEl = null;
1145
- currentFullText = "";
1146
- }
1147
-
1148
- function addSystemMessage(text, isError) {
1149
- var div = document.createElement("div");
1150
- div.className = "sys-msg" + (isError ? " error" : "");
1151
- div.innerHTML = '<span class="sys-text"></span>';
1152
- div.querySelector(".sys-text").textContent = text;
1153
- addToMessages(div);
1154
- scrollToBottom();
1155
- }
1156
-
1157
- function resetClientState() {
1158
- messagesEl.innerHTML = "";
1159
- currentMsgEl = null;
1160
- currentFullText = "";
1161
- resetToolState();
1162
- clearPendingImages();
1163
- activityEl = null;
1164
- processing = false;
1165
- turnCounter = 0;
1166
- messageUuidMap = [];
1167
- historyFrom = 0;
1168
- historyTotal = 0;
1169
- prependAnchor = null;
1170
- loadingMore = false;
1171
- isUserScrolledUp = false;
1172
- newMsgBtn.classList.add("hidden");
1173
- setRewindMode(false);
1174
- removeSearchTimeline();
1175
- setActivity(null);
1176
- setStatus("connected");
1177
- enableMainInput();
1178
- resetUsage();
1179
- resetContext();
1180
- }
1181
-
1182
- // --- WebSocket ---
1183
- var connectTimeoutId = null;
1184
-
1185
- function connect() {
1186
- if (ws) { ws.onclose = null; ws.close(); }
1187
- if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
1188
-
1189
- var protocol = location.protocol === "https:" ? "wss:" : "ws:";
1190
- ws = new WebSocket(protocol + "//" + location.host + wsPath);
1191
-
1192
- connectStatusEl.textContent = "Connecting...";
1193
-
1194
- // If not connected within 3s, force retry
1195
- connectTimeoutId = setTimeout(function () {
1196
- if (!connected) {
1197
- ws.onclose = null;
1198
- ws.onerror = null;
1199
- ws.close();
1200
- connect();
1201
- }
1202
- }, 3000);
1203
-
1204
- ws.onopen = function () {
1205
- if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
1206
- // Cancel pending "connection lost" notification if reconnected quickly
1207
- if (disconnectNotifTimer) {
1208
- clearTimeout(disconnectNotifTimer);
1209
- disconnectNotifTimer = null;
1210
- }
1211
- // Only show "restored" notification if "lost" was actually shown
1212
- if (wasConnected && disconnectNotifShown && !document.hasFocus() && "serviceWorker" in navigator) {
1213
- navigator.serviceWorker.ready.then(function (reg) {
1214
- reg.showNotification("Claude Relay", {
1215
- body: "Server connection restored",
1216
- tag: "claude-disconnect",
1217
- });
1218
- }).catch(function () {});
1219
- }
1220
- disconnectNotifShown = false;
1221
- wasConnected = true;
1222
- setStatus("connected");
1223
- reconnectDelay = 1000;
1224
- if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
1225
-
1226
- // Reset terminal xterm instances (server will send fresh term_list)
1227
- resetTerminals();
1228
-
1229
- // Re-send push subscription on reconnect
1230
- if (window._pushSubscription) {
1231
- try {
1232
- ws.send(JSON.stringify({
1233
- type: "push_subscribe",
1234
- subscription: window._pushSubscription.toJSON(),
1235
- }));
1236
- } catch(e) {}
1237
- }
1238
- };
1239
-
1240
- ws.onclose = function (e) {
1241
- if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
1242
- connectStatusEl.textContent = "Connection lost. Retrying...";
1243
- setStatus("disconnected");
1244
- processing = false;
1245
- setActivity(null);
1246
- // Delay "connection lost" notification by 5s to suppress brief disconnects
1247
- if (!disconnectNotifTimer) {
1248
- disconnectNotifTimer = setTimeout(function () {
1249
- disconnectNotifTimer = null;
1250
- disconnectNotifShown = true;
1251
- if (!document.hasFocus() && "serviceWorker" in navigator) {
1252
- navigator.serviceWorker.ready.then(function (reg) {
1253
- reg.showNotification("Claude Relay", {
1254
- body: "Server connection lost",
1255
- tag: "claude-disconnect",
1256
- });
1257
- }).catch(function () {});
1258
- }
1259
- }, 5000);
1260
- }
1261
- scheduleReconnect();
1262
- };
1263
-
1264
- ws.onerror = function () {
1265
- connectStatusEl.textContent = "Connection error. Retrying...";
1266
- };
1267
-
1268
- ws.onmessage = function (event) {
1269
- // Backup: if we're receiving messages, we're connected
1270
- if (!connected) {
1271
- setStatus("connected");
1272
- reconnectDelay = 1000;
1273
- if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
1274
- }
1275
-
1276
- blinkIO();
1277
- var msg;
1278
- try { msg = JSON.parse(event.data); } catch (e) { return; }
1279
- processMessage(msg);
1280
- };
1281
- }
1282
-
1283
- function processMessage(msg) {
1284
- switch (msg.type) {
1285
- case "history_meta":
1286
- historyFrom = msg.from;
1287
- historyTotal = msg.total;
1288
- updateHistorySentinel();
1289
- break;
1290
-
1291
- case "history_prepend":
1292
- prependOlderHistory(msg.items, msg.meta);
1293
- break;
1294
-
1295
- case "history_done":
1296
- // Render + finalize any incomplete turn from the replayed history
1297
- if (currentMsgEl && currentFullText) {
1298
- var replayContentEl = currentMsgEl.querySelector(".md-content");
1299
- if (replayContentEl) {
1300
- replayContentEl.innerHTML = renderMarkdown(currentFullText);
1301
- }
1302
- }
1303
- markAllToolsDone();
1304
- finalizeAssistantBlock();
1305
- scrollToBottom();
1306
- var pendingQuery = getActiveSearchQuery();
1307
- if (pendingQuery) {
1308
- requestAnimationFrame(function() { buildSearchTimeline(pendingQuery); });
1309
- }
1310
- // Scroll to tool element if navigating from file edit history
1311
- var nav = getPendingNavigate();
1312
- if (nav && (nav.toolId || nav.assistantUuid)) {
1313
- requestAnimationFrame(function() {
1314
- // Prefer scrolling to the exact tool element
1315
- var target = nav.toolId ? messagesEl.querySelector('[data-tool-id="' + nav.toolId + '"]') : null;
1316
- if (!target && nav.assistantUuid) {
1317
- target = messagesEl.querySelector('[data-uuid="' + nav.assistantUuid + '"]');
1318
- }
1319
- if (target) {
1320
- // Auto-expand parent tool group if collapsed
1321
- var parentGroup = target.closest(".tool-group");
1322
- if (parentGroup) parentGroup.classList.remove("collapsed");
1323
- target.scrollIntoView({ behavior: "smooth", block: "center" });
1324
- target.classList.add("message-blink");
1325
- setTimeout(function() { target.classList.remove("message-blink"); }, 2000);
1326
- }
1327
- });
1328
- }
1329
- break;
1330
-
1331
- case "info":
1332
- projectName = msg.project || msg.cwd;
1333
- if (msg.slug) currentSlug = msg.slug;
1334
- headerTitleEl.textContent = projectName;
1335
- updatePageTitle();
1336
- if (msg.version) {
1337
- var vEl = $("footer-version");
1338
- if (vEl) vEl.textContent = "v" + msg.version;
1339
- }
1340
- if (msg.debug) {
1341
- var debugWrap = $("debug-menu-wrap");
1342
- if (debugWrap) debugWrap.classList.remove("hidden");
1343
- }
1344
- if (msg.lanHost) window.__lanHost = msg.lanHost;
1345
- if (msg.dangerouslySkipPermissions) {
1346
- var spBanner = $("skip-perms-banner");
1347
- if (spBanner) spBanner.classList.remove("hidden");
1348
- }
1349
- updateProjectList(msg);
1350
- break;
1351
-
1352
- case "update_available":
1353
- var updateBanner = $("update-banner");
1354
- var updateVersion = $("update-version");
1355
- if (updateBanner && updateVersion && msg.version) {
1356
- updateVersion.textContent = "v" + msg.version;
1357
- updateBanner.classList.remove("hidden");
1358
- // Reset button state (may be stuck on "Updating..." after restart)
1359
- var updResetBtn = $("update-now");
1360
- if (updResetBtn) {
1361
- updResetBtn.textContent = "Update now";
1362
- updResetBtn.disabled = false;
1363
- }
1364
- refreshIcons();
1365
- }
1366
- // Show badge on footer update check item
1367
- var footerUpdateBtn = $("footer-update-check");
1368
- if (footerUpdateBtn && msg.version) {
1369
- var labelSpan = footerUpdateBtn.querySelector("span");
1370
- if (labelSpan) labelSpan.textContent = "Update available";
1371
- footerUpdateBtn.classList.add("has-badge");
1372
- var existingBadge = footerUpdateBtn.querySelector(".menu-badge");
1373
- if (!existingBadge) {
1374
- var badge = document.createElement("span");
1375
- badge.className = "menu-badge";
1376
- badge.textContent = "v" + msg.version;
1377
- footerUpdateBtn.appendChild(badge);
1378
- }
1379
- }
1380
- break;
1381
-
1382
- case "update_started":
1383
- var updNowBtn = $("update-now");
1384
- if (updNowBtn) {
1385
- updNowBtn.textContent = "Updating...";
1386
- updNowBtn.disabled = true;
1387
- }
1388
- // Block the entire screen with the connect overlay
1389
- connectStatusEl.textContent = "Updating" + (msg.version ? " to v" + msg.version : "") + "...";
1390
- connectOverlay.classList.remove("hidden");
1391
- startPixelAnim();
1392
- break;
1393
-
1394
- case "slash_commands":
1395
- var reserved = new Set(builtinCommands.map(function (c) { return c.name; }));
1396
- slashCommands = (msg.commands || []).filter(function (name) {
1397
- return !reserved.has(name);
1398
- }).map(function (name) {
1399
- return { name: name, desc: "Skill" };
1400
- });
1401
- break;
1402
-
1403
- case "model_info":
1404
- updateModelSelector(msg.model, msg.models || []);
1405
- break;
1406
-
1407
- case "client_count":
1408
- var countEl = document.getElementById("client-count");
1409
- if (countEl) {
1410
- if (msg.count > 1) {
1411
- countEl.textContent = msg.count;
1412
- countEl.dataset.tip = msg.count + " devices connected";
1413
- countEl.classList.remove("hidden");
1414
- } else {
1415
- countEl.classList.add("hidden");
1416
- }
1417
- }
1418
- break;
1419
-
1420
- case "toast":
1421
- showToast(msg.message, msg.level, msg.detail);
1422
- break;
1423
-
1424
- case "input_sync":
1425
- handleInputSync(msg.text);
1426
- break;
1427
-
1428
- case "session_list":
1429
- renderSessionList(msg.sessions || []);
1430
- break;
1431
-
1432
- case "search_results":
1433
- handleSearchResults(msg);
1434
- break;
1435
-
1436
- case "cli_session_list":
1437
- populateCliSessionList(msg.sessions || []);
1438
- break;
1439
-
1440
- case "session_switched":
1441
- // Save draft from outgoing session
1442
- if (activeSessionId && inputEl.value) {
1443
- sessionDrafts[activeSessionId] = inputEl.value;
1444
- } else if (activeSessionId) {
1445
- delete sessionDrafts[activeSessionId];
1446
- }
1447
- activeSessionId = msg.id;
1448
- cliSessionId = msg.cliSessionId || null;
1449
- resetClientState();
1450
- // Restore draft for incoming session
1451
- var draft = sessionDrafts[activeSessionId] || "";
1452
- inputEl.value = draft;
1453
- autoResize();
1454
- if (!("ontouchstart" in window)) {
1455
- inputEl.focus();
1456
- }
1457
- break;
1458
-
1459
- case "session_id":
1460
- cliSessionId = msg.cliSessionId;
1461
- break;
1462
-
1463
- case "message_uuid":
1464
- var uuidTarget;
1465
- if (msg.messageType === "user") {
1466
- var allUsers = messagesEl.querySelectorAll(".msg-user:not([data-uuid])");
1467
- if (allUsers.length > 0) uuidTarget = allUsers[allUsers.length - 1];
1468
- } else {
1469
- var allAssistants = messagesEl.querySelectorAll(".msg-assistant:not([data-uuid])");
1470
- if (allAssistants.length > 0) uuidTarget = allAssistants[allAssistants.length - 1];
1471
- }
1472
- if (uuidTarget) {
1473
- uuidTarget.dataset.uuid = msg.uuid;
1474
- }
1475
- messageUuidMap.push({ uuid: msg.uuid, type: msg.messageType });
1476
- break;
1477
-
1478
- case "user_message":
1479
- addUserMessage(msg.text, msg.images || null, msg.pastes || null);
1480
- break;
1481
-
1482
- case "status":
1483
- if (msg.status === "processing") {
1484
- setStatus("processing");
1485
- setActivity(randomThinkingVerb() + "...");
1486
- }
1487
- break;
1488
-
1489
- case "compacting":
1490
- if (msg.active) {
1491
- setActivity("Compacting conversation...");
1492
- } else {
1493
- setActivity(randomThinkingVerb() + "...");
1494
- }
1495
- break;
1496
-
1497
- case "thinking_start":
1498
- startThinking();
1499
- break;
1500
-
1501
- case "thinking_delta":
1502
- if (typeof msg.text === "string") appendThinking(msg.text);
1503
- break;
1504
-
1505
- case "thinking_stop":
1506
- stopThinking();
1507
- setActivity(randomThinkingVerb() + "...");
1508
- break;
1509
-
1510
- case "delta":
1511
- if (typeof msg.text !== "string") break;
1512
- stopThinking();
1513
- setActivity(null);
1514
- appendDelta(msg.text);
1515
- break;
1516
-
1517
- case "tool_start":
1518
- stopThinking();
1519
- markAllToolsDone();
1520
- if (msg.name === "EnterPlanMode") {
1521
- renderPlanBanner("enter");
1522
- getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
1523
- } else if (msg.name === "ExitPlanMode") {
1524
- if (getPlanContent()) {
1525
- renderPlanCard(getPlanContent());
1526
- }
1527
- renderPlanBanner("exit");
1528
- getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
1529
- } else if (getTodoTools()[msg.name]) {
1530
- getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
1531
- } else {
1532
- createToolItem(msg.id, msg.name);
1533
- }
1534
- break;
1535
-
1536
- case "tool_executing":
1537
- if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
1538
- var askTool = getTools()[msg.id];
1539
- if (askTool) {
1540
- if (askTool.el) askTool.el.style.display = "none";
1541
- askTool.done = true;
1542
- removeToolFromGroup(msg.id);
1543
- }
1544
- renderAskUserQuestion(msg.id, msg.input);
1545
- startUrgentBlink();
1546
- } else if (msg.name === "Write" && msg.input && isPlanFilePath(msg.input.file_path)) {
1547
- setPlanContent(msg.input.content || "");
1548
- updateToolExecuting(msg.id, msg.name, msg.input);
1549
- } else if (msg.name === "TodoWrite") {
1550
- handleTodoWrite(msg.input);
1551
- } else if (msg.name === "TaskCreate") {
1552
- handleTaskCreate(msg.input);
1553
- } else if (msg.name === "TaskUpdate") {
1554
- handleTaskUpdate(msg.input);
1555
- } else if (getTodoTools()[msg.name]) {
1556
- // TaskList, TaskGet - silently skip
1557
- } else {
1558
- var t = getTools()[msg.id];
1559
- if (t && t.hidden) break;
1560
- updateToolExecuting(msg.id, msg.name, msg.input);
1561
- }
1562
- break;
1563
-
1564
- case "tool_result": {
1565
- var tr = getTools()[msg.id];
1566
- if (tr && tr.hidden) break; // skip hidden plan tools
1567
- // Always call updateToolResult for Edit (to show diff from input), or when content exists
1568
- if (msg.content != null || (tr && tr.name === "Edit" && tr.input && tr.input.old_string)) {
1569
- updateToolResult(msg.id, msg.content || "", msg.is_error || false);
1570
- }
1571
- // Refresh file browser if an Edit/Write tool modified the open file
1572
- if (!msg.is_error && tr && (tr.name === "Edit" || tr.name === "Write") && tr.input && tr.input.file_path) {
1573
- refreshIfOpen(tr.input.file_path);
1574
- }
1575
- }
1576
- break;
1577
-
1578
- case "ask_user_answered":
1579
- markAskUserAnswered(msg.toolId);
1580
- stopUrgentBlink();
1581
- break;
1582
-
1583
- case "permission_request":
1584
- renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason);
1585
- startUrgentBlink();
1586
- break;
1587
-
1588
- case "permission_cancel":
1589
- markPermissionCancelled(msg.requestId);
1590
- stopUrgentBlink();
1591
- break;
1592
-
1593
- case "permission_resolved":
1594
- markPermissionResolved(msg.requestId, msg.decision);
1595
- stopUrgentBlink();
1596
- break;
1597
-
1598
- case "permission_request_pending":
1599
- renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason);
1600
- startUrgentBlink();
1601
- break;
1602
-
1603
- case "slash_command_result":
1604
- finalizeAssistantBlock();
1605
- var cmdBlock = document.createElement("div");
1606
- cmdBlock.className = "assistant-block";
1607
- cmdBlock.style.maxWidth = "var(--content-width)";
1608
- cmdBlock.style.margin = "12px auto";
1609
- cmdBlock.style.padding = "0 20px";
1610
- var pre = document.createElement("pre");
1611
- pre.style.cssText = "background:var(--code-bg);border:1px solid var(--border-subtle);border-radius:10px;padding:12px 14px;font-family:'SF Mono',Menlo,Monaco,monospace;font-size:12px;line-height:1.55;color:var(--text-secondary);white-space:pre-wrap;word-break:break-word;max-height:400px;overflow-y:auto;margin:0";
1612
- pre.textContent = msg.text;
1613
- cmdBlock.appendChild(pre);
1614
- addToMessages(cmdBlock);
1615
- scrollToBottom();
1616
- break;
1617
-
1618
- case "subagent_activity":
1619
- updateSubagentActivity(msg.parentToolId, msg.text);
1620
- break;
1621
-
1622
- case "subagent_tool":
1623
- addSubagentToolEntry(msg.parentToolId, msg.toolName, msg.toolId, msg.text);
1624
- break;
1625
-
1626
- case "subagent_done":
1627
- markSubagentDone(msg.parentToolId);
1628
- break;
1629
-
1630
- case "result":
1631
- setActivity(null);
1632
- stopThinking();
1633
- markAllToolsDone();
1634
- closeToolGroup();
1635
- finalizeAssistantBlock();
1636
- addTurnMeta(msg.cost, msg.duration);
1637
- accumulateUsage(msg.cost, msg.usage);
1638
- accumulateContext(msg.cost, msg.usage, msg.modelUsage);
1639
- break;
1640
-
1641
- case "done":
1642
- setActivity(null);
1643
- stopThinking();
1644
- markAllToolsDone();
1645
- closeToolGroup();
1646
- finalizeAssistantBlock();
1647
- processing = false;
1648
- setStatus("connected");
1649
- enableMainInput();
1650
- resetToolState();
1651
- if (document.hidden) {
1652
- if (isNotifAlertEnabled() && !window._pushSubscription) showDoneNotification();
1653
- if (isNotifSoundEnabled()) playDoneSound();
1654
- }
1655
- break;
1656
-
1657
- case "stderr":
1658
- addSystemMessage(msg.text, false);
1659
- break;
1660
-
1661
- case "info":
1662
- addSystemMessage(msg.text, false);
1663
- break;
1664
-
1665
- case "error":
1666
- setActivity(null);
1667
- addSystemMessage(msg.text, true);
1668
- updateFavicon(getComputedVar("--error"));
1669
- break;
1670
-
1671
- case "rewind_preview_result":
1672
- showRewindModal(msg);
1673
- break;
1674
-
1675
- case "rewind_complete":
1676
- setRewindMode(false);
1677
- var rewindText = "Rewound to earlier point. Files have been restored.";
1678
- if (msg.mode === "chat") rewindText = "Conversation rewound to earlier point.";
1679
- else if (msg.mode === "files") rewindText = "Files restored to earlier point.";
1680
- addSystemMessage(rewindText, false);
1681
- break;
1682
-
1683
- case "rewind_error":
1684
- clearPendingRewindUuid();
1685
- addSystemMessage(msg.text || "Rewind failed.", true);
1686
- break;
1687
-
1688
- case "fs_list_result":
1689
- handleFsList(msg);
1690
- break;
1691
-
1692
- case "fs_read_result":
1693
- handleFsRead(msg);
1694
- break;
1695
-
1696
- case "fs_file_changed":
1697
- handleFileChanged(msg);
1698
- break;
1699
-
1700
- case "fs_dir_changed":
1701
- handleDirChanged(msg);
1702
- break;
1703
-
1704
- case "fs_file_history_result":
1705
- handleFileHistory(msg);
1706
- break;
1707
-
1708
- case "fs_git_diff_result":
1709
- handleGitDiff(msg);
1710
- break;
1711
-
1712
- case "fs_file_at_result":
1713
- handleFileAt(msg);
1714
- break;
1715
-
1716
- case "term_list":
1717
- handleTermList(msg);
1718
- break;
1719
-
1720
- case "term_created":
1721
- handleTermCreated(msg);
1722
- break;
1723
-
1724
- case "term_output":
1725
- handleTermOutput(msg);
1726
- break;
1727
-
1728
- case "term_exited":
1729
- handleTermExited(msg);
1730
- break;
1731
-
1732
- case "term_closed":
1733
- handleTermClosed(msg);
1734
- break;
1735
-
1736
- case "process_stats":
1737
- updateStatusPanel(msg);
1738
- break;
1739
-
1740
- case "browse_dir_result":
1741
- handleBrowseDirResult(msg);
1742
- break;
1743
-
1744
- case "add_project_result":
1745
- handleAddProjectResult(msg);
1746
- break;
1747
-
1748
- case "remove_project_result":
1749
- handleRemoveProjectResult(msg);
1750
- break;
1751
-
1752
- case "projects_updated":
1753
- updateProjectList(msg);
1754
- break;
1755
- }
1756
- }
1757
-
1758
- // --- Progressive history loading ---
1759
- function updateHistorySentinel() {
1760
- var existing = messagesEl.querySelector(".history-sentinel");
1761
- if (historyFrom > 0) {
1762
- if (!existing) {
1763
- var sentinel = document.createElement("div");
1764
- sentinel.className = "history-sentinel";
1765
- sentinel.innerHTML = '<button class="load-more-btn">Load earlier messages</button>';
1766
- sentinel.querySelector(".load-more-btn").addEventListener("click", function () {
1767
- requestMoreHistory();
1768
- });
1769
- messagesEl.insertBefore(sentinel, messagesEl.firstChild);
1770
-
1771
- // Auto-load when sentinel scrolls into view
1772
- if (historySentinelObserver) historySentinelObserver.disconnect();
1773
- historySentinelObserver = new IntersectionObserver(function (entries) {
1774
- if (entries[0].isIntersecting && !loadingMore && historyFrom > 0) {
1775
- requestMoreHistory();
1776
- }
1777
- }, { root: messagesEl, rootMargin: "200px 0px 0px 0px" });
1778
- historySentinelObserver.observe(sentinel);
1779
- }
1780
- } else {
1781
- if (existing) existing.remove();
1782
- if (historySentinelObserver) { historySentinelObserver.disconnect(); historySentinelObserver = null; }
1783
- }
1784
- }
1785
-
1786
- function requestMoreHistory() {
1787
- if (loadingMore || historyFrom <= 0 || !ws || !connected) return;
1788
- loadingMore = true;
1789
- var btn = messagesEl.querySelector(".load-more-btn");
1790
- if (btn) btn.classList.add("loading");
1791
- ws.send(JSON.stringify({ type: "load_more_history", before: historyFrom }));
1792
- }
1793
-
1794
- function prependOlderHistory(items, meta) {
1795
- // Save current rendering state
1796
- var savedMsgEl = currentMsgEl;
1797
- var savedActivity = activityEl;
1798
- var savedFullText = currentFullText;
1799
- var savedTurnCounter = turnCounter;
1800
- var savedToolsState = saveToolState();
1801
-
1802
- // Reset to initial values for clean rendering
1803
- currentMsgEl = null;
1804
- activityEl = null;
1805
- currentFullText = "";
1806
- turnCounter = 0;
1807
- resetToolState();
1808
-
1809
- // Set prepend anchor to insert before existing content
1810
- // Skip the sentinel itself when setting anchor
1811
- var firstReal = messagesEl.querySelector(".history-sentinel");
1812
- prependAnchor = firstReal ? firstReal.nextSibling : messagesEl.firstChild;
1813
-
1814
- // Remember the first existing content element and its position
1815
- var anchorEl = prependAnchor;
1816
- var anchorOffset = anchorEl ? anchorEl.getBoundingClientRect().top : 0;
1817
-
1818
- // Process each item through the rendering pipeline
1819
- for (var i = 0; i < items.length; i++) {
1820
- processMessage(items[i]);
1821
- }
1822
-
1823
- // Finalize any open assistant block from the batch
1824
- finalizeAssistantBlock();
1825
-
1826
- // Clear prepend mode
1827
- prependAnchor = null;
1828
-
1829
- // Restore saved state
1830
- currentMsgEl = savedMsgEl;
1831
- activityEl = savedActivity;
1832
- currentFullText = savedFullText;
1833
- turnCounter = savedTurnCounter;
1834
- restoreToolState(savedToolsState);
1835
-
1836
- // Fix scroll: restore anchor element to same visual position
1837
- if (anchorEl) {
1838
- var newTop = anchorEl.getBoundingClientRect().top;
1839
- messagesEl.scrollTop += (newTop - anchorOffset);
1840
- }
1841
-
1842
- // Update state
1843
- historyFrom = meta.from;
1844
- loadingMore = false;
1845
-
1846
- // Renumber data-turn attributes in DOM order
1847
- var turnEls = messagesEl.querySelectorAll("[data-turn]");
1848
- for (var t = 0; t < turnEls.length; t++) {
1849
- turnEls[t].dataset.turn = t + 1;
1850
- }
1851
- turnCounter = turnEls.length;
1852
-
1853
- // Update sentinel
1854
- if (meta.hasMore) {
1855
- var btn = messagesEl.querySelector(".load-more-btn");
1856
- if (btn) btn.classList.remove("loading");
1857
- } else {
1858
- updateHistorySentinel();
1859
- }
1860
- }
1861
-
1862
- function scheduleReconnect() {
1863
- if (reconnectTimer) return;
1864
- reconnectTimer = setTimeout(function () {
1865
- reconnectTimer = null;
1866
- connect();
1867
- }, reconnectDelay);
1868
- reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
1869
- }
1870
-
1871
- // --- Input module (sendMessage, autoResize, paste/image, slash menu, input handlers) ---
1872
- initInput({
1873
- get ws() { return ws; },
1874
- get connected() { return connected; },
1875
- get processing() { return processing; },
1876
- inputEl: inputEl,
1877
- sendBtn: sendBtn,
1878
- slashMenu: slashMenu,
1879
- messagesEl: messagesEl,
1880
- imagePreviewBar: imagePreviewBar,
1881
- slashCommands: function() { return slashCommands; },
1882
- messageUuidMap: function() { return messageUuidMap; },
1883
- addUserMessage: addUserMessage,
1884
- addSystemMessage: addSystemMessage,
1885
- toggleUsagePanel: toggleUsagePanel,
1886
- toggleStatusPanel: toggleStatusPanel,
1887
- toggleContextPanel: toggleContextPanel,
1888
- resetContextData: resetContextData,
1889
- showImageModal: showImageModal,
1890
- });
1891
-
1892
- // --- Notifications module (viewport, banners, notifications, debug, service worker) ---
1893
- initNotifications({
1894
- $: $,
1895
- get ws() { return ws; },
1896
- get connected() { return connected; },
1897
- messagesEl: messagesEl,
1898
- sessionListEl: sessionListEl,
1899
- scrollToBottom: scrollToBottom,
1900
- basePath: basePath,
1901
- toggleUsagePanel: toggleUsagePanel,
1902
- toggleStatusPanel: toggleStatusPanel,
1903
- });
1904
-
1905
- // --- QR code ---
1906
- initQrCode();
1907
-
1908
- // --- File browser ---
1909
- initFileBrowser({
1910
- get ws() { return ws; },
1911
- get connected() { return connected; },
1912
- get activeSessionId() { return activeSessionId; },
1913
- messagesEl: messagesEl,
1914
- fileTreeEl: $("file-tree"),
1915
- fileViewerEl: $("file-viewer"),
1916
- });
1917
-
1918
- // --- Terminal ---
1919
- initTerminal({
1920
- get ws() { return ws; },
1921
- get connected() { return connected; },
1922
- terminalContainerEl: $("terminal-container"),
1923
- terminalBodyEl: $("terminal-body"),
1924
- fileViewerEl: $("file-viewer"),
1925
- });
1926
-
1927
- // --- Remove project ---
1928
- function confirmRemoveProject(slug, name) {
1929
- showConfirm("Remove project \"" + name + "\"?", function () {
1930
- if (ws && ws.readyState === 1) {
1931
- ws.send(JSON.stringify({ type: "remove_project", slug: slug }));
1932
- }
1933
- });
1934
- }
1935
-
1936
- function handleRemoveProjectResult(msg) {
1937
- if (msg.ok) {
1938
- showToast("Project removed", "success");
1939
- // If we removed the current project, navigate to first available
1940
- if (msg.slug === currentSlug) {
1941
- window.location.href = "/";
1942
- }
1943
- } else {
1944
- showToast(msg.error || "Failed to remove project", "error");
1945
- }
1946
- }
1947
-
1948
- // --- Add project modal ---
1949
- var addProjectModal = document.getElementById("add-project-modal");
1950
- var addProjectInput = document.getElementById("add-project-input");
1951
- var addProjectSuggestions = document.getElementById("add-project-suggestions");
1952
- var addProjectError = document.getElementById("add-project-error");
1953
- var addProjectOk = document.getElementById("add-project-ok");
1954
- var addProjectCancel = document.getElementById("add-project-cancel");
1955
- var addProjectDebounce = null;
1956
- var addProjectActiveIdx = -1;
1957
-
1958
- function openAddProjectModal() {
1959
- addProjectModal.classList.remove("hidden");
1960
- addProjectInput.value = "/";
1961
- addProjectError.classList.add("hidden");
1962
- addProjectError.textContent = "";
1963
- addProjectSuggestions.classList.add("hidden");
1964
- addProjectSuggestions.innerHTML = "";
1965
- addProjectActiveIdx = -1;
1966
- addProjectOk.disabled = false;
1967
- setTimeout(function () {
1968
- addProjectInput.focus();
1969
- addProjectInput.setSelectionRange(1, 1);
1970
- }, 50);
1971
- }
1972
-
1973
- function closeAddProjectModal() {
1974
- addProjectModal.classList.add("hidden");
1975
- addProjectInput.value = "";
1976
- addProjectSuggestions.classList.add("hidden");
1977
- addProjectSuggestions.innerHTML = "";
1978
- addProjectError.classList.add("hidden");
1979
- addProjectActiveIdx = -1;
1980
- if (addProjectDebounce) { clearTimeout(addProjectDebounce); addProjectDebounce = null; }
1981
- }
1982
-
1983
- function requestBrowseDir(val) {
1984
- if (!ws || ws.readyState !== 1) return;
1985
- ws.send(JSON.stringify({ type: "browse_dir", path: val }));
1986
- }
1987
-
1988
- function handleBrowseDirResult(msg) {
1989
- addProjectSuggestions.innerHTML = "";
1990
- addProjectActiveIdx = -1;
1991
- if (msg.error) {
1992
- addProjectSuggestions.classList.add("hidden");
1993
- return;
1994
- }
1995
- var entries = msg.entries || [];
1996
- if (entries.length === 0) {
1997
- addProjectSuggestions.classList.add("hidden");
1998
- return;
1999
- }
2000
- for (var si = 0; si < entries.length; si++) {
2001
- var entry = entries[si];
2002
- var item = document.createElement("div");
2003
- item.className = "add-project-suggestion-item";
2004
- item.dataset.path = entry.path;
2005
- item.innerHTML = '<i data-lucide="folder"></i><span class="add-project-suggestion-name">' +
2006
- escapeHtml(entry.name) + '</span>';
2007
- item.addEventListener("click", function (e) {
2008
- var p = this.dataset.path + "/";
2009
- addProjectInput.value = p;
2010
- addProjectInput.focus();
2011
- addProjectError.classList.add("hidden");
2012
- requestBrowseDir(p);
2013
- });
2014
- addProjectSuggestions.appendChild(item);
2015
- }
2016
- addProjectSuggestions.classList.remove("hidden");
2017
- refreshIcons();
2018
- }
2019
-
2020
- function handleAddProjectResult(msg) {
2021
- if (msg.ok) {
2022
- closeAddProjectModal();
2023
- if (msg.existing) {
2024
- showToast("Project already registered", "info");
2025
- } else {
2026
- showToast("Project added", "success");
2027
- // Navigate to the new project
2028
- if (msg.slug) {
2029
- window.location.href = "/p/" + msg.slug + "/";
2030
- }
2031
- }
2032
- } else {
2033
- addProjectError.textContent = msg.error || "Failed to add project";
2034
- addProjectError.classList.remove("hidden");
2035
- addProjectOk.disabled = false;
2036
- }
2037
- }
2038
-
2039
- function setActiveIdx(idx) {
2040
- var items = addProjectSuggestions.querySelectorAll(".add-project-suggestion-item");
2041
- addProjectActiveIdx = idx;
2042
- for (var ai = 0; ai < items.length; ai++) {
2043
- if (ai === idx) {
2044
- items[ai].classList.add("active");
2045
- items[ai].scrollIntoView({ block: "nearest" });
2046
- } else {
2047
- items[ai].classList.remove("active");
2048
- }
2049
- }
2050
- }
2051
-
2052
- addProjectInput.addEventListener("focus", function () {
2053
- var val = addProjectInput.value;
2054
- if (val && addProjectSuggestions.children.length === 0) {
2055
- requestBrowseDir(val);
2056
- } else if (addProjectSuggestions.children.length > 0) {
2057
- addProjectSuggestions.classList.remove("hidden");
2058
- }
2059
- });
2060
-
2061
- addProjectModal.querySelector(".confirm-dialog").addEventListener("click", function (e) {
2062
- if (e.target === addProjectInput || addProjectInput.contains(e.target)) return;
2063
- if (e.target === addProjectSuggestions || addProjectSuggestions.contains(e.target)) return;
2064
- addProjectSuggestions.classList.add("hidden");
2065
- addProjectActiveIdx = -1;
2066
- });
2067
-
2068
- addProjectInput.addEventListener("input", function () {
2069
- var val = addProjectInput.value;
2070
- addProjectError.classList.add("hidden");
2071
- if (addProjectDebounce) clearTimeout(addProjectDebounce);
2072
- addProjectDebounce = setTimeout(function () {
2073
- requestBrowseDir(val);
2074
- }, 200);
2075
- });
2076
-
2077
- addProjectInput.addEventListener("keydown", function (e) {
2078
- var items = addProjectSuggestions.querySelectorAll(".add-project-suggestion-item");
2079
-
2080
- if (e.key === "ArrowDown") {
2081
- e.preventDefault();
2082
- if (items.length > 0) {
2083
- var next = addProjectActiveIdx < items.length - 1 ? addProjectActiveIdx + 1 : 0;
2084
- setActiveIdx(next);
2085
- }
2086
- return;
2087
- }
2088
-
2089
- if (e.key === "ArrowUp") {
2090
- e.preventDefault();
2091
- if (items.length > 0) {
2092
- var prev = addProjectActiveIdx > 0 ? addProjectActiveIdx - 1 : items.length - 1;
2093
- setActiveIdx(prev);
2094
- }
2095
- return;
2096
- }
2097
-
2098
- if (e.key === "Tab") {
2099
- e.preventDefault();
2100
- var target = addProjectActiveIdx >= 0 && addProjectActiveIdx < items.length
2101
- ? items[addProjectActiveIdx]
2102
- : items.length > 0 ? items[0] : null;
2103
- if (target) {
2104
- var p = target.dataset.path + "/";
2105
- addProjectInput.value = p;
2106
- addProjectError.classList.add("hidden");
2107
- requestBrowseDir(p);
2108
- }
2109
- return;
2110
- }
2111
-
2112
- if (e.key === "Enter") {
2113
- e.preventDefault();
2114
- // If a suggestion is highlighted, pick it first
2115
- if (addProjectActiveIdx >= 0 && addProjectActiveIdx < items.length) {
2116
- var picked = items[addProjectActiveIdx].dataset.path + "/";
2117
- addProjectInput.value = picked;
2118
- addProjectError.classList.add("hidden");
2119
- requestBrowseDir(picked);
2120
- return;
2121
- }
2122
- // Otherwise submit
2123
- submitAddProject();
2124
- return;
2125
- }
2126
-
2127
- if (e.key === "Escape") {
2128
- e.preventDefault();
2129
- closeAddProjectModal();
2130
- return;
2131
- }
2132
- });
2133
-
2134
- function submitAddProject() {
2135
- var val = addProjectInput.value.replace(/\/+$/, "");
2136
- if (!val) return;
2137
- addProjectOk.disabled = true;
2138
- addProjectError.classList.add("hidden");
2139
- if (ws && ws.readyState === 1) {
2140
- ws.send(JSON.stringify({ type: "add_project", path: val }));
2141
- }
2142
- }
2143
-
2144
- addProjectOk.addEventListener("click", function () { submitAddProject(); });
2145
- addProjectCancel.addEventListener("click", function () { closeAddProjectModal(); });
2146
-
2147
- // Close on backdrop click
2148
- addProjectModal.querySelector(".confirm-backdrop").addEventListener("click", function () {
2149
- closeAddProjectModal();
2150
- });
2151
-
2152
- // --- Init ---
2153
- lucide.createIcons();
2154
- startVerbCycle();
2155
- startPixelAnim();
2156
- connect();
2157
- inputEl.focus();