@steipete/oracle 0.10.0 → 0.11.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 +56 -11
- package/dist/bin/oracle-cli.js +104 -16
- package/dist/src/browser/actions/archiveConversation.js +236 -0
- package/dist/src/browser/actions/assistantResponse.js +26 -0
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/modelSelection.js +86 -16
- package/dist/src/browser/actions/navigation.js +22 -0
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +52 -27
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +214 -3
- package/dist/src/browser/config.js +27 -9
- package/dist/src/browser/constants.js +8 -0
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/detect.js +206 -33
- package/dist/src/browser/domDebug.js +49 -0
- package/dist/src/browser/index.js +1234 -479
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/profileState.js +83 -3
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/reattach.js +117 -45
- package/dist/src/browser/reattachHelpers.js +1 -1
- package/dist/src/browser/sessionRunner.js +53 -1
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/cli/bridge/claudeConfig.js +12 -8
- package/dist/src/cli/bridge/codexConfig.js +2 -2
- package/dist/src/cli/browserConfig.js +41 -8
- package/dist/src/cli/browserDefaults.js +31 -7
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/dryRun.js +33 -1
- package/dist/src/cli/duplicatePromptGuard.js +10 -2
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +4 -0
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/sessionCommand.js +51 -0
- package/dist/src/cli/sessionDisplay.js +121 -9
- package/dist/src/cli/sessionRunner.js +51 -7
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/consult.js +201 -26
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/types.js +11 -2
- package/dist/src/mcp/utils.js +6 -1
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/types.js +1 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/sessionManager.js +1 -0
- package/package.json +7 -6
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { DEEP_RESEARCH_PLUS_BUTTON, DEEP_RESEARCH_DROPDOWN_ITEM_TEXT, DEEP_RESEARCH_PILL_LABEL, DEEP_RESEARCH_POLL_INTERVAL_MS, DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS, DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, CONVERSATION_TURN_SELECTOR, } from "../constants.js";
|
|
2
|
+
import { delay } from "../utils.js";
|
|
3
|
+
import { buildClickDispatcher } from "./domEvents.js";
|
|
4
|
+
import { captureAssistantMarkdown, readAssistantSnapshot } from "./assistantResponse.js";
|
|
5
|
+
import { BrowserAutomationError } from "../../oracle/errors.js";
|
|
6
|
+
/**
|
|
7
|
+
* Activates Deep Research mode through ChatGPT's slash command, with the
|
|
8
|
+
* composer tools menu as a fallback for older UI variants.
|
|
9
|
+
*/
|
|
10
|
+
export async function activateDeepResearch(Runtime, _Input, logger) {
|
|
11
|
+
const expression = buildActivateDeepResearchExpression();
|
|
12
|
+
const outcome = await Runtime.evaluate({
|
|
13
|
+
expression,
|
|
14
|
+
awaitPromise: true,
|
|
15
|
+
returnByValue: true,
|
|
16
|
+
});
|
|
17
|
+
const result = outcome.result?.value;
|
|
18
|
+
switch (result?.status) {
|
|
19
|
+
case "activated":
|
|
20
|
+
logger("Deep Research mode activated");
|
|
21
|
+
return;
|
|
22
|
+
case "already-active":
|
|
23
|
+
logger("Deep Research mode already active");
|
|
24
|
+
return;
|
|
25
|
+
case "plus-button-missing":
|
|
26
|
+
throw new BrowserAutomationError("Could not find the composer plus button to activate Deep Research.", { stage: "deep-research-activate", code: "plus-button-missing" });
|
|
27
|
+
case "dropdown-item-missing": {
|
|
28
|
+
const hint = result.available?.length
|
|
29
|
+
? ` Available options: ${result.available.join(", ")}`
|
|
30
|
+
: "";
|
|
31
|
+
throw new BrowserAutomationError(`"Deep research" option not found in composer dropdown.${hint} ` +
|
|
32
|
+
"This feature may require a ChatGPT Plus or Pro subscription.", { stage: "deep-research-activate", code: "dropdown-item-missing" });
|
|
33
|
+
}
|
|
34
|
+
case "pill-not-confirmed":
|
|
35
|
+
throw new BrowserAutomationError("Deep Research pill did not appear after selection. The UI may have changed.", { stage: "deep-research-activate", code: "pill-not-confirmed" });
|
|
36
|
+
default:
|
|
37
|
+
throw new BrowserAutomationError("Unexpected result from Deep Research activation.", {
|
|
38
|
+
stage: "deep-research-activate",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* After prompt submission, waits for the research plan to appear and
|
|
44
|
+
* auto-confirm (~60s countdown + 10s safety margin).
|
|
45
|
+
*/
|
|
46
|
+
export async function waitForResearchPlanAutoConfirm(Runtime, logger, autoConfirmWaitMs = DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS) {
|
|
47
|
+
// Phase A: Detect research plan appearance (up to 60s)
|
|
48
|
+
const planDeadline = Date.now() + 60_000;
|
|
49
|
+
let planDetected = false;
|
|
50
|
+
while (Date.now() < planDeadline) {
|
|
51
|
+
const { result } = await Runtime.evaluate({
|
|
52
|
+
expression: `(() => {
|
|
53
|
+
const iframes = document.querySelectorAll('iframe');
|
|
54
|
+
const hasResearchIframe = Array.from(iframes).some(f => {
|
|
55
|
+
const rect = f.getBoundingClientRect();
|
|
56
|
+
return rect.width > 200 && rect.height > 200;
|
|
57
|
+
});
|
|
58
|
+
const assistantText = (document.querySelector('[data-message-author-role="assistant"]')?.textContent || '').toLowerCase();
|
|
59
|
+
const hasResearchText = assistantText.includes('researching') ||
|
|
60
|
+
assistantText.includes('research plan') ||
|
|
61
|
+
assistantText.includes('survey') ||
|
|
62
|
+
assistantText.includes('analyze');
|
|
63
|
+
return { hasResearchIframe, hasResearchText };
|
|
64
|
+
})()`,
|
|
65
|
+
returnByValue: true,
|
|
66
|
+
});
|
|
67
|
+
const val = result?.value;
|
|
68
|
+
if (val?.hasResearchIframe || val?.hasResearchText) {
|
|
69
|
+
planDetected = true;
|
|
70
|
+
logger("Research plan detected, waiting for auto-confirm countdown...");
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
await delay(2_000);
|
|
74
|
+
}
|
|
75
|
+
if (!planDetected) {
|
|
76
|
+
logger("Warning: Research plan not detected within 60s; continuing (may have auto-confirmed already)");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Phase B: Wait for auto-confirm countdown
|
|
80
|
+
const confirmStart = Date.now();
|
|
81
|
+
while (Date.now() - confirmStart < autoConfirmWaitMs) {
|
|
82
|
+
const { result } = await Runtime.evaluate({
|
|
83
|
+
expression: `(() => {
|
|
84
|
+
const iframes = document.querySelectorAll('iframe');
|
|
85
|
+
const hasLargeIframe = Array.from(iframes).some(f => {
|
|
86
|
+
const rect = f.getBoundingClientRect();
|
|
87
|
+
return rect.width > 200 && rect.height > 200;
|
|
88
|
+
});
|
|
89
|
+
const text = (document.body?.innerText || '').toLowerCase();
|
|
90
|
+
const isResearching = text.includes('researching...') ||
|
|
91
|
+
text.includes('reading sources') ||
|
|
92
|
+
text.includes('considering');
|
|
93
|
+
return { hasLargeIframe, isResearching };
|
|
94
|
+
})()`,
|
|
95
|
+
returnByValue: true,
|
|
96
|
+
});
|
|
97
|
+
const val = result?.value;
|
|
98
|
+
if (val?.isResearching) {
|
|
99
|
+
logger("Research plan confirmed, execution started");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await delay(5_000);
|
|
103
|
+
}
|
|
104
|
+
logger("Auto-confirm wait complete, proceeding to monitor research progress");
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Polls for Deep Research completion over 5-30+ minutes.
|
|
108
|
+
* Returns the full response text, optional HTML, and turn metadata.
|
|
109
|
+
*/
|
|
110
|
+
export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs = DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, minTurnIndex, Page, client) {
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
let lastLogTime = start;
|
|
113
|
+
let lastTextLength = 0;
|
|
114
|
+
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
115
|
+
? Math.floor(minTurnIndex)
|
|
116
|
+
: -1;
|
|
117
|
+
logger(`Monitoring Deep Research (timeout: ${Math.round(timeoutMs / 60_000)}min)...`);
|
|
118
|
+
while (Date.now() - start < timeoutMs) {
|
|
119
|
+
const { result } = await Runtime.evaluate({
|
|
120
|
+
expression: buildDeepResearchCompletionPollExpression(minTurnLiteral),
|
|
121
|
+
returnByValue: true,
|
|
122
|
+
});
|
|
123
|
+
const val = result?.value;
|
|
124
|
+
if (val?.accountBlocked) {
|
|
125
|
+
throw new BrowserAutomationError("ChatGPT account security block detected during Deep Research. Open chatgpt.com in Chrome, secure the account, then rerun Oracle.", { stage: "chatgpt-account-blocked", code: "chatgpt-account-blocked" });
|
|
126
|
+
}
|
|
127
|
+
const frameResult = Page
|
|
128
|
+
? await readDeepResearchFrameResult(Runtime, Page).catch(() => null)
|
|
129
|
+
: client
|
|
130
|
+
? await readDeepResearchTargetResult(client).catch(() => null)
|
|
131
|
+
: null;
|
|
132
|
+
const scopedToNewTurns = minTurnLiteral >= 0;
|
|
133
|
+
if (frameResult?.completed &&
|
|
134
|
+
frameResult.text &&
|
|
135
|
+
(!scopedToNewTurns || val?.hasActiveScopedResearch)) {
|
|
136
|
+
logger(`Deep Research completed (${Math.round((Date.now() - start) / 1000)}s elapsed)`);
|
|
137
|
+
return {
|
|
138
|
+
text: frameResult.text,
|
|
139
|
+
html: frameResult.html,
|
|
140
|
+
meta: { turnId: null, messageId: null },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// Completion detected
|
|
144
|
+
if (val?.finished) {
|
|
145
|
+
logger(`Deep Research completed (${Math.round((Date.now() - start) / 1000)}s elapsed)`);
|
|
146
|
+
return await extractDeepResearchResult(Runtime, logger, minTurnIndex ?? undefined);
|
|
147
|
+
}
|
|
148
|
+
// Progress logging every 60 seconds
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
if (now - lastLogTime >= 60_000) {
|
|
151
|
+
const elapsed = Math.round((now - start) / 1000);
|
|
152
|
+
const chars = Math.max(val?.textLength ?? 0, frameResult?.textLength ?? 0);
|
|
153
|
+
const phase = frameResult?.inProgress || val?.hasIframe
|
|
154
|
+
? "researching"
|
|
155
|
+
: val?.stopVisible
|
|
156
|
+
? "generating"
|
|
157
|
+
: "waiting";
|
|
158
|
+
logger(`Deep Research ${phase}... ${elapsed}s elapsed, ~${chars} chars`);
|
|
159
|
+
lastLogTime = now;
|
|
160
|
+
}
|
|
161
|
+
lastTextLength = Math.max(val?.textLength ?? 0, frameResult?.textLength ?? 0, lastTextLength);
|
|
162
|
+
await delay(DEEP_RESEARCH_POLL_INTERVAL_MS);
|
|
163
|
+
}
|
|
164
|
+
// Timeout — throw with metadata for potential reattach
|
|
165
|
+
const elapsed = Math.round((Date.now() - start) / 1000);
|
|
166
|
+
throw new BrowserAutomationError(`Deep Research did not complete within ${Math.round(timeoutMs / 60_000)} minutes (${elapsed}s elapsed). ` +
|
|
167
|
+
"Use 'oracle session <id>' to reattach later, or increase --timeout.", {
|
|
168
|
+
stage: "deep-research-timeout",
|
|
169
|
+
code: "deep-research-timeout",
|
|
170
|
+
elapsedMs: Date.now() - start,
|
|
171
|
+
lastTextLength,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Extracts the Deep Research result using existing assistant response
|
|
176
|
+
* extraction logic (readAssistantSnapshot + captureAssistantMarkdown).
|
|
177
|
+
*/
|
|
178
|
+
export async function extractDeepResearchResult(Runtime, logger, minTurnIndex) {
|
|
179
|
+
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
|
|
180
|
+
const meta = {
|
|
181
|
+
turnId: snapshot?.turnId ?? null,
|
|
182
|
+
messageId: snapshot?.messageId ?? null,
|
|
183
|
+
};
|
|
184
|
+
// Try the copy-button approach first for clean markdown
|
|
185
|
+
const markdown = await captureAssistantMarkdown(Runtime, meta, logger);
|
|
186
|
+
if (markdown && !isDeepResearchPlaceholderText(markdown)) {
|
|
187
|
+
return { text: markdown, html: snapshot?.html ?? undefined, meta };
|
|
188
|
+
}
|
|
189
|
+
// Fall back to snapshot text
|
|
190
|
+
if (snapshot?.text && !isDeepResearchPlaceholderText(snapshot.text)) {
|
|
191
|
+
return { text: snapshot.text, html: snapshot.html ?? undefined, meta };
|
|
192
|
+
}
|
|
193
|
+
throw new BrowserAutomationError("Deep Research completed but failed to extract the response text.", { stage: "deep-research-extract", code: "extraction-failed" });
|
|
194
|
+
}
|
|
195
|
+
function isDeepResearchPlaceholderText(text) {
|
|
196
|
+
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
197
|
+
return (normalized === "called tool" ||
|
|
198
|
+
normalized === "used tool" ||
|
|
199
|
+
normalized === "użyto narzędzia" ||
|
|
200
|
+
normalized === "narzędzie wywołane");
|
|
201
|
+
}
|
|
202
|
+
export function isDeepResearchPlaceholderTextForTest(text) {
|
|
203
|
+
return isDeepResearchPlaceholderText(text);
|
|
204
|
+
}
|
|
205
|
+
async function readDeepResearchFrameResult(Runtime, Page) {
|
|
206
|
+
const pageWithFrames = Page;
|
|
207
|
+
if (typeof pageWithFrames.getFrameTree !== "function" ||
|
|
208
|
+
typeof pageWithFrames.createIsolatedWorld !== "function") {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const frameTree = (await pageWithFrames.getFrameTree())?.frameTree;
|
|
212
|
+
const frameId = findDeepResearchFrameId(frameTree);
|
|
213
|
+
if (!frameId) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const world = await pageWithFrames.createIsolatedWorld({
|
|
217
|
+
frameId,
|
|
218
|
+
worldName: "oracle-deep-research",
|
|
219
|
+
grantUniveralAccess: true,
|
|
220
|
+
});
|
|
221
|
+
if (typeof world.executionContextId !== "number") {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const { result } = await Runtime.evaluate({
|
|
225
|
+
expression: buildDeepResearchFrameStatusExpression(),
|
|
226
|
+
contextId: world.executionContextId,
|
|
227
|
+
returnByValue: true,
|
|
228
|
+
});
|
|
229
|
+
return result?.value ?? null;
|
|
230
|
+
}
|
|
231
|
+
async function readDeepResearchTargetResult(client) {
|
|
232
|
+
const rawClient = client;
|
|
233
|
+
if (typeof rawClient.send !== "function") {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const sessionIds = new Set();
|
|
237
|
+
const ownedSessionIds = new Set();
|
|
238
|
+
const onAttached = (params, sessionId) => {
|
|
239
|
+
const targetInfo = params
|
|
240
|
+
?.targetInfo;
|
|
241
|
+
const eventSessionId = params?.sessionId ?? sessionId;
|
|
242
|
+
const url = targetInfo?.url ?? "";
|
|
243
|
+
const type = targetInfo?.type ?? "";
|
|
244
|
+
if (eventSessionId && isDeepResearchTarget(url, type)) {
|
|
245
|
+
sessionIds.add(eventSessionId);
|
|
246
|
+
ownedSessionIds.add(eventSessionId);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
client.on?.("Target.attachedToTarget", onAttached);
|
|
250
|
+
try {
|
|
251
|
+
await rawClient.send("Target.setDiscoverTargets", { discover: true }).catch(() => undefined);
|
|
252
|
+
await rawClient
|
|
253
|
+
.send("Target.setAutoAttach", {
|
|
254
|
+
autoAttach: true,
|
|
255
|
+
waitForDebuggerOnStart: false,
|
|
256
|
+
flatten: true,
|
|
257
|
+
})
|
|
258
|
+
.catch(() => undefined);
|
|
259
|
+
await delay(100);
|
|
260
|
+
const targets = (await rawClient.send("Target.getTargets", {}));
|
|
261
|
+
for (const target of targets?.targetInfos ?? []) {
|
|
262
|
+
if (!target.targetId || !isDeepResearchTarget(target.url ?? "", target.type ?? "")) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const attached = (await rawClient
|
|
266
|
+
.send("Target.attachToTarget", { targetId: target.targetId, flatten: true })
|
|
267
|
+
.catch(() => null));
|
|
268
|
+
if (attached?.sessionId) {
|
|
269
|
+
sessionIds.add(attached.sessionId);
|
|
270
|
+
ownedSessionIds.add(attached.sessionId);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const sessionId of sessionIds) {
|
|
274
|
+
const value = await readDeepResearchTargetSession(rawClient, sessionId);
|
|
275
|
+
if (value?.completed) {
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
278
|
+
if (value?.inProgress || value?.textLength) {
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
await rawClient
|
|
286
|
+
.send("Target.setAutoAttach", {
|
|
287
|
+
autoAttach: false,
|
|
288
|
+
waitForDebuggerOnStart: false,
|
|
289
|
+
flatten: true,
|
|
290
|
+
})
|
|
291
|
+
.catch(() => undefined);
|
|
292
|
+
await Promise.all(Array.from(ownedSessionIds, (sessionId) => rawClient.send("Target.detachFromTarget", { sessionId }).catch(() => undefined)));
|
|
293
|
+
client.removeListener?.("Target.attachedToTarget", onAttached);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function readDeepResearchTargetSession(rawClient, sessionId) {
|
|
297
|
+
await rawClient.send("Runtime.enable", {}, sessionId).catch(() => undefined);
|
|
298
|
+
await rawClient.send("Page.enable", {}, sessionId).catch(() => undefined);
|
|
299
|
+
const frameTree = (await rawClient
|
|
300
|
+
.send("Page.getFrameTree", {}, sessionId)
|
|
301
|
+
.catch(() => null));
|
|
302
|
+
const frameIds = collectDeepResearchFrameIds(frameTree?.frameTree);
|
|
303
|
+
let best = null;
|
|
304
|
+
for (const frameId of frameIds) {
|
|
305
|
+
const world = (await rawClient
|
|
306
|
+
.send("Page.createIsolatedWorld", {
|
|
307
|
+
frameId,
|
|
308
|
+
worldName: "oracle-deep-research",
|
|
309
|
+
grantUniveralAccess: true,
|
|
310
|
+
}, sessionId)
|
|
311
|
+
.catch(() => null));
|
|
312
|
+
if (typeof world?.executionContextId !== "number") {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const value = await evaluateDeepResearchFrameStatus(rawClient, sessionId, world.executionContextId);
|
|
316
|
+
if (value?.completed) {
|
|
317
|
+
return value;
|
|
318
|
+
}
|
|
319
|
+
if ((value?.textLength ?? 0) > (best?.textLength ?? 0) || value?.inProgress) {
|
|
320
|
+
best = value;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const topFrameValue = await evaluateDeepResearchFrameStatus(rawClient, sessionId);
|
|
324
|
+
if (topFrameValue?.completed) {
|
|
325
|
+
return topFrameValue;
|
|
326
|
+
}
|
|
327
|
+
if ((topFrameValue?.textLength ?? 0) > (best?.textLength ?? 0) || topFrameValue?.inProgress) {
|
|
328
|
+
best = topFrameValue;
|
|
329
|
+
}
|
|
330
|
+
return best;
|
|
331
|
+
}
|
|
332
|
+
async function evaluateDeepResearchFrameStatus(rawClient, sessionId, contextId) {
|
|
333
|
+
const response = (await rawClient
|
|
334
|
+
.send("Runtime.evaluate", {
|
|
335
|
+
expression: buildDeepResearchFrameStatusExpression(),
|
|
336
|
+
returnByValue: true,
|
|
337
|
+
...(typeof contextId === "number" ? { contextId } : {}),
|
|
338
|
+
}, sessionId)
|
|
339
|
+
.catch(() => null));
|
|
340
|
+
return response?.result?.value ?? null;
|
|
341
|
+
}
|
|
342
|
+
function isDeepResearchTarget(url, type) {
|
|
343
|
+
const lowerUrl = url.toLowerCase();
|
|
344
|
+
const lowerType = type.toLowerCase();
|
|
345
|
+
return (lowerType === "iframe" ||
|
|
346
|
+
lowerUrl.includes("connector_openai_deep_research") ||
|
|
347
|
+
lowerUrl.includes("deep-research"));
|
|
348
|
+
}
|
|
349
|
+
function findDeepResearchFrameId(tree) {
|
|
350
|
+
if (!tree?.frame) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
const url = tree.frame.url ?? "";
|
|
354
|
+
const name = tree.frame.name ?? "";
|
|
355
|
+
if (url.includes("connector_openai_deep_research") ||
|
|
356
|
+
url.includes("deep-research") ||
|
|
357
|
+
name.includes("deep-research")) {
|
|
358
|
+
return tree.frame.id ?? null;
|
|
359
|
+
}
|
|
360
|
+
for (const child of tree.childFrames ?? []) {
|
|
361
|
+
const match = findDeepResearchFrameId(child);
|
|
362
|
+
if (match) {
|
|
363
|
+
return match;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
function collectDeepResearchFrameIds(tree) {
|
|
369
|
+
if (!tree?.frame) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
const ids = [];
|
|
373
|
+
const url = tree.frame.url ?? "";
|
|
374
|
+
const name = tree.frame.name ?? "";
|
|
375
|
+
if (url.includes("connector_openai_deep_research") ||
|
|
376
|
+
url.includes("deep-research") ||
|
|
377
|
+
name.includes("deep-research") ||
|
|
378
|
+
name === "root") {
|
|
379
|
+
if (tree.frame.id) {
|
|
380
|
+
ids.push(tree.frame.id);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
for (const child of tree.childFrames ?? []) {
|
|
384
|
+
ids.push(...collectDeepResearchFrameIds(child));
|
|
385
|
+
}
|
|
386
|
+
return ids;
|
|
387
|
+
}
|
|
388
|
+
function buildDeepResearchFrameStatusExpression() {
|
|
389
|
+
return `(() => {
|
|
390
|
+
const rawText = document.body?.innerText || '';
|
|
391
|
+
const html = document.body?.innerHTML || '';
|
|
392
|
+
const isPlaceholder = (line) => /^(called tool|used tool|użyto narzędzia|narzędzie wywołane)$/i.test(line);
|
|
393
|
+
const isCompletionLine = (line) =>
|
|
394
|
+
/^(research completed|badanie ukończone)\\b/i.test(line);
|
|
395
|
+
const isCounterLine = (line) =>
|
|
396
|
+
/^(\\d+\\s+)?(citation|citations|source|sources|search|searches|cytat|cytaty|cytatów|źródło|źródła|wyszukiwanie|wyszukiwania|wyszukiwań)\\b/i.test(line);
|
|
397
|
+
const normalizeReport = (text) => {
|
|
398
|
+
const lines = String(text || '')
|
|
399
|
+
.split(/\\n+/)
|
|
400
|
+
.map((line) => line.trim())
|
|
401
|
+
.filter(Boolean)
|
|
402
|
+
.filter((line) => !/^\\d+$/.test(line));
|
|
403
|
+
const reportIndex = lines.findIndex((line) => /deep research report/i.test(line));
|
|
404
|
+
const candidates = reportIndex >= 0 ? lines.slice(reportIndex + 1) : lines;
|
|
405
|
+
let started = false;
|
|
406
|
+
const reportLines = candidates.filter((line) => {
|
|
407
|
+
if (!started) {
|
|
408
|
+
if (
|
|
409
|
+
/deep research report/i.test(line) ||
|
|
410
|
+
isCompletionLine(line) ||
|
|
411
|
+
isCounterLine(line) ||
|
|
412
|
+
isPlaceholder(line)
|
|
413
|
+
) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
started = true;
|
|
417
|
+
}
|
|
418
|
+
return true;
|
|
419
|
+
});
|
|
420
|
+
if (reportLines.length > 1 && reportLines[0] === reportLines[1]) {
|
|
421
|
+
reportLines.shift();
|
|
422
|
+
}
|
|
423
|
+
return reportLines.join('\\n').trim();
|
|
424
|
+
};
|
|
425
|
+
const reportText = normalizeReport(rawText);
|
|
426
|
+
const completed = /research completed|badanie ukończone/i.test(rawText) &&
|
|
427
|
+
reportText.length >= 40 &&
|
|
428
|
+
!isPlaceholder(reportText);
|
|
429
|
+
const inProgress = /researching|badanie|searching|searches|wyszukiwa|citation|cytat|source|źród|reading|completed|ukończone/i.test(rawText);
|
|
430
|
+
return {
|
|
431
|
+
completed,
|
|
432
|
+
inProgress,
|
|
433
|
+
textLength: reportText.length || rawText.trim().length,
|
|
434
|
+
text: completed ? reportText : undefined,
|
|
435
|
+
html: completed ? html : undefined,
|
|
436
|
+
};
|
|
437
|
+
})()`;
|
|
438
|
+
}
|
|
439
|
+
export function findDeepResearchFrameIdForTest(tree) {
|
|
440
|
+
return findDeepResearchFrameId(tree);
|
|
441
|
+
}
|
|
442
|
+
export function buildDeepResearchFrameStatusExpressionForTest() {
|
|
443
|
+
return buildDeepResearchFrameStatusExpression();
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Quick status check for Deep Research — used during reattach to determine
|
|
447
|
+
* whether research has completed, is still in progress, or is in an unknown state.
|
|
448
|
+
*/
|
|
449
|
+
export async function checkDeepResearchStatus(Runtime, _logger) {
|
|
450
|
+
const { result } = await Runtime.evaluate({
|
|
451
|
+
expression: buildDeepResearchStatusExpression(),
|
|
452
|
+
returnByValue: true,
|
|
453
|
+
});
|
|
454
|
+
const val = result?.value;
|
|
455
|
+
return {
|
|
456
|
+
completed: val?.completed ?? false,
|
|
457
|
+
inProgress: val?.inProgress ?? false,
|
|
458
|
+
hasIframe: val?.hasIframe ?? false,
|
|
459
|
+
textLength: val?.textLength ?? 0,
|
|
460
|
+
placeholderOnly: val?.placeholderOnly ?? false,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// DOM expression builder
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
function buildDeepResearchStatusExpression() {
|
|
467
|
+
const finishedSelector = JSON.stringify(FINISHED_ACTIONS_SELECTOR);
|
|
468
|
+
const stopSelector = JSON.stringify(STOP_BUTTON_SELECTOR);
|
|
469
|
+
return `(() => {
|
|
470
|
+
const stopVisible = Boolean(document.querySelector(${stopSelector}));
|
|
471
|
+
const iframes = Array.from(document.querySelectorAll('iframe')).filter(f => {
|
|
472
|
+
const rect = f.getBoundingClientRect();
|
|
473
|
+
return rect.width > 200 && rect.height > 200;
|
|
474
|
+
});
|
|
475
|
+
const turns = document.querySelectorAll('[data-message-author-role="assistant"]');
|
|
476
|
+
const lastTurn = turns[turns.length - 1];
|
|
477
|
+
const finished = Boolean(lastTurn?.querySelector?.(${finishedSelector}));
|
|
478
|
+
const text = (lastTurn?.textContent || '').trim();
|
|
479
|
+
const normalized = text.toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
480
|
+
const placeholderOnly = /^(called tool|used tool|użyto narzędzia|narzędzie wywołane)$/.test(normalized);
|
|
481
|
+
const textLength = text.length;
|
|
482
|
+
return {
|
|
483
|
+
completed: finished && !placeholderOnly && textLength >= 40,
|
|
484
|
+
inProgress: stopVisible || iframes.length > 0,
|
|
485
|
+
hasIframe: iframes.length > 0,
|
|
486
|
+
textLength,
|
|
487
|
+
placeholderOnly,
|
|
488
|
+
};
|
|
489
|
+
})()`;
|
|
490
|
+
}
|
|
491
|
+
function buildDeepResearchCompletionPollExpression(minTurnIndex) {
|
|
492
|
+
const finishedSelector = JSON.stringify(FINISHED_ACTIONS_SELECTOR);
|
|
493
|
+
const stopSelector = JSON.stringify(STOP_BUTTON_SELECTOR);
|
|
494
|
+
const turnSelector = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
495
|
+
return `(() => {
|
|
496
|
+
const MIN_TURN_INDEX = ${minTurnIndex};
|
|
497
|
+
const stopVisible = Boolean(document.querySelector(${stopSelector}));
|
|
498
|
+
const scopedToNewTurns = MIN_TURN_INDEX >= 0;
|
|
499
|
+
const pageText = String(document.body?.innerText || '').toLowerCase().replace(/\\s+/g, ' ');
|
|
500
|
+
const accountBlocked = pageText.includes('suspicious activity detected') &&
|
|
501
|
+
pageText.includes('secure your account') &&
|
|
502
|
+
pageText.includes('regain access');
|
|
503
|
+
const isAssistantTurn = (node) => {
|
|
504
|
+
const attr = String(node.getAttribute('data-message-author-role') || node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
505
|
+
return attr === 'assistant' ||
|
|
506
|
+
Boolean(node.querySelector('[data-message-author-role="assistant"], [data-turn="assistant"]')) ||
|
|
507
|
+
String(node.getAttribute('data-testid') || '').toLowerCase().includes('conversation-turn') &&
|
|
508
|
+
/chatgpt\\s+said/i.test(node.innerText || node.textContent || '');
|
|
509
|
+
};
|
|
510
|
+
const conversationTurns = Array.from(document.querySelectorAll(${turnSelector}));
|
|
511
|
+
const allAssistantTurns = Array.from(document.querySelectorAll('[data-message-author-role="assistant"], [data-turn="assistant"]'));
|
|
512
|
+
const scopedTurns = scopedToNewTurns
|
|
513
|
+
? conversationTurns.slice(MIN_TURN_INDEX).filter(isAssistantTurn)
|
|
514
|
+
: allAssistantTurns;
|
|
515
|
+
const lastTurn = scopedTurns[scopedTurns.length - 1] || (scopedToNewTurns ? null : allAssistantTurns[allAssistantTurns.length - 1]);
|
|
516
|
+
const text = (lastTurn?.textContent || '').trim();
|
|
517
|
+
const normalized = text.toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
518
|
+
const textLength = text.length;
|
|
519
|
+
const isToolStub = normalized === 'called tool' ||
|
|
520
|
+
normalized === 'used tool' ||
|
|
521
|
+
normalized === 'użyto narzędzia' ||
|
|
522
|
+
normalized === 'narzędzie wywołane';
|
|
523
|
+
const finished = Boolean(lastTurn?.querySelector(${finishedSelector})) &&
|
|
524
|
+
textLength >= 40 &&
|
|
525
|
+
!isToolStub;
|
|
526
|
+
const hasIframe = Array.from(document.querySelectorAll('iframe')).some(f => {
|
|
527
|
+
const rect = f.getBoundingClientRect();
|
|
528
|
+
return rect.width > 200 && rect.height > 200;
|
|
529
|
+
});
|
|
530
|
+
const hasActiveScopedResearch = scopedToNewTurns && Boolean(lastTurn) && hasIframe &&
|
|
531
|
+
(textLength < 40 || isToolStub || /chatgpt\\s+said:?$/i.test(text));
|
|
532
|
+
return { finished, stopVisible, textLength, hasIframe, isToolStub, hasActiveScopedResearch, accountBlocked };
|
|
533
|
+
})()`;
|
|
534
|
+
}
|
|
535
|
+
export function buildDeepResearchStatusExpressionForTest() {
|
|
536
|
+
return buildDeepResearchStatusExpression();
|
|
537
|
+
}
|
|
538
|
+
export function buildDeepResearchCompletionPollExpressionForTest(minTurnIndex = -1) {
|
|
539
|
+
return buildDeepResearchCompletionPollExpression(minTurnIndex);
|
|
540
|
+
}
|
|
541
|
+
function buildActivateDeepResearchExpression() {
|
|
542
|
+
const plusBtnSelector = JSON.stringify(DEEP_RESEARCH_PLUS_BUTTON);
|
|
543
|
+
const targetText = JSON.stringify(DEEP_RESEARCH_DROPDOWN_ITEM_TEXT);
|
|
544
|
+
const pillLabel = JSON.stringify(DEEP_RESEARCH_PILL_LABEL);
|
|
545
|
+
// pillLabel is used inside the expression for verification
|
|
546
|
+
void pillLabel;
|
|
547
|
+
return `(async () => {
|
|
548
|
+
${buildClickDispatcher()}
|
|
549
|
+
|
|
550
|
+
const findDeepResearchPill = () => {
|
|
551
|
+
const pills = document.querySelectorAll('.__composer-pill-composite, .__composer-pill, [class*="composer-pill"]');
|
|
552
|
+
for (const pill of pills) {
|
|
553
|
+
const text = pill.textContent?.trim() || '';
|
|
554
|
+
const aria = pill.getAttribute('aria-label') ||
|
|
555
|
+
pill.querySelector('button')?.getAttribute('aria-label') ||
|
|
556
|
+
'';
|
|
557
|
+
if (text.toLowerCase().includes('deep research') ||
|
|
558
|
+
aria.toLowerCase().includes('deep research')) {
|
|
559
|
+
return pill;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const waitForPill = () => new Promise((resolve) => {
|
|
566
|
+
let elapsed = 0;
|
|
567
|
+
const tick = () => {
|
|
568
|
+
if (findDeepResearchPill()) {
|
|
569
|
+
resolve(true); return;
|
|
570
|
+
}
|
|
571
|
+
elapsed += 200;
|
|
572
|
+
if (elapsed > 5000) { resolve(false); return; }
|
|
573
|
+
setTimeout(tick, 200);
|
|
574
|
+
};
|
|
575
|
+
setTimeout(tick, 200);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const clearComposer = (composer) => {
|
|
579
|
+
if (!composer) return;
|
|
580
|
+
if ('value' in composer) composer.value = '';
|
|
581
|
+
else composer.textContent = '';
|
|
582
|
+
composer.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward', data: null }));
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const setComposerText = (composer, text) => {
|
|
586
|
+
composer.focus?.();
|
|
587
|
+
if ('value' in composer) composer.value = text;
|
|
588
|
+
else composer.textContent = text;
|
|
589
|
+
composer.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const findDeepResearchItem = () => {
|
|
593
|
+
const target = ${targetText}.toLowerCase();
|
|
594
|
+
const candidates = Array.from(document.querySelectorAll('[data-radix-collection-item], [role="option"], [cmdk-item], button, [role="menuitem"], [role="menuitemradio"]'));
|
|
595
|
+
return candidates.find(item => (item.textContent || '').trim().toLowerCase() === target) || null;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// Step 0: Check if already active
|
|
599
|
+
if (findDeepResearchPill()) {
|
|
600
|
+
return { status: 'already-active' };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Step 1: Prefer the official slash command flow.
|
|
604
|
+
const composer = document.querySelector('[contenteditable="true"], textarea');
|
|
605
|
+
if (composer) {
|
|
606
|
+
setComposerText(composer, '/Deepresearch');
|
|
607
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
608
|
+
const slashItem = findDeepResearchItem();
|
|
609
|
+
if (slashItem) {
|
|
610
|
+
dispatchClickSequence(slashItem);
|
|
611
|
+
if (await waitForPill()) return { status: 'activated' };
|
|
612
|
+
}
|
|
613
|
+
clearComposer(composer);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Step 2: Fall back to the composer tools menu.
|
|
617
|
+
const plusBtn = document.querySelector(${plusBtnSelector}) ||
|
|
618
|
+
Array.from(document.querySelectorAll('button')).find(
|
|
619
|
+
b => (b.getAttribute('aria-label') || '').toLowerCase().includes('add files')
|
|
620
|
+
);
|
|
621
|
+
if (!plusBtn) return { status: 'plus-button-missing' };
|
|
622
|
+
dispatchClickSequence(plusBtn);
|
|
623
|
+
|
|
624
|
+
// Step 3: Wait for dropdown
|
|
625
|
+
const waitForDropdown = () => new Promise((resolve) => {
|
|
626
|
+
let elapsed = 0;
|
|
627
|
+
const tick = () => {
|
|
628
|
+
const items = document.querySelectorAll('[data-radix-collection-item], [role="menuitem"], [role="menuitemradio"], [role="option"], [cmdk-item]');
|
|
629
|
+
if (items.length > 0) { resolve(items); return; }
|
|
630
|
+
elapsed += 150;
|
|
631
|
+
if (elapsed > 3000) { resolve(null); return; }
|
|
632
|
+
setTimeout(tick, 150);
|
|
633
|
+
};
|
|
634
|
+
setTimeout(tick, 150);
|
|
635
|
+
});
|
|
636
|
+
const items = await waitForDropdown();
|
|
637
|
+
if (!items) return { status: 'dropdown-item-missing', available: [] };
|
|
638
|
+
|
|
639
|
+
// Step 4: Find "Deep research" item
|
|
640
|
+
const target = ${targetText}.toLowerCase();
|
|
641
|
+
let match = null;
|
|
642
|
+
const available = [];
|
|
643
|
+
for (const item of items) {
|
|
644
|
+
const text = (item.textContent || '').trim();
|
|
645
|
+
available.push(text);
|
|
646
|
+
if (text.toLowerCase() === target) {
|
|
647
|
+
match = item;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (!match) return { status: 'dropdown-item-missing', available };
|
|
651
|
+
|
|
652
|
+
// Step 5: Click it
|
|
653
|
+
dispatchClickSequence(match);
|
|
654
|
+
|
|
655
|
+
// Step 6: Verify pill appeared
|
|
656
|
+
const pillConfirmed = await waitForPill();
|
|
657
|
+
return pillConfirmed ? { status: 'activated' } : { status: 'pill-not-confirmed' };
|
|
658
|
+
})()`;
|
|
659
|
+
}
|
|
660
|
+
export function buildActivateDeepResearchExpressionForTest() {
|
|
661
|
+
return buildActivateDeepResearchExpression();
|
|
662
|
+
}
|