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,70 @@
|
|
|
1
|
+
import { copyToClipboard } from './utils.js';
|
|
2
|
+
|
|
3
|
+
function getShareUrl() {
|
|
4
|
+
var url = window.location.href;
|
|
5
|
+
var h = window.location.hostname;
|
|
6
|
+
if ((h === "localhost" || h === "127.0.0.1") && window.__lanHost) {
|
|
7
|
+
url = url.replace(h + ":" + window.location.port, window.__lanHost);
|
|
8
|
+
}
|
|
9
|
+
return url;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function initQrCode() {
|
|
13
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
14
|
+
var qrBtn = $("qr-btn");
|
|
15
|
+
var qrOverlay = $("qr-overlay");
|
|
16
|
+
var qrCanvas = $("qr-canvas");
|
|
17
|
+
var qrUrl = $("qr-url");
|
|
18
|
+
|
|
19
|
+
qrBtn.addEventListener("click", function (e) {
|
|
20
|
+
e.stopPropagation();
|
|
21
|
+
var url = getShareUrl();
|
|
22
|
+
|
|
23
|
+
// Use Web Share API if available
|
|
24
|
+
if (navigator.share) {
|
|
25
|
+
navigator.share({ title: document.title || "Clay", url: url }).catch(function () {});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Fallback: show QR overlay
|
|
30
|
+
var qr = qrcode(0, "M");
|
|
31
|
+
qr.addData(url);
|
|
32
|
+
qr.make();
|
|
33
|
+
qrCanvas.innerHTML = qr.createSvgTag(5, 0);
|
|
34
|
+
qrUrl.innerHTML = url + '<span class="qr-hint">click to copy</span>';
|
|
35
|
+
|
|
36
|
+
qrOverlay.classList.remove("hidden");
|
|
37
|
+
qrBtn.classList.add("active");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// click URL to copy
|
|
41
|
+
qrUrl.addEventListener("click", function () {
|
|
42
|
+
var url = getShareUrl();
|
|
43
|
+
copyToClipboard(url).then(function () {
|
|
44
|
+
qrUrl.innerHTML = "Copied!";
|
|
45
|
+
qrUrl.classList.add("copied");
|
|
46
|
+
setTimeout(function () {
|
|
47
|
+
qrUrl.innerHTML = url + '<span class="qr-hint">click to copy</span>';
|
|
48
|
+
qrUrl.classList.remove("copied");
|
|
49
|
+
}, 1500);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
qrOverlay.addEventListener("click", function () {
|
|
54
|
+
qrOverlay.classList.add("hidden");
|
|
55
|
+
qrBtn.classList.remove("active");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// prevent closing when clicking the inner card
|
|
59
|
+
$("qr-overlay-inner").addEventListener("click", function (e) {
|
|
60
|
+
e.stopPropagation();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ESC to close
|
|
64
|
+
document.addEventListener("keydown", function (e) {
|
|
65
|
+
if (e.key === "Escape" && !qrOverlay.classList.contains("hidden")) {
|
|
66
|
+
qrOverlay.classList.add("hidden");
|
|
67
|
+
qrBtn.classList.remove("active");
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { escapeHtml } from './utils.js';
|
|
2
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
|
+
|
|
4
|
+
var ctx;
|
|
5
|
+
var rewindMode = false;
|
|
6
|
+
var pendingRewindUuid = null;
|
|
7
|
+
var rewindBannerEl = null;
|
|
8
|
+
var rewindScrollHandler = null;
|
|
9
|
+
var rewindModal, rewindSummary, rewindFilesList, rewindConfirmBtn, rewindCancelBtn, rewindModeOptions;
|
|
10
|
+
var cachedPreview = null;
|
|
11
|
+
|
|
12
|
+
export function setRewindMode(on) {
|
|
13
|
+
rewindMode = on;
|
|
14
|
+
var appEl = ctx.$("app");
|
|
15
|
+
if (on) {
|
|
16
|
+
appEl.classList.add("rewind-mode");
|
|
17
|
+
if (!rewindBannerEl) {
|
|
18
|
+
rewindBannerEl = document.createElement("div");
|
|
19
|
+
rewindBannerEl.className = "rewind-mode-banner";
|
|
20
|
+
rewindBannerEl.innerHTML =
|
|
21
|
+
'<i data-lucide="rotate-ccw"></i>' +
|
|
22
|
+
'<span class="rewind-banner-text">Select a message to rewind to</span>' +
|
|
23
|
+
'<button class="rewind-exit-btn" title="Exit rewind mode"><i data-lucide="x"></i></button>';
|
|
24
|
+
rewindBannerEl.querySelector(".rewind-exit-btn").addEventListener("click", function() {
|
|
25
|
+
setRewindMode(false);
|
|
26
|
+
});
|
|
27
|
+
ctx.$("app").appendChild(rewindBannerEl);
|
|
28
|
+
refreshIcons();
|
|
29
|
+
}
|
|
30
|
+
buildRewindTimeline();
|
|
31
|
+
} else {
|
|
32
|
+
appEl.classList.remove("rewind-mode");
|
|
33
|
+
if (rewindBannerEl) {
|
|
34
|
+
rewindBannerEl.remove();
|
|
35
|
+
rewindBannerEl = null;
|
|
36
|
+
}
|
|
37
|
+
removeRewindTimeline();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isRewindMode() {
|
|
42
|
+
return rewindMode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getPendingRewindUuid() {
|
|
46
|
+
return pendingRewindUuid;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function clearPendingRewindUuid() {
|
|
50
|
+
pendingRewindUuid = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function initiateRewind(uuid) {
|
|
54
|
+
if (ctx.processing) {
|
|
55
|
+
ctx.addSystemMessage("Cannot rewind while processing. Stop the current operation first.", true);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!uuid) {
|
|
59
|
+
ctx.addSystemMessage("No rewind point found for this turn.", true);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
pendingRewindUuid = uuid;
|
|
63
|
+
if (ctx.ws && ctx.connected) {
|
|
64
|
+
ctx.ws.send(JSON.stringify({ type: "rewind_preview", uuid: uuid }));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getSelectedMode() {
|
|
69
|
+
if (!rewindModeOptions) return "both";
|
|
70
|
+
var checked = rewindModeOptions.querySelector('input[name="rewind-mode"]:checked');
|
|
71
|
+
return checked ? checked.value : "both";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function updateSummaryForMode() {
|
|
75
|
+
if (!cachedPreview) return;
|
|
76
|
+
var mode = getSelectedMode();
|
|
77
|
+
var fileCount = cachedPreview.fileCount;
|
|
78
|
+
var insertions = cachedPreview.insertions;
|
|
79
|
+
var deletions = cachedPreview.deletions;
|
|
80
|
+
|
|
81
|
+
if (mode === "chat") {
|
|
82
|
+
rewindSummary.textContent = "Conversation will be rewound. Files will not be changed.";
|
|
83
|
+
rewindFilesList.style.display = "none";
|
|
84
|
+
} else if (fileCount > 0) {
|
|
85
|
+
var summary = fileCount + " file" + (fileCount !== 1 ? "s" : "") + " will be restored.";
|
|
86
|
+
if (insertions || deletions) summary += " (+" + insertions + " / -" + deletions + " lines)";
|
|
87
|
+
if (mode === "files") summary += " Conversation will not be changed.";
|
|
88
|
+
rewindSummary.textContent = summary;
|
|
89
|
+
rewindFilesList.style.display = "";
|
|
90
|
+
} else {
|
|
91
|
+
if (mode === "files") {
|
|
92
|
+
rewindSummary.textContent = "No file changes to restore.";
|
|
93
|
+
} else {
|
|
94
|
+
rewindSummary.textContent = "No file changes to restore. Conversation will be rewound.";
|
|
95
|
+
}
|
|
96
|
+
rewindFilesList.style.display = "none";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function showRewindModal(data) {
|
|
101
|
+
var p = data.preview || data;
|
|
102
|
+
var filePaths = p.filesChanged || p.filePaths || p.files || [];
|
|
103
|
+
var fileCount = filePaths.length;
|
|
104
|
+
var insertions = p.insertions || 0;
|
|
105
|
+
var deletions = p.deletions || 0;
|
|
106
|
+
|
|
107
|
+
cachedPreview = { fileCount: fileCount, insertions: insertions, deletions: deletions };
|
|
108
|
+
|
|
109
|
+
// Reset radio to default
|
|
110
|
+
if (rewindModeOptions) {
|
|
111
|
+
var defaultRadio = rewindModeOptions.querySelector('input[value="both"]');
|
|
112
|
+
if (defaultRadio) defaultRadio.checked = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (fileCount > 0) {
|
|
116
|
+
var summary = fileCount + " file" + (fileCount !== 1 ? "s" : "") + " will be restored.";
|
|
117
|
+
if (insertions || deletions) summary += " (+" + insertions + " / -" + deletions + " lines)";
|
|
118
|
+
rewindSummary.textContent = summary;
|
|
119
|
+
} else {
|
|
120
|
+
rewindSummary.textContent = "No file changes to restore. Conversation will be rewound.";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
var diffs = data.diffs || {};
|
|
124
|
+
rewindFilesList.innerHTML = "";
|
|
125
|
+
if (filePaths.length > 0) {
|
|
126
|
+
rewindFilesList.style.display = "";
|
|
127
|
+
for (var i = 0; i < filePaths.length; i++) {
|
|
128
|
+
var fp = filePaths[i];
|
|
129
|
+
var section = document.createElement("div");
|
|
130
|
+
section.className = "rewind-file-section expanded";
|
|
131
|
+
|
|
132
|
+
var header = document.createElement("div");
|
|
133
|
+
header.className = "rewind-file-header";
|
|
134
|
+
header.innerHTML = '<span class="rewind-file-chevron"><i data-lucide="chevron-right"></i></span>';
|
|
135
|
+
|
|
136
|
+
var pathSpan = document.createElement("span");
|
|
137
|
+
pathSpan.className = "rewind-file-path";
|
|
138
|
+
pathSpan.textContent = fp;
|
|
139
|
+
pathSpan.title = fp;
|
|
140
|
+
header.appendChild(pathSpan);
|
|
141
|
+
|
|
142
|
+
header.addEventListener("click", function(sec) {
|
|
143
|
+
return function() { sec.classList.toggle("expanded"); };
|
|
144
|
+
}(section));
|
|
145
|
+
|
|
146
|
+
section.appendChild(header);
|
|
147
|
+
|
|
148
|
+
var diffContainer = document.createElement("div");
|
|
149
|
+
diffContainer.className = "rewind-file-diff";
|
|
150
|
+
var diffText = diffs[fp];
|
|
151
|
+
if (diffText) {
|
|
152
|
+
diffContainer.appendChild(renderDiffPre(diffText));
|
|
153
|
+
} else {
|
|
154
|
+
var noDiff = document.createElement("div");
|
|
155
|
+
noDiff.className = "rewind-no-diff";
|
|
156
|
+
noDiff.textContent = "No diff available (file may be untracked)";
|
|
157
|
+
diffContainer.appendChild(noDiff);
|
|
158
|
+
}
|
|
159
|
+
section.appendChild(diffContainer);
|
|
160
|
+
|
|
161
|
+
rewindFilesList.appendChild(section);
|
|
162
|
+
}
|
|
163
|
+
refreshIcons();
|
|
164
|
+
} else {
|
|
165
|
+
rewindFilesList.style.display = "none";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
rewindModal.classList.remove("hidden");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function hideRewindModal() {
|
|
172
|
+
rewindModal.classList.add("hidden");
|
|
173
|
+
pendingRewindUuid = null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function renderDiffPre(text) {
|
|
177
|
+
var pre = document.createElement("pre");
|
|
178
|
+
pre.className = "diff-content";
|
|
179
|
+
var lines = text.split("\n");
|
|
180
|
+
for (var i = 0; i < lines.length; i++) {
|
|
181
|
+
var line = lines[i];
|
|
182
|
+
var span = document.createElement("span");
|
|
183
|
+
if (line.startsWith("@@")) {
|
|
184
|
+
span.className = "diff-hunk";
|
|
185
|
+
} else if (line.startsWith("---") || line.startsWith("+++")) {
|
|
186
|
+
span.className = "diff-file-header";
|
|
187
|
+
} else if (line.startsWith("+")) {
|
|
188
|
+
span.className = "diff-add";
|
|
189
|
+
} else if (line.startsWith("-")) {
|
|
190
|
+
span.className = "diff-del";
|
|
191
|
+
} else {
|
|
192
|
+
span.className = "diff-ctx";
|
|
193
|
+
}
|
|
194
|
+
span.textContent = line;
|
|
195
|
+
pre.appendChild(span);
|
|
196
|
+
if (i < lines.length - 1) pre.appendChild(document.createTextNode("\n"));
|
|
197
|
+
}
|
|
198
|
+
return pre;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- Rewind timeline ---
|
|
202
|
+
function buildRewindTimeline() {
|
|
203
|
+
removeRewindTimeline();
|
|
204
|
+
|
|
205
|
+
var userMsgs = ctx.messagesEl.querySelectorAll(".msg-user[data-uuid]");
|
|
206
|
+
if (userMsgs.length === 0) return;
|
|
207
|
+
|
|
208
|
+
var timeline = document.createElement("div");
|
|
209
|
+
timeline.className = "rewind-timeline";
|
|
210
|
+
timeline.id = "rewind-timeline";
|
|
211
|
+
|
|
212
|
+
var track = document.createElement("div");
|
|
213
|
+
track.className = "rewind-timeline-track";
|
|
214
|
+
|
|
215
|
+
var viewport = document.createElement("div");
|
|
216
|
+
viewport.className = "rewind-timeline-viewport";
|
|
217
|
+
track.appendChild(viewport);
|
|
218
|
+
|
|
219
|
+
for (var i = 0; i < userMsgs.length; i++) {
|
|
220
|
+
var msg = userMsgs[i];
|
|
221
|
+
var pct = userMsgs.length === 1 ? 50 : 6 + (i / (userMsgs.length - 1)) * 88;
|
|
222
|
+
|
|
223
|
+
var bubble = msg.querySelector(".bubble");
|
|
224
|
+
var msgText = bubble ? bubble.textContent.trim() : "";
|
|
225
|
+
if (msgText.length > 18) msgText = msgText.substring(0, 18) + "\u2026";
|
|
226
|
+
|
|
227
|
+
var marker = document.createElement("div");
|
|
228
|
+
marker.className = "rewind-timeline-marker";
|
|
229
|
+
marker.innerHTML = '<i data-lucide="message-square"></i><span class="marker-text">' + escapeHtml(msgText) + '</span>';
|
|
230
|
+
marker.style.top = pct + "%";
|
|
231
|
+
marker.dataset.uuid = msg.dataset.uuid;
|
|
232
|
+
marker.dataset.offsetTop = msg.offsetTop;
|
|
233
|
+
|
|
234
|
+
(function(targetMsg, markerEl) {
|
|
235
|
+
markerEl.addEventListener("click", function() {
|
|
236
|
+
targetMsg.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
237
|
+
});
|
|
238
|
+
})(msg, marker);
|
|
239
|
+
|
|
240
|
+
track.appendChild(marker);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
timeline.appendChild(track);
|
|
244
|
+
|
|
245
|
+
// Position timeline to align with messages area
|
|
246
|
+
var appEl = ctx.$("app");
|
|
247
|
+
var titleBarEl = document.querySelector(".title-bar-content");
|
|
248
|
+
var inputAreaEl = ctx.$("input-area");
|
|
249
|
+
var appRect = appEl.getBoundingClientRect();
|
|
250
|
+
var titleBarRect = titleBarEl ? titleBarEl.getBoundingClientRect() : { bottom: appRect.top };
|
|
251
|
+
var inputRect = inputAreaEl.getBoundingClientRect();
|
|
252
|
+
|
|
253
|
+
timeline.style.top = "4px";
|
|
254
|
+
timeline.style.bottom = (appRect.bottom - inputRect.top + 4) + "px";
|
|
255
|
+
|
|
256
|
+
appEl.appendChild(timeline);
|
|
257
|
+
refreshIcons();
|
|
258
|
+
|
|
259
|
+
rewindScrollHandler = function() { updateTimelineViewport(track, viewport); };
|
|
260
|
+
ctx.messagesEl.addEventListener("scroll", rewindScrollHandler);
|
|
261
|
+
updateTimelineViewport(track, viewport);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function updateTimelineViewport(track, viewport) {
|
|
265
|
+
if (!track) return;
|
|
266
|
+
var scrollH = ctx.messagesEl.scrollHeight;
|
|
267
|
+
var viewH = ctx.messagesEl.clientHeight;
|
|
268
|
+
if (scrollH <= viewH) {
|
|
269
|
+
viewport.style.top = "0";
|
|
270
|
+
viewport.style.height = "100%";
|
|
271
|
+
} else {
|
|
272
|
+
var viewTop = ctx.messagesEl.scrollTop / scrollH;
|
|
273
|
+
var viewBot = (ctx.messagesEl.scrollTop + viewH) / scrollH;
|
|
274
|
+
viewport.style.top = (viewTop * 100) + "%";
|
|
275
|
+
viewport.style.height = ((viewBot - viewTop) * 100) + "%";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
var markers = track.querySelectorAll(".rewind-timeline-marker");
|
|
279
|
+
var vTop = ctx.messagesEl.scrollTop;
|
|
280
|
+
var vBot = vTop + viewH;
|
|
281
|
+
|
|
282
|
+
for (var i = 0; i < markers.length; i++) {
|
|
283
|
+
var msgTop = parseInt(markers[i].dataset.offsetTop, 10);
|
|
284
|
+
if (msgTop >= vTop && msgTop <= vBot) {
|
|
285
|
+
markers[i].classList.add("in-view");
|
|
286
|
+
} else {
|
|
287
|
+
markers[i].classList.remove("in-view");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function removeRewindTimeline() {
|
|
293
|
+
var existing = document.getElementById("rewind-timeline");
|
|
294
|
+
if (existing) existing.remove();
|
|
295
|
+
if (rewindScrollHandler) {
|
|
296
|
+
ctx.messagesEl.removeEventListener("scroll", rewindScrollHandler);
|
|
297
|
+
rewindScrollHandler = null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function addRewindButton(msgEl) {
|
|
302
|
+
if (msgEl.querySelector(".msg-user-rewind-btn")) return;
|
|
303
|
+
var btn = document.createElement("button");
|
|
304
|
+
btn.className = "msg-user-rewind-btn";
|
|
305
|
+
btn.type = "button";
|
|
306
|
+
btn.title = "Rewind to here";
|
|
307
|
+
btn.innerHTML = iconHtml("rotate-ccw");
|
|
308
|
+
msgEl.appendChild(btn);
|
|
309
|
+
refreshIcons();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function initRewind(_ctx) {
|
|
313
|
+
ctx = _ctx;
|
|
314
|
+
|
|
315
|
+
rewindModal = ctx.$("rewind-modal");
|
|
316
|
+
rewindSummary = ctx.$("rewind-summary");
|
|
317
|
+
rewindFilesList = ctx.$("rewind-files-list");
|
|
318
|
+
rewindConfirmBtn = ctx.$("rewind-confirm");
|
|
319
|
+
rewindCancelBtn = ctx.$("rewind-cancel");
|
|
320
|
+
rewindModeOptions = ctx.$("rewind-mode-options");
|
|
321
|
+
|
|
322
|
+
// Update summary when rewind mode radio changes
|
|
323
|
+
if (rewindModeOptions) {
|
|
324
|
+
rewindModeOptions.addEventListener("change", updateSummaryForMode);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Click on rewind icon to rewind
|
|
328
|
+
ctx.messagesEl.addEventListener("click", function(e) {
|
|
329
|
+
var btn = e.target.closest(".msg-user-rewind-btn");
|
|
330
|
+
if (!btn) return;
|
|
331
|
+
var msgEl = btn.closest(".msg-user[data-uuid]");
|
|
332
|
+
if (msgEl) initiateRewind(msgEl.dataset.uuid);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
rewindConfirmBtn.addEventListener("click", function() {
|
|
336
|
+
if (pendingRewindUuid && ctx.ws && ctx.connected) {
|
|
337
|
+
var mode = getSelectedMode();
|
|
338
|
+
ctx.ws.send(JSON.stringify({ type: "rewind_execute", uuid: pendingRewindUuid, mode: mode }));
|
|
339
|
+
}
|
|
340
|
+
hideRewindModal();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
rewindCancelBtn.addEventListener("click", hideRewindModal);
|
|
344
|
+
rewindModal.querySelector(".confirm-backdrop").addEventListener("click", hideRewindModal);
|
|
345
|
+
}
|