Skip to content

fix: Meta double-paste + cancel on timeout/focus switch - #21

Merged
summeroff merged 2 commits into
masterfrom
fix/meta-double-paste-and-cancel-guards
Aug 4, 2026
Merged

summeroff merged 2 commits into
masterfrom
fix/meta-double-paste-and-cancel-guards

Conversation

@summeroff

Copy link
Copy Markdown
Owner

Summary

  • Meta double-paste: Lexical was stacking a second (often newline-collapsed) copy because insertText was followed by InputEvent(inputType=insertFromPaste). Insert once without that event; Lexical prefers synthetic paste; duplicate sniff/repair no longer re-fires paste events.
  • Warm tabs: ensureTab no longer re-navigates same-origin AI apps on query/hash/conversation path differences (was forcing cold=1 every hotkey).
  • Cancel guards (new): if the form/composer is not ready within 10s, or the user switches focus/title away from the AI tab/window, abort paste instead of landing text on an unexpected page. Hard cancels do not fall through to UIA surprise-paste.

Config defaults

  • pageReadyTimeoutMs = 10000
  • pasteEvenIfNotReady = false
  • cancelOnFocusSwitch = true
  • INI: page_ready_timeout_ms, paste_even_if_not_ready, cancel_on_focus_switch

Test plan

  • node --check extension JS
  • Debug build + --self-test exit 0
  • AppData companion JS updated (user must Reload unpacked extension)
  • Manual: Ctrl+Alt+J into Meta — single clean prompt (no collapsed+full double)
  • Manual: start J, switch to another app/tab within 10s — paste cancelled, log shows focus/timeout cancel
  • Manual: leave Meta tab open — warm path (no cold=1 every time)

After merge

Reload Chrome companion on %LOCALAPPDATA%\QiuckPrompts\extension and restart tray if PE build is installed from this branch.

Stop Lexical stacking a second collapsed prompt copy after insertText
(no insertFromPaste fireInput; stronger duplicate repair). Keep same-origin
AI tabs warm instead of re-navigating every hotkey.

Cancel paste if the composer is not ready within 10s or the user leaves the
AI tab/window (extension + UIA). Hard cancels no longer fall back to a
surprise UIA paste on the wrong page.

Co-authored-by: Hermes/grok-4.5/m3rcur1al <hermes-m3rcur1al@local>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds safer AI prompt pasting by preventing Meta duplicate inserts, preserving warm tabs, and cancelling delayed pastes after focus changes or timeouts.

Changes:

  • Revises extension paste verification and duplicate repair.
  • Adds focus, tab, origin, and readiness cancellation guards.
  • Introduces configurable 10-second readiness defaults.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/workflow.cpp Integrates cancellation guards and extension failure handling.
src/page_ready.cpp Detects focus changes while awaiting page readiness.
src/config.cpp Parses new readiness and cancellation settings.
src/app.cpp Verifies new configuration defaults.
include/page_ready.hpp Adds focus cancellation to readiness configuration.
include/config.hpp Defines safer workflow defaults.
extension/content.js Revises paste, verification, timeout, and duplicate repair logic.
extension/background.js Preserves warm tabs and adds tab/origin guards.
Suppressed comments (2)

