Skip to content

Commit 3ca39f2

Browse files
committed
fix(opencode): keep Mod+A scoped to chat input contenteditable
Browser default Mod+A on the iframe contenteditable could escape and select the iframe body, manifesting as focus loss in the chat input. Add a capture-phase keydown interceptor in the spaProxy bootstrap that selects only the editor's own contents via Range.selectNodeContents and stops propagation. Fix lives in the fork-owned bootstrap because the SPA source under packages/app is vendored and overwritten on every vendor update.
1 parent c1b483d commit 3ca39f2

1 file changed

Lines changed: 38 additions & 0 deletions

File tree

  • src/vs/workbench/contrib/opencode/electron-main

src/vs/workbench/contrib/opencode/electron-main/spaProxy.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,44 @@ export const BOOTSTRAP = `<script>
402402
function findPromptTarget() {
403403
return document.querySelector('[data-component="prompt-input"]') || document.querySelector('[contenteditable="true"]')
404404
}
405+
// Fix: Mod+A on contenteditable can escape and select the iframe body; intercept early to keep selection editor-local.
406+
document.addEventListener("keydown", function(e) {
407+
if (!(e.metaKey || e.ctrlKey)) return
408+
if (e.metaKey && e.ctrlKey) return
409+
if (e.shiftKey || e.altKey) return
410+
if ((e.key || "").toLowerCase() !== "a") return
411+
if (e.isComposing || e.keyCode === 229) return
412+
413+
var active = document.activeElement
414+
if (!active) return
415+
if (active.tagName === "INPUT" || active.tagName === "TEXTAREA") return
416+
417+
var prompt = active.closest && active.closest('[data-component="prompt-input"]')
418+
if (!prompt) {
419+
var fallbackPrompt = findPromptTarget()
420+
if (fallbackPrompt && (active === fallbackPrompt || fallbackPrompt.contains(active))) {
421+
prompt = fallbackPrompt
422+
}
423+
}
424+
425+
var target = prompt || (active.isContentEditable ? active : null)
426+
if (!target && active.closest) {
427+
target = active.closest('[contenteditable="true"]')
428+
}
429+
if (!target) return
430+
431+
e.preventDefault()
432+
e.stopImmediatePropagation()
433+
if (target.focus) target.focus()
434+
var range = document.createRange()
435+
range.selectNodeContents(target)
436+
var sel = window.getSelection()
437+
if (sel) {
438+
sel.removeAllRanges()
439+
sel.addRange(range)
440+
}
441+
blog("select-all contenteditable target=" + (target.getAttribute("data-component") || target.tagName))
442+
}, true)
405443
function dispatchInsertInput(target, text) {
406444
try {
407445
target.dispatchEvent(new InputEvent("input", {

0 commit comments

Comments
 (0)