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.
- package/bin/cli.js +1 -2350
- package/package.json +7 -42
- package/LICENSE +0 -21
- package/README.md +0 -281
- package/lib/cli-sessions.js +0 -270
- package/lib/config.js +0 -222
- package/lib/daemon.js +0 -423
- package/lib/ipc.js +0 -112
- package/lib/pages.js +0 -714
- package/lib/project.js +0 -1224
- package/lib/public/app.js +0 -2157
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +0 -145
- package/lib/public/css/diff.css +0 -128
- package/lib/public/css/filebrowser.css +0 -1076
- package/lib/public/css/highlight.css +0 -144
- package/lib/public/css/input.css +0 -512
- package/lib/public/css/menus.css +0 -683
- package/lib/public/css/messages.css +0 -1159
- package/lib/public/css/overlays.css +0 -731
- package/lib/public/css/rewind.css +0 -529
- package/lib/public/css/sidebar.css +0 -794
- package/lib/public/favicon.svg +0 -26
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-mono.svg +0 -19
- package/lib/public/index.html +0 -460
- package/lib/public/manifest.json +0 -27
- package/lib/public/modules/diff.js +0 -398
- package/lib/public/modules/events.js +0 -21
- package/lib/public/modules/filebrowser.js +0 -1375
- package/lib/public/modules/fileicons.js +0 -172
- package/lib/public/modules/icons.js +0 -54
- package/lib/public/modules/input.js +0 -578
- package/lib/public/modules/markdown.js +0 -149
- package/lib/public/modules/notifications.js +0 -643
- package/lib/public/modules/qrcode.js +0 -70
- package/lib/public/modules/rewind.js +0 -334
- package/lib/public/modules/sidebar.js +0 -628
- package/lib/public/modules/state.js +0 -3
- package/lib/public/modules/terminal.js +0 -658
- package/lib/public/modules/theme.js +0 -622
- package/lib/public/modules/tools.js +0 -1410
- package/lib/public/modules/utils.js +0 -56
- package/lib/public/style.css +0 -10
- package/lib/public/sw.js +0 -75
- package/lib/push.js +0 -125
- package/lib/sdk-bridge.js +0 -771
- package/lib/server.js +0 -577
- package/lib/sessions.js +0 -402
- package/lib/terminal-manager.js +0 -187
- package/lib/terminal.js +0 -24
- package/lib/themes/ayu-light.json +0 -9
- package/lib/themes/catppuccin-latte.json +0 -9
- package/lib/themes/catppuccin-mocha.json +0 -9
- package/lib/themes/claude-light.json +0 -9
- package/lib/themes/claude.json +0 -9
- package/lib/themes/dracula.json +0 -9
- package/lib/themes/everforest-light.json +0 -9
- package/lib/themes/everforest.json +0 -9
- package/lib/themes/github-light.json +0 -9
- package/lib/themes/gruvbox-dark.json +0 -9
- package/lib/themes/gruvbox-light.json +0 -9
- package/lib/themes/monokai.json +0 -9
- package/lib/themes/nord-light.json +0 -9
- package/lib/themes/nord.json +0 -9
- package/lib/themes/one-dark.json +0 -9
- package/lib/themes/one-light.json +0 -9
- package/lib/themes/rose-pine-dawn.json +0 -9
- package/lib/themes/rose-pine.json +0 -9
- package/lib/themes/solarized-dark.json +0 -9
- package/lib/themes/solarized-light.json +0 -9
- package/lib/themes/tokyo-night-light.json +0 -9
- package/lib/themes/tokyo-night.json +0 -9
- 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
|
-
}
|