@steipete/oracle 0.9.0 → 0.11.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/LICENSE +1 -1
- package/README.md +107 -49
- package/dist/bin/oracle-cli.js +551 -410
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/archiveConversation.js +224 -0
- package/dist/src/browser/actions/assistantResponse.js +175 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +342 -119
- package/dist/src/browser/actions/navigation.js +183 -137
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +152 -91
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- 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 +276 -63
- package/dist/src/browser/config.js +59 -16
- package/dist/src/browser/constants.js +25 -12
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +250 -77
- package/dist/src/browser/domDebug.js +50 -1
- package/dist/src/browser/index.js +1559 -692
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +127 -42
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +178 -73
- package/dist/src/browser/reattachHelpers.js +32 -27
- package/dist/src/browser/sessionRunner.js +89 -25
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +24 -20
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +102 -48
- package/dist/src/cli/browserDefaults.js +51 -26
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +62 -26
- package/dist/src/cli/duplicatePromptGuard.js +12 -4
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +131 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +82 -21
- package/dist/src/cli/sessionDisplay.js +213 -87
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +149 -95
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +18 -12
- package/dist/src/mcp/tools/consult.js +246 -67
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +12 -5
- package/dist/src/mcp/utils.js +21 -8
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +160 -135
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +78 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +67 -62
- package/vendor/oracle-notifier/README.md +2 -0
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/profileSync.js +0 -141
- /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
|
@@ -1,46 +1,51 @@
|
|
|
1
|
-
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, } from
|
|
2
|
-
import { delay } from
|
|
3
|
-
import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression } from
|
|
4
|
-
import { buildClickDispatcher } from
|
|
5
|
-
const ASSISTANT_POLL_TIMEOUT_ERROR =
|
|
1
|
+
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, COPY_BUTTON_SELECTOR, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, } from "../constants.js";
|
|
2
|
+
import { delay } from "../utils.js";
|
|
3
|
+
import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression, } from "../domDebug.js";
|
|
4
|
+
import { buildClickDispatcher } from "./domEvents.js";
|
|
5
|
+
const ASSISTANT_POLL_TIMEOUT_ERROR = "assistant-response-watchdog-timeout";
|
|
6
6
|
function isAnswerNowPlaceholderText(normalized) {
|
|
7
7
|
const text = normalized.trim();
|
|
8
8
|
if (!text)
|
|
9
9
|
return false;
|
|
10
10
|
// Learned: "Pro thinking" shows a placeholder turn that contains "Answer now".
|
|
11
11
|
// That is not the final answer and must be ignored in browser automation.
|
|
12
|
-
if (text ===
|
|
12
|
+
if (text === "chatgpt said:" || text === "chatgpt said")
|
|
13
13
|
return true;
|
|
14
|
-
if (text.includes(
|
|
14
|
+
if (text.includes("file upload request") &&
|
|
15
|
+
(text.includes("pro thinking") || text.includes("chatgpt said"))) {
|
|
15
16
|
return true;
|
|
16
17
|
}
|
|
17
|
-
return text.includes(
|
|
18
|
+
return (text.includes("answer now") && (text.includes("pro thinking") || text.includes("chatgpt said")));
|
|
18
19
|
}
|
|
19
|
-
export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex) {
|
|
20
|
+
export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId) {
|
|
20
21
|
const start = Date.now();
|
|
21
|
-
logger(
|
|
22
|
+
logger("Waiting for ChatGPT response");
|
|
22
23
|
// Learned: two paths are needed:
|
|
23
24
|
// 1) DOM observer (fast when mutations fire),
|
|
24
25
|
// 2) snapshot poller (fallback when observers miss or JS stalls).
|
|
25
|
-
const expression = buildResponseObserverExpression(timeoutMs, minTurnIndex);
|
|
26
|
-
const evaluationPromise = Runtime.evaluate({
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const expression = buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConversationId);
|
|
27
|
+
const evaluationPromise = Runtime.evaluate({
|
|
28
|
+
expression,
|
|
29
|
+
awaitPromise: true,
|
|
30
|
+
returnByValue: true,
|
|
31
|
+
});
|
|
32
|
+
const raceReadyEvaluation = evaluationPromise.then((value) => ({ kind: "evaluation", value }), (error) => {
|
|
33
|
+
throw { source: "evaluation", error };
|
|
29
34
|
});
|
|
30
35
|
// Use AbortController to stop the poller when the evaluation wins the race,
|
|
31
36
|
// preventing abandoned polling loops from consuming resources.
|
|
32
37
|
const pollerAbort = new AbortController();
|
|
33
|
-
const pollerPromise = pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, pollerAbort.signal).then((value) => ({ kind:
|
|
34
|
-
throw { source:
|
|
38
|
+
const pollerPromise = pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expectedConversationId, pollerAbort.signal).then((value) => ({ kind: "poll", value }), (error) => {
|
|
39
|
+
throw { source: "poll", error };
|
|
35
40
|
});
|
|
36
41
|
let evaluation = null;
|
|
37
42
|
try {
|
|
38
43
|
const winner = await Promise.race([raceReadyEvaluation, pollerPromise]);
|
|
39
|
-
if (winner.kind ===
|
|
44
|
+
if (winner.kind === "poll") {
|
|
40
45
|
if (!winner.value) {
|
|
41
|
-
throw { source:
|
|
46
|
+
throw { source: "poll", error: new Error(ASSISTANT_POLL_TIMEOUT_ERROR) };
|
|
42
47
|
}
|
|
43
|
-
logger(
|
|
48
|
+
logger("Captured assistant response via snapshot watchdog");
|
|
44
49
|
evaluationPromise.catch(() => undefined);
|
|
45
50
|
await terminateRuntimeExecution(Runtime);
|
|
46
51
|
return winner.value;
|
|
@@ -50,21 +55,26 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
50
55
|
evaluation = winner.value;
|
|
51
56
|
}
|
|
52
57
|
catch (wrappedError) {
|
|
53
|
-
if (wrappedError &&
|
|
58
|
+
if (wrappedError &&
|
|
59
|
+
typeof wrappedError === "object" &&
|
|
60
|
+
"source" in wrappedError &&
|
|
61
|
+
"error" in wrappedError) {
|
|
54
62
|
const { source, error } = wrappedError;
|
|
55
|
-
if (source ===
|
|
63
|
+
if (source === "poll" &&
|
|
64
|
+
error instanceof Error &&
|
|
65
|
+
error.message === ASSISTANT_POLL_TIMEOUT_ERROR) {
|
|
56
66
|
evaluation = await evaluationPromise;
|
|
57
67
|
}
|
|
58
|
-
else if (source ===
|
|
68
|
+
else if (source === "poll") {
|
|
59
69
|
throw error;
|
|
60
70
|
}
|
|
61
|
-
else if (source ===
|
|
62
|
-
const recovered = await recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex);
|
|
71
|
+
else if (source === "evaluation") {
|
|
72
|
+
const recovered = await recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId);
|
|
63
73
|
if (recovered) {
|
|
64
74
|
return recovered;
|
|
65
75
|
}
|
|
66
|
-
await logDomFailure(Runtime, logger,
|
|
67
|
-
throw error ?? new Error(
|
|
76
|
+
await logDomFailure(Runtime, logger, "assistant-response");
|
|
77
|
+
throw error ?? new Error("Failed to capture assistant response");
|
|
68
78
|
}
|
|
69
79
|
}
|
|
70
80
|
else {
|
|
@@ -72,14 +82,14 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
72
82
|
}
|
|
73
83
|
}
|
|
74
84
|
if (!evaluation) {
|
|
75
|
-
await logDomFailure(Runtime, logger,
|
|
76
|
-
throw new Error(
|
|
85
|
+
await logDomFailure(Runtime, logger, "assistant-response");
|
|
86
|
+
throw new Error("Failed to capture assistant response");
|
|
77
87
|
}
|
|
78
88
|
const parsed = await parseAssistantEvaluationResult(Runtime, evaluation, logger);
|
|
79
89
|
if (!parsed) {
|
|
80
90
|
let remainingMs = Math.max(0, timeoutMs - (Date.now() - start));
|
|
81
91
|
if (remainingMs > 0) {
|
|
82
|
-
const recovered = await recoverAssistantResponse(Runtime, remainingMs, logger, minTurnIndex);
|
|
92
|
+
const recovered = await recoverAssistantResponse(Runtime, remainingMs, logger, minTurnIndex, expectedConversationId);
|
|
83
93
|
if (recovered) {
|
|
84
94
|
return recovered;
|
|
85
95
|
}
|
|
@@ -89,16 +99,20 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
89
99
|
pollerPromise.catch(() => null),
|
|
90
100
|
delay(remainingMs).then(() => null),
|
|
91
101
|
]);
|
|
92
|
-
if (polled && polled.kind ===
|
|
102
|
+
if (polled && polled.kind === "poll" && polled.value) {
|
|
93
103
|
return polled.value;
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
|
-
await logDomFailure(Runtime, logger,
|
|
98
|
-
throw new Error(
|
|
107
|
+
await logDomFailure(Runtime, logger, "assistant-response");
|
|
108
|
+
throw new Error("Unable to capture assistant response");
|
|
99
109
|
}
|
|
100
|
-
const refreshed = await refreshAssistantSnapshot(Runtime, parsed, logger, minTurnIndex);
|
|
110
|
+
const refreshed = await refreshAssistantSnapshot(Runtime, parsed, logger, minTurnIndex, expectedConversationId);
|
|
101
111
|
const candidate = refreshed ?? parsed;
|
|
112
|
+
if (isGeneratedImageAssistantAnswer(candidate)) {
|
|
113
|
+
logger("Captured assistant generated image response");
|
|
114
|
+
return candidate;
|
|
115
|
+
}
|
|
102
116
|
// The evaluation path can race ahead of completion. If ChatGPT is still streaming, wait for the watchdog poller.
|
|
103
117
|
const elapsedMs = Date.now() - start;
|
|
104
118
|
const remainingMs = Math.max(0, timeoutMs - elapsedMs);
|
|
@@ -108,8 +122,8 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
108
122
|
isCompletionVisible(Runtime),
|
|
109
123
|
]);
|
|
110
124
|
if (stopVisible) {
|
|
111
|
-
logger(
|
|
112
|
-
const completed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex);
|
|
125
|
+
logger("Assistant still generating; waiting for completion");
|
|
126
|
+
const completed = await pollAssistantCompletion(Runtime, remainingMs, minTurnIndex, expectedConversationId);
|
|
113
127
|
if (completed) {
|
|
114
128
|
return completed;
|
|
115
129
|
}
|
|
@@ -120,16 +134,16 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
120
134
|
}
|
|
121
135
|
return candidate;
|
|
122
136
|
}
|
|
123
|
-
export async function readAssistantSnapshot(Runtime, minTurnIndex) {
|
|
137
|
+
export async function readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId) {
|
|
124
138
|
const { result } = await Runtime.evaluate({
|
|
125
|
-
expression: buildAssistantSnapshotExpression(minTurnIndex),
|
|
139
|
+
expression: buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId),
|
|
126
140
|
returnByValue: true,
|
|
127
141
|
});
|
|
128
142
|
const value = result?.value;
|
|
129
|
-
if (value && typeof value ===
|
|
143
|
+
if (value && typeof value === "object") {
|
|
130
144
|
const snapshot = value;
|
|
131
|
-
if (typeof minTurnIndex ===
|
|
132
|
-
const turnIndex = typeof snapshot.turnIndex ===
|
|
145
|
+
if (typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex)) {
|
|
146
|
+
const turnIndex = typeof snapshot.turnIndex === "number" ? snapshot.turnIndex : null;
|
|
133
147
|
if (turnIndex === null) {
|
|
134
148
|
return snapshot;
|
|
135
149
|
}
|
|
@@ -147,42 +161,45 @@ export async function captureAssistantMarkdown(Runtime, meta, logger) {
|
|
|
147
161
|
returnByValue: true,
|
|
148
162
|
awaitPromise: true,
|
|
149
163
|
});
|
|
150
|
-
if (result?.value?.success && typeof result.value.markdown ===
|
|
164
|
+
if (result?.value?.success && typeof result.value.markdown === "string") {
|
|
151
165
|
return result.value.markdown;
|
|
152
166
|
}
|
|
153
167
|
const status = result?.value?.status;
|
|
154
|
-
if (status && status !==
|
|
168
|
+
if (status && status !== "missing-button") {
|
|
155
169
|
logger(`Copy button fallback status: ${status}`);
|
|
156
|
-
await logDomFailure(Runtime, logger,
|
|
170
|
+
await logDomFailure(Runtime, logger, "copy-markdown");
|
|
157
171
|
}
|
|
158
172
|
if (!status) {
|
|
159
|
-
await logDomFailure(Runtime, logger,
|
|
173
|
+
await logDomFailure(Runtime, logger, "copy-markdown");
|
|
160
174
|
}
|
|
161
175
|
return null;
|
|
162
176
|
}
|
|
163
177
|
export function buildAssistantExtractorForTest(name) {
|
|
164
178
|
return buildAssistantExtractor(name);
|
|
165
179
|
}
|
|
180
|
+
export function buildAssistantSnapshotExpressionForTest(minTurnIndex, expectedConversationId) {
|
|
181
|
+
return buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId);
|
|
182
|
+
}
|
|
166
183
|
export function buildConversationDebugExpressionForTest() {
|
|
167
184
|
return buildConversationDebugExpression();
|
|
168
185
|
}
|
|
169
|
-
export function buildMarkdownFallbackExtractorForTest(minTurnLiteral =
|
|
186
|
+
export function buildMarkdownFallbackExtractorForTest(minTurnLiteral = "0") {
|
|
170
187
|
return buildMarkdownFallbackExtractor(minTurnLiteral);
|
|
171
188
|
}
|
|
172
189
|
export function buildCopyExpressionForTest(meta = {}) {
|
|
173
190
|
return buildCopyExpression(meta);
|
|
174
191
|
}
|
|
175
|
-
async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex) {
|
|
192
|
+
async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId) {
|
|
176
193
|
const recoveryTimeoutMs = Math.max(0, timeoutMs);
|
|
177
194
|
if (recoveryTimeoutMs === 0) {
|
|
178
195
|
return null;
|
|
179
196
|
}
|
|
180
197
|
const recovered = await waitForCondition(async () => {
|
|
181
|
-
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
|
|
198
|
+
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId);
|
|
182
199
|
return normalizeAssistantSnapshot(snapshot);
|
|
183
200
|
}, recoveryTimeoutMs, 400);
|
|
184
201
|
if (recovered) {
|
|
185
|
-
logger(
|
|
202
|
+
logger("Recovered assistant response via polling fallback");
|
|
186
203
|
return recovered;
|
|
187
204
|
}
|
|
188
205
|
await logConversationSnapshot(Runtime, logger).catch(() => undefined);
|
|
@@ -190,24 +207,27 @@ async function recoverAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex
|
|
|
190
207
|
}
|
|
191
208
|
async function parseAssistantEvaluationResult(_Runtime, evaluation, _logger) {
|
|
192
209
|
const { result } = evaluation;
|
|
193
|
-
if (result.type ===
|
|
194
|
-
|
|
210
|
+
if (result.type === "object" &&
|
|
211
|
+
result.value &&
|
|
212
|
+
typeof result.value === "object" &&
|
|
213
|
+
"text" in result.value) {
|
|
214
|
+
const html = typeof result.value.html === "string"
|
|
195
215
|
? (result.value.html ?? undefined)
|
|
196
216
|
: undefined;
|
|
197
|
-
const turnId = typeof result.value.turnId ===
|
|
217
|
+
const turnId = typeof result.value.turnId === "string"
|
|
198
218
|
? (result.value.turnId ?? undefined)
|
|
199
219
|
: undefined;
|
|
200
|
-
const messageId = typeof result.value.messageId ===
|
|
220
|
+
const messageId = typeof result.value.messageId === "string"
|
|
201
221
|
? (result.value.messageId ?? undefined)
|
|
202
222
|
: undefined;
|
|
203
|
-
const text = cleanAssistantText(String(result.value.text ??
|
|
223
|
+
const text = cleanAssistantText(String(result.value.text ?? ""));
|
|
204
224
|
const normalized = text.toLowerCase();
|
|
205
225
|
if (isAnswerNowPlaceholderText(normalized)) {
|
|
206
226
|
return null;
|
|
207
227
|
}
|
|
208
228
|
return { text, html, meta: { turnId, messageId } };
|
|
209
229
|
}
|
|
210
|
-
const fallbackText = typeof result.value ===
|
|
230
|
+
const fallbackText = typeof result.value === "string" ? cleanAssistantText(result.value) : "";
|
|
211
231
|
if (!fallbackText) {
|
|
212
232
|
return null;
|
|
213
233
|
}
|
|
@@ -216,14 +236,14 @@ async function parseAssistantEvaluationResult(_Runtime, evaluation, _logger) {
|
|
|
216
236
|
}
|
|
217
237
|
return { text: fallbackText, html: undefined, meta: {} };
|
|
218
238
|
}
|
|
219
|
-
async function refreshAssistantSnapshot(Runtime, current, logger, minTurnIndex) {
|
|
239
|
+
async function refreshAssistantSnapshot(Runtime, current, logger, minTurnIndex, expectedConversationId) {
|
|
220
240
|
const deadline = Date.now() + 5_000;
|
|
221
241
|
let best = null;
|
|
222
242
|
let stableCycles = 0;
|
|
223
243
|
const stableTarget = 3;
|
|
224
244
|
while (Date.now() < deadline) {
|
|
225
245
|
// Learned: short/fast answers can race; poll a few extra cycles to pick up messageId + full text.
|
|
226
|
-
const latestSnapshot = await readAssistantSnapshot(Runtime, minTurnIndex).catch(() => null);
|
|
246
|
+
const latestSnapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId).catch(() => null);
|
|
227
247
|
const latest = normalizeAssistantSnapshot(latestSnapshot);
|
|
228
248
|
if (latest) {
|
|
229
249
|
if (!best ||
|
|
@@ -250,13 +270,13 @@ async function refreshAssistantSnapshot(Runtime, current, logger, minTurnIndex)
|
|
|
250
270
|
const isLonger = latestLength > currentLength;
|
|
251
271
|
const hasDifferentText = best.text.trim() !== current.text.trim();
|
|
252
272
|
if (isLonger || hasBetterId || hasDifferentText) {
|
|
253
|
-
logger(
|
|
273
|
+
logger("Refreshed assistant response via latest snapshot");
|
|
254
274
|
return best;
|
|
255
275
|
}
|
|
256
276
|
return null;
|
|
257
277
|
}
|
|
258
278
|
async function terminateRuntimeExecution(Runtime) {
|
|
259
|
-
if (typeof Runtime.terminateExecution !==
|
|
279
|
+
if (typeof Runtime.terminateExecution !== "function") {
|
|
260
280
|
return;
|
|
261
281
|
}
|
|
262
282
|
try {
|
|
@@ -266,7 +286,7 @@ async function terminateRuntimeExecution(Runtime) {
|
|
|
266
286
|
// ignore termination failures
|
|
267
287
|
}
|
|
268
288
|
}
|
|
269
|
-
async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, abortSignal) {
|
|
289
|
+
async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expectedConversationId, abortSignal) {
|
|
270
290
|
const watchdogDeadline = Date.now() + timeoutMs;
|
|
271
291
|
let previousLength = 0;
|
|
272
292
|
let stableCycles = 0;
|
|
@@ -276,7 +296,7 @@ async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, abortSi
|
|
|
276
296
|
if (abortSignal?.aborted) {
|
|
277
297
|
return null;
|
|
278
298
|
}
|
|
279
|
-
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
|
|
299
|
+
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId);
|
|
280
300
|
const normalized = normalizeAssistantSnapshot(snapshot);
|
|
281
301
|
if (normalized) {
|
|
282
302
|
const currentLength = normalized.text.length;
|
|
@@ -292,6 +312,9 @@ async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, abortSi
|
|
|
292
312
|
isStopButtonVisible(Runtime),
|
|
293
313
|
isCompletionVisible(Runtime),
|
|
294
314
|
]);
|
|
315
|
+
if (isGeneratedImageAssistantAnswer(normalized)) {
|
|
316
|
+
return normalized;
|
|
317
|
+
}
|
|
295
318
|
const shortAnswer = currentLength > 0 && currentLength < 16;
|
|
296
319
|
const mediumAnswer = currentLength >= 16 && currentLength < 40;
|
|
297
320
|
const longAnswer = currentLength >= 40 && currentLength < 500;
|
|
@@ -377,7 +400,7 @@ async function isCompletionVisible(Runtime) {
|
|
|
377
400
|
}
|
|
378
401
|
}
|
|
379
402
|
function normalizeAssistantSnapshot(snapshot) {
|
|
380
|
-
const text = snapshot?.text ? cleanAssistantText(snapshot.text) :
|
|
403
|
+
const text = snapshot?.text ? cleanAssistantText(snapshot.text) : "";
|
|
381
404
|
if (!text.trim()) {
|
|
382
405
|
return null;
|
|
383
406
|
}
|
|
@@ -388,7 +411,7 @@ function normalizeAssistantSnapshot(snapshot) {
|
|
|
388
411
|
return null;
|
|
389
412
|
}
|
|
390
413
|
// Ignore user echo turns that can show up in project view fallbacks.
|
|
391
|
-
if (normalized.startsWith(
|
|
414
|
+
if (normalized.startsWith("you said")) {
|
|
392
415
|
return null;
|
|
393
416
|
}
|
|
394
417
|
return {
|
|
@@ -397,6 +420,9 @@ function normalizeAssistantSnapshot(snapshot) {
|
|
|
397
420
|
meta: { turnId: snapshot?.turnId ?? undefined, messageId: snapshot?.messageId ?? undefined },
|
|
398
421
|
};
|
|
399
422
|
}
|
|
423
|
+
function isGeneratedImageAssistantAnswer(answer) {
|
|
424
|
+
return Boolean(answer?.html?.includes("/backend-api/estuary/content?id=file_"));
|
|
425
|
+
}
|
|
400
426
|
async function waitForCondition(getter, timeoutMs, pollIntervalMs = 400) {
|
|
401
427
|
const deadline = Date.now() + timeoutMs;
|
|
402
428
|
while (Date.now() < deadline) {
|
|
@@ -408,14 +434,27 @@ async function waitForCondition(getter, timeoutMs, pollIntervalMs = 400) {
|
|
|
408
434
|
}
|
|
409
435
|
return null;
|
|
410
436
|
}
|
|
411
|
-
function buildAssistantSnapshotExpression(minTurnIndex) {
|
|
412
|
-
const minTurnLiteral = typeof minTurnIndex ===
|
|
437
|
+
function buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId) {
|
|
438
|
+
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
413
439
|
? Math.floor(minTurnIndex)
|
|
414
440
|
: -1;
|
|
441
|
+
const expectedConversationLiteral = typeof expectedConversationId === "string" && expectedConversationId.trim().length > 0
|
|
442
|
+
? JSON.stringify(expectedConversationId.trim())
|
|
443
|
+
: "null";
|
|
415
444
|
return `(() => {
|
|
416
445
|
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
446
|
+
const EXPECTED_CONVERSATION_ID = ${expectedConversationLiteral};
|
|
447
|
+
const currentHref = typeof location === 'object' && location.href ? location.href : '';
|
|
448
|
+
const currentConversationId = currentHref.match(/\\/c\\/([a-zA-Z0-9-]+)/)?.[1] ?? null;
|
|
449
|
+
if (
|
|
450
|
+
EXPECTED_CONVERSATION_ID &&
|
|
451
|
+
currentConversationId &&
|
|
452
|
+
currentConversationId !== EXPECTED_CONVERSATION_ID
|
|
453
|
+
) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
417
456
|
// Learned: the default turn DOM misses project view; keep a fallback extractor.
|
|
418
|
-
${buildAssistantExtractor(
|
|
457
|
+
${buildAssistantExtractor("extractAssistantTurn")}
|
|
419
458
|
const extracted = extractAssistantTurn();
|
|
420
459
|
const isPlaceholder = (snapshot) => {
|
|
421
460
|
const normalized = String(snapshot?.text ?? '').toLowerCase().trim();
|
|
@@ -429,17 +468,20 @@ function buildAssistantSnapshotExpression(minTurnIndex) {
|
|
|
429
468
|
return extracted;
|
|
430
469
|
}
|
|
431
470
|
// Fallback for ChatGPT project view: answers can live outside conversation turns.
|
|
432
|
-
const fallback = ${buildMarkdownFallbackExtractor(
|
|
471
|
+
const fallback = ${buildMarkdownFallbackExtractor("MIN_TURN_INDEX")};
|
|
433
472
|
return fallback ?? extracted;
|
|
434
473
|
})()`;
|
|
435
474
|
}
|
|
436
|
-
function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
|
|
475
|
+
function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConversationId) {
|
|
437
476
|
const selectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
|
|
438
477
|
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
439
478
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
440
|
-
const minTurnLiteral = typeof minTurnIndex ===
|
|
479
|
+
const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
|
|
441
480
|
? Math.floor(minTurnIndex)
|
|
442
481
|
: -1;
|
|
482
|
+
const expectedConversationLiteral = typeof expectedConversationId === "string" && expectedConversationId.trim().length > 0
|
|
483
|
+
? JSON.stringify(expectedConversationId.trim())
|
|
484
|
+
: "null";
|
|
443
485
|
return `(() => {
|
|
444
486
|
${buildClickDispatcher()}
|
|
445
487
|
const SELECTORS = ${selectorsLiteral};
|
|
@@ -447,8 +489,18 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
|
|
|
447
489
|
const FINISHED_SELECTOR = '${FINISHED_ACTIONS_SELECTOR}';
|
|
448
490
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
449
491
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
492
|
+
const EXPECTED_CONVERSATION_ID = ${expectedConversationLiteral};
|
|
450
493
|
// Learned: settling avoids capturing mid-stream HTML; keep short.
|
|
451
494
|
const settleDelayMs = 800;
|
|
495
|
+
const currentConversationId = () => {
|
|
496
|
+
const href = typeof location === 'object' && location.href ? location.href : '';
|
|
497
|
+
return href.match(/\\/c\\/([a-zA-Z0-9-]+)/)?.[1] ?? null;
|
|
498
|
+
};
|
|
499
|
+
const matchesExpectedConversation = () => {
|
|
500
|
+
if (!EXPECTED_CONVERSATION_ID) return true;
|
|
501
|
+
const currentId = currentConversationId();
|
|
502
|
+
return !currentId || currentId === EXPECTED_CONVERSATION_ID;
|
|
503
|
+
};
|
|
452
504
|
const isAnswerNowPlaceholder = (snapshot) => {
|
|
453
505
|
const normalized = String(snapshot?.text ?? '').toLowerCase().trim();
|
|
454
506
|
if (normalized === 'chatgpt said:' || normalized === 'chatgpt said') return true;
|
|
@@ -471,12 +523,13 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
|
|
|
471
523
|
};
|
|
472
524
|
|
|
473
525
|
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
474
|
-
${buildAssistantExtractor(
|
|
526
|
+
${buildAssistantExtractor("extractFromTurns")}
|
|
475
527
|
// Learned: some layouts (project view) render markdown without assistant turn wrappers.
|
|
476
|
-
const extractFromMarkdownFallback = ${buildMarkdownFallbackExtractor(
|
|
528
|
+
const extractFromMarkdownFallback = ${buildMarkdownFallbackExtractor("MIN_TURN_INDEX")};
|
|
477
529
|
|
|
478
530
|
const acceptSnapshot = (snapshot) => {
|
|
479
531
|
if (!snapshot) return null;
|
|
532
|
+
if (!matchesExpectedConversation()) return null;
|
|
480
533
|
const index = typeof snapshot.turnIndex === 'number' ? snapshot.turnIndex : -1;
|
|
481
534
|
if (MIN_TURN_INDEX >= 0) {
|
|
482
535
|
if (index < 0 || index < MIN_TURN_INDEX) {
|
|
@@ -584,6 +637,9 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex) {
|
|
|
584
637
|
};
|
|
585
638
|
|
|
586
639
|
const waitForSettle = async (snapshot) => {
|
|
640
|
+
if (String(snapshot?.html ?? '').includes('/backend-api/estuary/content?id=file_')) {
|
|
641
|
+
return snapshot;
|
|
642
|
+
}
|
|
587
643
|
// Learned: short answers can be 1-2 tokens; enforce longer settle windows to avoid truncation.
|
|
588
644
|
// Learned: long streaming responses (esp. thinking models) can pause mid-stream;
|
|
589
645
|
// use progressively longer windows to avoid truncation (#71).
|
|
@@ -711,6 +767,19 @@ function buildAssistantExtractor(functionName) {
|
|
|
711
767
|
const html = contentRoot?.innerHTML ?? '';
|
|
712
768
|
const messageId = messageRoot.getAttribute('data-message-id');
|
|
713
769
|
const turnId = messageRoot.getAttribute('data-testid');
|
|
770
|
+
const generatedImages = Array.from(messageRoot.querySelectorAll('img')).filter((img) =>
|
|
771
|
+
String(img?.src || '').includes('/backend-api/estuary/content?id=file_')
|
|
772
|
+
);
|
|
773
|
+
const normalizedText = String(text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
774
|
+
const imageOnlyChrome =
|
|
775
|
+
!normalizedText ||
|
|
776
|
+
normalizedText === 'edit' ||
|
|
777
|
+
normalizedText === 'stopped thinking' ||
|
|
778
|
+
normalizedText === 'stopped thinking edit';
|
|
779
|
+
if (generatedImages.length > 0 && imageOnlyChrome) {
|
|
780
|
+
const label = generatedImages.length === 1 ? 'Generated image.' : \`Generated \${generatedImages.length} images.\`;
|
|
781
|
+
return { text: label, html: messageRoot?.innerHTML ?? html, messageId, turnId, turnIndex: index };
|
|
782
|
+
}
|
|
714
783
|
if (text.trim()) {
|
|
715
784
|
return { text, html, messageId, turnId, turnIndex: index };
|
|
716
785
|
}
|
|
@@ -719,7 +788,9 @@ function buildAssistantExtractor(functionName) {
|
|
|
719
788
|
};`;
|
|
720
789
|
}
|
|
721
790
|
function buildMarkdownFallbackExtractor(minTurnLiteral) {
|
|
722
|
-
const turnIndexValue = minTurnLiteral
|
|
791
|
+
const turnIndexValue = minTurnLiteral
|
|
792
|
+
? `(${minTurnLiteral} >= 0 ? ${minTurnLiteral} : null)`
|
|
793
|
+
: "null";
|
|
723
794
|
return `(() => {
|
|
724
795
|
const __minTurn = ${turnIndexValue};
|
|
725
796
|
const roots = [
|
|
@@ -1025,37 +1096,37 @@ function buildCopyExpression(meta) {
|
|
|
1025
1096
|
})()`;
|
|
1026
1097
|
}
|
|
1027
1098
|
const LANGUAGE_TAGS = new Set([
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1099
|
+
"copy code",
|
|
1100
|
+
"markdown",
|
|
1101
|
+
"bash",
|
|
1102
|
+
"sh",
|
|
1103
|
+
"shell",
|
|
1104
|
+
"javascript",
|
|
1105
|
+
"typescript",
|
|
1106
|
+
"ts",
|
|
1107
|
+
"js",
|
|
1108
|
+
"yaml",
|
|
1109
|
+
"json",
|
|
1110
|
+
"python",
|
|
1111
|
+
"py",
|
|
1112
|
+
"go",
|
|
1113
|
+
"java",
|
|
1114
|
+
"c",
|
|
1115
|
+
"c++",
|
|
1116
|
+
"cpp",
|
|
1117
|
+
"c#",
|
|
1118
|
+
"php",
|
|
1119
|
+
"ruby",
|
|
1120
|
+
"rust",
|
|
1121
|
+
"swift",
|
|
1122
|
+
"kotlin",
|
|
1123
|
+
"html",
|
|
1124
|
+
"css",
|
|
1125
|
+
"sql",
|
|
1126
|
+
"text",
|
|
1056
1127
|
].map((token) => token.toLowerCase()));
|
|
1057
1128
|
function cleanAssistantText(text) {
|
|
1058
|
-
const normalized = text.replace(/\u00a0/g,
|
|
1129
|
+
const normalized = text.replace(/\u00a0/g, " ");
|
|
1059
1130
|
const lines = normalized.split(/\r?\n/);
|
|
1060
1131
|
const filtered = lines.filter((line) => {
|
|
1061
1132
|
const trimmed = line.trim().toLowerCase();
|
|
@@ -1063,5 +1134,8 @@ function cleanAssistantText(text) {
|
|
|
1063
1134
|
return false;
|
|
1064
1135
|
return true;
|
|
1065
1136
|
});
|
|
1066
|
-
return filtered
|
|
1137
|
+
return filtered
|
|
1138
|
+
.join("\n")
|
|
1139
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1140
|
+
.trim();
|
|
1067
1141
|
}
|