@steipete/oracle 0.7.6 → 0.8.1
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/README.md +6 -3
- package/dist/bin/oracle-cli.js +4 -0
- package/dist/src/browser/actions/assistantResponse.js +437 -84
- package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
- package/dist/src/browser/actions/attachments.js +1300 -132
- package/dist/src/browser/actions/modelSelection.js +9 -4
- package/dist/src/browser/actions/navigation.js +160 -5
- package/dist/src/browser/actions/promptComposer.js +54 -11
- package/dist/src/browser/actions/remoteFileTransfer.js +5 -156
- package/dist/src/browser/actions/thinkingTime.js +5 -0
- package/dist/src/browser/chromeLifecycle.js +9 -1
- package/dist/src/browser/config.js +11 -3
- package/dist/src/browser/constants.js +4 -1
- package/dist/src/browser/cookies.js +55 -21
- package/dist/src/browser/index.js +342 -69
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/profileState.js +16 -0
- package/dist/src/browser/reattach.js +27 -179
- package/dist/src/browser/reattachHelpers.js +382 -0
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/browserConfig.js +12 -5
- package/dist/src/cli/browserDefaults.js +12 -0
- package/dist/src/cli/sessionDisplay.js +7 -0
- package/dist/src/gemini-web/executor.js +107 -46
- package/dist/src/oracle/oscProgress.js +7 -0
- package/dist/src/oracle/run.js +23 -32
- package/dist/src/remote/server.js +30 -15
- package/package.json +8 -17
|
@@ -3,15 +3,31 @@ import { delay } from '../utils.js';
|
|
|
3
3
|
import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression } from '../domDebug.js';
|
|
4
4
|
import { buildClickDispatcher } from './domEvents.js';
|
|
5
5
|
const ASSISTANT_POLL_TIMEOUT_ERROR = 'assistant-response-watchdog-timeout';
|
|
6
|
-
|
|
6
|
+
function isAnswerNowPlaceholderText(normalized) {
|
|
7
|
+
const text = normalized.trim();
|
|
8
|
+
if (!text)
|
|
9
|
+
return false;
|
|
10
|
+
// Learned: "Pro thinking" shows a placeholder turn that contains "Answer now".
|
|
11
|
+
// That is not the final answer and must be ignored in browser automation.
|
|
12
|
+
if (text === 'chatgpt said:' || text === 'chatgpt said')
|
|
13
|
+
return true;
|
|
14
|
+
if (text.includes('file upload request') && (text.includes('pro thinking') || text.includes('chatgpt said'))) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return text.includes('answer now') && (text.includes('pro thinking') || text.includes('chatgpt said'));
|
|
18
|
+
}
|
|
19
|
+
export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex) {
|
|
7
20
|
const start = Date.now();
|
|
8
21
|
logger('Waiting for ChatGPT response');
|
|
9
|
-
|
|
22
|
+
// Learned: two paths are needed:
|
|
23
|
+
// 1) DOM observer (fast when mutations fire),
|
|
24
|
+
// 2) snapshot poller (fallback when observers miss or JS stalls).
|
|
25
|
+
const expression = buildResponseObserverExpression(timeoutMs, minTurnIndex);
|
|
10
26
|
const evaluationPromise = Runtime.evaluate({ expression, awaitPromise: true, returnByValue: true });
|
|
11
27
|
const raceReadyEvaluation = evaluationPromise.then((value) => ({ kind: 'evaluation', value }), (error) => {
|
|
12
28
|
throw { source: 'evaluation', error };
|
|
13
29
|
});
|
|
14
|
-
const pollerPromise = pollAssistantCompletion(Runtime, timeoutMs).then((value) => {
|
|
30
|
+
const pollerPromise = pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex).then((value) => {
|
|
15
31
|
if (!value) {
|
|
16
32
|
throw { source: 'poll', error: new Error(ASSISTANT_POLL_TIMEOUT_ERROR) };
|
|
17
33
|
}
|
|
@@ -40,7 +56,7 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger) {
|
|
|
40
56
|
throw error;
|
|
41
57
|
}
|
|
42
58
|
else if (source === 'evaluation') {
|
|
43
|
-
const recovered = await recoverAssistantResponse(Runtime, timeoutMs, logger);
|
|
59
|
+
const recovered = await recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex);
|
|
44
60
|
if (recovered) {
|
|
45
61
|
return recovered;
|
|
46
62
|
}
|
|
@@ -56,12 +72,29 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger) {
|
|
|
56
72
|
await logDomFailure(Runtime, logger, 'assistant-response');
|
|
57
73
|
throw new Error('Failed to capture assistant response');
|
|
58
74
|
}
|
|
59
|
-
const parsed = await parseAssistantEvaluationResult(Runtime, evaluation,
|
|
75
|
+
const parsed = await parseAssistantEvaluationResult(Runtime, evaluation, logger);
|
|
60
76
|
if (!parsed) {
|
|
77
|
+
let remainingMs = Math.max(0, timeoutMs - (Date.now() - start));
|
|
78
|
+
if (remainingMs > 0) {
|
|
79
|
+
const recovered = await recoverAssistantResponse(Runtime, remainingMs, logger, minTurnIndex);
|
|
80
|
+
if (recovered) {
|
|
81
|
+
return recovered;
|
|
82
|
+
}
|
|
83
|
+
remainingMs = Math.max(0, timeoutMs - (Date.now() - start));
|
|
84
|
+
if (remainingMs > 0) {
|
|
85
|
+
const polled = await Promise.race([
|
|
86
|
+
pollerPromise.catch(() => null),
|
|
87
|
+
delay(remainingMs).then(() => null),
|
|
88
|
+
]);
|
|
89
|
+
if (polled && polled.kind === 'poll') {
|
|
90
|
+
return polled.value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
61
94
|
await logDomFailure(Runtime, logger, 'assistant-response');
|
|
62
95
|
throw new Error('Unable to capture assistant response');
|
|
63
96
|
}
|
|
64
|
-
const refreshed = await refreshAssistantSnapshot(Runtime, parsed, logger);
|
|
97
|
+
const refreshed = await refreshAssistantSnapshot(Runtime, parsed, logger, minTurnIndex);
|
|
65
98
|
const candidate = refreshed ?? parsed;
|
|
66
99
|
// The evaluation path can race ahead of completion. If ChatGPT is still streaming, wait for the watchdog poller.
|
|
67
100
|
const elapsedMs = Date.now() - start;
|
|
@@ -71,21 +104,37 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger) {
|
|
|
71
104
|
isStopButtonVisible(Runtime),
|
|
72
105
|
isCompletionVisible(Runtime),
|
|
73
106
|
]);
|
|
74
|
-
if (stopVisible
|
|
107
|
+
if (stopVisible) {
|
|
75
108
|
logger('Assistant still generating; waiting for completion');
|
|
76
|
-
const completed = await pollAssistantCompletion(Runtime, remainingMs);
|
|
109
|
+
const completed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex);
|
|
77
110
|
if (completed) {
|
|
78
111
|
return completed;
|
|
79
112
|
}
|
|
80
113
|
}
|
|
114
|
+
else if (completionVisible) {
|
|
115
|
+
// No-op: completion UI surfaced and stop button is gone.
|
|
116
|
+
}
|
|
81
117
|
}
|
|
82
118
|
return candidate;
|
|
83
119
|
}
|
|
84
|
-
export async function readAssistantSnapshot(Runtime) {
|
|
85
|
-
const { result } = await Runtime.evaluate({
|
|
120
|
+
export async function readAssistantSnapshot(Runtime, minTurnIndex) {
|
|
121
|
+
const { result } = await Runtime.evaluate({
|
|
122
|
+
expression: buildAssistantSnapshotExpression(minTurnIndex),
|
|
123
|
+
returnByValue: true,
|
|
124
|
+
});
|
|
86
125
|
const value = result?.value;
|
|
87
126
|
if (value && typeof value === 'object') {
|
|
88
|
-
|
|
127
|
+
const snapshot = value;
|
|
128
|
+
if (typeof minTurnIndex === 'number' && Number.isFinite(minTurnIndex)) {
|
|
129
|
+
const turnIndex = typeof snapshot.turnIndex === 'number' ? snapshot.turnIndex : null;
|
|
130
|
+
if (turnIndex === null) {
|
|
131
|
+
return snapshot;
|
|
132
|
+
}
|
|
133
|
+
if (turnIndex < minTurnIndex) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return snapshot;
|
|
89
138
|
}
|
|
90
139
|
return null;
|
|
91
140
|
}
|
|
@@ -114,9 +163,21 @@ export function buildAssistantExtractorForTest(name) {
|
|
|
114
163
|
export function buildConversationDebugExpressionForTest() {
|
|
115
164
|
return buildConversationDebugExpression();
|
|
116
165
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
166
|
+
export function buildMarkdownFallbackExtractorForTest(minTurnLiteral = '0') {
|
|
167
|
+
return buildMarkdownFallbackExtractor(minTurnLiteral);
|
|
168
|
+
}
|
|
169
|
+
export function buildCopyExpressionForTest(meta = {}) {
|
|
170
|
+
return buildCopyExpression(meta);
|
|
171
|
+
}
|
|
172
|
+
async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex) {
|
|
173
|
+
const recoveryTimeoutMs = Math.max(0, timeoutMs);
|
|
174
|
+
if (recoveryTimeoutMs === 0) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const recovered = await waitForCondition(async () => {
|
|
178
|
+
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
|
|
179
|
+
return normalizeAssistantSnapshot(snapshot);
|
|
180
|
+
}, recoveryTimeoutMs, 400);
|
|
120
181
|
if (recovered) {
|
|
121
182
|
logger('Recovered assistant response via polling fallback');
|
|
122
183
|
return recovered;
|
|
@@ -124,7 +185,7 @@ async function recoverAssistantResponse(Runtime, timeoutMs, logger) {
|
|
|
124
185
|
await logConversationSnapshot(Runtime, logger).catch(() => undefined);
|
|
125
186
|
return null;
|
|
126
187
|
}
|
|
127
|
-
async function parseAssistantEvaluationResult(
|
|
188
|
+
async function parseAssistantEvaluationResult(_Runtime, evaluation, _logger) {
|
|
128
189
|
const { result } = evaluation;
|
|
129
190
|
if (result.type === 'object' && result.value && typeof result.value === 'object' && 'text' in result.value) {
|
|
130
191
|
const html = typeof result.value.html === 'string'
|
|
@@ -138,37 +199,56 @@ async function parseAssistantEvaluationResult(Runtime, evaluation, timeoutMs, lo
|
|
|
138
199
|
: undefined;
|
|
139
200
|
const text = cleanAssistantText(String(result.value.text ?? ''));
|
|
140
201
|
const normalized = text.toLowerCase();
|
|
141
|
-
if (normalized
|
|
142
|
-
|
|
143
|
-
const recovered = await recoverAssistantResponse(Runtime, Math.min(timeoutMs, 10_000), logger);
|
|
144
|
-
return recovered ?? null;
|
|
202
|
+
if (isAnswerNowPlaceholderText(normalized)) {
|
|
203
|
+
return null;
|
|
145
204
|
}
|
|
146
205
|
return { text, html, meta: { turnId, messageId } };
|
|
147
206
|
}
|
|
148
207
|
const fallbackText = typeof result.value === 'string' ? cleanAssistantText(result.value) : '';
|
|
149
208
|
if (!fallbackText) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
if (isAnswerNowPlaceholderText(fallbackText.toLowerCase())) {
|
|
154
212
|
return null;
|
|
155
213
|
}
|
|
156
214
|
return { text: fallbackText, html: undefined, meta: {} };
|
|
157
215
|
}
|
|
158
|
-
async function refreshAssistantSnapshot(Runtime, current, logger) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
216
|
+
async function refreshAssistantSnapshot(Runtime, current, logger, minTurnIndex) {
|
|
217
|
+
const deadline = Date.now() + 5_000;
|
|
218
|
+
let best = null;
|
|
219
|
+
let stableCycles = 0;
|
|
220
|
+
const stableTarget = 3;
|
|
221
|
+
while (Date.now() < deadline) {
|
|
222
|
+
// Learned: short/fast answers can race; poll a few extra cycles to pick up messageId + full text.
|
|
223
|
+
const latestSnapshot = await readAssistantSnapshot(Runtime, minTurnIndex).catch(() => null);
|
|
224
|
+
const latest = normalizeAssistantSnapshot(latestSnapshot);
|
|
225
|
+
if (latest) {
|
|
226
|
+
if (!best ||
|
|
227
|
+
latest.text.length > best.text.length ||
|
|
228
|
+
(!best.meta.messageId && latest.meta.messageId)) {
|
|
229
|
+
best = latest;
|
|
230
|
+
stableCycles = 0;
|
|
231
|
+
}
|
|
232
|
+
else if (latest.text.trim() === best.text.trim()) {
|
|
233
|
+
stableCycles += 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (best && stableCycles >= stableTarget) {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
await delay(300);
|
|
240
|
+
}
|
|
241
|
+
if (!best) {
|
|
162
242
|
return null;
|
|
163
243
|
}
|
|
164
244
|
const currentLength = cleanAssistantText(current.text).trim().length;
|
|
165
|
-
const latestLength =
|
|
166
|
-
const hasBetterId = !current.meta?.messageId && Boolean(
|
|
245
|
+
const latestLength = best.text.length;
|
|
246
|
+
const hasBetterId = !current.meta?.messageId && Boolean(best.meta.messageId);
|
|
167
247
|
const isLonger = latestLength > currentLength;
|
|
168
|
-
const hasDifferentText =
|
|
248
|
+
const hasDifferentText = best.text.trim() !== current.text.trim();
|
|
169
249
|
if (isLonger || hasBetterId || hasDifferentText) {
|
|
170
250
|
logger('Refreshed assistant response via latest snapshot');
|
|
171
|
-
return
|
|
251
|
+
return best;
|
|
172
252
|
}
|
|
173
253
|
return null;
|
|
174
254
|
}
|
|
@@ -183,19 +263,20 @@ async function terminateRuntimeExecution(Runtime) {
|
|
|
183
263
|
// ignore termination failures
|
|
184
264
|
}
|
|
185
265
|
}
|
|
186
|
-
async function pollAssistantCompletion(Runtime, timeoutMs) {
|
|
266
|
+
async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex) {
|
|
187
267
|
const watchdogDeadline = Date.now() + timeoutMs;
|
|
188
268
|
let previousLength = 0;
|
|
189
269
|
let stableCycles = 0;
|
|
190
|
-
|
|
270
|
+
let lastChangeAt = Date.now();
|
|
191
271
|
while (Date.now() < watchdogDeadline) {
|
|
192
|
-
const snapshot = await readAssistantSnapshot(Runtime);
|
|
272
|
+
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
|
|
193
273
|
const normalized = normalizeAssistantSnapshot(snapshot);
|
|
194
274
|
if (normalized) {
|
|
195
275
|
const currentLength = normalized.text.length;
|
|
196
276
|
if (currentLength > previousLength) {
|
|
197
277
|
previousLength = currentLength;
|
|
198
278
|
stableCycles = 0;
|
|
279
|
+
lastChangeAt = Date.now();
|
|
199
280
|
}
|
|
200
281
|
else {
|
|
201
282
|
stableCycles += 1;
|
|
@@ -204,10 +285,19 @@ async function pollAssistantCompletion(Runtime, timeoutMs) {
|
|
|
204
285
|
isStopButtonVisible(Runtime),
|
|
205
286
|
isCompletionVisible(Runtime),
|
|
206
287
|
]);
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
288
|
+
const shortAnswer = currentLength > 0 && currentLength < 16;
|
|
289
|
+
// Learned: short answers need a longer stability window or they truncate.
|
|
290
|
+
const completionStableTarget = shortAnswer ? 12 : currentLength < 40 ? 8 : 4;
|
|
291
|
+
const requiredStableCycles = shortAnswer ? 12 : 6;
|
|
292
|
+
const stableMs = Date.now() - lastChangeAt;
|
|
293
|
+
const minStableMs = shortAnswer ? 8000 : 1200;
|
|
294
|
+
// Require stop button to disappear before treating completion as final.
|
|
295
|
+
if (!stopVisible) {
|
|
296
|
+
const stableEnough = stableCycles >= requiredStableCycles && stableMs >= minStableMs;
|
|
297
|
+
const completionEnough = completionVisible && stableCycles >= completionStableTarget && stableMs >= minStableMs;
|
|
298
|
+
if (completionEnough || stableEnough) {
|
|
299
|
+
return normalized;
|
|
300
|
+
}
|
|
211
301
|
}
|
|
212
302
|
}
|
|
213
303
|
else {
|
|
@@ -283,7 +373,11 @@ function normalizeAssistantSnapshot(snapshot) {
|
|
|
283
373
|
const normalized = text.toLowerCase();
|
|
284
374
|
// "Pro thinking" often renders a placeholder turn containing an "Answer now" gate.
|
|
285
375
|
// Treat it as incomplete so browser mode keeps waiting for the real assistant text.
|
|
286
|
-
if (
|
|
376
|
+
if (isAnswerNowPlaceholderText(normalized)) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
// Ignore user echo turns that can show up in project view fallbacks.
|
|
380
|
+
if (normalized.startsWith('you said')) {
|
|
287
381
|
return null;
|
|
288
382
|
}
|
|
289
383
|
return {
|
|
@@ -292,9 +386,6 @@ function normalizeAssistantSnapshot(snapshot) {
|
|
|
292
386
|
meta: { turnId: snapshot?.turnId ?? undefined, messageId: snapshot?.messageId ?? undefined },
|
|
293
387
|
};
|
|
294
388
|
}
|
|
295
|
-
async function waitForAssistantSnapshot(Runtime, timeoutMs) {
|
|
296
|
-
return waitForCondition(() => readAssistantSnapshot(Runtime), timeoutMs);
|
|
297
|
-
}
|
|
298
389
|
async function waitForCondition(getter, timeoutMs, pollIntervalMs = 400) {
|
|
299
390
|
const deadline = Date.now() + timeoutMs;
|
|
300
391
|
while (Date.now() < deadline) {
|
|
@@ -306,16 +397,38 @@ async function waitForCondition(getter, timeoutMs, pollIntervalMs = 400) {
|
|
|
306
397
|
}
|
|
307
398
|
return null;
|
|
308
399
|
}
|
|
309
|
-
function buildAssistantSnapshotExpression() {
|
|
400
|
+
function buildAssistantSnapshotExpression(minTurnIndex) {
|
|
401
|
+
const minTurnLiteral = typeof minTurnIndex === 'number' && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
402
|
+
? Math.floor(minTurnIndex)
|
|
403
|
+
: -1;
|
|
310
404
|
return `(() => {
|
|
405
|
+
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
406
|
+
// Learned: the default turn DOM misses project view; keep a fallback extractor.
|
|
311
407
|
${buildAssistantExtractor('extractAssistantTurn')}
|
|
312
|
-
|
|
408
|
+
const extracted = extractAssistantTurn();
|
|
409
|
+
const isPlaceholder = (snapshot) => {
|
|
410
|
+
const normalized = String(snapshot?.text ?? '').toLowerCase().trim();
|
|
411
|
+
if (normalized === 'chatgpt said:' || normalized === 'chatgpt said') return true;
|
|
412
|
+
if (normalized.includes('file upload request') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'))) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
return normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'));
|
|
416
|
+
};
|
|
417
|
+
if (extracted && extracted.text && !isPlaceholder(extracted)) {
|
|
418
|
+
return extracted;
|
|
419
|
+
}
|
|
420
|
+
// Fallback for ChatGPT project view: answers can live outside conversation turns.
|
|
421
|
+
const fallback = ${buildMarkdownFallbackExtractor('MIN_TURN_INDEX')};
|
|
422
|
+
return fallback ?? extracted;
|
|
313
423
|
})()`;
|
|
314
424
|
}
|
|
315
|
-
function buildResponseObserverExpression(timeoutMs) {
|
|
425
|
+
function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
|
|
316
426
|
const selectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
|
|
317
427
|
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
318
428
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
429
|
+
const minTurnLiteral = typeof minTurnIndex === 'number' && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
430
|
+
? Math.floor(minTurnIndex)
|
|
431
|
+
: -1;
|
|
319
432
|
return `(() => {
|
|
320
433
|
${buildClickDispatcher()}
|
|
321
434
|
const SELECTORS = ${selectorsLiteral};
|
|
@@ -323,13 +436,18 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
323
436
|
const FINISHED_SELECTOR = '${FINISHED_ACTIONS_SELECTOR}';
|
|
324
437
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
325
438
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
439
|
+
// Learned: settling avoids capturing mid-stream HTML; keep short.
|
|
326
440
|
const settleDelayMs = 800;
|
|
327
441
|
const isAnswerNowPlaceholder = (snapshot) => {
|
|
328
|
-
const normalized = String(snapshot?.text ?? '').toLowerCase();
|
|
442
|
+
const normalized = String(snapshot?.text ?? '').toLowerCase().trim();
|
|
443
|
+
if (normalized === 'chatgpt said:' || normalized === 'chatgpt said') return true;
|
|
444
|
+
if (normalized.includes('file upload request') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'))) {
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
329
447
|
return normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'));
|
|
330
448
|
};
|
|
331
449
|
|
|
332
|
-
// Helper to detect assistant turns -
|
|
450
|
+
// Helper to detect assistant turns - must match buildAssistantExtractor logic for consistency.
|
|
333
451
|
const isAssistantTurn = (node) => {
|
|
334
452
|
if (!(node instanceof HTMLElement)) return false;
|
|
335
453
|
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
@@ -341,7 +459,21 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
341
459
|
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
342
460
|
};
|
|
343
461
|
|
|
462
|
+
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
344
463
|
${buildAssistantExtractor('extractFromTurns')}
|
|
464
|
+
// Learned: some layouts (project view) render markdown without assistant turn wrappers.
|
|
465
|
+
const extractFromMarkdownFallback = ${buildMarkdownFallbackExtractor('MIN_TURN_INDEX')};
|
|
466
|
+
|
|
467
|
+
const acceptSnapshot = (snapshot) => {
|
|
468
|
+
if (!snapshot) return null;
|
|
469
|
+
const index = typeof snapshot.turnIndex === 'number' ? snapshot.turnIndex : -1;
|
|
470
|
+
if (MIN_TURN_INDEX >= 0) {
|
|
471
|
+
if (index < 0 || index < MIN_TURN_INDEX) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return snapshot;
|
|
476
|
+
};
|
|
345
477
|
|
|
346
478
|
const captureViaObserver = () =>
|
|
347
479
|
new Promise((resolve, reject) => {
|
|
@@ -349,7 +481,15 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
349
481
|
let stopInterval = null;
|
|
350
482
|
const observer = new MutationObserver(() => {
|
|
351
483
|
const extractedRaw = extractFromTurns();
|
|
352
|
-
const
|
|
484
|
+
const extractedCandidate =
|
|
485
|
+
extractedRaw && !isAnswerNowPlaceholder(extractedRaw) ? extractedRaw : null;
|
|
486
|
+
let extracted = acceptSnapshot(extractedCandidate);
|
|
487
|
+
if (!extracted) {
|
|
488
|
+
const fallbackRaw = extractFromMarkdownFallback();
|
|
489
|
+
const fallbackCandidate =
|
|
490
|
+
fallbackRaw && !isAnswerNowPlaceholder(fallbackRaw) ? fallbackRaw : null;
|
|
491
|
+
extracted = acceptSnapshot(fallbackCandidate);
|
|
492
|
+
}
|
|
353
493
|
if (extracted) {
|
|
354
494
|
observer.disconnect();
|
|
355
495
|
if (stopInterval) {
|
|
@@ -386,7 +526,7 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
386
526
|
}, ${timeoutMs});
|
|
387
527
|
});
|
|
388
528
|
|
|
389
|
-
// Check if the last assistant turn has finished (scoped to avoid detecting old turns)
|
|
529
|
+
// Check if the last assistant turn has finished (scoped to avoid detecting old turns).
|
|
390
530
|
const isLastAssistantTurnFinished = () => {
|
|
391
531
|
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
392
532
|
let lastAssistantTurn = null;
|
|
@@ -405,30 +545,56 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
405
545
|
};
|
|
406
546
|
|
|
407
547
|
const waitForSettle = async (snapshot) => {
|
|
408
|
-
|
|
548
|
+
// Learned: short answers can be 1-2 tokens; enforce longer settle windows to avoid truncation.
|
|
549
|
+
const initialLength = snapshot?.text?.length ?? 0;
|
|
550
|
+
const shortAnswer = initialLength > 0 && initialLength < 16;
|
|
551
|
+
const settleWindowMs = shortAnswer ? 12_000 : 5_000;
|
|
409
552
|
const settleIntervalMs = 400;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
553
|
+
const deadline = Date.now() + settleWindowMs;
|
|
554
|
+
let latest = snapshot;
|
|
555
|
+
let lastLength = snapshot?.text?.length ?? 0;
|
|
556
|
+
let stableCycles = 0;
|
|
557
|
+
const stableTarget = shortAnswer ? 6 : 3;
|
|
558
|
+
while (Date.now() < deadline) {
|
|
559
|
+
await new Promise((resolve) => setTimeout(resolve, settleIntervalMs));
|
|
560
|
+
const refreshedRaw = extractFromTurns();
|
|
561
|
+
const refreshedCandidate =
|
|
562
|
+
refreshedRaw && !isAnswerNowPlaceholder(refreshedRaw) ? refreshedRaw : null;
|
|
563
|
+
let refreshed = acceptSnapshot(refreshedCandidate);
|
|
564
|
+
if (!refreshed) {
|
|
565
|
+
const fallbackRaw = extractFromMarkdownFallback();
|
|
566
|
+
const fallbackCandidate =
|
|
567
|
+
fallbackRaw && !isAnswerNowPlaceholder(fallbackRaw) ? fallbackRaw : null;
|
|
568
|
+
refreshed = acceptSnapshot(fallbackCandidate);
|
|
569
|
+
}
|
|
570
|
+
const nextLength = refreshed?.text?.length ?? lastLength;
|
|
571
|
+
if (refreshed && nextLength >= lastLength) {
|
|
572
|
+
latest = refreshed;
|
|
573
|
+
}
|
|
574
|
+
if (nextLength > lastLength) {
|
|
575
|
+
lastLength = nextLength;
|
|
576
|
+
stableCycles = 0;
|
|
577
|
+
} else {
|
|
578
|
+
stableCycles += 1;
|
|
579
|
+
}
|
|
580
|
+
const stopVisible = Boolean(document.querySelector(STOP_SELECTOR));
|
|
581
|
+
const finishedVisible = isLastAssistantTurnFinished();
|
|
422
582
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
583
|
+
if (finishedVisible || (!stopVisible && stableCycles >= stableTarget)) {
|
|
584
|
+
break;
|
|
426
585
|
}
|
|
427
|
-
|
|
428
|
-
|
|
586
|
+
}
|
|
587
|
+
return latest ?? snapshot;
|
|
588
|
+
};
|
|
429
589
|
|
|
430
590
|
const extractedRaw = extractFromTurns();
|
|
431
|
-
const
|
|
591
|
+
const extractedCandidate = extractedRaw && !isAnswerNowPlaceholder(extractedRaw) ? extractedRaw : null;
|
|
592
|
+
let extracted = acceptSnapshot(extractedCandidate);
|
|
593
|
+
if (!extracted) {
|
|
594
|
+
const fallbackRaw = extractFromMarkdownFallback();
|
|
595
|
+
const fallbackCandidate = fallbackRaw && !isAnswerNowPlaceholder(fallbackRaw) ? fallbackRaw : null;
|
|
596
|
+
extracted = acceptSnapshot(fallbackCandidate);
|
|
597
|
+
}
|
|
432
598
|
if (extracted) {
|
|
433
599
|
return waitForSettle(extracted);
|
|
434
600
|
}
|
|
@@ -484,23 +650,164 @@ function buildAssistantExtractor(functionName) {
|
|
|
484
650
|
}
|
|
485
651
|
const messageRoot = turn.querySelector(ASSISTANT_SELECTOR) ?? turn;
|
|
486
652
|
expandCollapsibles(messageRoot);
|
|
487
|
-
const preferred =
|
|
488
|
-
|
|
653
|
+
const preferred =
|
|
654
|
+
(messageRoot.matches?.('.markdown') || messageRoot.matches?.('[data-message-content]') ? messageRoot : null) ||
|
|
655
|
+
messageRoot.querySelector('.markdown') ||
|
|
656
|
+
messageRoot.querySelector('[data-message-content]') ||
|
|
657
|
+
messageRoot.querySelector('[data-testid*="message"]') ||
|
|
658
|
+
messageRoot.querySelector('[data-testid*="assistant"]') ||
|
|
659
|
+
messageRoot.querySelector('.prose') ||
|
|
660
|
+
messageRoot.querySelector('[class*="markdown"]');
|
|
661
|
+
const contentRoot = preferred ?? messageRoot;
|
|
662
|
+
if (!contentRoot) {
|
|
489
663
|
continue;
|
|
490
664
|
}
|
|
491
|
-
const innerText =
|
|
492
|
-
const textContent =
|
|
665
|
+
const innerText = contentRoot?.innerText ?? '';
|
|
666
|
+
const textContent = contentRoot?.textContent ?? '';
|
|
493
667
|
const text = innerText.trim().length > 0 ? innerText : textContent;
|
|
494
|
-
const html =
|
|
668
|
+
const html = contentRoot?.innerHTML ?? '';
|
|
495
669
|
const messageId = messageRoot.getAttribute('data-message-id');
|
|
496
670
|
const turnId = messageRoot.getAttribute('data-testid');
|
|
497
671
|
if (text.trim()) {
|
|
498
|
-
return { text, html, messageId, turnId };
|
|
672
|
+
return { text, html, messageId, turnId, turnIndex: index };
|
|
499
673
|
}
|
|
500
674
|
}
|
|
501
675
|
return null;
|
|
502
676
|
};`;
|
|
503
677
|
}
|
|
678
|
+
function buildMarkdownFallbackExtractor(minTurnLiteral) {
|
|
679
|
+
const turnIndexValue = minTurnLiteral ? `(${minTurnLiteral} >= 0 ? ${minTurnLiteral} : null)` : 'null';
|
|
680
|
+
return `(() => {
|
|
681
|
+
const MIN_TURN_INDEX = ${turnIndexValue};
|
|
682
|
+
const roots = [
|
|
683
|
+
document.querySelector('section[data-testid="screen-threadFlyOut"]'),
|
|
684
|
+
document.querySelector('[data-testid="chat-thread"]'),
|
|
685
|
+
document.querySelector('main'),
|
|
686
|
+
document.querySelector('[role="main"]'),
|
|
687
|
+
].filter(Boolean);
|
|
688
|
+
if (roots.length === 0) return null;
|
|
689
|
+
const markdownSelector = '.markdown,[data-message-content],[data-testid*="message"],.prose,[class*="markdown"]';
|
|
690
|
+
const isExcluded = (node) =>
|
|
691
|
+
Boolean(
|
|
692
|
+
node?.closest?.(
|
|
693
|
+
'nav, aside, [data-testid*="sidebar"], [data-testid*="chat-history"], [data-testid*="composer"], form',
|
|
694
|
+
),
|
|
695
|
+
);
|
|
696
|
+
const scoreRoot = (node) => {
|
|
697
|
+
const actions = node.querySelectorAll('${FINISHED_ACTIONS_SELECTOR}').length;
|
|
698
|
+
const assistants = node.querySelectorAll('[data-message-author-role="assistant"], [data-turn="assistant"]').length;
|
|
699
|
+
const markdowns = node.querySelectorAll(markdownSelector).length;
|
|
700
|
+
return actions * 10 + assistants * 5 + markdowns;
|
|
701
|
+
};
|
|
702
|
+
let root = roots[0];
|
|
703
|
+
let bestScore = scoreRoot(root);
|
|
704
|
+
for (let i = 1; i < roots.length; i += 1) {
|
|
705
|
+
const candidate = roots[i];
|
|
706
|
+
const score = scoreRoot(candidate);
|
|
707
|
+
if (score > bestScore) {
|
|
708
|
+
bestScore = score;
|
|
709
|
+
root = candidate;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (!root) return null;
|
|
713
|
+
const CONVERSATION_SELECTOR = '${CONVERSATION_TURN_SELECTOR}';
|
|
714
|
+
const turnNodes = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
715
|
+
const hasTurns = turnNodes.length > 0;
|
|
716
|
+
const resolveTurnIndex = (node) => {
|
|
717
|
+
const turn = node?.closest?.(CONVERSATION_SELECTOR);
|
|
718
|
+
if (!turn) return null;
|
|
719
|
+
const idx = turnNodes.indexOf(turn);
|
|
720
|
+
return idx >= 0 ? idx : null;
|
|
721
|
+
};
|
|
722
|
+
const isAfterMinTurn = (node) => {
|
|
723
|
+
if (MIN_TURN_INDEX === null) return true;
|
|
724
|
+
if (!hasTurns) return true;
|
|
725
|
+
const idx = resolveTurnIndex(node);
|
|
726
|
+
return idx !== null && idx >= MIN_TURN_INDEX;
|
|
727
|
+
};
|
|
728
|
+
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
729
|
+
const collectUserText = (scope) => {
|
|
730
|
+
if (!scope?.querySelectorAll) return '';
|
|
731
|
+
const userTurns = Array.from(scope.querySelectorAll('[data-message-author-role="user"], [data-turn="user"]'));
|
|
732
|
+
const lastUser = userTurns[userTurns.length - 1];
|
|
733
|
+
return lastUser ? normalize(lastUser.innerText || lastUser.textContent || '') : '';
|
|
734
|
+
};
|
|
735
|
+
const userText = collectUserText(root) || collectUserText(document);
|
|
736
|
+
const isUserEcho = (text) => {
|
|
737
|
+
if (!userText) return false;
|
|
738
|
+
const normalized = normalize(text);
|
|
739
|
+
if (!normalized) return false;
|
|
740
|
+
return normalized === userText || normalized.startsWith(userText);
|
|
741
|
+
};
|
|
742
|
+
const markdowns = Array.from(root.querySelectorAll(markdownSelector))
|
|
743
|
+
.filter((node) => !isExcluded(node))
|
|
744
|
+
.filter((node) => {
|
|
745
|
+
const container = node.closest('[data-message-author-role], [data-turn]');
|
|
746
|
+
if (!container) return true;
|
|
747
|
+
const role =
|
|
748
|
+
(container.getAttribute('data-message-author-role') || container.getAttribute('data-turn') || '').toLowerCase();
|
|
749
|
+
return role !== 'user';
|
|
750
|
+
});
|
|
751
|
+
if (markdowns.length === 0) return null;
|
|
752
|
+
const actionButtons = Array.from(root.querySelectorAll('${FINISHED_ACTIONS_SELECTOR}'));
|
|
753
|
+
const actionMarkdowns = [];
|
|
754
|
+
for (const button of actionButtons) {
|
|
755
|
+
const container =
|
|
756
|
+
button.closest('${CONVERSATION_TURN_SELECTOR}') ||
|
|
757
|
+
button.closest('[data-message-author-role="assistant"], [data-turn="assistant"]') ||
|
|
758
|
+
button.closest('[data-message-author-role], [data-turn]') ||
|
|
759
|
+
button.closest('[data-testid*="assistant"]');
|
|
760
|
+
if (!container || container === root || container === document.body) continue;
|
|
761
|
+
const scoped = Array.from(container.querySelectorAll(markdownSelector))
|
|
762
|
+
.filter((node) => !isExcluded(node))
|
|
763
|
+
.filter((node) => {
|
|
764
|
+
const roleNode = node.closest('[data-message-author-role], [data-turn]');
|
|
765
|
+
if (!roleNode) return true;
|
|
766
|
+
const role =
|
|
767
|
+
(roleNode.getAttribute('data-message-author-role') || roleNode.getAttribute('data-turn') || '').toLowerCase();
|
|
768
|
+
return role !== 'user';
|
|
769
|
+
});
|
|
770
|
+
if (scoped.length === 0) continue;
|
|
771
|
+
for (const node of scoped) {
|
|
772
|
+
actionMarkdowns.push(node);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const assistantMarkdowns = markdowns.filter((node) => {
|
|
776
|
+
const container = node.closest('[data-message-author-role], [data-turn], [data-testid*="assistant"]');
|
|
777
|
+
if (!container) return false;
|
|
778
|
+
const role =
|
|
779
|
+
(container.getAttribute('data-message-author-role') || container.getAttribute('data-turn') || '').toLowerCase();
|
|
780
|
+
if (role === 'assistant') return true;
|
|
781
|
+
const testId = (container.getAttribute('data-testid') || '').toLowerCase();
|
|
782
|
+
return testId.includes('assistant');
|
|
783
|
+
});
|
|
784
|
+
const hasAssistantIndicators = Boolean(
|
|
785
|
+
root.querySelector('${FINISHED_ACTIONS_SELECTOR}') ||
|
|
786
|
+
root.querySelector('[data-message-author-role="assistant"], [data-turn="assistant"], [data-testid*="assistant"]'),
|
|
787
|
+
);
|
|
788
|
+
const allowMarkdownFallback = hasAssistantIndicators || hasTurns || Boolean(userText);
|
|
789
|
+
const candidates =
|
|
790
|
+
actionMarkdowns.length > 0
|
|
791
|
+
? actionMarkdowns
|
|
792
|
+
: assistantMarkdowns.length > 0
|
|
793
|
+
? assistantMarkdowns
|
|
794
|
+
: allowMarkdownFallback
|
|
795
|
+
? markdowns
|
|
796
|
+
: [];
|
|
797
|
+
for (let i = candidates.length - 1; i >= 0; i -= 1) {
|
|
798
|
+
const node = candidates[i];
|
|
799
|
+
if (!node) continue;
|
|
800
|
+
if (!isAfterMinTurn(node)) continue;
|
|
801
|
+
const text = (node.innerText || node.textContent || '').trim();
|
|
802
|
+
if (!text) continue;
|
|
803
|
+
if (isUserEcho(text)) continue;
|
|
804
|
+
const html = node.innerHTML ?? '';
|
|
805
|
+
const turnIndex = resolveTurnIndex(node);
|
|
806
|
+
return { text, html, messageId: null, turnId: null, turnIndex };
|
|
807
|
+
}
|
|
808
|
+
return null;
|
|
809
|
+
})`;
|
|
810
|
+
}
|
|
504
811
|
function buildCopyExpression(meta) {
|
|
505
812
|
return `(() => {
|
|
506
813
|
${buildClickDispatcher()}
|
|
@@ -525,13 +832,41 @@ function buildCopyExpression(meta) {
|
|
|
525
832
|
return button;
|
|
526
833
|
}
|
|
527
834
|
}
|
|
835
|
+
const CONVERSATION_SELECTOR = ${JSON.stringify(CONVERSATION_TURN_SELECTOR)};
|
|
836
|
+
const ASSISTANT_SELECTOR = '${ASSISTANT_ROLE_SELECTOR}';
|
|
837
|
+
const isAssistantTurn = (node) => {
|
|
838
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
839
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
840
|
+
if (turnAttr === 'assistant') return true;
|
|
841
|
+
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
842
|
+
if (role === 'assistant') return true;
|
|
843
|
+
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
844
|
+
if (testId.includes('assistant')) return true;
|
|
845
|
+
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
846
|
+
};
|
|
847
|
+
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
848
|
+
for (let i = turns.length - 1; i >= 0; i -= 1) {
|
|
849
|
+
const turn = turns[i];
|
|
850
|
+
if (!isAssistantTurn(turn)) continue;
|
|
851
|
+
const button = turn.querySelector(BUTTON_SELECTOR);
|
|
852
|
+
if (button) {
|
|
853
|
+
return button;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
528
856
|
const all = Array.from(document.querySelectorAll(BUTTON_SELECTOR));
|
|
529
|
-
|
|
857
|
+
for (let i = all.length - 1; i >= 0; i -= 1) {
|
|
858
|
+
const button = all[i];
|
|
859
|
+
const turn = button?.closest?.(CONVERSATION_SELECTOR);
|
|
860
|
+
if (turn && isAssistantTurn(turn)) {
|
|
861
|
+
return button;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return null;
|
|
530
865
|
};
|
|
531
866
|
|
|
532
867
|
const interceptClipboard = () => {
|
|
533
868
|
const clipboard = navigator.clipboard;
|
|
534
|
-
const state = { text: '' };
|
|
869
|
+
const state = { text: '', updatedAt: 0 };
|
|
535
870
|
if (!clipboard) {
|
|
536
871
|
return { state, restore: () => {} };
|
|
537
872
|
}
|
|
@@ -539,6 +874,7 @@ function buildCopyExpression(meta) {
|
|
|
539
874
|
const originalWrite = clipboard.write;
|
|
540
875
|
clipboard.writeText = (value) => {
|
|
541
876
|
state.text = typeof value === 'string' ? value : '';
|
|
877
|
+
state.updatedAt = Date.now();
|
|
542
878
|
return Promise.resolve();
|
|
543
879
|
};
|
|
544
880
|
clipboard.write = async (items) => {
|
|
@@ -551,11 +887,13 @@ function buildCopyExpression(meta) {
|
|
|
551
887
|
const blob = await item.getType('text/plain');
|
|
552
888
|
const text = await blob.text();
|
|
553
889
|
state.text = text ?? '';
|
|
890
|
+
state.updatedAt = Date.now();
|
|
554
891
|
break;
|
|
555
892
|
}
|
|
556
893
|
}
|
|
557
894
|
} catch {
|
|
558
895
|
state.text = '';
|
|
896
|
+
state.updatedAt = Date.now();
|
|
559
897
|
}
|
|
560
898
|
return Promise.resolve();
|
|
561
899
|
};
|
|
@@ -595,22 +933,37 @@ function buildCopyExpression(meta) {
|
|
|
595
933
|
|
|
596
934
|
const readIntercepted = () => {
|
|
597
935
|
const markdown = interception.state.text ?? '';
|
|
598
|
-
|
|
936
|
+
const updatedAt = interception.state.updatedAt ?? 0;
|
|
937
|
+
return { success: Boolean(markdown.trim()), markdown, updatedAt };
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
let lastText = '';
|
|
941
|
+
let stableTicks = 0;
|
|
942
|
+
const requiredStableTicks = 3;
|
|
943
|
+
const requiredStableMs = 250;
|
|
944
|
+
const maybeFinish = () => {
|
|
945
|
+
const payload = readIntercepted();
|
|
946
|
+
if (!payload.success) return;
|
|
947
|
+
if (payload.markdown !== lastText) {
|
|
948
|
+
lastText = payload.markdown;
|
|
949
|
+
stableTicks = 0;
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
stableTicks += 1;
|
|
953
|
+
const ageMs = Date.now() - (payload.updatedAt || 0);
|
|
954
|
+
if (stableTicks >= requiredStableTicks && ageMs >= requiredStableMs) {
|
|
955
|
+
finish(payload);
|
|
956
|
+
}
|
|
599
957
|
};
|
|
600
958
|
|
|
601
959
|
const handleCopy = () => {
|
|
602
|
-
|
|
960
|
+
maybeFinish();
|
|
603
961
|
};
|
|
604
962
|
|
|
605
963
|
button.addEventListener('copy', handleCopy, true);
|
|
606
964
|
button.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
607
965
|
dispatchClickSequence(button);
|
|
608
|
-
pollId = setInterval(
|
|
609
|
-
const payload = readIntercepted();
|
|
610
|
-
if (payload.success) {
|
|
611
|
-
finish(payload);
|
|
612
|
-
}
|
|
613
|
-
}, 100);
|
|
966
|
+
pollId = setInterval(maybeFinish, 120);
|
|
614
967
|
timeoutId = setTimeout(() => {
|
|
615
968
|
button.removeEventListener('copy', handleCopy, true);
|
|
616
969
|
finish({ success: false, status: 'timeout' });
|