Add replace-with-answer button to selection tools#996
Conversation
When the selection toolbar is triggered on text inside an editable
target (input, textarea, or contenteditable element), the editable
selection is captured before it gets lost by interacting with the
toolbar. Completed answers in the floating toolbar then show a Replace
button next to Copy that writes the answer back over the originally
selected text, e.g. to apply a polished or translated version in place.
The replacement verifies the original text is still present before
writing (stale captures never overwrite changed content), inserts plain
text only (never parsed as HTML), prefers execCommand('insertText') so
native undo keeps working, and falls back to setting the value through
the prototype setter and dispatching an input event so pages with
controlled inputs (React and similar) observe the change.
Static page content is intentionally not replaceable, and other users
of ConversationCard and FloatingToolbar (popup, independent panel, site
adapters) are unaffected since the capture is only wired up in the
selection tools path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughAdds a replace-selection flow that captures editable text, threads the captured selection through the toolbar and conversation UI, and replaces it with new content using dedicated utilities. It also adds tests and new locale strings. ChangesReplace Selection Feature
Estimated code review effort: 4 (Complex) | ~50 minutes Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Summary by QodoAdd Replace button to selection-tools answers to write back into editable fields
AI Description
Diagram
High-Level Assessment
Files changed (21)
|
There was a problem hiding this comment.
Code Review
This pull request introduces a feature to replace captured editable text selections (such as text fields and contenteditable regions) with generated or translated content, including a new ReplaceButton component, updated localization files, and comprehensive unit tests. The code review feedback suggests several important improvements: traversing the DOM to find the top-most contenteditable ancestor to ensure focus works correctly, re-applying the collapsed range to the active selection in the contenteditable fallback path to update the cursor position, and using a useEffect hook in ReplaceButton to clear the status timeout on unmount to prevent memory leaks.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const findContentEditableElement = (node) => { | ||
| if (!node) return null | ||
| const element = node.nodeType === 3 ? node.parentElement : node | ||
| return element && element.isContentEditable ? element : null | ||
| } |
There was a problem hiding this comment.
When the selection is inside a child element of a contenteditable region (e.g., a <b> or <p> tag), findContentEditableElement returns that child element. However, calling .focus() on non-focusable elements like <b> does not actually focus the editing host, which can cause subsequent doc.execCommand calls to fail or behave unexpectedly.
Traversing up the DOM tree to find the top-most contenteditable ancestor (the editing host) ensures that .focus() is called on a focusable element.
const findContentEditableElement = (node) => {
if (!node) return null
let element = node.nodeType === 3 ? node.parentElement : node
if (!element || !element.isContentEditable) return null
while (element.parentElement && element.parentElement.isContentEditable) {
element = element.parentElement
}
return element
}| range.deleteContents() | ||
| range.insertNode(doc.createTextNode(text)) | ||
| range.collapse(false) | ||
| dispatchInputEvent(element) |
There was a problem hiding this comment.
In the fallback path for contenteditable replacement, range.collapse(false) is called to collapse the range to the end of the inserted text. However, this only updates the Range object itself and does not update the browser's actual selection/cursor.
To ensure the cursor is correctly placed at the end of the replaced text so the user can continue typing seamlessly, we should re-apply the collapsed range to the active selection.
range.deleteContents()
const textNode = doc.createTextNode(text)
range.insertNode(textNode)
range.collapse(false)
if (selection) {
selection.removeAllRanges()
selection.addRange(range)
}
dispatchInputEvent(element)| const timeoutRef = useRef(null) | ||
|
|
||
| const onClick = () => { | ||
| setStatus(onReplace() ? 'replaced' : 'failed') | ||
| if (timeoutRef.current) clearTimeout(timeoutRef.current) | ||
| timeoutRef.current = setTimeout(() => { | ||
| setStatus('idle') | ||
| timeoutRef.current = null | ||
| }, 800) | ||
| } |
There was a problem hiding this comment.
Add a useEffect hook to clear the pending timeout when the component unmounts. This prevents memory leaks and potential state updates on unmounted components if the button is unmounted while the success/failure status timeout is still active.
const timeoutRef = useRef(null)
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current)
}
}, [])
const onClick = () => {
setStatus(onReplace() ? 'replaced' : 'failed')
if (timeoutRef.current) clearTimeout(timeoutRef.current)
timeoutRef.current = setTimeout(() => {
setStatus('idle')
timeoutRef.current = null
}, 800)
}There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 50b65a1c83
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (tagName === 'TEXTAREA') return true | ||
| if (tagName !== 'INPUT') return false | ||
| return TEXT_FIELD_INPUT_TYPES.includes((element.type || 'text').toLowerCase()) |
There was a problem hiding this comment.
When the selected target is a readonly textarea or text input, this helper still captures it as replaceable, and the later fallback path writes via the native value setter even if execCommand refuses to edit it. That makes the Replace button able to mutate fields the page explicitly marked as non-editable; please exclude readOnly/disabled controls when capturing and re-check before replacing.
Useful? React with 👍 / 👎.
Code Review by Qodo
1.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/ReplaceButton/index.jsx`:
- Around line 15-24: The ReplaceButton timeout is only cleared before scheduling
a new one, but not when the component unmounts, so the delayed callback in
onClick can still call setStatus after unmount. Add a cleanup effect in the
ReplaceButton component that clears timeoutRef.current on unmount, and make sure
it also nulls the ref after clearing so no pending timer survives component
teardown.
In `@src/content-script/index.jsx`:
- Around line 329-331: The mouseup and touchend selection handling in index.jsx
is skipping Firefox text-field selections because it bails out on
window.getSelection().toString() before calling captureEditableSelection().
Update the selection guard in the flows around captureEditableSelection() and
createSelectionTools() to also recognize <input>/<textarea> selections via
document.activeElement.selectionStart/selectionEnd when the document selection
is empty, so toolbar creation still runs for editable-field text selections.
In `@src/utils/selection-replace.mjs`:
- Line 7: Remove password from TEXT_FIELD_INPUT_TYPES in selection-replace.mjs
so captureEditableSelection no longer treats password inputs as text fields;
update the selection capture logic that uses TEXT_FIELD_INPUT_TYPES and
activeElement.value.slice(start, end) to exclude password fields entirely,
ensuring the descriptor text property is never populated from password input
content.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7a54da2e-a370-4599-9c6a-94ec17320d82
📒 Files selected for processing (21)
src/_locales/de/main.jsonsrc/_locales/en/main.jsonsrc/_locales/es/main.jsonsrc/_locales/fr/main.jsonsrc/_locales/in/main.jsonsrc/_locales/it/main.jsonsrc/_locales/ja/main.jsonsrc/_locales/ko/main.jsonsrc/_locales/pt/main.jsonsrc/_locales/ru/main.jsonsrc/_locales/tr/main.jsonsrc/_locales/zh-hans/main.jsonsrc/_locales/zh-hant/main.jsonsrc/components/ConversationCard/index.jsxsrc/components/ConversationItem/index.jsxsrc/components/FloatingToolbar/index.jsxsrc/components/ReplaceButton/index.jsxsrc/content-script/index.jsxsrc/utils/index.mjssrc/utils/selection-replace.mjstests/unit/utils/selection-replace.test.mjs
|
|
||
| const onClick = () => { | ||
| setStatus(onReplace() ? 'replaced' : 'failed') | ||
| if (timeoutRef.current) clearTimeout(timeoutRef.current) |
There was a problem hiding this comment.
SUGGESTION: setTimeout created in onClick is not cleared on unmount
If the component unmounts before the 800ms timeout fires, the callback runs and calls setStatus on an unmounted component. Add an effect cleanup (or useEffect) to clear the timer on unmount.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge All prior review findings (password capture, readOnly/disabled capture, non-event input dispatch, editing-host resolution, cursor re-application for contenteditable, ReplaceButton unmount cleanup, Firefox text-field selection support, sensitive selection logging, retained Files Reviewed (20 files)
Previous Review Summaries (3 snapshots)Current summary above is authoritative. Previous snapshots are kept for context only. Previous reviewStatus: No Issues Found | Recommendation: Merge Files Reviewed (3 files)
Previous review (commit 0fb4fd7)Status: No Issues Found | Recommendation: Merge Files Reviewed (4 files)
Previous review (commit 50b65a1)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (18 files)
Reviewed by hy3-20260706:free · Input: 72K · Output: 11.9K · Cached: 176.7K |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The context menu click arrives asynchronously via the background script, so the editable selection is captured on the contextmenu event alongside the menu position. Menu tools operating on the whole page intentionally do not offer replacement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Exclude password inputs from capture so credential text is never stored in the descriptor, and exclude readonly/disabled fields both at capture time and again before replacing - Resolve nested contenteditable selections to the editing host so focus() lands on a focusable element before execCommand - Re-apply the collapsed range to the selection in the contenteditable fallback so the cursor ends up after the inserted text - Dispatch a real Event instance when InputEvent is unavailable instead of a plain object - Clear the ReplaceButton status timeout on unmount - Fall back to the captured text field selection when window.getSelection() is empty, so selection tools also trigger for input/textarea selections on Firefox Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed the review feedback in 0fb4fd7:
All covered by new unit tests (24 in |
|
Code review by qodo was updated up to the latest commit 0fb4fd7 |
|
Addressed the second qodo review round in 25fbd1e:
(The branch was force-pushed once to drop an accidentally committed local build artifact from history; no code changes.) 714 tests passing, lint and production build clean. |
- Log only the captured selection kind instead of the full descriptor so selected text and DOM references never end up in page console logs - Clear the context menu selection capture once the menu action has been handled instead of retaining element and range references - Store the full text field value at capture time and require an exact match before replacing, so edits that shift the selection offsets while leaving the same substring in place fail safely Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
01863f2 to
25fbd1e
Compare
|
/gemini review |
There was a problem hiding this comment.
Pull request overview
Adds a “Replace Selection” action to the selection-tools answer UI so users can write an AI answer back into the originally selected text inside editable fields (inputs/textareas/contenteditable), avoiding manual copy/paste. This integrates capture + safe replace logic into the content script’s selection-tools flow while keeping non-editable page content non-replaceable.
Changes:
- Add
captureEditableSelection/replaceCapturedSelectionutilities with stale/disconnected guards for text fields andcontenteditable. - Thread captured selection through the selection-tools FloatingToolbar → ConversationCard → ConversationItem and introduce a
ReplaceButtonwith success/failure feedback. - Add unit tests for capture/replace behavior and propagate new i18n strings across locales.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/utils/selection-replace.test.mjs | New unit tests covering capture + replace for text fields and contenteditable, including stale/disconnected guards. |
| src/utils/selection-replace.mjs | New utility implementing capture + safe replacement (execCommand preferred, fallback for controlled inputs). |
| src/utils/index.mjs | Re-export selection-replace utilities via utils barrel. |
| src/content-script/index.jsx | Capture editable selection before toolbar interaction; pass captured selection into FloatingToolbar for selection-tools flows. |
| src/components/ReplaceButton/index.jsx | New Replace button component with success/failure icon feedback and localized tooltip text. |
| src/components/FloatingToolbar/index.jsx | Wire onReplaceContent into ConversationCard when an editable selection was captured. |
| src/components/ConversationItem/index.jsx | Render ReplaceButton on completed answers when onReplace is provided. |
| src/components/ConversationCard/index.jsx | Provide onReplace callback per completed answer, stripping trailing <hr/> like Copy does. |
| src/_locales/*/main.json | Add localized strings for “Replace Selection” and “Replace Failed”. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <span | ||
| title={status === 'failed' ? t('Replace Failed') : t('Replace Selection')} | ||
| className={`gpt-util-icon ${className ? className : ''}`} | ||
| onClick={onClick} | ||
| > |
There was a problem hiding this comment.
Code Review
This pull request introduces a feature allowing users to replace selected text in editable fields (text inputs, textareas, and contenteditable regions) with the AI-generated response. It includes localized strings, a new ReplaceButton component, selection capture/replacement utilities, and comprehensive unit tests. The review feedback highlights a potential memory leak in the content script due to uncleared context menu selections, potential wrong-document DOM exceptions when interacting with elements inside iframes, and a robustness issue in ReplaceButton where void-returning callbacks incorrectly trigger a failure state.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| menuY = e.clientY | ||
| // the menu click arrives asynchronously via the background script, so the | ||
| // editable selection has to be captured while it still exists | ||
| menuCapturedSelection = captureEditableSelection() |
There was a problem hiding this comment.
Memory Leak Risk
The global variable menuCapturedSelection is assigned a captured selection when the context menu is opened. However, if the user dismisses the context menu without clicking any of the extension's menu items (e.g., by clicking elsewhere on the page or pressing Escape), CREATE_CHAT is never received, and menuCapturedSelection will continue to hold a reference to the DOM element and the Range object indefinitely.
This prevents the DOM element, its owner document, and associated resources from being garbage collected, leading to a memory leak on long-lived pages.
Suggestion
Clear menuCapturedSelection after a reasonable timeout (e.g., 30 seconds) to prevent memory leaks if the menu is dismissed.
menuCapturedSelection = captureEditableSelection()
setTimeout(() => {
if (menuCapturedSelection) {
menuCapturedSelection = null
}
}, 30000)| const replaceInTextField = (captured, text, doc) => { | ||
| const { element, start, end, text: originalText } = captured | ||
| if (element.isConnected === false || element.readOnly || element.disabled) return false | ||
| const value = element.value ?? '' | ||
| if (value !== captured.fieldValue) return false | ||
|
|
||
| if (typeof element.focus === 'function') element.focus() | ||
| let replaced = false | ||
| if (typeof element.setSelectionRange === 'function' && typeof doc.execCommand === 'function') { | ||
| element.setSelectionRange(start, end) | ||
| try { | ||
| // execCommand keeps the native undo history working where supported | ||
| replaced = doc.execCommand('insertText', false, text) && element.value !== value | ||
| } catch (error) { | ||
| replaced = false | ||
| } | ||
| if (text === originalText) replaced = true | ||
| } |
There was a problem hiding this comment.
Wrong Document Error in Iframes
If the selection is inside an iframe (which is very common for rich text editors), doc (which defaults to globalThis.document, the outer document) will be different from the iframe's document. Calling doc.execCommand on an element inside the iframe will fail or throw a DOMException (Wrong Document Error).
Suggestion
Use element.ownerDocument instead of doc to ensure the command is executed in the correct document context.
const replaceInTextField = (captured, text, doc) => {
const { element, start, end, text: originalText } = captured
if (element.isConnected === false || element.readOnly || element.disabled) return false
const value = element.value ?? ''
if (value !== captured.fieldValue) return false
const ownerDoc = element.ownerDocument || doc
if (typeof element.focus === 'function') element.focus()
let replaced = false
if (typeof element.setSelectionRange === 'function' && typeof ownerDoc.execCommand === 'function') {
element.setSelectionRange(start, end)
try {
// execCommand keeps the native undo history working where supported
replaced = ownerDoc.execCommand('insertText', false, text) && element.value !== value
} catch (error) {
replaced = false
}
if (text === originalText) replaced = true
}| const replaceInContentEditable = (captured, text, doc) => { | ||
| const { element, range, text: originalText } = captured | ||
| if (element.isConnected === false || !element.isContentEditable) return false | ||
| if (range.toString() !== originalText) return false | ||
|
|
||
| if (typeof element.focus === 'function') element.focus() | ||
| const selection = typeof doc.getSelection === 'function' ? doc.getSelection() : null | ||
| if (selection) { | ||
| selection.removeAllRanges() | ||
| selection.addRange(range) | ||
| if (typeof doc.execCommand === 'function') { | ||
| try { | ||
| if (doc.execCommand('insertText', false, text)) return true | ||
| } catch (error) { | ||
| // fall through to manual replacement | ||
| } | ||
| } | ||
| } | ||
| range.deleteContents() | ||
| range.insertNode(doc.createTextNode(text)) |
There was a problem hiding this comment.
Wrong Document Error in Iframes
Similar to text fields, if the contenteditable region is inside an iframe, creating a text node using doc.createTextNode (the outer document) and inserting it into the iframe's range will throw a WRONG_DOCUMENT_ERR DOMException.
Suggestion
Use element.ownerDocument to get the correct document context for creating the text node, retrieving the selection, and executing commands.
const replaceInContentEditable = (captured, text, doc) => {
const { element, range, text: originalText } = captured
if (element.isConnected === false || !element.isContentEditable) return false
if (range.toString() !== originalText) return false
const ownerDoc = element.ownerDocument || doc
if (typeof element.focus === 'function') element.focus()
const selection = typeof ownerDoc.getSelection === 'function' ? ownerDoc.getSelection() : null
if (selection) {
selection.removeAllRanges()
selection.addRange(range)
if (typeof ownerDoc.execCommand === 'function') {
try {
if (ownerDoc.execCommand('insertText', false, text)) return true
} catch (error) {
// fall through to manual replacement
}
}
}
range.deleteContents()
range.insertNode(ownerDoc.createTextNode(text))| }, []) | ||
|
|
||
| const onClick = () => { | ||
| setStatus(onReplace() ? 'replaced' : 'failed') |
There was a problem hiding this comment.
Robustness of Callback Return Value Check
Currently, ReplaceButton assumes that onReplace() strictly returns a truthy value on success. However, in React, many callback functions do not return a value (i.e., they return undefined). If another developer reuses ReplaceButton or ConversationCard with a standard void-returning function, the button will incorrectly show the "Replace Failed" state.
Suggestion
Only treat the result as a failure if it explicitly returns false.
| setStatus(onReplace() ? 'replaced' : 'failed') | |
| const success = onReplace() | |
| setStatus(success !== false ? 'replaced' : 'failed') |
Code Review by Qodo
1. Firefox keydown closes toolbar
|
| const capturedSelection = captureEditableSelection() | ||
| const selection = | ||
| window | ||
| .getSelection() | ||
| ?.toString() | ||
| .trim() | ||
| .replace(/^-+|-+$/g, '') || | ||
| // Firefox does not expose text field selections via window.getSelection() | ||
| (capturedSelection?.kind === 'text-field' | ||
| ? capturedSelection.text.trim().replace(/^-+|-+$/g, '') | ||
| : '') |
There was a problem hiding this comment.
1. Firefox keydown closes toolbar 🐞 Bug ≡ Correctness
The PR adds a Firefox fallback to derive selected text from captured text-field selection, but the keydown cleanup path still checks only window.getSelection(). In Firefox (where text-field selection isn’t exposed via window.getSelection), this can incorrectly delete the selection-tools toolbar even while a text-field selection still exists.
Agent Prompt
### Issue description
Selection-tools now supports Firefox text-field selections via `captureEditableSelection()`, but the toolbar auto-cleanup on `keydown` still relies on `window.getSelection()`. In Firefox this can evaluate to empty for input/textarea selections, causing premature toolbar deletion.
### Issue Context
The PR explicitly notes Firefox does not expose text-field selections via `window.getSelection()`, and adds a fallback for detecting the selected text on mouseup/touchend. The keydown cleanup should use the same strategy (or directly inspect `selectionStart/selectionEnd`).
### Fix Focus Areas
- src/content-script/index.jsx[365-377]
- src/content-script/index.jsx[298-308]
### Implementation notes
- In the keydown `setTimeout`, decide “selection exists” using either:
- `window.getSelection()?.toString().trim()` (for contenteditable/static selections), OR
- for INPUT/TEXTAREA: `e.target.selectionStart < e.target.selectionEnd` (or `captureEditableSelection()` and check `kind === 'text-field'` and non-empty text).
- Only call `deleteToolbar()` when *both* checks indicate there is no selection.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const TEXT_FIELD_INPUT_TYPES = ['text', 'search', 'url', 'tel'] | ||
|
|
||
| const isTextField = (element) => { | ||
| if (!element || typeof element.tagName !== 'string') return false | ||
| if (element.readOnly || element.disabled) return false | ||
| const tagName = element.tagName.toUpperCase() | ||
| if (tagName === 'TEXTAREA') return true | ||
| if (tagName !== 'INPUT') return false | ||
| return TEXT_FIELD_INPUT_TYPES.includes((element.type || 'text').toLowerCase()) | ||
| } |
There was a problem hiding this comment.
2. Email inputs not supported 🐞 Bug ≡ Correctness
Text-field capture only allows INPUT types in ['text','search','url','tel'], so selections in common editable inputs like <input type="email"> will never be captured. As a result, Replace will never be offered for those inputs even though they support selectionStart/selectionEnd.
Agent Prompt
### Issue description
`captureEditableSelection()` currently allowlists only a few INPUT types. This blocks Replace for other text-like inputs (notably `type="email"`).
### Issue Context
`isTextField()` gates whether a selection can be captured/replaced in inputs; anything excluded here makes the Replace feature unavailable for that field type.
### Fix Focus Areas
- src/utils/selection-replace.mjs[7-16]
### Implementation notes
Prefer a capability-based check over a narrow allowlist:
- Allow INPUT elements that expose numeric `selectionStart/selectionEnd` and are not `readOnly/disabled`.
- Keep an explicit denylist for non-text inputs and sensitive types (e.g., `password`, `checkbox`, `radio`, `file`, `submit`, `button`, etc.).
- At minimum, include `email` in the allowed list if you want to stay with allowlisting.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const dispatchInputEvent = (element) => { | ||
| if (typeof element.dispatchEvent !== 'function') return | ||
| const event = globalThis.InputEvent | ||
| ? new globalThis.InputEvent('input', { bubbles: true, inputType: 'insertText' }) | ||
| : globalThis.Event | ||
| ? new globalThis.Event('input', { bubbles: true }) | ||
| : null | ||
| if (event) element.dispatchEvent(event) | ||
| } |
There was a problem hiding this comment.
3. Inputevent creation not guarded 🐞 Bug ☼ Reliability
dispatchInputEvent constructs a new InputEvent without guarding against constructor failures, which can cause replaceCapturedSelection to return false due to a thrown error after performing the replacement work. This can lead to incorrect failure feedback even though the content was updated.
Agent Prompt
### Issue description
`dispatchInputEvent()` assumes `new InputEvent(...)` is always constructible when `globalThis.InputEvent` exists. If that constructor throws in some environments, the outer replacement call catches and reports failure.
### Issue Context
`replaceCapturedSelection()` wraps the replace helpers in a try/catch and returns `false` on any thrown error, so a failure during event construction/dispatch affects the success signal.
### Fix Focus Areas
- src/utils/selection-replace.mjs[72-80]
- src/utils/selection-replace.mjs[163-171]
### Implementation notes
- Wrap InputEvent creation in a try/catch and fall back to `new Event('input', { bubbles: true })`.
- Consider wrapping `element.dispatchEvent(...)` in try/catch as well to avoid flipping success/failure due to a dispatch error.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Summary
Adds a Replace button to answers in the selection-tools floating toolbar, so the AI response (e.g. a polished or translated version) can be written back over the originally selected text in one click — no more manual copy & paste when polishing text you are writing in an input, textarea, or rich-text editor.
How it works
captureEditableSelection) — a text field (input/textarea) with its selection offsets, or a clonedRangeinside acontenteditableregion. This happens before the selection is lost by interacting with the toolbar.replaceCapturedSelection, which writes the answer text over the original selection. A ✓/✗ icon gives success/failure feedback (same pattern asCopyButton).Safety / security considerations
.value/ text nodes /execCommand('insertText')), never parsed as HTML.range.toString()for contenteditable). If the content changed in the meantime, nothing is overwritten and the button shows a failure state.document.execCommand('insertText')is preferred so the native undo history keeps working; a fallback sets the value via the prototype setter and dispatches aninputevent so pages with controlled inputs (React and similar) observe the change.Containment
src/utils/selection-replace.mjs+ new componentsrc/components/ReplaceButton/; everything else is optional-prop threading (capturedSelection→FloatingToolbar,onReplaceContent→ConversationCard,onReplace→ConversationItem).ConversationCard/FloatingToolbar(popup, independent panel, site adapters, menu tools) are unaffected — the capture is only wired up in the selection-tools path (mouse and touch).i18n
Replace SelectionandReplace Failedtosrc/_locales/en/main.jsonand propagated translations to all other locales (de, es, fr, in, it, ja, ko, pt, ru, tr, zh-hans, zh-hant). Native speakers, please feel free to correct.Validation
npm test: 708 tests pass, including 19 new unit tests forselection-replace.mjs(capture and replace for both text fields and contenteditable, execCommand and fallback paths, stale/disconnected guards).npm run lintandnpm run pretty: clean.npm run build: all build variants and artifacts produced successfully.🤖 Generated with Claude Code
Summary by CodeRabbit
Summary
New Features
Bug Fixes
Tests