@steipete/oracle 0.18.0 → 0.19.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 (159) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +65 -63
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +2 -1
  13. package/dist/src/browser/actions/modelSelection.js +13 -3
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +361 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +119 -16
  19. package/dist/src/browser/attachRunning.js +46 -2
  20. package/dist/src/browser/attachmentValidation.js +53 -0
  21. package/dist/src/browser/cancellation.js +105 -0
  22. package/dist/src/browser/chatgptImages.js +2 -0
  23. package/dist/src/browser/chatgptThrottle.js +104 -0
  24. package/dist/src/browser/chromeLifecycle.js +267 -43
  25. package/dist/src/browser/config.js +13 -0
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +444 -261
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/profileState.js +114 -9
  31. package/dist/src/browser/projectSourcesRunner.js +83 -15
  32. package/dist/src/browser/prompt.js +272 -113
  33. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  34. package/dist/src/browser/reattach.js +10 -3
  35. package/dist/src/browser/sessionRunner.js +71 -11
  36. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  37. package/dist/src/browser/utils.js +20 -2
  38. package/dist/src/cli/browserConfig.js +3 -1
  39. package/dist/src/cli/browserDefaults.js +10 -0
  40. package/dist/src/cli/browserTabs.js +12 -29
  41. package/dist/src/cli/detachedSession.js +69 -0
  42. package/dist/src/cli/dryRun.js +58 -48
  43. package/dist/src/cli/harvestIntegrity.js +140 -0
  44. package/dist/src/cli/serveBrowserConfig.js +24 -0
  45. package/dist/src/cli/sessionDisplay.js +9 -2
  46. package/dist/src/cli/sessionRunner.js +91 -7
  47. package/dist/src/config.js +1 -0
  48. package/dist/src/gemini-web/client.js +67 -21
  49. package/dist/src/gemini-web/executor.js +4 -0
  50. package/dist/src/gemini-web/http.js +15 -0
  51. package/dist/src/gemini-web/models.js +15 -11
  52. package/dist/src/mcp/server.js +11 -14
  53. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  54. package/dist/src/mcp/tools/consult.js +84 -22
  55. package/dist/src/mcp/tools/projectSources.js +4 -6
  56. package/dist/src/mcp/tools/sessionResources.js +1 -1
  57. package/dist/src/mcp/tools/sessions.js +2 -2
  58. package/dist/src/mcp/tools/wait.js +182 -0
  59. package/dist/src/mcp/types.js +7 -0
  60. package/dist/src/oracle/errors.js +11 -0
  61. package/dist/src/oracle/providerRoutePlan.js +3 -2
  62. package/dist/src/remote/client.js +70 -6
  63. package/dist/src/remote/health.js +6 -2
  64. package/dist/src/remote/runSlots.js +83 -0
  65. package/dist/src/remote/server.js +345 -71
  66. package/dist/src/sessionManager.js +36 -1
  67. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  68. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  69. package/package.json +25 -23
  70. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  71. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  72. package/dist/bin/oracle.js +0 -683
  73. package/dist/docs-site/.nojekyll +0 -0
  74. package/dist/docs-site/CNAME +0 -1
  75. package/dist/docs-site/RELEASING.html +0 -410
  76. package/dist/docs-site/agents.html +0 -374
  77. package/dist/docs-site/anthropic.html +0 -368
  78. package/dist/docs-site/bridge.html +0 -400
  79. package/dist/docs-site/browser-mode.html +0 -588
  80. package/dist/docs-site/chromium-forks.html +0 -347
  81. package/dist/docs-site/cli-reference.html +0 -346
  82. package/dist/docs-site/configuration.html +0 -436
  83. package/dist/docs-site/favicon.svg +0 -14
  84. package/dist/docs-site/followup.html +0 -368
  85. package/dist/docs-site/gemini.html +0 -379
  86. package/dist/docs-site/grok.html +0 -325
  87. package/dist/docs-site/index.html +0 -360
  88. package/dist/docs-site/install.html +0 -335
  89. package/dist/docs-site/linux.html +0 -321
  90. package/dist/docs-site/llms.txt +0 -43
  91. package/dist/docs-site/manual-tests.html +0 -595
  92. package/dist/docs-site/mcp.html +0 -368
  93. package/dist/docs-site/multimodel.html +0 -364
  94. package/dist/docs-site/mythical-pro-agents.html +0 -360
  95. package/dist/docs-site/notifier.html +0 -338
  96. package/dist/docs-site/openai-endpoints.html +0 -387
  97. package/dist/docs-site/openrouter.html +0 -344
  98. package/dist/docs-site/quickstart.html +0 -369
  99. package/dist/docs-site/refactor/ux.html +0 -532
  100. package/dist/docs-site/sessions.html +0 -388
  101. package/dist/docs-site/social-card.png +0 -0
  102. package/dist/docs-site/social-card.svg +0 -79
  103. package/dist/docs-site/spec.html +0 -363
  104. package/dist/docs-site/testing.html +0 -320
  105. package/dist/docs-site/tui-debug.html +0 -326
  106. package/dist/docs-site/windows-work.html +0 -323
  107. package/dist/docs-site/windows.html +0 -320
  108. package/dist/markdansi/types/index.js +0 -4
  109. package/dist/oracle/bin/oracle-cli.js +0 -472
  110. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  111. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  112. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  113. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  114. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  115. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  116. package/dist/oracle/src/browser/config.js +0 -33
  117. package/dist/oracle/src/browser/constants.js +0 -40
  118. package/dist/oracle/src/browser/cookies.js +0 -210
  119. package/dist/oracle/src/browser/domDebug.js +0 -36
  120. package/dist/oracle/src/browser/index.js +0 -331
  121. package/dist/oracle/src/browser/pageActions.js +0 -5
  122. package/dist/oracle/src/browser/prompt.js +0 -88
  123. package/dist/oracle/src/browser/promptSummary.js +0 -20
  124. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  125. package/dist/oracle/src/browser/types.js +0 -1
  126. package/dist/oracle/src/browser/utils.js +0 -62
  127. package/dist/oracle/src/browserMode.js +0 -1
  128. package/dist/oracle/src/cli/browserConfig.js +0 -44
  129. package/dist/oracle/src/cli/dryRun.js +0 -59
  130. package/dist/oracle/src/cli/engine.js +0 -17
  131. package/dist/oracle/src/cli/errorUtils.js +0 -9
  132. package/dist/oracle/src/cli/help.js +0 -70
  133. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  134. package/dist/oracle/src/cli/options.js +0 -103
  135. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  136. package/dist/oracle/src/cli/rootAlias.js +0 -30
  137. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  138. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  139. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  140. package/dist/oracle/src/heartbeat.js +0 -43
  141. package/dist/oracle/src/oracle/client.js +0 -48
  142. package/dist/oracle/src/oracle/config.js +0 -29
  143. package/dist/oracle/src/oracle/errors.js +0 -101
  144. package/dist/oracle/src/oracle/files.js +0 -220
  145. package/dist/oracle/src/oracle/format.js +0 -33
  146. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  147. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  148. package/dist/oracle/src/oracle/request.js +0 -48
  149. package/dist/oracle/src/oracle/run.js +0 -444
  150. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  151. package/dist/oracle/src/oracle/types.js +0 -1
  152. package/dist/oracle/src/oracle.js +0 -9
  153. package/dist/oracle/src/sessionManager.js +0 -205
  154. package/dist/oracle/src/version.js +0 -39
  155. package/dist/scripts/chrome/browser-tools.js +0 -295
  156. package/dist/src/browser/chromeCookies.js +0 -312
  157. package/dist/src/browser/keytarShim.js +0 -56
  158. package/dist/src/browser/profileSync.js +0 -141
  159. package/dist/src/browser/windowsCookies.js +0 -219
