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.
@@ -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
- var pre = document.createElement("pre");
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
+ }