clay-server 2.26.0-beta.1 → 2.26.0-beta.10

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 (42) hide show
  1. package/bin/cli.js +5 -9
  2. package/lib/browser-mcp-server.js +496 -0
  3. package/lib/daemon.js +1 -1
  4. package/lib/os-users.js +23 -0
  5. package/lib/project-debate.js +206 -88
  6. package/lib/project-mate-interaction.js +766 -0
  7. package/lib/project-memory.js +677 -0
  8. package/lib/project.js +546 -1361
  9. package/lib/public/app.js +817 -175
  10. package/lib/public/css/debate.css +224 -2
  11. package/lib/public/css/icon-strip.css +10 -10
  12. package/lib/public/css/input.css +263 -83
  13. package/lib/public/css/mates.css +56 -57
  14. package/lib/public/css/mention.css +7 -4
  15. package/lib/public/css/menus.css +7 -0
  16. package/lib/public/css/messages.css +17 -0
  17. package/lib/public/css/mobile-nav.css +3 -1
  18. package/lib/public/css/overlays.css +181 -0
  19. package/lib/public/css/rewind.css +79 -0
  20. package/lib/public/css/server-settings.css +1 -0
  21. package/lib/public/css/sidebar.css +10 -0
  22. package/lib/public/css/title-bar.css +189 -3
  23. package/lib/public/index.html +53 -16
  24. package/lib/public/modules/context-sources.js +313 -0
  25. package/lib/public/modules/debate.js +184 -97
  26. package/lib/public/modules/input.js +18 -1
  27. package/lib/public/modules/mate-knowledge.js +11 -11
  28. package/lib/public/modules/mate-memory.js +5 -5
  29. package/lib/public/modules/mate-sidebar.js +13 -9
  30. package/lib/public/modules/mention.js +40 -2
  31. package/lib/public/modules/notifications.js +109 -1
  32. package/lib/public/modules/rewind.js +36 -0
  33. package/lib/public/modules/sidebar.js +107 -28
  34. package/lib/public/modules/terminal.js +8 -0
  35. package/lib/public/modules/theme.js +2 -1
  36. package/lib/public/modules/tools.js +69 -24
  37. package/lib/sdk-bridge.js +81 -7
  38. package/lib/sdk-worker.js +13 -1
  39. package/lib/server.js +42 -0
  40. package/lib/sessions.js +39 -7
  41. package/lib/terminal-manager.js +36 -6
  42. package/package.json +2 -2
