fix: Meta double-paste + cancel on timeout/focus switch - #21
Conversation
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>
There was a problem hiding this comment.
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
resultto failure when the composer still looks duplicated. If both repair attempts leave the composer blank, truncated, or otherwise invalid,looksDuplicatedis false and the original successfulresultis returned, so the tray reports success despite losing the prompt. Anyrepaired2 === falsemust 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
remainis 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.
| if (Date.now() - start >= timeoutMs) { | ||
| resolve(lastEl); // may be null | ||
| resolve({ | ||
| el: lastEl, | ||
| cancelReason: lastEl ? null : 'composer not found (timeout)', | ||
| }); |
There was a problem hiding this comment.
Fixed in 70e702f — timeout path always returns el=null + composer not found (timeout); never hands back unstable lastEl.
| 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) { |
There was a problem hiding this comment.
Fixed in 70e702f — tabStillActiveOnOrigin now checks chrome.windows.get(...).focused; added windows permission. Also honors cancelOnFocusSwitch.
| // 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)) |
There was a problem hiding this comment.
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>
|
Addressed Copilot in 70e702f:
Left open for re-review. |
There was a problem hiding this comment.
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 andpasteWithVerifycan mutate the composer. The background window check aftersendMessageonly discovers the switch after the paste has happened. Includedocument.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, orbrowser 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 duringWaitForAiPageReady'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
detailanderror, butExtBridge::PrepareAndPastecurrently extractsdetailfirst. 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. Omitdetailfrom this error response (or make the native bridge prefererrorwhenokis false).
detail: resp.detail,
Summary
insertTextwas followed byInputEvent(inputType=insertFromPaste). Insert once without that event; Lexical prefers synthetic paste; duplicate sniff/repair no longer re-fires paste events.ensureTabno longer re-navigates same-origin AI apps on query/hash/conversation path differences (was forcingcold=1every hotkey).Config defaults
pageReadyTimeoutMs= 10000pasteEvenIfNotReady= falsecancelOnFocusSwitch= truepage_ready_timeout_ms,paste_even_if_not_ready,cancel_on_focus_switchTest plan
node --checkextension JS--self-testexit 0cold=1every time)After merge
Reload Chrome companion on
%LOCALAPPDATA%\QiuckPrompts\extensionand restart tray if PE build is installed from this branch.