@steipete/oracle 0.10.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 (52) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +104 -16
  3. package/dist/src/browser/actions/archiveConversation.js +224 -0
  4. package/dist/src/browser/actions/assistantResponse.js +26 -0
  5. package/dist/src/browser/actions/deepResearch.js +662 -0
  6. package/dist/src/browser/actions/modelSelection.js +78 -13
  7. package/dist/src/browser/actions/navigation.js +22 -0
  8. package/dist/src/browser/actions/projectSources.js +491 -0
  9. package/dist/src/browser/actions/promptComposer.js +52 -27
  10. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  11. package/dist/src/browser/artifacts.js +150 -0
  12. package/dist/src/browser/attachRunning.js +31 -0
  13. package/dist/src/browser/chatgptImages.js +315 -0
  14. package/dist/src/browser/chromeLifecycle.js +214 -3
  15. package/dist/src/browser/config.js +26 -2
  16. package/dist/src/browser/constants.js +8 -0
  17. package/dist/src/browser/controlPlan.js +81 -0
  18. package/dist/src/browser/detect.js +206 -33
  19. package/dist/src/browser/domDebug.js +49 -0
  20. package/dist/src/browser/index.js +1257 -485
  21. package/dist/src/browser/liveTabs.js +434 -0
  22. package/dist/src/browser/profileState.js +83 -3
  23. package/dist/src/browser/projectSourcesRunner.js +366 -0
  24. package/dist/src/browser/reattach.js +117 -45
  25. package/dist/src/browser/reattachHelpers.js +1 -1
  26. package/dist/src/browser/sessionRunner.js +53 -1
  27. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  28. package/dist/src/cli/bridge/claudeConfig.js +12 -8
  29. package/dist/src/cli/bridge/codexConfig.js +2 -2
  30. package/dist/src/cli/browserConfig.js +40 -0
  31. package/dist/src/cli/browserDefaults.js +31 -7
  32. package/dist/src/cli/browserTabs.js +228 -0
  33. package/dist/src/cli/dryRun.js +33 -1
  34. package/dist/src/cli/duplicatePromptGuard.js +10 -2
  35. package/dist/src/cli/help.js +1 -1
  36. package/dist/src/cli/options.js +4 -0
  37. package/dist/src/cli/projectSources.js +116 -0
  38. package/dist/src/cli/sessionCommand.js +51 -0
  39. package/dist/src/cli/sessionDisplay.js +121 -9
  40. package/dist/src/cli/sessionRunner.js +51 -7
  41. package/dist/src/mcp/consultPresets.js +19 -0
  42. package/dist/src/mcp/server.js +2 -0
  43. package/dist/src/mcp/tools/consult.js +201 -26
  44. package/dist/src/mcp/tools/projectSources.js +123 -0
  45. package/dist/src/mcp/types.js +7 -0
  46. package/dist/src/mcp/utils.js +6 -1
  47. package/dist/src/oracle/run.js +4 -1
  48. package/dist/src/projectSources/plan.js +27 -0
  49. package/dist/src/projectSources/types.js +1 -0
  50. package/dist/src/projectSources/url.js +23 -0
  51. package/dist/src/sessionManager.js +1 -0
  52. package/package.json +2 -1
