@steipete/oracle 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/bin/oracle-cli.js +3 -1
- package/dist/src/browser/actions/attachments.js +7 -3
- package/dist/src/browser/actions/modelSelection.js +51 -10
- package/dist/src/browser/actions/thinkingTime.js +40 -31
- package/dist/src/browser/chromeLifecycle.js +2 -0
- package/dist/src/browser/config.js +1 -1
- package/dist/src/browser/index.js +57 -9
- package/dist/src/browser/reattach.js +192 -17
- package/dist/src/cli/browserConfig.js +26 -16
- package/dist/src/cli/notifier.js +8 -2
- package/dist/src/cli/options.js +13 -3
- package/dist/src/cli/oscUtils.js +1 -19
- package/dist/src/cli/sessionDisplay.js +6 -3
- package/dist/src/cli/sessionTable.js +5 -1
- package/dist/src/oracle/files.js +8 -1
- package/dist/src/oracle/modelResolver.js +11 -4
- package/dist/src/oracle/multiModelRunner.js +3 -14
- package/dist/src/oracle/oscProgress.js +12 -61
- package/dist/src/oracle/run.js +62 -34
- package/dist/src/sessionManager.js +91 -2
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +43 -26
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/README.md +24 -0
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
package/README.md
CHANGED
|
@@ -64,6 +64,12 @@ Engine auto-picks API when `OPENAI_API_KEY` is set, otherwise browser; browser i
|
|
|
64
64
|
```
|
|
65
65
|
- Tip: set `browser.chatgptUrl` in config (or `--chatgpt-url`) to a dedicated ChatGPT project folder so browser runs don’t clutter your main history.
|
|
66
66
|
|
|
67
|
+
**Codex skill**
|
|
68
|
+
- Copy the bundled skill from this repo to your Codex skills folder:
|
|
69
|
+
- `mkdir -p ~/.codex/skills`
|
|
70
|
+
- `cp -R skills/oracle ~/.codex/skills/oracle`
|
|
71
|
+
- Then reference it in your `AGENTS.md`/`CLAUDE.md` so Codex loads it.
|
|
72
|
+
|
|
67
73
|
**MCP**
|
|
68
74
|
- Run the stdio server via `oracle-mcp`.
|
|
69
75
|
- Configure clients via [steipete/mcporter](https://github.com/steipete/mcporter) or `.mcp.json`; see [docs/mcp.md](docs/mcp.md) for connection examples.
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -174,7 +174,9 @@ program
|
|
|
174
174
|
.addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
|
|
175
175
|
.addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
|
|
176
176
|
.addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
|
|
177
|
-
.addOption(new Option('--browser-
|
|
177
|
+
.addOption(new Option('--browser-thinking-time <level>', 'Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.')
|
|
178
|
+
.choices(['light', 'standard', 'extended', 'heavy'])
|
|
179
|
+
.hideHelp())
|
|
178
180
|
.addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
|
|
179
181
|
.addOption(new Option('--browser-attachments <mode>', 'How to deliver --file inputs in browser mode: auto (default) pastes inline up to ~60k chars then uploads; never always paste inline; always always upload.')
|
|
180
182
|
.choices(['auto', 'never', 'always'])
|
|
@@ -177,12 +177,15 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
177
177
|
}
|
|
178
178
|
await dom.setFileInputFiles({ nodeId: resultNode.nodeId, files: [attachment.path] });
|
|
179
179
|
await runtime.evaluate({ expression: dispatchEventsFor(idx), returnByValue: true }).catch(() => undefined);
|
|
180
|
-
|
|
180
|
+
await delay(350);
|
|
181
|
+
const probeDeadline = Date.now() + 6500;
|
|
182
|
+
const pokeIntervalMs = 1200;
|
|
181
183
|
let lastPoke = 0;
|
|
184
|
+
let seenInputHasFile = false;
|
|
182
185
|
while (Date.now() < probeDeadline) {
|
|
183
186
|
// ChatGPT's composer can take a moment to hydrate the file-input onChange handler after navigation/model switches.
|
|
184
187
|
// If the UI hasn't reacted yet, poke the input a few times to ensure the handler fires once it's mounted.
|
|
185
|
-
if (Date.now() - lastPoke >
|
|
188
|
+
if (!seenInputHasFile && Date.now() - lastPoke > pokeIntervalMs) {
|
|
186
189
|
lastPoke = Date.now();
|
|
187
190
|
await runtime.evaluate({ expression: dispatchEventsFor(idx), returnByValue: true }).catch(() => undefined);
|
|
188
191
|
}
|
|
@@ -198,6 +201,7 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
198
201
|
composerText: typeof snapshot.composerText === 'string' ? snapshot.composerText : '',
|
|
199
202
|
};
|
|
200
203
|
const inputHasFile = finalSnapshot.inputNames.some((name) => name.toLowerCase().includes(expectedName.toLowerCase()));
|
|
204
|
+
seenInputHasFile = seenInputHasFile || inputHasFile;
|
|
201
205
|
const expectedLower = expectedName.toLowerCase();
|
|
202
206
|
const expectedNoExt = expectedLower.replace(/\.[a-z0-9]{1,10}$/i, '');
|
|
203
207
|
const uiAcknowledged = finalSnapshot.chipCount > baselineChipCount ||
|
|
@@ -209,7 +213,7 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
209
213
|
break;
|
|
210
214
|
}
|
|
211
215
|
}
|
|
212
|
-
await delay(
|
|
216
|
+
await delay(250);
|
|
213
217
|
}
|
|
214
218
|
const inputHasFile = finalSnapshot?.inputNames?.some((name) => name.toLowerCase().includes(expectedName.toLowerCase())) ?? false;
|
|
215
219
|
const uiAcknowledged = (finalSnapshot?.chipCount ?? 0) > baselineChipCount;
|
|
@@ -97,6 +97,10 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
97
97
|
if (wantsPro && !normalizedLabel.includes(' pro')) return false;
|
|
98
98
|
if (wantsInstant && !normalizedLabel.includes('instant')) return false;
|
|
99
99
|
if (wantsThinking && !normalizedLabel.includes('thinking')) return false;
|
|
100
|
+
// Also reject if button has variants we DON'T want
|
|
101
|
+
if (!wantsPro && normalizedLabel.includes(' pro')) return false;
|
|
102
|
+
if (!wantsInstant && normalizedLabel.includes('instant')) return false;
|
|
103
|
+
if (!wantsThinking && normalizedLabel.includes('thinking')) return false;
|
|
100
104
|
return true;
|
|
101
105
|
};
|
|
102
106
|
|
|
@@ -172,14 +176,21 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
172
176
|
return 0;
|
|
173
177
|
}
|
|
174
178
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
179
|
+
// Exact testid matches take priority over substring matches
|
|
180
|
+
const exactMatch = TEST_IDS.find((id) => id && normalizedTestId === id);
|
|
181
|
+
if (exactMatch) {
|
|
182
|
+
score += 1500;
|
|
183
|
+
if (exactMatch.startsWith('model-switcher-')) score += 200;
|
|
184
|
+
} else {
|
|
185
|
+
const matches = TEST_IDS.filter((id) => id && normalizedTestId.includes(id));
|
|
186
|
+
if (matches.length > 0) {
|
|
187
|
+
// Prefer the most specific match (longest token) instead of treating any hit as equal.
|
|
188
|
+
// This prevents generic tokens (e.g. "pro") from outweighing version-specific targets.
|
|
189
|
+
const best = matches.reduce((acc, token) => (token.length > acc.length ? token : acc), '');
|
|
190
|
+
score += 200 + Math.min(900, best.length * 25);
|
|
191
|
+
if (best.startsWith('model-switcher-')) score += 120;
|
|
192
|
+
if (best.includes('gpt-')) score += 60;
|
|
193
|
+
}
|
|
183
194
|
}
|
|
184
195
|
}
|
|
185
196
|
if (normalizedText && normalizedTarget) {
|
|
@@ -215,6 +226,22 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
215
226
|
} else if (normalizedText.includes(' pro')) {
|
|
216
227
|
score -= 40;
|
|
217
228
|
}
|
|
229
|
+
// Similarly for Thinking variant
|
|
230
|
+
if (wantsThinking) {
|
|
231
|
+
if (!normalizedText.includes('thinking') && !normalizedTestId.includes('thinking')) {
|
|
232
|
+
score -= 80;
|
|
233
|
+
}
|
|
234
|
+
} else if (normalizedText.includes('thinking') || normalizedTestId.includes('thinking')) {
|
|
235
|
+
score -= 40;
|
|
236
|
+
}
|
|
237
|
+
// Similarly for Instant variant
|
|
238
|
+
if (wantsInstant) {
|
|
239
|
+
if (!normalizedText.includes('instant') && !normalizedTestId.includes('instant')) {
|
|
240
|
+
score -= 80;
|
|
241
|
+
}
|
|
242
|
+
} else if (normalizedText.includes('instant') || normalizedTestId.includes('instant')) {
|
|
243
|
+
score -= 40;
|
|
244
|
+
}
|
|
218
245
|
return Math.max(score, 0);
|
|
219
246
|
};
|
|
220
247
|
|
|
@@ -377,10 +404,24 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
377
404
|
push('gpt5-2', labelTokens);
|
|
378
405
|
push('gpt52', labelTokens);
|
|
379
406
|
push('chatgpt 5.2', labelTokens);
|
|
380
|
-
|
|
407
|
+
// Thinking variant: explicit testid for "Thinking" picker option
|
|
408
|
+
if (base.includes('thinking')) {
|
|
381
409
|
push('thinking', labelTokens);
|
|
382
|
-
|
|
410
|
+
testIdTokens.add('model-switcher-gpt-5-2-thinking');
|
|
411
|
+
testIdTokens.add('gpt-5-2-thinking');
|
|
412
|
+
testIdTokens.add('gpt-5.2-thinking');
|
|
413
|
+
}
|
|
414
|
+
// Instant variant: explicit testid for "Instant" picker option
|
|
415
|
+
if (base.includes('instant')) {
|
|
383
416
|
push('instant', labelTokens);
|
|
417
|
+
testIdTokens.add('model-switcher-gpt-5-2-instant');
|
|
418
|
+
testIdTokens.add('gpt-5-2-instant');
|
|
419
|
+
testIdTokens.add('gpt-5.2-instant');
|
|
420
|
+
}
|
|
421
|
+
// Base 5.2 testids (for "Auto" mode when no suffix specified)
|
|
422
|
+
if (!base.includes('thinking') && !base.includes('instant') && !base.includes('pro')) {
|
|
423
|
+
testIdTokens.add('model-switcher-gpt-5-2');
|
|
424
|
+
}
|
|
384
425
|
testIdTokens.add('gpt-5-2');
|
|
385
426
|
testIdTokens.add('gpt5-2');
|
|
386
427
|
testIdTokens.add('gpt52');
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR } from '../constants.js';
|
|
2
2
|
import { logDomFailure } from '../domDebug.js';
|
|
3
3
|
import { buildClickDispatcher } from './domEvents.js';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Selects a specific thinking time level in ChatGPT's composer pill menu.
|
|
6
|
+
* @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
|
|
7
|
+
*/
|
|
8
|
+
export async function ensureThinkingTime(Runtime, level, logger) {
|
|
9
|
+
const result = await evaluateThinkingTimeSelection(Runtime, level);
|
|
10
|
+
const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
|
|
6
11
|
switch (result?.status) {
|
|
7
|
-
case 'already-
|
|
8
|
-
logger(`Thinking time: ${result.label ??
|
|
12
|
+
case 'already-selected':
|
|
13
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
|
|
9
14
|
return;
|
|
10
15
|
case 'switched':
|
|
11
|
-
logger(`Thinking time: ${result.label ??
|
|
16
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
|
|
12
17
|
return;
|
|
13
18
|
case 'chip-not-found': {
|
|
14
19
|
await logDomFailure(Runtime, logger, 'thinking-chip');
|
|
@@ -18,33 +23,35 @@ export async function ensureExtendedThinking(Runtime, logger) {
|
|
|
18
23
|
await logDomFailure(Runtime, logger, 'thinking-time-menu');
|
|
19
24
|
throw new Error('Unable to find the Thinking time dropdown menu.');
|
|
20
25
|
}
|
|
21
|
-
case '
|
|
22
|
-
await logDomFailure(Runtime, logger,
|
|
23
|
-
throw new Error(
|
|
26
|
+
case 'option-not-found': {
|
|
27
|
+
await logDomFailure(Runtime, logger, `${level}-option`);
|
|
28
|
+
throw new Error(`Unable to find the ${capitalizedLevel} option in the Thinking time menu.`);
|
|
24
29
|
}
|
|
25
30
|
default: {
|
|
26
31
|
await logDomFailure(Runtime, logger, 'thinking-time-unknown');
|
|
27
|
-
throw new Error(
|
|
32
|
+
throw new Error(`Unknown error selecting ${capitalizedLevel} thinking time.`);
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
/**
|
|
32
|
-
* Best-effort selection of
|
|
37
|
+
* Best-effort selection of a thinking time level in ChatGPT's composer pill menu.
|
|
33
38
|
* Safe by default: if the pill/menu/option isn't present, we continue without throwing.
|
|
39
|
+
* @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
|
|
34
40
|
*/
|
|
35
|
-
export async function
|
|
41
|
+
export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
|
|
36
42
|
try {
|
|
37
|
-
const result = await evaluateThinkingTimeSelection(Runtime);
|
|
43
|
+
const result = await evaluateThinkingTimeSelection(Runtime, level);
|
|
44
|
+
const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
|
|
38
45
|
switch (result?.status) {
|
|
39
|
-
case 'already-
|
|
40
|
-
logger(`Thinking time: ${result.label ??
|
|
46
|
+
case 'already-selected':
|
|
47
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
|
|
41
48
|
return true;
|
|
42
49
|
case 'switched':
|
|
43
|
-
logger(`Thinking time: ${result.label ??
|
|
50
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
|
|
44
51
|
return true;
|
|
45
52
|
case 'chip-not-found':
|
|
46
53
|
case 'menu-not-found':
|
|
47
|
-
case '
|
|
54
|
+
case 'option-not-found':
|
|
48
55
|
if (logger.verbose) {
|
|
49
56
|
logger(`Thinking time: ${result.status.replaceAll('-', ' ')}; continuing with default.`);
|
|
50
57
|
}
|
|
@@ -65,22 +72,24 @@ export async function ensureExtendedThinkingIfAvailable(Runtime, logger) {
|
|
|
65
72
|
return false;
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
|
-
async function evaluateThinkingTimeSelection(Runtime) {
|
|
75
|
+
async function evaluateThinkingTimeSelection(Runtime, level) {
|
|
69
76
|
const outcome = await Runtime.evaluate({
|
|
70
|
-
expression: buildThinkingTimeExpression(),
|
|
77
|
+
expression: buildThinkingTimeExpression(level),
|
|
71
78
|
awaitPromise: true,
|
|
72
79
|
returnByValue: true,
|
|
73
80
|
});
|
|
74
81
|
return outcome.result?.value;
|
|
75
82
|
}
|
|
76
|
-
function buildThinkingTimeExpression() {
|
|
83
|
+
function buildThinkingTimeExpression(level) {
|
|
77
84
|
const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
|
|
78
85
|
const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
|
|
86
|
+
const targetLevelLiteral = JSON.stringify(level.toLowerCase());
|
|
79
87
|
return `(async () => {
|
|
80
88
|
${buildClickDispatcher()}
|
|
81
89
|
|
|
82
90
|
const MENU_CONTAINER_SELECTOR = ${menuContainerLiteral};
|
|
83
91
|
const MENU_ITEM_SELECTOR = ${menuItemLiteral};
|
|
92
|
+
const TARGET_LEVEL = ${targetLevelLiteral};
|
|
84
93
|
|
|
85
94
|
const CHIP_SELECTORS = [
|
|
86
95
|
'[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]',
|
|
@@ -136,11 +145,11 @@ function buildThinkingTimeExpression() {
|
|
|
136
145
|
return null;
|
|
137
146
|
};
|
|
138
147
|
|
|
139
|
-
const
|
|
148
|
+
const findTargetOption = (menu) => {
|
|
140
149
|
const items = menu.querySelectorAll(MENU_ITEM_SELECTOR);
|
|
141
150
|
for (const item of items) {
|
|
142
151
|
const text = normalize(item.textContent ?? '');
|
|
143
|
-
if (text.includes(
|
|
152
|
+
if (text.includes(TARGET_LEVEL)) {
|
|
144
153
|
return item;
|
|
145
154
|
}
|
|
146
155
|
}
|
|
@@ -167,24 +176,24 @@ function buildThinkingTimeExpression() {
|
|
|
167
176
|
return;
|
|
168
177
|
}
|
|
169
178
|
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
172
|
-
resolve({ status: '
|
|
179
|
+
const targetOption = findTargetOption(menu);
|
|
180
|
+
if (!targetOption) {
|
|
181
|
+
resolve({ status: 'option-not-found' });
|
|
173
182
|
return;
|
|
174
183
|
}
|
|
175
184
|
|
|
176
185
|
const alreadySelected =
|
|
177
|
-
optionIsSelected(
|
|
178
|
-
optionIsSelected(
|
|
179
|
-
const label =
|
|
180
|
-
dispatchClickSequence(
|
|
181
|
-
resolve({ status: alreadySelected ? 'already-
|
|
186
|
+
optionIsSelected(targetOption) ||
|
|
187
|
+
optionIsSelected(targetOption.querySelector?.('[aria-checked="true"], [data-state="checked"], [data-state="selected"]'));
|
|
188
|
+
const label = targetOption.textContent?.trim?.() || null;
|
|
189
|
+
dispatchClickSequence(targetOption);
|
|
190
|
+
resolve({ status: alreadySelected ? 'already-selected' : 'switched', label });
|
|
182
191
|
};
|
|
183
192
|
|
|
184
193
|
setTimeout(attempt, INITIAL_WAIT_MS);
|
|
185
194
|
});
|
|
186
195
|
})()`;
|
|
187
196
|
}
|
|
188
|
-
export function buildThinkingTimeExpressionForTest() {
|
|
189
|
-
return buildThinkingTimeExpression();
|
|
197
|
+
export function buildThinkingTimeExpressionForTest(level = 'extended') {
|
|
198
|
+
return buildThinkingTimeExpression(level);
|
|
190
199
|
}
|
|
@@ -25,6 +25,7 @@ export async function launchChrome(config, userDataDir, logger) {
|
|
|
25
25
|
chromePath: config.chromePath ?? undefined,
|
|
26
26
|
chromeFlags,
|
|
27
27
|
userDataDir,
|
|
28
|
+
handleSIGINT: false,
|
|
28
29
|
port: debugPort ?? undefined,
|
|
29
30
|
});
|
|
30
31
|
const pidLabel = typeof launcher.pid === 'number' ? ` (pid ${launcher.pid})` : '';
|
|
@@ -216,6 +217,7 @@ async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host
|
|
|
216
217
|
chromePath: chromePath ?? undefined,
|
|
217
218
|
chromeFlags,
|
|
218
219
|
userDataDir,
|
|
220
|
+
handleSIGINT: false,
|
|
219
221
|
port: requestedPort ?? undefined,
|
|
220
222
|
});
|
|
221
223
|
if (host) {
|
|
@@ -24,7 +24,6 @@ export const DEFAULT_BROWSER_CONFIG = {
|
|
|
24
24
|
remoteChrome: null,
|
|
25
25
|
manualLogin: false,
|
|
26
26
|
manualLoginProfileDir: null,
|
|
27
|
-
extendedThinking: false,
|
|
28
27
|
};
|
|
29
28
|
export function resolveBrowserConfig(config) {
|
|
30
29
|
const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
|
|
@@ -64,6 +63,7 @@ export function resolveBrowserConfig(config) {
|
|
|
64
63
|
chromeCookiePath: config?.chromeCookiePath ?? DEFAULT_BROWSER_CONFIG.chromeCookiePath,
|
|
65
64
|
debug: config?.debug ?? DEFAULT_BROWSER_CONFIG.debug,
|
|
66
65
|
allowCookieErrors: config?.allowCookieErrors ?? envAllowCookieErrors ?? DEFAULT_BROWSER_CONFIG.allowCookieErrors,
|
|
66
|
+
thinkingTime: config?.thinkingTime,
|
|
67
67
|
manualLogin,
|
|
68
68
|
manualLoginProfileDir: manualLogin ? resolvedProfileDir : null,
|
|
69
69
|
};
|
|
@@ -7,7 +7,7 @@ import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToChro
|
|
|
7
7
|
import { syncCookies } from './cookies.js';
|
|
8
8
|
import { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureModelSelection, submitPrompt, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from './pageActions.js';
|
|
9
9
|
import { uploadAttachmentViaDataTransfer } from './actions/remoteFileTransfer.js';
|
|
10
|
-
import {
|
|
10
|
+
import { ensureThinkingTime } from './actions/thinkingTime.js';
|
|
11
11
|
import { estimateTokenCount, withRetries, delay } from './utils.js';
|
|
12
12
|
import { formatElapsed } from '../oracle/format.js';
|
|
13
13
|
import { CHATGPT_URL } from './constants.js';
|
|
@@ -37,12 +37,14 @@ export async function runBrowserMode(options) {
|
|
|
37
37
|
if (!runtimeHintCb || !chrome?.port) {
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
+
const conversationId = lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined;
|
|
40
41
|
const hint = {
|
|
41
42
|
chromePid: chrome.pid,
|
|
42
43
|
chromePort: chrome.port,
|
|
43
44
|
chromeHost,
|
|
44
45
|
chromeTargetId: lastTargetId,
|
|
45
46
|
tabUrl: lastUrl,
|
|
47
|
+
conversationId,
|
|
46
48
|
userDataDir,
|
|
47
49
|
controllerPid: process.pid,
|
|
48
50
|
};
|
|
@@ -264,13 +266,15 @@ export async function runBrowserMode(options) {
|
|
|
264
266
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
265
267
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
266
268
|
}
|
|
267
|
-
if
|
|
268
|
-
|
|
269
|
+
// Handle thinking time selection if specified
|
|
270
|
+
const thinkingTime = config.thinkingTime;
|
|
271
|
+
if (thinkingTime) {
|
|
272
|
+
await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
|
|
269
273
|
retries: 2,
|
|
270
274
|
delayMs: 300,
|
|
271
275
|
onRetry: (attempt, error) => {
|
|
272
276
|
if (options.verbose) {
|
|
273
|
-
logger(`[retry]
|
|
277
|
+
logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
274
278
|
}
|
|
275
279
|
},
|
|
276
280
|
}));
|
|
@@ -284,6 +288,7 @@ export async function runBrowserMode(options) {
|
|
|
284
288
|
for (const attachment of submissionAttachments) {
|
|
285
289
|
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
286
290
|
await uploadAttachmentFile({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
291
|
+
await delay(500);
|
|
287
292
|
}
|
|
288
293
|
// Scale timeout based on number of files: base 30s + 15s per additional file
|
|
289
294
|
const baseTimeout = config.inputTimeoutMs ?? 30_000;
|
|
@@ -315,7 +320,7 @@ export async function runBrowserMode(options) {
|
|
|
315
320
|
}
|
|
316
321
|
}
|
|
317
322
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
318
|
-
const answer = await raceWithDisconnect(
|
|
323
|
+
const answer = await raceWithDisconnect(waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger));
|
|
319
324
|
answerText = answer.text;
|
|
320
325
|
answerHtml = answer.html ?? '';
|
|
321
326
|
const copiedMarkdown = await raceWithDisconnect(withRetries(async () => {
|
|
@@ -661,13 +666,15 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
661
666
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
662
667
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
663
668
|
}
|
|
664
|
-
if
|
|
665
|
-
|
|
669
|
+
// Handle thinking time selection if specified
|
|
670
|
+
const thinkingTime = config.thinkingTime;
|
|
671
|
+
if (thinkingTime) {
|
|
672
|
+
await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
|
|
666
673
|
retries: 2,
|
|
667
674
|
delayMs: 300,
|
|
668
675
|
onRetry: (attempt, error) => {
|
|
669
676
|
if (options.verbose) {
|
|
670
|
-
logger(`[retry]
|
|
677
|
+
logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
671
678
|
}
|
|
672
679
|
},
|
|
673
680
|
});
|
|
@@ -682,6 +689,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
682
689
|
for (const attachment of submissionAttachments) {
|
|
683
690
|
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
684
691
|
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
692
|
+
await delay(500);
|
|
685
693
|
}
|
|
686
694
|
// Scale timeout based on number of files: base 30s + 15s per additional file
|
|
687
695
|
const baseTimeout = config.inputTimeoutMs ?? 30_000;
|
|
@@ -709,7 +717,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
709
717
|
}
|
|
710
718
|
}
|
|
711
719
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
712
|
-
const answer = await
|
|
720
|
+
const answer = await waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger);
|
|
713
721
|
answerText = answer.text;
|
|
714
722
|
answerHtml = answer.html ?? '';
|
|
715
723
|
const copiedMarkdown = await withRetries(async () => {
|
|
@@ -859,6 +867,42 @@ export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
|
|
|
859
867
|
const statusLabel = message ? ` — ${message}` : '';
|
|
860
868
|
return `${pct}% [${elapsedText} / ~10m]${statusLabel}${locatorSuffix}`;
|
|
861
869
|
}
|
|
870
|
+
async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger) {
|
|
871
|
+
try {
|
|
872
|
+
return await waitForAssistantResponse(Runtime, timeoutMs, logger);
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
if (!shouldReloadAfterAssistantError(error)) {
|
|
876
|
+
throw error;
|
|
877
|
+
}
|
|
878
|
+
const conversationUrl = await readConversationUrl(Runtime);
|
|
879
|
+
if (!conversationUrl || !isConversationUrl(conversationUrl)) {
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
logger('Assistant response stalled; reloading conversation and retrying once');
|
|
883
|
+
await Page.navigate({ url: conversationUrl });
|
|
884
|
+
await delay(1000);
|
|
885
|
+
return await waitForAssistantResponse(Runtime, timeoutMs, logger);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function shouldReloadAfterAssistantError(error) {
|
|
889
|
+
if (!(error instanceof Error))
|
|
890
|
+
return false;
|
|
891
|
+
const message = error.message.toLowerCase();
|
|
892
|
+
return message.includes('assistant-response') || message.includes('watchdog') || message.includes('timeout');
|
|
893
|
+
}
|
|
894
|
+
async function readConversationUrl(Runtime) {
|
|
895
|
+
try {
|
|
896
|
+
const currentUrl = await Runtime.evaluate({ expression: 'location.href', returnByValue: true });
|
|
897
|
+
return typeof currentUrl.result?.value === 'string' ? currentUrl.result.value : null;
|
|
898
|
+
}
|
|
899
|
+
catch {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function isConversationUrl(url) {
|
|
904
|
+
return /\/c\/[a-z0-9-]+/i.test(url);
|
|
905
|
+
}
|
|
862
906
|
function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false) {
|
|
863
907
|
let stopped = false;
|
|
864
908
|
let pending = false;
|
|
@@ -947,6 +991,10 @@ function isWsl() {
|
|
|
947
991
|
return true;
|
|
948
992
|
return os.release().toLowerCase().includes('microsoft');
|
|
949
993
|
}
|
|
994
|
+
function extractConversationIdFromUrl(url) {
|
|
995
|
+
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
996
|
+
return match?.[1];
|
|
997
|
+
}
|
|
950
998
|
async function resolveUserDataBaseDir() {
|
|
951
999
|
// On WSL, Chrome launched via Windows can choke on UNC paths; prefer a Windows-backed temp folder.
|
|
952
1000
|
if (isWsl()) {
|