fix(react-ui/chat): stop wiping selection on every /api/operations poll (#9904)#9917
Merged
Merged
Conversation
…ll (#9904) useOperations() was calling setOperations() with a fresh array on every 1s poll, even when the payload was identical. In React 19 the DOM diff no longer short-circuits dangerouslySetInnerHTML on equal __html, so the forced Chat re-render re-assigned innerHTML on every assistant message once per second — wiping any text the user had selected. Skip the state update when the serialised operations payload is unchanged, and switch loading/error to functional setters so they also short-circuit at the source. Also fixes the chat copy button on plain HTTP: navigator.clipboard is undefined in non-secure contexts (a common LXC+Docker deployment), but the previous code called it unconditionally and showed a success toast regardless. Routed Chat, AgentChat and CanvasPanel through a new copyToClipboard() helper that uses navigator.clipboard when available and falls back to a hidden-textarea + execCommand('copy') trick that browsers still honour outside secure contexts. The fallback preserves the user's existing selection. Regression coverage in e2e/chat-polling-selection.spec.js: a MutationObserver counts mutations on the assistant content node across 3s of polling (must be 0); the copy test stubs out navigator.clipboard and asserts that execCommand('copy') is invoked. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: claude-opus-4-7-1m
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the two issues reported in #9904: text selection in chat being wiped every second, and the per-message copy button silently failing over plain HTTP.
Root cause 1 — selection wiped on every poll
useOperations()polls/api/operationsevery 1s and calledsetOperations(ops)with a fresh array reference every time, even when the payload was identical. That forced Chat to re-render. The catch is that React 19 dropped the oldlastHtml === nextHtmlshort-circuit in its DOM diff, so on every Chat re-render React re-assignsinnerHTMLon the<div dangerouslySetInnerHTML=...>for each assistant message — collapsing any text the user had selected.Fix: JSON-compare the incoming operations payload against the last one written and skip
setOperationswhen it didn't change. Functional setters forloading/errorso they short-circuit at the source too.Root cause 2 — copy button broken on plain HTTP
navigator.clipboardis undefined in non-secure contexts (which is exactly what the user has: LXC -> Docker -> remote HTTP). The previous code callednavigator.clipboard.writeText(text)unconditionally and showed a success toast regardless of what actually happened.Fix: new
src/utils/clipboard.jshelper withnavigator.clipboard.writeTextas the primary path (gated onwindow.isSecureContext) and a hidden-textarea +document.execCommand('copy')fallback that browsers still honour outside secure contexts. Preserves the user's existing selection during the fallback. Wired through Chat, AgentChat, and CanvasPanel (all three had the same bug). Newtoasts.copyFailedstring in all five existing locales.Test plan
Regression coverage in
core/http/react-ui/e2e/chat-polling-selection.spec.js:/api/operations(empty payload), opens a chat, attaches aMutationObserverto the assistant content node over 3s of polling. Without the fix the observer records ~1 mutation per poll; with the fix it records 0. Also verifies the programmatic selection survives 2.5s of polling.navigator.clipboard+window.isSecureContext = false, spies ondocument.execCommand, clicks the copy button and asserts the fallback path runs.Both new tests fail on
masterand pass with this change.Closes #9904