clay-server 2.11.0 → 2.12.0-beta.1
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/bin/cli.js +16 -4
- package/lib/daemon.js +167 -0
- package/lib/project.js +83 -1
- package/lib/public/app.js +567 -20
- package/lib/public/css/icon-strip.css +308 -5
- package/lib/public/css/menus.css +1 -16
- package/lib/public/css/messages.css +7 -0
- package/lib/public/css/session-search.css +150 -0
- package/lib/public/css/sidebar.css +30 -0
- package/lib/public/css/tooltip.css +20 -0
- package/lib/public/index.html +2 -1
- package/lib/public/modules/notifications.js +1 -58
- package/lib/public/modules/session-search.js +440 -0
- package/lib/public/modules/sidebar.js +576 -148
- package/lib/public/modules/tooltip.js +123 -0
- package/lib/public/style.css +2 -0
- package/lib/server.js +46 -3
- package/lib/sessions.js +37 -0
- package/lib/worktree.js +134 -0
- package/package.json +1 -1
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// --- In-session search (Cmd+F / Ctrl+F) ---
|
|
2
|
+
import { escapeHtml } from './utils.js';
|
|
3
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
4
|
+
|
|
5
|
+
var ctx = null;
|
|
6
|
+
var searchBarEl = null;
|
|
7
|
+
var searchInputEl = null;
|
|
8
|
+
var matchCountEl = null;
|
|
9
|
+
var currentQuery = "";
|
|
10
|
+
var matches = []; // DOM highlight marks (for loaded messages)
|
|
11
|
+
var currentMatchIndex = -1;
|
|
12
|
+
var highlightClass = "session-search-highlight";
|
|
13
|
+
var activeHighlightClass = "session-search-highlight-active";
|
|
14
|
+
|
|
15
|
+
// Timeline state
|
|
16
|
+
var timelineEl = null;
|
|
17
|
+
var timelineScrollHandler = null;
|
|
18
|
+
var serverHits = []; // full history hits from server
|
|
19
|
+
var serverTotal = 0;
|
|
20
|
+
var pendingScrollTarget = null;
|
|
21
|
+
|
|
22
|
+
function initSessionSearch(context) {
|
|
23
|
+
ctx = context;
|
|
24
|
+
createSearchBar();
|
|
25
|
+
document.addEventListener("keydown", function (e) {
|
|
26
|
+
var isMod = e.metaKey || e.ctrlKey;
|
|
27
|
+
if (isMod && e.key === "f") {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
openSearch();
|
|
30
|
+
}
|
|
31
|
+
if (e.key === "Escape" && isSearchOpen()) {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
closeSearch();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createSearchBar() {
|
|
39
|
+
searchBarEl = document.createElement("div");
|
|
40
|
+
searchBarEl.id = "session-search-bar";
|
|
41
|
+
searchBarEl.className = "session-search-bar hidden";
|
|
42
|
+
searchBarEl.innerHTML =
|
|
43
|
+
'<div class="session-search-inner">' +
|
|
44
|
+
'<input type="text" id="find-in-session-input" placeholder="Search in this session..." autocomplete="off" spellcheck="false">' +
|
|
45
|
+
'<span class="session-search-count" id="find-in-session-count"></span>' +
|
|
46
|
+
'<button class="session-search-btn" id="find-in-session-prev" title="Previous (Shift+Enter)" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></button>' +
|
|
47
|
+
'<button class="session-search-btn" id="find-in-session-next" title="Next (Enter)" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button>' +
|
|
48
|
+
'<button class="session-search-btn" id="find-in-session-close" title="Close (Esc)" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>' +
|
|
49
|
+
'</div>';
|
|
50
|
+
|
|
51
|
+
// Insert at top of #app
|
|
52
|
+
var appEl = ctx.messagesEl.parentElement;
|
|
53
|
+
appEl.insertBefore(searchBarEl, appEl.firstChild);
|
|
54
|
+
|
|
55
|
+
searchInputEl = document.getElementById("find-in-session-input");
|
|
56
|
+
matchCountEl = document.getElementById("find-in-session-count");
|
|
57
|
+
|
|
58
|
+
var debounceTimer = null;
|
|
59
|
+
searchInputEl.addEventListener("input", function () {
|
|
60
|
+
clearTimeout(debounceTimer);
|
|
61
|
+
debounceTimer = setTimeout(function () {
|
|
62
|
+
performSearch(searchInputEl.value);
|
|
63
|
+
}, 250);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
searchInputEl.addEventListener("keydown", function (e) {
|
|
67
|
+
if (e.key === "Enter") {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
if (e.shiftKey) {
|
|
70
|
+
goToPrevMatch();
|
|
71
|
+
} else {
|
|
72
|
+
goToNextMatch();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (e.key === "Escape") {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
closeSearch();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
document.getElementById("find-in-session-prev").addEventListener("click", function () {
|
|
82
|
+
goToPrevMatch();
|
|
83
|
+
});
|
|
84
|
+
document.getElementById("find-in-session-next").addEventListener("click", function () {
|
|
85
|
+
goToNextMatch();
|
|
86
|
+
});
|
|
87
|
+
document.getElementById("find-in-session-close").addEventListener("click", function () {
|
|
88
|
+
closeSearch();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isSearchOpen() {
|
|
93
|
+
return searchBarEl && !searchBarEl.classList.contains("hidden");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function openSearch() {
|
|
97
|
+
if (!searchBarEl) return;
|
|
98
|
+
searchBarEl.classList.remove("hidden");
|
|
99
|
+
var btn = document.getElementById("find-in-session-btn");
|
|
100
|
+
if (btn) btn.classList.add("active");
|
|
101
|
+
searchInputEl.focus();
|
|
102
|
+
searchInputEl.select();
|
|
103
|
+
if (searchInputEl.value) {
|
|
104
|
+
performSearch(searchInputEl.value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function closeSearch() {
|
|
109
|
+
if (!searchBarEl) return;
|
|
110
|
+
searchBarEl.classList.add("hidden");
|
|
111
|
+
var btn = document.getElementById("find-in-session-btn");
|
|
112
|
+
if (btn) btn.classList.remove("active");
|
|
113
|
+
clearHighlights();
|
|
114
|
+
removeTimeline();
|
|
115
|
+
currentQuery = "";
|
|
116
|
+
matches = [];
|
|
117
|
+
currentMatchIndex = -1;
|
|
118
|
+
serverHits = [];
|
|
119
|
+
serverTotal = 0;
|
|
120
|
+
matchCountEl.textContent = "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toggleSearch() {
|
|
124
|
+
if (isSearchOpen()) {
|
|
125
|
+
closeSearch();
|
|
126
|
+
} else {
|
|
127
|
+
openSearch();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function performSearch(query) {
|
|
132
|
+
clearHighlights();
|
|
133
|
+
removeTimeline();
|
|
134
|
+
matches = [];
|
|
135
|
+
currentMatchIndex = -1;
|
|
136
|
+
serverHits = [];
|
|
137
|
+
serverTotal = 0;
|
|
138
|
+
currentQuery = query.trim();
|
|
139
|
+
|
|
140
|
+
if (!currentQuery) {
|
|
141
|
+
matchCountEl.textContent = "";
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Highlight in currently loaded DOM
|
|
146
|
+
highlightLoadedMessages();
|
|
147
|
+
|
|
148
|
+
// Request full-history search from server
|
|
149
|
+
if (ctx.ws && ctx.ws.readyState === 1) {
|
|
150
|
+
ctx.ws.send(JSON.stringify({ type: "search_session_content", query: currentQuery, source: "find_in_session" }));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function highlightLoadedMessages() {
|
|
155
|
+
var messagesEl = ctx.messagesEl;
|
|
156
|
+
var msgEls = messagesEl.querySelectorAll(".msg-user, .msg-assistant");
|
|
157
|
+
var queryLower = currentQuery.toLowerCase();
|
|
158
|
+
|
|
159
|
+
for (var i = 0; i < msgEls.length; i++) {
|
|
160
|
+
var contentEl = msgEls[i].querySelector(".bubble") || msgEls[i].querySelector(".md-content");
|
|
161
|
+
if (!contentEl) continue;
|
|
162
|
+
highlightInElement(contentEl, queryLower);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
matches = Array.from(messagesEl.querySelectorAll("." + highlightClass));
|
|
166
|
+
|
|
167
|
+
if (matches.length > 0) {
|
|
168
|
+
currentMatchIndex = 0;
|
|
169
|
+
setActiveMatch(0);
|
|
170
|
+
matchCountEl.textContent = "1 / " + matches.length;
|
|
171
|
+
} else {
|
|
172
|
+
matchCountEl.textContent = "Searching...";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Handle server response with full-history search results
|
|
177
|
+
function handleFindInSessionResults(msg) {
|
|
178
|
+
if (!isSearchOpen()) return;
|
|
179
|
+
if (msg.query !== currentQuery) return; // stale
|
|
180
|
+
|
|
181
|
+
serverHits = msg.hits || [];
|
|
182
|
+
serverTotal = msg.total || 0;
|
|
183
|
+
|
|
184
|
+
// Update count to reflect total server hits
|
|
185
|
+
if (serverHits.length > 0) {
|
|
186
|
+
// Re-highlight loaded messages to get accurate DOM match count
|
|
187
|
+
clearHighlights();
|
|
188
|
+
highlightLoadedMessages();
|
|
189
|
+
if (matches.length > 0) {
|
|
190
|
+
matchCountEl.textContent = "1 / " + matches.length + " (" + serverHits.length + " total)";
|
|
191
|
+
} else {
|
|
192
|
+
matchCountEl.textContent = serverHits.length + " matches";
|
|
193
|
+
}
|
|
194
|
+
buildTimeline();
|
|
195
|
+
} else {
|
|
196
|
+
matchCountEl.textContent = "No results";
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Called after older history is prepended to DOM
|
|
201
|
+
function onHistoryPrepended() {
|
|
202
|
+
if (!pendingScrollTarget) return;
|
|
203
|
+
var target = pendingScrollTarget;
|
|
204
|
+
pendingScrollTarget = null;
|
|
205
|
+
// Re-highlight with current query
|
|
206
|
+
if (currentQuery && isSearchOpen()) {
|
|
207
|
+
clearHighlights();
|
|
208
|
+
highlightLoadedMessages();
|
|
209
|
+
}
|
|
210
|
+
requestAnimationFrame(function() {
|
|
211
|
+
findAndScrollToMatch(target.snippet, target.query);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Timeline (scroll map) ---
|
|
216
|
+
function buildTimeline() {
|
|
217
|
+
removeTimeline();
|
|
218
|
+
if (serverHits.length === 0) return;
|
|
219
|
+
|
|
220
|
+
var messagesEl = ctx.messagesEl;
|
|
221
|
+
var appEl = messagesEl.parentElement;
|
|
222
|
+
|
|
223
|
+
timelineEl = document.createElement("div");
|
|
224
|
+
timelineEl.className = "find-in-session-timeline";
|
|
225
|
+
timelineEl.id = "find-in-session-timeline";
|
|
226
|
+
|
|
227
|
+
var track = document.createElement("div");
|
|
228
|
+
track.className = "rewind-timeline-track";
|
|
229
|
+
track.dataset.historyTotal = serverTotal;
|
|
230
|
+
|
|
231
|
+
var viewport = document.createElement("div");
|
|
232
|
+
viewport.className = "rewind-timeline-viewport";
|
|
233
|
+
track.appendChild(viewport);
|
|
234
|
+
|
|
235
|
+
for (var i = 0; i < serverHits.length; i++) {
|
|
236
|
+
var hit = serverHits[i];
|
|
237
|
+
var pct = serverTotal <= 1 ? 50 : 6 + (hit.historyIndex / (serverTotal - 1)) * 88;
|
|
238
|
+
|
|
239
|
+
var snippetText = hit.snippet;
|
|
240
|
+
if (snippetText.length > 24) snippetText = snippetText.substring(0, 24) + "\u2026";
|
|
241
|
+
|
|
242
|
+
var marker = document.createElement("div");
|
|
243
|
+
marker.className = "rewind-timeline-marker search-hit-marker";
|
|
244
|
+
marker.innerHTML = iconHtml("search") + '<span class="marker-text">' + escapeHtml(snippetText) + '</span>';
|
|
245
|
+
marker.style.top = pct + "%";
|
|
246
|
+
marker.dataset.historyIndex = hit.historyIndex;
|
|
247
|
+
|
|
248
|
+
(function(hitData, markerEl) {
|
|
249
|
+
markerEl.addEventListener("click", function() {
|
|
250
|
+
scrollToSearchHit(hitData.historyIndex, hitData.snippet, currentQuery);
|
|
251
|
+
});
|
|
252
|
+
})(hit, marker);
|
|
253
|
+
|
|
254
|
+
track.appendChild(marker);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
timelineEl.appendChild(track);
|
|
258
|
+
|
|
259
|
+
// Position timeline to align with messages area
|
|
260
|
+
var titleBarEl = document.querySelector(".title-bar-content");
|
|
261
|
+
var inputAreaEl = document.getElementById("input-area");
|
|
262
|
+
var appRect = appEl.getBoundingClientRect();
|
|
263
|
+
var titleBarRect = titleBarEl ? titleBarEl.getBoundingClientRect() : { bottom: appRect.top };
|
|
264
|
+
var inputRect = inputAreaEl.getBoundingClientRect();
|
|
265
|
+
|
|
266
|
+
timelineEl.style.top = (titleBarRect.bottom - appRect.top + 4) + "px";
|
|
267
|
+
timelineEl.style.bottom = (appRect.bottom - inputRect.top + 4) + "px";
|
|
268
|
+
|
|
269
|
+
appEl.appendChild(timelineEl);
|
|
270
|
+
refreshIcons();
|
|
271
|
+
|
|
272
|
+
timelineScrollHandler = function() { updateTimelineViewport(track, viewport); };
|
|
273
|
+
messagesEl.addEventListener("scroll", timelineScrollHandler);
|
|
274
|
+
updateTimelineViewport(track, viewport);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function removeTimeline() {
|
|
278
|
+
if (timelineEl) {
|
|
279
|
+
timelineEl.remove();
|
|
280
|
+
timelineEl = null;
|
|
281
|
+
}
|
|
282
|
+
if (timelineScrollHandler && ctx.messagesEl) {
|
|
283
|
+
ctx.messagesEl.removeEventListener("scroll", timelineScrollHandler);
|
|
284
|
+
timelineScrollHandler = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function updateTimelineViewport(track, viewport) {
|
|
289
|
+
if (!track) return;
|
|
290
|
+
var messagesEl = ctx.messagesEl;
|
|
291
|
+
var scrollH = messagesEl.scrollHeight;
|
|
292
|
+
var viewH = messagesEl.clientHeight;
|
|
293
|
+
|
|
294
|
+
var historyFrom = ctx.getHistoryFrom ? ctx.getHistoryFrom() : 0;
|
|
295
|
+
var total = parseInt(track.dataset.historyTotal || "0", 10) || 1;
|
|
296
|
+
var timelineStart = 6 + (historyFrom / (total - 1 || 1)) * 88;
|
|
297
|
+
var timelineEnd = 94;
|
|
298
|
+
var timelineRange = timelineEnd - timelineStart;
|
|
299
|
+
|
|
300
|
+
if (scrollH <= viewH) {
|
|
301
|
+
viewport.style.top = timelineStart + "%";
|
|
302
|
+
viewport.style.height = timelineRange + "%";
|
|
303
|
+
} else {
|
|
304
|
+
var scrollFrac = messagesEl.scrollTop / scrollH;
|
|
305
|
+
var viewFrac = viewH / scrollH;
|
|
306
|
+
viewport.style.top = (timelineStart + scrollFrac * timelineRange) + "%";
|
|
307
|
+
viewport.style.height = (viewFrac * timelineRange) + "%";
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function scrollToSearchHit(historyIndex, snippet, query) {
|
|
312
|
+
var historyFrom = ctx.getHistoryFrom ? ctx.getHistoryFrom() : 0;
|
|
313
|
+
if (historyIndex < historyFrom) {
|
|
314
|
+
// Need to load older history first
|
|
315
|
+
pendingScrollTarget = { historyIndex: historyIndex, snippet: snippet, query: query };
|
|
316
|
+
if (ctx.ws && ctx.ws.readyState === 1) {
|
|
317
|
+
ctx.ws.send(JSON.stringify({ type: "load_more_history", before: historyFrom, target: historyIndex }));
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
findAndScrollToMatch(snippet, query);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function findAndScrollToMatch(snippet, query) {
|
|
325
|
+
var messagesEl = ctx.messagesEl;
|
|
326
|
+
var q = query.toLowerCase();
|
|
327
|
+
var allMsgs = messagesEl.querySelectorAll(".msg-user, .msg-assistant");
|
|
328
|
+
var cleanSnippet = snippet.replace(/^\u2026/, "").replace(/\u2026$/, "");
|
|
329
|
+
|
|
330
|
+
for (var i = 0; i < allMsgs.length; i++) {
|
|
331
|
+
var msgEl = allMsgs[i];
|
|
332
|
+
var textEl = msgEl.querySelector(".bubble") || msgEl.querySelector(".md-content");
|
|
333
|
+
if (!textEl) continue;
|
|
334
|
+
var text = textEl.textContent || "";
|
|
335
|
+
if (text.indexOf(cleanSnippet) !== -1) {
|
|
336
|
+
msgEl.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
337
|
+
msgEl.classList.remove("search-blink");
|
|
338
|
+
void msgEl.offsetWidth;
|
|
339
|
+
msgEl.classList.add("search-blink");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Fallback: any element containing the query
|
|
344
|
+
for (var j = 0; j < allMsgs.length; j++) {
|
|
345
|
+
var el = allMsgs[j];
|
|
346
|
+
var tEl = el.querySelector(".bubble") || el.querySelector(".md-content");
|
|
347
|
+
if (!tEl) continue;
|
|
348
|
+
if ((tEl.textContent || "").toLowerCase().indexOf(q) !== -1) {
|
|
349
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
350
|
+
el.classList.remove("search-blink");
|
|
351
|
+
void el.offsetWidth;
|
|
352
|
+
el.classList.add("search-blink");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// --- DOM highlighting ---
|
|
359
|
+
function highlightInElement(el, queryLower) {
|
|
360
|
+
var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
|
|
361
|
+
var textNodes = [];
|
|
362
|
+
var node;
|
|
363
|
+
while ((node = walker.nextNode())) {
|
|
364
|
+
textNodes.push(node);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (var i = 0; i < textNodes.length; i++) {
|
|
368
|
+
var textNode = textNodes[i];
|
|
369
|
+
var parent = textNode.parentNode;
|
|
370
|
+
if (!parent || parent.classList && parent.classList.contains(highlightClass)) continue;
|
|
371
|
+
if (parent.tagName === "BUTTON" || parent.tagName === "SCRIPT" || parent.tagName === "STYLE") continue;
|
|
372
|
+
|
|
373
|
+
var text = textNode.nodeValue;
|
|
374
|
+
var textLower = text.toLowerCase();
|
|
375
|
+
var idx = textLower.indexOf(queryLower);
|
|
376
|
+
if (idx === -1) continue;
|
|
377
|
+
|
|
378
|
+
var frag = document.createDocumentFragment();
|
|
379
|
+
var lastIdx = 0;
|
|
380
|
+
|
|
381
|
+
while (idx !== -1) {
|
|
382
|
+
if (idx > lastIdx) {
|
|
383
|
+
frag.appendChild(document.createTextNode(text.substring(lastIdx, idx)));
|
|
384
|
+
}
|
|
385
|
+
var mark = document.createElement("mark");
|
|
386
|
+
mark.className = highlightClass;
|
|
387
|
+
mark.textContent = text.substring(idx, idx + currentQuery.length);
|
|
388
|
+
frag.appendChild(mark);
|
|
389
|
+
lastIdx = idx + currentQuery.length;
|
|
390
|
+
idx = textLower.indexOf(queryLower, lastIdx);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (lastIdx < text.length) {
|
|
394
|
+
frag.appendChild(document.createTextNode(text.substring(lastIdx)));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
parent.replaceChild(frag, textNode);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function clearHighlights() {
|
|
402
|
+
var messagesEl = ctx.messagesEl;
|
|
403
|
+
var marks = messagesEl.querySelectorAll("mark." + highlightClass);
|
|
404
|
+
for (var i = 0; i < marks.length; i++) {
|
|
405
|
+
var mark = marks[i];
|
|
406
|
+
var parent = mark.parentNode;
|
|
407
|
+
parent.replaceChild(document.createTextNode(mark.textContent), mark);
|
|
408
|
+
parent.normalize();
|
|
409
|
+
}
|
|
410
|
+
matches = [];
|
|
411
|
+
currentMatchIndex = -1;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function setActiveMatch(index) {
|
|
415
|
+
var prev = ctx.messagesEl.querySelector("." + activeHighlightClass);
|
|
416
|
+
if (prev) prev.classList.remove(activeHighlightClass);
|
|
417
|
+
|
|
418
|
+
if (index >= 0 && index < matches.length) {
|
|
419
|
+
matches[index].classList.add(activeHighlightClass);
|
|
420
|
+
matches[index].scrollIntoView({ behavior: "smooth", block: "center" });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function goToNextMatch() {
|
|
425
|
+
if (matches.length === 0) return;
|
|
426
|
+
currentMatchIndex = (currentMatchIndex + 1) % matches.length;
|
|
427
|
+
setActiveMatch(currentMatchIndex);
|
|
428
|
+
matchCountEl.textContent = (currentMatchIndex + 1) + " / " + matches.length +
|
|
429
|
+
(serverHits.length > matches.length ? " (" + serverHits.length + " total)" : "");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function goToPrevMatch() {
|
|
433
|
+
if (matches.length === 0) return;
|
|
434
|
+
currentMatchIndex = (currentMatchIndex - 1 + matches.length) % matches.length;
|
|
435
|
+
setActiveMatch(currentMatchIndex);
|
|
436
|
+
matchCountEl.textContent = (currentMatchIndex + 1) + " / " + matches.length +
|
|
437
|
+
(serverHits.length > matches.length ? " (" + serverHits.length + " total)" : "");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export { initSessionSearch, openSearch, closeSearch, toggleSearch, isSearchOpen, handleFindInSessionResults, onHistoryPrepended };
|