@steipete/oracle 0.15.2 → 0.16.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/dist/bin/oracle-cli.js +1 -1
- package/dist/src/browser/actions/assistantResponse.js +218 -126
- package/dist/src/browser/actions/modelSelection.js +133 -22
- package/dist/src/browser/actions/navigation.js +235 -14
- package/dist/src/browser/actions/thinkingStatus.js +228 -0
- package/dist/src/browser/actions/thinkingTime.js +68 -11
- package/dist/src/browser/chromeLifecycle.js +29 -28
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/constants.js +5 -1
- package/dist/src/browser/controlPlan.js +2 -2
- package/dist/src/browser/index.js +56 -16
- package/dist/src/browser/liveTabs.js +113 -31
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/projectSourcesRunner.js +4 -4
- package/dist/src/browser/reattach.js +2 -2
- package/dist/src/browser/recoverConversation.js +90 -29
- package/dist/src/cli/browserConfig.js +5 -1
- package/dist/src/cli/browserTabs.js +54 -33
- package/dist/src/cli/options.js +24 -0
- package/dist/src/cli/runOptions.js +6 -1
- package/dist/src/cli/sessionDisplay.js +38 -14
- package/dist/src/oracle/config.js +25 -0
- package/dist/src/oracle/geminiModels.js +2 -0
- 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 +9 -10
- 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 -569
- 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 -416
- package/dist/docs-site/browser-mode.html +0 -594
- 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 -462
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -375
- package/dist/docs-site/gemini.html +0 -383
- 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 -596
- package/dist/docs-site/mcp.html +0 -391
- 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 -399
- 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/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -404,6 +404,234 @@ function buildThinkingStatusExpression() {
|
|
|
404
404
|
return null;
|
|
405
405
|
})()`;
|
|
406
406
|
}
|
|
407
|
+
// Present-tense/gerund status labels that mean the model is ACTIVELY working. The
|
|
408
|
+
// past-tense "thought for Xs" summary is deliberately excluded: it persists in the DOM
|
|
409
|
+
// on every completed reasoning turn (and on reattach), so treating its mere presence as
|
|
410
|
+
// "still thinking" would veto completion forever and hang the call. Kept in sync with
|
|
411
|
+
// THINKING_STATUS_LABELS in assistantResponse.ts (the connector phases "searching the
|
|
412
|
+
// web"/"reading"/"finalizing answer" are exactly the GPT-5.5 Pro gaps that produce the
|
|
413
|
+
// preamble->answer window this predicate must cover).
|
|
414
|
+
const ACTIVE_THINKING_LABELS = [
|
|
415
|
+
"thinking",
|
|
416
|
+
"pro thinking",
|
|
417
|
+
"thinking longer for a better answer",
|
|
418
|
+
"reasoning",
|
|
419
|
+
"finalizing answer",
|
|
420
|
+
"finalizing",
|
|
421
|
+
"analyzing",
|
|
422
|
+
"researching",
|
|
423
|
+
"working on it",
|
|
424
|
+
"working",
|
|
425
|
+
"planning",
|
|
426
|
+
"searching the web",
|
|
427
|
+
"searching",
|
|
428
|
+
"reading",
|
|
429
|
+
];
|
|
430
|
+
// buildThinkingActivePredicateJs: a SIDE-EFFECT-FREE injected predicate `${fnName}()` that
|
|
431
|
+
// returns true iff the model is ACTIVELY generating/thinking right now. Unlike
|
|
432
|
+
// buildThinkingStatusExpression it never clicks a disclosure and never keys on the mere
|
|
433
|
+
// PRESENCE of a reasoning container ([data-testid*="reasoning"] persists after completion);
|
|
434
|
+
// it keys only on ACTIVITY signals: a visible stop/interrupt control, a visible animated
|
|
435
|
+
// loading-shimmer skeleton, aria-busy, a visible thinking sidecar panel with live progress,
|
|
436
|
+
// or a visible ACTIVE (present-tense) thinking status label. Used as a completion VETO so a
|
|
437
|
+
// settled preamble is never finalized while the reasoning/tool phase is still running.
|
|
438
|
+
function buildThinkingActivityPredicateJs(fnName, detailed) {
|
|
439
|
+
const stopLiteral = JSON.stringify(STOP_BUTTON_SELECTORS.join(", "));
|
|
440
|
+
const activeLabelsLiteral = JSON.stringify(ACTIVE_THINKING_LABELS);
|
|
441
|
+
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
442
|
+
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
443
|
+
const strong = detailed ? "{ active: true, strong: true }" : "true";
|
|
444
|
+
const weak = detailed ? "{ active: true, strong: false }" : "true";
|
|
445
|
+
const idle = detailed ? "{ active: false, strong: false }" : "false";
|
|
446
|
+
return `const ${fnName} = () => {
|
|
447
|
+
const STOP_SELECTOR = ${stopLiteral};
|
|
448
|
+
const ACTIVE_LABELS = ${activeLabelsLiteral};
|
|
449
|
+
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
450
|
+
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
451
|
+
const isVisible = (node) => {
|
|
452
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
453
|
+
const rect = node.getBoundingClientRect();
|
|
454
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
|
|
455
|
+
const style = window.getComputedStyle(node);
|
|
456
|
+
if (
|
|
457
|
+
style.display === 'none' ||
|
|
458
|
+
style.visibility === 'hidden' ||
|
|
459
|
+
(style.opacity !== '' && Number(style.opacity) === 0)
|
|
460
|
+
) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
return true;
|
|
464
|
+
};
|
|
465
|
+
const some = (selector, pred) => {
|
|
466
|
+
let nodes;
|
|
467
|
+
try { nodes = document.querySelectorAll(selector); } catch { return false; }
|
|
468
|
+
return Array.from(nodes).some((node) => pred(node));
|
|
469
|
+
};
|
|
470
|
+
// 1) Stop/interrupt control visible -> generation is active (language-independent).
|
|
471
|
+
if (some(STOP_SELECTOR, isVisible)) return ${strong};
|
|
472
|
+
// 2-3) Shimmer/aria-busy are checked only inside the current turn or verified thinking
|
|
473
|
+
// panel below. Page-global busy UI (history/sidebar/upload) is not model activity.
|
|
474
|
+
const hasBusyIndicator = (scope) => {
|
|
475
|
+
let nodes;
|
|
476
|
+
try {
|
|
477
|
+
nodes = scope.querySelectorAll(
|
|
478
|
+
'span.loading-shimmer, .loading-shimmer, [class*="loading-shimmer"], [aria-busy="true"]',
|
|
479
|
+
);
|
|
480
|
+
} catch { return false; }
|
|
481
|
+
return Array.from(nodes).some((node) => isVisible(node));
|
|
482
|
+
};
|
|
483
|
+
// 4) Active (present-tense) thinking status label near a status/reasoning node.
|
|
484
|
+
const norm = (value) =>
|
|
485
|
+
String(value || '')
|
|
486
|
+
.normalize('NFD')
|
|
487
|
+
.replace(/[\\u0300-\\u036f]/g, '')
|
|
488
|
+
.toLowerCase()
|
|
489
|
+
.replace(/\\s+/g, ' ')
|
|
490
|
+
.trim();
|
|
491
|
+
// Completed reasoning summary: the whole visible label must be a duration summary. Anchoring
|
|
492
|
+
// prevents an early live trace such as "Thought for 2s: Searching the web" from being
|
|
493
|
+
// mistaken for completion before the trace grows beyond an arbitrary length threshold.
|
|
494
|
+
const isCompletedSummary = (text) =>
|
|
495
|
+
/^(?:(?:reasoning|pro thinking)\\s*)?thought for (?:\\d+(?:\\.\\d+)?\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)(?:\\s+\\d+(?:\\.\\d+)?\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours))*|(?:a|an) [a-z]+(?: [a-z]+){0,2})(?: edit)?$/.test(text);
|
|
496
|
+
const isActiveLabel = (raw) => {
|
|
497
|
+
const text = norm(raw);
|
|
498
|
+
if (!text || text.length > 60) return false;
|
|
499
|
+
if (isCompletedSummary(text)) return false;
|
|
500
|
+
return ACTIVE_LABELS.some((label) => text === label || text.startsWith(label + ' '));
|
|
501
|
+
};
|
|
502
|
+
const statusNodes = (() => {
|
|
503
|
+
try {
|
|
504
|
+
return Array.from(
|
|
505
|
+
document.querySelectorAll(
|
|
506
|
+
'[data-testid*="thinking"], [data-testid*="reasoning"]',
|
|
507
|
+
),
|
|
508
|
+
);
|
|
509
|
+
} catch {
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
})();
|
|
513
|
+
for (const node of statusNodes) {
|
|
514
|
+
if (!(node instanceof HTMLElement) || !isVisible(node)) continue;
|
|
515
|
+
const testId = norm(node.getAttribute('data-testid'));
|
|
516
|
+
const verifiedThinkingChrome = testId.includes('thinking') || testId.includes('reasoning');
|
|
517
|
+
const matches =
|
|
518
|
+
verifiedThinkingChrome &&
|
|
519
|
+
(isActiveLabel(node.textContent) || isActiveLabel(node.getAttribute('aria-label')));
|
|
520
|
+
if (matches) return ${strong};
|
|
521
|
+
}
|
|
522
|
+
// 5) A live progress bar (determinate or indeterminate) is active generation, even when no
|
|
523
|
+
// label or shimmer is present (some connector/tool phases surface only a progress bar).
|
|
524
|
+
const hasLiveProgress = (scope) => {
|
|
525
|
+
let nodes;
|
|
526
|
+
// Only genuine progress indicators — NOT generic [aria-valuenow] range widgets (sliders,
|
|
527
|
+
// spinbuttons), which are not liveness signals and would falsely veto completion forever.
|
|
528
|
+
try {
|
|
529
|
+
nodes = scope.querySelectorAll('progress, [role="progressbar"]');
|
|
530
|
+
} catch { return false; }
|
|
531
|
+
return Array.from(nodes).some((n) => {
|
|
532
|
+
if (!(n instanceof HTMLElement) || !isVisible(n)) return false;
|
|
533
|
+
if (n instanceof HTMLProgressElement) {
|
|
534
|
+
// Determinate <progress> is active only while it has not reached its max.
|
|
535
|
+
if (Number.isFinite(n.value) && Number.isFinite(n.max) && n.max > 0) return n.value < n.max;
|
|
536
|
+
return true; // indeterminate
|
|
537
|
+
}
|
|
538
|
+
const rawNow = n.getAttribute('aria-valuenow');
|
|
539
|
+
if (rawNow != null) {
|
|
540
|
+
const now = Number(rawNow);
|
|
541
|
+
const rawMax = n.getAttribute('aria-valuemax');
|
|
542
|
+
const max = rawMax != null && Number.isFinite(Number(rawMax)) ? Number(rawMax) : 100;
|
|
543
|
+
return Number.isFinite(now) ? now < max : true;
|
|
544
|
+
}
|
|
545
|
+
return true; // role=progressbar with no value -> indeterminate -> active
|
|
546
|
+
});
|
|
547
|
+
};
|
|
548
|
+
// Scoped to the CURRENT assistant turn, not the whole document: unrelated page UI can keep
|
|
549
|
+
// a visible progress bar mounted indefinitely (review P1), and a document-wide veto would
|
|
550
|
+
// then hold thinkingActive true until the watchdog timeout on a completed response. The
|
|
551
|
+
// 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
|
+
})();
|
|
555
|
+
const lastTurn = turns.length ? turns[turns.length - 1] : null;
|
|
556
|
+
if (
|
|
557
|
+
lastTurn instanceof HTMLElement &&
|
|
558
|
+
(hasBusyIndicator(lastTurn) || hasLiveProgress(lastTurn))
|
|
559
|
+
) return ${strong};
|
|
560
|
+
// 6) A visible thinking/reasoning sidecar panel (the connector/reasoning phase is often
|
|
561
|
+
// exposed ONLY through a right-side panel with no inline label). Match the existing
|
|
562
|
+
// thinking-monitor heuristic: a right-side panel that looks like thinking, or any such
|
|
563
|
+
// container that carries a live progress bar. Presence alone is NOT enough (a collapsed
|
|
564
|
+
// reasoning summary persists post-completion); require the thinking cue or live progress.
|
|
565
|
+
const looksLikeThinking = (node) => {
|
|
566
|
+
// Judge completion on the panel's VISIBLE text only: data-testid values like
|
|
567
|
+
// "reasoning-panel" would otherwise taint the completed-summary check, and a live trace
|
|
568
|
+
// is judged by its full rendered length, not by fragments of it.
|
|
569
|
+
const visible = norm(node.textContent) || norm(node.getAttribute?.('aria-label'));
|
|
570
|
+
if (isCompletedSummary(visible)) return false;
|
|
571
|
+
if (visible.includes('thought for ')) return true;
|
|
572
|
+
const label = norm([
|
|
573
|
+
node.textContent,
|
|
574
|
+
node.getAttribute?.('aria-label'),
|
|
575
|
+
node.getAttribute?.('data-testid'),
|
|
576
|
+
].filter(Boolean).join(' '));
|
|
577
|
+
return label.includes('thinking') || label.includes('reasoning') || label.includes('pro thinking');
|
|
578
|
+
};
|
|
579
|
+
let panels;
|
|
580
|
+
try {
|
|
581
|
+
panels = document.querySelectorAll(
|
|
582
|
+
'aside, [role="complementary"], [role="dialog"], [data-testid*="thinking"], [data-testid*="reasoning"], [class*="sidecar"], [class*="sidebar"]',
|
|
583
|
+
);
|
|
584
|
+
} catch { panels = []; }
|
|
585
|
+
for (const node of Array.from(panels)) {
|
|
586
|
+
if (!(node instanceof HTMLElement) || !isVisible(node)) continue;
|
|
587
|
+
const rect = node.getBoundingClientRect();
|
|
588
|
+
const rightSide = rect.left >= window.innerWidth * 0.35 && rect.width >= 180 && rect.height >= 120;
|
|
589
|
+
const panelLabel = norm([
|
|
590
|
+
node.getAttribute?.('aria-label'),
|
|
591
|
+
node.getAttribute?.('data-testid'),
|
|
592
|
+
node.className,
|
|
593
|
+
].filter(Boolean).join(' '));
|
|
594
|
+
const verifiedThinkingPanel =
|
|
595
|
+
panelLabel.includes('thinking') ||
|
|
596
|
+
panelLabel.includes('reasoning') ||
|
|
597
|
+
panelLabel.includes('sidecar');
|
|
598
|
+
if ((hasBusyIndicator(node) || hasLiveProgress(node)) && verifiedThinkingPanel) return ${strong};
|
|
599
|
+
// A text-only sidecar match is intentionally weak: completed turns can retain a mounted
|
|
600
|
+
// reasoning panel whose shape/text heuristics still look active. The terminal gate may
|
|
601
|
+
// override only this weak evidence after a stable, debounced finished-action bar.
|
|
602
|
+
if (rightSide && looksLikeThinking(node)) return ${weak};
|
|
603
|
+
}
|
|
604
|
+
return ${idle};
|
|
605
|
+
};`;
|
|
606
|
+
}
|
|
607
|
+
export function buildThinkingActivePredicateJs(fnName) {
|
|
608
|
+
return buildThinkingActivityPredicateJs(fnName, false);
|
|
609
|
+
}
|
|
610
|
+
export function buildThinkingActivityDetailsPredicateJs(fnName) {
|
|
611
|
+
return buildThinkingActivityPredicateJs(fnName, true);
|
|
612
|
+
}
|
|
613
|
+
export async function readThinkingActivity(Runtime) {
|
|
614
|
+
try {
|
|
615
|
+
const { result } = await Runtime.evaluate({
|
|
616
|
+
expression: `(() => {
|
|
617
|
+
${buildThinkingActivityDetailsPredicateJs("readThinkingActivity")}
|
|
618
|
+
return readThinkingActivity();
|
|
619
|
+
})()`,
|
|
620
|
+
returnByValue: true,
|
|
621
|
+
});
|
|
622
|
+
const value = result?.value;
|
|
623
|
+
return { active: Boolean(value?.active), strong: Boolean(value?.strong) };
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return { active: false, strong: false };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
export async function isThinkingActive(Runtime) {
|
|
630
|
+
return (await readThinkingActivity(Runtime)).active;
|
|
631
|
+
}
|
|
407
632
|
export const startThinkingStatusMonitorForTest = startThinkingStatusMonitor;
|
|
408
633
|
export const readThinkingStatusForTest = readThinkingStatus;
|
|
409
634
|
export const buildThinkingStatusExpressionForTest = buildThinkingStatusExpression;
|
|
635
|
+
export const buildThinkingActivePredicateJsForTest = buildThinkingActivePredicateJs;
|
|
636
|
+
export const buildThinkingActivityDetailsPredicateJsForTest = buildThinkingActivityDetailsPredicateJs;
|
|
637
|
+
export { ACTIVE_THINKING_LABELS };
|
|
@@ -124,6 +124,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
124
124
|
const modelButtonLiteral = JSON.stringify(MODEL_BUTTON_SELECTOR);
|
|
125
125
|
const targetLevelLiteral = JSON.stringify(level.toLowerCase());
|
|
126
126
|
const targetModelKindLiteral = JSON.stringify(inferThinkingTargetModelKind(desiredModel));
|
|
127
|
+
const targetIsGpt56ModelLiteral = JSON.stringify(/(?:^|[^0-9])5[._ -]6(?:[^0-9]|$)/i.test(desiredModel ?? ""));
|
|
127
128
|
return `(async () => {
|
|
128
129
|
${buildClickDispatcher()}
|
|
129
130
|
|
|
@@ -132,13 +133,14 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
132
133
|
const MODEL_BUTTON_SELECTOR = ${modelButtonLiteral};
|
|
133
134
|
const TARGET_LEVEL = ${targetLevelLiteral};
|
|
134
135
|
const TARGET_MODEL_KIND = ${targetModelKindLiteral};
|
|
136
|
+
const TARGET_IS_GPT56_MODEL = ${targetIsGpt56ModelLiteral};
|
|
135
137
|
|
|
136
138
|
// Bilingual matchers: English level token + observed Chinese variants.
|
|
137
139
|
const LEVEL_TOKENS = {
|
|
138
|
-
light: ['light', 'instant', '轻'],
|
|
139
|
-
standard: ['standard', 'medium', '标准'],
|
|
140
|
-
extended: ['extended', 'high', '扩展', '深度', '加强'],
|
|
141
|
-
heavy: ['heavy', 'extra high', '重度', '加重', '
|
|
140
|
+
light: ['light', 'instant', '轻', '极速'],
|
|
141
|
+
standard: ['standard', 'medium', '标准', '中'],
|
|
142
|
+
extended: ['extended', 'high', '扩展', '深度', '加强', '高'],
|
|
143
|
+
heavy: ['heavy', 'extra high', '重度', '加重', '极高'],
|
|
142
144
|
};
|
|
143
145
|
const targetTokens = LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
|
|
144
146
|
|
|
@@ -166,6 +168,13 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
166
168
|
if (!token) return false;
|
|
167
169
|
if (token === 'high') return hasToken(t, 'high') && !hasToken(t, 'extra');
|
|
168
170
|
if (token === 'extra high') return hasToken(t, 'extra') && hasToken(t, 'high');
|
|
171
|
+
if (token === '极速') {
|
|
172
|
+
const suffix = t.slice(token.length);
|
|
173
|
+
return t === token || hasToken(t, token) || /^[0-9]/.test(suffix);
|
|
174
|
+
}
|
|
175
|
+
if (['中', '高', '极高'].includes(token)) {
|
|
176
|
+
return t === token || hasToken(t, token);
|
|
177
|
+
}
|
|
169
178
|
return t === token || hasToken(t, token) || t.includes(token);
|
|
170
179
|
});
|
|
171
180
|
};
|
|
@@ -334,6 +343,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
334
343
|
if (menu?.getAttribute?.('data-testid') === 'composer-intelligence-picker-content') {
|
|
335
344
|
return true;
|
|
336
345
|
}
|
|
346
|
+
if (menu?.querySelector?.(INTELLIGENCE_MENU_SELECTOR)) {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
337
349
|
const label = menu?.querySelector?.('.__menu-label, [class*="menu-label"]');
|
|
338
350
|
return normalize(label?.textContent ?? '').includes('intelligence');
|
|
339
351
|
};
|
|
@@ -347,6 +359,21 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
347
359
|
const items = Array.from(menu.querySelectorAll(MENU_ITEM_SELECTOR));
|
|
348
360
|
const modelKind = modelKindOverride || effectiveTargetModelKind();
|
|
349
361
|
if (modelKind === 'pro') {
|
|
362
|
+
// GPT-5.6's unified Intelligence picker exposes Pro as the highest
|
|
363
|
+
// effort radio directly. It no longer has a nested "Pro Extended"
|
|
364
|
+
// row, so preserve the legacy request semantics by selecting Pro.
|
|
365
|
+
if (
|
|
366
|
+
TARGET_LEVEL === 'extended' &&
|
|
367
|
+
isIntelligenceEffortMenu(menu) &&
|
|
368
|
+
!document.querySelector(PRO_EFFORT_TRIGGER_SELECTOR)
|
|
369
|
+
) {
|
|
370
|
+
for (const item of items) {
|
|
371
|
+
const itemText = normalize(
|
|
372
|
+
(item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
|
|
373
|
+
);
|
|
374
|
+
if (itemText === 'pro') return item;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
350
377
|
for (const item of items) {
|
|
351
378
|
const itemText = normalize(
|
|
352
379
|
(item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
|
|
@@ -367,7 +394,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
367
394
|
const itemText = normalize(
|
|
368
395
|
(item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
|
|
369
396
|
);
|
|
370
|
-
if (modelKind
|
|
397
|
+
if (modelKind !== 'pro' && hasToken(itemText, 'pro')) {
|
|
371
398
|
continue;
|
|
372
399
|
}
|
|
373
400
|
if (
|
|
@@ -377,6 +404,16 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
377
404
|
return item;
|
|
378
405
|
}
|
|
379
406
|
}
|
|
407
|
+
if (TARGET_LEVEL === 'heavy') {
|
|
408
|
+
// Older Chinese layouts used bare 高 for the highest effort. Keep it
|
|
409
|
+
// only as a second-pass exact fallback so a current 高 row can never
|
|
410
|
+
// win before the primary 极高 row.
|
|
411
|
+
for (const item of items) {
|
|
412
|
+
const itemText = normalize(item.textContent ?? '');
|
|
413
|
+
const ariaLabel = normalize(item.getAttribute?.('aria-label') ?? '');
|
|
414
|
+
if (itemText === '高' || ariaLabel === '高') return item;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
380
417
|
return null;
|
|
381
418
|
};
|
|
382
419
|
const countEffortLevels = (menu) => {
|
|
@@ -390,6 +427,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
390
427
|
const isEffortMenu = (menu) => {
|
|
391
428
|
if (!isVisible(menu)) return false;
|
|
392
429
|
if (menu.getAttribute?.('data-testid') === 'composer-intelligence-picker-content') return true;
|
|
430
|
+
if (menu.querySelector?.(INTELLIGENCE_MENU_SELECTOR)) return true;
|
|
393
431
|
const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
|
|
394
432
|
const labelText = normalize(label?.textContent ?? '');
|
|
395
433
|
return (
|
|
@@ -450,8 +488,16 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
450
488
|
}
|
|
451
489
|
return null;
|
|
452
490
|
};
|
|
491
|
+
const freshComposerTrigger = (trigger) => {
|
|
492
|
+
if (!trigger?.matches?.('button.__composer-pill')) return null;
|
|
493
|
+
// React can replace the composer pill after an effort click. Keep using
|
|
494
|
+
// the captured node while it is live, but re-query once it is detached so
|
|
495
|
+
// verification does not read its stale pre-click label.
|
|
496
|
+
if (trigger.isConnected !== false) return trigger;
|
|
497
|
+
return findComposerEffortPill() || findModelButton() || trigger;
|
|
498
|
+
};
|
|
453
499
|
const currentProEffortPillMatchesTarget = (trigger, modelKindOverride = null) => {
|
|
454
|
-
const button = trigger
|
|
500
|
+
const button = freshComposerTrigger(trigger) || findModelButton();
|
|
455
501
|
if ((modelKindOverride || TARGET_MODEL_KIND || modelKindFromNode(button)) !== 'pro') {
|
|
456
502
|
return false;
|
|
457
503
|
}
|
|
@@ -466,7 +512,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
466
512
|
};
|
|
467
513
|
const currentEffortPillMatchesTarget = (trigger, modelKindOverride = null) => {
|
|
468
514
|
if (currentProEffortPillMatchesTarget(trigger, modelKindOverride)) return true;
|
|
469
|
-
const button = trigger
|
|
515
|
+
const button = freshComposerTrigger(trigger) || findModelButton();
|
|
470
516
|
if ((modelKindOverride || TARGET_MODEL_KIND || modelKindFromNode(button)) === 'pro') {
|
|
471
517
|
return false;
|
|
472
518
|
}
|
|
@@ -499,8 +545,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
499
545
|
return { status: 'switched', label };
|
|
500
546
|
}
|
|
501
547
|
|
|
502
|
-
|
|
503
|
-
|
|
548
|
+
const reopenTrigger = freshComposerTrigger(trigger) || trigger;
|
|
549
|
+
if (!refreshed && reopenTrigger?.getAttribute?.('aria-expanded') !== 'true') {
|
|
550
|
+
dispatchClickSequence(reopenTrigger);
|
|
504
551
|
await sleep(INITIAL_WAIT_MS);
|
|
505
552
|
}
|
|
506
553
|
const deadline = performance.now() + 2000;
|
|
@@ -556,6 +603,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
556
603
|
];
|
|
557
604
|
const findComposerEffortPill = () => {
|
|
558
605
|
const seen = new Set();
|
|
606
|
+
let gpt56Fallback = null;
|
|
559
607
|
for (const selector of COMPOSER_EFFORT_PILL_SELECTORS) {
|
|
560
608
|
for (const button of document.querySelectorAll(selector)) {
|
|
561
609
|
if (seen.has(button) || !isVisible(button)) continue;
|
|
@@ -574,9 +622,16 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
574
622
|
) {
|
|
575
623
|
return button;
|
|
576
624
|
}
|
|
625
|
+
if (
|
|
626
|
+
TARGET_IS_GPT56_MODEL &&
|
|
627
|
+
button.matches?.('button.__composer-pill') &&
|
|
628
|
+
normalize(button.textContent ?? '') === 'pro'
|
|
629
|
+
) {
|
|
630
|
+
gpt56Fallback ||= button;
|
|
631
|
+
}
|
|
577
632
|
}
|
|
578
633
|
}
|
|
579
|
-
return
|
|
634
|
+
return gpt56Fallback;
|
|
580
635
|
};
|
|
581
636
|
let composerEffortPill = findComposerEffortPill();
|
|
582
637
|
let modelBtn = findModelButton();
|
|
@@ -634,7 +689,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
634
689
|
}
|
|
635
690
|
if (composerEffortPill) {
|
|
636
691
|
if (attemptedModelButton && attemptedModelButton !== composerEffortPill) closeOpenMenus();
|
|
637
|
-
const composerModelKind =
|
|
692
|
+
const composerModelKind =
|
|
693
|
+
TARGET_MODEL_KIND ||
|
|
694
|
+
(TARGET_IS_GPT56_MODEL ? 'versioned' : modelKindFromNode(composerEffortPill));
|
|
638
695
|
if (composerEffortPill.getAttribute?.('aria-expanded') !== 'true') {
|
|
639
696
|
dispatchClickSequence(composerEffortPill);
|
|
640
697
|
await sleep(INITIAL_WAIT_MS);
|
|
@@ -2,18 +2,15 @@ import { rm } from "node:fs/promises";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import net from "node:net";
|
|
5
|
-
import { execFile } from "node:child_process";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
5
|
import CDP from "chrome-remote-interface";
|
|
8
6
|
import { launch, Launcher } from "chrome-launcher";
|
|
9
7
|
import { cleanupStaleProfileState } from "./profileState.js";
|
|
10
8
|
import { delay } from "./utils.js";
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
12
9
|
export async function launchChrome(config, userDataDir, logger) {
|
|
13
10
|
const connectHost = resolveRemoteDebugHost();
|
|
14
11
|
const debugBindAddress = connectHost && connectHost !== "127.0.0.1" ? "0.0.0.0" : connectHost;
|
|
15
12
|
const debugPort = config.debugPort ?? parseDebugPortEnv();
|
|
16
|
-
const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress);
|
|
13
|
+
const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress, config.hideWindow ?? false);
|
|
17
14
|
const usePatchedLauncher = Boolean(connectHost && connectHost !== "127.0.0.1");
|
|
18
15
|
// copy-profile reuses a copied signed-in profile whose cookies are
|
|
19
16
|
// Keychain-encrypted, so it must launch with the real Keychain (not mocked):
|
|
@@ -46,6 +43,24 @@ export async function launchChrome(config, userDataDir, logger) {
|
|
|
46
43
|
logger(`Launched Chrome${pidLabel} on port ${launcher.port}${hostLabel}`);
|
|
47
44
|
return Object.assign(launcher, { host: connectHost ?? "127.0.0.1" });
|
|
48
45
|
}
|
|
46
|
+
export async function positionChromeWindowOffscreen(client, logger) {
|
|
47
|
+
if (process.platform !== "darwin") {
|
|
48
|
+
logger("Window hiding is only supported on macOS");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const { windowId } = await client.Browser.getWindowForTarget();
|
|
53
|
+
await client.Browser.setWindowBounds({
|
|
54
|
+
windowId,
|
|
55
|
+
bounds: { left: -32_000, top: -32_000, windowState: "normal" },
|
|
56
|
+
});
|
|
57
|
+
logger("Chrome window positioned off-screen");
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
logger(`Failed to position Chrome window off-screen: ${message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
49
64
|
export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logger, opts) {
|
|
50
65
|
const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
|
|
51
66
|
let handling;
|
|
@@ -110,29 +125,6 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
|
|
|
110
125
|
}
|
|
111
126
|
};
|
|
112
127
|
}
|
|
113
|
-
export async function hideChromeWindow(chrome, logger) {
|
|
114
|
-
if (process.platform !== "darwin") {
|
|
115
|
-
logger("Window hiding is only supported on macOS");
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (!chrome.pid) {
|
|
119
|
-
logger("Unable to hide window: missing Chrome PID");
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const script = `tell application "System Events"
|
|
123
|
-
try
|
|
124
|
-
set visible of (first process whose unix id is ${chrome.pid}) to false
|
|
125
|
-
end try
|
|
126
|
-
end tell`;
|
|
127
|
-
try {
|
|
128
|
-
await execFileAsync("osascript", ["-e", script]);
|
|
129
|
-
logger("Chrome window hidden (Cmd-H)");
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
-
logger(`Failed to hide Chrome window: ${message}`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
128
|
export async function connectToChrome(port, logger, host) {
|
|
137
129
|
const client = await CDP({ port, host });
|
|
138
130
|
logger("Connected to Chrome DevTools protocol");
|
|
@@ -459,7 +451,7 @@ function isBlankPageTarget(target) {
|
|
|
459
451
|
const url = (target.url ?? "").trim().toLowerCase();
|
|
460
452
|
return url === "about:blank" || url === "chrome://newtab/" || url === "chrome://new-tab-page/";
|
|
461
453
|
}
|
|
462
|
-
function buildChromeFlags(headless, debugBindAddress) {
|
|
454
|
+
function buildChromeFlags(headless, debugBindAddress, hideWindow = false) {
|
|
463
455
|
const flags = [
|
|
464
456
|
"--disable-background-networking",
|
|
465
457
|
"--disable-background-timer-throttling",
|
|
@@ -489,8 +481,17 @@ function buildChromeFlags(headless, debugBindAddress) {
|
|
|
489
481
|
if (headless) {
|
|
490
482
|
flags.push("--headless=new");
|
|
491
483
|
}
|
|
484
|
+
else if (hideWindow && process.platform === "darwin") {
|
|
485
|
+
// Cmd-H stops macOS Chrome from compositing the page, which can swallow
|
|
486
|
+
// trusted CDP clicks and retain the prompt as a draft. Keeping the window
|
|
487
|
+
// off-screen avoids desktop disruption while preserving normal rendering.
|
|
488
|
+
flags.push("--window-position=-32000,-32000");
|
|
489
|
+
}
|
|
492
490
|
return flags;
|
|
493
491
|
}
|
|
492
|
+
export function buildChromeFlagsForTest(headless, debugBindAddress, hideWindow = false) {
|
|
493
|
+
return buildChromeFlags(headless, debugBindAddress, hideWindow);
|
|
494
|
+
}
|
|
494
495
|
function resolveChromeLaunchOptions(chromeFlags, usingCopiedProfile) {
|
|
495
496
|
if (!usingCopiedProfile) {
|
|
496
497
|
return { chromeFlags, ignoreDefaultFlags: false };
|
|
@@ -61,6 +61,7 @@ export function resolveBrowserConfig(config) {
|
|
|
61
61
|
const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
|
|
62
62
|
const envAllowCookieErrors = (process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? "").trim().toLowerCase() === "true" ||
|
|
63
63
|
(process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? "").trim() === "1";
|
|
64
|
+
const envMaxConcurrentTabs = parseMaxConcurrentTabs(process.env.ORACLE_BROWSER_MAX_CONCURRENT_TABS);
|
|
64
65
|
const rawUrl = config?.chatgptUrl ?? config?.url ?? DEFAULT_BROWSER_CONFIG.url;
|
|
65
66
|
const normalizedUrl = normalizeChatgptUrl(rawUrl ?? DEFAULT_BROWSER_CONFIG.url, DEFAULT_BROWSER_CONFIG.url);
|
|
66
67
|
const desiredModel = config?.desiredModel ?? DEFAULT_BROWSER_CONFIG.desiredModel ?? DEFAULT_MODEL_TARGET;
|
|
@@ -87,7 +88,7 @@ export function resolveBrowserConfig(config) {
|
|
|
87
88
|
assistantRecheckTimeoutMs: config?.assistantRecheckTimeoutMs ?? DEFAULT_BROWSER_CONFIG.assistantRecheckTimeoutMs,
|
|
88
89
|
reuseChromeWaitMs: config?.reuseChromeWaitMs ?? DEFAULT_BROWSER_CONFIG.reuseChromeWaitMs,
|
|
89
90
|
profileLockTimeoutMs: config?.profileLockTimeoutMs ?? DEFAULT_BROWSER_CONFIG.profileLockTimeoutMs,
|
|
90
|
-
maxConcurrentTabs: normalizeMaxConcurrentTabs(config?.maxConcurrentTabs ?? DEFAULT_BROWSER_CONFIG.maxConcurrentTabs),
|
|
91
|
+
maxConcurrentTabs: normalizeMaxConcurrentTabs(config?.maxConcurrentTabs ?? envMaxConcurrentTabs ?? DEFAULT_BROWSER_CONFIG.maxConcurrentTabs),
|
|
91
92
|
autoReattachDelayMs: config?.autoReattachDelayMs ?? DEFAULT_BROWSER_CONFIG.autoReattachDelayMs,
|
|
92
93
|
autoReattachIntervalMs: config?.autoReattachIntervalMs ?? DEFAULT_BROWSER_CONFIG.autoReattachIntervalMs,
|
|
93
94
|
autoReattachTimeoutMs: config?.autoReattachTimeoutMs ?? DEFAULT_BROWSER_CONFIG.autoReattachTimeoutMs,
|
|
@@ -134,6 +135,18 @@ function parseDebugPort(raw) {
|
|
|
134
135
|
}
|
|
135
136
|
return value;
|
|
136
137
|
}
|
|
138
|
+
function parseMaxConcurrentTabs(raw) {
|
|
139
|
+
if (!raw)
|
|
140
|
+
return null;
|
|
141
|
+
const trimmed = raw.trim();
|
|
142
|
+
if (!/^\d+$/.test(trimmed))
|
|
143
|
+
return null;
|
|
144
|
+
const value = Number(trimmed);
|
|
145
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
137
150
|
function resolveManualLoginProfileDir(...candidates) {
|
|
138
151
|
for (const candidate of candidates) {
|
|
139
152
|
const profileDir = candidate?.trim();
|
|
@@ -64,10 +64,14 @@ export const UPLOAD_STATUS_SELECTORS = [
|
|
|
64
64
|
'[aria-live="assertive"]',
|
|
65
65
|
];
|
|
66
66
|
export const STOP_BUTTON_SELECTOR = '[data-testid="stop-button"]';
|
|
67
|
+
// The aria-label fallback exists for data-testid drift, but a document-wide match makes ANY
|
|
68
|
+
// visible "stop" control (read-aloud, voice/dictation) read as "still generating", which blocks
|
|
69
|
+
// completion until the response timeout. Scope it to the composer form and exclude the known
|
|
70
|
+
// non-generation stop controls that legitimately live there.
|
|
67
71
|
export const STOP_BUTTON_SELECTORS = [
|
|
68
72
|
STOP_BUTTON_SELECTOR,
|
|
69
73
|
'[data-testid="composer-stop-button"]',
|
|
70
|
-
'button[aria-label*="stop" i]',
|
|
74
|
+
'form button[aria-label*="stop" i]:not([aria-label*="dictat" i]):not([aria-label*="voice" i]):not([aria-label*="read" i])',
|
|
71
75
|
];
|
|
72
76
|
export const SEND_BUTTON_SELECTORS = [
|
|
73
77
|
'button[data-testid="send-button"]',
|
|
@@ -45,13 +45,13 @@ export function describeBrowserControlPlan(config = {}) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
if (config.hideWindow) {
|
|
48
|
-
guidance.push("
|
|
48
|
+
guidance.push("On macOS, Oracle launches Chrome off-screen while keeping the page rendered.");
|
|
49
49
|
guidance.push("For the calmest shared-desktop flow, prefer --browser-attach-running or --remote-chrome.");
|
|
50
50
|
return {
|
|
51
51
|
mode: "hidden-window",
|
|
52
52
|
launchesChrome: true,
|
|
53
53
|
mayFocusWindow: true,
|
|
54
|
-
summary: "launch Chrome
|
|
54
|
+
summary: "launch Chrome in hidden-window mode",
|
|
55
55
|
guidance,
|
|
56
56
|
};
|
|
57
57
|
}
|