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.
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/bin/cli.js +2385 -0
- package/lib/cli-sessions.js +270 -0
- package/lib/config.js +237 -0
- package/lib/daemon.js +489 -0
- package/lib/ipc.js +112 -0
- package/lib/notes.js +120 -0
- package/lib/pages.js +664 -0
- package/lib/project.js +1433 -0
- package/lib/public/app.js +2795 -0
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +264 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +1114 -0
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/icon-strip.css +296 -0
- package/lib/public/css/input.css +573 -0
- package/lib/public/css/menus.css +856 -0
- package/lib/public/css/messages.css +1445 -0
- package/lib/public/css/mobile-nav.css +354 -0
- package/lib/public/css/overlays.css +697 -0
- package/lib/public/css/rewind.css +505 -0
- package/lib/public/css/server-settings.css +761 -0
- package/lib/public/css/sidebar.css +936 -0
- package/lib/public/css/sticky-notes.css +358 -0
- package/lib/public/css/title-bar.css +314 -0
- package/lib/public/favicon-dark.svg +1 -0
- package/lib/public/favicon.svg +1 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-mono.svg +1 -0
- package/lib/public/index.html +762 -0
- package/lib/public/manifest.json +27 -0
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/events.js +21 -0
- package/lib/public/modules/filebrowser.js +1411 -0
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/icons.js +54 -0
- package/lib/public/modules/input.js +584 -0
- package/lib/public/modules/markdown.js +356 -0
- package/lib/public/modules/notifications.js +649 -0
- package/lib/public/modules/qrcode.js +70 -0
- package/lib/public/modules/rewind.js +345 -0
- package/lib/public/modules/server-settings.js +510 -0
- package/lib/public/modules/sidebar.js +1083 -0
- package/lib/public/modules/state.js +3 -0
- package/lib/public/modules/sticky-notes.js +688 -0
- package/lib/public/modules/terminal.js +697 -0
- package/lib/public/modules/theme.js +738 -0
- package/lib/public/modules/tools.js +1608 -0
- package/lib/public/modules/utils.js +56 -0
- package/lib/public/style.css +15 -0
- package/lib/public/sw.js +75 -0
- package/lib/push.js +124 -0
- package/lib/sdk-bridge.js +989 -0
- package/lib/server.js +582 -0
- package/lib/sessions.js +424 -0
- package/lib/terminal-manager.js +187 -0
- package/lib/terminal.js +24 -0
- package/lib/themes/ayu-light.json +9 -0
- package/lib/themes/catppuccin-latte.json +9 -0
- package/lib/themes/catppuccin-mocha.json +9 -0
- package/lib/themes/clay-light.json +10 -0
- package/lib/themes/clay.json +10 -0
- package/lib/themes/dracula.json +9 -0
- package/lib/themes/everforest-light.json +9 -0
- package/lib/themes/everforest.json +9 -0
- package/lib/themes/github-light.json +9 -0
- package/lib/themes/gruvbox-dark.json +9 -0
- package/lib/themes/gruvbox-light.json +9 -0
- package/lib/themes/monokai.json +9 -0
- package/lib/themes/nord-light.json +9 -0
- package/lib/themes/nord.json +9 -0
- package/lib/themes/one-dark.json +9 -0
- package/lib/themes/one-light.json +9 -0
- package/lib/themes/rose-pine-dawn.json +9 -0
- package/lib/themes/rose-pine.json +9 -0
- package/lib/themes/solarized-dark.json +9 -0
- package/lib/themes/solarized-light.json +9 -0
- package/lib/themes/tokyo-night-light.json +9 -0
- package/lib/themes/tokyo-night.json +9 -0
- package/lib/updater.js +97 -0
- 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
|
+
}
|