@@ -6,6 +6,7 @@ import { isKnownModel } from "../oracle/modelResolver.js";
6
6
  import { buildPromptMarkdown } from "../oracle/promptAssembly.js";
7
7
  import { buildAttachmentPlan } from "./policies.js";
8
8
  import { createStoredZip } from "./zipBundle.js";
9
+ import { buildAttachmentBasenameCollisionDetails, findAttachmentBasenameCollisions, formatAttachmentBasenameCollisionMessage, } from "./attachmentValidation.js";
9
10
  const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
10
11
  const MAX_BROWSER_ATTACHMENTS = 10;
11
12
  const MAX_BROWSER_ZIP_BUNDLE_BYTES = 128 * 1024 * 1024;
@@ -82,70 +83,197 @@ export function isRawUploadFile(filePath) {
82
83
  const ext = path.extname(filePath).toLowerCase();
83
84
  return MEDIA_EXTENSIONS.has(ext) || ARCHIVE_EXTENSIONS.has(ext);
84
85
  }
86
+ const GENERATED_BUNDLE_DIR_PREFIX = "oracle-browser-bundle-";
87
+ const pendingFallbackBundles = new WeakMap();
85
88
  function formatSectionsForBundle(sections, options = {}) {
86
89
  return formatFileSections(sections, {
87
90
  lineNumbers: options.lineNumbers ?? true,
88
91
  trailingNewline: true,
89
92
  });
90
93
  }
