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.
- package/README.md +31 -6
- package/bin/cli.js +7 -3
- package/lib/pages.js +3 -2
- package/lib/project.js +130 -39
- package/lib/public/app.js +381 -10
- package/lib/public/css/base.css +7 -0
- package/lib/public/css/filebrowser.css +149 -2
- package/lib/public/css/input.css +83 -10
- package/lib/public/css/menus.css +281 -1
- package/lib/public/css/messages.css +191 -0
- package/lib/public/css/sidebar.css +93 -1
- package/lib/public/index.html +90 -9
- package/lib/public/modules/filebrowser.js +33 -2
- package/lib/public/modules/input.js +98 -1
- package/lib/public/modules/notifications.js +19 -1
- package/lib/public/modules/qrcode.js +7 -1
- package/lib/public/modules/sidebar.js +233 -3
- package/lib/public/modules/terminal.js +484 -74
- package/lib/public/modules/tools.js +346 -2
- package/lib/public/sw.js +2 -5
- package/lib/push.js +16 -0
- package/lib/sdk-bridge.js +56 -6
- package/lib/server.js +4 -1
- package/lib/sessions.js +34 -0
- package/lib/terminal-manager.js +187 -0
- package/lib/terminal.js +3 -3
- package/lib/usage.js +90 -0
- package/package.json +1 -1
|
@@ -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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
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 +=
|
|
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 =
|
|
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
|
+
}
|