From 99a284a0d9f758932feb6cb5c21c8fea3f585564 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 16 Jun 2026 19:59:48 -0700 Subject: [PATCH] Fix live page editable focus handling (#256) --- skill/scripts/live-browser.js | 21 +++++++++++++++- tests/live-browser-regression.test.mjs | 34 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/skill/scripts/live-browser.js b/skill/scripts/live-browser.js index 3c2e72555..ed9606409 100644 --- a/skill/scripts/live-browser.js +++ b/skill/scripts/live-browser.js @@ -6489,10 +6489,13 @@ ) { return; } + if (isPageEditableElement(deepActive) && !isInlineEditActive(deepActive)) { + return; + } // While a contenteditable text-leaf is focused, let the browser handle // all keys except Escape. Escape cancels the current edit (restores // original text) and blurs without saving, staying in CONFIGURING. - if (e.target.isContentEditable && inlineEditRows.some((r) => r.el === e.target)) { + if (e.target.isContentEditable && isInlineEditActive(e.target)) { if (e.key !== 'Escape') return; e.preventDefault(); e.stopPropagation(); @@ -8312,6 +8315,21 @@ void main() { && !steerLocked; } + function isPageEditableElement(el) { + if (!el || own(el)) return false; + if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName || '')) return true; + return !!el.isContentEditable; + } + + function isInlineEditActive(el) { + return !!el && inlineEditRows.some((r) => r.el === el); + } + + function isPageEditableActive() { + const active = activeElementDeep(); + return isPageEditableElement(active) && !isInlineEditActive(active); + } + function pageHasHostTextSelection() { const sel = window.getSelection?.(); if (!sel || sel.isCollapsed) return false; @@ -8325,6 +8343,7 @@ void main() { function shouldSteerAutoFocus() { return shouldFocusSteerChat() && !steerFocusSuspended + && !isPageEditableActive() && performance.now() >= steerFocusPauseUntil; } diff --git a/tests/live-browser-regression.test.mjs b/tests/live-browser-regression.test.mjs index 27e814c06..5b82a0491 100644 --- a/tests/live-browser-regression.test.mjs +++ b/tests/live-browser-regression.test.mjs @@ -154,6 +154,24 @@ describe('live-browser.js regression guards', () => { ); }); + it('does not autofocus the steering chat while a page editable is focused', () => { + assert.match( + SOURCE, + /function isPageEditableElement\(el\) \{[\s\S]{0,160}?own\(el\)[\s\S]{0,160}?\^\(INPUT\|TEXTAREA\|SELECT\)\$[\s\S]{0,80}?el\.isContentEditable/, + 'page-owned inputs, textareas, selects, and contenteditables must be recognized before steer focus recovery runs', + ); + assert.match( + SOURCE, + /function isPageEditableActive\(\) \{[\s\S]{0,120}?activeElementDeep\(\)[\s\S]{0,120}?isPageEditableElement\(active\) && !isInlineEditActive\(active\)/, + 'auto-focus recovery must check the deep active element instead of only host text selection', + ); + assert.match( + SOURCE, + /function shouldSteerAutoFocus\(\) \{[\s\S]{0,160}?&& !isPageEditableActive\(\)/, + 'steer chat auto-focus must back off while the page owns an editable caret', + ); + }); + it('pins edit badge button metrics instead of inheriting host button chrome', () => { const start = SOURCE.indexOf('const calloutStyle = (color, borderColor) => ({'); const end = SOURCE.indexOf(' });', start); @@ -345,6 +363,22 @@ describe('live-browser.js regression guards', () => { ); }); + it('lets page editables keep Enter and arrow keydown events', () => { + const start = SOURCE.indexOf('function handleKeyDown(e)'); + const pendingApplyStart = SOURCE.indexOf('if (pendingApplyInFlight)', start); + const guardSource = SOURCE.slice(start, pendingApplyStart); + assert.match( + guardSource, + /isPageEditableElement\(deepActive\) && !isInlineEditActive\(deepActive\)[\s\S]{0,40}?return;/, + 'page-owned editables must short-circuit global key handling before variant navigation or accept handling', + ); + assert.match( + guardSource, + /e\.target\.isContentEditable && isInlineEditActive\(e\.target\)/, + 'impeccable inline edit rows must keep their existing Escape-cancel path', + ); + }); + it('configure input Escape tears down annotation overlay before returning to picking', () => { // The configure prompt auto-focuses. While focused, the global keydown // handler bails on own() inputs, so this local Escape path must hide the