91
- function resolveBrowserBundleFormat(format, sources) {
94
+ function resolveBrowserBundleFormat(format, { hasRawUploadFiles }) {
92
95
  if (format !== "auto") {
93
96
  return format;
94
97
  }
95
- return sources.hasRawUploadFiles ? "zip" : "text";
98
+ return hasRawUploadFiles ? "zip" : "text";
96
99
  }
97
- function shouldWriteBrowserBundle(format, { attachmentCount, bundleRequested, textSourceCount, textPlanShouldBundle, }) {
98
- if (format === "zip") {
99
- return (textPlanShouldBundle ||
100
- (bundleRequested && attachmentCount > 0) ||
101
- attachmentCount > MAX_BROWSER_ATTACHMENTS);
100
+ function resolveBrowserBundleScope(format, { bundleRequested, rawAttachmentCount, textAttachmentCount, }) {
101
+ const attachmentCount = textAttachmentCount + rawAttachmentCount;
102
+ if (attachmentCount === 0) {
103
+ return "none";
104
+ }
105
+ if (format === "text") {
106
+ if (textAttachmentCount === 0) {
107
+ return "none";
108
+ }
109
+ const shouldBundleText = bundleRequested || textAttachmentCount > 1 || attachmentCount > MAX_BROWSER_ATTACHMENTS;
110
+ return shouldBundleText ? "text-only" : "none";
111
+ }
112
+ if (bundleRequested) {
113
+ return "all";
102
114
  }
103
- return textSourceCount > 0 && (textPlanShouldBundle || attachmentCount > MAX_BROWSER_ATTACHMENTS);
115
+ // Preserve native uploads for images, PDFs, archives, and other raw inputs.
116
+ // Multiple text/source files benefit from a real filesystem tree, so bundle
117
+ // those into one ZIP while leaving any native attachments alongside it.
118
+ if (textAttachmentCount > 1) {
119
+ return rawAttachmentCount + 1 <= MAX_BROWSER_ATTACHMENTS ? "text-only" : "all";
120
+ }
121
+ return attachmentCount > MAX_BROWSER_ATTACHMENTS ? "all" : "none";
122
+ }
123
+ function appendZipBundleInstruction(composerText, originalCount, bundlePath) {
124
+ const fileLabel = originalCount === 1 ? "file" : "files";
125
+ const instruction = [
126
+ `The attached \`${path.basename(bundlePath)}\` contains ${originalCount} selected ${fileLabel} with relative paths preserved.`,
127
+ "Extract it into a temporary directory, then inspect the resulting file tree with filesystem and search tools before answering.",
128
+ ].join(" ");
129
+ return [composerText, instruction].filter(Boolean).join("\n\n").trim();
130
+ }
131
+ function assertAttachmentCount(attachmentCount, format) {
132
+ if (attachmentCount <= MAX_BROWSER_ATTACHMENTS)
133
+ return;
134
+ throw new Error(`Browser upload has ${attachmentCount} attachments after applying bundle format "${format}". Use --browser-bundle-format auto or zip to stay within the ${MAX_BROWSER_ATTACHMENTS}-attachment limit.`);
104
135
  }
105
- function assertAttachmentCount(attachments, format) {
106
- if (attachments.length <= MAX_BROWSER_ATTACHMENTS)
136
+ function assertUniqueAttachmentBasenames(attachments, cwd) {
137
+ const collisions = findAttachmentBasenameCollisions(attachments);
138
+ if (collisions.length === 0)
107
139
  return;
108
- throw new Error(`Browser upload has ${attachments.length} attachments after applying bundle format "${format}". Use --browser-bundle-format auto or zip to stay within the ${MAX_BROWSER_ATTACHMENTS}-attachment limit.`);
140
+ const details = buildAttachmentBasenameCollisionDetails(collisions, (attachment) => path.isAbsolute(attachment.path) ? attachment.path : path.resolve(cwd, attachment.path));
141
+ throw new FileValidationError(formatAttachmentBasenameCollisionMessage("Browser upload", details.collisions), { ...details });
142
+ }
143
+ async function applyWrittenBundle({ sections, sources, format, scope, rawUploadAttachments, composerText, bundleParentDir, }) {
144
+ const nativeAttachments = scope === "text-only" ? rawUploadAttachments : [];
145
+ assertAttachmentCount(1 + nativeAttachments.length, format);
146
+ assertUniqueAttachmentBasenames(nativeAttachments, process.cwd());
147
+ const reservedNames = new Set(nativeAttachments.map((a) => path.basename(a.path).toLowerCase()));
148
+ let bundleName = `attachments-bundle.${format === "zip" ? "zip" : "txt"}`;
149
+ for (let suffix = 2; reservedNames.has(bundleName.toLowerCase()); suffix += 1) {
150
+ bundleName = `attachments-bundle-${suffix}.${format === "zip" ? "zip" : "txt"}`;
151
+ }
152
+ const writtenBundle = await writeBrowserBundle(sections, sources, format, bundleName, bundleParentDir);
153
+ const attachments = [writtenBundle.attachment];
154
+ if (scope === "text-only") {
155
+ attachments.push(...rawUploadAttachments);
156
+ }
157
+ assertAttachmentCount(attachments.length, format);
158
+ return {
159
+ attachments,
160
+ bundled: writtenBundle.metadata,
161
+ composerText: format === "zip"
162
+ ? appendZipBundleInstruction(composerText, writtenBundle.metadata.originalCount, writtenBundle.metadata.bundlePath)
163
+ : composerText,
164
+ tokenEstimateText: writtenBundle.tokenEstimateText,
165
+ };
109
166
  }
110
- async function writeBrowserBundle(sections, sources, format) {
111
- const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
112
- const tokenEstimateText = formatSectionsForBundle(sections, {
113
- lineNumbers: format === "text",
167
+ function generatedBundleDirectory(attachment) {
168
+ if (!attachment.generatedBundle)
169
+ return null;
170
+ const dir = path.dirname(attachment.path);
171
+ return path.basename(dir).startsWith(GENERATED_BUNDLE_DIR_PREFIX) ? dir : null;
172
+ }
173
+ export function listGeneratedBrowserBundleDirs(artifacts) {
174
+ const dirs = new Set();
175
+ for (const attachment of [...artifacts.attachments, ...(artifacts.fallback?.attachments ?? [])]) {
176
+ const dir = generatedBundleDirectory(attachment);
177
+ if (dir)
178
+ dirs.add(dir);
179
+ }
180
+ return [...dirs];
181
+ }
182
+ export async function cleanupGeneratedBrowserBundles(artifacts) {
183
+ await Promise.all(listGeneratedBrowserBundleDirs(artifacts).map((dir) => fs.rm(dir, { recursive: true, force: true })));
184
+ }
185
+ export async function materializeBrowserFallback(artifacts) {
186
+ const pending = pendingFallbackBundles.get(artifacts);
187
+ if (!pending || !artifacts.fallback) {
188
+ return artifacts.fallback ?? null;
189
+ }
190
+ pendingFallbackBundles.delete(artifacts);
191
+ const applied = await applyWrittenBundle({
192
+ sections: pending.sections,
193
+ sources: pending.scope === "all" ? pending.allSources : pending.textSources,
194
+ format: pending.format,
195
+ scope: pending.scope,
196
+ rawUploadAttachments: pending.rawUploadAttachments,
197
+ composerText: artifacts.fallback.composerText,
114
198
  });
199
+ artifacts.fallback.composerText = applied.composerText;
200
+ artifacts.fallback.attachments = applied.attachments;
201
+ artifacts.fallback.bundled = applied.bundled;
202
+ artifacts.fallback.pendingBundle = null;
203
+ return artifacts.fallback;
204
+ }
205
+ export async function materializeStagedFallbackBundle({ composerText, attachments, format, scope, bundleParentDir, }) {
206
+ const textAttachments = attachments.filter((attachment) => !isRawUploadFile(attachment.path));
207
+ const rawAttachments = attachments.filter((attachment) => isRawUploadFile(attachment.path));
208
+ const textSources = textAttachments.map((attachment) => ({
209
+ absolutePath: attachment.path,
210
+ displayPath: attachment.displayPath,
211
+ sizeBytes: attachment.sizeBytes ?? 0,
212
+ }));
213
+ const allSources = [
214
+ ...textSources,
215
+ ...rawAttachments.map((attachment) => ({
216
+ absolutePath: attachment.path,
217
+ displayPath: attachment.displayPath,
218
+ sizeBytes: attachment.sizeBytes ?? 0,
219
+ })),
220
+ ];
221
+ const sections = await Promise.all(textAttachments.map(async (attachment, index) => {
222
+ const content = await fs.readFile(attachment.path, "utf8");
223
+ return {
224
+ index: index + 1,
225
+ absolutePath: attachment.path,
226
+ displayPath: attachment.displayPath,
227
+ content,
228
+ sectionText: "",
229
+ };
230
+ }));
231
+ return applyWrittenBundle({
232
+ sections,
233
+ sources: scope === "all" ? allSources : textSources,
234
+ format,
235
+ scope,
236
+ rawUploadAttachments: rawAttachments,
237
+ composerText,
238
+ bundleParentDir,
239
+ });
240
+ }
241
+ async function writeBrowserBundle(sections, sources, format, bundleName, bundleParentDir = os.tmpdir()) {
242
+ const tokenEstimateText = formatSectionsForBundle(sections, { lineNumbers: format === "text" });
243
+ let content = tokenEstimateText;
115
244
  if (format === "zip") {
116
245
  const totalSourceBytes = sources.reduce((total, source) => total + source.sizeBytes, 0);
117
246
  if (totalSourceBytes > MAX_BROWSER_ZIP_BUNDLE_BYTES) {
118
247
  throw new Error(`Browser ZIP bundle inputs exceed the ${MAX_BROWSER_ZIP_BUNDLE_BYTES}-byte in-memory limit.`);
119
248
  }
120
- const bundlePath = path.join(bundleDir, "attachments-bundle.zip");
121
- const buffer = createStoredZip(await Promise.all(sources.map(async (source) => ({
249
+ content = createStoredZip(await Promise.all(sources.map(async (source) => ({
122
250
  path: source.displayPath,
123
251
  content: await fs.readFile(source.absolutePath),
124
252
  }))));
125
- await fs.writeFile(bundlePath, buffer);
253
+ }
254
+ const bundleDir = await fs.mkdtemp(path.join(bundleParentDir, GENERATED_BUNDLE_DIR_PREFIX));
255
+ const bundlePath = path.join(bundleDir, bundleName);
256
+ try {
257
+ await fs.writeFile(bundlePath, content);
126
258
  return {
127
259
  attachment: {
128
260
  path: bundlePath,
129
261
  displayPath: bundlePath,
130
- sizeBytes: buffer.length,
262
+ sizeBytes: Buffer.byteLength(content),
131
263
  generatedBundle: true,
132
264
  },
133
- metadata: { originalCount: sources.length, bundlePath, format },
265
+ metadata: {
266
+ originalCount: format === "zip" ? sources.length : sections.length,
267
+ bundlePath,
268
+ format,
269
+ },
134
270
  tokenEstimateText,
135
271
  };
136
272
  }
137
- const bundlePath = path.join(bundleDir, "attachments-bundle.txt");
138
- await fs.writeFile(bundlePath, tokenEstimateText, "utf8");
139
- return {
140
- attachment: {
141
- path: bundlePath,
142
- displayPath: bundlePath,
143
- sizeBytes: Buffer.byteLength(tokenEstimateText, "utf8"),
144
- generatedBundle: true,
145
- },
146
- metadata: { originalCount: sections.length, bundlePath, format },
147
- tokenEstimateText,
148
- };
273
+ catch (error) {
274
+ await fs.rm(bundleDir, { recursive: true, force: true });
275
+ throw error;
276
+ }
149
277
  }
150
278
  export async function assembleBrowserPrompt(runOptions, deps = {}) {
151
279
  const cwd = deps.cwd ?? process.cwd();
@@ -206,10 +334,10 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
206
334
  .filter(Boolean)
207
335
  .join("\n\n")
208
336
  .trim();
209
- const selectedPlan = attachmentsPolicy === "always"
210
- ? uploadPlan
211
- : attachmentsPolicy === "never"
212
- ? inlinePlan
337
+ const selectedPlan = attachmentsPolicy === "never"
338
+ ? inlinePlan
339
+ : attachmentsPolicy === "always" || bundleRequested
340
+ ? uploadPlan
213
341
  : inlineComposerText.length <= DEFAULT_BROWSER_INLINE_CHAR_BUDGET || sections.length === 0
214
342
  ? inlinePlan
215
343
  : uploadPlan;
@@ -228,13 +356,13 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
228
356
  const resolvedBundleFormat = resolveBrowserBundleFormat(bundleFormat, {
229
357
  hasRawUploadFiles: rawUploadAttachments.length > 0,
230
358
  });
231
- const shouldBundle = shouldWriteBrowserBundle(resolvedBundleFormat, {
232
- attachmentCount: attachments.length,
359
+ const bundleScope = resolveBrowserBundleScope(resolvedBundleFormat, {
233
360
  bundleRequested,
234
- textSourceCount: textBundleSources.length,
235
- textPlanShouldBundle: selectedPlan.shouldBundle,
361
+ rawAttachmentCount: rawUploadAttachments.length,
362
+ textAttachmentCount: selectedPlan.attachments.length,
236
363
  });
237
- const composerText = (!shouldBundle && selectedPlan.inlineBlock
364
+ const shouldBundle = bundleScope !== "none";
365
+ let composerText = (!shouldBundle && selectedPlan.inlineBlock
238
366
  ? [...baseComposerSections, selectedPlan.inlineBlock]
239
367
  : baseComposerSections)
240
368
  .filter(Boolean)
@@ -242,85 +370,116 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
242
370
  .trim();
243
371
  let bundleText = null;
244
372
  let bundled = null;
245
- if (shouldBundle) {
246
- const writtenBundle = await writeBrowserBundle(sections, resolvedBundleFormat === "zip" ? allBundleSources : textBundleSources, resolvedBundleFormat);
373
+ if (bundleScope !== "none") {
374
+ const writtenBundle = await applyWrittenBundle({
375
+ sections,
376
+ sources: bundleScope === "all" ? allBundleSources : textBundleSources,
377
+ format: resolvedBundleFormat,
378
+ scope: bundleScope,
379
+ rawUploadAttachments,
380
+ composerText,
381
+ });
247
382
  bundleText = writtenBundle.tokenEstimateText;
248
383
  attachments.length = 0;
249
- attachments.push(writtenBundle.attachment);
250
- if (resolvedBundleFormat === "text") {
251
- attachments.push(...rawUploadAttachments);
252
- }
253
- bundled = writtenBundle.metadata;
384
+ attachments.push(...writtenBundle.attachments);
385
+ bundled = writtenBundle.bundled;
386
+ composerText = writtenBundle.composerText;
254
387
  }
255
- assertAttachmentCount(attachments, resolvedBundleFormat);
256
- const inlineFileCount = shouldBundle ? 0 : selectedPlan.inlineFileCount;
257
- const modelConfig = isKnownModel(runOptions.model)
258
- ? MODEL_CONFIGS[runOptions.model]
259
- : MODEL_CONFIGS["gpt-5.1"];
260
- const tokenizer = deps.tokenizeImpl ?? modelConfig.tokenizer;
261
- const tokenizerUserContent = inlineFileCount > 0 && selectedPlan.inlineBlock
262
- ? [userPrompt, selectedPlan.inlineBlock]
388
+ else {
389
+ assertAttachmentCount(attachments.length, resolvedBundleFormat);
390
+ }
391
+ try {
392
+ assertUniqueAttachmentBasenames(attachments, cwd);
393
+ const inlineFileCount = shouldBundle ? 0 : selectedPlan.inlineFileCount;
394
+ const modelConfig = isKnownModel(runOptions.model)
395
+ ? MODEL_CONFIGS[runOptions.model]
396
+ : MODEL_CONFIGS["gpt-5.1"];
397
+ const tokenizer = deps.tokenizeImpl ?? modelConfig.tokenizer;
398
+ const tokenizerUserSections = [userPrompt];
399
+ if (inlineFileCount > 0 && selectedPlan.inlineBlock) {
400
+ tokenizerUserSections.push(selectedPlan.inlineBlock);
401
+ }
402
+ if (shouldBundle && resolvedBundleFormat === "zip" && bundled) {
403
+ tokenizerUserSections.push(appendZipBundleInstruction("", bundled.originalCount, bundled.bundlePath));
404
+ }
405
+ const tokenizerUserContent = tokenizerUserSections
263
406
  .filter((value) => Boolean(value?.trim()))
264
407
  .join("\n\n")
265
- .trim()
266
- : userPrompt;
267
- const tokenizerMessages = [
268
- systemPrompt ? { role: "system", content: systemPrompt } : null,
269
- tokenizerUserContent ? { role: "user", content: tokenizerUserContent } : null,
270
- ].filter(Boolean);
271
- let estimatedInputTokens = tokenizer(tokenizerMessages.length > 0 ? tokenizerMessages : [{ role: "user", content: "" }], TOKENIZER_OPTIONS);
272
- const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(selectedPlan.inlineBlock);
273
- if (!tokenEstimateIncludesInlineFiles && sections.length > 0) {
274
- const attachmentText = bundleText ?? formatFileSections(sections, { lineNumbers: false });
275
- const attachmentTokens = tokenizer([{ role: "user", content: attachmentText }], TOKENIZER_OPTIONS);
276
- estimatedInputTokens += attachmentTokens;
277
- }
278
- let fallback = null;
279
- if (attachmentsPolicy === "auto" && selectedPlan.mode === "inline" && sections.length > 0) {
280
- const fallbackComposerText = baseComposerSections.join("\n\n").trim();
281
- const fallbackAttachments = [...uploadPlan.attachments, ...rawUploadAttachments];
282
- let fallbackBundled = null;
283
- const fallbackBundleFormat = resolveBrowserBundleFormat(bundleFormat, {
284
- hasRawUploadFiles: rawUploadAttachments.length > 0,
285
- });
286
- const fallbackShouldBundle = shouldWriteBrowserBundle(fallbackBundleFormat, {
287
- attachmentCount: fallbackAttachments.length,
288
- bundleRequested,
289
- textSourceCount: textBundleSources.length,
290
- textPlanShouldBundle: uploadPlan.shouldBundle,
291
- });
292
- if (fallbackShouldBundle) {
293
- const writtenBundle = await writeBrowserBundle(sections, fallbackBundleFormat === "zip" ? allBundleSources : textBundleSources, fallbackBundleFormat);
294
- fallbackAttachments.length = 0;
295
- fallbackAttachments.push(writtenBundle.attachment);
296
- if (fallbackBundleFormat === "text") {
297
- fallbackAttachments.push(...rawUploadAttachments);
408
+ .trim();
409
+ const tokenizerMessages = [
410
+ systemPrompt ? { role: "system", content: systemPrompt } : null,
411
+ tokenizerUserContent ? { role: "user", content: tokenizerUserContent } : null,
412
+ ].filter(Boolean);
413
+ let estimatedInputTokens = tokenizer(tokenizerMessages.length > 0 ? tokenizerMessages : [{ role: "user", content: "" }], TOKENIZER_OPTIONS);
414
+ const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(selectedPlan.inlineBlock);
415
+ if (!tokenEstimateIncludesInlineFiles && sections.length > 0) {
416
+ const attachmentText = bundleText ?? formatFileSections(sections, { lineNumbers: false });
417
+ const attachmentTokens = tokenizer([{ role: "user", content: attachmentText }], TOKENIZER_OPTIONS);
418
+ estimatedInputTokens += attachmentTokens;
419
+ }
420
+ let fallback = null;
421
+ let pendingFallback;
422
+ if (attachmentsPolicy === "auto" && selectedPlan.mode === "inline" && sections.length > 0) {
423
+ const fallbackComposerText = baseComposerSections.join("\n\n").trim();
424
+ const fallbackAttachments = [...uploadPlan.attachments, ...rawUploadAttachments];
425
+ const fallbackBundleFormat = resolveBrowserBundleFormat(bundleFormat, {
426
+ hasRawUploadFiles: rawUploadAttachments.length > 0,
427
+ });
428
+ const fallbackBundleScope = resolveBrowserBundleScope(fallbackBundleFormat, {
429
+ bundleRequested,
430
+ rawAttachmentCount: rawUploadAttachments.length,
431
+ textAttachmentCount: uploadPlan.attachments.length,
432
+ });
433
+ fallback = {
434
+ composerText: fallbackComposerText,
435
+ attachments: fallbackAttachments,
436
+ bundled: null,
437
+ pendingBundle: fallbackBundleScope === "none"
438
+ ? null
439
+ : { format: fallbackBundleFormat, scope: fallbackBundleScope },
440
+ };
441
+ if (fallbackBundleScope !== "none") {
442
+ pendingFallback = {
443
+ format: fallbackBundleFormat,
444
+ scope: fallbackBundleScope,
445
+ sections,
446
+ textSources: textBundleSources,
447
+ allSources: allBundleSources,
448
+ rawUploadAttachments,
449
+ };
450
+ }
451
+ else {
452
+ assertAttachmentCount(fallbackAttachments.length, fallbackBundleFormat);
298
453
  }
299
- fallbackBundled = writtenBundle.metadata;
300
454
  }
301
- assertAttachmentCount(fallbackAttachments, fallbackBundleFormat);
302
- fallback = {
303
- composerText: fallbackComposerText,
304
- attachments: fallbackAttachments,
305
- bundled: fallbackBundled,
455
+ const artifacts = {
456
+ markdown,
457
+ composerText,
458
+ estimatedInputTokens,
459
+ attachments,
460
+ inlineFileCount,
461
+ tokenEstimateIncludesInlineFiles,
462
+ attachmentsPolicy,
463
+ attachmentMode: shouldBundle
464
+ ? "bundle"
465
+ : attachments.length > 0
466
+ ? "upload"
467
+ : selectedPlan.mode === "bundle"
468
+ ? "inline"
469
+ : selectedPlan.mode,
470
+ fallback,
471
+ bundled,
306
472
  };
473
+ if (pendingFallback) {
474
+ pendingFallbackBundles.set(artifacts, pendingFallback);
475
+ }
476
+ return artifacts;
477
+ }
478
+ catch (error) {
479
+ await Promise.all(attachments
480
+ .map(generatedBundleDirectory)
481
+ .filter((dir) => dir !== null)
482
+ .map((dir) => fs.rm(dir, { recursive: true, force: true })));
483
+ throw error;
307
484
  }
308
- return {
309
- markdown,
310
- composerText,
311
- estimatedInputTokens,
312
- attachments,
313
- inlineFileCount,
314
- tokenEstimateIncludesInlineFiles,
315
- attachmentsPolicy,
316
- attachmentMode: shouldBundle
317
- ? "bundle"
318
- : attachments.length > 0
319
- ? "upload"
320
- : selectedPlan.mode === "bundle"
321
- ? "inline"
322
- : selectedPlan.mode,
323
- fallback,
324
- bundled,
325
- };
326
485
  }
@@ -20,7 +20,9 @@ async function submitPromptViaAdapter(ctx) {
20
20
  const committedTurns = await submitPrompt({
21
21
  runtime: state.runtime,
22
22
  input: state.input,
23
+ page: state.page,
23
24
  attachmentNames: state.attachmentNames ?? [],
25
+ attachmentNavigationUrl: state.attachmentNavigationUrl,
24
26
  baselineTurns: state.baselineTurns ?? undefined,
25
27
  inputTimeoutMs: state.inputTimeoutMs ?? undefined,
26
28
  attachmentTimeoutMs: state.attachmentTimeoutMs ?? undefined,
@@ -3,8 +3,8 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { mkdtemp, mkdir, rm } from "node:fs/promises";
5
5
  import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, } from "./pageActions.js";
6
- import { launchChrome, connectToChrome, positionChromeWindowOffscreen, connectToRemoteChromeTarget, listRemoteChromeTargets, } from "./chromeLifecycle.js";
7
- import { resolveBrowserConfig } from "./config.js";
6
+ import { launchChrome, connectToChrome, positionChromeWindowOffscreen, positionChromeWindowOnscreen, connectToRemoteChromeTarget, listRemoteChromeTargets, } from "./chromeLifecycle.js";
7
+ import { resolveBrowserApprovalWait, resolveBrowserConfig } from "./config.js";
8
8
  import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
9
9
  import { CHATGPT_URL } from "./constants.js";
10
10
  import { buildConversationTurnListExpression } from "./conversationTurns.js";
@@ -31,11 +31,14 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
31
31
  const host = liveRuntime.chromeHost ?? "127.0.0.1";
32
32
  const port = liveRuntime.chromePort ?? inferPortFromBrowserWSEndpoint(liveRuntime.chromeBrowserWSEndpoint);
33
33
  const browserWSEndpoint = liveRuntime.chromeBrowserWSEndpoint ?? undefined;
34
+ const approvalWaitMs = resolveBrowserApprovalWait(config?.approvalWaitMs);
34
35
  const listTargets = deps.listTargets ??
35
36
  (async () => (await listRemoteChromeTargets({
36
37
  host,
37
38
  port: port ?? 9222,
38
39
  browserWSEndpoint,
40
+ approvalWaitMs,
41
+ logger,
39
42
  })));
40
43
  const targetList = (await listTargets());
41
44
  const target = pickTarget(targetList, liveRuntime);
@@ -44,6 +47,7 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
44
47
  browserWSEndpoint,
45
48
  targetId: target?.targetId ?? target?.id,
46
49
  closeTargetOnDispose: false,
50
+ approvalWaitMs,
47
51
  })
48
52
  : await (async () => {
49
53
  const client = (await (deps.connect ?? ((options) => CDP(options)))(browserWSEndpoint
@@ -216,7 +220,10 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
216
220
  await DOM.enable();
217
221
  }
218
222
  if (!resolved.headless && resolved.hideWindow) {
219
- await positionChromeWindowOffscreen(client, logger);
223
+ await positionChromeWindowOffscreen(client, userDataDir, logger);
224
+ }
225
+ else if (!resolved.headless) {
226
+ await positionChromeWindowOnscreen(client, userDataDir, logger);
220
227
  }
221
228
  let appliedCookies = 0;
222
229
  if (shouldSyncBrowserCookies(resolved, { manualLogin })) {
@@ -2,10 +2,11 @@ import chalk from "chalk";
2
2
  import { formatTokenCount } from "../oracle/runUtils.js";
3
3
  import { formatFinishLine } from "../oracle/finishLine.js";
4
4
  import { runBrowserMode } from "../browserMode.js";
5
- import { assembleBrowserPrompt } from "./prompt.js";
5
+ import { DEFAULT_BROWSER_CONFIG } from "./config.js";
6
+ import { assembleBrowserPrompt, cleanupGeneratedBrowserBundles, materializeBrowserFallback, } from "./prompt.js";
6
7
  import { BrowserAutomationError } from "../oracle/errors.js";
7
8
  import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
8
- import { formatBrowserModelSelectionEvidence, formatBrowserModelTarget, resolveBrowserModelDisplayName, } from "./modelDisplay.js";
9
+ import { formatBrowserModelSelectionEvidence, formatBrowserThinkingSelectionEvidence, formatBrowserModelTarget, resolveBrowserModelDisplayName, } from "./modelDisplay.js";
9
10
  const LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD = 25_000;
10
11
  const LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD = 120_000;
11
12
  function buildUnavailableModelSelectionEvidence(browserConfig) {
@@ -57,7 +58,50 @@ function buildBrowserRunWarnings(args) {
57
58
  export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, deps = {}) {
58
59
  const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
59
60
  const executeBrowser = deps.executeBrowser ?? runBrowserMode;
60
- const promptArtifacts = await assemblePrompt(runOptions, { cwd });
61
+ const persistRuntimeHint = deps.persistRuntimeHint ?? (() => { });
62
+ const inputTimeoutMs = browserConfig.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs;
63
+ let preparationTimedOut = false;
64
+ let preparationTimeout;
65
+ let promptArtifacts;
66
+ try {
67
+ promptArtifacts = await Promise.race([
68
+ assemblePrompt(runOptions, { cwd }).then(async (artifacts) => {
69
+ if (preparationTimedOut)
70
+ await cleanupGeneratedBrowserBundles(artifacts);
71
+ return artifacts;
72
+ }),
73
+ new Promise((_, reject) => {
74
+ preparationTimeout = setTimeout(() => {
75
+ preparationTimedOut = true;
76
+ reject(new BrowserAutomationError(`Browser prompt preparation timed out after ${inputTimeoutMs}ms; increase --browser-input-timeout if local files need more time.`, {
77
+ stage: "prepare-prompt",
78
+ code: "prompt-preparation-timeout",
79
+ timeoutMs: inputTimeoutMs,
80
+ }));
81
+ }, inputTimeoutMs);
82
+ }),
83
+ ]);
84
+ }
85
+ finally {
86
+ if (preparationTimeout) {
87
+ clearTimeout(preparationTimeout);
88
+ }
89
+ }
90
+ try {
91
+ return await executeAssembledBrowserSession({
92
+ runOptions,
93
+ browserConfig,
94
+ log,
95
+ promptArtifacts,
96
+ executeBrowser,
97
+ persistRuntimeHint,
98
+ });
99
+ }
100
+ finally {
101
+ await cleanupGeneratedBrowserBundles(promptArtifacts);
102
+ }
103
+ }
104
+ async function executeAssembledBrowserSession({ runOptions, browserConfig, log, promptArtifacts, executeBrowser, persistRuntimeHint, }) {
61
105
  if (runOptions.verbose) {
62
106
  log(chalk.dim(`[verbose] Browser config: ${JSON.stringify({
63
107
  ...browserConfig,
@@ -91,7 +135,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
91
135
  if (typeof message !== "string")
92
136
  return;
93
137
  const shouldAlwaysPrint = message.startsWith("[browser] ") &&
94
- /archive|fallback|follow-up|retry|thinking|waiting for chatgpt|browser slot|browser control|browser guidance|model selection|model picker/i.test(message);
138
+ /archive|fallback|follow-up|retry|thinking|waiting for chatgpt|remote debugging approval|browser slot|browser control|browser guidance|model selection|model picker/i.test(message);
95
139
  if (!runOptions.verbose && !shouldAlwaysPrint)
96
140
  return;
97
141
  log(message);
@@ -103,7 +147,22 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
103
147
  if (runOptions.verbose) {
104
148
  log(chalk.dim("Chrome automation does not stream output; this may take a minute..."));
105
149
  }
106
- const persistRuntimeHint = deps.persistRuntimeHint ?? (() => { });
150
+ let fallbackSubmission = undefined;
151
+ if (promptArtifacts.fallback) {
152
+ fallbackSubmission = {
153
+ prompt: promptArtifacts.fallback.composerText,
154
+ attachments: promptArtifacts.fallback.attachments,
155
+ pendingBundle: promptArtifacts.fallback.pendingBundle ?? undefined,
156
+ prepare: async () => {
157
+ const prepared = await materializeBrowserFallback(promptArtifacts);
158
+ if (!prepared || !fallbackSubmission)
159
+ return;
160
+ fallbackSubmission.prompt = prepared.composerText;
161
+ fallbackSubmission.attachments = prepared.attachments;
162
+ fallbackSubmission.pendingBundle = undefined;
163
+ },
164
+ };
165
+ }
107
166
  const executionBrowserConfig = runOptions.browserResumeConversationUrl
108
167
  ? { ...browserConfig, resumeConversationUrl: runOptions.browserResumeConversationUrl }
109
168
  : browserConfig;
@@ -112,12 +171,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
112
171
  browserResult = await executeBrowser({
113
172
  prompt: promptArtifacts.composerText,
114
173
  attachments: promptArtifacts.attachments,
115
- fallbackSubmission: promptArtifacts.fallback
116
- ? {
117
- prompt: promptArtifacts.fallback.composerText,
118
- attachments: promptArtifacts.fallback.attachments,
119
- }
120
- : undefined,
174
+ fallbackSubmission,
121
175
  config: executionBrowserConfig,
122
176
  log: automationLogger,
123
177
  heartbeatIntervalMs: runOptions.heartbeatIntervalMs,
@@ -151,6 +205,10 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
151
205
  if (modelSelection) {
152
206
  log(`[browser] Model selection evidence: ${formatBrowserModelSelectionEvidence(modelSelection, runOptions.model)}`);
153
207
  }
208
+ const thinkingSelection = browserResult.thinkingSelection;
209
+ if (thinkingSelection) {
210
+ log(`[browser] Thinking effort evidence: ${formatBrowserThinkingSelectionEvidence(thinkingSelection)}`);
211
+ }
154
212
  const warnings = buildBrowserRunWarnings({
155
213
  runOptions,
156
214
  browserConfig,
@@ -227,9 +285,11 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
227
285
  },
228
286
  archive: browserResult.archive,
229
287
  modelSelection,
288
+ thinkingSelection,
230
289
  warnings,
231
290
  answerText,
232
291
  artifacts: savedArtifacts,
292
+ savedFiles: browserResult.savedFiles,
233
293
  };
234
294
  }
235
295
  export async function ensureSessionArtifacts(params) {