@steipete/oracle 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/oracle-cli.js +1 -1
- package/dist/scripts/test-browser.js +13 -2
- package/dist/src/browser/actions/assistantResponse.js +235 -121
- package/dist/src/browser/actions/attachments.js +3 -4
- package/dist/src/browser/actions/deepResearch.js +6 -6
- package/dist/src/browser/actions/modelSelection.js +133 -22
- package/dist/src/browser/actions/navigation.js +235 -14
- package/dist/src/browser/actions/promptComposer.js +4 -5
- package/dist/src/browser/actions/thinkingStatus.js +228 -0
- package/dist/src/browser/actions/thinkingTime.js +68 -11
- package/dist/src/browser/chatgptFiles.js +4 -7
- package/dist/src/browser/chatgptImages.js +3 -4
- package/dist/src/browser/chromeLifecycle.js +30 -28
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/constants.js +6 -1
- package/dist/src/browser/controlPlan.js +2 -2
- package/dist/src/browser/conversationTurns.js +16 -0
- package/dist/src/browser/conversationUrlMonitor.js +64 -0
- package/dist/src/browser/cookies.js +72 -0
- package/dist/src/browser/index.js +121 -72
- package/dist/src/browser/liveTabs.js +113 -31
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/projectSourcesRunner.js +7 -6
- package/dist/src/browser/reattach.js +29 -13
- package/dist/src/browser/reattachHelpers.js +14 -5
- package/dist/src/browser/recoverConversation.js +90 -29
- package/dist/src/cli/browserConfig.js +5 -1
- package/dist/src/cli/browserTabs.js +54 -33
- package/dist/src/cli/options.js +24 -0
- package/dist/src/cli/runOptions.js +6 -1
- package/dist/src/cli/sessionDisplay.js +38 -14
- package/dist/src/oracle/config.js +25 -0
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/oracle/oscProgress.js +3 -2
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +21 -22
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -569
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -416
- package/dist/docs-site/browser-mode.html +0 -594
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -462
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -375
- package/dist/docs-site/gemini.html +0 -383
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -596
- package/dist/docs-site/mcp.html +0 -391
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -399
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/windowsCookies.js +0 -219
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -227,7 +227,7 @@ program
|
|
|
227
227
|
.addOption(new Option("--copy-markdown", "Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.").default(false))
|
|
228
228
|
.addOption(new Option("--copy").hideHelp().default(false))
|
|
229
229
|
.option("-s, --slug <words>", "Custom session slug (3-5 words).")
|
|
230
|
-
.option("-m, --model <model>", 'Model to target (gpt-5.5-pro default). Also gpt-5.5, gpt-5.4-pro, gpt-5.4, gpt-5.1-pro, gpt-5-pro, gpt-5.1, gpt-5.1-codex API-only, gpt-5.2, gpt-5.2-instant, gpt-5.2-pro, gemini-3.1-flash-lite, gemini-3.5-flash, gemini-3.1-pro, legacy gemini-3-pro, claude-4.6-sonnet, claude-4.1-opus, or ChatGPT labels like "5.5 Pro" / "5.2 Thinking" for browser runs).', normalizeModelOption)
|
|
230
|
+
.option("-m, --model <model>", 'Model to target (gpt-5.5-pro default). GPT-5.6 aliases: gpt-5.6 and gpt-5.6-sol (OpenAI API or ChatGPT browser). Also gpt-5.5, gpt-5.4-pro, gpt-5.4, gpt-5.1-pro, gpt-5-pro, gpt-5.1, gpt-5.1-codex API-only, gpt-5.2, gpt-5.2-instant, gpt-5.2-pro, gemini-3.1-flash-lite, gemini-3.5-flash, gemini-3.1-pro, legacy gemini-3-pro, claude-4.6-sonnet, claude-4.1-opus, or ChatGPT labels like "5.5 Pro" / "5.2 Thinking" for browser runs).', normalizeModelOption)
|
|
231
231
|
.addOption(new Option("--models <models>", 'Comma-separated API model list to query in parallel (e.g., "gpt-5.5-pro,gemini-3-pro").')
|
|
232
232
|
.argParser(collectModelList)
|
|
233
233
|
.default([]))
|
|
@@ -58,9 +58,9 @@ function firewallHint(host, devtoolsPort) {
|
|
|
58
58
|
"Re-run ./runner pnpm test:browser after adding the rule.",
|
|
59
59
|
].join("\n");
|
|
60
60
|
}
|
|
61
|
-
async function fetchVersion(host, devtoolsPort) {
|
|
61
|
+
async function fetchVersion(host, devtoolsPort, timeoutMs = 5000) {
|
|
62
62
|
const controller = new AbortController();
|
|
63
|
-
const timer = setTimeout(() => controller.abort(),
|
|
63
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
64
64
|
try {
|
|
65
65
|
const res = await fetch(`http://${host}:${devtoolsPort}/json/version`, {
|
|
66
66
|
signal: controller.signal,
|
|
@@ -77,6 +77,16 @@ async function fetchVersion(host, devtoolsPort) {
|
|
|
77
77
|
clearTimeout(timer);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
async function waitForDevToolsShutdown(host, devtoolsPort) {
|
|
81
|
+
const deadline = Date.now() + 5000;
|
|
82
|
+
while (Date.now() < deadline) {
|
|
83
|
+
if (!(await fetchVersion(host, devtoolsPort, 250))) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await sleep(100);
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`DevTools did not stop listening at ${host}:${devtoolsPort}`);
|
|
89
|
+
}
|
|
80
90
|
async function main() {
|
|
81
91
|
console.log(`[browser-test] launching Chrome on ${targetHost}:${port} (headful)…`);
|
|
82
92
|
const chrome = await launch({
|
|
@@ -89,6 +99,7 @@ async function main() {
|
|
|
89
99
|
ok = await fetchVersion(targetHost, chrome.port);
|
|
90
100
|
}
|
|
91
101
|
await chrome.kill();
|
|
102
|
+
await waitForDevToolsShutdown(targetHost, chrome.port);
|
|
92
103
|
if (ok) {
|
|
93
104
|
console.log(`[browser-test] PASS: DevTools responding on ${targetHost}:${chrome.port}`);
|
|
94
105
|
process.exit(0);
|
|
@@ -1,9 +1,61 @@
|
|
|
1
1
|
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTORS, } from "../constants.js";
|
|
2
|
+
import { buildConversationTurnListExpression } from "../conversationTurns.js";
|
|
3
|
+
import { buildThinkingActivePredicateJs, readThinkingActivity } from "./thinkingStatus.js";
|
|
2
4
|
import { delay } from "../utils.js";
|
|
3
5
|
import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression, } from "../domDebug.js";
|
|
4
6
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
5
7
|
const ASSISTANT_POLL_TIMEOUT_ERROR = "assistant-response-watchdog-timeout";
|
|
6
8
|
const STOP_CONTROL_SELECTOR = STOP_BUTTON_SELECTORS.join(", ");
|
|
9
|
+
// Still used by the in-page settle heuristic's length buckets (see buildResponseObserverExpression).
|
|
10
|
+
const MIN_CONFIDENT_ANSWER_LENGTH = 16;
|
|
11
|
+
function readPositiveIntEnv(name, fallback) {
|
|
12
|
+
const raw = Number(process.env[name]);
|
|
13
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
|
|
14
|
+
}
|
|
15
|
+
const TERMINAL_GATE_CONFIG = {
|
|
16
|
+
barConfirmCycles: readPositiveIntEnv("ORACLE_BAR_CONFIRM_CYCLES", 3),
|
|
17
|
+
minStableMs: readPositiveIntEnv("ORACLE_TERMINAL_MIN_STABLE_MS", 1_200),
|
|
18
|
+
};
|
|
19
|
+
export function createTerminalGateState(now) {
|
|
20
|
+
return {
|
|
21
|
+
lastKey: "",
|
|
22
|
+
lastChangeAt: now,
|
|
23
|
+
barStableCycles: 0,
|
|
24
|
+
seen: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Pure, unit-testable per-cycle classifier. Feed it one sample every poll; when it returns
|
|
28
|
+
// terminal:true the capture is proven complete and safe to finalize.
|
|
29
|
+
export function classifyTurnTerminal(state, sample, config) {
|
|
30
|
+
const changed = !state.seen || sample.contentKey !== state.lastKey;
|
|
31
|
+
const lastChangeAt = changed ? sample.now : state.lastChangeAt;
|
|
32
|
+
// proofA debounce: weak/stale sidecar evidence may be overridden, but strong live activity
|
|
33
|
+
// must reset the debounce. It also resets on ANY content change so a bar that appears while
|
|
34
|
+
// the answer is still rendering (the transient-bar / first-tokens race) cannot finalize.
|
|
35
|
+
const barStableCycles = sample.barVisible && !sample.stopVisible && !sample.strongThinkingActive && !changed
|
|
36
|
+
? state.barStableCycles + 1
|
|
37
|
+
: 0;
|
|
38
|
+
const next = {
|
|
39
|
+
lastKey: sample.contentKey,
|
|
40
|
+
lastChangeAt,
|
|
41
|
+
barStableCycles,
|
|
42
|
+
seen: true,
|
|
43
|
+
};
|
|
44
|
+
let terminal = false;
|
|
45
|
+
if (!sample.stopVisible && sample.len > 0) {
|
|
46
|
+
const stableMs = sample.now - lastChangeAt;
|
|
47
|
+
// Debounced action bar AND content stable for a minimum time. The time-stability
|
|
48
|
+
// requirement guards the documented race where finished-action controls surface while only
|
|
49
|
+
// the first tokens have rendered. Weak sidecar evidence cannot hang a finished turn, but
|
|
50
|
+
// strong live activity vetoes this proof and restarts its debounce.
|
|
51
|
+
terminal =
|
|
52
|
+
sample.barVisible &&
|
|
53
|
+
!sample.strongThinkingActive &&
|
|
54
|
+
barStableCycles >= config.barConfirmCycles &&
|
|
55
|
+
stableMs >= config.minStableMs;
|
|
56
|
+
}
|
|
57
|
+
return { state: next, terminal };
|
|
58
|
+
}
|
|
7
59
|
const THINKING_STATUS_LABELS = [
|
|
8
60
|
"thinking",
|
|
9
61
|
"pro thinking",
|
|
@@ -25,12 +77,14 @@ function matchesThinkingStatusLabel(trimmed) {
|
|
|
25
77
|
return false;
|
|
26
78
|
if (THINKING_STATUS_LABELS.includes(trimmed))
|
|
27
79
|
return true;
|
|
28
|
-
|
|
80
|
+
// includes, not startsWith: the completed summary can carry a heading prefix
|
|
81
|
+
// ("Reasoning Thought for 12s"); the length cap keeps real answers out.
|
|
82
|
+
if (trimmed.includes("thought for ") && trimmed.length <= 40)
|
|
29
83
|
return true;
|
|
30
84
|
return trimmed.startsWith("pro thinking") && trimmed.length <= 40;
|
|
31
85
|
}
|
|
32
|
-
function isAnswerNowPlaceholderText(
|
|
33
|
-
const text =
|
|
86
|
+
export function isAnswerNowPlaceholderText(value) {
|
|
87
|
+
const text = value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
34
88
|
if (!text)
|
|
35
89
|
return false;
|
|
36
90
|
// Learned: "Pro thinking" shows a placeholder turn that contains "Answer now".
|
|
@@ -52,7 +106,7 @@ function buildActiveThinkingStatusPredicateJs(fnName) {
|
|
|
52
106
|
const labels = ${labelsLiteral};
|
|
53
107
|
const matches =
|
|
54
108
|
labels.includes(normalized) ||
|
|
55
|
-
(normalized.
|
|
109
|
+
(normalized.includes('thought for ') && normalized.length <= 40) ||
|
|
56
110
|
(normalized.startsWith('pro thinking') && normalized.length <= 40);
|
|
57
111
|
return matches && isStopControlVisible();
|
|
58
112
|
};`;
|
|
@@ -109,7 +163,9 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
109
163
|
if (source === "poll" &&
|
|
110
164
|
error instanceof Error &&
|
|
111
165
|
error.message === ASSISTANT_POLL_TIMEOUT_ERROR) {
|
|
112
|
-
|
|
166
|
+
evaluationPromise.catch(() => undefined);
|
|
167
|
+
await terminateRuntimeExecution(Runtime);
|
|
168
|
+
throw error;
|
|
113
169
|
}
|
|
114
170
|
else if (source === "poll") {
|
|
115
171
|
throw error;
|
|
@@ -159,29 +215,32 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
159
215
|
logger("Captured assistant generated image response");
|
|
160
216
|
return candidate;
|
|
161
217
|
}
|
|
162
|
-
// The
|
|
218
|
+
// The observer/refresh path can race ahead of true completion: a settled GPT-5.5 Pro
|
|
219
|
+
// preamble (or any mid-stream capture) looks done for a moment before the reasoning/tool
|
|
220
|
+
// phase begins. Re-confirm EVERY captured text through the terminal-only poller, which
|
|
221
|
+
// finalizes only on positive proof (a debounced action bar, or a quiet window with no
|
|
222
|
+
// active thinking). We deliberately drop the old ">= candidate length" acceptance: the
|
|
223
|
+
// poller is turn-scoped (minTurnIndex), so whatever it proves terminal is the right turn,
|
|
224
|
+
// even when the real answer is shorter than a verbose preamble.
|
|
163
225
|
const elapsedMs = Date.now() - start;
|
|
164
226
|
const remainingMs = Math.max(0, timeoutMs - elapsedMs);
|
|
165
227
|
if (remainingMs > 0) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// Completion controls can appear briefly while Pro is still replacing its thinking UI.
|
|
171
|
-
// Confirm every capture from that transition with the stability-based watchdog; a
|
|
172
|
-
// partial first paragraph can be arbitrarily long.
|
|
173
|
-
const candidateText = String(candidate?.text ?? "").trim();
|
|
174
|
-
if (stopVisible || completionVisible) {
|
|
175
|
-
logger(stopVisible
|
|
176
|
-
? "Assistant still generating; waiting for completion"
|
|
177
|
-
: "Completion controls surfaced; confirming stable assistant response");
|
|
178
|
-
const completed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex, expectedConversationId);
|
|
179
|
-
if (completed && String(completed.text ?? "").trim().length >= candidateText.length) {
|
|
180
|
-
return completed;
|
|
181
|
-
}
|
|
228
|
+
logger("Confirming the capture is terminal (not a mid-stream/preamble capture)");
|
|
229
|
+
const completed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex, expectedConversationId);
|
|
230
|
+
if (completed) {
|
|
231
|
+
return completed;
|
|
182
232
|
}
|
|
233
|
+
// Could not prove completion within the budget: refuse rather than finalize a possibly
|
|
234
|
+
// incomplete capture. A clean, fast failure is recoverable (retry/salvage) and never
|
|
235
|
+
// ships a preamble as if it were the answer.
|
|
236
|
+
await logDomFailure(Runtime, logger, "assistant-response-unconfirmed");
|
|
237
|
+
throw new Error("assistant-response could not be confirmed complete before timeout; refusing to finalize a possibly-incomplete capture");
|
|
183
238
|
}
|
|
184
|
-
|
|
239
|
+
// Budget already exhausted before we could confirm: refuse rather than fall through and ship
|
|
240
|
+
// an unconfirmed capture. A settled preamble that arrived near the deadline must not be
|
|
241
|
+
// finalized just because there was no time left to prove it terminal.
|
|
242
|
+
await logDomFailure(Runtime, logger, "assistant-response-unconfirmed");
|
|
243
|
+
throw new Error("assistant-response could not be confirmed complete before the deadline; refusing to finalize a possibly-incomplete capture");
|
|
185
244
|
}
|
|
186
245
|
export async function readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId) {
|
|
187
246
|
const { result } = await Runtime.evaluate({
|
|
@@ -243,13 +302,31 @@ async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex
|
|
|
243
302
|
if (recoveryTimeoutMs === 0) {
|
|
244
303
|
return null;
|
|
245
304
|
}
|
|
305
|
+
const recoveryStartedAt = Date.now();
|
|
246
306
|
const recovered = await waitForCondition(async () => {
|
|
247
307
|
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId);
|
|
248
308
|
return normalizeAssistantSnapshot(snapshot);
|
|
249
309
|
}, recoveryTimeoutMs, 400);
|
|
250
310
|
if (recovered) {
|
|
251
|
-
|
|
252
|
-
return
|
|
311
|
+
// Route EVERY recovered snapshot through the terminal-only poller (not just short ones):
|
|
312
|
+
// a recovered long preamble is exactly the raw-return bug this gate exists to prevent.
|
|
313
|
+
logger("Recovered a candidate response; confirming it is terminal before finalizing");
|
|
314
|
+
const remainingMs = Math.max(0, recoveryTimeoutMs - (Date.now() - recoveryStartedAt));
|
|
315
|
+
if (remainingMs > 0) {
|
|
316
|
+
const confirmed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex, expectedConversationId);
|
|
317
|
+
if (confirmed) {
|
|
318
|
+
logger("Recovered and confirmed assistant response via polling fallback");
|
|
319
|
+
return confirmed;
|
|
320
|
+
}
|
|
321
|
+
// Unconfirmable within budget: refuse (return null) so the caller fails fast instead
|
|
322
|
+
// of finalizing a possibly-incomplete recovered capture.
|
|
323
|
+
await logConversationSnapshot(Runtime, logger).catch(() => undefined);
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
// No confirmation time left: refuse rather than return the unconfirmed recovered snapshot
|
|
327
|
+
// (returning it raw would reopen the recovered-long-preamble leak this gate closes).
|
|
328
|
+
await logConversationSnapshot(Runtime, logger).catch(() => undefined);
|
|
329
|
+
return null;
|
|
253
330
|
}
|
|
254
331
|
await logConversationSnapshot(Runtime, logger).catch(() => undefined);
|
|
255
332
|
return null;
|
|
@@ -337,9 +414,7 @@ async function terminateRuntimeExecution(Runtime) {
|
|
|
337
414
|
}
|
|
338
415
|
async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expectedConversationId, abortSignal) {
|
|
339
416
|
const watchdogDeadline = Date.now() + timeoutMs;
|
|
340
|
-
let
|
|
341
|
-
let stableCycles = 0;
|
|
342
|
-
let lastChangeAt = Date.now();
|
|
417
|
+
let gate = createTerminalGateState(Date.now());
|
|
343
418
|
while (Date.now() < watchdogDeadline) {
|
|
344
419
|
// Check abort signal to stop polling when another path won the race
|
|
345
420
|
if (abortSignal?.aborted) {
|
|
@@ -348,44 +423,34 @@ async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expecte
|
|
|
348
423
|
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId);
|
|
349
424
|
const normalized = normalizeAssistantSnapshot(snapshot);
|
|
350
425
|
if (normalized) {
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
stableCycles = 0;
|
|
355
|
-
lastChangeAt = Date.now();
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
stableCycles += 1;
|
|
426
|
+
// Generated-image answers stream no text and mount no action bar; accept immediately.
|
|
427
|
+
if (isGeneratedImageAssistantAnswer(normalized)) {
|
|
428
|
+
return normalized;
|
|
359
429
|
}
|
|
360
|
-
const [stopVisible,
|
|
430
|
+
const [stopVisible, barVisible, thinkingActivity] = await Promise.all([
|
|
361
431
|
isStopButtonVisible(Runtime),
|
|
362
|
-
isCompletionVisible(Runtime),
|
|
432
|
+
isCompletionVisible(Runtime, normalized.meta, minTurnIndex),
|
|
433
|
+
readThinkingActivity(Runtime),
|
|
363
434
|
]);
|
|
364
|
-
|
|
435
|
+
const decision = classifyTurnTerminal(gate, {
|
|
436
|
+
now: Date.now(),
|
|
437
|
+
len: normalized.text.length,
|
|
438
|
+
// Fingerprint = turn/message identity + the full text, so a same-length rewrite, a
|
|
439
|
+
// shorter final answer replacing a longer preamble, or a new turn all count as change.
|
|
440
|
+
contentKey: `${normalized.meta.messageId ?? normalized.meta.turnId ?? ""}::${normalized.text}`,
|
|
441
|
+
stopVisible,
|
|
442
|
+
barVisible,
|
|
443
|
+
strongThinkingActive: thinkingActivity.strong,
|
|
444
|
+
}, TERMINAL_GATE_CONFIG);
|
|
445
|
+
gate = decision.state;
|
|
446
|
+
if (decision.terminal) {
|
|
365
447
|
return normalized;
|
|
366
448
|
}
|
|
367
|
-
const shortAnswer = currentLength > 0 && currentLength < 16;
|
|
368
|
-
const mediumAnswer = currentLength >= 16 && currentLength < 40;
|
|
369
|
-
const longAnswer = currentLength >= 40 && currentLength < 500;
|
|
370
|
-
// Learned: short answers need a longer stability window or they truncate.
|
|
371
|
-
// Learned: long streaming responses (esp. thinking models) can pause mid-stream;
|
|
372
|
-
// use progressively longer windows to avoid truncation (#71).
|
|
373
|
-
const completionStableTarget = shortAnswer ? 12 : mediumAnswer ? 8 : longAnswer ? 6 : 8;
|
|
374
|
-
const requiredStableCycles = shortAnswer ? 12 : mediumAnswer ? 8 : longAnswer ? 8 : 10;
|
|
375
|
-
const stableMs = Date.now() - lastChangeAt;
|
|
376
|
-
const minStableMs = shortAnswer ? 8000 : mediumAnswer ? 1200 : longAnswer ? 2000 : 3000;
|
|
377
|
-
// Require stop button to disappear before treating completion as final.
|
|
378
|
-
if (!stopVisible) {
|
|
379
|
-
const stableEnough = stableCycles >= requiredStableCycles && stableMs >= minStableMs;
|
|
380
|
-
const completionEnough = completionVisible && stableCycles >= completionStableTarget && stableMs >= minStableMs;
|
|
381
|
-
if (completionEnough || stableEnough) {
|
|
382
|
-
return normalized;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
449
|
}
|
|
386
450
|
else {
|
|
387
|
-
|
|
388
|
-
|
|
451
|
+
// The turn disappeared/reset (navigation, re-render): restart the gate so a stale
|
|
452
|
+
// action-bar debounce cannot carry over onto a fresh turn.
|
|
453
|
+
gate = createTerminalGateState(Date.now());
|
|
389
454
|
}
|
|
390
455
|
await delay(400);
|
|
391
456
|
}
|
|
@@ -427,43 +492,70 @@ function buildStopButtonVisibilityPredicateJs(fnName) {
|
|
|
427
492
|
};`;
|
|
428
493
|
}
|
|
429
494
|
export const buildStopButtonVisibilityExpressionForTest = buildStopButtonVisibilityExpression;
|
|
430
|
-
|
|
495
|
+
function buildCompletionVisibilityExpression(meta, minTurnIndex) {
|
|
496
|
+
const expectedMessageId = meta.messageId ? JSON.stringify(meta.messageId) : "null";
|
|
497
|
+
const expectedTurnId = meta.turnId ? JSON.stringify(meta.turnId) : "null";
|
|
498
|
+
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
499
|
+
? Math.floor(minTurnIndex)
|
|
500
|
+
: -1;
|
|
501
|
+
return `(() => {
|
|
502
|
+
const EXPECTED_MESSAGE_ID = ${expectedMessageId};
|
|
503
|
+
const EXPECTED_TURN_ID = ${expectedTurnId};
|
|
504
|
+
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
505
|
+
// Find the LAST assistant turn to check completion status. Must match the same logic as
|
|
506
|
+
// buildAssistantExtractor, then correlate the controls to the sampled response.
|
|
507
|
+
const ASSISTANT_SELECTOR = '${ASSISTANT_ROLE_SELECTOR}';
|
|
508
|
+
const isAssistantTurn = (node) => {
|
|
509
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
510
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
511
|
+
if (turnAttr === 'assistant') return true;
|
|
512
|
+
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
513
|
+
if (role === 'assistant') return true;
|
|
514
|
+
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
515
|
+
if (testId.includes('assistant')) return true;
|
|
516
|
+
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
520
|
+
let lastAssistantTurn = null;
|
|
521
|
+
let lastAssistantIndex = -1;
|
|
522
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
523
|
+
if (isAssistantTurn(turns[i])) {
|
|
524
|
+
lastAssistantTurn = turns[i];
|
|
525
|
+
lastAssistantIndex = i;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (!lastAssistantTurn) return false;
|
|
530
|
+
|
|
531
|
+
const hasExpectedIdentity = Boolean(EXPECTED_MESSAGE_ID || EXPECTED_TURN_ID);
|
|
532
|
+
if (hasExpectedIdentity) {
|
|
533
|
+
const identityNodes = [
|
|
534
|
+
lastAssistantTurn,
|
|
535
|
+
...Array.from(lastAssistantTurn.querySelectorAll('[data-message-id], [data-testid]')),
|
|
536
|
+
];
|
|
537
|
+
const identityMatches = identityNodes.some((node) =>
|
|
538
|
+
(EXPECTED_MESSAGE_ID && node.getAttribute?.('data-message-id') === EXPECTED_MESSAGE_ID) ||
|
|
539
|
+
(EXPECTED_TURN_ID && node.getAttribute?.('data-testid') === EXPECTED_TURN_ID),
|
|
540
|
+
);
|
|
541
|
+
if (!identityMatches) return false;
|
|
542
|
+
} else if (MIN_TURN_INDEX < 0 || lastAssistantIndex < MIN_TURN_INDEX) {
|
|
543
|
+
// Fallback/project snapshots without an identity may use the new-turn baseline, but an
|
|
544
|
+
// uncorrelated persistent action bar from an older turn must never prove completion.
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (lastAssistantTurn.querySelector('${FINISHED_ACTIONS_SELECTOR}')) return true;
|
|
549
|
+
const markdowns = lastAssistantTurn.querySelectorAll('.markdown');
|
|
550
|
+
return Array.from(markdowns).some((node) => (node.textContent || '').trim() === 'Done');
|
|
551
|
+
})()`;
|
|
552
|
+
}
|
|
553
|
+
async function isCompletionVisible(Runtime, meta, minTurnIndex) {
|
|
554
|
+
if (hasScopedCompletionProof(meta))
|
|
555
|
+
return true;
|
|
431
556
|
try {
|
|
432
557
|
const { result } = await Runtime.evaluate({
|
|
433
|
-
expression:
|
|
434
|
-
// Find the LAST assistant turn to check completion status
|
|
435
|
-
// Must match the same logic as buildAssistantExtractor for consistency
|
|
436
|
-
const ASSISTANT_SELECTOR = '${ASSISTANT_ROLE_SELECTOR}';
|
|
437
|
-
const isAssistantTurn = (node) => {
|
|
438
|
-
if (!(node instanceof HTMLElement)) return false;
|
|
439
|
-
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
440
|
-
if (turnAttr === 'assistant') return true;
|
|
441
|
-
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
442
|
-
if (role === 'assistant') return true;
|
|
443
|
-
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
444
|
-
if (testId.includes('assistant')) return true;
|
|
445
|
-
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
const turns = Array.from(document.querySelectorAll('${CONVERSATION_TURN_SELECTOR}'));
|
|
449
|
-
let lastAssistantTurn = null;
|
|
450
|
-
for (let i = turns.length - 1; i >= 0; i--) {
|
|
451
|
-
if (isAssistantTurn(turns[i])) {
|
|
452
|
-
lastAssistantTurn = turns[i];
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
if (!lastAssistantTurn) {
|
|
457
|
-
return false;
|
|
458
|
-
}
|
|
459
|
-
// Check if the last assistant turn has finished action buttons (copy, thumbs up/down, share)
|
|
460
|
-
if (lastAssistantTurn.querySelector('${FINISHED_ACTIONS_SELECTOR}')) {
|
|
461
|
-
return true;
|
|
462
|
-
}
|
|
463
|
-
// Also check for "Done" text in the last assistant turn's markdown
|
|
464
|
-
const markdowns = lastAssistantTurn.querySelectorAll('.markdown');
|
|
465
|
-
return Array.from(markdowns).some((n) => (n.textContent || '').trim() === 'Done');
|
|
466
|
-
})()`,
|
|
558
|
+
expression: buildCompletionVisibilityExpression(meta, minTurnIndex),
|
|
467
559
|
returnByValue: true,
|
|
468
560
|
});
|
|
469
561
|
return Boolean(result?.value);
|
|
@@ -472,6 +564,10 @@ async function isCompletionVisible(Runtime) {
|
|
|
472
564
|
return false;
|
|
473
565
|
}
|
|
474
566
|
}
|
|
567
|
+
export function hasScopedCompletionProof(meta) {
|
|
568
|
+
return meta.completionVisible === true;
|
|
569
|
+
}
|
|
570
|
+
export const buildCompletionVisibilityExpressionForTest = buildCompletionVisibilityExpression;
|
|
475
571
|
function normalizeAssistantSnapshot(snapshot) {
|
|
476
572
|
const text = snapshot?.text ? cleanAssistantText(snapshot.text) : "";
|
|
477
573
|
if (!text.trim()) {
|
|
@@ -490,7 +586,11 @@ function normalizeAssistantSnapshot(snapshot) {
|
|
|
490
586
|
return {
|
|
491
587
|
text,
|
|
492
588
|
html: snapshot?.html ?? undefined,
|
|
493
|
-
meta: {
|
|
589
|
+
meta: {
|
|
590
|
+
turnId: snapshot?.turnId ?? undefined,
|
|
591
|
+
messageId: snapshot?.messageId ?? undefined,
|
|
592
|
+
...(snapshot?.completionVisible === true ? { completionVisible: true } : {}),
|
|
593
|
+
},
|
|
494
594
|
};
|
|
495
595
|
}
|
|
496
596
|
function isGeneratedImageAssistantAnswer(answer) {
|
|
@@ -557,7 +657,6 @@ function buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId)
|
|
|
557
657
|
}
|
|
558
658
|
function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConversationId) {
|
|
559
659
|
const selectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
|
|
560
|
-
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
561
660
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
562
661
|
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
563
662
|
? Math.floor(minTurnIndex)
|
|
@@ -570,7 +669,6 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
570
669
|
const SELECTORS = ${selectorsLiteral};
|
|
571
670
|
const STOP_SELECTOR = ${JSON.stringify(STOP_CONTROL_SELECTOR)};
|
|
572
671
|
const FINISHED_SELECTOR = '${FINISHED_ACTIONS_SELECTOR}';
|
|
573
|
-
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
574
672
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
575
673
|
const EXPECTED_CONVERSATION_ID = ${expectedConversationLiteral};
|
|
576
674
|
// Learned: settling avoids capturing mid-stream HTML; keep short.
|
|
@@ -593,6 +691,7 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
593
691
|
return normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'));
|
|
594
692
|
};
|
|
595
693
|
${buildActiveThinkingStatusPredicateJs("isActiveThinkingStatus")}
|
|
694
|
+
${buildThinkingActivePredicateJs("isThinkingActiveNow")}
|
|
596
695
|
|
|
597
696
|
// Helper to detect assistant turns - must match buildAssistantExtractor logic for consistency.
|
|
598
697
|
const isAssistantTurn = (node) => {
|
|
@@ -693,7 +792,7 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
693
792
|
|
|
694
793
|
// Check if the last assistant turn has finished (scoped to avoid detecting old turns).
|
|
695
794
|
const isLastAssistantTurnFinished = () => {
|
|
696
|
-
const turns =
|
|
795
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
697
796
|
let lastAssistantTurn = null;
|
|
698
797
|
for (let i = turns.length - 1; i >= 0; i--) {
|
|
699
798
|
if (isAssistantTurn(turns[i])) {
|
|
@@ -717,8 +816,8 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
717
816
|
// Learned: long streaming responses (esp. thinking models) can pause mid-stream;
|
|
718
817
|
// use progressively longer windows to avoid truncation (#71).
|
|
719
818
|
const initialLength = snapshot?.text?.length ?? 0;
|
|
720
|
-
const shortAnswer = initialLength > 0 && initialLength <
|
|
721
|
-
const mediumAnswer = initialLength >=
|
|
819
|
+
const shortAnswer = initialLength > 0 && initialLength < ${MIN_CONFIDENT_ANSWER_LENGTH};
|
|
820
|
+
const mediumAnswer = initialLength >= ${MIN_CONFIDENT_ANSWER_LENGTH} && initialLength < 40;
|
|
722
821
|
const longAnswer = initialLength >= 40 && initialLength < 500;
|
|
723
822
|
const settleWindowMs = shortAnswer ? 12_000 : mediumAnswer ? 5_000 : longAnswer ? 8_000 : 10_000;
|
|
724
823
|
const settleIntervalMs = 400;
|
|
@@ -759,8 +858,15 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
759
858
|
}
|
|
760
859
|
const stopVisible = Boolean(document.querySelector(STOP_SELECTOR));
|
|
761
860
|
const finishedVisible = isLastAssistantTurnFinished();
|
|
861
|
+
// Defense in depth (the node side re-confirms every capture): never settle on a
|
|
862
|
+
// stable-but-quiet candidate while the model is actively thinking/generating, so the
|
|
863
|
+
// observer does not hand a settled preamble to the node path during the reasoning gap.
|
|
864
|
+
const thinkingActiveNow = isThinkingActiveNow();
|
|
762
865
|
|
|
763
|
-
if (
|
|
866
|
+
if (
|
|
867
|
+
finishedVisible ||
|
|
868
|
+
(!stopVisible && !thinkingActiveNow && stableCycles >= stableTarget)
|
|
869
|
+
) {
|
|
764
870
|
break;
|
|
765
871
|
}
|
|
766
872
|
}
|
|
@@ -792,11 +898,9 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
792
898
|
})()`;
|
|
793
899
|
}
|
|
794
900
|
function buildAssistantExtractor(functionName) {
|
|
795
|
-
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
796
901
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
797
902
|
return `const ${functionName} = () => {
|
|
798
903
|
${buildClickDispatcher()}
|
|
799
|
-
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
800
904
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
801
905
|
const isAssistantTurn = (node) => {
|
|
802
906
|
if (!(node instanceof HTMLElement)) return false;
|
|
@@ -832,7 +936,7 @@ function buildAssistantExtractor(functionName) {
|
|
|
832
936
|
}
|
|
833
937
|
};
|
|
834
938
|
|
|
835
|
-
const turns =
|
|
939
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
836
940
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
837
941
|
const turn = turns[index];
|
|
838
942
|
if (!isAssistantTurn(turn)) {
|
|
@@ -867,7 +971,7 @@ function buildAssistantExtractor(functionName) {
|
|
|
867
971
|
normalizedText === 'edit' ||
|
|
868
972
|
normalizedText === 'stopped thinking' ||
|
|
869
973
|
normalizedText === 'stopped thinking edit' ||
|
|
870
|
-
/^thought for \\d+(?:\\.\\d+)?\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\\s+edit$/.test(normalizedText);
|
|
974
|
+
/^(?:reasoning\\s+|pro thinking\\s+)?thought for \\d+(?:\\.\\d+)?\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\\s+edit$/.test(normalizedText);
|
|
871
975
|
if (generatedImages.length > 0 && imageOnlyChrome) {
|
|
872
976
|
const label = generatedImages.length === 1 ? 'Generated image.' : \`Generated \${generatedImages.length} images.\`;
|
|
873
977
|
return { text: label, html: messageRoot?.innerHTML ?? html, messageId, turnId, turnIndex: index };
|
|
@@ -916,29 +1020,32 @@ function buildMarkdownFallbackExtractor(minTurnLiteral) {
|
|
|
916
1020
|
}
|
|
917
1021
|
}
|
|
918
1022
|
if (!root) return null;
|
|
919
|
-
const
|
|
920
|
-
const turnNodes = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
1023
|
+
const turnNodes = ${buildConversationTurnListExpression()};
|
|
921
1024
|
const hasTurns = turnNodes.length > 0;
|
|
922
1025
|
const resolveTurnIndex = (node) => {
|
|
923
|
-
const
|
|
924
|
-
if (!turn) return null;
|
|
925
|
-
const idx = turnNodes.indexOf(turn);
|
|
1026
|
+
const idx = turnNodes.findIndex((turn) => turn === node || turn.contains?.(node));
|
|
926
1027
|
return idx >= 0 ? idx : null;
|
|
927
1028
|
};
|
|
1029
|
+
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
1030
|
+
const collectLastUser = (scope) => {
|
|
1031
|
+
if (!scope?.querySelectorAll) return null;
|
|
1032
|
+
const userTurns = Array.from(scope.querySelectorAll('[data-message-author-role="user"], [data-turn="user"]'));
|
|
1033
|
+
return userTurns[userTurns.length - 1] ?? null;
|
|
1034
|
+
};
|
|
1035
|
+
const lastUser = collectLastUser(root) || collectLastUser(document);
|
|
1036
|
+
const userText = lastUser ? normalize(lastUser.innerText || lastUser.textContent || '') : '';
|
|
1037
|
+
const isAfterCurrentUser = (node) => {
|
|
1038
|
+
if (!lastUser || typeof lastUser.compareDocumentPosition !== 'function') return false;
|
|
1039
|
+
// Node.DOCUMENT_POSITION_FOLLOWING = 4. Use the numeric bit so the injected expression
|
|
1040
|
+
// also works in stripped browser test contexts without a global Node constructor.
|
|
1041
|
+
return Boolean(lastUser.compareDocumentPosition(node) & 4);
|
|
1042
|
+
};
|
|
928
1043
|
const isAfterMinTurn = (node) => {
|
|
929
1044
|
if (__minTurn === null) return true;
|
|
930
|
-
if (!hasTurns) return
|
|
1045
|
+
if (!hasTurns) return isAfterCurrentUser(node);
|
|
931
1046
|
const idx = resolveTurnIndex(node);
|
|
932
1047
|
return idx !== null && idx >= __minTurn;
|
|
933
1048
|
};
|
|
934
|
-
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
935
|
-
const collectUserText = (scope) => {
|
|
936
|
-
if (!scope?.querySelectorAll) return '';
|
|
937
|
-
const userTurns = Array.from(scope.querySelectorAll('[data-message-author-role="user"], [data-turn="user"]'));
|
|
938
|
-
const lastUser = userTurns[userTurns.length - 1];
|
|
939
|
-
return lastUser ? normalize(lastUser.innerText || lastUser.textContent || '') : '';
|
|
940
|
-
};
|
|
941
|
-
const userText = collectUserText(root) || collectUserText(document);
|
|
942
1049
|
const isUserEcho = (text) => {
|
|
943
1050
|
if (!userText) return false;
|
|
944
1051
|
const normalized = normalize(text);
|
|
@@ -1009,7 +1116,14 @@ function buildMarkdownFallbackExtractor(minTurnLiteral) {
|
|
|
1009
1116
|
if (isUserEcho(text)) continue;
|
|
1010
1117
|
const html = node.innerHTML ?? '';
|
|
1011
1118
|
const turnIndex = resolveTurnIndex(node);
|
|
1012
|
-
return {
|
|
1119
|
+
return {
|
|
1120
|
+
text,
|
|
1121
|
+
html,
|
|
1122
|
+
messageId: null,
|
|
1123
|
+
turnId: null,
|
|
1124
|
+
turnIndex,
|
|
1125
|
+
completionVisible: actionMarkdowns.includes(node),
|
|
1126
|
+
};
|
|
1013
1127
|
}
|
|
1014
1128
|
return null;
|
|
1015
1129
|
})`;
|
|
@@ -1050,7 +1164,7 @@ function buildCopyExpression(meta) {
|
|
|
1050
1164
|
if (testId.includes('assistant')) return true;
|
|
1051
1165
|
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
1052
1166
|
};
|
|
1053
|
-
const turns =
|
|
1167
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
1054
1168
|
for (let i = turns.length - 1; i >= 0; i -= 1) {
|
|
1055
1169
|
const turn = turns[i];
|
|
1056
1170
|
if (!isAssistantTurn(turn)) continue;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { INPUT_SELECTORS, SEND_BUTTON_SELECTORS, UPLOAD_STATUS_SELECTORS } from "../constants.js";
|
|
3
|
+
import { buildConversationTurnListExpression } from "../conversationTurns.js";
|
|
3
4
|
import { delay } from "../utils.js";
|
|
4
5
|
import { logDomFailure } from "../domDebug.js";
|
|
5
6
|
import { transferAttachmentViaDataTransfer } from "./attachmentDataTransfer.js";
|
|
@@ -1594,14 +1595,12 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
1594
1595
|
throw new Error("Attachment was not present on the sent user message.");
|
|
1595
1596
|
}
|
|
1596
1597
|
function buildUserTurnAttachmentExpression(options) {
|
|
1597
|
-
const conversationSelectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
1598
1598
|
const minTurnLiteral = options.minTurnIndex === null ? "null" : String(options.minTurnIndex);
|
|
1599
1599
|
const expectedPromptLiteral = JSON.stringify(options.expectedPromptPrefix);
|
|
1600
1600
|
const expectedConversationLiteral = options.expectedConversationId
|
|
1601
1601
|
? JSON.stringify(options.expectedConversationId)
|
|
1602
1602
|
: "null";
|
|
1603
1603
|
return `(() => {
|
|
1604
|
-
const CONVERSATION_SELECTOR = ${conversationSelectorLiteral};
|
|
1605
1604
|
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
1606
1605
|
const EXPECTED_PROMPT_PREFIX = ${expectedPromptLiteral};
|
|
1607
1606
|
const EXPECTED_CONVERSATION_ID = ${expectedConversationLiteral};
|
|
@@ -1614,7 +1613,7 @@ function buildUserTurnAttachmentExpression(options) {
|
|
|
1614
1613
|
) {
|
|
1615
1614
|
return { ok: false, conversationMismatch: true };
|
|
1616
1615
|
}
|
|
1617
|
-
const turns =
|
|
1616
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
1618
1617
|
const userTurns = turns.map((node, index) => ({ node, index })).filter(({ node }) => {
|
|
1619
1618
|
const attr = (node.getAttribute('data-message-author-role') || node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
1620
1619
|
if (attr === 'user') return true;
|