@steipete/oracle 0.18.0 → 0.20.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/README.md +1 -1
- package/dist/bin/oracle-cli.js +76 -76
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/actions/archiveConversation.js +31 -7
- package/dist/src/browser/actions/assistantResponse.js +43 -1
- package/dist/src/browser/actions/attachmentContext.js +239 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
- package/dist/src/browser/actions/attachmentEvidence.js +78 -0
- package/dist/src/browser/actions/attachmentProgress.js +41 -0
- package/dist/src/browser/actions/attachmentPrompt.js +130 -0
- package/dist/src/browser/actions/attachments.js +306 -355
- package/dist/src/browser/actions/deepResearch.js +170 -45
- package/dist/src/browser/actions/modelSelection.js +91 -4
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +364 -186
- package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
- package/dist/src/browser/actions/thinkingStatus.js +2 -5
- package/dist/src/browser/actions/thinkingTime.js +156 -16
- package/dist/src/browser/actions/webSearch.js +96 -0
- package/dist/src/browser/attachRunning.js +46 -2
- package/dist/src/browser/attachmentValidation.js +53 -0
- package/dist/src/browser/cancellation.js +105 -0
- package/dist/src/browser/chatgptImages.js +2 -0
- package/dist/src/browser/chatgptThrottle.js +104 -0
- package/dist/src/browser/chromeLifecycle.js +272 -47
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +525 -274
- package/dist/src/browser/liveTabs.js +3 -4
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +83 -15
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
- package/dist/src/browser/reattach.js +31 -4
- package/dist/src/browser/recoveryTarget.js +131 -0
- package/dist/src/browser/sessionRunner.js +73 -11
- package/dist/src/browser/tabLeaseRegistry.js +434 -38
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +36 -4
- package/dist/src/cli/browserDefaults.js +10 -0
- package/dist/src/cli/browserTabs.js +15 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -48
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/options.js +15 -0
- package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
- package/dist/src/cli/runOptions.js +19 -4
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +13 -7
- package/dist/src/cli/sessionRunner.js +95 -12
- package/dist/src/config.js +1 -0
- package/dist/src/gemini-web/client.js +67 -21
- package/dist/src/gemini-web/executor.js +4 -0
- package/dist/src/gemini-web/http.js +15 -0
- package/dist/src/gemini-web/models.js +15 -11
- package/dist/src/mcp/server.js +11 -14
- package/dist/src/mcp/tools/chatgptImage.js +4 -4
- package/dist/src/mcp/tools/consult.js +86 -24
- package/dist/src/mcp/tools/projectSources.js +4 -6
- package/dist/src/mcp/tools/sessionResources.js +1 -1
- package/dist/src/mcp/tools/sessions.js +2 -2
- package/dist/src/mcp/tools/wait.js +182 -0
- package/dist/src/mcp/types.js +8 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +73 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +346 -71
- package/dist/src/sessionManager.js +36 -1
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +25 -23
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -683
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -400
- package/dist/docs-site/browser-mode.html +0 -588
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -436
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -368
- package/dist/docs-site/gemini.html +0 -379
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -595
- package/dist/docs-site/mcp.html +0 -368
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -387
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/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/types.js +0 -1
- 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/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/profileSync.js +0 -141
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -4,6 +4,7 @@ import { waitForAttachmentVisible } from "./attachments.js";
|
|
|
4
4
|
import { delay } from "../utils.js";
|
|
5
5
|
import { logDomFailure } from "../domDebug.js";
|
|
6
6
|
import { transferAttachmentViaDataTransfer } from "./attachmentDataTransfer.js";
|
|
7
|
+
import { beginAttachmentEvidence } from "./attachmentEvidence.js";
|
|
7
8
|
/**
|
|
8
9
|
* Upload file to remote Chrome by transferring content via CDP
|
|
9
10
|
* Used when browser is on a different machine than CLI
|
|
@@ -28,10 +29,11 @@ export async function uploadAttachmentViaDataTransfer(deps, attachment, logger)
|
|
|
28
29
|
await logDomFailure(runtime, logger, "file-input");
|
|
29
30
|
throw new Error("Unable to locate ChatGPT file attachment input.");
|
|
30
31
|
}
|
|
31
|
-
const
|
|
32
|
+
const evidenceId = await beginAttachmentEvidence(runtime, path.basename(attachment.path));
|
|
33
|
+
const transferResult = await transferAttachmentViaDataTransfer(runtime, attachment, fileInputSelector, deps.navigationUrl);
|
|
32
34
|
logger(`File transferred: ${transferResult.fileName} (${transferResult.size} bytes)`);
|
|
33
35
|
// Give ChatGPT a moment to process the file
|
|
34
36
|
await delay(500);
|
|
35
|
-
await waitForAttachmentVisible(runtime, transferResult.fileName, 10_000, logger);
|
|
37
|
+
await waitForAttachmentVisible(runtime, transferResult.fileName, 10_000, logger, evidenceId);
|
|
36
38
|
logger("Attachment queued");
|
|
37
39
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatElapsed } from "../../oracle/format.js";
|
|
2
2
|
import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, STOP_BUTTON_SELECTORS, } from "../constants.js";
|
|
3
|
+
import { buildConversationTurnListExpression } from "../conversationTurns.js";
|
|
3
4
|
const THINKING_STALE_HINT_MS = 10 * 60_000;
|
|
4
5
|
export function startThinkingStatusMonitor(Runtime, logger, options = {}) {
|
|
5
6
|
const intervalMs = resolveThinkingStatusInterval(options.intervalMs);
|
|
@@ -438,7 +439,6 @@ const ACTIVE_THINKING_LABELS = [
|
|
|
438
439
|
function buildThinkingActivityPredicateJs(fnName, detailed) {
|
|
439
440
|
const stopLiteral = JSON.stringify(STOP_BUTTON_SELECTORS.join(", "));
|
|
440
441
|
const activeLabelsLiteral = JSON.stringify(ACTIVE_THINKING_LABELS);
|
|
441
|
-
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
442
442
|
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
443
443
|
const strong = detailed ? "{ active: true, strong: true }" : "true";
|
|
444
444
|
const weak = detailed ? "{ active: true, strong: false }" : "true";
|
|
@@ -446,7 +446,6 @@ function buildThinkingActivityPredicateJs(fnName, detailed) {
|
|
|
446
446
|
return `const ${fnName} = () => {
|
|
447
447
|
const STOP_SELECTOR = ${stopLiteral};
|
|
448
448
|
const ACTIVE_LABELS = ${activeLabelsLiteral};
|
|
449
|
-
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
450
449
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
451
450
|
const isVisible = (node) => {
|
|
452
451
|
if (!(node instanceof HTMLElement)) return false;
|
|
@@ -549,9 +548,7 @@ function buildThinkingActivityPredicateJs(fnName, detailed) {
|
|
|
549
548
|
// a visible progress bar mounted indefinitely (review P1), and a document-wide veto would
|
|
550
549
|
// then hold thinkingActive true until the watchdog timeout on a completed response. The
|
|
551
550
|
// sidecar check below covers verified reasoning panels; here only the latest turn counts.
|
|
552
|
-
const turns = (
|
|
553
|
-
try { return document.querySelectorAll(CONVERSATION_SELECTOR); } catch { return []; }
|
|
554
|
-
})();
|
|
551
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
555
552
|
const lastTurn = turns.length ? turns[turns.length - 1] : null;
|
|
556
553
|
if (
|
|
557
554
|
lastTurn instanceof HTMLElement &&
|
|
@@ -52,8 +52,8 @@ function logPickerDiagnostic(result, logger) {
|
|
|
52
52
|
/**
|
|
53
53
|
* Selects a thinking-time level in ChatGPT's composer.
|
|
54
54
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
55
|
+
* Returns the UI selection observed at capturedAt, not backend attestation.
|
|
56
|
+
* Explicit Pro and Pro Extended requests still fail closed when unconfirmed.
|
|
57
57
|
*/
|
|
58
58
|
export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
59
59
|
const result = await evaluateThinkingTimeSelection(Runtime, level, desiredModel);
|
|
@@ -65,13 +65,24 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
65
65
|
// own, independently of the legacy Pro-model + "extended" combination.
|
|
66
66
|
const strictProEffort = level === "pro" ||
|
|
67
67
|
((targetModelKind === "pro" || observedModelKind === "pro") && level === "extended");
|
|
68
|
+
const evidence = (status, resolvedLabel) => ({
|
|
69
|
+
requestedLevel: level,
|
|
70
|
+
status,
|
|
71
|
+
resolvedLabel,
|
|
72
|
+
verified: status === "already-selected" || status === "switched",
|
|
73
|
+
strictFailClosed: strictProEffort,
|
|
74
|
+
targetModelKind: targetModelKind ?? null,
|
|
75
|
+
observedModelKind: observedModelKind ?? null,
|
|
76
|
+
source: "chatgpt-thinking-picker",
|
|
77
|
+
capturedAt: new Date().toISOString(),
|
|
78
|
+
});
|
|
68
79
|
switch (result?.status) {
|
|
69
80
|
case "already-selected":
|
|
70
81
|
logger(formatBrowserThinkingLog(`${result.label ?? capitalizedLevel} (already selected)`));
|
|
71
|
-
return;
|
|
82
|
+
return evidence("already-selected", result.label ?? null);
|
|
72
83
|
case "switched":
|
|
73
84
|
logger(formatBrowserThinkingLog(result.label ?? capitalizedLevel));
|
|
74
|
-
return;
|
|
85
|
+
return evidence("switched", result.label ?? null);
|
|
75
86
|
case "option-disabled": {
|
|
76
87
|
await logDomFailure(Runtime, logger, "thinking-option-disabled");
|
|
77
88
|
logPickerDiagnostic(result, logger);
|
|
@@ -82,7 +93,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
82
93
|
// strict error's "refusing to submit" wording: the request really is sent,
|
|
83
94
|
// at whatever effort ChatGPT already had selected.
|
|
84
95
|
logger(formatBrowserThinkingLog(`${result.label ?? capitalizedLevel} is unavailable on this account (${result.notice ?? "no reason given"}); keeping the effort already selected in ChatGPT.`));
|
|
85
|
-
return;
|
|
96
|
+
return evidence("unverified", null);
|
|
86
97
|
}
|
|
87
98
|
case "chip-not-found":
|
|
88
99
|
case "menu-not-found":
|
|
@@ -108,7 +119,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
108
119
|
? "the effort in ChatGPT is unconfirmed"
|
|
109
120
|
: "keeping the effort already selected in ChatGPT";
|
|
110
121
|
logger(formatBrowserThinkingLog(`${message}; ${outcome}.`));
|
|
111
|
-
return;
|
|
122
|
+
return evidence("unverified", null);
|
|
112
123
|
}
|
|
113
124
|
default: {
|
|
114
125
|
await logDomFailure(Runtime, logger, "thinking-time-unknown");
|
|
@@ -118,7 +129,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
118
129
|
throw new Error(`Thinking time: unknown outcome selecting ${capitalizedLevel}; refusing to submit without confirmed ${target}.`);
|
|
119
130
|
}
|
|
120
131
|
logger(formatBrowserThinkingLog(`unknown outcome selecting ${capitalizedLevel}; continuing with ChatGPT default.`));
|
|
121
|
-
return;
|
|
132
|
+
return evidence("unverified", null);
|
|
122
133
|
}
|
|
123
134
|
}
|
|
124
135
|
}
|
|
@@ -181,6 +192,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
181
192
|
const targetLevelLiteral = JSON.stringify(level.toLowerCase());
|
|
182
193
|
const targetModelKindLiteral = JSON.stringify(inferThinkingTargetModelKind(desiredModel));
|
|
183
194
|
const targetIsGpt56ModelLiteral = JSON.stringify(/(?:^|[^0-9])5[._ -]6(?:[^0-9]|$)/i.test(desiredModel ?? ""));
|
|
195
|
+
// Astra resolves the gpt-6-pro request to this exact model-picker alias. Keep
|
|
196
|
+
// this deliberately narrow: a generic unknown model must not claim its
|
|
197
|
+
// version-prefixed effort pill whose ownership we cannot establish.
|
|
198
|
+
const targetIsAstraLatestLiteral = JSON.stringify((desiredModel ?? "").trim().toLowerCase() === "latest");
|
|
184
199
|
return `(async () => {
|
|
185
200
|
${buildClickDispatcher()}
|
|
186
201
|
|
|
@@ -190,13 +205,14 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
190
205
|
const TARGET_LEVEL = ${targetLevelLiteral};
|
|
191
206
|
const TARGET_MODEL_KIND = ${targetModelKindLiteral};
|
|
192
207
|
const TARGET_IS_GPT56_MODEL = ${targetIsGpt56ModelLiteral};
|
|
208
|
+
const TARGET_IS_ASTRA_LATEST = ${targetIsAstraLatestLiteral};
|
|
193
209
|
|
|
194
|
-
// Multilingual matchers: English level token + observed
|
|
210
|
+
// Multilingual matchers: English level token + observed localized variants.
|
|
195
211
|
const LEVEL_TOKENS = {
|
|
196
|
-
light: ['light', 'instant', 'sofort', 'leicht', '最速', '轻', '极速'],
|
|
197
|
-
standard: ['standard', 'medium', 'mittel', '中程度', '标准', '中'],
|
|
198
|
-
extended: ['extended', 'high', 'hoch', 'erweitert', '高い', '扩展', '深度', '加强', '高'],
|
|
199
|
-
'extra-high': ['extra high', 'sehr hoch', '非常に高い', '极高'],
|
|
212
|
+
light: ['light', 'instant', 'sofort', 'leicht', '最速', '轻', '极速', '즉시'],
|
|
213
|
+
standard: ['standard', 'medium', 'mittel', '中程度', '标准', '中', '중간'],
|
|
214
|
+
extended: ['extended', 'high', 'hoch', 'erweitert', '高い', '扩展', '深度', '加强', '高', '높음'],
|
|
215
|
+
'extra-high': ['extra high', 'sehr hoch', '非常に高い', '極高', '极高', '매우 높음'],
|
|
200
216
|
heavy: ['heavy', 'schwer', '重度', '加重'],
|
|
201
217
|
};
|
|
202
218
|
// Pro is a tier you can request, but it is also a MODEL name, so it must never
|
|
@@ -205,6 +221,29 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
205
221
|
// "Instant"/"Pro" would look like a tier list. Keep it to target matching only.
|
|
206
222
|
const TARGET_LEVEL_TOKENS = { ...LEVEL_TOKENS, pro: ['pro'] };
|
|
207
223
|
const targetTokens = TARGET_LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
|
|
224
|
+
const normalizeAstraLabel = (value) =>
|
|
225
|
+
String(value ?? '')
|
|
226
|
+
.normalize('NFC')
|
|
227
|
+
.toLowerCase()
|
|
228
|
+
.split(String.fromCharCode(9)).join(' ')
|
|
229
|
+
.split(String.fromCharCode(10)).join(' ')
|
|
230
|
+
.split(String.fromCharCode(13)).join(' ')
|
|
231
|
+
.split(String.fromCharCode(12)).join(' ')
|
|
232
|
+
.split(' ').filter(Boolean).join(' ');
|
|
233
|
+
const isAstraLatestEffortPill = (label) =>
|
|
234
|
+
TARGET_IS_ASTRA_LATEST &&
|
|
235
|
+
Object.values(TARGET_LEVEL_TOKENS).some((tokens) =>
|
|
236
|
+
tokens.some((token) => {
|
|
237
|
+
// Do not use the generic matcher here: ownership needs an exact
|
|
238
|
+
// version prefix plus a known localized tier, not token containment.
|
|
239
|
+
const tier = normalizeAstraLabel(token);
|
|
240
|
+
const observed = normalizeAstraLabel(label);
|
|
241
|
+
return observed === '6 ' + tier || observed === '6' + tier;
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
const isSolModelPillForLatest = (label) =>
|
|
245
|
+
TARGET_IS_ASTRA_LATEST &&
|
|
246
|
+
['5.6 pro', '5.6pro', '5 6 pro'].includes(normalizeAstraLabel(label));
|
|
208
247
|
|
|
209
248
|
const INITIAL_WAIT_MS = 150;
|
|
210
249
|
const STEP_WAIT_MS = 200;
|
|
@@ -215,7 +254,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
215
254
|
const INTELLIGENCE_WAIT_MS = 2500;
|
|
216
255
|
|
|
217
256
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
218
|
-
// Keep CJK characters, including Japanese kana, so
|
|
257
|
+
// Keep CJK characters, including Japanese kana and Hangul, so labels survive
|
|
219
258
|
// normalization before being matched against LEVEL_TOKENS and picker controls.
|
|
220
259
|
const normalize = (value) => (value || '')
|
|
221
260
|
// Compose first so NFD umlauts fold too, then map them onto ASCII before
|
|
@@ -226,7 +265,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
226
265
|
.replace(/ö/g, 'o')
|
|
227
266
|
.replace(/ü/g, 'u')
|
|
228
267
|
.replace(/ß/g, 'ss')
|
|
229
|
-
.replace(/[^a-z0-9\\u3040-\\u30ff\\u4e00-\\u9fff]+/g, ' ')
|
|
268
|
+
.replace(/[^a-z0-9\\u3040-\\u30ff\\u4e00-\\u9fff\\uac00-\\ud7af]+/g, ' ')
|
|
230
269
|
.replace(/\\s+/g, ' ')
|
|
231
270
|
.trim();
|
|
232
271
|
const hasToken = (text, token) => normalize(text).split(' ').includes(token);
|
|
@@ -255,6 +294,8 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
255
294
|
if (token === 'extra high') return hasPhrase(t, 'extra high');
|
|
256
295
|
if (token === 'hoch') return hasPhrase(t, 'hoch') && !hasPhrase(t, 'sehr hoch');
|
|
257
296
|
if (token === 'sehr hoch') return hasPhrase(t, 'sehr hoch');
|
|
297
|
+
if (token === '높음') return hasPhrase(t, '높음') && !hasPhrase(t, '매우 높음');
|
|
298
|
+
if (token === '매우 높음') return hasPhrase(t, '매우 높음');
|
|
258
299
|
if (token === '高い' || token === '非常に高い') {
|
|
259
300
|
return t === token || hasToken(t, token);
|
|
260
301
|
}
|
|
@@ -842,11 +883,11 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
842
883
|
// token matching cannot be used here — substring is deliberate, as in
|
|
843
884
|
// countEffortLevels above.
|
|
844
885
|
const ADVANCED_WORDS = [
|
|
845
|
-
'advanced', 'erweitert', '高级', '詳細設定', '詳細表示',
|
|
886
|
+
'advanced', 'erweitert', '高级', '詳細設定', '詳細表示', '고급',
|
|
846
887
|
'avanzado', 'avancado', 'avance',
|
|
847
888
|
];
|
|
848
889
|
const EFFORT_WORDS = [
|
|
849
|
-
'effort', 'aufwand', '强度', '努力', '推論レベル',
|
|
890
|
+
'effort', 'aufwand', '强度', '努力', '推論レベル', '思考量', '추론 수준',
|
|
850
891
|
'esfuerzo', 'esforco', 'sforzo', 'inspanning', 'wysilek',
|
|
851
892
|
];
|
|
852
893
|
const containsAny = (label, words) => words.some((word) => label.includes(word));
|
|
@@ -945,6 +986,94 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
945
986
|
return null;
|
|
946
987
|
};
|
|
947
988
|
|
|
989
|
+
// The direct-slider rollout removes the Effort submenu entirely. Its keyboard
|
|
990
|
+
// owner announces the actual tier via aria-describedby; neither the pill nor
|
|
991
|
+
// the slider's maximum position alone proves that Pro was selected.
|
|
992
|
+
const selectDirectEffortSlider = async (menu) => {
|
|
993
|
+
const view = menu.querySelector?.('[data-model-selection-view="true"]');
|
|
994
|
+
const simple = view?.querySelector?.('[data-testid="composer-model-picker-slider-simple-view"]');
|
|
995
|
+
if (!simple || simple.getAttribute('data-active') !== 'true' || !isVisible(simple)) return null;
|
|
996
|
+
const resolve = () => {
|
|
997
|
+
const currentView = menu.querySelector?.('[data-model-selection-view="true"]');
|
|
998
|
+
const currentSimple = currentView?.querySelector?.('[data-testid="composer-model-picker-slider-simple-view"]');
|
|
999
|
+
if (currentSimple?.getAttribute('data-active') !== 'true') return null;
|
|
1000
|
+
const slider = currentSimple.querySelector('[data-model-reasoning-effort-slider]');
|
|
1001
|
+
const control = slider?.closest?.('[role="menuitem"]');
|
|
1002
|
+
const thumb = slider?.querySelector?.('[role="slider"]');
|
|
1003
|
+
if (!control || !thumb || !isVisible(control)) return null;
|
|
1004
|
+
const levels = ['light', 'standard', 'extended', 'extra-high', 'pro'];
|
|
1005
|
+
// The selected label leads localized aria-describedby prose, while punctuation
|
|
1006
|
+
// and ordinal grammar vary by locale. Match only that leading label and let
|
|
1007
|
+
// the thumb's numeric ARIA state independently prove its position.
|
|
1008
|
+
const readLeadingLevel = (description) => {
|
|
1009
|
+
const value = (description ?? '').normalize('NFC').trim();
|
|
1010
|
+
if (!value) return null;
|
|
1011
|
+
const matches = levels.flatMap((level, index) => {
|
|
1012
|
+
const token = [...TARGET_LEVEL_TOKENS[level]]
|
|
1013
|
+
.sort((left, right) => right.length - left.length)
|
|
1014
|
+
.find((candidate) => {
|
|
1015
|
+
const normalizedCandidate = candidate.normalize('NFC');
|
|
1016
|
+
const prefix = value.slice(0, normalizedCandidate.length);
|
|
1017
|
+
if (normalize(prefix) !== normalize(normalizedCandidate)) return false;
|
|
1018
|
+
// normalize() erases Unicode letters and marks; inspect the boundary intact.
|
|
1019
|
+
const suffix = value.slice(prefix.length);
|
|
1020
|
+
return !suffix || /^[\\s\\p{P}]/u.test(suffix);
|
|
1021
|
+
});
|
|
1022
|
+
return token
|
|
1023
|
+
? [{ level, index, label: value.slice(0, token.normalize('NFC').length) }]
|
|
1024
|
+
: [];
|
|
1025
|
+
});
|
|
1026
|
+
return matches.length === 1 ? matches[0] : null;
|
|
1027
|
+
};
|
|
1028
|
+
const selections = describedIds(control)
|
|
1029
|
+
.map((id) => readLeadingLevel(document.getElementById?.(id)?.textContent ?? ''))
|
|
1030
|
+
.filter(Boolean);
|
|
1031
|
+
if (selections.length !== 1) return null;
|
|
1032
|
+
const { label, index, level } = selections[0];
|
|
1033
|
+
// This adapter owns the observed five-tier layout only. A different range
|
|
1034
|
+
// or contradictory announcement must not turn a numeric guess into proof.
|
|
1035
|
+
if (thumb.getAttribute('aria-valuemin') !== '0' || thumb.getAttribute('aria-valuemax') !== '4' ||
|
|
1036
|
+
thumb.getAttribute('aria-valuenow') !== String(index)) return null;
|
|
1037
|
+
return { control, label, index, level };
|
|
1038
|
+
};
|
|
1039
|
+
let current = resolve();
|
|
1040
|
+
const finish = (result) => { closeOpenMenus(); return result; };
|
|
1041
|
+
// The picker can expose its simple view before the keyboard owner is mounted or visible.
|
|
1042
|
+
const readyDeadline = performance.now() + MAX_WAIT_MS;
|
|
1043
|
+
while (!current && performance.now() < readyDeadline) {
|
|
1044
|
+
await sleep(100);
|
|
1045
|
+
current = resolve();
|
|
1046
|
+
}
|
|
1047
|
+
if (!current) return finish(failure('selection-unverified'));
|
|
1048
|
+
// Preserve the legacy Pro-model + extended contract on unified pickers.
|
|
1049
|
+
const target = TARGET_MODEL_KIND === 'pro' && TARGET_LEVEL === 'extended' ? 'pro' : TARGET_LEVEL;
|
|
1050
|
+
const targetIndex = ['light', 'standard', 'extended', 'extra-high', 'pro'].indexOf(target);
|
|
1051
|
+
if (targetIndex < 0) {
|
|
1052
|
+
if (TARGET_IS_GPT56_MODEL && TARGET_LEVEL === 'heavy' && current.level === 'pro') {
|
|
1053
|
+
return finish({ status: 'already-selected', label: current.label });
|
|
1054
|
+
}
|
|
1055
|
+
return finish(failure('option-not-found'));
|
|
1056
|
+
}
|
|
1057
|
+
if (current.level === target) return finish({ status: 'already-selected', label: current.label });
|
|
1058
|
+
const deadline = performance.now() + MAX_WAIT_MS;
|
|
1059
|
+
for (let attempt = 0; attempt < 4 && performance.now() < deadline; attempt += 1) {
|
|
1060
|
+
if (isOptionDisabled(current.control)) return finish(failure('option-disabled', { label: current.label }));
|
|
1061
|
+
const previousIndex = current.index;
|
|
1062
|
+
const key = targetIndex > previousIndex ? 'ArrowRight' : 'ArrowLeft';
|
|
1063
|
+
current.control.focus?.();
|
|
1064
|
+
current.control.dispatchEvent(new KeyboardEvent('keydown', { key, code: key, bubbles: true, cancelable: true }));
|
|
1065
|
+
current = null;
|
|
1066
|
+
while (performance.now() < deadline) {
|
|
1067
|
+
await sleep(100);
|
|
1068
|
+
const next = resolve();
|
|
1069
|
+
if (next && next.index !== previousIndex) { current = next; break; }
|
|
1070
|
+
}
|
|
1071
|
+
if (!current) return finish(failure('selection-unverified'));
|
|
1072
|
+
if (current.level === target) return finish({ status: 'switched', label: current.label });
|
|
1073
|
+
}
|
|
1074
|
+
return finish(failure('selection-unverified'));
|
|
1075
|
+
};
|
|
1076
|
+
|
|
948
1077
|
// Current ChatGPT exposes a standalone Pro or Thinking composer pill whose
|
|
949
1078
|
// controlled menu contains the effort levels. Prefer this ownership boundary
|
|
950
1079
|
// before probing older model-picker layouts.
|
|
@@ -961,6 +1090,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
961
1090
|
if (seen.has(button) || !isVisible(button)) continue;
|
|
962
1091
|
seen.add(button);
|
|
963
1092
|
if (button.getAttribute?.('data-testid') === 'model-switcher-dropdown-button') continue;
|
|
1093
|
+
// A 5.6 Pro model pill is not Astra Latest's 6-prefixed effort owner.
|
|
1094
|
+
// Keep this rejection ahead of the generic compatibility matcher.
|
|
1095
|
+
if (isSolModelPillForLatest(button.textContent ?? '')) continue;
|
|
964
1096
|
const label = normalize(
|
|
965
1097
|
(button.getAttribute?.('aria-label') ?? '') + ' ' +
|
|
966
1098
|
(button.getAttribute?.('data-testid') ?? '') + ' ' +
|
|
@@ -970,6 +1102,11 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
970
1102
|
(TARGET_MODEL_KIND === 'pro' && hasToken(label, 'pro') && !hasToken(label, 'thinking')) ||
|
|
971
1103
|
(TARGET_MODEL_KIND === 'thinking' && hasToken(label, 'thinking') && !hasToken(label, 'pro')) ||
|
|
972
1104
|
(!TARGET_MODEL_KIND && hasToken(label, 'thinking')) ||
|
|
1105
|
+
// Astra Latest prefixes a supported effort label with "6" (for
|
|
1106
|
+
// example, "6 Pro" or textContent-concatenated "6Pro"). This is
|
|
1107
|
+
// recognized only for the exact Latest target; selection still
|
|
1108
|
+
// requires the direct slider's leading label and numeric ARIA proof.
|
|
1109
|
+
isAstraLatestEffortPill(button.textContent ?? '') ||
|
|
973
1110
|
(button.matches?.('button.__composer-pill') && matchesAnyEffortLevel(label))
|
|
974
1111
|
) {
|
|
975
1112
|
return button;
|
|
@@ -1056,6 +1193,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1056
1193
|
tokens.some((token) => normalize(token) === pillLabel),
|
|
1057
1194
|
);
|
|
1058
1195
|
const pillNamesEffortNotModel =
|
|
1196
|
+
TARGET_IS_ASTRA_LATEST ||
|
|
1059
1197
|
TARGET_IS_GPT56_MODEL ||
|
|
1060
1198
|
(pillIsBareEffortTier && Boolean(document.querySelector(INTELLIGENCE_MENU_SELECTOR)));
|
|
1061
1199
|
const composerModelKind =
|
|
@@ -1069,6 +1207,8 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1069
1207
|
while (performance.now() < deadline) {
|
|
1070
1208
|
const menu = findVisibleEffortMenu(composerEffortPill);
|
|
1071
1209
|
if (menu) {
|
|
1210
|
+
const sliderResult = await selectDirectEffortSlider(menu);
|
|
1211
|
+
if (sliderResult) return sliderResult;
|
|
1072
1212
|
const proEffortResult = await selectProEffortFromSubmenu();
|
|
1073
1213
|
if (proEffortResult) {
|
|
1074
1214
|
return proEffortResult;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { BrowserAutomationError } from "../../oracle/errors.js";
|
|
2
|
+
import { INPUT_SELECTORS } from "../constants.js";
|
|
3
|
+
import { delay } from "../utils.js";
|
|
4
|
+
import { activateComposerPlus, captureComposerNavigationUrl, assertComposerPlusStayedInPlace, } from "./attachments.js";
|
|
5
|
+
import { buildComposerNavigationValidationExpression } from "./attachmentContext.js";
|
|
6
|
+
import { buildClickDispatcher } from "./domEvents.js";
|
|
7
|
+
export function matchesWebSearchMenuLabel(value) {
|
|
8
|
+
return [
|
|
9
|
+
"search",
|
|
10
|
+
"searchfindontheweb",
|
|
11
|
+
"websearch",
|
|
12
|
+
"websearchfindreal-timenewsandinfo",
|
|
13
|
+
].includes(value.replace(/\s+/g, "").toLowerCase());
|
|
14
|
+
}
|
|
15
|
+
export function buildWebSearchVerificationExpression(prompt) {
|
|
16
|
+
return `(() => {
|
|
17
|
+
const visible = node => node instanceof HTMLElement && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0;
|
|
18
|
+
const editor = ${JSON.stringify(INPUT_SELECTORS)}.flatMap(selector => Array.from(document.querySelectorAll(selector))).find(visible);
|
|
19
|
+
if (!editor) return { selected: false, promptMatches: false };
|
|
20
|
+
const chip = editor.querySelector('[data-inline-selection-pill][data-id="search"][data-system-hint-type="search"]');
|
|
21
|
+
const copy = editor.cloneNode(true);
|
|
22
|
+
copy.querySelectorAll('[data-inline-selection-pill], [data-inline-selection-pill-cursor-target]').forEach(node => node.remove());
|
|
23
|
+
const readText = node => {
|
|
24
|
+
if (node.nodeType === 3) return node.textContent ?? '';
|
|
25
|
+
const text = Array.from(node.childNodes).map(readText).join('');
|
|
26
|
+
return text + (['P', 'DIV', 'BR', 'LI', 'PRE'].includes(node.nodeName) ? '\\n' : '');
|
|
27
|
+
};
|
|
28
|
+
const normalize = text => String(text ?? '').replace(/[\\u200b\\ufeff]/g, '').replace(/\\s+/g, ' ').trim();
|
|
29
|
+
return { selected: Boolean(chip && visible(chip)), promptMatches: normalize(readText(copy)) === normalize(${JSON.stringify(prompt)}) };
|
|
30
|
+
})()`;
|
|
31
|
+
}
|
|
32
|
+
export function buildWebSearchSelectionExpression(navigationUrl) {
|
|
33
|
+
return `(() => {
|
|
34
|
+
${buildClickDispatcher()}
|
|
35
|
+
const matchesLabel = ${matchesWebSearchMenuLabel.toString()};
|
|
36
|
+
const navigation = ${buildComposerNavigationValidationExpression(navigationUrl)};
|
|
37
|
+
if (!navigation.contextMatches || navigation.workSelected || navigation.modeUnverified) return 'context-changed';
|
|
38
|
+
const visible = node => node instanceof HTMLElement && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0;
|
|
39
|
+
const roots = Array.from(document.querySelectorAll('main .popover, [data-radix-popper-content-wrapper], [data-floating-ui-portal], [role="menu"], [role="listbox"]')).filter(visible);
|
|
40
|
+
const candidates = roots.flatMap(root => Array.from(root.querySelectorAll('[data-radix-collection-item], [role="menuitem"], [role="option"], .__menu-item, [class*="menu-item"]')));
|
|
41
|
+
const match = candidates.find(node => {
|
|
42
|
+
if (!visible(node) || node.hasAttribute('disabled') || node.getAttribute('aria-disabled') === 'true') return false;
|
|
43
|
+
return matchesLabel(node.textContent ?? '');
|
|
44
|
+
});
|
|
45
|
+
if (!match) return 'missing';
|
|
46
|
+
dispatchClickSequence(match);
|
|
47
|
+
return 'clicked';
|
|
48
|
+
})()`;
|
|
49
|
+
}
|
|
50
|
+
/** Web Search is an inline editor hint, so activate it after staging text/attachments. */
|
|
51
|
+
export async function activateWebSearch(runtime, input, prompt, logger) {
|
|
52
|
+
const navigationUrl = await captureComposerNavigationUrl(runtime);
|
|
53
|
+
const verify = async () => {
|
|
54
|
+
const { result, exceptionDetails } = await runtime.evaluate({
|
|
55
|
+
expression: buildWebSearchVerificationExpression(prompt),
|
|
56
|
+
returnByValue: true,
|
|
57
|
+
});
|
|
58
|
+
if (exceptionDetails)
|
|
59
|
+
return false;
|
|
60
|
+
return result?.value?.selected === true && result?.value?.promptMatches === true;
|
|
61
|
+
};
|
|
62
|
+
if (await verify())
|
|
63
|
+
return;
|
|
64
|
+
const activated = await activateComposerPlus(runtime, input, navigationUrl);
|
|
65
|
+
if (activated.method === "unavailable")
|
|
66
|
+
throw new BrowserAutomationError("Web Search requires the ChatGPT composer tools menu.", {
|
|
67
|
+
stage: "web-search-activate",
|
|
68
|
+
});
|
|
69
|
+
const deadline = Date.now() + 5_000;
|
|
70
|
+
let clicked = false;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
const outcome = await runtime.evaluate({
|
|
73
|
+
expression: buildWebSearchSelectionExpression(navigationUrl),
|
|
74
|
+
returnByValue: true,
|
|
75
|
+
});
|
|
76
|
+
if (outcome.result?.value === "clicked") {
|
|
77
|
+
clicked = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (outcome.exceptionDetails || outcome.result?.value !== "missing")
|
|
81
|
+
break;
|
|
82
|
+
await delay(100);
|
|
83
|
+
}
|
|
84
|
+
if (clicked) {
|
|
85
|
+
const confirmationDeadline = Date.now() + 3_000;
|
|
86
|
+
do {
|
|
87
|
+
await assertComposerPlusStayedInPlace(runtime, navigationUrl);
|
|
88
|
+
if (await verify()) {
|
|
89
|
+
logger("Web Search selected; inline search hint and staged prompt verified.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await delay(100);
|
|
93
|
+
} while (Date.now() < confirmationDeadline);
|
|
94
|
+
}
|
|
95
|
+
throw new BrowserAutomationError("Web Search selection could not be verified; the prompt was not submitted. This pilot supports the English ChatGPT Web search control.", { stage: "web-search-activate" });
|
|
96
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { discoverDevToolsActivePortCandidates, } from "./detect.js";
|
|
1
|
+
import { discoverDevToolsActivePortCandidates, formatWebSocketHost, resolveDevToolsActivePortDiscoveryRoots, } from "./detect.js";
|
|
2
2
|
export async function resolveAttachRunningConnection(config, logger) {
|
|
3
3
|
const host = config.remoteChrome?.host ?? "127.0.0.1";
|
|
4
4
|
const port = config.remoteChrome?.port ?? 9222;
|
|
@@ -12,7 +12,17 @@ export async function resolveAttachRunningConnection(config, logger) {
|
|
|
12
12
|
.filter((candidate) => candidate.port === port)
|
|
13
13
|
.sort(compareDevToolsCandidates);
|
|
14
14
|
if (candidates.length === 0) {
|
|
15
|
-
|
|
15
|
+
const probe = await probeDevToolsBrowserWSEndpoint({ host, port });
|
|
16
|
+
if (probe.ok) {
|
|
17
|
+
return {
|
|
18
|
+
host,
|
|
19
|
+
port,
|
|
20
|
+
browserWSEndpoint: probe.browserWSEndpoint,
|
|
21
|
+
profileRoot: null,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const discoveryRoots = resolveDevToolsActivePortDiscoveryRoots();
|
|
25
|
+
throw new Error(`No running browser matched ${host}:${port}. DevToolsActivePort discovery searched ${discoveryRoots.join(", ") || "no roots"}, and endpoint probe http://${formatWebSocketHost(host)}:${port}/json/version failed: ${probe.error}.`);
|
|
16
26
|
}
|
|
17
27
|
const candidate = candidates[0];
|
|
18
28
|
logger(`Selected attach-running browser metadata from ${candidate.path}`);
|
|
@@ -23,6 +33,40 @@ export async function resolveAttachRunningConnection(config, logger) {
|
|
|
23
33
|
profileRoot: candidate.profileRoot,
|
|
24
34
|
};
|
|
25
35
|
}
|
|
36
|
+
async function probeDevToolsBrowserWSEndpoint({ host, port, attempts = 2, timeoutMs = 1000, }) {
|
|
37
|
+
const versionUrl = `http://${formatWebSocketHost(host)}:${port}/json/version`;
|
|
38
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
39
|
+
try {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetch(versionUrl, { signal: controller.signal });
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Error(`HTTP ${response.status}`);
|
|
46
|
+
}
|
|
47
|
+
const version = (await response.json());
|
|
48
|
+
if (typeof version.webSocketDebuggerUrl !== "string" || !version.webSocketDebuggerUrl) {
|
|
49
|
+
throw new Error("response did not include webSocketDebuggerUrl");
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, browserWSEndpoint: version.webSocketDebuggerUrl };
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
// Headers can arrive before a stalled body; bound and clean up the whole request.
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
controller.abort();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (attempt < attempts - 1) {
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
return { ok: false, error: message };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { ok: false, error: "unreachable" };
|
|
69
|
+
}
|
|
26
70
|
function compareDevToolsCandidates(left, right) {
|
|
27
71
|
if (right.mtimeMs !== left.mtimeMs) {
|
|
28
72
|
return right.mtimeMs - left.mtimeMs;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
function compareStrings(left, right) {
|
|
3
|
+
if (left < right)
|
|
4
|
+
return -1;
|
|
5
|
+
if (left > right)
|
|
6
|
+
return 1;
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
export function findAttachmentBasenameCollisions(attachments) {
|
|
10
|
+
const attachmentsByBasename = new Map();
|
|
11
|
+
for (const attachment of attachments) {
|
|
12
|
+
const basename = path.basename(attachment.path);
|
|
13
|
+
const matchingAttachments = attachmentsByBasename.get(basename) ?? [];
|
|
14
|
+
matchingAttachments.push(attachment);
|
|
15
|
+
attachmentsByBasename.set(basename, matchingAttachments);
|
|
16
|
+
}
|
|
17
|
+
return [...attachmentsByBasename.entries()]
|
|
18
|
+
.filter(([, matchingAttachments]) => matchingAttachments.length > 1)
|
|
19
|
+
.sort(([leftBasename], [rightBasename]) => compareStrings(leftBasename, rightBasename))
|
|
20
|
+
.map(([basename, matchingAttachments]) => ({
|
|
21
|
+
basename,
|
|
22
|
+
attachments: [...matchingAttachments].sort((left, right) => compareStrings(left.displayPath, right.displayPath) ||
|
|
23
|
+
compareStrings(left.path, right.path)),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
export function buildAttachmentBasenameCollisionDetails(collisions, displayFile) {
|
|
27
|
+
const collisionDetails = collisions.map((collision) => ({
|
|
28
|
+
basename: collision.basename,
|
|
29
|
+
files: collision.attachments.map(displayFile),
|
|
30
|
+
}));
|
|
31
|
+
return {
|
|
32
|
+
collisions: collisionDetails,
|
|
33
|
+
files: collisionDetails.flatMap((collision) => collision.files),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function formatAttachmentBasenameCollisionMessage(subject, collisions) {
|
|
37
|
+
const collisionLines = collisions.length === 1
|
|
38
|
+
? [
|
|
39
|
+
`${subject} cannot safely include multiple files named "${collisions[0].basename}":`,
|
|
40
|
+
...collisions[0].files.map((file) => `- ${file}`),
|
|
41
|
+
]
|
|
42
|
+
: [
|
|
43
|
+
`${subject} cannot safely include files with colliding basenames:`,
|
|
44
|
+
...collisions.flatMap((collision) => [
|
|
45
|
+
`Files named "${collision.basename}":`,
|
|
46
|
+
...collision.files.map((file) => `- ${file}`),
|
|
47
|
+
]),
|
|
48
|
+
];
|
|
49
|
+
return [
|
|
50
|
+
...collisionLines,
|
|
51
|
+
"Use --browser-bundle-files (and --browser-bundle-format zip for raw or binary files), or choose unique names before retrying.",
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|