From 8e8a4a54719bfbde70df02d2b2ddadd801ea2fb5 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:33:15 +0300 Subject: [PATCH] fix: the log panel stops eating your selection mid-copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every 1500 ms the poll did `bodyEl.textContent = lines.join("\n")`, and assigning textContent destroys and rebuilds the text node — which drops whatever the user had selected. Copying three lines out of a running log was a race against the next tick. Now the write is withheld while a non-collapsed selection sits inside the log body (a small "paused — selection held" note says so), and lands as soon as the selection is released or moves out. Unchanged text no longer touches the DOM at all, so an idle log is completely still. Adds an explicit ⏸ Pause toggle for deliberate freezing, and Copy now copies just the selection when there is one. Co-Authored-By: Claude Opus 5 --- movie_editor/frontend/log.js | 134 ++++++++++++++++++++++++++++--- movie_editor/frontend/styles.css | 3 +- 2 files changed, 126 insertions(+), 11 deletions(-) diff --git a/movie_editor/frontend/log.js b/movie_editor/frontend/log.js index 63d46918..4bf9e020 100644 --- a/movie_editor/frontend/log.js +++ b/movie_editor/frontend/log.js @@ -1,5 +1,5 @@ // Menubar "Log" button + a collapsible panel showing ComfyUI's real backend log -// (tee'd stdout/stderr from the server). Polls only while open. +// (tee'd stdout/stderr from the server). Polls only while open. Freezes during selection to preserve user interaction. (function () { const API = window.MovieEditorAPI; const right = document.querySelector(".menubar-right"); @@ -19,27 +19,141 @@ panel.append(head, bodyEl); document.body.append(panel); - let timer = null, autoscroll = true; + let timer = null, autoscroll = true, lastText = "", isFrozen = false, isManuallyPaused = false, pendingText = null; + + // Helper to check if a node is inside bodyEl (handles text nodes too). + function nodeInBody(node) { + return node === bodyEl || bodyEl.contains(node); + } + + // Helper to check if user has a non-collapsed selection inside bodyEl. + function selectionInBody() { + const sel = window.getSelection(); + if (sel.isCollapsed) return false; + return nodeInBody(sel.anchorNode) || nodeInBody(sel.focusNode); + } async function refresh() { + if (isManuallyPaused) return; + try { const r = await API.log(800); - bodyEl.textContent = (r.lines || []).join("\n"); + const newText = (r.lines || []).join("\n"); + + // Skip DOM write if text hasn't changed. + if (newText === lastText) return; + + // If user is selecting, freeze updates and keep polling. textContent assignment + // destroys the text node tree, which nukes the selection mid-copy, so we defer the + // write until the selection is released or moves out of the log. + if (selectionInBody()) { + isFrozen = true; + pendingText = newText; + updateFreezeIndicator(); + return; + } + + // Update is safe to apply. + lastText = newText; + bodyEl.textContent = newText; if (autoscroll) bodyEl.scrollTop = bodyEl.scrollHeight; + if (isFrozen) { + isFrozen = false; + pendingText = null; + updateFreezeIndicator(); + } } catch (e) { - bodyEl.textContent = "(log unavailable: " + e.message + ")"; + const errMsg = "(log unavailable: " + e.message + ")"; + lastText = errMsg; + bodyEl.textContent = errMsg; } } + bodyEl.addEventListener("scroll", () => { autoscroll = bodyEl.scrollHeight - bodyEl.scrollTop - bodyEl.clientHeight < 40; }); - function open() { panel.hidden = false; btn.classList.add("active"); autoscroll = true; refresh(); timer = setInterval(refresh, 1500); } - function close() { panel.hidden = true; btn.classList.remove("active"); clearInterval(timer); timer = null; } + function updateFreezeIndicator() { + const existingIndicator = actions.querySelector(".log-freeze-indicator"); + if (isFrozen) { + if (!existingIndicator) { + const indicator = document.createElement("span"); + indicator.className = "log-freeze-indicator"; + indicator.textContent = "paused — selection held"; + actions.insertBefore(indicator, actions.firstChild); + } + } else { + if (existingIndicator) existingIndicator.remove(); + } + } + + function open() { + panel.hidden = false; + btn.classList.add("active"); + autoscroll = true; + isFrozen = false; + isManuallyPaused = false; + pendingText = null; + updatePauseButton(); + updateFreezeIndicator(); + refresh(); + timer = setInterval(refresh, 1500); + } + + function close() { + panel.hidden = true; + btn.classList.remove("active"); + clearInterval(timer); + timer = null; + isFrozen = false; + isManuallyPaused = false; + pendingText = null; + updateFreezeIndicator(); + } + btn.onclick = () => (panel.hidden ? open() : close()); - const copy = document.createElement("button"); copy.className = "btn ghost tiny"; copy.textContent = "Copy"; - copy.onclick = () => { try { navigator.clipboard.writeText(bodyEl.textContent); copy.textContent = "Copied"; setTimeout(() => (copy.textContent = "Copy"), 1200); } catch (_) {} }; - const x = document.createElement("button"); x.className = "btn ghost tiny"; x.textContent = "✕"; x.onclick = close; - actions.append(copy, x); + // Helper to get the currently selected text within bodyEl. + function getSelectedText() { + const sel = window.getSelection(); + if (sel.isCollapsed) return null; + if (!nodeInBody(sel.anchorNode) && !nodeInBody(sel.focusNode)) return null; + return sel.toString(); + } + + const copy = document.createElement("button"); + copy.className = "btn ghost tiny"; + copy.textContent = "Copy"; + copy.onclick = () => { + try { + const selectedText = getSelectedText(); + const textToCopy = selectedText || lastText; + navigator.clipboard.writeText(textToCopy); + copy.textContent = selectedText ? "Copied selection" : "Copied"; + setTimeout(() => (copy.textContent = "Copy"), 1200); + } catch (_) {} + }; + + const pauseBtn = document.createElement("button"); + pauseBtn.className = "btn ghost tiny"; + pauseBtn.textContent = "⏸ Pause"; + pauseBtn.onclick = () => { + isManuallyPaused = !isManuallyPaused; + updatePauseButton(); + // Refetch rather than flushing pendingText: nothing was polled while paused, so the + // only text held is whatever a selection froze earlier and it may be minutes stale. + if (!isManuallyPaused) { pendingText = null; refresh(); } + }; + + function updatePauseButton() { + pauseBtn.textContent = isManuallyPaused ? "▶ Resume" : "⏸ Pause"; + pauseBtn.classList.toggle("on", isManuallyPaused); + } + + const x = document.createElement("button"); + x.className = "btn ghost tiny"; + x.textContent = "✕"; + x.onclick = close; + + actions.append(copy, pauseBtn, x); })(); diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index c116833c..1f7e6c0b 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -121,7 +121,8 @@ button, input, select, textarea { font-family: inherit; color: var(--text); } .log-panel[hidden] { display: none; } /* explicit display:flex above would otherwise beat the hidden attribute */ .log-head { display: flex; align-items: center; justify-content: space-between; padding: 7px 10px; border-bottom: 1px solid var(--line); font-family: var(--font-mono); font-size: 11px; letter-spacing: 1px; text-transform: uppercase; color: var(--amber); } -.log-actions { display: flex; gap: 6px; } +.log-actions { display: flex; gap: 6px; align-items: center; } +.log-freeze-indicator { font-family: var(--font-mono); font-size: 10px; color: var(--faint); white-space: nowrap; } .log-body { flex: 1; margin: 0; padding: 8px 10px; overflow: auto; white-space: pre-wrap; word-break: break-word; font-family: var(--font-mono); font-size: 11px; line-height: 1.45; color: var(--muted); background: #060606; }