@steipete/oracle 0.17.3 → 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 (161) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +75 -64
  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 +267 -15
  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 +15 -3
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +447 -263
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/policies.js +6 -2
  31. package/dist/src/browser/profileState.js +114 -9
  32. package/dist/src/browser/projectSourcesRunner.js +88 -17
  33. package/dist/src/browser/prompt.js +272 -113
  34. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  35. package/dist/src/browser/reattach.js +14 -4
  36. package/dist/src/browser/sessionRunner.js +71 -11
  37. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  38. package/dist/src/browser/utils.js +20 -2
  39. package/dist/src/cli/browserConfig.js +40 -15
  40. package/dist/src/cli/browserDefaults.js +13 -0
  41. package/dist/src/cli/browserTabs.js +12 -29
  42. package/dist/src/cli/detachedSession.js +69 -0
  43. package/dist/src/cli/dryRun.js +58 -47
  44. package/dist/src/cli/harvestIntegrity.js +140 -0
  45. package/dist/src/cli/projectSources.js +5 -3
  46. package/dist/src/cli/serveBrowserConfig.js +24 -0
  47. package/dist/src/cli/sessionDisplay.js +9 -2
  48. package/dist/src/cli/sessionRunner.js +91 -7
  49. package/dist/src/config.js +1 -0
  50. package/dist/src/gemini-web/client.js +67 -21
  51. package/dist/src/gemini-web/executor.js +4 -0
  52. package/dist/src/gemini-web/http.js +15 -0
  53. package/dist/src/gemini-web/models.js +15 -11
  54. package/dist/src/mcp/server.js +11 -14
  55. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  56. package/dist/src/mcp/tools/consult.js +97 -26
  57. package/dist/src/mcp/tools/projectSources.js +4 -6
  58. package/dist/src/mcp/tools/sessionResources.js +1 -1
  59. package/dist/src/mcp/tools/sessions.js +2 -2
  60. package/dist/src/mcp/tools/wait.js +182 -0
  61. package/dist/src/mcp/types.js +7 -0
  62. package/dist/src/oracle/errors.js +11 -0
  63. package/dist/src/oracle/providerRoutePlan.js +3 -2
  64. package/dist/src/remote/client.js +70 -6
  65. package/dist/src/remote/health.js +6 -2
  66. package/dist/src/remote/runSlots.js +83 -0
  67. package/dist/src/remote/server.js +351 -72
  68. package/dist/src/sessionManager.js +36 -1
  69. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  70. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  71. package/package.json +25 -23
  72. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  73. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  74. package/dist/bin/oracle.js +0 -683
  75. package/dist/docs-site/.nojekyll +0 -0
  76. package/dist/docs-site/CNAME +0 -1
  77. package/dist/docs-site/RELEASING.html +0 -410
  78. package/dist/docs-site/agents.html +0 -374
  79. package/dist/docs-site/anthropic.html +0 -368
  80. package/dist/docs-site/bridge.html +0 -400
  81. package/dist/docs-site/browser-mode.html +0 -588
  82. package/dist/docs-site/chromium-forks.html +0 -347
  83. package/dist/docs-site/cli-reference.html +0 -346
  84. package/dist/docs-site/configuration.html +0 -436
  85. package/dist/docs-site/favicon.svg +0 -14
  86. package/dist/docs-site/followup.html +0 -368
  87. package/dist/docs-site/gemini.html +0 -379
  88. package/dist/docs-site/grok.html +0 -325
  89. package/dist/docs-site/index.html +0 -360
  90. package/dist/docs-site/install.html +0 -335
  91. package/dist/docs-site/linux.html +0 -321
  92. package/dist/docs-site/llms.txt +0 -43
  93. package/dist/docs-site/manual-tests.html +0 -595
  94. package/dist/docs-site/mcp.html +0 -368
  95. package/dist/docs-site/multimodel.html +0 -364
  96. package/dist/docs-site/mythical-pro-agents.html +0 -360
  97. package/dist/docs-site/notifier.html +0 -338
  98. package/dist/docs-site/openai-endpoints.html +0 -387
  99. package/dist/docs-site/openrouter.html +0 -344
  100. package/dist/docs-site/quickstart.html +0 -369
  101. package/dist/docs-site/refactor/ux.html +0 -532
  102. package/dist/docs-site/sessions.html +0 -388
  103. package/dist/docs-site/social-card.png +0 -0
  104. package/dist/docs-site/social-card.svg +0 -79
  105. package/dist/docs-site/spec.html +0 -363
  106. package/dist/docs-site/testing.html +0 -320
  107. package/dist/docs-site/tui-debug.html +0 -326
  108. package/dist/docs-site/windows-work.html +0 -323
  109. package/dist/docs-site/windows.html +0 -320
  110. package/dist/markdansi/types/index.js +0 -4
  111. package/dist/oracle/bin/oracle-cli.js +0 -472
  112. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  113. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  114. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  115. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  116. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  117. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  118. package/dist/oracle/src/browser/config.js +0 -33
  119. package/dist/oracle/src/browser/constants.js +0 -40
  120. package/dist/oracle/src/browser/cookies.js +0 -210
  121. package/dist/oracle/src/browser/domDebug.js +0 -36
  122. package/dist/oracle/src/browser/index.js +0 -331
  123. package/dist/oracle/src/browser/pageActions.js +0 -5
  124. package/dist/oracle/src/browser/prompt.js +0 -88
  125. package/dist/oracle/src/browser/promptSummary.js +0 -20
  126. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  127. package/dist/oracle/src/browser/types.js +0 -1
  128. package/dist/oracle/src/browser/utils.js +0 -62
  129. package/dist/oracle/src/browserMode.js +0 -1
  130. package/dist/oracle/src/cli/browserConfig.js +0 -44
  131. package/dist/oracle/src/cli/dryRun.js +0 -59
  132. package/dist/oracle/src/cli/engine.js +0 -17
  133. package/dist/oracle/src/cli/errorUtils.js +0 -9
  134. package/dist/oracle/src/cli/help.js +0 -70
  135. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  136. package/dist/oracle/src/cli/options.js +0 -103
  137. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  138. package/dist/oracle/src/cli/rootAlias.js +0 -30
  139. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  140. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  141. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  142. package/dist/oracle/src/heartbeat.js +0 -43
  143. package/dist/oracle/src/oracle/client.js +0 -48
  144. package/dist/oracle/src/oracle/config.js +0 -29
  145. package/dist/oracle/src/oracle/errors.js +0 -101
  146. package/dist/oracle/src/oracle/files.js +0 -220
  147. package/dist/oracle/src/oracle/format.js +0 -33
  148. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  149. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  150. package/dist/oracle/src/oracle/request.js +0 -48
  151. package/dist/oracle/src/oracle/run.js +0 -444
  152. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  153. package/dist/oracle/src/oracle/types.js +0 -1
  154. package/dist/oracle/src/oracle.js +0 -9
  155. package/dist/oracle/src/sessionManager.js +0 -205
  156. package/dist/oracle/src/version.js +0 -39
  157. package/dist/scripts/chrome/browser-tools.js +0 -295
  158. package/dist/src/browser/chromeCookies.js +0 -312
  159. package/dist/src/browser/keytarShim.js +0 -56
  160. package/dist/src/browser/profileSync.js +0 -141
  161. 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";
@@ -12,7 +12,7 @@ import { cleanupStaleProfileState } from "./profileState.js";
12
12
  import { readDevToolsActivePortInfo } from "./detect.js";
13
13
  import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from "./reattachHelpers.js";
14
14
  import { waitForDeepResearchCompletion } from "./actions/deepResearch.js";
15
- import { shouldSyncBrowserCookies } from "./policies.js";
15
+ import { CHROME_COOKIE_SYNC_WARNING, shouldSyncBrowserCookies } from "./policies.js";
16
16
  export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
17
17
  const recoverSession = deps.recoverSession ??
18
18
  (async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
@@ -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,10 +220,16 @@ 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 })) {
230
+ if (!resolved.inlineCookies) {
231
+ logger(CHROME_COOKIE_SYNC_WARNING);
232
+ }
223
233
  const sync = deps.syncCookies ?? syncCookies;
224
234
  try {
225
235
  appliedCookies = await sync(Network, resolved.url, resolved.chromeProfile, logger, {