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
@@ -1,628 +0,0 @@
1
- import { escapeHtml, copyToClipboard } from './utils.js';
2
- import { iconHtml, refreshIcons } from './icons.js';
3
-
4
- var ctx;
5
-
6
- // --- Session search ---
7
- var searchQuery = "";
8
- var searchMatchIds = null; // null = no search, Set of matched session IDs
9
- var searchDebounce = null;
10
- var cachedSessions = [];
11
-
12
- // --- Session context menu ---
13
- var sessionCtxMenu = null;
14
- var sessionCtxSessionId = null;
15
-
16
- function closeSessionCtxMenu() {
17
- if (sessionCtxMenu) {
18
- sessionCtxMenu.remove();
19
- sessionCtxMenu = null;
20
- sessionCtxSessionId = null;
21
- }
22
- }
23
-
24
- function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid) {
25
- closeSessionCtxMenu();
26
- sessionCtxSessionId = sessionId;
27
-
28
- var menu = document.createElement("div");
29
- menu.className = "session-ctx-menu";
30
-
31
- var renameItem = document.createElement("button");
32
- renameItem.className = "session-ctx-item";
33
- renameItem.innerHTML = iconHtml("pencil") + " <span>Rename</span>";
34
- renameItem.addEventListener("click", function (e) {
35
- e.stopPropagation();
36
- closeSessionCtxMenu();
37
- startInlineRename(sessionId, title);
38
- });
39
- menu.appendChild(renameItem);
40
-
41
- if (cliSid) {
42
- var copyResumeItem = document.createElement("button");
43
- copyResumeItem.className = "session-ctx-item";
44
- copyResumeItem.innerHTML = iconHtml("copy") + " <span>Copy resume command</span>";
45
- copyResumeItem.addEventListener("click", function (e) {
46
- e.stopPropagation();
47
- copyToClipboard("claude --resume " + cliSid).then(function () {
48
- copyResumeItem.innerHTML = iconHtml("check") + " <span>Copied!</span>";
49
- refreshIcons();
50
- setTimeout(function () { closeSessionCtxMenu(); }, 800);
51
- });
52
- });
53
- menu.appendChild(copyResumeItem);
54
- }
55
-
56
- var deleteItem = document.createElement("button");
57
- deleteItem.className = "session-ctx-item session-ctx-delete";
58
- deleteItem.innerHTML = iconHtml("trash-2") + " <span>Delete</span>";
59
- deleteItem.addEventListener("click", function (e) {
60
- e.stopPropagation();
61
- closeSessionCtxMenu();
62
- ctx.showConfirm('Delete "' + (title || "New Session") + '"? This session and its history will be permanently removed.', function () {
63
- var ws = ctx.ws;
64
- if (ws && ctx.connected) {
65
- ws.send(JSON.stringify({ type: "delete_session", id: sessionId }));
66
- }
67
- });
68
- });
69
- menu.appendChild(deleteItem);
70
-
71
- anchorBtn.parentElement.appendChild(menu);
72
- sessionCtxMenu = menu;
73
- refreshIcons();
74
-
75
- // Position: align to right edge of parent, below the button
76
- requestAnimationFrame(function () {
77
- var rect = menu.getBoundingClientRect();
78
- var parentRect = menu.parentElement.getBoundingClientRect();
79
- // If menu overflows below the sidebar, flip up
80
- var sidebarRect = ctx.sessionListEl.getBoundingClientRect();
81
- if (rect.bottom > sidebarRect.bottom) {
82
- menu.style.top = "auto";
83
- menu.style.bottom = "100%";
84
- menu.style.marginBottom = "2px";
85
- }
86
- });
87
- }
88
-
89
- function startInlineRename(sessionId, currentTitle) {
90
- var el = ctx.sessionListEl.querySelector('.session-item[data-session-id="' + sessionId + '"]');
91
- if (!el) return;
92
- var textSpan = el.querySelector(".session-item-text");
93
- if (!textSpan) return;
94
-
95
- var input = document.createElement("input");
96
- input.type = "text";
97
- input.className = "session-rename-input";
98
- input.value = currentTitle || "New Session";
99
-
100
- var originalHtml = textSpan.innerHTML;
101
- textSpan.innerHTML = "";
102
- textSpan.appendChild(input);
103
- input.focus();
104
- input.select();
105
-
106
- function commitRename() {
107
- var newTitle = input.value.trim();
108
- if (newTitle && newTitle !== currentTitle && ctx.ws && ctx.connected) {
109
- ctx.ws.send(JSON.stringify({ type: "rename_session", id: sessionId, title: newTitle }));
110
- }
111
- // Restore text (server will send updated session_list)
112
- textSpan.innerHTML = originalHtml;
113
- if (newTitle && newTitle !== currentTitle) {
114
- textSpan.textContent = newTitle;
115
- }
116
- }
117
-
118
- input.addEventListener("keydown", function (e) {
119
- if (e.key === "Enter") { e.preventDefault(); commitRename(); }
120
- if (e.key === "Escape") { e.preventDefault(); textSpan.innerHTML = originalHtml; }
121
- });
122
- input.addEventListener("blur", commitRename);
123
- input.addEventListener("click", function (e) { e.stopPropagation(); });
124
- }
125
-
126
- function getDateGroup(ts) {
127
- var now = new Date();
128
- var d = new Date(ts);
129
- var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
130
- var yesterday = new Date(today.getTime() - 86400000);
131
- var weekAgo = new Date(today.getTime() - 7 * 86400000);
132
- if (d >= today) return "Today";
133
- if (d >= yesterday) return "Yesterday";
134
- if (d >= weekAgo) return "This Week";
135
- return "Older";
136
- }
137
-
138
- function highlightMatch(text, query) {
139
- if (!query) return escapeHtml(text);
140
- var lower = text.toLowerCase();
141
- var qLower = query.toLowerCase();
142
- var idx = lower.indexOf(qLower);
143
- if (idx === -1) return escapeHtml(text);
144
- var before = text.substring(0, idx);
145
- var match = text.substring(idx, idx + query.length);
146
- var after = text.substring(idx + query.length);
147
- return escapeHtml(before) + '<mark class="session-highlight">' + escapeHtml(match) + '</mark>' + escapeHtml(after);
148
- }
149
-
150
- function renderSessionItem(s) {
151
- var el = document.createElement("div");
152
- var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
153
- var dimmed = searchMatchIds !== null && !isMatch;
154
- el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "") + (dimmed ? " search-dimmed" : "");
155
- el.dataset.sessionId = s.id;
156
-
157
- var textSpan = document.createElement("span");
158
- textSpan.className = "session-item-text";
159
- var textHtml = "";
160
- if (s.isProcessing) {
161
- textHtml += '<span class="session-processing"></span>';
162
- }
163
- textHtml += highlightMatch(s.title || "New Session", searchQuery);
164
- textSpan.innerHTML = textHtml;
165
- el.appendChild(textSpan);
166
-
167
- var moreBtn = document.createElement("button");
168
- moreBtn.className = "session-more-btn";
169
- moreBtn.innerHTML = iconHtml("ellipsis");
170
- moreBtn.title = "More options";
171
- moreBtn.addEventListener("click", (function(id, title, cliSid, btn) {
172
- return function(e) {
173
- e.stopPropagation();
174
- showSessionCtxMenu(btn, id, title, cliSid);
175
- };
176
- })(s.id, s.title, s.cliSessionId, moreBtn));
177
- el.appendChild(moreBtn);
178
-
179
- el.addEventListener("click", (function (id) {
180
- return function () {
181
- if (ctx.ws && ctx.connected) {
182
- ctx.ws.send(JSON.stringify({ type: "switch_session", id: id }));
183
- closeSidebar();
184
- }
185
- };
186
- })(s.id));
187
-
188
- return el;
189
- }
190
-
191
- export function renderSessionList(sessions) {
192
- if (sessions) cachedSessions = sessions;
193
-
194
- ctx.sessionListEl.innerHTML = "";
195
-
196
- // Sort by lastActivity descending (most recent first)
197
- var sorted = cachedSessions.slice().sort(function (a, b) {
198
- return (b.lastActivity || 0) - (a.lastActivity || 0);
199
- });
200
-
201
- var currentGroup = "";
202
- for (var i = 0; i < sorted.length; i++) {
203
- var s = sorted[i];
204
- var group = getDateGroup(s.lastActivity || 0);
205
- if (group !== currentGroup) {
206
- currentGroup = group;
207
- var header = document.createElement("div");
208
- header.className = "session-group-header";
209
- header.textContent = group;
210
- ctx.sessionListEl.appendChild(header);
211
- }
212
- ctx.sessionListEl.appendChild(renderSessionItem(s));
213
- }
214
- refreshIcons();
215
- updatePageTitle();
216
- }
217
-
218
- export function handleSearchResults(msg) {
219
- if (msg.query !== searchQuery) return; // stale response
220
- var ids = new Set();
221
- for (var i = 0; i < msg.results.length; i++) {
222
- ids.add(msg.results[i].id);
223
- }
224
- searchMatchIds = ids;
225
- renderSessionList(null);
226
-
227
- // Build timeline for current session if it matches
228
- var activeEl = ctx.sessionListEl.querySelector(".session-item.active");
229
- if (activeEl) {
230
- var activeId = parseInt(activeEl.dataset.sessionId, 10);
231
- if (ids.has(activeId)) {
232
- buildSearchTimeline(searchQuery);
233
- } else {
234
- removeSearchTimeline();
235
- }
236
- }
237
- }
238
-
239
- export function updatePageTitle() {
240
- var sessionTitle = "";
241
- var activeItem = ctx.sessionListEl.querySelector(".session-item.active .session-item-text");
242
- if (activeItem) sessionTitle = activeItem.textContent;
243
- if (ctx.headerTitleEl) {
244
- ctx.headerTitleEl.textContent = sessionTitle || ctx.projectName || "Claude Relay";
245
- }
246
- if (ctx.projectName && sessionTitle) {
247
- document.title = sessionTitle + " - " + ctx.projectName;
248
- } else if (ctx.projectName) {
249
- document.title = ctx.projectName + " - Claude Relay";
250
- } else {
251
- document.title = "Claude Relay";
252
- }
253
- }
254
-
255
- export function openSidebar() {
256
- ctx.sidebar.classList.add("open");
257
- ctx.sidebarOverlay.classList.add("visible");
258
- }
259
-
260
- export function closeSidebar() {
261
- ctx.sidebar.classList.remove("open");
262
- ctx.sidebarOverlay.classList.remove("visible");
263
- }
264
-
265
- export function initSidebar(_ctx) {
266
- ctx = _ctx;
267
-
268
- document.addEventListener("click", function () { closeSessionCtxMenu(); });
269
-
270
- ctx.hamburgerBtn.addEventListener("click", function () {
271
- ctx.sidebar.classList.contains("open") ? closeSidebar() : openSidebar();
272
- });
273
-
274
- ctx.sidebarOverlay.addEventListener("click", closeSidebar);
275
-
276
- // --- Desktop sidebar collapse/expand ---
277
- function toggleSidebarCollapse() {
278
- var layout = ctx.$("layout");
279
- var collapsed = layout.classList.toggle("sidebar-collapsed");
280
- try { localStorage.setItem("sidebar-collapsed", collapsed ? "1" : ""); } catch (e) {}
281
- }
282
-
283
- ctx.sidebarToggleBtn.addEventListener("click", toggleSidebarCollapse);
284
- ctx.sidebarExpandBtn.addEventListener("click", toggleSidebarCollapse);
285
-
286
- // Restore collapsed state from localStorage
287
- try {
288
- if (localStorage.getItem("sidebar-collapsed") === "1") {
289
- ctx.$("layout").classList.add("sidebar-collapsed");
290
- }
291
- } catch (e) {}
292
-
293
- ctx.newSessionBtn.addEventListener("click", function () {
294
- if (ctx.ws && ctx.connected) {
295
- ctx.ws.send(JSON.stringify({ type: "new_session" }));
296
- closeSidebar();
297
- }
298
- });
299
-
300
- // --- Session search ---
301
- var searchBtn = ctx.$("search-session-btn");
302
- var searchBox = ctx.$("session-search");
303
- var searchInput = ctx.$("session-search-input");
304
- var searchClear = ctx.$("session-search-clear");
305
-
306
- function openSearch() {
307
- searchBox.classList.remove("hidden");
308
- searchBtn.classList.add("active");
309
- searchInput.value = "";
310
- searchQuery = "";
311
- setTimeout(function () { searchInput.focus(); }, 50);
312
- }
313
-
314
- function closeSearch() {
315
- searchBox.classList.add("hidden");
316
- searchBtn.classList.remove("active");
317
- searchInput.value = "";
318
- searchQuery = "";
319
- searchMatchIds = null;
320
- if (searchDebounce) { clearTimeout(searchDebounce); searchDebounce = null; }
321
- removeSearchTimeline();
322
- renderSessionList(null);
323
- }
324
-
325
- searchBtn.addEventListener("click", function () {
326
- if (searchBox.classList.contains("hidden")) {
327
- openSearch();
328
- } else {
329
- closeSearch();
330
- }
331
- });
332
-
333
- if (searchClear) {
334
- searchClear.addEventListener("click", function () {
335
- closeSearch();
336
- });
337
- }
338
-
339
- searchInput.addEventListener("input", function () {
340
- searchQuery = searchInput.value.trim();
341
- if (searchDebounce) clearTimeout(searchDebounce);
342
- if (!searchQuery) {
343
- searchMatchIds = null;
344
- removeSearchTimeline();
345
- renderSessionList(null);
346
- return;
347
- }
348
- searchDebounce = setTimeout(function () {
349
- if (ctx.ws && ctx.connected) {
350
- ctx.ws.send(JSON.stringify({ type: "search_sessions", query: searchQuery }));
351
- }
352
- }, 200);
353
- });
354
-
355
- searchInput.addEventListener("keydown", function (e) {
356
- if (e.key === "Escape") {
357
- e.preventDefault();
358
- closeSearch();
359
- }
360
- });
361
-
362
- // --- Resume session picker ---
363
- var resumeModal = ctx.$("resume-modal");
364
- var resumeCancel = ctx.$("resume-cancel");
365
- var pickerLoading = ctx.$("resume-picker-loading");
366
- var pickerEmpty = ctx.$("resume-picker-empty");
367
- var pickerList = ctx.$("resume-picker-list");
368
-
369
- function openResumeModal() {
370
- resumeModal.classList.remove("hidden");
371
- pickerLoading.classList.remove("hidden");
372
- pickerEmpty.classList.add("hidden");
373
- pickerList.classList.add("hidden");
374
- pickerList.innerHTML = "";
375
- if (ctx.ws && ctx.connected) {
376
- ctx.ws.send(JSON.stringify({ type: "list_cli_sessions" }));
377
- }
378
- }
379
-
380
- function closeResumeModal() {
381
- resumeModal.classList.add("hidden");
382
- }
383
-
384
- ctx.resumeSessionBtn.addEventListener("click", openResumeModal);
385
- resumeCancel.addEventListener("click", closeResumeModal);
386
- resumeModal.querySelector(".confirm-backdrop").addEventListener("click", closeResumeModal);
387
-
388
- // --- File browser panel switch ---
389
- var fileBrowserBtn = ctx.$("file-browser-btn");
390
- var sessionsPanel = ctx.$("sidebar-panel-sessions");
391
- var filesPanel = ctx.$("sidebar-panel-files");
392
- var sessionsHeaderContent = ctx.$("sessions-header-content");
393
- var filesHeaderContent = ctx.$("files-header-content");
394
- var filePanelClose = ctx.$("file-panel-close");
395
-
396
- function showFilesPanel() {
397
- sessionsPanel.classList.add("hidden");
398
- filesPanel.classList.remove("hidden");
399
- if (sessionsHeaderContent) sessionsHeaderContent.classList.add("hidden");
400
- if (filesHeaderContent) filesHeaderContent.classList.remove("hidden");
401
- if (ctx.onFilesTabOpen) ctx.onFilesTabOpen();
402
- }
403
-
404
- function showSessionsPanel() {
405
- filesPanel.classList.add("hidden");
406
- sessionsPanel.classList.remove("hidden");
407
- if (filesHeaderContent) filesHeaderContent.classList.add("hidden");
408
- if (sessionsHeaderContent) sessionsHeaderContent.classList.remove("hidden");
409
- }
410
-
411
- if (fileBrowserBtn) {
412
- fileBrowserBtn.addEventListener("click", showFilesPanel);
413
- }
414
- if (filePanelClose) {
415
- filePanelClose.addEventListener("click", showSessionsPanel);
416
- }
417
- }
418
-
419
- // --- CLI session picker ---
420
- function relativeTime(isoString) {
421
- if (!isoString) return "";
422
- var ms = Date.now() - new Date(isoString).getTime();
423
- var sec = Math.floor(ms / 1000);
424
- if (sec < 60) return "just now";
425
- var min = Math.floor(sec / 60);
426
- if (min < 60) return min + "m ago";
427
- var hr = Math.floor(min / 60);
428
- if (hr < 24) return hr + "h ago";
429
- var days = Math.floor(hr / 24);
430
- if (days < 30) return days + "d ago";
431
- return new Date(isoString).toLocaleDateString();
432
- }
433
-
434
- export function populateCliSessionList(sessions) {
435
- var pickerLoading = ctx.$("resume-picker-loading");
436
- var pickerEmpty = ctx.$("resume-picker-empty");
437
- var pickerList = ctx.$("resume-picker-list");
438
- if (!pickerLoading || !pickerList) return;
439
-
440
- pickerLoading.classList.add("hidden");
441
-
442
- if (!sessions || sessions.length === 0) {
443
- pickerEmpty.classList.remove("hidden");
444
- pickerList.classList.add("hidden");
445
- return;
446
- }
447
-
448
- pickerEmpty.classList.add("hidden");
449
- pickerList.classList.remove("hidden");
450
- pickerList.innerHTML = "";
451
-
452
- for (var i = 0; i < sessions.length; i++) {
453
- var s = sessions[i];
454
- var item = document.createElement("div");
455
- item.className = "cli-session-item";
456
-
457
- var title = document.createElement("div");
458
- title.className = "cli-session-title";
459
- title.textContent = s.firstPrompt || "Untitled session";
460
- item.appendChild(title);
461
-
462
- var meta = document.createElement("div");
463
- meta.className = "cli-session-meta";
464
- if (s.lastActivity) {
465
- var time = document.createElement("span");
466
- time.textContent = relativeTime(s.lastActivity);
467
- meta.appendChild(time);
468
- }
469
- if (s.model) {
470
- var model = document.createElement("span");
471
- model.className = "badge";
472
- model.textContent = s.model;
473
- meta.appendChild(model);
474
- }
475
- if (s.gitBranch) {
476
- var branch = document.createElement("span");
477
- branch.className = "badge";
478
- branch.textContent = s.gitBranch;
479
- meta.appendChild(branch);
480
- }
481
- item.appendChild(meta);
482
-
483
- (function (sessionId) {
484
- item.addEventListener("click", function () {
485
- if (ctx.ws && ctx.connected) {
486
- ctx.ws.send(JSON.stringify({ type: "resume_session", cliSessionId: sessionId }));
487
- }
488
- var modal = ctx.$("resume-modal");
489
- if (modal) modal.classList.add("hidden");
490
- closeSidebar();
491
- });
492
- })(s.sessionId);
493
-
494
- pickerList.appendChild(item);
495
- }
496
- }
497
-
498
- // --- Search hit timeline (right-side markers) ---
499
- var searchTimelineScrollHandler = null;
500
- var activeSearchQuery = ""; // query active in the timeline
501
-
502
- export function getActiveSearchQuery() {
503
- return searchQuery;
504
- }
505
-
506
- export function buildSearchTimeline(query) {
507
- removeSearchTimeline();
508
- if (!query) return;
509
- activeSearchQuery = query;
510
-
511
- var q = query.toLowerCase();
512
- var messagesEl = ctx.messagesEl;
513
-
514
- // Collect all message elements that contain the query
515
- var allMsgs = messagesEl.querySelectorAll(".msg-user, .msg-assistant");
516
- var hits = [];
517
- for (var i = 0; i < allMsgs.length; i++) {
518
- var msgEl = allMsgs[i];
519
- var textEl = msgEl.querySelector(".bubble") || msgEl.querySelector(".md-content");
520
- if (!textEl) continue;
521
- var text = textEl.textContent || "";
522
- if (text.toLowerCase().indexOf(q) === -1) continue;
523
-
524
- // Extract a snippet around the match
525
- var idx = text.toLowerCase().indexOf(q);
526
- var start = Math.max(0, idx - 10);
527
- var end = Math.min(text.length, idx + query.length + 10);
528
- var snippet = (start > 0 ? "\u2026" : "") + text.substring(start, end) + (end < text.length ? "\u2026" : "");
529
- hits.push({ el: msgEl, snippet: snippet });
530
- }
531
-
532
- if (hits.length === 0) return;
533
-
534
- var timeline = document.createElement("div");
535
- timeline.className = "search-timeline";
536
- timeline.id = "search-timeline";
537
-
538
- var track = document.createElement("div");
539
- track.className = "rewind-timeline-track";
540
-
541
- var viewport = document.createElement("div");
542
- viewport.className = "rewind-timeline-viewport";
543
- track.appendChild(viewport);
544
-
545
- for (var i = 0; i < hits.length; i++) {
546
- var hit = hits[i];
547
- var pct = hits.length === 1 ? 50 : 6 + (i / (hits.length - 1)) * 88;
548
-
549
- var snippetText = hit.snippet;
550
- if (snippetText.length > 24) snippetText = snippetText.substring(0, 24) + "\u2026";
551
-
552
- var marker = document.createElement("div");
553
- marker.className = "rewind-timeline-marker search-hit-marker";
554
- marker.innerHTML = iconHtml("search") + '<span class="marker-text">' + escapeHtml(snippetText) + '</span>';
555
- marker.style.top = pct + "%";
556
- marker.dataset.offsetTop = hit.el.offsetTop;
557
-
558
- (function(targetEl, markerEl) {
559
- markerEl.addEventListener("click", function() {
560
- targetEl.scrollIntoView({ behavior: "smooth", block: "center" });
561
- targetEl.classList.remove("search-blink");
562
- void targetEl.offsetWidth; // force reflow
563
- targetEl.classList.add("search-blink");
564
- });
565
- })(hit.el, marker);
566
-
567
- track.appendChild(marker);
568
- }
569
-
570
- timeline.appendChild(track);
571
-
572
- // Position to align with messages area
573
- var appEl = ctx.$("app");
574
- var headerEl = ctx.$("header");
575
- var inputAreaEl = ctx.$("input-area");
576
- var appRect = appEl.getBoundingClientRect();
577
- var headerRect = headerEl.getBoundingClientRect();
578
- var inputRect = inputAreaEl.getBoundingClientRect();
579
-
580
- timeline.style.top = (headerRect.bottom - appRect.top + 4) + "px";
581
- timeline.style.bottom = (appRect.bottom - inputRect.top + 4) + "px";
582
-
583
- appEl.appendChild(timeline);
584
- refreshIcons();
585
-
586
- searchTimelineScrollHandler = function() { updateSearchTimelineViewport(track, viewport); };
587
- messagesEl.addEventListener("scroll", searchTimelineScrollHandler);
588
- updateSearchTimelineViewport(track, viewport);
589
- }
590
-
591
- function updateSearchTimelineViewport(track, viewport) {
592
- if (!track) return;
593
- var messagesEl = ctx.messagesEl;
594
- var scrollH = messagesEl.scrollHeight;
595
- var viewH = messagesEl.clientHeight;
596
- if (scrollH <= viewH) {
597
- viewport.style.top = "0";
598
- viewport.style.height = "100%";
599
- } else {
600
- var viewTop = messagesEl.scrollTop / scrollH;
601
- var viewBot = (messagesEl.scrollTop + viewH) / scrollH;
602
- viewport.style.top = (viewTop * 100) + "%";
603
- viewport.style.height = ((viewBot - viewTop) * 100) + "%";
604
- }
605
-
606
- var markers = track.querySelectorAll(".search-hit-marker");
607
- var vTop = messagesEl.scrollTop;
608
- var vBot = vTop + viewH;
609
-
610
- for (var i = 0; i < markers.length; i++) {
611
- var msgTop = parseInt(markers[i].dataset.offsetTop, 10);
612
- if (msgTop >= vTop && msgTop <= vBot) {
613
- markers[i].classList.add("in-view");
614
- } else {
615
- markers[i].classList.remove("in-view");
616
- }
617
- }
618
- }
619
-
620
- export function removeSearchTimeline() {
621
- var existing = document.getElementById("search-timeline");
622
- if (existing) existing.remove();
623
- if (searchTimelineScrollHandler && ctx.messagesEl) {
624
- ctx.messagesEl.removeEventListener("scroll", searchTimelineScrollHandler);
625
- searchTimelineScrollHandler = null;
626
- }
627
- activeSearchQuery = "";
628
- }
@@ -1,3 +0,0 @@
1
- // Centralized state — will be populated in Phase 2
2
- // For now, modules that need state receive it via init()
3
- export const state = {};