Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions docs/windows-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 4 additions & 4 deletions src/browser/actions/promptComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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}));
Comment thread
JJLiebig marked this conversation as resolved.
const assistantVisible = Boolean(
document.querySelector(${assistantSelectorLiteral}) ||
document.querySelector('[data-testid*="assistant"]'),
Expand Down Expand Up @@ -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;
}
Expand Down
43 changes: 43 additions & 0 deletions tests/browser/promptComposer.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
Loading