@steipete/oracle 0.7.1 → 0.7.3

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.
Files changed (30) hide show
  1. package/dist/src/browser/actions/assistantResponse.js +53 -33
  2. package/dist/src/browser/actions/attachments.js +276 -133
  3. package/dist/src/browser/actions/modelSelection.js +33 -2
  4. package/dist/src/browser/actions/promptComposer.js +38 -45
  5. package/dist/src/browser/chromeLifecycle.js +2 -0
  6. package/dist/src/browser/config.js +7 -2
  7. package/dist/src/browser/index.js +12 -2
  8. package/dist/src/browser/pageActions.js +1 -1
  9. package/dist/src/browser/reattach.js +192 -17
  10. package/dist/src/browser/utils.js +10 -0
  11. package/dist/src/browserMode.js +1 -1
  12. package/dist/src/cli/browserConfig.js +11 -6
  13. package/dist/src/cli/notifier.js +8 -2
  14. package/dist/src/cli/oscUtils.js +1 -19
  15. package/dist/src/cli/sessionDisplay.js +6 -3
  16. package/dist/src/cli/sessionTable.js +5 -1
  17. package/dist/src/oracle/files.js +8 -1
  18. package/dist/src/oracle/modelResolver.js +11 -4
  19. package/dist/src/oracle/multiModelRunner.js +3 -14
  20. package/dist/src/oracle/oscProgress.js +12 -61
  21. package/dist/src/oracle/run.js +62 -34
  22. package/dist/src/sessionManager.js +91 -2
  23. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  24. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  25. package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
  26. package/package.json +43 -26
  27. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  28. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  29. package/vendor/oracle-notifier/README.md +24 -0
  30. package/vendor/oracle-notifier/build-notifier.sh +0 -0
@@ -4,6 +4,7 @@ import { logDomFailure, logConversationSnapshot, buildConversationDebugExpressio
4
4
  import { buildClickDispatcher } from './domEvents.js';
5
5
  const ASSISTANT_POLL_TIMEOUT_ERROR = 'assistant-response-watchdog-timeout';
6
6
  export async function waitForAssistantResponse(Runtime, timeoutMs, logger) {
7
+ const start = Date.now();
7
8
  logger('Waiting for ChatGPT response');
8
9
  const expression = buildResponseObserverExpression(timeoutMs);
9
10
  const evaluationPromise = Runtime.evaluate({ expression, awaitPromise: true, returnByValue: true });
@@ -61,7 +62,24 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger) {
61
62
  throw new Error('Unable to capture assistant response');
62
63
  }
63
64
  const refreshed = await refreshAssistantSnapshot(Runtime, parsed, logger);
64
- return refreshed ?? parsed;
65
+ const candidate = refreshed ?? parsed;
66
+ // The evaluation path can race ahead of completion. If ChatGPT is still streaming, wait for the watchdog poller.
67
+ const elapsedMs = Date.now() - start;
68
+ const remainingMs = Math.max(0, timeoutMs - elapsedMs);
69
+ if (remainingMs > 0) {
70
+ const [stopVisible, completionVisible] = await Promise.all([
71
+ isStopButtonVisible(Runtime),
72
+ isCompletionVisible(Runtime),
73
+ ]);
74
+ if (stopVisible && !completionVisible) {
75
+ logger('Assistant still generating; waiting for completion');
76
+ const completed = await pollAssistantCompletion(Runtime, remainingMs);
77
+ if (completed) {
78
+ return completed;
79
+ }
80
+ }
81
+ }
82
+ return candidate;
65
83
  }
