claude-relay 2.1.2 → 2.2.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.
@@ -137,6 +137,7 @@ export function initNotifications(_ctx) {
137
137
  var footerBtn = $("sidebar-footer-btn");
138
138
  var footerMenu = $("sidebar-footer-menu");
139
139
  var footerUpdateCheck = $("footer-update-check");
140
+ var footerUsage = $("footer-usage");
140
141
  if (!footerBtn || !footerMenu) return;
141
142
 
142
143
  footerBtn.addEventListener("click", function (e) {
@@ -150,6 +151,14 @@ export function initNotifications(_ctx) {
150
151
  }
151
152
  });
152
153
 
154
+ if (footerUsage && ctx.toggleUsagePanel) {
155
+ footerUsage.addEventListener("click", function (e) {
156
+ e.stopPropagation();
157
+ footerMenu.classList.add("hidden");
158
+ ctx.toggleUsagePanel();
159
+ });
160
+ }
161
+
153
162
  function setUpdateIcon(name, spin) {
154
163
  var el = footerUpdateCheck.querySelector(".lucide, [data-lucide]");
155
164
  if (!el) return;
@@ -233,7 +242,16 @@ export function initNotifications(_ctx) {
233
242
  tooltipEl.textContent = target.dataset.tip;
234
243
  var rect = target.getBoundingClientRect();
235
244
  tooltipEl.style.top = (rect.bottom + 8) + "px";
236
- tooltipEl.style.left = (rect.left + rect.width / 2) + "px";
245
+ tooltipEl.style.left = "";
246
+ tooltipEl.style.right = "";
247
+ tooltipEl.style.transform = "";
248
+ var centerX = rect.left + rect.width / 2;
249
+ if (centerX + 60 > window.innerWidth) {
250
+ tooltipEl.style.right = "8px";
251
+ } else {
252
+ tooltipEl.style.left = centerX + "px";
253
+ tooltipEl.style.transform = "translateX(-50%)";
254
+ }
237
255
  tooltipEl.classList.add("visible");
238
256
  clearTimeout(tooltipTimer);
239
257
  tooltipTimer = setTimeout(function () {
@@ -11,7 +11,13 @@ export function initQrCode() {
11
11
  e.stopPropagation();
12
12
  var url = window.location.href;
13
13
 
14
- // generate QR
14
+ // Use Web Share API if available
15
+ if (navigator.share) {
16
+ navigator.share({ title: document.title || "Claude Relay", url: url }).catch(function () {});
17
+ return;
18
+ }
19
+
20
+ // Fallback: show QR overlay
15
21
  var qr = qrcode(0, "M");
16
22
  qr.addData(url);
17
23
  qr.make();
@@ -3,6 +3,12 @@ import { iconHtml, refreshIcons } from './icons.js';
3
3
 
4
4
  var ctx;
5
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
+
6
12
  // --- Session context menu ---
7
13
  var sessionCtxMenu = null;
8
14
  var sessionCtxSessionId = null;
@@ -129,9 +135,23 @@ function getDateGroup(ts) {
129
135
  return "Older";
130
136
  }
131
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
+
132
150
  function renderSessionItem(s) {
133
151
  var el = document.createElement("div");
134
- el.className = "session-item" + (s.active ? " active" : "");
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" : "");
135
155
  el.dataset.sessionId = s.id;
136
156
 
137
157
  var textSpan = document.createElement("span");
@@ -140,7 +160,7 @@ function renderSessionItem(s) {
140
160
  if (s.isProcessing) {
141
161
  textHtml += '<span class="session-processing"></span>';
142
162
  }
143
- textHtml += escapeHtml(s.title || "New Session");
163
+ textHtml += highlightMatch(s.title || "New Session", searchQuery);
144
164
  textSpan.innerHTML = textHtml;
145
165
  el.appendChild(textSpan);
146
166
 
@@ -169,10 +189,12 @@ function renderSessionItem(s) {
169
189
  }
170
190
 
171
191
  export function renderSessionList(sessions) {
192
+ if (sessions) cachedSessions = sessions;
193
+
172
194
  ctx.sessionListEl.innerHTML = "";
173
195
 
174
196
  // Sort by lastActivity descending (most recent first)
175
- var sorted = sessions.slice().sort(function (a, b) {
197
+ var sorted = cachedSessions.slice().sort(function (a, b) {
176
198
  return (b.lastActivity || 0) - (a.lastActivity || 0);
177
199
  });
178
200
 
@@ -193,6 +215,27 @@ export function renderSessionList(sessions) {
193
215
  updatePageTitle();
194
216
  }
195
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
+
196
239
  export function updatePageTitle() {
197
240
  var sessionTitle = "";
198
241
  var activeItem = ctx.sessionListEl.querySelector(".session-item.active .session-item-text");
@@ -251,6 +294,61 @@ export function initSidebar(_ctx) {
251
294
  }
252
295
  });
253
296
 
297
+ // --- Session search ---
298
+ var searchBtn = ctx.$("search-session-btn");
299
+ var searchBox = ctx.$("session-search");
300
+ var searchInput = ctx.$("session-search-input");
301
+
302
+ function openSearch() {
303
+ searchBox.classList.remove("hidden");
304
+ searchBtn.classList.add("active");
305
+ searchInput.value = "";
306
+ searchQuery = "";
307
+ setTimeout(function () { searchInput.focus(); }, 50);
308
+ }
309
+
310
+ function closeSearch() {
311
+ searchBox.classList.add("hidden");
312
+ searchBtn.classList.remove("active");
313
+ searchInput.value = "";
314
+ searchQuery = "";
315
+ searchMatchIds = null;
316
+ if (searchDebounce) { clearTimeout(searchDebounce); searchDebounce = null; }
317
+ removeSearchTimeline();
318
+ renderSessionList(null);
319
+ }
320
+
321
+ searchBtn.addEventListener("click", function () {
322
+ if (searchBox.classList.contains("hidden")) {
323
+ openSearch();
324
+ } else {
325
+ closeSearch();
326
+ }
327
+ });
328
+
329
+ searchInput.addEventListener("input", function () {
330
+ searchQuery = searchInput.value.trim();
331
+ if (searchDebounce) clearTimeout(searchDebounce);
332
+ if (!searchQuery) {
333
+ searchMatchIds = null;
334
+ removeSearchTimeline();
335
+ renderSessionList(null);
336
+ return;
337
+ }
338
+ searchDebounce = setTimeout(function () {
339
+ if (ctx.ws && ctx.connected) {
340
+ ctx.ws.send(JSON.stringify({ type: "search_sessions", query: searchQuery }));
341
+ }
342
+ }, 200);
343
+ });
344
+
345
+ searchInput.addEventListener("keydown", function (e) {
346
+ if (e.key === "Escape") {
347
+ e.preventDefault();
348
+ closeSearch();
349
+ }
350
+ });
351
+
254
352
  // --- Resume session modal ---
255
353
  var resumeModal = ctx.$("resume-modal");
256
354
  var resumeInput = ctx.$("resume-session-input");
@@ -318,3 +416,135 @@ export function initSidebar(_ctx) {
318
416
  filePanelBack.addEventListener("click", showSessionsPanel);
319
417
  }
320
418
  }
419
+
420
+ // --- Search hit timeline (right-side markers) ---
421
+ var searchTimelineScrollHandler = null;
422
+ var activeSearchQuery = ""; // query active in the timeline
423
+
424
+ export function getActiveSearchQuery() {
425
+ return searchQuery;
426
+ }
427
+
428
+ export function buildSearchTimeline(query) {
429
+ removeSearchTimeline();
430
+ if (!query) return;
431
+ activeSearchQuery = query;
432
+
433
+ var q = query.toLowerCase();
434
+ var messagesEl = ctx.messagesEl;
435
+
436
+ // Collect all message elements that contain the query
437
+ var allMsgs = messagesEl.querySelectorAll(".msg-user, .msg-assistant");
438
+ var hits = [];
439
+ for (var i = 0; i < allMsgs.length; i++) {
440
+ var msgEl = allMsgs[i];
441
+ var textEl = msgEl.querySelector(".bubble") || msgEl.querySelector(".md-content");
442
+ if (!textEl) continue;
443
+ var text = textEl.textContent || "";
444
+ if (text.toLowerCase().indexOf(q) === -1) continue;
445
+
446
+ // Extract a snippet around the match
447
+ var idx = text.toLowerCase().indexOf(q);
448
+ var start = Math.max(0, idx - 10);
449
+ var end = Math.min(text.length, idx + query.length + 10);
450
+ var snippet = (start > 0 ? "\u2026" : "") + text.substring(start, end) + (end < text.length ? "\u2026" : "");
451
+ hits.push({ el: msgEl, snippet: snippet });
452
+ }
453
+
454
+ if (hits.length === 0) return;
455
+
456
+ var timeline = document.createElement("div");
457
+ timeline.className = "search-timeline";
458
+ timeline.id = "search-timeline";
459
+
460
+ var track = document.createElement("div");
461
+ track.className = "rewind-timeline-track";
462
+
463
+ var viewport = document.createElement("div");
464
+ viewport.className = "rewind-timeline-viewport";
465
+ track.appendChild(viewport);
466
+
467
+ for (var i = 0; i < hits.length; i++) {
468
+ var hit = hits[i];
469
+ var pct = hits.length === 1 ? 50 : 6 + (i / (hits.length - 1)) * 88;
470
+
471
+ var snippetText = hit.snippet;
472
+ if (snippetText.length > 24) snippetText = snippetText.substring(0, 24) + "\u2026";
473
+
474
+ var marker = document.createElement("div");
475
+ marker.className = "rewind-timeline-marker search-hit-marker";
476
+ marker.innerHTML = iconHtml("search") + '<span class="marker-text">' + escapeHtml(snippetText) + '</span>';
477
+ marker.style.top = pct + "%";
478
+ marker.dataset.offsetTop = hit.el.offsetTop;
479
+
480
+ (function(targetEl, markerEl) {
481
+ markerEl.addEventListener("click", function() {
482
+ targetEl.scrollIntoView({ behavior: "smooth", block: "center" });
483
+ targetEl.classList.remove("search-blink");
484
+ void targetEl.offsetWidth; // force reflow
485
+ targetEl.classList.add("search-blink");
486
+ });
487
+ })(hit.el, marker);
488
+
489
+ track.appendChild(marker);
490
+ }
491
+
492
+ timeline.appendChild(track);
493
+
494
+ // Position to align with messages area
495
+ var appEl = ctx.$("app");
496
+ var headerEl = ctx.$("header");
497
+ var inputAreaEl = ctx.$("input-area");
498
+ var appRect = appEl.getBoundingClientRect();
499
+ var headerRect = headerEl.getBoundingClientRect();
500
+ var inputRect = inputAreaEl.getBoundingClientRect();
501
+
502
+ timeline.style.top = (headerRect.bottom - appRect.top + 4) + "px";
503
+ timeline.style.bottom = (appRect.bottom - inputRect.top + 4) + "px";
504
+
505
+ appEl.appendChild(timeline);
506
+ refreshIcons();
507
+
508
+ searchTimelineScrollHandler = function() { updateSearchTimelineViewport(track, viewport); };
509
+ messagesEl.addEventListener("scroll", searchTimelineScrollHandler);
510
+ updateSearchTimelineViewport(track, viewport);
511
+ }
512
+
513
+ function updateSearchTimelineViewport(track, viewport) {
514
+ if (!track) return;
515
+ var messagesEl = ctx.messagesEl;
516
+ var scrollH = messagesEl.scrollHeight;
517
+ var viewH = messagesEl.clientHeight;
518
+ if (scrollH <= viewH) {
519
+ viewport.style.top = "0";
520
+ viewport.style.height = "100%";
521
+ } else {
522
+ var viewTop = messagesEl.scrollTop / scrollH;
523
+ var viewBot = (messagesEl.scrollTop + viewH) / scrollH;
524
+ viewport.style.top = (viewTop * 100) + "%";
525
+ viewport.style.height = ((viewBot - viewTop) * 100) + "%";
526
+ }
527
+
528
+ var markers = track.querySelectorAll(".search-hit-marker");
529
+ var vTop = messagesEl.scrollTop;
530
+ var vBot = vTop + viewH;
531
+
532
+ for (var i = 0; i < markers.length; i++) {
533
+ var msgTop = parseInt(markers[i].dataset.offsetTop, 10);
534
+ if (msgTop >= vTop && msgTop <= vBot) {
535
+ markers[i].classList.add("in-view");
536
+ } else {
537
+ markers[i].classList.remove("in-view");
538
+ }
539
+ }
540
+ }
541
+
542
+ export function removeSearchTimeline() {
543
+ var existing = document.getElementById("search-timeline");
544
+ if (existing) existing.remove();
545
+ if (searchTimelineScrollHandler && ctx.messagesEl) {
546
+ ctx.messagesEl.removeEventListener("scroll", searchTimelineScrollHandler);
547
+ searchTimelineScrollHandler = null;
548
+ }
549
+ activeSearchQuery = "";
550
+ }