@@ -0,0 +1,491 @@
1
+ import path from "node:path";
2
+ import { PROJECT_SOURCES_MAX_UPLOAD_BATCH, buildProjectSourcesUploadPlan, } from "../../projectSources/plan.js";
3
+ import { delay } from "../utils.js";
4
+ const PROJECT_SOURCES_INPUT_MARKER = "data-oracle-project-sources-input";
5
+ export async function waitForProjectSourcesReady(runtime, timeoutMs, logger) {
6
+ const deadline = Date.now() + timeoutMs;
7
+ let lastStatus = "unknown";
8
+ while (Date.now() < deadline) {
9
+ const status = await readProjectSourcesSurfaceStatus(runtime).catch((error) => ({
10
+ ready: false,
11
+ reason: error instanceof Error ? error.message : String(error),
12
+ }));
13
+ lastStatus = status.reason ?? "unknown";
14
+ if (status.ready) {
15
+ return;
16
+ }
17
+ await delay(250);
18
+ }
19
+ logger(`Project Sources tab did not become ready before timeout (${lastStatus})`);
20
+ throw new Error("Project Sources tab did not become ready before timeout.");
21
+ }
22
+ export async function openProjectSourcesTab(runtime, input, timeoutMs, logger) {
23
+ const deadline = Date.now() + timeoutMs;
24
+ let lastReason = "sources tab not found";
25
+ while (Date.now() < deadline) {
26
+ const locate = await runtime.evaluate({
27
+ expression: buildOpenProjectSourcesTabExpression(),
28
+ returnByValue: true,
29
+ });
30
+ const point = locate.result?.value;
31
+ if (point?.alreadyOpen) {
32
+ return;
33
+ }
34
+ if (point?.ok && typeof point.x === "number" && typeof point.y === "number") {
35
+ await clickPoint(runtime, input, point.x, point.y);
36
+ await delay(500);
37
+ return;
38
+ }
39
+ lastReason = point?.reason ?? lastReason;
40
+ await delay(250);
41
+ }
42
+ logger(`Project Sources tab did not become selectable before timeout (${lastReason})`);
43
+ throw new Error("Project Sources tab did not become selectable before timeout.");
44
+ }
45
+ async function readProjectSourcesSurfaceStatus(runtime) {
46
+ const outcome = await runtime.evaluate({
47
+ expression: buildProjectSourcesReadyExpression(),
48
+ returnByValue: true,
49
+ });
50
+ const value = outcome.result?.value;
51
+ return {
52
+ ready: value?.ready === true,
53
+ reason: typeof value?.reason === "string" ? value.reason : undefined,
54
+ };
55
+ }
56
+ export async function listProjectSources(runtime) {
57
+ const outcome = await runtime.evaluate({
58
+ expression: buildProjectSourcesListExpression(),
59
+ returnByValue: true,
60
+ });
61
+ const value = outcome.result?.value;
62
+ if (!value?.ok) {
63
+ throw new Error(value?.error ?? "Unable to read ChatGPT project sources.");
64
+ }
65
+ return Array.isArray(value.sources) ? value.sources.filter(isProjectSourceEntry) : [];
66
+ }
67
+ export async function waitForProjectSourcesListSettled(runtime, timeoutMs, logger) {
68
+ const deadline = Date.now() + Math.min(timeoutMs, 30_000);
69
+ const startedAt = Date.now();
70
+ let previousKey = null;
71
+ let stableSince = Date.now();
72
+ let latest = [];
73
+ while (Date.now() < deadline) {
74
+ latest = await listProjectSources(runtime);
75
+ const key = latest.map((source) => source.name).join("\n");
76
+ if (key !== previousKey) {
77
+ previousKey = key;
78
+ stableSince = Date.now();
79
+ }
80
+ const stableForMs = Date.now() - stableSince;
81
+ const observedForMs = Date.now() - startedAt;
82
+ if (observedForMs >= 2500 && stableForMs >= 700) {
83
+ return latest;
84
+ }
85
+ await delay(300);
86
+ }
87
+ logger("Project Sources list did not settle before timeout; returning latest observed list.");
88
+ return latest;
89
+ }
90
+ export async function uploadProjectSources(deps, attachments, logger, timeoutMs) {
91
+ const { runtime, dom, input } = deps;
92
+ if (!dom) {
93
+ throw new Error("Chrome DOM domain unavailable while uploading project sources.");
94
+ }
95
+ if (attachments.length === 0) {
96
+ return await listProjectSources(runtime);
97
+ }
98
+ const plan = buildProjectSourcesUploadPlan(attachments);
99
+ let latestSources = await listProjectSources(runtime);
100
+ for (let offset = 0; offset < attachments.length; offset += PROJECT_SOURCES_MAX_UPLOAD_BATCH) {
101
+ const batch = attachments.slice(offset, offset + PROJECT_SOURCES_MAX_UPLOAD_BATCH);
102
+ const batchIndex = Math.floor(offset / PROJECT_SOURCES_MAX_UPLOAD_BATCH) + 1;
103
+ const batchNames = batch.map((file) => path.basename(file.path));
104
+ logger(`Uploading project source batch ${batchIndex} (${batch.length} file${batch.length === 1 ? "" : "s"})`);
105
+ await openProjectSourcesAddDialog(runtime, input);
106
+ await waitForUploadInput(runtime, Math.min(timeoutMs, 30_000));
107
+ await markProjectSourcesUploadInput(runtime);
108
+ const documentNode = await dom.getDocument({ depth: 5 });
109
+ const query = await dom.querySelector({
110
+ nodeId: documentNode.root.nodeId,
111
+ selector: `input[${PROJECT_SOURCES_INPUT_MARKER}="1"]`,
112
+ });
113
+ if (!query.nodeId) {
114
+ throw new Error("Unable to locate the Project Sources upload input.");
115
+ }
116
+ await dom.setFileInputFiles({ nodeId: query.nodeId, files: batch.map((file) => file.path) });
117
+ await runtime.evaluate({
118
+ expression: `(() => {
119
+ const input = document.querySelector('input[${PROJECT_SOURCES_INPUT_MARKER}="1"]');
120
+ if (!(input instanceof HTMLInputElement)) return false;
121
+ input.dispatchEvent(new Event('input', { bubbles: true }));
122
+ input.dispatchEvent(new Event('change', { bubbles: true }));
123
+ return true;
124
+ })()`,
125
+ returnByValue: true,
126
+ });
127
+ await clickProjectSourcesUploadConfirmation(runtime, input).catch(() => false);
128
+ latestSources = await waitForUploadedProjectSources(runtime, latestSources, batchNames, timeoutMs);
129
+ }
130
+ logger(`Project source upload complete (${plan.length} planned file${plan.length === 1 ? "" : "s"}).`);
131
+ return latestSources;
132
+ }
133
+ export async function openProjectSourcesAddDialog(runtime, input) {
134
+ const locate = await runtime.evaluate({
135
+ expression: buildOpenProjectSourcesAddDialogExpression(),
136
+ returnByValue: true,
137
+ });
138
+ const point = locate.result?.value;
139
+ if (!point?.ok) {
140
+ throw new Error(point?.reason ?? "Unable to open the Project Sources Add dialog.");
141
+ }
142
+ if (point.alreadyOpen) {
143
+ return;
144
+ }
145
+ if (typeof point.x !== "number" || typeof point.y !== "number") {
146
+ throw new Error("Unable to locate the Project Sources Add control.");
147
+ }
148
+ await clickPoint(runtime, input, point.x, point.y);
149
+ const deadline = Date.now() + 10_000;
150
+ while (Date.now() < deadline) {
151
+ const ready = await runtime.evaluate({
152
+ expression: buildProjectSourcesDialogReadyExpression(),
153
+ returnByValue: true,
154
+ });
155
+ if (ready.result?.value === true) {
156
+ return;
157
+ }
158
+ await delay(200);
159
+ }
160
+ throw new Error("Project Sources Add dialog did not open.");
161
+ }
162
+ export async function markProjectSourcesUploadInput(runtime) {
163
+ const outcome = await runtime.evaluate({
164
+ expression: buildMarkProjectSourcesUploadInputExpression(PROJECT_SOURCES_INPUT_MARKER),
165
+ returnByValue: true,
166
+ });
167
+ const value = outcome.result?.value;
168
+ if (!value?.ok) {
169
+ throw new Error(value?.reason ?? "Project Sources upload input did not appear.");
170
+ }
171
+ }
172
+ async function waitForUploadInput(runtime, timeoutMs) {
173
+ const deadline = Date.now() + timeoutMs;
174
+ while (Date.now() < deadline) {
175
+ const found = await runtime.evaluate({
176
+ expression: buildMarkProjectSourcesUploadInputExpression(PROJECT_SOURCES_INPUT_MARKER),
177
+ returnByValue: true,
178
+ });
179
+ if (found.result?.value?.ok === true) {
180
+ return;
181
+ }
182
+ await delay(200);
183
+ }
184
+ throw new Error("Project Sources upload input did not appear.");
185
+ }
186
+ async function waitForUploadedProjectSources(runtime, beforeBatch, expectedNames, timeoutMs) {
187
+ const deadline = Date.now() + Math.max(timeoutMs, 30_000);
188
+ const beforeCounts = countSourceNames(beforeBatch);
189
+ let latestSources = beforeBatch;
190
+ while (Date.now() < deadline) {
191
+ latestSources = await listProjectSources(runtime);
192
+ const currentCounts = countSourceNames(latestSources);
193
+ const ready = hasUploadedProjectSourceBatch(beforeCounts, currentCounts, expectedNames);
194
+ if (ready) {
195
+ return latestSources;
196
+ }
197
+ await delay(500);
198
+ }
199
+ throw new Error(`Timed out waiting for uploaded project sources: ${expectedNames.join(", ")}`);
200
+ }
201
+ async function clickProjectSourcesUploadConfirmation(runtime, input) {
202
+ const locate = await runtime.evaluate({
203
+ expression: buildProjectSourcesConfirmationButtonExpression(),
204
+ returnByValue: true,
205
+ });
206
+ const point = locate.result?.value;
207
+ if (!point?.ok || typeof point.x !== "number" || typeof point.y !== "number") {
208
+ return false;
209
+ }
210
+ await clickPoint(runtime, input, point.x, point.y);
211
+ return true;
212
+ }
213
+ async function clickPoint(runtime, input, x, y) {
214
+ if (input && typeof input.dispatchMouseEvent === "function") {
215
+ await input.dispatchMouseEvent({ type: "mouseMoved", x, y });
216
+ await input.dispatchMouseEvent({ type: "mousePressed", x, y, button: "left", clickCount: 1 });
217
+ await input.dispatchMouseEvent({ type: "mouseReleased", x, y, button: "left", clickCount: 1 });
218
+ return;
219
+ }
220
+ await runtime.evaluate({
221
+ expression: `(() => {
222
+ const el = document.elementFromPoint(${JSON.stringify(x)}, ${JSON.stringify(y)});
223
+ if (!(el instanceof HTMLElement)) return false;
224
+ el.click();
225
+ return true;
226
+ })()`,
227
+ returnByValue: true,
228
+ });
229
+ }
230
+ function countSourceNames(sources) {
231
+ const counts = new Map();
232
+ for (const source of sources) {
233
+ counts.set(source.name, (counts.get(source.name) ?? 0) + 1);
234
+ }
235
+ return counts;
236
+ }
237
+ function hasUploadedProjectSourceBatch(beforeCounts, currentCounts, expectedNames) {
238
+ const expectedCounts = new Map();
239
+ for (const name of expectedNames) {
240
+ expectedCounts.set(name, (expectedCounts.get(name) ?? 0) + 1);
241
+ }
242
+ for (const [name, expectedCount] of expectedCounts) {
243
+ const before = beforeCounts.get(name) ?? 0;
244
+ const current = currentCounts.get(name) ?? 0;
245
+ if (current < before + expectedCount) {
246
+ return false;
247
+ }
248
+ }
249
+ return true;
250
+ }
251
+ export function hasUploadedProjectSourceBatchForTest(before, current, expectedNames) {
252
+ return hasUploadedProjectSourceBatch(countSourceNames(before), countSourceNames(current), expectedNames);
253
+ }
254
+ function isProjectSourceEntry(value) {
255
+ const entry = value;
256
+ return Boolean(entry) && typeof entry.name === "string" && typeof entry.index === "number";
257
+ }
258
+ export function buildProjectSourcesReadyExpression() {
259
+ return `(() => {
260
+ const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
261
+ const visible = (node) => {
262
+ if (!(node instanceof HTMLElement)) return false;
263
+ const rect = node.getBoundingClientRect();
264
+ if (rect.width <= 0 || rect.height <= 0) return false;
265
+ const style = window.getComputedStyle(node);
266
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
267
+ };
268
+ const bodyText = normalize(document.body?.innerText || document.body?.textContent || '');
269
+ const selectedSourceTab = Array.from(document.querySelectorAll('[role="tab"],button,[aria-selected]')).some((node) => {
270
+ const label = normalize(node.textContent || node.getAttribute?.('aria-label') || '');
271
+ const selected =
272
+ node.getAttribute?.('aria-selected') === 'true' ||
273
+ node.getAttribute?.('data-state') === 'active' ||
274
+ /\\bactive\\b/i.test(node.getAttribute?.('class') || '');
275
+ return visible(node) && selected && (label === 'sources' || label === 'źródła' || label === 'zrodla');
276
+ });
277
+ const addSurface = Array.from(document.querySelectorAll('button,[role="button"]')).some((node) => {
278
+ const label = normalize(node.textContent || node.getAttribute?.('aria-label') || node.getAttribute?.('title') || '');
279
+ return visible(node) && /^(add source|add sources|dodaj źródła|dodaj zrodla)$/u.test(label);
280
+ });
281
+ if (selectedSourceTab || addSurface) return { ready: true };
282
+ if (bodyText.includes('new chat') || bodyText.includes('nowy czat')) return { ready: false, reason: 'project page loaded but sources tab not visible' };
283
+ return { ready: false, reason: 'sources surface not detected' };
284
+ })()`;
285
+ }
286
+ export function buildProjectSourcesDialogReadyExpression() {
287
+ return `(() => {
288
+ const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
289
+ return Array.from(document.querySelectorAll('[role="dialog"],dialog')).some((dialog) => {
290
+ if (!(dialog instanceof HTMLElement)) return false;
291
+ const text = normalize(dialog.innerText || dialog.textContent || '');
292
+ return text.includes('add source') || text.includes('add sources') || text.includes('dodaj źródła') || text.includes('dodaj zrodla') || text.includes('przeciągnij źródła') || text.includes('przeciagnij zrodla');
293
+ });
294
+ })()`;
295
+ }
296
+ export function buildProjectSourcesListExpression() {
297
+ return `(() => {
298
+ const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
299
+ const lower = (value) => normalize(value).toLowerCase();
300
+ const visible = (node) => {
301
+ if (!(node instanceof HTMLElement)) return false;
302
+ const rect = node.getBoundingClientRect();
303
+ if (rect.width <= 0 || rect.height <= 0) return false;
304
+ const style = window.getComputedStyle(node);
305
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
306
+ };
307
+ const isChromeLabel = (label) => {
308
+ const text = lower(label);
309
+ return (
310
+ !text ||
311
+ text === 'sources' ||
312
+ text === 'źródła' ||
313
+ text === 'zrodla' ||
314
+ text === 'chats' ||
315
+ text === 'czaty' ||
316
+ text === 'all' ||
317
+ text === 'wszystkie' ||
318
+ text === 'add' ||
319
+ text === 'add sources' ||
320
+ text === 'dodaj' ||
321
+ text === 'dodaj źródła' ||
322
+ text === 'dodaj zrodla' ||
323
+ text === 'source actions' ||
324
+ text === 'actions' ||
325
+ text === 'share' ||
326
+ text === 'udostępnij' ||
327
+ text === 'udostepnij' ||
328
+ text === 'add files and more' ||
329
+ text === 'chat with chatgpt' ||
330
+ text === 'ask anything' ||
331
+ text === 'pro' ||
332
+ text === 'voice' ||
333
+ text === 'start voice' ||
334
+ text === 'start dictation'
335
+ );
336
+ };
337
+ const hasLikelyFileName = (label) => /\\.[a-z0-9]{1,12}(?:\\s|$)/iu.test(label);
338
+ const cleanSourceName = (label) => {
339
+ const text = normalize(label);
340
+ return text
341
+ .replace(/(?:file|document|spreadsheet|presentation|pdf|plik|dokument)\\s*·.*$/iu, '')
342
+ .trim();
343
+ };
344
+ const panel =
345
+ document.querySelector('[role="tabpanel"][id*="source" i]') ||
346
+ document.querySelector('[data-testid*="source" i]') ||
347
+ document.querySelector('main') ||
348
+ document.body;
349
+ if (!(panel instanceof HTMLElement)) {
350
+ return { ok: false, error: 'Project Sources panel not found.' };
351
+ }
352
+ const candidates = Array.from(panel.querySelectorAll('[aria-label], [title], a, button, [role="listitem"], [data-testid*="source" i], [class*="file" i], [class*="source" i]'))
353
+ .filter((node) => node instanceof HTMLElement && visible(node))
354
+ .map((node) => {
355
+ const raw = normalize(node.getAttribute('aria-label') || node.getAttribute('title') || node.textContent || '');
356
+ const rect = node.getBoundingClientRect();
357
+ return {
358
+ node,
359
+ raw,
360
+ name: cleanSourceName(raw),
361
+ hasMetadata: raw !== cleanSourceName(raw),
362
+ top: Math.round(rect.top),
363
+ left: Math.round(rect.left),
364
+ };
365
+ });
366
+ const plainNames = new Set(candidates.filter((candidate) => !candidate.hasMetadata).map((candidate) => candidate.name));
367
+ const rows = [];
368
+ for (const node of candidates) {
369
+ const raw = node.raw;
370
+ const name = node.name;
371
+ if (node.hasMetadata && plainNames.has(name)) continue;
372
+ if (isChromeLabel(name)) continue;
373
+ if (!node.hasMetadata && !hasLikelyFileName(name)) continue;
374
+ if (name.length < 2 || name.length > 200) continue;
375
+ if (/^(pdf|docx?|txt|md|csv|xlsx?|json)$/iu.test(name)) continue;
376
+ if (/^(copy|edit|remove|delete|pobierz|usuń|usun)$/iu.test(name)) continue;
377
+ if (/^(file|document|spreadsheet|presentation|plik|dokument)$/iu.test(name)) continue;
378
+ rows.push(node);
379
+ }
380
+ const sources = [];
381
+ const seen = new Set();
382
+ for (const row of rows) {
383
+ const key = row.name + '|' + row.top + '|' + row.left;
384
+ if (seen.has(key)) continue;
385
+ seen.add(key);
386
+ sources.push({ name: row.name, index: sources.length, status: 'unknown' });
387
+ }
388
+ return { ok: true, sources };
389
+ })()`;
390
+ }
391
+ export function buildOpenProjectSourcesTabExpression() {
392
+ return `(() => {
393
+ const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
394
+ const visible = (node) => {
395
+ if (!(node instanceof HTMLElement)) return false;
396
+ const rect = node.getBoundingClientRect();
397
+ if (rect.width <= 0 || rect.height <= 0) return false;
398
+ const style = window.getComputedStyle(node);
399
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
400
+ };
401
+ const controls = Array.from(document.querySelectorAll('[role="tab"],button,a,[aria-selected]'));
402
+ const sourceTab = controls.find((node) => {
403
+ if (!visible(node)) return false;
404
+ const label = normalize(node.textContent || node.getAttribute('aria-label') || node.getAttribute('title'));
405
+ return label === 'sources' || label === 'źródła' || label === 'zrodla';
406
+ });
407
+ if (!(sourceTab instanceof HTMLElement)) {
408
+ return { ok: false, reason: 'Project Sources tab control not found.' };
409
+ }
410
+ const selected =
411
+ sourceTab.getAttribute('aria-selected') === 'true' ||
412
+ sourceTab.getAttribute('data-state') === 'active' ||
413
+ /\\bactive\\b/i.test(sourceTab.getAttribute('class') || '');
414
+ if (selected) return { ok: true, alreadyOpen: true };
415
+ sourceTab.scrollIntoView({ block: 'center', inline: 'center' });
416
+ const rect = sourceTab.getBoundingClientRect();
417
+ return { ok: true, x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
418
+ })()`;
419
+ }
420
+ export function buildOpenProjectSourcesAddDialogExpression() {
421
+ return `(() => {
422
+ const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
423
+ const visible = (node) => {
424
+ if (!(node instanceof HTMLElement)) return false;
425
+ const rect = node.getBoundingClientRect();
426
+ if (rect.width <= 0 || rect.height <= 0) return false;
427
+ const style = window.getComputedStyle(node);
428
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
429
+ };
430
+ if (${buildProjectSourcesDialogReadyExpression().replace(/\n/g, " ")}) return { ok: true, alreadyOpen: true };
431
+ const roots = [
432
+ document.querySelector('[role="tabpanel"][id*="source" i]'),
433
+ document.querySelector('[data-testid*="source" i]'),
434
+ document.querySelector('main'),
435
+ document.body,
436
+ ].filter(Boolean);
437
+ const controls = roots.flatMap((root) => Array.from(root.querySelectorAll('button,[role="button"],a,label')));
438
+ const add = controls.find((node) => {
439
+ if (!visible(node)) return false;
440
+ const label = normalize(node.innerText || node.textContent || node.getAttribute('aria-label') || node.getAttribute('title'));
441
+ return label === 'add source' || label === 'add sources' || label === 'dodaj źródła' || label === 'dodaj zrodla';
442
+ });
443
+ if (!(add instanceof HTMLElement)) return { ok: false, reason: 'Project Sources add control not found.' };
444
+ add.scrollIntoView({ block: 'center', inline: 'center' });
445
+ const rect = add.getBoundingClientRect();
446
+ return { ok: true, x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
447
+ })()`;
448
+ }
449
+ export function buildMarkProjectSourcesUploadInputExpression(marker) {
450
+ return `(() => {
451
+ const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
452
+ const dialog = Array.from(document.querySelectorAll('[role="dialog"],dialog')).find((node) => {
453
+ if (!(node instanceof HTMLElement)) return false;
454
+ const text = normalize(node.innerText || node.textContent || '');
455
+ return text.includes('add source') || text.includes('add sources') || text.includes('dodaj źródła') || text.includes('dodaj zrodla') || text.includes('przeciągnij źródła') || text.includes('przeciagnij zrodla');
456
+ });
457
+ if (!dialog) return { ok: false, reason: 'Project Sources Add dialog missing' };
458
+ const roots = [dialog];
459
+ const input = roots
460
+ .flatMap((root) => Array.from(root.querySelectorAll('input[type="file"]')))
461
+ .find((node) => node instanceof HTMLInputElement);
462
+ if (!(input instanceof HTMLInputElement)) return { ok: false, reason: 'file input missing' };
463
+ Array.from(document.querySelectorAll('input[${marker}]')).forEach((node) => node.removeAttribute('${marker}'));
464
+ input.setAttribute('${marker}', '1');
465
+ return { ok: true };
466
+ })()`;
467
+ }
468
+ export function buildProjectSourcesConfirmationButtonExpression() {
469
+ return `(() => {
470
+ const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
471
+ const visible = (node) => {
472
+ if (!(node instanceof HTMLElement)) return false;
473
+ const rect = node.getBoundingClientRect();
474
+ if (rect.width <= 0 || rect.height <= 0) return false;
475
+ const style = window.getComputedStyle(node);
476
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
477
+ };
478
+ const labels = new Set(['upload anyway', 'upload', 'add', 'continue', 'prześlij', 'przeslij', 'dodaj', 'kontynuuj']);
479
+ const roots = Array.from(document.querySelectorAll('[role="dialog"],dialog')).filter((node) => node instanceof HTMLElement);
480
+ const buttons = (roots.length > 0 ? roots : [document.body]).flatMap((root) => Array.from(root.querySelectorAll('button,[role="button"]')));
481
+ const button = buttons.find((node) => {
482
+ if (!visible(node)) return false;
483
+ const label = normalize(node.innerText || node.textContent || node.getAttribute('aria-label') || node.getAttribute('title'));
484
+ return labels.has(label);
485
+ });
486
+ if (!(button instanceof HTMLElement)) return { ok: false };
487
+ button.scrollIntoView({ block: 'center', inline: 'center' });
488
+ const rect = button.getBoundingClientRect();
489
+ return { ok: true, x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
490
+ })()`;
491
+ }
@@ -193,44 +193,69 @@ export async function clearPromptComposer(Runtime, logger) {
193
193
  const inputSelectorsLiteral = JSON.stringify(INPUT_SELECTORS);
194
194
  const result = await Runtime.evaluate({
195
195
  expression: `(() => {
196
+ const SELECTORS = ${inputSelectorsLiteral};
196
197
  const fallback = document.querySelector(${fallbackSelectorLiteral});
197
198
  const editor = document.querySelector(${primarySelectorLiteral});
198
- const inputSelectors = ${inputSelectorsLiteral};
199
- let cleared = false;
200
- if (fallback) {
201
- fallback.value = '';
202
- fallback.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
203
- fallback.dispatchEvent(new Event('change', { bubbles: true }));
204
- cleared = true;
205
- }
206
- if (editor) {
207
- editor.textContent = '';
208
- editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
209
- cleared = true;
210
- }
211
- const nodes = inputSelectors
212
- .map((selector) => document.querySelector(selector))
213
- .filter((node) => Boolean(node));
214
- for (const node of nodes) {
215
- if (!node) continue;
216
- if (node instanceof HTMLTextAreaElement) {
217
- node.value = '';
199
+ const readValue = (node) => {
200
+ if (!node) return '';
201
+ if (node instanceof HTMLTextAreaElement || node instanceof HTMLInputElement) return node.value ?? '';
202
+ return node.innerText ?? node.textContent ?? '';
203
+ };
204
+ const dispatchClearEvents = (node) => {
205
+ try {
206
+ node.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: true, data: null, inputType: 'deleteContentBackward' }));
207
+ } catch {}
208
+ try {
218
209
  node.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
219
- node.dispatchEvent(new Event('change', { bubbles: true }));
220
- cleared = true;
221
- continue;
210
+ } catch {
211
+ node.dispatchEvent(new Event('input', { bubbles: true }));
212
+ }
213
+ node.dispatchEvent(new Event('change', { bubbles: true }));
214
+ };
215
+ const clearEditable = (node) => {
216
+ if (!node) return false;
217
+ try {
218
+ node.focus?.();
219
+ } catch {}
220
+ if (node instanceof HTMLTextAreaElement || node instanceof HTMLInputElement) {
221
+ node.value = '';
222
+ dispatchClearEvents(node);
223
+ return true;
222
224
  }
223
225
  if (node.isContentEditable || node.getAttribute('contenteditable') === 'true') {
226
+ try {
227
+ const selection = node.ownerDocument?.getSelection?.();
228
+ const range = node.ownerDocument?.createRange?.();
229
+ if (selection && range) {
230
+ range.selectNodeContents(node);
231
+ selection.removeAllRanges();
232
+ selection.addRange(range);
233
+ node.ownerDocument?.execCommand?.('delete', false);
234
+ }
235
+ } catch {}
224
236
  node.textContent = '';
225
- node.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
226
- cleared = true;
237
+ dispatchClearEvents(node);
238
+ return true;
227
239
  }
240
+ return false;
241
+ };
242
+ let cleared = false;
243
+ const nodes = SELECTORS
244
+ .map((selector) => document.querySelector(selector))
245
+ .filter((node) => Boolean(node));
246
+ for (const node of Array.from(new Set([fallback, editor, ...nodes])).filter(Boolean)) {
247
+ cleared = clearEditable(node) || cleared;
228
248
  }
229
- return { cleared };
249
+ const remaining = Array.from(new Set([fallback, editor, ...nodes]))
250
+ .filter(Boolean)
251
+ .map((node) => readValue(node).trim())
252
+ .filter(Boolean);
253
+ return { cleared, remaining };
230
254
  })()`,
231
255
  returnByValue: true,
232
256
  });
233
- if (!result.result?.value?.cleared) {
257
+ const value = result.result?.value;
258
+ if (!value?.cleared || (value.remaining?.length ?? 0) > 0) {
234
259
  await logDomFailure(Runtime, logger, "clear-composer");
235
260
  throw new Error("Failed to clear prompt composer");
236
261
  }