@@ -71,6 +71,21 @@ export var builtinCommands = [
71
71
 
72
72
  // --- Send ---
73
73
  export function sendMessage() {
74
+ // Debate ended mode intercept: route to resume handler
75
+ if (ctx.isDebateEndedMode && ctx.isDebateEndedMode() && ctx.handleDebateEndedSend) {
76
+ ctx.handleDebateEndedSend();
77
+ return;
78
+ }
79
+ // Debate conclude mode intercept: route to conclude handler
80
+ if (ctx.isDebateConcludeMode && ctx.isDebateConcludeMode() && ctx.handleDebateConcludeSend) {
81
+ ctx.handleDebateConcludeSend();
82
+ return;
83
+ }
84
+ // Debate floor mode intercept: route to floor response handler
85
+ if (ctx.isDebateFloorMode && ctx.isDebateFloorMode() && ctx.handleDebateFloorSend) {
86
+ ctx.handleDebateFloorSend();
87
+ return;
88
+ }
74
89
  // DM mode intercept: if in DM mode, route to DM handler instead
75
90
  if (ctx.isDmMode && ctx.isDmMode() && ctx.handleDmSend) {
76
91
  ctx.handleDmSend();
@@ -193,9 +208,11 @@ export function sendMessage() {
193
208
  }
194
209
  ctx.ws.send(JSON.stringify(payload));
195
210
 
196
- // Mate DM: show pre-thinking dots after a short delay (before server responds)
211
+ // Show pre-thinking dots before server responds
197
212
  if (ctx.isMateDm && ctx.isMateDm()) {
198
213
  ctx.showMatePreThinking();
214
+ } else if (ctx.showClaudePreThinking) {
215
+ ctx.showClaudePreThinking();
199
216
  }
200
217
 
201
218
  ctx.inputEl.value = "";
@@ -3,7 +3,7 @@ import { hideNotes } from './sticky-notes.js';
3
3
  import { hideMemory } from './mate-memory.js';
4
4
  import { renderMarkdown, highlightCodeBlocks } from './markdown.js';
5
5
 
6
- var getMateWs = null;
6
+ var getWs = null;
7
7
  var filesEl = null;
8
8
  var sidebarBtn = null;
9
9
  var countBadge = null;
@@ -46,8 +46,8 @@ var dirty = false;
46
46
  var mode = "none"; // "none" | "viewer" | "editor"
47
47
  var pendingEditMode = false;
48
48
 
49
- export function initMateKnowledge(mateWsGetter) {
50
- getMateWs = mateWsGetter;
49
+ export function initMateKnowledge(wsGetter) {
50
+ getWs = wsGetter;
51
51
  filesEl = document.getElementById("mate-knowledge-files");
52
52
  sidebarBtn = document.getElementById("mate-knowledge-btn");
53
53
  countBadge = document.getElementById("mate-knowledge-count");
@@ -123,7 +123,7 @@ export function initMateKnowledge(mateWsGetter) {
123
123
  if (editorDeleteBtn) {
124
124
  editorDeleteBtn.addEventListener("click", function () {
125
125
  if (editingFile) {
126
- var ws = getMateWs ? getMateWs() : null;
126
+ var ws = getWs ? getWs() : null;
127
127
  if (ws && ws.readyState === 1) {
128
128
  ws.send(JSON.stringify({ type: "knowledge_delete", name: editingFile }));
129
129
  }
@@ -317,7 +317,7 @@ export function isKnowledgeVisible() {
317
317
  }
318
318
 
319
319
  export function requestKnowledgeList() {
320
- var ws = getMateWs ? getMateWs() : null;
320
+ var ws = getWs ? getWs() : null;
321
321
  if (ws && ws.readyState === 1) {
322
322
  ws.send(JSON.stringify({ type: "knowledge_list" }));
323
323
  }
@@ -457,7 +457,7 @@ function renderFileItem(file) {
457
457
 
458
458
  item.addEventListener("click", (function (f) {
459
459
  return function () {
460
- var ws = getMateWs ? getMateWs() : null;
460
+ var ws = getWs ? getWs() : null;
461
461
  if (ws && ws.readyState === 1) {
462
462
  var msg = { type: "knowledge_read", name: f.name };
463
463
  if (f.common) {
@@ -554,7 +554,7 @@ function saveKnowledge() {
554
554
  setTimeout(function () { editorNameEl.style.outline = ""; }, 1500);
555
555
  return;
556
556
  }
557
- var ws = getMateWs ? getMateWs() : null;
557
+ var ws = getWs ? getWs() : null;
558
558
  if (ws && ws.readyState === 1) {
559
559
  ws.send(JSON.stringify({ type: "knowledge_save", name: name, content: content }));
560
560
  }
@@ -720,7 +720,7 @@ function showFileMenu(file, anchorBtn) {
720
720
  // Request file content, then open editor directly
721
721
  editingCommon = false;
722
722
  editingFile = file.name;
723
- var ws = getMateWs ? getMateWs() : null;
723
+ var ws = getWs ? getWs() : null;
724
724
  if (ws && ws.readyState === 1) {
725
725
  ws.send(JSON.stringify({ type: "knowledge_read", name: file.name }));
726
726
  // Override handleKnowledgeContent to go straight to editor
@@ -736,7 +736,7 @@ function showFileMenu(file, anchorBtn) {
736
736
  promoteItem.innerHTML = iconHtml("share-2") + "<span>Share to all mates</span>";
737
737
  promoteItem.addEventListener("click", function (e) {
738
738
  e.stopPropagation();
739
- var ws = getMateWs ? getMateWs() : null;
739
+ var ws = getWs ? getWs() : null;
740
740
  if (ws && ws.readyState === 1) {
741
741
  ws.send(JSON.stringify({ type: "knowledge_promote", name: file.name }));
742
742
  }
@@ -749,7 +749,7 @@ function showFileMenu(file, anchorBtn) {
749
749
  depromoteItem.innerHTML = iconHtml("x-circle") + "<span>Unshare</span>";
750
750
  depromoteItem.addEventListener("click", function (e) {
751
751
  e.stopPropagation();
752
- var ws = getMateWs ? getMateWs() : null;
752
+ var ws = getWs ? getWs() : null;
753
753
  if (ws && ws.readyState === 1) {
754
754
  ws.send(JSON.stringify({ type: "knowledge_depromote", name: file.name }));
755
755
  }
@@ -770,7 +770,7 @@ function showFileMenu(file, anchorBtn) {
770
770
  deleteItem.innerHTML = iconHtml("trash-2") + "<span>Delete</span>";
771
771
  deleteItem.addEventListener("click", function (e) {
772
772
  e.stopPropagation();
773
- var ws = getMateWs ? getMateWs() : null;
773
+ var ws = getWs ? getWs() : null;
774
774
  if (ws && ws.readyState === 1) {
775
775
  ws.send(JSON.stringify({ type: "knowledge_delete", name: file.name }));
776
776
  }
@@ -1,7 +1,7 @@
1
1
  import { iconHtml, refreshIcons } from './icons.js';
2
2
  import { escapeHtml } from './utils.js';
3
3
 
4
- var getMateWs = null;
4
+ var getWs = null;
5
5
  var visible = false;
6
6
  var cachedEntries = [];
7
7
  var cachedSummary = "";
@@ -24,8 +24,8 @@ var confirmOverlay = null;
24
24
 
25
25
  var _onShow = null;
26
26
 
27
- export function initMateMemory(mateWsGetter, opts) {
28
- getMateWs = mateWsGetter;
27
+ export function initMateMemory(wsGetter, opts) {
28
+ getWs = wsGetter;
29
29
  if (opts && opts.onShow) _onShow = opts.onShow;
30
30
 
31
31
  sidebarBtn = document.getElementById("mate-memory-btn");
@@ -99,7 +99,7 @@ export function isMemoryVisible() {
99
99
  }
100
100
 
101
101
  function requestMemoryList() {
102
- var ws = getMateWs ? getMateWs() : null;
102
+ var ws = getWs ? getWs() : null;
103
103
  if (ws && ws.readyState === 1) {
104
104
  ws.send(JSON.stringify({ type: "memory_list" }));
105
105
  }
@@ -325,7 +325,7 @@ function confirmDelete(index) {
325
325
  deleteBtn.className = "mate-memory-confirm-delete";
326
326
  deleteBtn.textContent = "Delete";
327
327
  deleteBtn.addEventListener("click", function () {
328
- var ws = getMateWs ? getMateWs() : null;
328
+ var ws = getWs ? getWs() : null;
329
329
  if (ws && ws.readyState === 1) {
330
330
  ws.send(JSON.stringify({ type: "memory_delete", index: index }));
331
331
  }
@@ -6,7 +6,7 @@ import { isSchedulerOpen, closeScheduler } from './scheduler.js';
6
6
  import { hideNotes } from './sticky-notes.js';
7
7
  import { openSearch as openSessionSearch } from './session-search.js';
8
8
 
9
- var getMateWs = null;
9
+ var getWs = null;
10
10
  var currentMateId = null;
11
11
  var currentMate = null;
12
12
  var cachedSessions = [];
@@ -27,8 +27,8 @@ var searchQuery = "";
27
27
  var searchMatchIds = null;
28
28
  var searchDebounce = null;
29
29
 
30
- export function initMateSidebar(mateWsGetter) {
31
- getMateWs = mateWsGetter;
30
+ export function initMateSidebar(wsGetter) {
31
+ getWs = wsGetter;
32
32
  columnEl = document.getElementById("mate-sidebar-column");
33
33
  listEl = document.getElementById("mate-session-list");
34
34
  avatarEl = document.getElementById("mate-sidebar-avatar");
@@ -37,7 +37,7 @@ export function initMateSidebar(mateWsGetter) {
37
37
 
38
38
  if (newSessionBtn) {
39
39
  newSessionBtn.addEventListener("click", function () {
40
- var ws = getMateWs ? getMateWs() : null;
40
+ var ws = getWs ? getWs() : null;
41
41
  if (ws && ws.readyState === 1) {
42
42
  ws.send(JSON.stringify({ type: "new_session" }));
43
43
  }
@@ -73,7 +73,7 @@ export function initMateSidebar(mateWsGetter) {
73
73
  return;
74
74
  }
75
75
  searchDebounce = setTimeout(function () {
76
- var ws = getMateWs ? getMateWs() : null;
76
+ var ws = getWs ? getWs() : null;
77
77
  if (ws && ws.readyState === 1) {
78
78
  ws.send(JSON.stringify({ type: "search_sessions", query: q }));
79
79
  }
@@ -316,6 +316,10 @@ function spawnSparks(cx, cy) {
316
316
  }
317
317
  }
318
318
 
319
+ export function getMateSessions() {
320
+ return cachedSessions;
321
+ }
322
+
319
323
  export function hideMateSidebar() {
320
324
  currentMateId = null;
321
325
  currentMate = null;
@@ -498,7 +502,7 @@ function renderDebateGroup(children, groupKey) {
498
502
  hideKnowledge();
499
503
  if (isSchedulerOpen()) closeScheduler();
500
504
  hideNotes();
501
- var ws = getMateWs ? getMateWs() : null;
505
+ var ws = getWs ? getWs() : null;
502
506
  if (ws && ws.readyState === 1) {
503
507
  ws.send(JSON.stringify({ type: "switch_session", id: id }));
504
508
  }
@@ -534,7 +538,7 @@ function renderDebateGroup(children, groupKey) {
534
538
  hideKnowledge();
535
539
  if (isSchedulerOpen()) closeScheduler();
536
540
  hideNotes();
537
- var ws = getMateWs ? getMateWs() : null;
541
+ var ws = getWs ? getWs() : null;
538
542
  if (ws && ws.readyState === 1) {
539
543
  ws.send(JSON.stringify({ type: "switch_session", id: id }));
540
544
  }
@@ -580,7 +584,7 @@ function renderMateSessionItem(s) {
580
584
  if (isSchedulerOpen()) closeScheduler();
581
585
  hideNotes();
582
586
  var pendingQuery = searchQuery || "";
583
- var ws = getMateWs ? getMateWs() : null;
587
+ var ws = getWs ? getWs() : null;
584
588
  if (ws && ws.readyState === 1) {
585
589
  ws.send(JSON.stringify({ type: "switch_session", id: id }));
586
590
  }
@@ -642,7 +646,7 @@ function showMateSessionCtxMenu(anchorEl, sessionId, title) {
642
646
  overlay.querySelector(".mate-confirm-cancel").addEventListener("click", function () { overlay.remove(); });
643
647
  overlay.querySelector(".mate-confirm-delete").addEventListener("click", function () {
644
648
  overlay.remove();
645
- var ws = getMateWs ? getMateWs() : null;
649
+ var ws = getWs ? getWs() : null;
646
650
  if (ws && ws.readyState === 1) {
647
651
  ws.send(JSON.stringify({ type: "delete_session", id: sessionId }));
648
652
  }
@@ -176,13 +176,42 @@ function selectMentionItem(idx) {
176
176
  hideMentionMenu();
177
177
  }
178
178
 
179
+ function ensureChipContrast(hex) {
180
+ if (!hex || hex.charAt(0) !== "#") return hex;
181
+ var r = parseInt(hex.substring(1, 3), 16);
182
+ var g = parseInt(hex.substring(3, 5), 16);
183
+ var b = parseInt(hex.substring(5, 7), 16);
184
+ // Relative luminance (sRGB)
185
+ var lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
186
+ var isDark = document.documentElement.classList.contains("dark") ||
187
+ document.body.classList.contains("dark-mode");
188
+ if (isDark) {
189
+ // Dark mode: lighten if too dark
190
+ return lum < 0.4 ? color_mix_lighten(r, g, b, 0.35) : hex;
191
+ }
192
+ // Light mode: darken if too bright
193
+ return lum > 0.55 ? color_mix_darken(r, g, b, 0.4) : hex;
194
+ }
195
+
196
+ function color_mix_darken(r, g, b, amount) {
197
+ var f = 1 - amount;
198
+ return "#" + [Math.round(r * f), Math.round(g * f), Math.round(b * f)]
199
+ .map(function (v) { return v.toString(16).padStart(2, "0"); }).join("");
200
+ }
201
+
202
+ function color_mix_lighten(r, g, b, amount) {
203
+ return "#" + [Math.round(r + (255 - r) * amount), Math.round(g + (255 - g) * amount), Math.round(b + (255 - b) * amount)]
204
+ .map(function (v) { return v.toString(16).padStart(2, "0"); }).join("");
205
+ }
206
+
179
207
  function showInputMentionChip(name, color, avatarSrc) {
180
208
  removeInputMentionChip();
209
+ var textColor = ensureChipContrast(color);
181
210
  var chip = document.createElement("div");
182
211
  chip.id = "input-mention-chip";
183
212
  chip.innerHTML =
184
213
  '<img class="input-mention-chip-avatar" src="' + escapeHtml(avatarSrc) + '" width="18" height="18" />' +
185
- '<span class="input-mention-chip-name" style="color:' + escapeHtml(color) + '">@' + escapeHtml(name) + '</span>' +
214
+ '<span class="input-mention-chip-name" style="color:' + escapeHtml(textColor) + '">@' + escapeHtml(name) + '</span>' +
186
215
  '<button class="input-mention-chip-remove" type="button" aria-label="Remove mention">&times;</button>';
187
216
  chip.style.setProperty("--chip-color", color);
188
217
 
@@ -277,7 +306,16 @@ export function sendMention(mateId, text, pastes, images) {
277
306
 
278
307
  // Recreate the mention block if it was lost (e.g. session switch)
279
308
  function ensureMentionBlock() {
280
- if (currentMentionEl && currentMentionEl.parentNode) return; // still in DOM
309
+ if (currentMentionEl && currentMentionEl.parentNode) {
310
+ // If other elements (e.g. permission requests) were added after the mention
311
+ // block, move it to the bottom to maintain chronological order.
312
+ var parent = currentMentionEl.parentNode;
313
+ if (parent.lastElementChild !== currentMentionEl) {
314
+ parent.appendChild(currentMentionEl);
315
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
316
+ }
317
+ return;
318
+ }
281
319
  if (!activeMentionMeta) return;
282
320
  // Recreate from saved meta
283
321
  handleMentionStart(activeMentionMeta);
@@ -1,4 +1,4 @@
1
- import { copyToClipboard } from './utils.js';
1
+ import { copyToClipboard, showToast } from './utils.js';
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
3
 
4
4
  var ctx;
@@ -155,6 +155,114 @@ export function initNotifications(_ctx) {
155
155
  });
156
156
  })();
157
157
 
158
+ // --- Extension pill popover ---
159
+ (function () {
160
+ var extPillWrap = $("ext-pill-wrap");
161
+ var extPillBtn = $("ext-pill");
162
+ var extPopover = $("ext-popover");
163
+ var extDownloadBtn = $("ext-download-btn");
164
+ var extDownloadStatus = $("ext-download-status");
165
+ var extCopyUrl = $("ext-copy-url");
166
+ if (!extPillWrap || !extPillBtn || !extPopover) return;
167
+
168
+ // Detect extension connection via postMessage from content.js
169
+ var extConnected = false;
170
+ var connectedBanner = $("ext-connected-banner");
171
+ var extDesc = $("ext-popover-desc");
172
+ var extDivider = $("ext-popover-divider");
173
+ var extGuideTitle = $("ext-popover-guide-title");
174
+ var extSteps = extPopover.querySelector(".ext-popover-steps");
175
+
176
+ function setExtConnected() {
177
+ if (extConnected) return;
178
+ extConnected = true;
179
+ extPillBtn.classList.add("ext-connected");
180
+ extPillBtn.innerHTML = '<i data-lucide="check"></i> Extension';
181
+ refreshIcons(extPillBtn);
182
+ if (connectedBanner) connectedBanner.classList.remove("hidden");
183
+ if (extDownloadBtn) extDownloadBtn.style.display = "none";
184
+ if (extDownloadStatus) extDownloadStatus.classList.add("hidden");
185
+ if (extDesc) extDesc.style.display = "none";
186
+ if (extDivider) extDivider.style.display = "none";
187
+ if (extGuideTitle) extGuideTitle.style.display = "none";
188
+ if (extSteps) extSteps.style.display = "none";
189
+ }
190
+
191
+ window.addEventListener("message", function (event) {
192
+ if (event.source !== window) return;
193
+ if (!event.data || event.data.source !== "clay-chrome-extension") return;
194
+ setExtConnected();
195
+ });
196
+
197
+ // Toggle popover
198
+ extPillBtn.addEventListener("click", function (e) {
199
+ e.stopPropagation();
200
+ extPopover.classList.toggle("visible");
201
+ refreshIcons(extPopover);
202
+ });
203
+
204
+ document.addEventListener("click", function (e) {
205
+ if (!extPopover.contains(e.target) && e.target !== extPillBtn && !extPillBtn.contains(e.target)) {
206
+ extPopover.classList.remove("visible");
207
+ }
208
+ });
209
+
210
+ // Download button
211
+ if (extDownloadBtn) {
212
+ extDownloadBtn.addEventListener("click", function (e) {
213
+ e.stopPropagation();
214
+ extDownloadBtn.disabled = true;
215
+ extDownloadBtn.innerHTML = iconHtml("loader") + " Downloading...";
216
+ refreshIcons(extDownloadBtn);
217
+ var loaderIcon = extDownloadBtn.querySelector(".lucide");
218
+ if (loaderIcon) loaderIcon.style.animation = "spin 1s linear infinite";
219
+ if (extDownloadStatus) {
220
+ extDownloadStatus.classList.remove("hidden");
221
+ extDownloadStatus.textContent = "Fetching from GitHub...";
222
+ extDownloadStatus.style.color = "";
223
+ }
224
+ fetch("/api/extension/download").then(function (resp) {
225
+ if (!resp.ok) throw new Error("Download failed (" + resp.status + ")");
226
+ return resp.blob();
227
+ }).then(function (blob) {
228
+ var url = URL.createObjectURL(blob);
229
+ var a = document.createElement("a");
230
+ a.href = url;
231
+ a.download = "clay-chrome-extension.zip";
232
+ document.body.appendChild(a);
233
+ a.click();
234
+ document.body.removeChild(a);
235
+ URL.revokeObjectURL(url);
236
+ if (extDownloadStatus) {
237
+ extDownloadStatus.textContent = "Download complete!";
238
+ extDownloadStatus.style.color = "var(--accent)";
239
+ }
240
+ showToast("Extension downloaded");
241
+ }).catch(function (err) {
242
+ if (extDownloadStatus) {
243
+ extDownloadStatus.textContent = "Failed: " + err.message;
244
+ extDownloadStatus.style.color = "var(--danger, #e53935)";
245
+ }
246
+ showToast("Download failed");
247
+ }).finally(function () {
248
+ extDownloadBtn.disabled = false;
249
+ extDownloadBtn.innerHTML = iconHtml("download") + " Download Extension (.zip)";
250
+ refreshIcons(extDownloadBtn);
251
+ });
252
+ });
253
+ }
254
+
255
+ // Copy chrome://extensions URL
256
+ if (extCopyUrl) {
257
+ extCopyUrl.addEventListener("click", function (e) {
258
+ e.stopPropagation();
259
+ copyToClipboard("chrome://extensions").then(function () {
260
+ showToast("Copied chrome://extensions");
261
+ });
262
+ });
263
+ }
264
+ })();
265
+
158
266
  // --- Settings: Check for updates ---
159
267
  (function () {
160
268
  var settingsUpdateCheck = $("settings-update-check");
@@ -4,6 +4,8 @@ import { iconHtml, refreshIcons } from './icons.js';
4
4
  var ctx;
5
5
  var rewindMode = false;
6
6
  var pendingRewindUuid = null;
7
+ var rewindPreviewInFlight = false;
8
+ var rewindExecuting = false;
7
9
  var rewindBannerEl = null;
8
10
  var rewindScrollHandler = null;
9
11
  var rewindModal, rewindSummary, rewindFilesList, rewindConfirmBtn, rewindCancelBtn, rewindModeOptions;
@@ -50,6 +52,17 @@ export function clearPendingRewindUuid() {
50
52
  pendingRewindUuid = null;
51
53
  }
52
54
 
55
+ export function onRewindComplete() {
56
+ rewindExecuting = false;
57
+ rewindPreviewInFlight = false;
58
+ pendingRewindUuid = null;
59
+ }
60
+
61
+ export function onRewindError() {
62
+ rewindExecuting = false;
63
+ rewindPreviewInFlight = false;
64
+ }
65
+
53
66
  function initiateRewind(uuid) {
54
67
  if (ctx.processing) {
55
68
  ctx.addSystemMessage("Cannot rewind while processing. Stop the current operation first.", true);
@@ -59,7 +72,16 @@ function initiateRewind(uuid) {
59
72
  ctx.addSystemMessage("No rewind point found for this turn.", true);
60
73
  return;
61
74
  }
75
+ if (rewindPreviewInFlight) {
76
+ // Debounce: ignore clicks while a preview is already in flight
77
+ return;
78
+ }
79
+ if (rewindExecuting) {
80
+ ctx.addSystemMessage("Rewind already in progress.", true);
81
+ return;
82
+ }
62
83
  pendingRewindUuid = uuid;
84
+ rewindPreviewInFlight = true;
63
85
  if (ctx.ws && ctx.connected) {
64
86
  ctx.ws.send(JSON.stringify({ type: "rewind_preview", uuid: uuid }));
65
87
  }
@@ -98,6 +120,17 @@ function updateSummaryForMode() {
98
120
  }
99
121
 
100
122
  export function showRewindModal(data) {
123
+ rewindPreviewInFlight = false;
124
+
125
+ // Ignore stale preview results that don't match current pending UUID
126
+ if (data.uuid && pendingRewindUuid && data.uuid !== pendingRewindUuid) {
127
+ return;
128
+ }
129
+ // Ignore if a rewind is already executing
130
+ if (rewindExecuting) {
131
+ return;
132
+ }
133
+
101
134
  var p = data.preview || data;
102
135
  var filePaths = p.filesChanged || p.filePaths || p.files || [];
103
136
  var fileCount = filePaths.length;
@@ -171,6 +204,7 @@ export function showRewindModal(data) {
171
204
  export function hideRewindModal() {
172
205
  rewindModal.classList.add("hidden");
173
206
  pendingRewindUuid = null;
207
+ rewindPreviewInFlight = false;
174
208
  }
175
209
 
176
210
  export function renderDiffPre(text) {
@@ -333,7 +367,9 @@ export function initRewind(_ctx) {
333
367
  });
334
368
 
335
369
  rewindConfirmBtn.addEventListener("click", function() {
370
+ if (rewindExecuting) return;
336
371
  if (pendingRewindUuid && ctx.ws && ctx.connected) {
372
+ rewindExecuting = true;
337
373
  var mode = getSelectedMode();
338
374
  ctx.ws.send(JSON.stringify({ type: "rewind_execute", uuid: pendingRewindUuid, mode: mode }));
339
375
  }