claude-relay 2.2.3 → 2.3.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 +12 -0
- package/bin/cli.js +271 -22
- package/lib/config.js +39 -2
- package/lib/daemon.js +53 -1
- package/lib/ipc.js +7 -3
- package/lib/pages.js +15 -1
- package/lib/project.js +324 -27
- package/lib/public/app.js +313 -7
- package/lib/public/css/base.css +5 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +541 -0
- package/lib/public/css/input.css +1 -0
- package/lib/public/css/menus.css +89 -5
- package/lib/public/css/messages.css +84 -49
- package/lib/public/css/overlays.css +40 -0
- package/lib/public/index.html +100 -17
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/filebrowser.js +1023 -11
- package/lib/public/modules/input.js +96 -2
- package/lib/public/modules/notifications.js +29 -3
- package/lib/public/modules/qrcode.js +11 -2
- package/lib/public/modules/rewind.js +51 -2
- package/lib/public/modules/tools.js +43 -104
- package/lib/public/modules/utils.js +10 -2
- package/lib/public/style.css +1 -0
- package/lib/public/sw.js +21 -7
- package/lib/push.js +5 -1
- package/lib/sdk-bridge.js +40 -7
- package/lib/server.js +37 -4
- package/lib/sessions.js +14 -5
- package/lib/terminal.js +2 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { iconHtml, refreshIcons } from './icons.js';
|
|
|
2
2
|
import { escapeHtml, copyToClipboard } from './utils.js';
|
|
3
3
|
import { renderMarkdown, highlightCodeBlocks } from './markdown.js';
|
|
4
4
|
import { closeSidebar } from './sidebar.js';
|
|
5
|
+
import { renderUnifiedDiff, renderSplitDiff } from './diff.js';
|
|
5
6
|
|
|
6
7
|
var ctx;
|
|
7
8
|
var treeData = {}; // path -> { loaded, children }
|
|
@@ -9,6 +10,16 @@ var currentContent = null; // last read file content for copy
|
|
|
9
10
|
var currentFilePath = null; // path of the currently viewed file
|
|
10
11
|
var isRendered = false; // markdown render toggle state
|
|
11
12
|
var currentIsMarkdown = false;
|
|
13
|
+
var historyVisible = false;
|
|
14
|
+
var currentHistoryEntries = [];
|
|
15
|
+
var pendingNavigate = null; // { sessionLocalId, assistantUuid }
|
|
16
|
+
var selectedEntries = []; // up to 2 selected for compare
|
|
17
|
+
var compareMode = false;
|
|
18
|
+
var inlineDiffActive = false;
|
|
19
|
+
var gitDiffCache = {}; // hash -> diff text
|
|
20
|
+
var pendingGitDiff = null; // callback for pending git diff
|
|
21
|
+
var fileAtCache = {}; // hash -> file content
|
|
22
|
+
var pendingFileAt = null; // callback for pending file-at
|
|
12
23
|
|
|
13
24
|
export function initFileBrowser(_ctx) {
|
|
14
25
|
ctx = _ctx;
|
|
@@ -30,6 +41,31 @@ export function initFileBrowser(_ctx) {
|
|
|
30
41
|
renderBody();
|
|
31
42
|
});
|
|
32
43
|
|
|
44
|
+
// History button
|
|
45
|
+
document.getElementById("file-viewer-history").addEventListener("click", function () {
|
|
46
|
+
if (currentHistoryEntries.length === 0) return;
|
|
47
|
+
historyVisible = !historyVisible;
|
|
48
|
+
inlineDiffActive = false;
|
|
49
|
+
compareMode = false;
|
|
50
|
+
selectedEntries = [];
|
|
51
|
+
ctx.fileViewerEl.classList.remove("file-viewer-wide");
|
|
52
|
+
if (historyVisible) {
|
|
53
|
+
renderHistoryPanel();
|
|
54
|
+
} else {
|
|
55
|
+
rerenderFileContent();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Refresh button
|
|
60
|
+
var refreshBtn = document.getElementById("file-panel-refresh");
|
|
61
|
+
if (refreshBtn) {
|
|
62
|
+
refreshBtn.addEventListener("click", function () {
|
|
63
|
+
refreshBtn.classList.add("spinning");
|
|
64
|
+
setTimeout(function () { refreshBtn.classList.remove("spinning"); }, 500);
|
|
65
|
+
refreshTree();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
33
69
|
// ESC to close
|
|
34
70
|
document.addEventListener("keydown", function (e) {
|
|
35
71
|
if (e.key === "Escape" && !ctx.fileViewerEl.classList.contains("hidden")) {
|
|
@@ -53,9 +89,23 @@ function sendUnwatch() {
|
|
|
53
89
|
|
|
54
90
|
export function closeFileViewer() {
|
|
55
91
|
sendUnwatch();
|
|
92
|
+
inlineDiffActive = false;
|
|
93
|
+
ctx.fileViewerEl.classList.remove("file-viewer-wide");
|
|
56
94
|
ctx.fileViewerEl.classList.add("hidden");
|
|
57
95
|
}
|
|
58
96
|
|
|
97
|
+
var pendingOpenMode = null; // { type: "diff", oldStr, newStr } or null
|
|
98
|
+
|
|
99
|
+
export function openFile(filePath, opts) {
|
|
100
|
+
if (!filePath) return;
|
|
101
|
+
if (opts && opts.diff) {
|
|
102
|
+
pendingOpenMode = { type: "diff", oldStr: opts.diff.oldStr, newStr: opts.diff.newStr };
|
|
103
|
+
} else {
|
|
104
|
+
pendingOpenMode = null;
|
|
105
|
+
}
|
|
106
|
+
requestFileContent(filePath);
|
|
107
|
+
}
|
|
108
|
+
|
|
59
109
|
function renderBody() {
|
|
60
110
|
var bodyEl = document.getElementById("file-viewer-body");
|
|
61
111
|
var renderBtn = document.getElementById("file-viewer-render");
|
|
@@ -97,6 +147,23 @@ export function loadRootDirectory() {
|
|
|
97
147
|
requestDirectory(".");
|
|
98
148
|
}
|
|
99
149
|
|
|
150
|
+
export function refreshTree() {
|
|
151
|
+
// Collect currently expanded directory paths
|
|
152
|
+
var expandedDirs = ["."];
|
|
153
|
+
var expandedEls = ctx.fileTreeEl.querySelectorAll(".file-tree-item.expanded");
|
|
154
|
+
for (var i = 0; i < expandedEls.length; i++) {
|
|
155
|
+
var childEl = expandedEls[i].nextElementSibling;
|
|
156
|
+
if (childEl && childEl.dataset.parentPath) {
|
|
157
|
+
expandedDirs.push(childEl.dataset.parentPath);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Clear cache for expanded dirs and re-request them
|
|
161
|
+
for (var j = 0; j < expandedDirs.length; j++) {
|
|
162
|
+
delete treeData[expandedDirs[j]];
|
|
163
|
+
requestDirectory(expandedDirs[j]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
100
167
|
function requestDirectory(dirPath) {
|
|
101
168
|
if (ctx.ws && ctx.connected) {
|
|
102
169
|
ctx.ws.send(JSON.stringify({ type: "fs_list", path: dirPath }));
|
|
@@ -113,6 +180,8 @@ var pendingRefresh = false;
|
|
|
113
180
|
|
|
114
181
|
export function refreshIfOpen(filePath) {
|
|
115
182
|
if (!currentFilePath || ctx.fileViewerEl.classList.contains("hidden")) return;
|
|
183
|
+
// Don't refresh while history panel or inline diff is showing
|
|
184
|
+
if (historyVisible || inlineDiffActive) return;
|
|
116
185
|
// Compare by suffix — tool paths are absolute, currentFilePath is relative
|
|
117
186
|
if (filePath === currentFilePath || filePath.endsWith("/" + currentFilePath)) {
|
|
118
187
|
pendingRefresh = true;
|
|
@@ -136,7 +205,15 @@ export function handleFsList(msg) {
|
|
|
136
205
|
|
|
137
206
|
// Root level
|
|
138
207
|
if (dirPath === ".") {
|
|
208
|
+
// Preserve expanded state across re-render
|
|
209
|
+
var expandedSet = {};
|
|
210
|
+
var expandedEls = ctx.fileTreeEl.querySelectorAll(".file-tree-item.expanded");
|
|
211
|
+
for (var ei = 0; ei < expandedEls.length; ei++) {
|
|
212
|
+
var sib = expandedEls[ei].nextElementSibling;
|
|
213
|
+
if (sib && sib.dataset.parentPath) expandedSet[sib.dataset.parentPath] = true;
|
|
214
|
+
}
|
|
139
215
|
renderTree();
|
|
216
|
+
restoreExpanded(expandedSet);
|
|
140
217
|
return;
|
|
141
218
|
}
|
|
142
219
|
|
|
@@ -150,6 +227,68 @@ export function handleFsList(msg) {
|
|
|
150
227
|
}
|
|
151
228
|
}
|
|
152
229
|
|
|
230
|
+
export function handleDirChanged(msg) {
|
|
231
|
+
var dirPath = msg.path || ".";
|
|
232
|
+
var oldData = treeData[dirPath];
|
|
233
|
+
treeData[dirPath] = { loaded: true, children: msg.entries || [] };
|
|
234
|
+
|
|
235
|
+
// Only re-render if the entries actually changed
|
|
236
|
+
if (oldData && oldData.loaded) {
|
|
237
|
+
var oldKeys = (oldData.children || []).map(function (e) { return e.name + ":" + e.type; }).sort().join(",");
|
|
238
|
+
var newKeys = (msg.entries || []).map(function (e) { return e.name + ":" + e.type; }).sort().join(",");
|
|
239
|
+
if (oldKeys === newKeys) return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Collect expanded directories before re-render
|
|
243
|
+
var expandedSet = {};
|
|
244
|
+
var expandedEls = ctx.fileTreeEl.querySelectorAll(".file-tree-item.expanded");
|
|
245
|
+
for (var i = 0; i < expandedEls.length; i++) {
|
|
246
|
+
var sib = expandedEls[i].nextElementSibling;
|
|
247
|
+
if (sib && sib.dataset.parentPath) expandedSet[sib.dataset.parentPath] = true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (dirPath === ".") {
|
|
251
|
+
renderTree();
|
|
252
|
+
// Restore expanded state
|
|
253
|
+
restoreExpanded(expandedSet);
|
|
254
|
+
} else {
|
|
255
|
+
var childEl = ctx.fileTreeEl.querySelector('.file-tree-children[data-parent-path="' + dirPath + '"]');
|
|
256
|
+
if (childEl && !childEl.classList.contains("hidden")) {
|
|
257
|
+
childEl.innerHTML = "";
|
|
258
|
+
var depth = dirPath.split("/").length;
|
|
259
|
+
renderEntries(childEl, treeData[dirPath].children, depth);
|
|
260
|
+
refreshIcons();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function restoreExpanded(expandedSet) {
|
|
266
|
+
var containers = ctx.fileTreeEl.querySelectorAll(".file-tree-children");
|
|
267
|
+
for (var i = 0; i < containers.length; i++) {
|
|
268
|
+
var p = containers[i].dataset.parentPath;
|
|
269
|
+
if (p && expandedSet[p] && treeData[p] && treeData[p].loaded) {
|
|
270
|
+
containers[i].classList.remove("hidden");
|
|
271
|
+
var row = containers[i].previousElementSibling;
|
|
272
|
+
if (row) row.classList.add("expanded");
|
|
273
|
+
containers[i].innerHTML = "";
|
|
274
|
+
var depth = p.split("/").length;
|
|
275
|
+
renderEntries(containers[i], treeData[p].children, depth);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Restore active file highlight
|
|
279
|
+
if (currentFilePath && !ctx.fileViewerEl.classList.contains("hidden")) {
|
|
280
|
+
var items = ctx.fileTreeEl.querySelectorAll(".file-tree-item");
|
|
281
|
+
for (var j = 0; j < items.length; j++) {
|
|
282
|
+
var nameEl = items[j].querySelector(".file-tree-name");
|
|
283
|
+
if (nameEl && nameEl.textContent === currentFilePath.split("/").pop()) {
|
|
284
|
+
items[j].classList.add("active");
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
refreshIcons();
|
|
290
|
+
}
|
|
291
|
+
|
|
153
292
|
export function handleFsRead(msg) {
|
|
154
293
|
showFileContent(msg);
|
|
155
294
|
}
|
|
@@ -295,28 +434,52 @@ function showFileContent(msg) {
|
|
|
295
434
|
if (currentIsMarkdown) {
|
|
296
435
|
renderBody();
|
|
297
436
|
} else {
|
|
298
|
-
|
|
299
|
-
var code = document.createElement("code");
|
|
300
|
-
var lang = mapExtToLanguage(ext);
|
|
301
|
-
if (lang) code.className = "language-" + lang;
|
|
302
|
-
code.textContent = msg.content;
|
|
303
|
-
pre.appendChild(code);
|
|
304
|
-
bodyEl.innerHTML = "";
|
|
305
|
-
bodyEl.appendChild(pre);
|
|
306
|
-
if (typeof hljs !== "undefined") {
|
|
307
|
-
hljs.highlightElement(code);
|
|
308
|
-
}
|
|
437
|
+
renderCodeWithLineNumbers(bodyEl, msg.content, ext);
|
|
309
438
|
}
|
|
310
439
|
}
|
|
311
440
|
|
|
312
441
|
ctx.fileViewerEl.classList.remove("hidden");
|
|
313
442
|
sendWatch(msg.path);
|
|
314
443
|
refreshIcons();
|
|
444
|
+
|
|
445
|
+
// If opened with a diff request, show full-file split diff in wide mode
|
|
446
|
+
if (pendingOpenMode && pendingOpenMode.type === "diff" && currentContent != null) {
|
|
447
|
+
var diffOpts = pendingOpenMode;
|
|
448
|
+
pendingOpenMode = null;
|
|
449
|
+
historyVisible = false;
|
|
450
|
+
compareMode = false;
|
|
451
|
+
selectedEntries = [];
|
|
452
|
+
currentHistoryEntries = [];
|
|
453
|
+
gitDiffCache = {};
|
|
454
|
+
fileAtCache = {};
|
|
455
|
+
var historyBtn2 = document.getElementById("file-viewer-history");
|
|
456
|
+
historyBtn2.classList.add("hidden");
|
|
457
|
+
historyBtn2.classList.remove("active");
|
|
458
|
+
requestFileHistory(msg.path);
|
|
459
|
+
showInlineDiff(diffOpts.oldStr, diffOpts.newStr);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
pendingOpenMode = null;
|
|
463
|
+
|
|
464
|
+
// Request edit history for this file (skip on auto-refresh)
|
|
465
|
+
if (!keepRenderState) {
|
|
466
|
+
historyVisible = false;
|
|
467
|
+
compareMode = false;
|
|
468
|
+
selectedEntries = [];
|
|
469
|
+
currentHistoryEntries = [];
|
|
470
|
+
gitDiffCache = {};
|
|
471
|
+
fileAtCache = {};
|
|
472
|
+
var historyBtn = document.getElementById("file-viewer-history");
|
|
473
|
+
historyBtn.classList.add("hidden");
|
|
474
|
+
historyBtn.classList.remove("active");
|
|
475
|
+
requestFileHistory(msg.path);
|
|
476
|
+
}
|
|
315
477
|
}
|
|
316
478
|
|
|
317
479
|
export function handleFileChanged(msg) {
|
|
318
480
|
if (!msg.path || msg.path !== currentFilePath) return;
|
|
319
481
|
if (ctx.fileViewerEl.classList.contains("hidden")) return;
|
|
482
|
+
if (historyVisible || inlineDiffActive) return;
|
|
320
483
|
if (msg.content === currentContent) return;
|
|
321
484
|
|
|
322
485
|
var bodyEl = document.getElementById("file-viewer-body");
|
|
@@ -326,6 +489,91 @@ export function handleFileChanged(msg) {
|
|
|
326
489
|
if (bodyEl) bodyEl.scrollTop = scrollPos;
|
|
327
490
|
}
|
|
328
491
|
|
|
492
|
+
function showInlineDiff(oldStr, newStr) {
|
|
493
|
+
var bodyEl = document.getElementById("file-viewer-body");
|
|
494
|
+
inlineDiffActive = true;
|
|
495
|
+
ctx.fileViewerEl.classList.add("file-viewer-wide");
|
|
496
|
+
|
|
497
|
+
if (!currentContent) return;
|
|
498
|
+
|
|
499
|
+
// Reconstruct full "before" file by replacing new_string with old_string
|
|
500
|
+
var fileBefore = currentContent;
|
|
501
|
+
var fileAfter = currentContent;
|
|
502
|
+
if (newStr && oldStr != null) {
|
|
503
|
+
var pos = currentContent.indexOf(newStr);
|
|
504
|
+
if (pos >= 0) {
|
|
505
|
+
fileBefore = currentContent.substring(0, pos) + oldStr + currentContent.substring(pos + newStr.length);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
var diffLang = currentLang();
|
|
510
|
+
var viewMode = "split";
|
|
511
|
+
|
|
512
|
+
function render() {
|
|
513
|
+
bodyEl.innerHTML = "";
|
|
514
|
+
|
|
515
|
+
// Top bar
|
|
516
|
+
var topBar = document.createElement("div");
|
|
517
|
+
topBar.className = "file-history-view-bar";
|
|
518
|
+
|
|
519
|
+
var backBtn = document.createElement("button");
|
|
520
|
+
backBtn.className = "file-history-compare-back";
|
|
521
|
+
backBtn.textContent = "Back to file";
|
|
522
|
+
backBtn.addEventListener("click", function () {
|
|
523
|
+
inlineDiffActive = false;
|
|
524
|
+
ctx.fileViewerEl.classList.remove("file-viewer-wide");
|
|
525
|
+
rerenderFileContent();
|
|
526
|
+
});
|
|
527
|
+
topBar.appendChild(backBtn);
|
|
528
|
+
|
|
529
|
+
var toggleWrap = document.createElement("div");
|
|
530
|
+
toggleWrap.className = "file-history-view-toggle";
|
|
531
|
+
|
|
532
|
+
var splitBtn = document.createElement("button");
|
|
533
|
+
splitBtn.className = "file-history-toggle-btn" + (viewMode === "split" ? " active" : "");
|
|
534
|
+
splitBtn.textContent = "Split";
|
|
535
|
+
splitBtn.addEventListener("click", function () {
|
|
536
|
+
viewMode = "split";
|
|
537
|
+
render();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
var unifiedBtn = document.createElement("button");
|
|
541
|
+
unifiedBtn.className = "file-history-toggle-btn" + (viewMode === "unified" ? " active" : "");
|
|
542
|
+
unifiedBtn.textContent = "Unified";
|
|
543
|
+
unifiedBtn.addEventListener("click", function () {
|
|
544
|
+
viewMode = "unified";
|
|
545
|
+
render();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
toggleWrap.appendChild(splitBtn);
|
|
549
|
+
toggleWrap.appendChild(unifiedBtn);
|
|
550
|
+
topBar.appendChild(toggleWrap);
|
|
551
|
+
bodyEl.appendChild(topBar);
|
|
552
|
+
|
|
553
|
+
// Full-file diff
|
|
554
|
+
var diffWrap = document.createElement("div");
|
|
555
|
+
diffWrap.className = "file-history-diff-full";
|
|
556
|
+
|
|
557
|
+
if (viewMode === "split") {
|
|
558
|
+
diffWrap.appendChild(renderSplitDiff(fileBefore, fileAfter, diffLang));
|
|
559
|
+
} else {
|
|
560
|
+
diffWrap.appendChild(renderUnifiedDiff(fileBefore, fileAfter, diffLang));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
bodyEl.appendChild(diffWrap);
|
|
564
|
+
|
|
565
|
+
// Scroll to first changed row
|
|
566
|
+
requestAnimationFrame(function () {
|
|
567
|
+
var firstChange = diffWrap.querySelector(".diff-row-change, .diff-row-add, .diff-row-remove");
|
|
568
|
+
if (firstChange) {
|
|
569
|
+
firstChange.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
render();
|
|
575
|
+
}
|
|
576
|
+
|
|
329
577
|
function mapExtToLanguage(ext) {
|
|
330
578
|
var map = {
|
|
331
579
|
js: "javascript", ts: "typescript", jsx: "javascript", tsx: "typescript",
|
|
@@ -338,8 +586,772 @@ function mapExtToLanguage(ext) {
|
|
|
338
586
|
return map[ext] || null;
|
|
339
587
|
}
|
|
340
588
|
|
|
589
|
+
function currentLang() {
|
|
590
|
+
if (!currentFilePath) return null;
|
|
591
|
+
var ext = currentFilePath.split(".").pop().toLowerCase();
|
|
592
|
+
return mapExtToLanguage(ext);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function renderCodeWithLineNumbers(bodyEl, content, ext) {
|
|
596
|
+
var lang = mapExtToLanguage(ext);
|
|
597
|
+
var lines = content.split("\n");
|
|
598
|
+
var lineCount = lines.length;
|
|
599
|
+
|
|
600
|
+
var viewer = document.createElement("div");
|
|
601
|
+
viewer.className = "file-viewer-code";
|
|
602
|
+
|
|
603
|
+
var gutter = document.createElement("pre");
|
|
604
|
+
gutter.className = "file-viewer-gutter";
|
|
605
|
+
var nums = [];
|
|
606
|
+
for (var i = 1; i <= lineCount; i++) nums.push(i);
|
|
607
|
+
gutter.textContent = nums.join("\n");
|
|
608
|
+
|
|
609
|
+
var codeWrap = document.createElement("pre");
|
|
610
|
+
codeWrap.className = "file-viewer-code-content";
|
|
611
|
+
var codeEl = document.createElement("code");
|
|
612
|
+
if (lang) codeEl.className = "language-" + lang;
|
|
613
|
+
codeEl.textContent = content;
|
|
614
|
+
codeWrap.appendChild(codeEl);
|
|
615
|
+
|
|
616
|
+
viewer.appendChild(gutter);
|
|
617
|
+
viewer.appendChild(codeWrap);
|
|
618
|
+
|
|
619
|
+
bodyEl.innerHTML = "";
|
|
620
|
+
bodyEl.appendChild(viewer);
|
|
621
|
+
|
|
622
|
+
if (typeof hljs !== "undefined" && lang) {
|
|
623
|
+
hljs.highlightElement(codeEl);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
341
627
|
function formatSize(bytes) {
|
|
342
628
|
if (bytes < 1024) return bytes + " B";
|
|
343
629
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
|
|
344
630
|
return (bytes / 1048576).toFixed(1) + " MB";
|
|
345
631
|
}
|
|
632
|
+
|
|
633
|
+
// --- File edit history ---
|
|
634
|
+
|
|
635
|
+
function requestFileHistory(filePath) {
|
|
636
|
+
if (ctx.ws && ctx.connected) {
|
|
637
|
+
ctx.ws.send(JSON.stringify({ type: "fs_file_history", path: filePath }));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function requestGitDiff(hash, hash2) {
|
|
642
|
+
if (ctx.ws && ctx.connected) {
|
|
643
|
+
var msg = { type: "fs_git_diff", path: currentFilePath, hash: hash };
|
|
644
|
+
if (hash2) msg.hash2 = hash2;
|
|
645
|
+
ctx.ws.send(JSON.stringify(msg));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function handleFileHistory(msg) {
|
|
650
|
+
currentHistoryEntries = msg.entries || [];
|
|
651
|
+
var historyBtn = document.getElementById("file-viewer-history");
|
|
652
|
+
|
|
653
|
+
if (currentHistoryEntries.length > 0 && currentContent !== null) {
|
|
654
|
+
historyBtn.classList.remove("hidden");
|
|
655
|
+
} else {
|
|
656
|
+
historyBtn.classList.add("hidden");
|
|
657
|
+
historyVisible = false;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (historyVisible && !compareMode) {
|
|
661
|
+
renderHistoryPanel();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export function handleGitDiff(msg) {
|
|
666
|
+
if (msg.hash && msg.diff !== undefined) {
|
|
667
|
+
var key = msg.hash2 ? msg.hash + ".." + msg.hash2 : msg.hash;
|
|
668
|
+
gitDiffCache[key] = msg.diff;
|
|
669
|
+
}
|
|
670
|
+
if (pendingGitDiff) {
|
|
671
|
+
var cb = pendingGitDiff;
|
|
672
|
+
pendingGitDiff = null;
|
|
673
|
+
cb(msg);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function requestFileAt(hash) {
|
|
678
|
+
if (ctx.ws && ctx.connected) {
|
|
679
|
+
ctx.ws.send(JSON.stringify({ type: "fs_file_at", path: currentFilePath, hash: hash }));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function handleFileAt(msg) {
|
|
684
|
+
if (msg.hash && msg.content !== undefined) {
|
|
685
|
+
fileAtCache[msg.hash] = msg.content;
|
|
686
|
+
}
|
|
687
|
+
if (pendingFileAt) {
|
|
688
|
+
var cb = pendingFileAt;
|
|
689
|
+
pendingFileAt = null;
|
|
690
|
+
cb(msg);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function rerenderFileContent() {
|
|
695
|
+
var historyBtn = document.getElementById("file-viewer-history");
|
|
696
|
+
historyBtn.classList.remove("active");
|
|
697
|
+
|
|
698
|
+
if (!currentContent || !currentFilePath) return;
|
|
699
|
+
var bodyEl = document.getElementById("file-viewer-body");
|
|
700
|
+
var ext = currentFilePath.split(".").pop().toLowerCase();
|
|
701
|
+
|
|
702
|
+
if (currentIsMarkdown) {
|
|
703
|
+
renderBody();
|
|
704
|
+
} else {
|
|
705
|
+
renderCodeWithLineNumbers(bodyEl, currentContent, ext);
|
|
706
|
+
}
|
|
707
|
+
refreshIcons();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function isEntrySelected(entry) {
|
|
711
|
+
for (var i = 0; i < selectedEntries.length; i++) {
|
|
712
|
+
if (selectedEntries[i] === entry) return i + 1;
|
|
713
|
+
}
|
|
714
|
+
return 0;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function toggleSelect(entry) {
|
|
718
|
+
var idx = -1;
|
|
719
|
+
for (var i = 0; i < selectedEntries.length; i++) {
|
|
720
|
+
if (selectedEntries[i] === entry) { idx = i; break; }
|
|
721
|
+
}
|
|
722
|
+
if (idx >= 0) {
|
|
723
|
+
selectedEntries.splice(idx, 1);
|
|
724
|
+
} else {
|
|
725
|
+
if (selectedEntries.length >= 2) selectedEntries.shift();
|
|
726
|
+
selectedEntries.push(entry);
|
|
727
|
+
}
|
|
728
|
+
var bodyEl = document.getElementById("file-viewer-body");
|
|
729
|
+
var scrollPos = bodyEl ? bodyEl.scrollTop : 0;
|
|
730
|
+
renderHistoryPanel();
|
|
731
|
+
if (bodyEl) {
|
|
732
|
+
if (selectedEntries.length === 2) {
|
|
733
|
+
// Both slots filled: scroll compare bar into view
|
|
734
|
+
requestAnimationFrame(function () {
|
|
735
|
+
var compareBtn = bodyEl.querySelector(".file-history-compare-btn");
|
|
736
|
+
if (compareBtn) {
|
|
737
|
+
compareBtn.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
} else {
|
|
741
|
+
// Restore scroll position
|
|
742
|
+
bodyEl.scrollTop = scrollPos;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function renderHistoryPanel() {
|
|
748
|
+
var bodyEl = document.getElementById("file-viewer-body");
|
|
749
|
+
var historyBtn = document.getElementById("file-viewer-history");
|
|
750
|
+
historyBtn.classList.add("active");
|
|
751
|
+
|
|
752
|
+
bodyEl.innerHTML = "";
|
|
753
|
+
|
|
754
|
+
var panel = document.createElement("div");
|
|
755
|
+
panel.className = "file-history-panel";
|
|
756
|
+
|
|
757
|
+
// Header
|
|
758
|
+
var header = document.createElement("div");
|
|
759
|
+
header.className = "file-history-header";
|
|
760
|
+
|
|
761
|
+
var headerTitle = document.createElement("span");
|
|
762
|
+
headerTitle.textContent = "History (" + currentHistoryEntries.length + ")";
|
|
763
|
+
header.appendChild(headerTitle);
|
|
764
|
+
|
|
765
|
+
panel.appendChild(header);
|
|
766
|
+
|
|
767
|
+
// Compare bar
|
|
768
|
+
var compareBar = document.createElement("div");
|
|
769
|
+
compareBar.className = "file-history-compare-bar-slots";
|
|
770
|
+
|
|
771
|
+
var compareLabel = document.createElement("span");
|
|
772
|
+
compareLabel.className = "compare-bar-label";
|
|
773
|
+
compareLabel.innerHTML = iconHtml("arrow-left-right") + " Compare";
|
|
774
|
+
compareBar.appendChild(compareLabel);
|
|
775
|
+
|
|
776
|
+
var slotsRow = document.createElement("div");
|
|
777
|
+
slotsRow.className = "compare-slots-row";
|
|
778
|
+
|
|
779
|
+
var slotA = document.createElement("div");
|
|
780
|
+
slotA.className = "file-history-compare-slot";
|
|
781
|
+
if (selectedEntries.length >= 1) {
|
|
782
|
+
slotA.classList.add("filled");
|
|
783
|
+
slotA.innerHTML = '<span class="compare-slot-num">A</span><span class="compare-slot-text"></span><button class="compare-slot-clear">\u00d7</button>';
|
|
784
|
+
slotA.querySelector(".compare-slot-text").textContent = shortEntryLabel(selectedEntries[0]);
|
|
785
|
+
slotA.querySelector(".compare-slot-clear").addEventListener("click", function () {
|
|
786
|
+
selectedEntries.splice(0, 1);
|
|
787
|
+
renderHistoryPanel();
|
|
788
|
+
});
|
|
789
|
+
} else {
|
|
790
|
+
slotA.innerHTML = '<span class="compare-slot-num">A</span><span class="compare-slot-placeholder">Select entry below</span>';
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
var arrowSpan = document.createElement("span");
|
|
794
|
+
arrowSpan.className = "compare-slot-arrow";
|
|
795
|
+
arrowSpan.innerHTML = iconHtml("arrow-right");
|
|
796
|
+
|
|
797
|
+
var slotB = document.createElement("div");
|
|
798
|
+
slotB.className = "file-history-compare-slot";
|
|
799
|
+
if (selectedEntries.length >= 2) {
|
|
800
|
+
slotB.classList.add("filled");
|
|
801
|
+
slotB.innerHTML = '<span class="compare-slot-num">B</span><span class="compare-slot-text"></span><button class="compare-slot-clear">\u00d7</button>';
|
|
802
|
+
slotB.querySelector(".compare-slot-text").textContent = shortEntryLabel(selectedEntries[1]);
|
|
803
|
+
slotB.querySelector(".compare-slot-clear").addEventListener("click", function () {
|
|
804
|
+
selectedEntries.splice(1, 1);
|
|
805
|
+
renderHistoryPanel();
|
|
806
|
+
});
|
|
807
|
+
} else {
|
|
808
|
+
slotB.innerHTML = '<span class="compare-slot-num">B</span><span class="compare-slot-placeholder">Select entry below</span>';
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
slotsRow.appendChild(slotA);
|
|
812
|
+
slotsRow.appendChild(arrowSpan);
|
|
813
|
+
slotsRow.appendChild(slotB);
|
|
814
|
+
|
|
815
|
+
if (selectedEntries.length === 2) {
|
|
816
|
+
var compareBtn = document.createElement("button");
|
|
817
|
+
compareBtn.className = "file-history-compare-btn";
|
|
818
|
+
compareBtn.innerHTML = iconHtml("arrow-left-right") + " Compare";
|
|
819
|
+
compareBtn.addEventListener("click", function () {
|
|
820
|
+
compareMode = true;
|
|
821
|
+
renderCompareView();
|
|
822
|
+
});
|
|
823
|
+
slotsRow.appendChild(compareBtn);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
compareBar.appendChild(slotsRow);
|
|
827
|
+
panel.appendChild(compareBar);
|
|
828
|
+
|
|
829
|
+
var list = document.createElement("div");
|
|
830
|
+
list.className = "file-history-list";
|
|
831
|
+
|
|
832
|
+
for (var i = 0; i < currentHistoryEntries.length; i++) {
|
|
833
|
+
var item = currentHistoryEntries[i];
|
|
834
|
+
var entry = document.createElement("div");
|
|
835
|
+
entry.className = "file-history-entry";
|
|
836
|
+
if (item.source === "git") entry.classList.add("git-entry");
|
|
837
|
+
|
|
838
|
+
var selNum = isEntrySelected(item);
|
|
839
|
+
if (selNum) {
|
|
840
|
+
entry.classList.add("selected");
|
|
841
|
+
entry.dataset.selectNum = selNum;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Header row
|
|
845
|
+
var entryHeader = document.createElement("div");
|
|
846
|
+
entryHeader.className = "file-history-entry-header";
|
|
847
|
+
|
|
848
|
+
var titleSpan = document.createElement("span");
|
|
849
|
+
titleSpan.className = "file-history-title";
|
|
850
|
+
|
|
851
|
+
if (item.source === "git") {
|
|
852
|
+
titleSpan.textContent = item.message || "No message";
|
|
853
|
+
} else {
|
|
854
|
+
// Use assistant's pre-edit reasoning as title (explains what Claude is doing)
|
|
855
|
+
titleSpan.textContent = item.assistantSnippet || item.toolName + " " + (currentFilePath || "").split("/").pop();
|
|
856
|
+
}
|
|
857
|
+
entryHeader.appendChild(titleSpan);
|
|
858
|
+
|
|
859
|
+
var badge = document.createElement("span");
|
|
860
|
+
badge.className = "file-history-badge";
|
|
861
|
+
if (item.source === "git") {
|
|
862
|
+
badge.classList.add("badge-commit");
|
|
863
|
+
badge.textContent = "Git Commit";
|
|
864
|
+
} else {
|
|
865
|
+
badge.textContent = item.toolName === "Write" ? "Claude Write" : "Claude Edit";
|
|
866
|
+
}
|
|
867
|
+
entryHeader.appendChild(badge);
|
|
868
|
+
|
|
869
|
+
entry.appendChild(entryHeader);
|
|
870
|
+
|
|
871
|
+
// Subtitle: code-based summary for Edit entries
|
|
872
|
+
if (item.source === "session" && item.toolName === "Edit" && (item.old_string || item.new_string)) {
|
|
873
|
+
var codeSummary = editCodeSummary(item.old_string || "", item.new_string || "");
|
|
874
|
+
if (codeSummary) {
|
|
875
|
+
var subtitleEl = document.createElement("div");
|
|
876
|
+
subtitleEl.className = "file-history-code-subtitle";
|
|
877
|
+
subtitleEl.textContent = codeSummary;
|
|
878
|
+
entry.appendChild(subtitleEl);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Meta line
|
|
883
|
+
if (item.source === "git") {
|
|
884
|
+
var sub = document.createElement("div");
|
|
885
|
+
sub.className = "file-history-meta";
|
|
886
|
+
sub.textContent = item.hash.substring(0, 7) + " by " + (item.author || "unknown") + formatTimeAgo(item.timestamp);
|
|
887
|
+
entry.appendChild(sub);
|
|
888
|
+
} else {
|
|
889
|
+
var sessionMeta = document.createElement("div");
|
|
890
|
+
sessionMeta.className = "file-history-meta";
|
|
891
|
+
var shortSession = (item.sessionTitle || "Untitled");
|
|
892
|
+
if (shortSession.length > 20) shortSession = shortSession.substring(0, 20) + "...";
|
|
893
|
+
sessionMeta.textContent = shortSession;
|
|
894
|
+
entry.appendChild(sessionMeta);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Diff preview for session edits (inline unified)
|
|
898
|
+
if (item.source === "session") {
|
|
899
|
+
var diffContainer = document.createElement("div");
|
|
900
|
+
diffContainer.className = "file-history-diff diff-compact";
|
|
901
|
+
|
|
902
|
+
if (item.toolName === "Edit" && (item.old_string || item.new_string)) {
|
|
903
|
+
var unifiedEl = renderUnifiedDiff(item.old_string || "", item.new_string || "", currentLang());
|
|
904
|
+
diffContainer.appendChild(unifiedEl);
|
|
905
|
+
} else {
|
|
906
|
+
var writeBadge = document.createElement("div");
|
|
907
|
+
writeBadge.className = "file-history-write-badge";
|
|
908
|
+
writeBadge.textContent = "Full file write";
|
|
909
|
+
diffContainer.appendChild(writeBadge);
|
|
910
|
+
}
|
|
911
|
+
entry.appendChild(diffContainer);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Action buttons row
|
|
915
|
+
var actions = document.createElement("div");
|
|
916
|
+
actions.className = "file-history-actions";
|
|
917
|
+
|
|
918
|
+
// View diff / View file button (both git and session)
|
|
919
|
+
(function (itemData) {
|
|
920
|
+
var hasEditDiff = itemData.source === "session" && itemData.toolName === "Edit" && (itemData.old_string || itemData.new_string);
|
|
921
|
+
var viewBtn = document.createElement("button");
|
|
922
|
+
viewBtn.className = "file-history-action-btn";
|
|
923
|
+
viewBtn.textContent = hasEditDiff ? "View diff" : "View file";
|
|
924
|
+
viewBtn.addEventListener("click", function (e) {
|
|
925
|
+
e.stopPropagation();
|
|
926
|
+
viewEntryFile(itemData);
|
|
927
|
+
});
|
|
928
|
+
actions.appendChild(viewBtn);
|
|
929
|
+
|
|
930
|
+
// Navigate to conversation link (session only)
|
|
931
|
+
if (itemData.source === "session" && itemData.assistantUuid && itemData.sessionLocalId) {
|
|
932
|
+
var navBtn = document.createElement("button");
|
|
933
|
+
navBtn.className = "file-history-action-btn file-history-nav-btn";
|
|
934
|
+
navBtn.textContent = "Go to chat";
|
|
935
|
+
navBtn.addEventListener("click", function (e) {
|
|
936
|
+
e.stopPropagation();
|
|
937
|
+
navigateToEdit(itemData);
|
|
938
|
+
});
|
|
939
|
+
actions.appendChild(navBtn);
|
|
940
|
+
}
|
|
941
|
+
})(item);
|
|
942
|
+
|
|
943
|
+
entry.appendChild(actions);
|
|
944
|
+
|
|
945
|
+
// Click handler: always toggle selection
|
|
946
|
+
(function (itemData, entryEl) {
|
|
947
|
+
entryEl.addEventListener("click", function () {
|
|
948
|
+
toggleSelect(itemData);
|
|
949
|
+
});
|
|
950
|
+
})(item, entry);
|
|
951
|
+
|
|
952
|
+
list.appendChild(entry);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
panel.appendChild(list);
|
|
956
|
+
bodyEl.appendChild(panel);
|
|
957
|
+
refreshIcons();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function renderCompareView() {
|
|
961
|
+
var bodyEl = document.getElementById("file-viewer-body");
|
|
962
|
+
bodyEl.innerHTML = "";
|
|
963
|
+
ctx.fileViewerEl.classList.add("file-viewer-wide");
|
|
964
|
+
|
|
965
|
+
var wrapper = document.createElement("div");
|
|
966
|
+
wrapper.className = "file-history-compare-view";
|
|
967
|
+
|
|
968
|
+
// Back button
|
|
969
|
+
var backBar = document.createElement("div");
|
|
970
|
+
backBar.className = "file-history-compare-bar";
|
|
971
|
+
|
|
972
|
+
var backBtn = document.createElement("button");
|
|
973
|
+
backBtn.className = "file-history-compare-back";
|
|
974
|
+
backBtn.textContent = "Back to timeline";
|
|
975
|
+
backBtn.addEventListener("click", function () {
|
|
976
|
+
compareMode = false;
|
|
977
|
+
ctx.fileViewerEl.classList.remove("file-viewer-wide");
|
|
978
|
+
renderHistoryPanel();
|
|
979
|
+
});
|
|
980
|
+
backBar.appendChild(backBtn);
|
|
981
|
+
wrapper.appendChild(backBar);
|
|
982
|
+
|
|
983
|
+
var a = selectedEntries[0];
|
|
984
|
+
var b = selectedEntries[1];
|
|
985
|
+
|
|
986
|
+
// Loading state while fetching
|
|
987
|
+
var loadingEl = document.createElement("div");
|
|
988
|
+
loadingEl.className = "file-history-write-badge";
|
|
989
|
+
loadingEl.textContent = "Loading...";
|
|
990
|
+
wrapper.appendChild(loadingEl);
|
|
991
|
+
bodyEl.appendChild(wrapper);
|
|
992
|
+
|
|
993
|
+
// A = "before" state of entry A, B = "after" state of entry B
|
|
994
|
+
resolveEntryContentBefore(a, function (contentA) {
|
|
995
|
+
resolveEntryContent(b, function (contentB) {
|
|
996
|
+
loadingEl.remove();
|
|
997
|
+
renderCompareDiff(wrapper, a, contentA, b, contentB);
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function resolveEntryContent(entry, cb) {
|
|
1003
|
+
if (entry.source === "git") {
|
|
1004
|
+
if (fileAtCache[entry.hash] !== undefined) {
|
|
1005
|
+
cb(fileAtCache[entry.hash]);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
pendingFileAt = function () {
|
|
1009
|
+
cb(fileAtCache[entry.hash] || "");
|
|
1010
|
+
};
|
|
1011
|
+
requestFileAt(entry.hash);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
// Session edit: reconstruct full file with the edit applied
|
|
1015
|
+
if (entry.toolName === "Edit" && entry.new_string != null && currentContent) {
|
|
1016
|
+
var pos = currentContent.indexOf(entry.new_string);
|
|
1017
|
+
if (pos >= 0 && entry.old_string != null) {
|
|
1018
|
+
// Return full file with new_string in place (current state contains it)
|
|
1019
|
+
cb(currentContent);
|
|
1020
|
+
} else {
|
|
1021
|
+
cb(currentContent || "");
|
|
1022
|
+
}
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
// Write or fallback: use current file content (best approximation)
|
|
1026
|
+
cb(currentContent || "");
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Reconstruct the full file as it was BEFORE this edit was applied
|
|
1030
|
+
function resolveEntryContentBefore(entry, cb) {
|
|
1031
|
+
if (entry.source === "git") {
|
|
1032
|
+
// For git, get the parent commit's version
|
|
1033
|
+
resolveEntryContent(entry, cb);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (entry.toolName === "Edit" && entry.new_string != null && entry.old_string != null && currentContent) {
|
|
1037
|
+
var pos = currentContent.indexOf(entry.new_string);
|
|
1038
|
+
if (pos >= 0) {
|
|
1039
|
+
cb(currentContent.substring(0, pos) + entry.old_string + currentContent.substring(pos + entry.new_string.length));
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
cb(currentContent || "");
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function renderCompareDiff(container, a, contentA, b, contentB) {
|
|
1047
|
+
var viewMode = "split";
|
|
1048
|
+
|
|
1049
|
+
function render() {
|
|
1050
|
+
// Remove previous diff content (keep back bar)
|
|
1051
|
+
var old = container.querySelector(".file-history-compare-content");
|
|
1052
|
+
if (old) old.remove();
|
|
1053
|
+
|
|
1054
|
+
var content = document.createElement("div");
|
|
1055
|
+
content.className = "file-history-compare-content";
|
|
1056
|
+
|
|
1057
|
+
// Label bar with toggle
|
|
1058
|
+
var labelBar = document.createElement("div");
|
|
1059
|
+
labelBar.className = "file-history-view-bar";
|
|
1060
|
+
|
|
1061
|
+
var labelText = document.createElement("span");
|
|
1062
|
+
labelText.className = "file-history-split-label";
|
|
1063
|
+
labelText.style.flex = "1";
|
|
1064
|
+
labelText.textContent = describeEntry(a) + " vs " + describeEntry(b);
|
|
1065
|
+
labelBar.appendChild(labelText);
|
|
1066
|
+
|
|
1067
|
+
var toggleWrap = document.createElement("div");
|
|
1068
|
+
toggleWrap.className = "file-history-view-toggle";
|
|
1069
|
+
|
|
1070
|
+
var splitBtn = document.createElement("button");
|
|
1071
|
+
splitBtn.className = "file-history-toggle-btn" + (viewMode === "split" ? " active" : "");
|
|
1072
|
+
splitBtn.textContent = "Split";
|
|
1073
|
+
splitBtn.addEventListener("click", function () {
|
|
1074
|
+
viewMode = "split";
|
|
1075
|
+
render();
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
var unifiedBtn = document.createElement("button");
|
|
1079
|
+
unifiedBtn.className = "file-history-toggle-btn" + (viewMode === "unified" ? " active" : "");
|
|
1080
|
+
unifiedBtn.textContent = "Unified";
|
|
1081
|
+
unifiedBtn.addEventListener("click", function () {
|
|
1082
|
+
viewMode = "unified";
|
|
1083
|
+
render();
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
toggleWrap.appendChild(splitBtn);
|
|
1087
|
+
toggleWrap.appendChild(unifiedBtn);
|
|
1088
|
+
labelBar.appendChild(toggleWrap);
|
|
1089
|
+
content.appendChild(labelBar);
|
|
1090
|
+
|
|
1091
|
+
// Diff content
|
|
1092
|
+
var diffWrap = document.createElement("div");
|
|
1093
|
+
diffWrap.className = "file-history-diff-full";
|
|
1094
|
+
|
|
1095
|
+
var diffLang = currentLang();
|
|
1096
|
+
if (viewMode === "split") {
|
|
1097
|
+
diffWrap.appendChild(renderSplitDiff(contentA, contentB, diffLang));
|
|
1098
|
+
} else {
|
|
1099
|
+
diffWrap.appendChild(renderUnifiedDiff(contentA, contentB, diffLang));
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
content.appendChild(diffWrap);
|
|
1103
|
+
container.appendChild(content);
|
|
1104
|
+
|
|
1105
|
+
// Scroll to first change
|
|
1106
|
+
requestAnimationFrame(function () {
|
|
1107
|
+
var firstChange = diffWrap.querySelector(".diff-row-change, .diff-row-add, .diff-row-remove");
|
|
1108
|
+
if (firstChange) {
|
|
1109
|
+
firstChange.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
render();
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function editCodeSummary(oldStr, newStr) {
|
|
1118
|
+
// Find the first meaningful added or changed line to use as a subtitle
|
|
1119
|
+
var oldLines = oldStr ? oldStr.split("\n") : [];
|
|
1120
|
+
var newLines = newStr ? newStr.split("\n") : [];
|
|
1121
|
+
var oldSet = {};
|
|
1122
|
+
for (var i = 0; i < oldLines.length; i++) {
|
|
1123
|
+
var trimmed = oldLines[i].trim();
|
|
1124
|
+
if (trimmed) oldSet[trimmed] = true;
|
|
1125
|
+
}
|
|
1126
|
+
// Find first new line not in old
|
|
1127
|
+
for (var j = 0; j < newLines.length; j++) {
|
|
1128
|
+
var line = newLines[j].trim();
|
|
1129
|
+
if (line && !oldSet[line] && line.length > 2) {
|
|
1130
|
+
if (line.length > 80) line = line.substring(0, 80) + "...";
|
|
1131
|
+
return "+ " + line;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
// Fallback: find first removed line
|
|
1135
|
+
var newSet = {};
|
|
1136
|
+
for (var k = 0; k < newLines.length; k++) {
|
|
1137
|
+
var t = newLines[k].trim();
|
|
1138
|
+
if (t) newSet[t] = true;
|
|
1139
|
+
}
|
|
1140
|
+
for (var l = 0; l < oldLines.length; l++) {
|
|
1141
|
+
var oLine = oldLines[l].trim();
|
|
1142
|
+
if (oLine && !newSet[oLine] && oLine.length > 2) {
|
|
1143
|
+
if (oLine.length > 80) oLine = oLine.substring(0, 80) + "...";
|
|
1144
|
+
return "- " + oLine;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function describeEntry(entry) {
|
|
1151
|
+
if (entry.source === "git") return entry.hash.substring(0, 7) + " " + (entry.message || "").substring(0, 40);
|
|
1152
|
+
return (entry.sessionTitle || "Untitled") + " (" + (entry.toolName || "Edit") + ")";
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function shortEntryLabel(entry) {
|
|
1156
|
+
if (entry.source === "git") {
|
|
1157
|
+
var msg = (entry.message || "").substring(0, 24);
|
|
1158
|
+
if ((entry.message || "").length > 24) msg += "...";
|
|
1159
|
+
return entry.hash.substring(0, 7) + " " + msg;
|
|
1160
|
+
}
|
|
1161
|
+
return (entry.assistantSnippet || entry.toolName || "Edit").substring(0, 30);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function formatTimeAgo(ts) {
|
|
1165
|
+
if (!ts) return "";
|
|
1166
|
+
var diff = Date.now() - ts;
|
|
1167
|
+
if (diff < 60000) return ", just now";
|
|
1168
|
+
if (diff < 3600000) return ", " + Math.floor(diff / 60000) + "m ago";
|
|
1169
|
+
if (diff < 86400000) return ", " + Math.floor(diff / 3600000) + "h ago";
|
|
1170
|
+
var d = new Date(ts);
|
|
1171
|
+
return ", " + d.toLocaleDateString();
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
function viewEntryFile(entry) {
|
|
1176
|
+
var viewerEl = ctx.fileViewerEl;
|
|
1177
|
+
var bodyEl = document.getElementById("file-viewer-body");
|
|
1178
|
+
bodyEl.innerHTML = '<div class="file-history-write-badge">Loading...</div>';
|
|
1179
|
+
|
|
1180
|
+
// Widen the viewer for diff
|
|
1181
|
+
viewerEl.classList.add("file-viewer-wide");
|
|
1182
|
+
|
|
1183
|
+
// For session edits with old/new, show diff. For git or Write, show file content.
|
|
1184
|
+
var hasEditDiff = entry.source === "session" && entry.toolName === "Edit" && (entry.old_string || entry.new_string);
|
|
1185
|
+
|
|
1186
|
+
if (hasEditDiff) {
|
|
1187
|
+
renderViewFileDiff(entry);
|
|
1188
|
+
} else {
|
|
1189
|
+
resolveEntryContent(entry, function (content) {
|
|
1190
|
+
renderViewFileContent(entry, content);
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function renderViewFileDiff(entry) {
|
|
1196
|
+
var bodyEl = document.getElementById("file-viewer-body");
|
|
1197
|
+
|
|
1198
|
+
// Reconstruct full before/after files
|
|
1199
|
+
var oldStr = entry.old_string || "";
|
|
1200
|
+
var newStr = entry.new_string || "";
|
|
1201
|
+
var fileAfter = currentContent || "";
|
|
1202
|
+
var fileBefore = fileAfter;
|
|
1203
|
+
if (newStr) {
|
|
1204
|
+
var pos = fileAfter.indexOf(newStr);
|
|
1205
|
+
if (pos >= 0) {
|
|
1206
|
+
fileBefore = fileAfter.substring(0, pos) + oldStr + fileAfter.substring(pos + newStr.length);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
var diffLang = currentLang();
|
|
1211
|
+
var viewMode = "split";
|
|
1212
|
+
|
|
1213
|
+
function render() {
|
|
1214
|
+
bodyEl.innerHTML = "";
|
|
1215
|
+
|
|
1216
|
+
// Top bar: back + toggle
|
|
1217
|
+
var topBar = document.createElement("div");
|
|
1218
|
+
topBar.className = "file-history-view-bar";
|
|
1219
|
+
|
|
1220
|
+
var backBtn = document.createElement("button");
|
|
1221
|
+
backBtn.className = "file-history-compare-back";
|
|
1222
|
+
backBtn.textContent = "Back to timeline";
|
|
1223
|
+
backBtn.addEventListener("click", function () {
|
|
1224
|
+
ctx.fileViewerEl.classList.remove("file-viewer-wide");
|
|
1225
|
+
renderHistoryPanel();
|
|
1226
|
+
});
|
|
1227
|
+
topBar.appendChild(backBtn);
|
|
1228
|
+
|
|
1229
|
+
var toggleWrap = document.createElement("div");
|
|
1230
|
+
toggleWrap.className = "file-history-view-toggle";
|
|
1231
|
+
|
|
1232
|
+
var splitBtn = document.createElement("button");
|
|
1233
|
+
splitBtn.className = "file-history-toggle-btn" + (viewMode === "split" ? " active" : "");
|
|
1234
|
+
splitBtn.textContent = "Split";
|
|
1235
|
+
splitBtn.addEventListener("click", function () {
|
|
1236
|
+
viewMode = "split";
|
|
1237
|
+
render();
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
var unifiedBtn = document.createElement("button");
|
|
1241
|
+
unifiedBtn.className = "file-history-toggle-btn" + (viewMode === "unified" ? " active" : "");
|
|
1242
|
+
unifiedBtn.textContent = "Unified";
|
|
1243
|
+
unifiedBtn.addEventListener("click", function () {
|
|
1244
|
+
viewMode = "unified";
|
|
1245
|
+
render();
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
toggleWrap.appendChild(splitBtn);
|
|
1249
|
+
toggleWrap.appendChild(unifiedBtn);
|
|
1250
|
+
topBar.appendChild(toggleWrap);
|
|
1251
|
+
bodyEl.appendChild(topBar);
|
|
1252
|
+
|
|
1253
|
+
// Label
|
|
1254
|
+
var label = document.createElement("div");
|
|
1255
|
+
label.className = "file-history-split-label";
|
|
1256
|
+
label.textContent = describeEntry(entry);
|
|
1257
|
+
bodyEl.appendChild(label);
|
|
1258
|
+
|
|
1259
|
+
var diffWrap = document.createElement("div");
|
|
1260
|
+
diffWrap.className = "file-history-diff-full";
|
|
1261
|
+
|
|
1262
|
+
if (viewMode === "split") {
|
|
1263
|
+
diffWrap.appendChild(renderSplitDiff(fileBefore, fileAfter, diffLang));
|
|
1264
|
+
} else {
|
|
1265
|
+
diffWrap.appendChild(renderUnifiedDiff(fileBefore, fileAfter, diffLang));
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
bodyEl.appendChild(diffWrap);
|
|
1269
|
+
|
|
1270
|
+
// Scroll to first change
|
|
1271
|
+
requestAnimationFrame(function () {
|
|
1272
|
+
var firstChange = diffWrap.querySelector(".diff-row-change, .diff-row-add, .diff-row-remove");
|
|
1273
|
+
if (firstChange) {
|
|
1274
|
+
firstChange.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
render();
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function renderViewFileContent(entry, content) {
|
|
1283
|
+
var bodyEl = document.getElementById("file-viewer-body");
|
|
1284
|
+
bodyEl.innerHTML = "";
|
|
1285
|
+
|
|
1286
|
+
// Back bar
|
|
1287
|
+
var topBar = document.createElement("div");
|
|
1288
|
+
topBar.className = "file-history-view-bar";
|
|
1289
|
+
var backBtn = document.createElement("button");
|
|
1290
|
+
backBtn.className = "file-history-compare-back";
|
|
1291
|
+
backBtn.textContent = "Back to timeline";
|
|
1292
|
+
backBtn.addEventListener("click", function () {
|
|
1293
|
+
ctx.fileViewerEl.classList.remove("file-viewer-wide");
|
|
1294
|
+
renderHistoryPanel();
|
|
1295
|
+
});
|
|
1296
|
+
topBar.appendChild(backBtn);
|
|
1297
|
+
bodyEl.appendChild(topBar);
|
|
1298
|
+
|
|
1299
|
+
// Label
|
|
1300
|
+
var label = document.createElement("div");
|
|
1301
|
+
label.className = "file-history-split-label";
|
|
1302
|
+
label.textContent = describeEntry(entry);
|
|
1303
|
+
bodyEl.appendChild(label);
|
|
1304
|
+
|
|
1305
|
+
// Code with line numbers
|
|
1306
|
+
var codeContainer = document.createElement("div");
|
|
1307
|
+
codeContainer.className = "file-history-split-code";
|
|
1308
|
+
codeContainer.style.flex = "1";
|
|
1309
|
+
codeContainer.style.overflow = "hidden";
|
|
1310
|
+
var ext = (currentFilePath || "").split(".").pop().toLowerCase();
|
|
1311
|
+
renderCodeWithLineNumbers(codeContainer, content, ext);
|
|
1312
|
+
bodyEl.appendChild(codeContainer);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function navigateToEdit(edit) {
|
|
1316
|
+
// If already in the same session, scroll directly without replaying history
|
|
1317
|
+
if (ctx.activeSessionId === edit.sessionLocalId) {
|
|
1318
|
+
scrollToToolElement(edit.toolId, edit.assistantUuid);
|
|
1319
|
+
if (window.innerWidth <= 768) closeFileViewer();
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
pendingNavigate = {
|
|
1324
|
+
sessionLocalId: edit.sessionLocalId,
|
|
1325
|
+
assistantUuid: edit.assistantUuid,
|
|
1326
|
+
toolId: edit.toolId,
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
if (ctx.ws && ctx.connected) {
|
|
1330
|
+
ctx.ws.send(JSON.stringify({ type: "switch_session", id: edit.sessionLocalId }));
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Close file viewer on mobile
|
|
1334
|
+
if (window.innerWidth <= 768) {
|
|
1335
|
+
closeFileViewer();
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function scrollToToolElement(toolId, assistantUuid) {
|
|
1340
|
+
requestAnimationFrame(function () {
|
|
1341
|
+
var target = toolId ? ctx.messagesEl.querySelector('[data-tool-id="' + toolId + '"]') : null;
|
|
1342
|
+
if (!target && assistantUuid) {
|
|
1343
|
+
target = ctx.messagesEl.querySelector('[data-uuid="' + assistantUuid + '"]');
|
|
1344
|
+
}
|
|
1345
|
+
if (target) {
|
|
1346
|
+
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1347
|
+
target.classList.add("message-blink");
|
|
1348
|
+
setTimeout(function () { target.classList.remove("message-blink"); }, 2000);
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
export function getPendingNavigate() {
|
|
1354
|
+
var nav = pendingNavigate;
|
|
1355
|
+
pendingNavigate = null;
|
|
1356
|
+
return nav;
|
|
1357
|
+
}
|