diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ca2187de..5152b2bf1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ### Fixed +- Recognize a cleared composer and active ChatGPT Stop control as a submitted + prompt when conversation turn selectors miss the new turn. - Point installation instructions and package/plugin links at `JJLiebig/ask-pro`. - Select ChatGPT’s rolling `Latest` model with `Pro` intelligence. - Recognize active tool work as thinking, exclude tool-status panels from answers, diff --git a/docs/windows-work.md b/docs/windows-work.md index b202b1976d..3cc77192ba 100644 --- a/docs/windows-work.md +++ b/docs/windows-work.md @@ -73,3 +73,6 @@ Future Windows gotchas belong here. literal `Latest` option and keep the reasoning slider on `Pro`. Tool work appears inside `[data-streaming-response-status]`; it is progress, not answer content. Refreshing the plugin leaves existing immutable session runtimes running. +- September 23: ChatGPT can clear the composer and start generating before its + current conversation turn selector reports a new turn. A visible Stop control + confirms submission in that case; a hidden matching element does not. diff --git a/src/browser/actions/promptComposer.ts b/src/browser/actions/promptComposer.ts index 67eb0699a0..6375985461 100644 --- a/src/browser/actions/promptComposer.ts +++ b/src/browser/actions/promptComposer.ts @@ -804,7 +804,8 @@ async function verifyPromptCommitted( const isVisible = (node) => { if (!node || typeof node.getBoundingClientRect !== 'function') return false; const rect = node.getBoundingClientRect(); - return rect.width > 0 && rect.height > 0; + const style = window.getComputedStyle(node); + return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none'; }; const inputs = inputSelectors .map((selector) => document.querySelector(selector)) @@ -823,7 +824,7 @@ async function verifyPromptCommitted( (normalizedPromptPrefix.length > 30 && lastTurn.includes(normalizedPromptPrefix))); const baseline = ${baselineLiteral}; const hasNewTurn = baseline < 0 ? false : normalizedTurns.length > baseline; - const stopVisible = Boolean(document.querySelector(${stopSelectorLiteral})); + const stopVisible = isVisible(document.querySelector(${stopSelectorLiteral})); const assistantVisible = Boolean( document.querySelector(${assistantSelectorLiteral}) || document.querySelector('[data-testid*="assistant"]'), @@ -908,8 +909,7 @@ async function verifyPromptCommitted( } const fallbackCommit = info?.composerCleared && - Boolean(info?.hasNewTurn) && - ((info?.stopVisible ?? false) || info?.assistantVisible || info?.inConversation); + (info?.stopVisible || (info?.hasNewTurn && (info?.assistantVisible || info?.inConversation))); if (fallbackCommit) { return typeof turnsCount === "number" && Number.isFinite(turnsCount) ? turnsCount : null; } diff --git a/tests/browser/promptComposer.test.ts b/tests/browser/promptComposer.test.ts index 4a22765110..d1e296309d 100644 --- a/tests/browser/promptComposer.test.ts +++ b/tests/browser/promptComposer.test.ts @@ -1,7 +1,50 @@ +import { runInNewContext } from "node:vm"; import { describe, expect, test, vi } from "vitest"; import { __test__ } from "../../src/browser/actions/promptComposer.js"; describe("prompt composer actions", () => { + test("accepts an active answer when turn selectors miss the submitted prompt", async () => { + const evaluate = vi.fn(async (_: { expression: string }) => ({ + result: { + value: { + baseline: 0, + turnsCount: 0, + hasNewTurn: false, + composerCleared: true, + stopVisible: true, + }, + }, + })); + + await expect( + __test__.verifyPromptCommitted({ evaluate } as never, "question", 100, undefined, 0), + ).resolves.toBe(0); + expect(evaluate).toHaveBeenCalledTimes(1); + expect(evaluate.mock.calls[0]?.[0]?.expression).toContain("isVisible(document.querySelector"); + }); + + test("does not accept a cleared composer without visible submission evidence", async () => { + const evaluate = vi.fn(async ({ expression }: { expression: string }) => ({ + result: { + value: runInNewContext(expression, { + document: { + querySelector: (selector: string) => + selector.includes("stop-button") + ? { getBoundingClientRect: () => ({ width: 20, height: 20 }) } + : null, + querySelectorAll: () => [], + }, + window: { getComputedStyle: () => ({ visibility: "hidden", display: "block" }) }, + location: { href: "https://chatgpt.com/c/test" }, + }), + }, + })); + + await expect( + __test__.verifyPromptCommitted({ evaluate } as never, "question", 1, undefined, 0), + ).rejects.toThrow("Prompt did not appear in conversation"); + }); + test("arms post-submit guard before defocus and pointer cleanup", async () => { const order: string[] = []; const evaluate = vi.fn(async () => {