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