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