66
84
  export async function readAssistantSnapshot(Runtime) {
67
85
  const { result } = await Runtime.evaluate({ expression: buildAssistantSnapshotExpression(), returnByValue: true });
@@ -118,11 +136,14 @@ async function parseAssistantEvaluationResult(Runtime, evaluation, timeoutMs, lo
118
136
  const messageId = typeof result.value.messageId === 'string'
119
137
  ? (result.value.messageId ?? undefined)
120
138
  : undefined;
121
- return {
122
- text: cleanAssistantText(String(result.value.text ?? '')),
123
- html,
124
- meta: { turnId, messageId },
125
- };
139
+ const text = cleanAssistantText(String(result.value.text ?? ''));
140
+ const normalized = text.toLowerCase();
141
+ if (normalized.includes('answer now') &&
142
+ (normalized.includes('pro thinking') || normalized.includes('chatgpt said'))) {
143
+ const recovered = await recoverAssistantResponse(Runtime, Math.min(timeoutMs, 10_000), logger);
144
+ return recovered ?? null;
145
+ }
146
+ return { text, html, meta: { turnId, messageId } };
126
147
  }
127
148
  const fallbackText = typeof result.value === 'string' ? cleanAssistantText(result.value) : '';
128
149
  if (!fallbackText) {
@@ -261,7 +282,7 @@ function normalizeAssistantSnapshot(snapshot) {
261
282
  }
262
283
  const normalized = text.toLowerCase();
263
284
  // "Pro thinking" often renders a placeholder turn containing an "Answer now" gate.
264
- // Treat it as incomplete so browser mode keeps waiting (and can click the gate).
285
+ // Treat it as incomplete so browser mode keeps waiting for the real assistant text.
265
286
  if (normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'))) {
266
287
  return null;
267
288
  }
@@ -303,7 +324,10 @@ function buildResponseObserverExpression(timeoutMs) {
303
324
  const CONVERSATION_SELECTOR = ${conversationLiteral};
304
325
  const ASSISTANT_SELECTOR = ${assistantLiteral};
305
326
  const settleDelayMs = 800;
306
- const ANSWER_NOW_LABEL = 'answer now';
327
+ const isAnswerNowPlaceholder = (snapshot) => {
328
+ const normalized = String(snapshot?.text ?? '').toLowerCase();
329
+ return normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'));
330
+ };
307
331
 
308
332
  // Helper to detect assistant turns - matches buildAssistantExtractor logic
309
333
  const isAssistantTurn = (node) => {
@@ -324,7 +348,8 @@ function buildResponseObserverExpression(timeoutMs) {
324
348
  const deadline = Date.now() + ${timeoutMs};
325
349
  let stopInterval = null;
326
350
  const observer = new MutationObserver(() => {
327
- const extracted = extractFromTurns();
351
+ const extractedRaw = extractFromTurns();
352
+ const extracted = extractedRaw && !isAnswerNowPlaceholder(extractedRaw) ? extractedRaw : null;
328
353
  if (extracted) {
329
354
  observer.disconnect();
330
355
  if (stopInterval) {
@@ -341,11 +366,6 @@ function buildResponseObserverExpression(timeoutMs) {
341
366
  });
342
367
  observer.observe(document.body, { childList: true, subtree: true, characterData: true });
343
368
  stopInterval = setInterval(() => {
344
- // Pro thinking can gate the response behind an "Answer now" button. Keep clicking it while present.
345
- const answerNow = Array.from(document.querySelectorAll('button,span')).find((el) => (el?.textContent || '').trim().toLowerCase() === ANSWER_NOW_LABEL);
346
- if (answerNow) {
347
- dispatchClickSequence(answerNow.closest('button') ?? answerNow);
348
- }
349
369
  const stop = document.querySelector(STOP_SELECTOR);
350
370
  if (!stop) {
351
371
  return;
@@ -387,28 +407,28 @@ function buildResponseObserverExpression(timeoutMs) {
387
407
  const waitForSettle = async (snapshot) => {
388
408
  const settleWindowMs = 5000;
389
409
  const settleIntervalMs = 400;
390
- const deadline = Date.now() + settleWindowMs;
391
- let latest = snapshot;
392
- let lastLength = snapshot?.text?.length ?? 0;
393
- while (Date.now() < deadline) {
394
- await new Promise((resolve) => setTimeout(resolve, settleIntervalMs));
395
- const refreshed = extractFromTurns();
396
- if (refreshed && (refreshed.text?.length ?? 0) >= lastLength) {
397
- latest = refreshed;
398
- lastLength = refreshed.text?.length ?? lastLength;
399
- }
400
- const stopVisible = Boolean(document.querySelector(STOP_SELECTOR));
401
- const answerNowVisible = Boolean(Array.from(document.querySelectorAll('button,span')).find((el) => (el?.textContent || '').trim().toLowerCase() === ANSWER_NOW_LABEL));
402
- const finishedVisible = isLastAssistantTurnFinished();
410
+ const deadline = Date.now() + settleWindowMs;
411
+ let latest = snapshot;
412
+ let lastLength = snapshot?.text?.length ?? 0;
413
+ while (Date.now() < deadline) {
414
+ await new Promise((resolve) => setTimeout(resolve, settleIntervalMs));
415
+ const refreshed = extractFromTurns();
416
+ if (refreshed && !isAnswerNowPlaceholder(refreshed) && (refreshed.text?.length ?? 0) >= lastLength) {
417
+ latest = refreshed;
418
+ lastLength = refreshed.text?.length ?? lastLength;
419
+ }
420
+ const stopVisible = Boolean(document.querySelector(STOP_SELECTOR));
421
+ const finishedVisible = isLastAssistantTurnFinished();
403
422
 
404
- if ((!stopVisible && !answerNowVisible) || finishedVisible) {
405
- break;
423
+ if (!stopVisible || finishedVisible) {
424
+ break;
425
+ }
406
426
  }
407
- }
408
- return latest ?? snapshot;
409
- };
427
+ return latest ?? snapshot;
428
+ };
410
429
 
411
- const extracted = extractFromTurns();
430
+ const extractedRaw = extractFromTurns();
431
+ const extracted = extractedRaw && !isAnswerNowPlaceholder(extractedRaw) ? extractedRaw : null;
412
432
  if (extracted) {
413
433
  return waitForSettle(extracted);
414
434
  }