@steipete/oracle 0.9.0 → 0.11.0

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