From 39d133ddfb1a17a5376938c90e0f108ae6396881 Mon Sep 17 00:00:00 2001 From: type-two Date: Tue, 7 Jul 2026 18:54:05 +1000 Subject: [PATCH] fix: soft-detect submission + only NEW completion messages count - countDoneMsgs baseline before submit: lingering 'has been scheduled' chatter from the PREVIOUS gen no longer triggers an instant post-gen reload (the self-reload bug) - promptEchoed: chat echoing the prompt = strongest it-stuck signal, added to the post-Enter check; and on retry it means the earlier submit registered -> skip re-typing entirely (no double generation / double credits) Co-Authored-By: Claude Fable 5 --- content.js | 65 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/content.js b/content.js index 17c660e..d39d43d 100644 --- a/content.js +++ b/content.js @@ -221,13 +221,24 @@ function persistentError() { (e.textContent || '').length < 180); } -// The agent's end-of-generation chatter. John's law: this message LIES — "scheduled / -// waiting in the queue due to high demand" often means it's actually DONE, and the -// stale page won't show the asset until a refresh. -function agentSaysDone() { - return [...document.querySelectorAll('*')].some(e => visible(e) && - /has been scheduled|waiting in the queue|check back in a few|is ready|i'?ve (generated|created)|here('s| is) (your|the) video/i.test(e.textContent || '') && - (e.textContent || '').length < 240); +// The agent's end-of-generation chatter. John's law #1: this message LIES — "scheduled / +// waiting in the queue" usually means it's actually DONE (refresh to see it). +// John's law #2: OLD messages linger in the chat history, so never match absolutely — +// COUNT matches and only act when a NEW one appears after our submit. +function countDoneMsgs() { + const m = (document.body.innerText || '').match( + /has been scheduled|waiting in the queue|check back in a few|is ready|i'?ve (generated|created)|here('s| is) (your|the) video/gi); + return m ? m.length : 0; +} + +// The chat panel echoes the submitted prompt — the strongest "it stuck" signal, and +// on a retry it means the PREVIOUS submit registered (so don't type it again). +function promptEchoed(prompt) { + const needle = String(prompt).slice(0, 60).trim(); + if (!needle) return false; + return [...document.querySelectorAll('div, p, span')].some(e => visible(e) && + !e.closest('[contenteditable="true"], textarea') && + (e.textContent || '').includes(needle) && (e.textContent || '').length < needle.length + 500); } // Flow's "Try again" button, shown beside the "Something went wrong" banner. @@ -348,25 +359,31 @@ async function executeTask(task) { if (!input) return await requeueWithReload(task, 'agent input never became ready'); const before = snapshotAssets(); + const doneBase = countDoneMsgs(); // old "finished" chatter lingers — only a NEW one counts - const landed = await typeIntoEditor(input, task.prompt); - if (!landed) return await requeueWithReload(task, 'prompt did not register in the editor'); - log('info', `Prompt set (${landed} chars). Submitting…`); - await sleep(600); // John's ritual: paste, wait a sec, THEN enter + // Soft-detect first: if the chat already echoes this prompt, the PREVIOUS attempt's + // submit registered before its reload — re-typing would double-generate. Skip to waiting. + const alreadySubmitted = promptEchoed(task.prompt); + if (alreadySubmitted) { + log('info', 'Prompt already echoed in the session (earlier submit registered) — not re-typing.'); + } else { + const landed = await typeIntoEditor(input, task.prompt); + if (!landed) return await requeueWithReload(task, 'prompt did not register in the editor'); + log('info', `Prompt set (${landed} chars). Submitting…`); + await sleep(600); // John's ritual: paste, wait a sec, THEN enter - await submitEditor(input); // trusted Enter via the DevTools Protocol — this submits - // Success signal = the square stop button appears (or the box empties). If neither, - // try the labelled submit button once, then hard-refresh — Enter into a wedged - // session never recovers on its own. - let took = await waitFor(() => findStopSquare() || editorText(input).length === 0, 6000, 300); - if (!took) { - const submit = findSubmit(input); - if (submit) { submit.click(); log('info', 'Enter fallback → clicked submit.'); } - took = await waitFor(() => findStopSquare() || editorText(input).length === 0, 6000, 300); + await submitEditor(input); // trusted Enter via the DevTools Protocol — this submits + // "It stuck" = stop square appears, OR the box empties, OR the chat echoes the prompt. + const stuck = () => findStopSquare() || editorText(input).length === 0 || promptEchoed(task.prompt); + let took = await waitFor(stuck, 6000, 300); + if (!took) { + const submit = findSubmit(input); + if (submit) { submit.click(); log('info', 'Enter fallback → clicked submit.'); } + took = await waitFor(stuck, 6000, 300); + } + if (!took) return await requeueWithReload(task, 'submission never stuck (no stop square, no echo, prompt still in box)'); + log('info', 'Submitted (soft-detect confirmed). Waiting for generation…'); } - if (!took) return await requeueWithReload(task, 'submission never stuck (no stop button, prompt still in box)'); - - log('info', 'Submitted (stop square seen). Waiting for generation…'); const timeoutMs = task.kind === 'image' ? 3 * 60 * 1000 : settings.genTimeoutMs; @@ -379,7 +396,7 @@ async function executeTask(task) { let signal = null; while (Date.now() - t0 < timeoutMs) { if (snapshotAssets().size > before.size) { signal = 'asset in grid'; break; } - if (agentSaysDone()) { signal = 'agent completion message'; break; } + if (countDoneMsgs() > doneBase) { signal = 'NEW agent completion message'; break; } if (Date.now() - t0 > 6000 && persistentError()) { await handleRateLimit(task); return; } if (Date.now() - t0 > 8000 && (findTryAgain() || flowError())) return await requeueWithReload(task, 'Flow errored during video generation');