extension/content.js:702

  • A failed second repair only changes result to failure when the composer still looks duplicated. If both repair attempts leave the composer blank, truncated, or otherwise invalid, looksDuplicated is false and the original successful result is returned, so the tray reports success despite losing the prompt. Any repaired2 === false must fail the operation.
              const repaired2 = await repairToSingle(el, text);
              detail += repaired2 ? '+repaired' : '+repair_failed';
              if (!repaired2 && looksDuplicated(el, text)) {
                // Last resort: clear so user does not send a doubled prompt by accident.
                clearComposer(el);

extension/content.js:669

  • remain is calculated before the 250/500 ms sleep, then the retry wait is forced to at least 400 ms. For example, a cold retry with 450 ms remaining sleeps 500 ms and still waits another 400 ms before it may paste, so the advertised hard timeout can be exceeded. Recompute the budget after sleeping, abort when it is exhausted, and never pass a wait longer than the actual remaining time.
            await sleep(cold ? 500 : 250);
            waited = await waitForStableComposer(Math.max(400, remain - 500), {
              cold: true,
              wantOrigin,
            });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread extension/content.js
Comment on lines 149 to +153
if (Date.now() - start >= timeoutMs) {
resolve(lastEl); // may be null
resolve({
el: lastEl,
cancelReason: lastEl ? null : 'composer not found (timeout)',
});

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 70e702f — timeout path always returns el=null + composer not found (timeout); never hands back unstable lastEl.

Comment thread extension/background.js
Comment on lines +181 to +184
const t = await chrome.tabs.get(tabId);
if (!t) return { ok: false, error: 'tab gone' };
if (t.active === false) return { ok: false, error: 'tab inactive / focus switched' };
if (wantOrigin && t.url) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 70e702f — tabStillActiveOnOrigin now checks chrome.windows.get(...).focused; added windows permission. Also honors cancelOnFocusSwitch.

Comment thread src/workflow.cpp Outdated
Comment on lines 291 to 294
// Cap at page-ready timeout (default 10s). No +5s cushion — long waits → surprise paste.
const DWORD extTimeout =
static_cast<DWORD>((std::max)(cfg_.pageReadyTimeoutMs, 5000) + 5000);
static_cast<DWORD>((std::max)(1000, (std::min)(cfg_.pageReadyTimeoutMs, 60000)));
if (ExtBridge::Instance().PrepareAndPaste(url, payload, extTimeout, &detail, &extErr))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 70e702f — PrepareAndPaste JSON now includes cancelOnFocusSwitch from cfg; background/content skip focus-leave abort when false (timeout still applies).

- Timeout never returns unstable lastEl
- Failed second duplicate repair always fails the paste
- Retry budget recomputed after settle (hard 10s cap)
- Tab focus check uses chrome.windows.focused (+ windows permission)
- cancelOnFocusSwitch passed tray → NM → extension

Co-authored-by: Hermes/grok-4.5/m3rcur1al <hermes-m3rcur1al@local>
@summeroff

Copy link
Copy Markdown
Owner Author

Addressed Copilot in 70e702f:

  1. Timeout: no unstable lastEl
  2. Window focused check (+ windows perm)
  3. cancelOnFocusSwitch plumbed tray→ext
  4. (suppressed) second repair fail always fails paste
  5. (suppressed) retry budget recomputed after settle

Left open for re-review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Suppressed comments (4)

extension/content.js:169

  • Switching to another application does not normally make an active Chrome document hidden, so this per-tick guard remains clear and pasteWithVerify can mutate the composer. The background window check after sendMessage only discovers the switch after the paste has happened. Include document.hasFocus() in this pre-paste/poll guard so an OS focus switch cancels before mutation.
        if (document.hidden || document.visibilityState === 'hidden') {

src/workflow.cpp:73

  • The extension's new origin/tab guards return navigated off AI origin, tab gone, or browser window disappeared, but none match this hard-cancel list. Those explicit safety cancellations therefore fall through to the UIA path, which can paste into a newly focused page. Classify these states as hard cancellations as well.
        L"page not ready",

src/workflow.cpp:418

  • This guard runs only after ActivateBrowser, so a user focus switch during WaitForAiPageReady's final settle period is overwritten by reactivating the AI window and the guard then succeeds. When focus cancellation is enabled, do not reactivate before checking; preserve activation only for the opt-out path.
        if (!ForegroundOkForPaste(browser.hwnd, pr.titleHint, &focusWhy))

extension/background.js:278

  • This failure response contains both detail and error, but ExtBridge::PrepareAndPaste currently extracts detail first. The native workflow therefore sees the successful-paste detail instead of the focus-cancellation error, misses the hard-cancel classification, and can fall back to UIA for a second paste. Omit detail from this error response (or make the native bridge prefer error when ok is false).
          detail: resp.detail,

@summeroff
summeroff merged commit ada7f06 into master Aug 4, 2026
9 checks passed
@summeroff
summeroff deleted the fix/meta-double-paste-and-cancel-guards branch August 4, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants