teamshare-bridge 0.21.16 → 0.21.18

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.
@@ -53,7 +53,12 @@ const config_1 = require("../../lib/config");
53
53
  const fs = __importStar(require("fs"));
54
54
  const os = __importStar(require("os"));
55
55
  const path = __importStar(require("path"));
56
+ const node_path_1 = require("node:path");
57
+ const node_os_1 = require("node:os");
56
58
  const child_process_1 = require("child_process");
59
+ const cdp_1 = require("../../lib/browser/cdp");
60
+ const launch_1 = require("../../lib/browser/launch");
61
+ const session_1 = require("../../lib/browser/session");
57
62
  /** Create a fingerprint from the current element list. */
58
63
  function createFingerprint(elements) {
59
64
  const groups = new Map();
@@ -109,148 +114,148 @@ function calculateChangeScore(prev, curr) {
109
114
  }
110
115
  return score;
111
116
  }
112
- const SYSTEM_PROMPT = `You are a browser automation agent. A screenshot and a grouped list of interactive elements are attached.
113
-
114
- Your task: identify unanswered questions and answer them by clicking the correct elements.
115
-
116
- RULES (STRICT):
117
- 1. NEVER click Next, Submit, Back, Save, Continue, or any navigation. ONLY answer questions on the current screen.
118
- 2. NEVER ask for help. If uncertain, pick the BEST GUESS. Always decide — never wait.
119
- 3. Handle MULTIPLE unanswered questions in a SINGLE response.
120
- 4. NEVER return done unless you have VERIFIED every single question is answered. See VERIFICATION CHECKLIST below.
121
- 5. NEVER answer personal questions — see PERSONAL QUESTIONS section below.
122
-
123
- VERIFICATION CHECKLIST (MANDATORY before returning done):
124
- Before returning done, you MUST check EVERY question group:
125
- - Radio questions: exactly ONE option must be selected (checked). If none selected → you MUST pick an answer.
126
- - Checkbox questions: ALL correct options must be checked. If any correct option is missing → you MUST check it.
127
- - Text input questions: the input must have a non-empty value. If empty → you MUST type an answer.
128
- If ANY question fails this check, you MUST answer it — NEVER return done with unanswered questions.
129
- If you return done while questions are unanswered, the task will FAIL. You will be retried.
130
-
131
- QUESTION TYPES AND STATUS:
132
- - radio (single answer): Only ONE option can be correct.
133
- * "unanswered" → click the best answer
134
- * "answered (X selected, can change)" → if you think X is WRONG, click a DIFFERENT option (browser auto-deselects the old one)
135
- * Only return done if you're confident the selected answer is correct
136
-
137
- - checkbox (MULTIPLE answers possible): MULTIPLE options can be correct.
138
- * "unanswered" → click ALL correct answers
139
- * "partially answered (N/M checked, can add/remove)" → if wrong answers are checked, click them AGAIN to uncheck. If correct answers are missing, click them to check.
140
- * Clicking a [CHECKED] element unchecks it. Clicking an unchecked element checks it.
141
-
142
- - text input: Type the answer into the field.
143
- * "unanswered" → type the answer
144
- * "answered (has input, can edit)" → clear and retype if wrong
145
-
146
- MULTI-SELECT EXAMPLE (checkbox with 2 correct, 1 wrong currently checked):
147
- [{"action":"click","elementIndex":4,"reason":"Uncheck wrong answer"},
148
- {"action":"click","elementIndex":2,"reason":"Check correct answer A"},
149
- {"action":"click","elementIndex":5,"reason":"Check correct answer C"}]
150
-
151
- RADIO CHANGE EXAMPLE (radio has wrong answer selected):
152
- [{"action":"click","elementIndex":1,"reason":"Change to correct answer B"}]
153
-
154
- COMBINED EXAMPLE:
155
- [{"action":"click","elementIndex":1,"reason":"Q1: select B (radio)"},
156
- {"action":"click","elementIndex":4,"reason":"Q2: uncheck wrong"},
157
- {"action":"click","elementIndex":5,"reason":"Q2: check correct"},
158
- {"action":"type","elementIndex":9,"text":"42","reason":"Q3: fill answer"}]
159
-
160
- CODE TYPE EXAMPLE (typing code into an editor — note escaped newlines and quotes):
161
- [{"action":"type","elementIndex":5,"text":"function twoSum(nums: number[], target: number): number[] {\\n const map = new Map<number, number>();\\n for (let i = 0; i < nums.length; i++) {\\n const complement = target - nums[i];\\n if (map.has(complement)) {\\n return [map.get(complement), i];\\n }\\n map.set(nums[i], i);\\n }\\n return [];\\n}","reason":"Type the solution function"}]
162
-
163
- WRITE_CODE EXAMPLE (large code blocks — preferred for code > 100 chars):
164
- [{"action":"write_code","elementIndex":5,"code":"function twoSum(nums: number[], target: number): number[] {\\n const map = new Map<number, number>();\\n for (let i = 0; i < nums.length; i++) {\\n const complement = target - nums[i];\\n if (map.has(complement)) {\\n return [map.get(complement), i];\\n }\\n map.set(nums[i], i);\\n }\\n return [];\\n}","reason":"Type the solution function"}]
165
-
166
- WHEN TO USE write_code vs type:
167
- - "write_code" for code blocks, functions, classes, multi-line snippets (> 100 chars)
168
- - "type" for short answers: numbers, single words, short strings (< 100 chars)
169
- - "write_code" preserves exact indentation and newlines — no formatting changes
170
-
171
- CODE FORMATTING RULES (CRITICAL):
172
- - Use 2-space indentation. Each nesting level adds exactly 2 more spaces.
173
- - Function body = 2 spaces, loop body = 4 spaces, if-body = 6 spaces.
174
- - NEVER use tabs or 4/8-space indentation.
175
- - Keep code compact: single-line statements when appropriate.
176
-
177
- CODE OPTIMIZATION RULES (CRITICAL — for coding problems):
178
- 1. ALWAYS write the most time-efficient solution. Correctness alone is NOT enough.
179
- 2. Prefer O(n) or O(n log n) over O(n²). Never write O(n²) if an O(n) approach exists.
180
- 3. Use HashMap/Set for O(1) lookups instead of nested loops for searching/matching.
181
- 4. Consider space-time tradeoffs — a HashMap is worth the O(n) space for O(n) time.
182
- 5. Avoid unnecessary re-computation — cache results in variables.
183
- 6. Use sliding window, two pointers, or binary search where applicable.
184
- 7. Pre-compute prefix sums, frequency maps, or lookup tables when helpful.
185
- 8. Choose the right data structure:
186
- - Deque for sliding window problems
187
- - Heap/priority queue for top-K problems
188
- - Trie for prefix/string matching problems
189
- - Union-Find for connected components
190
- - BFS for shortest path, DFS for exploration
191
- 9. NEVER write brute force if an optimized approach exists.
192
- 10. If the problem has constraints (e.g., n <= 10^5), ensure your solution runs within limits.
193
-
194
- OPTIMIZATION EXAMPLE:
195
- Problem: "Given an array of integers, return indices of two numbers that add up to target."
196
-
197
- NAIVE (WRONG for efficiency — O(n²) time):
198
- for (let i = 0; i < nums.length; i++) {
199
- for (let j = i + 1; j < nums.length; j++) {
200
- if (nums[i] + nums[j] === target) return [i, j];
201
- }
202
- }
203
-
204
- OPTIMIZED (CORRECT — O(n) time, O(n) space):
205
- const map = new Map();
206
- for (let i = 0; i < nums.length; i++) {
207
- const complement = target - nums[i];
208
- if (map.has(complement)) return [map.get(complement), i];
209
- map.set(nums[i], i);
210
- }
211
- return [];
212
-
213
- Available actions:
214
- - click: {action:"click", elementIndex:number} — click to check/uncheck/select
215
- - type: {action:"type", elementIndex:number, text:string} — type short input (< 100 chars)
216
- - write_code: {action:"write_code", elementIndex:number, code:string} — write code block (> 100 chars), preserves exact indentation
217
- - scroll: {action:"scroll", delta:number} — scroll up (-) or down (+)
218
- - press: {action:"press", key:string} — press Enter, Tab, etc.
219
- - done: {action:"done", summary:string} — all visible questions answered correctly
220
-
221
- ERROR CORRECTION (CRITICAL):
222
- - Check <page_context> for "Errors on screen" after each action
223
- - If errors are visible (wrong answer, validation error, "Incorrect", "Wrong Answer", "Error"), FIX THEM immediately
224
- - For text inputs with errors: clear and retype the correct answer
225
- - For code editors with errors: retype the correct code using write_code
226
- - For radio/checkbox with errors: click the correct option
227
- - NEVER return "done" if errors are visible on screen
228
- - Keep running until ALL questions are answered correctly with NO errors
229
-
230
- CORRECTION MODE:
231
- - The <page_context> block shows the current editor content — compare it to the expected answer
232
- - If editor content is WRONG or INCOMPLETE, type the CORRECT code to replace it
233
- - If you previously typed wrong code, use write_code to replace with correct code
234
- - You may be asked the same question multiple times — each time, check if the current answer is correct before acting
235
-
236
- PERSONAL QUESTIONS — SKIP THEM:
237
- - NEVER type answers for questions asking: name, email, phone, address, SSN, password, credit card, date of birth, or any personal information
238
- - If a question asks for personal info, do NOT click or type — skip it entirely
239
- - If ALL remaining questions are personal, return done
240
-
241
- RESPONSE FORMAT RULES (CRITICAL — follow exactly):
242
- - Return ONLY a valid JSON array. No explanations, no markdown fences, no extra text.
243
- - The response MUST be parseable by JSON.parse(). Test mentally: is your output valid JSON?
244
- - For type actions with code: the text field is a JSON string. Escape special characters:
245
- * Newlines → \\n (backslash + n, NOT a literal newline)
246
- * Quotes → \\" (backslash + quote)
247
- * Backslashes → \\\\ (double backslash)
248
- * Tabs → \\t
249
- - WRONG: {"text":"function foo() {\n return 1;}"}
250
- - RIGHT: {"text":"function foo() {\\n return 1;}"}
251
- - WRONG: {"text":"say "hello""}
252
- - RIGHT: {"text":"say \\"hello\\""}
253
- - NEVER put raw newlines or unescaped quotes inside the text string.
117
+ const SYSTEM_PROMPT = `You are a browser automation agent. A screenshot and a grouped list of interactive elements are attached.
118
+
119
+ Your task: identify unanswered questions and answer them by clicking the correct elements.
120
+
121
+ RULES (STRICT):
122
+ 1. NEVER click Next, Submit, Back, Save, Continue, or any navigation. ONLY answer questions on the current screen.
123
+ 2. NEVER ask for help. If uncertain, pick the BEST GUESS. Always decide — never wait.
124
+ 3. Handle MULTIPLE unanswered questions in a SINGLE response.
125
+ 4. NEVER return done unless you have VERIFIED every single question is answered. See VERIFICATION CHECKLIST below.
126
+ 5. NEVER answer personal questions — see PERSONAL QUESTIONS section below.
127
+
128
+ VERIFICATION CHECKLIST (MANDATORY before returning done):
129
+ Before returning done, you MUST check EVERY question group:
130
+ - Radio questions: exactly ONE option must be selected (checked). If none selected → you MUST pick an answer.
131
+ - Checkbox questions: ALL correct options must be checked. If any correct option is missing → you MUST check it.
132
+ - Text input questions: the input must have a non-empty value. If empty → you MUST type an answer.
133
+ If ANY question fails this check, you MUST answer it — NEVER return done with unanswered questions.
134
+ If you return done while questions are unanswered, the task will FAIL. You will be retried.
135
+
136
+ QUESTION TYPES AND STATUS:
137
+ - radio (single answer): Only ONE option can be correct.
138
+ * "unanswered" → click the best answer
139
+ * "answered (X selected, can change)" → if you think X is WRONG, click a DIFFERENT option (browser auto-deselects the old one)
140
+ * Only return done if you're confident the selected answer is correct
141
+
142
+ - checkbox (MULTIPLE answers possible): MULTIPLE options can be correct.
143
+ * "unanswered" → click ALL correct answers
144
+ * "partially answered (N/M checked, can add/remove)" → if wrong answers are checked, click them AGAIN to uncheck. If correct answers are missing, click them to check.
145
+ * Clicking a [CHECKED] element unchecks it. Clicking an unchecked element checks it.
146
+
147
+ - text input: Type the answer into the field.
148
+ * "unanswered" → type the answer
149
+ * "answered (has input, can edit)" → clear and retype if wrong
150
+
151
+ MULTI-SELECT EXAMPLE (checkbox with 2 correct, 1 wrong currently checked):
152
+ [{"action":"click","elementIndex":4,"reason":"Uncheck wrong answer"},
153
+ {"action":"click","elementIndex":2,"reason":"Check correct answer A"},
154
+ {"action":"click","elementIndex":5,"reason":"Check correct answer C"}]
155
+
156
+ RADIO CHANGE EXAMPLE (radio has wrong answer selected):
157
+ [{"action":"click","elementIndex":1,"reason":"Change to correct answer B"}]
158
+
159
+ COMBINED EXAMPLE:
160
+ [{"action":"click","elementIndex":1,"reason":"Q1: select B (radio)"},
161
+ {"action":"click","elementIndex":4,"reason":"Q2: uncheck wrong"},
162
+ {"action":"click","elementIndex":5,"reason":"Q2: check correct"},
163
+ {"action":"type","elementIndex":9,"text":"42","reason":"Q3: fill answer"}]
164
+
165
+ CODE TYPE EXAMPLE (typing code into an editor — note escaped newlines and quotes):
166
+ [{"action":"type","elementIndex":5,"text":"function twoSum(nums: number[], target: number): number[] {\\n const map = new Map<number, number>();\\n for (let i = 0; i < nums.length; i++) {\\n const complement = target - nums[i];\\n if (map.has(complement)) {\\n return [map.get(complement), i];\\n }\\n map.set(nums[i], i);\\n }\\n return [];\\n}","reason":"Type the solution function"}]
167
+
168
+ WRITE_CODE EXAMPLE (large code blocks — preferred for code > 100 chars):
169
+ [{"action":"write_code","elementIndex":5,"code":"function twoSum(nums: number[], target: number): number[] {\\n const map = new Map<number, number>();\\n for (let i = 0; i < nums.length; i++) {\\n const complement = target - nums[i];\\n if (map.has(complement)) {\\n return [map.get(complement), i];\\n }\\n map.set(nums[i], i);\\n }\\n return [];\\n}","reason":"Type the solution function"}]
170
+
171
+ WHEN TO USE write_code vs type:
172
+ - "write_code" for code blocks, functions, classes, multi-line snippets (> 100 chars)
173
+ - "type" for short answers: numbers, single words, short strings (< 100 chars)
174
+ - "write_code" preserves exact indentation and newlines — no formatting changes
175
+
176
+ CODE FORMATTING RULES (CRITICAL):
177
+ - Use 2-space indentation. Each nesting level adds exactly 2 more spaces.
178
+ - Function body = 2 spaces, loop body = 4 spaces, if-body = 6 spaces.
179
+ - NEVER use tabs or 4/8-space indentation.
180
+ - Keep code compact: single-line statements when appropriate.
181
+
182
+ CODE OPTIMIZATION RULES (CRITICAL — for coding problems):
183
+ 1. ALWAYS write the most time-efficient solution. Correctness alone is NOT enough.
184
+ 2. Prefer O(n) or O(n log n) over O(n²). Never write O(n²) if an O(n) approach exists.
185
+ 3. Use HashMap/Set for O(1) lookups instead of nested loops for searching/matching.
186
+ 4. Consider space-time tradeoffs — a HashMap is worth the O(n) space for O(n) time.
187
+ 5. Avoid unnecessary re-computation — cache results in variables.
188
+ 6. Use sliding window, two pointers, or binary search where applicable.
189
+ 7. Pre-compute prefix sums, frequency maps, or lookup tables when helpful.
190
+ 8. Choose the right data structure:
191
+ - Deque for sliding window problems
192
+ - Heap/priority queue for top-K problems
193
+ - Trie for prefix/string matching problems
194
+ - Union-Find for connected components
195
+ - BFS for shortest path, DFS for exploration
196
+ 9. NEVER write brute force if an optimized approach exists.
197
+ 10. If the problem has constraints (e.g., n <= 10^5), ensure your solution runs within limits.
198
+
199
+ OPTIMIZATION EXAMPLE:
200
+ Problem: "Given an array of integers, return indices of two numbers that add up to target."
201
+
202
+ NAIVE (WRONG for efficiency — O(n²) time):
203
+ for (let i = 0; i < nums.length; i++) {
204
+ for (let j = i + 1; j < nums.length; j++) {
205
+ if (nums[i] + nums[j] === target) return [i, j];
206
+ }
207
+ }
208
+
209
+ OPTIMIZED (CORRECT — O(n) time, O(n) space):
210
+ const map = new Map();
211
+ for (let i = 0; i < nums.length; i++) {
212
+ const complement = target - nums[i];
213
+ if (map.has(complement)) return [map.get(complement), i];
214
+ map.set(nums[i], i);
215
+ }
216
+ return [];
217
+
218
+ Available actions:
219
+ - click: {action:"click", elementIndex:number} — click to check/uncheck/select
220
+ - type: {action:"type", elementIndex:number, text:string} — type short input (< 100 chars)
221
+ - write_code: {action:"write_code", elementIndex:number, code:string} — write code block (> 100 chars), preserves exact indentation
222
+ - scroll: {action:"scroll", delta:number} — scroll up (-) or down (+)
223
+ - press: {action:"press", key:string} — press Enter, Tab, etc.
224
+ - done: {action:"done", summary:string} — all visible questions answered correctly
225
+
226
+ ERROR CORRECTION (CRITICAL):
227
+ - Check <page_context> for "Errors on screen" after each action
228
+ - If errors are visible (wrong answer, validation error, "Incorrect", "Wrong Answer", "Error"), FIX THEM immediately
229
+ - For text inputs with errors: clear and retype the correct answer
230
+ - For code editors with errors: retype the correct code using write_code
231
+ - For radio/checkbox with errors: click the correct option
232
+ - NEVER return "done" if errors are visible on screen
233
+ - Keep running until ALL questions are answered correctly with NO errors
234
+
235
+ CORRECTION MODE:
236
+ - The <page_context> block shows the current editor content — compare it to the expected answer
237
+ - If editor content is WRONG or INCOMPLETE, type the CORRECT code to replace it
238
+ - If you previously typed wrong code, use write_code to replace with correct code
239
+ - You may be asked the same question multiple times — each time, check if the current answer is correct before acting
240
+
241
+ PERSONAL QUESTIONS — SKIP THEM:
242
+ - NEVER type answers for questions asking: name, email, phone, address, SSN, password, credit card, date of birth, or any personal information
243
+ - If a question asks for personal info, do NOT click or type — skip it entirely
244
+ - If ALL remaining questions are personal, return done
245
+
246
+ RESPONSE FORMAT RULES (CRITICAL — follow exactly):
247
+ - Return ONLY a valid JSON array. No explanations, no markdown fences, no extra text.
248
+ - The response MUST be parseable by JSON.parse(). Test mentally: is your output valid JSON?
249
+ - For type actions with code: the text field is a JSON string. Escape special characters:
250
+ * Newlines → \\n (backslash + n, NOT a literal newline)
251
+ * Quotes → \\" (backslash + quote)
252
+ * Backslashes → \\\\ (double backslash)
253
+ * Tabs → \\t
254
+ - WRONG: {"text":"function foo() {\n return 1;}"}
255
+ - RIGHT: {"text":"function foo() {\\n return 1;}"}
256
+ - WRONG: {"text":"say "hello""}
257
+ - RIGHT: {"text":"say \\"hello\\""}
258
+ - NEVER put raw newlines or unescaped quotes inside the text string.
254
259
  - If you need to explain your reasoning, use the "reason" field, NOT the "text" field.`;
255
260
  async function cmdBrowse(flags) {
256
261
  const autoAnswer = flags.get('auto-answer') === 'true';
@@ -264,21 +269,35 @@ async function cmdBrowse(flags) {
264
269
  if (!browserAgentKey) {
265
270
  console.error('[browse] Access denied. A valid browser agent key is required.');
266
271
  console.error('[browse] Set --key <ts_ba_...> or BROWSER_AGENT_KEY env var.');
267
- console.error('[browse] Keys are issued by superadmin at /admin/tools.');
272
+ console.error('[browse] Keys are issued by an admin under Admin → Browser Keys.');
268
273
  process.exitCode = 1;
269
274
  return;
270
275
  }
271
- // Validate key against backend
276
+ // Validate key against backend. Prefer the agent-bound browser-key endpoint
277
+ // (expiry + max-uses aware); fall back to the legacy per-user key endpoint.
272
278
  const apiUrl = flags.get('api-url') ?? process.env.TEAMSHARE_API_URL ?? 'https://api.teamshare.name.ng';
273
279
  try {
274
- const res = await fetch(`${apiUrl}/tools/validate-browser-key`, {
280
+ let res = await fetch(`${apiUrl}/tools/browser-agent/validate`, {
275
281
  method: 'POST',
276
282
  headers: { 'Content-Type': 'application/json' },
277
283
  body: JSON.stringify({ key: browserAgentKey }),
278
284
  });
285
+ if (res.status === 404) {
286
+ res = await fetch(`${apiUrl}/tools/validate-browser-key`, {
287
+ method: 'POST',
288
+ headers: { 'Content-Type': 'application/json' },
289
+ body: JSON.stringify({ key: browserAgentKey }),
290
+ });
291
+ }
279
292
  if (!res.ok) {
280
- console.error('[browse] Invalid or expired browser agent key.');
281
- console.error('[browse] Contact your superadmin to get a valid key at /admin/tools.');
293
+ let code = '';
294
+ try {
295
+ const err = (await res.json());
296
+ code = err?.error?.code ?? '';
297
+ }
298
+ catch { /* ignore */ }
299
+ console.error(`[browse] Invalid browser agent key${code ? ` (${code})` : ''}.`);
300
+ console.error('[browse] Ask an admin to issue a new key under Admin → Browser Keys.');
282
301
  process.exitCode = 1;
283
302
  return;
284
303
  }
@@ -296,7 +315,7 @@ async function cmdBrowse(flags) {
296
315
  const continuous = flags.has('continuous');
297
316
  const maxActions = continuous ? Number.MAX_SAFE_INTEGER : parseInt(flags.get('max-actions') ?? '50', 10);
298
317
  const intervalMs = parseInt(flags.get('interval') ?? '2000', 10);
299
- const modelSpec = flags.get('model') ?? 'mimo:mimo-v2.5';
318
+ const modelSpec = flags.get('model') ?? models_1.DEFAULT_VISION_MODEL;
300
319
  const timeoutSec = Math.min(Math.max(parseInt(flags.get('timeout') ?? '300', 10), 30), 600);
301
320
  const timeoutMs = timeoutSec * 1000;
302
321
  const quality = Math.min(Math.max(parseInt(flags.get('quality') ?? '78', 10), 50), 100);
@@ -305,11 +324,21 @@ async function cmdBrowse(flags) {
305
324
  const optimize = flags.has('optimize');
306
325
  const stealth = flags.has('stealth');
307
326
  const hybrid = flags.has('hybrid');
308
- // Resolve LLM endpoint
309
- const resolved = (0, models_1.resolveModel)(modelSpec, process.env.LLM_BASE_URL);
310
- const apiKey = (0, models_1.resolveApiKeyForProvider)(modelSpec);
327
+ const autoLaunch = flags.has('auto-launch');
328
+ const headless = flags.has('headless');
329
+ const profileName = flags.get('profile') ?? undefined;
330
+ const baseUrlOverride = flags.get('base-url') ?? undefined;
331
+ // Resolve LLM endpoint (direct OpenAI-compatible call; opencode Zen is
332
+ // direct-callable, opencode-go/anthropic remain harness-only).
333
+ const resolved = (0, models_1.resolveBrowseModel)(modelSpec, process.env.LLM_BASE_URL, baseUrlOverride);
334
+ if (resolved.harnessOnly) {
335
+ console.error(`[browse] Model "${modelSpec}" is harness-only and cannot be used for a direct vision loop. Pick a direct model (e.g. mimo:mimo-v2.5, gemini:gemini-3.5-flash, opencode/gemini-3.5-flash).`);
336
+ process.exitCode = 1;
337
+ return;
338
+ }
339
+ const apiKey = flags.get('api-key') ?? (0, models_1.resolveApiKeyForProvider)(modelSpec);
311
340
  if (!apiKey) {
312
- console.error(`[browse] No API key for model "${modelSpec}". Set the provider env var or LLM_API_KEY.`);
341
+ console.error(`[browse] No API key for model "${modelSpec}". Provide one on the run form, set the provider env var, or LLM_API_KEY.`);
313
342
  process.exitCode = 1;
314
343
  return;
315
344
  }
@@ -327,6 +356,27 @@ async function cmdBrowse(flags) {
327
356
  if (!stealth) {
328
357
  console.log(`[browse] Max consecutive failures: ${maxConsecutiveFailures === 0 ? 'unlimited' : maxConsecutiveFailures}`);
329
358
  }
359
+ // ── Auto-launch the debug browser (persistent profile) when asked ──
360
+ if (autoLaunch) {
361
+ try {
362
+ if (await (0, cdp_1.isChromeReachable)(port)) {
363
+ console.log(`[browse] Reusing the debug Chrome already on port ${port}`);
364
+ }
365
+ else {
366
+ const userDataDir = profileName
367
+ ? (0, node_path_1.join)((0, node_os_1.homedir)(), '.teamshare', 'chrome-profiles', profileName)
368
+ : session_1.DEFAULT_BROWSER_PROFILE_DIR;
369
+ const instance = await (0, launch_1.launchChrome)({ port, headless, userDataDir });
370
+ console.log(`[browse] Launched Chrome on port ${instance.port} (${headless ? 'headless' : 'visible'}) profile=${userDataDir}`);
371
+ }
372
+ }
373
+ catch (err) {
374
+ console.error(`[browse] Could not launch Chrome: ${(0, config_1.msg)(err)}`);
375
+ console.error('[browse] Start it manually: teamshare-agent browser start --port ' + port);
376
+ process.exitCode = 1;
377
+ return;
378
+ }
379
+ }
330
380
  // ── Stealth mode: bypass CDP entirely ──
331
381
  if (stealth) {
332
382
  await runStealthLoop(baseUrl, apiKey, model, maxActions, intervalMs, timeoutMs, charDelayMs, continuous);
@@ -340,7 +390,7 @@ async function cmdBrowse(flags) {
340
390
  }
341
391
  catch (err) {
342
392
  console.error(`[browse] Failed to connect: ${(0, config_1.msg)(err)}`);
343
- console.error('[browse] Make sure Chrome is running with --remote-debugging-port=' + port);
393
+ console.error('[browse] Start the browser with: teamshare-agent browser start --port ' + port);
344
394
  process.exitCode = 1;
345
395
  return;
346
396
  }
@@ -359,24 +409,24 @@ async function cmdBrowse(flags) {
359
409
  // console.debug(obj); if(window.__detected) → CDP is serializing args
360
410
  // Countermeasure: wrap console methods to prevent deep serialization.
361
411
  try {
362
- await cdpEval(page, `
363
- (() => {
364
- const CK = Symbol('__ts_cp');
365
- if (window[CK]) return;
366
- const wrap = (fn) => function(...args) {
367
- return fn.apply(console, args.map(a => {
368
- if (a && typeof a === 'object' && typeof a !== 'function') {
369
- try { return Object.assign(Array.isArray(a) ? [] : {}, a); } catch { return a; }
370
- }
371
- return a;
372
- }));
373
- };
374
- console.debug = wrap(console.debug);
375
- console.log = wrap(console.log);
376
- console.warn = wrap(console.warn);
377
- console.error = wrap(console.error);
378
- window[CK] = true;
379
- })()
412
+ await cdpEval(page, `
413
+ (() => {
414
+ const CK = Symbol('__ts_cp');
415
+ if (window[CK]) return;
416
+ const wrap = (fn) => function(...args) {
417
+ return fn.apply(console, args.map(a => {
418
+ if (a && typeof a === 'object' && typeof a !== 'function') {
419
+ try { return Object.assign(Array.isArray(a) ? [] : {}, a); } catch { return a; }
420
+ }
421
+ return a;
422
+ }));
423
+ };
424
+ console.debug = wrap(console.debug);
425
+ console.log = wrap(console.log);
426
+ console.warn = wrap(console.warn);
427
+ console.error = wrap(console.error);
428
+ window[CK] = true;
429
+ })()
380
430
  `);
381
431
  }
382
432
  catch { /* non-critical */ }
@@ -390,73 +440,73 @@ async function cmdBrowse(flags) {
390
440
  // Debounces mutations (500ms) to batch rapid changes.
391
441
  // Stealth: uses Symbol-keyed property (invisible to Object.keys / anti-bot scans).
392
442
  try {
393
- await cdpEval(page, `
394
- (() => {
395
- const KEY = Symbol('__ts_obs');
396
- if (window[KEY]) return 'already attached';
397
- const IGNORE = /timer|countdown|elapsed|clock|animation|animate|transition|spinner|progress|loading|toast|notification|alert|banner|snackbar|cookie|consent|modal|overlay|popup|tooltip|badge|tag|avatar|icon|svg|img|video|audio|canvas|font|stylesheet|script|meta|link|head/i;
398
- let timer = null;
399
- let lastText = '';
400
- function getText() {
401
- const els = document.querySelectorAll('input[type="radio"],input[type="checkbox"],input[type="text"],input[type="email"],input[type="password"],textarea,label,button');
402
- return [...els].map(e => {
403
- const t = (e.innerText || e.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 200);
404
- return e.type + '|' + t + '|' + (e.checked || false);
405
- }).join('\\n');
406
- }
407
- function check() {
408
- try {
409
- const curr = getText();
410
- if (curr === lastText) return;
411
- const prev = lastText;
412
- lastText = curr;
413
- const prevGroups = prev ? prev.split('\\n').filter(Boolean) : [];
414
- const currGroups = curr.split('\\n').filter(Boolean);
415
- const prevSet = new Set(prevGroups);
416
- const currSet = new Set(currGroups);
417
- let removed = 0, changed = 0;
418
- for (const g of prevGroups) {
419
- if (!currSet.has(g)) {
420
- if (prevGroups.indexOf(g) < currGroups.length) changed++;
421
- else removed++;
422
- }
423
- }
424
- const added = currGroups.filter(g => !prevSet.has(g)).length;
425
- const score = added * 10 + removed * 10 + changed * 8;
426
- if (score >= 15) {
427
- window[KEY].changed = Date.now();
428
- }
429
- } catch {}
430
- }
431
- const obs = new MutationObserver((mutations) => {
432
- let hasRelevant = false;
433
- for (const m of mutations) {
434
- if (m.target instanceof Element) {
435
- const tag = m.target.tagName;
436
- if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'HEAD') continue;
437
- const cls = m.target.className || '';
438
- if (typeof cls === 'string' && IGNORE.test(cls)) continue;
439
- const id = m.target.id || '';
440
- if (IGNORE.test(id)) continue;
441
- if (m.type === 'characterData' && m.target.parentElement) {
442
- const ptag = m.target.parentElement.tagName;
443
- if (ptag === 'SCRIPT' || ptag === 'STYLE') continue;
444
- }
445
- }
446
- hasRelevant = true;
447
- }
448
- if (hasRelevant) {
449
- clearTimeout(timer);
450
- timer = setTimeout(check, 500);
451
- }
452
- });
453
- obs.observe(document.body || document.documentElement, {
454
- childList: true, subtree: true, characterData: true
455
- });
456
- window[KEY] = { observer: obs, changed: 0 };
457
- lastText = getText();
458
- return 'ok';
459
- })()
443
+ await cdpEval(page, `
444
+ (() => {
445
+ const KEY = Symbol('__ts_obs');
446
+ if (window[KEY]) return 'already attached';
447
+ const IGNORE = /timer|countdown|elapsed|clock|animation|animate|transition|spinner|progress|loading|toast|notification|alert|banner|snackbar|cookie|consent|modal|overlay|popup|tooltip|badge|tag|avatar|icon|svg|img|video|audio|canvas|font|stylesheet|script|meta|link|head/i;
448
+ let timer = null;
449
+ let lastText = '';
450
+ function getText() {
451
+ const els = document.querySelectorAll('input[type="radio"],input[type="checkbox"],input[type="text"],input[type="email"],input[type="password"],textarea,label,button');
452
+ return [...els].map(e => {
453
+ const t = (e.innerText || e.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 200);
454
+ return e.type + '|' + t + '|' + (e.checked || false);
455
+ }).join('\\n');
456
+ }
457
+ function check() {
458
+ try {
459
+ const curr = getText();
460
+ if (curr === lastText) return;
461
+ const prev = lastText;
462
+ lastText = curr;
463
+ const prevGroups = prev ? prev.split('\\n').filter(Boolean) : [];
464
+ const currGroups = curr.split('\\n').filter(Boolean);
465
+ const prevSet = new Set(prevGroups);
466
+ const currSet = new Set(currGroups);
467
+ let removed = 0, changed = 0;
468
+ for (const g of prevGroups) {
469
+ if (!currSet.has(g)) {
470
+ if (prevGroups.indexOf(g) < currGroups.length) changed++;
471
+ else removed++;
472
+ }
473
+ }
474
+ const added = currGroups.filter(g => !prevSet.has(g)).length;
475
+ const score = added * 10 + removed * 10 + changed * 8;
476
+ if (score >= 15) {
477
+ window[KEY].changed = Date.now();
478
+ }
479
+ } catch {}
480
+ }
481
+ const obs = new MutationObserver((mutations) => {
482
+ let hasRelevant = false;
483
+ for (const m of mutations) {
484
+ if (m.target instanceof Element) {
485
+ const tag = m.target.tagName;
486
+ if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'HEAD') continue;
487
+ const cls = m.target.className || '';
488
+ if (typeof cls === 'string' && IGNORE.test(cls)) continue;
489
+ const id = m.target.id || '';
490
+ if (IGNORE.test(id)) continue;
491
+ if (m.type === 'characterData' && m.target.parentElement) {
492
+ const ptag = m.target.parentElement.tagName;
493
+ if (ptag === 'SCRIPT' || ptag === 'STYLE') continue;
494
+ }
495
+ }
496
+ hasRelevant = true;
497
+ }
498
+ if (hasRelevant) {
499
+ clearTimeout(timer);
500
+ timer = setTimeout(check, 500);
501
+ }
502
+ });
503
+ obs.observe(document.body || document.documentElement, {
504
+ childList: true, subtree: true, characterData: true
505
+ });
506
+ window[KEY] = { observer: obs, changed: 0 };
507
+ lastText = getText();
508
+ return 'ok';
509
+ })()
460
510
  `);
461
511
  console.log('[browse] MutationObserver injected (stealth, debounced, filtered)');
462
512
  }
@@ -752,17 +802,17 @@ async function cmdBrowse(flags) {
752
802
  extractPageContext(page),
753
803
  ]);
754
804
  if (newScreenshot) {
755
- const optResult = await analyzeScreenshotWithPrompt(baseUrl, apiKey, model, newScreenshot, newElements, newPageContext, timeoutMs, 1, 1, `You are a code optimization expert. The code in the editor is CORRECT but may not be EFFICIENT.
756
-
757
- Review the code and check:
758
- 1. Is it the most time-efficient approach? (O(n log n) or better?)
759
- 2. Are there better data structures to use?
760
- 3. Can nested loops be replaced with HashMap/Set lookups?
761
- 4. Can the solution be optimized with sliding window, two pointers, or binary search?
762
-
763
- If the code is ALREADY optimal (top-tier efficiency), return: [{"action":"done","summary":"Code is already optimal"}]
764
-
765
- If you can optimize it, return the FULL optimized code using write_code. Focus ONLY on performance — do not change correctness.
805
+ const optResult = await analyzeScreenshotWithPrompt(baseUrl, apiKey, model, newScreenshot, newElements, newPageContext, timeoutMs, 1, 1, `You are a code optimization expert. The code in the editor is CORRECT but may not be EFFICIENT.
806
+
807
+ Review the code and check:
808
+ 1. Is it the most time-efficient approach? (O(n log n) or better?)
809
+ 2. Are there better data structures to use?
810
+ 3. Can nested loops be replaced with HashMap/Set lookups?
811
+ 4. Can the solution be optimized with sliding window, two pointers, or binary search?
812
+
813
+ If the code is ALREADY optimal (top-tier efficiency), return: [{"action":"done","summary":"Code is already optimal"}]
814
+
815
+ If you can optimize it, return the FULL optimized code using write_code. Focus ONLY on performance — do not change correctness.
766
816
  Do NOT simplify variable names or change the function signature. Only optimize the algorithm.`, page);
767
817
  if (optResult.actions.length > 0 && optResult.actions[0].action !== 'done') {
768
818
  const optAct = optResult.actions[0];
@@ -975,158 +1025,158 @@ async function captureScreenshot(page, quality) {
975
1025
  * bounding boxes, state, and question group assignments.
976
1026
  */
977
1027
  async function extractElements(page) {
978
- const js = `
979
- (() => {
980
- const els = [];
981
- const sels = [
982
- 'input[type="radio"]', 'input[type="checkbox"]',
983
- 'input[type="text"]', 'input[type="email"]', 'input[type="password"]',
984
- 'input:not([type])',
985
- 'textarea', 'select', 'button', 'a',
986
- '[role="radio"]', '[role="checkbox"]', '[role="button"]',
987
- '[role="option"]', '[role="tab"]', '[role="menuitem"]',
988
- 'label',
989
- // Code editor hidden textareas + contenteditable surfaces
990
- '.monaco-editor textarea.inputarea',
991
- '.CodeMirror textarea',
992
- '.cm-content[contenteditable="true"]',
993
- '.ace_editor textarea.ace_text-input',
994
- '.ql-editor',
995
- '.ProseMirror[contenteditable="true"]',
996
- '.DraftEditor-root [contenteditable="true"]',
997
- '.ck-editor__editable[contenteditable="true"]',
998
- ];
999
-
1000
- // First pass: collect all elements with their positions
1001
- const raw = [];
1002
- document.querySelectorAll(sels.join(',')).forEach((el) => {
1003
- const rect = el.getBoundingClientRect();
1004
- if (rect.width === 0 || rect.height === 0) return;
1005
- if (el.offsetParent === null && getComputedStyle(el).position !== 'fixed') return;
1006
- const text = (el.innerText || el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 500);
1007
- if (!text && el.tagName !== 'INPUT') return;
1008
- raw.push({
1009
- el,
1010
- text,
1011
- tag: el.tagName.toLowerCase(),
1012
- type: el.type || el.getAttribute('role') || '',
1013
- checked: el.checked || el.getAttribute('aria-checked') === 'true' || false,
1014
- x: Math.round(rect.x + rect.width / 2),
1015
- y: Math.round(rect.y + rect.height / 2),
1016
- inViewport: rect.bottom >= 0 && rect.top <= window.innerHeight && rect.right >= 0 && rect.left <= window.innerWidth,
1017
- groupId: null,
1018
- });
1019
- });
1020
-
1021
- // Group by common parent (fieldset, div with question/quiz in class, etc.)
1022
- let groupCounter = 0;
1023
- const parentGroupMap = new Map();
1024
-
1025
- for (const item of raw) {
1026
- if (item.el.type !== 'radio' && item.el.type !== 'checkbox') continue;
1027
- // Walk up to find a group container
1028
- let parent = item.el.parentElement;
1029
- let foundGroup = null;
1030
- for (let depth = 0; depth < 8 && parent; depth++) {
1031
- const cls = (parent.className || '').toLowerCase();
1032
- const id = (parent.id || '').toLowerCase();
1033
- const tag = parent.tagName.toLowerCase();
1034
- if (
1035
- tag === 'fieldset' ||
1036
- cls.includes('question') || cls.includes('quiz') || cls.includes('option') ||
1037
- cls.includes('choice') || cls.includes('answer') || cls.includes('group') ||
1038
- id.includes('question') || id.includes('quiz')
1039
- ) {
1040
- const key = parent;
1041
- if (parentGroupMap.has(key)) {
1042
- foundGroup = parentGroupMap.get(key);
1043
- break;
1044
- } else {
1045
- foundGroup = groupCounter++;
1046
- parentGroupMap.set(key, foundGroup);
1047
- break;
1048
- }
1049
- }
1050
- parent = parent.parentElement;
1051
- }
1052
- if (foundGroup !== null) {
1053
- item.groupId = foundGroup;
1054
- }
1055
- }
1056
-
1057
- // For ungrouped radios/checkboxes, group by proximity (same Y ± 60px)
1058
- const ungrouped = raw.filter(e => (e.el.type === 'radio' || e.el.type === 'checkbox') && e.groupId === null);
1059
- ungrouped.sort((a, b) => a.y - b.y);
1060
- for (const item of ungrouped) {
1061
- // Check if close to an existing group
1062
- let matched = false;
1063
- for (const other of raw) {
1064
- if (other.groupId === null || other === item) continue;
1065
- if (other.el.type !== item.el.type) continue;
1066
- if (Math.abs(other.y - item.y) < 60) {
1067
- item.groupId = other.groupId;
1068
- matched = true;
1069
- break;
1070
- }
1071
- }
1072
- if (!matched) {
1073
- item.groupId = groupCounter++;
1074
- }
1075
- }
1076
-
1077
- // Assign remaining ungrouped elements (non-radio/checkbox)
1078
- for (const item of raw) {
1079
- if (item.groupId === null) {
1080
- item.groupId = groupCounter++;
1081
- }
1082
- }
1083
-
1084
- // Extract question text from group containers (label/legend/parent text)
1085
- const groupQuestionText = new Map();
1086
- for (const item of raw) {
1087
- if (item.groupId === null) continue;
1088
- if (groupQuestionText.has(item.groupId)) continue;
1089
- // Walk up from the element to find the group container
1090
- let parent = item.el.parentElement;
1091
- for (let depth = 0; depth < 8 && parent; depth++) {
1092
- const tag = parent.tagName.toLowerCase();
1093
- // Check for label/legend within the container
1094
- const label = parent.querySelector('label, legend, [class*="question"], [class*="prompt"], [class*="label"]');
1095
- if (label && label.textContent && label.textContent.trim().length > 5) {
1096
- const qText = label.innerText.trim().replace(/\\s+/g, ' ').slice(0, 500);
1097
- groupQuestionText.set(item.groupId, qText);
1098
- break;
1099
- }
1100
- // Also try the container's own text if it's a fieldset/legend pattern
1101
- if (tag === 'fieldset') {
1102
- const legend = parent.querySelector('legend');
1103
- if (legend && legend.textContent) {
1104
- groupQuestionText.set(item.groupId, legend.innerText.trim().replace(/\\s+/g, ' ').slice(0, 500));
1105
- break;
1106
- }
1107
- }
1108
- parent = parent.parentElement;
1109
- }
1110
- }
1111
-
1112
- // Build final list
1113
- raw.forEach((item, i) => {
1114
- els.push({
1115
- i,
1116
- tag: item.tag,
1117
- type: item.type,
1118
- text: item.text,
1119
- questionText: groupQuestionText.get(item.groupId) || '',
1120
- checked: item.checked,
1121
- x: item.x,
1122
- y: item.y,
1123
- group: item.groupId,
1124
- inViewport: item.inViewport,
1125
- });
1126
- });
1127
-
1128
- return els;
1129
- })()
1028
+ const js = `
1029
+ (() => {
1030
+ const els = [];
1031
+ const sels = [
1032
+ 'input[type="radio"]', 'input[type="checkbox"]',
1033
+ 'input[type="text"]', 'input[type="email"]', 'input[type="password"]',
1034
+ 'input:not([type])',
1035
+ 'textarea', 'select', 'button', 'a',
1036
+ '[role="radio"]', '[role="checkbox"]', '[role="button"]',
1037
+ '[role="option"]', '[role="tab"]', '[role="menuitem"]',
1038
+ 'label',
1039
+ // Code editor hidden textareas + contenteditable surfaces
1040
+ '.monaco-editor textarea.inputarea',
1041
+ '.CodeMirror textarea',
1042
+ '.cm-content[contenteditable="true"]',
1043
+ '.ace_editor textarea.ace_text-input',
1044
+ '.ql-editor',
1045
+ '.ProseMirror[contenteditable="true"]',
1046
+ '.DraftEditor-root [contenteditable="true"]',
1047
+ '.ck-editor__editable[contenteditable="true"]',
1048
+ ];
1049
+
1050
+ // First pass: collect all elements with their positions
1051
+ const raw = [];
1052
+ document.querySelectorAll(sels.join(',')).forEach((el) => {
1053
+ const rect = el.getBoundingClientRect();
1054
+ if (rect.width === 0 || rect.height === 0) return;
1055
+ if (el.offsetParent === null && getComputedStyle(el).position !== 'fixed') return;
1056
+ const text = (el.innerText || el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 500);
1057
+ if (!text && el.tagName !== 'INPUT') return;
1058
+ raw.push({
1059
+ el,
1060
+ text,
1061
+ tag: el.tagName.toLowerCase(),
1062
+ type: el.type || el.getAttribute('role') || '',
1063
+ checked: el.checked || el.getAttribute('aria-checked') === 'true' || false,
1064
+ x: Math.round(rect.x + rect.width / 2),
1065
+ y: Math.round(rect.y + rect.height / 2),
1066
+ inViewport: rect.bottom >= 0 && rect.top <= window.innerHeight && rect.right >= 0 && rect.left <= window.innerWidth,
1067
+ groupId: null,
1068
+ });
1069
+ });
1070
+
1071
+ // Group by common parent (fieldset, div with question/quiz in class, etc.)
1072
+ let groupCounter = 0;
1073
+ const parentGroupMap = new Map();
1074
+
1075
+ for (const item of raw) {
1076
+ if (item.el.type !== 'radio' && item.el.type !== 'checkbox') continue;
1077
+ // Walk up to find a group container
1078
+ let parent = item.el.parentElement;
1079
+ let foundGroup = null;
1080
+ for (let depth = 0; depth < 8 && parent; depth++) {
1081
+ const cls = (parent.className || '').toLowerCase();
1082
+ const id = (parent.id || '').toLowerCase();
1083
+ const tag = parent.tagName.toLowerCase();
1084
+ if (
1085
+ tag === 'fieldset' ||
1086
+ cls.includes('question') || cls.includes('quiz') || cls.includes('option') ||
1087
+ cls.includes('choice') || cls.includes('answer') || cls.includes('group') ||
1088
+ id.includes('question') || id.includes('quiz')
1089
+ ) {
1090
+ const key = parent;
1091
+ if (parentGroupMap.has(key)) {
1092
+ foundGroup = parentGroupMap.get(key);
1093
+ break;
1094
+ } else {
1095
+ foundGroup = groupCounter++;
1096
+ parentGroupMap.set(key, foundGroup);
1097
+ break;
1098
+ }
1099
+ }
1100
+ parent = parent.parentElement;
1101
+ }
1102
+ if (foundGroup !== null) {
1103
+ item.groupId = foundGroup;
1104
+ }
1105
+ }
1106
+
1107
+ // For ungrouped radios/checkboxes, group by proximity (same Y ± 60px)
1108
+ const ungrouped = raw.filter(e => (e.el.type === 'radio' || e.el.type === 'checkbox') && e.groupId === null);
1109
+ ungrouped.sort((a, b) => a.y - b.y);
1110
+ for (const item of ungrouped) {
1111
+ // Check if close to an existing group
1112
+ let matched = false;
1113
+ for (const other of raw) {
1114
+ if (other.groupId === null || other === item) continue;
1115
+ if (other.el.type !== item.el.type) continue;
1116
+ if (Math.abs(other.y - item.y) < 60) {
1117
+ item.groupId = other.groupId;
1118
+ matched = true;
1119
+ break;
1120
+ }
1121
+ }
1122
+ if (!matched) {
1123
+ item.groupId = groupCounter++;
1124
+ }
1125
+ }
1126
+
1127
+ // Assign remaining ungrouped elements (non-radio/checkbox)
1128
+ for (const item of raw) {
1129
+ if (item.groupId === null) {
1130
+ item.groupId = groupCounter++;
1131
+ }
1132
+ }
1133
+
1134
+ // Extract question text from group containers (label/legend/parent text)
1135
+ const groupQuestionText = new Map();
1136
+ for (const item of raw) {
1137
+ if (item.groupId === null) continue;
1138
+ if (groupQuestionText.has(item.groupId)) continue;
1139
+ // Walk up from the element to find the group container
1140
+ let parent = item.el.parentElement;
1141
+ for (let depth = 0; depth < 8 && parent; depth++) {
1142
+ const tag = parent.tagName.toLowerCase();
1143
+ // Check for label/legend within the container
1144
+ const label = parent.querySelector('label, legend, [class*="question"], [class*="prompt"], [class*="label"]');
1145
+ if (label && label.textContent && label.textContent.trim().length > 5) {
1146
+ const qText = label.innerText.trim().replace(/\\s+/g, ' ').slice(0, 500);
1147
+ groupQuestionText.set(item.groupId, qText);
1148
+ break;
1149
+ }
1150
+ // Also try the container's own text if it's a fieldset/legend pattern
1151
+ if (tag === 'fieldset') {
1152
+ const legend = parent.querySelector('legend');
1153
+ if (legend && legend.textContent) {
1154
+ groupQuestionText.set(item.groupId, legend.innerText.trim().replace(/\\s+/g, ' ').slice(0, 500));
1155
+ break;
1156
+ }
1157
+ }
1158
+ parent = parent.parentElement;
1159
+ }
1160
+ }
1161
+
1162
+ // Build final list
1163
+ raw.forEach((item, i) => {
1164
+ els.push({
1165
+ i,
1166
+ tag: item.tag,
1167
+ type: item.type,
1168
+ text: item.text,
1169
+ questionText: groupQuestionText.get(item.groupId) || '',
1170
+ checked: item.checked,
1171
+ x: item.x,
1172
+ y: item.y,
1173
+ group: item.groupId,
1174
+ inViewport: item.inViewport,
1175
+ });
1176
+ });
1177
+
1178
+ return els;
1179
+ })()
1130
1180
  `;
1131
1181
  try {
1132
1182
  const result = await cdpEval(page, js);
@@ -1476,19 +1526,19 @@ async function clearEditor(page) {
1476
1526
  /** Read current editor content from Monaco/CM5/CM6/Ace. */
1477
1527
  async function readEditorContent(page) {
1478
1528
  try {
1479
- const result = await cdpEval(page, `
1480
- (() => {
1481
- const monacoNs = typeof monaco !== 'undefined' ? monaco : window.monaco;
1482
- const editors = monacoNs?.editor?.getEditors?.() || [];
1483
- if (editors.length > 0) return editors[0].getValue();
1484
- const cm6 = document.querySelector('.cm-content');
1485
- if (cm6?.cmView?.view) return cm6.cmView.view.state.doc.toString();
1486
- const cm5 = document.querySelector('.CodeMirror')?.CodeMirror;
1487
- if (cm5) return cm5.getValue();
1488
- const ace = document.querySelector('.ace_editor')?.env?.editor;
1489
- if (ace) return ace.getValue();
1490
- return '';
1491
- })()
1529
+ const result = await cdpEval(page, `
1530
+ (() => {
1531
+ const monacoNs = typeof monaco !== 'undefined' ? monaco : window.monaco;
1532
+ const editors = monacoNs?.editor?.getEditors?.() || [];
1533
+ if (editors.length > 0) return editors[0].getValue();
1534
+ const cm6 = document.querySelector('.cm-content');
1535
+ if (cm6?.cmView?.view) return cm6.cmView.view.state.doc.toString();
1536
+ const cm5 = document.querySelector('.CodeMirror')?.CodeMirror;
1537
+ if (cm5) return cm5.getValue();
1538
+ const ace = document.querySelector('.ace_editor')?.env?.editor;
1539
+ if (ace) return ace.getValue();
1540
+ return '';
1541
+ })()
1492
1542
  `);
1493
1543
  return result?.result?.value ?? '';
1494
1544
  }
@@ -1499,26 +1549,26 @@ async function readEditorContent(page) {
1499
1549
  /** Scroll an off-screen element into view before clicking. */
1500
1550
  async function scrollElementIntoView(page, el) {
1501
1551
  const textSlice = el.text?.slice(0, 100) || '';
1502
- await cdpEval(page, `
1503
- (() => {
1504
- const sels = [
1505
- 'input[type="radio"]', 'input[type="checkbox"]',
1506
- 'input[type="text"]', 'input[type="email"]', 'input[type="password"]',
1507
- 'input:not([type])', 'textarea', 'select', 'button', 'a',
1508
- '[role="radio"]', '[role="checkbox"]', '[role="button"]',
1509
- '[role="option"]', '[role="tab"]', '[role="menuitem"]', 'label',
1510
- ];
1511
- const all = [...document.querySelectorAll(sels.join(','))];
1512
- const target = all.find(el => {
1513
- const t = (el.innerText || el.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 100);
1514
- return t === ${JSON.stringify(textSlice)};
1515
- });
1516
- if (target) {
1517
- target.scrollIntoView({ behavior: 'instant', block: 'center' });
1518
- return true;
1519
- }
1520
- return false;
1521
- })()
1552
+ await cdpEval(page, `
1553
+ (() => {
1554
+ const sels = [
1555
+ 'input[type="radio"]', 'input[type="checkbox"]',
1556
+ 'input[type="text"]', 'input[type="email"]', 'input[type="password"]',
1557
+ 'input:not([type])', 'textarea', 'select', 'button', 'a',
1558
+ '[role="radio"]', '[role="checkbox"]', '[role="button"]',
1559
+ '[role="option"]', '[role="tab"]', '[role="menuitem"]', 'label',
1560
+ ];
1561
+ const all = [...document.querySelectorAll(sels.join(','))];
1562
+ const target = all.find(el => {
1563
+ const t = (el.innerText || el.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 100);
1564
+ return t === ${JSON.stringify(textSlice)};
1565
+ });
1566
+ if (target) {
1567
+ target.scrollIntoView({ behavior: 'instant', block: 'center' });
1568
+ return true;
1569
+ }
1570
+ return false;
1571
+ })()
1522
1572
  `);
1523
1573
  await sleep(300);
1524
1574
  }
@@ -1602,27 +1652,27 @@ async function isActionRedundant(baseUrl, apiKey, model, page, elements, action,
1602
1652
  }
1603
1653
  // Ask the LLM to judge
1604
1654
  console.log(`[browse] Code is ${matchPercent}% similar — asking LLM to judge...`);
1605
- const prompt = `You are a code comparison expert. Two versions of code are provided.
1606
-
1607
- CURRENT editor content (truncated to first 500 chars):
1608
- \`\`\`
1609
- ${currentContent.slice(0, 500)}
1610
- \`\`\`
1611
-
1612
- TARGET code to write (truncated to first 500 chars):
1613
- \`\`\`
1614
- ${targetCode.slice(0, 500)}
1615
- \`\`\`
1616
-
1617
- The user wants to write the TARGET code into the editor.
1618
-
1619
- TASK: Compare these two code versions. Are they semantically the same (same logic, same algorithm)? Ignore whitespace, formatting, variable naming differences. Focus on LOGIC.
1620
-
1621
- Return EXACTLY one JSON object:
1622
- - {"decision":"skip","reason":"..."} — if the current code is already correct (same logic)
1623
- - {"decision":"partial","reason":"..."} — if mostly correct, only small changes needed
1624
- - {"decision":"full","reason":"..."} — if completely different code is needed
1625
-
1655
+ const prompt = `You are a code comparison expert. Two versions of code are provided.
1656
+
1657
+ CURRENT editor content (truncated to first 500 chars):
1658
+ \`\`\`
1659
+ ${currentContent.slice(0, 500)}
1660
+ \`\`\`
1661
+
1662
+ TARGET code to write (truncated to first 500 chars):
1663
+ \`\`\`
1664
+ ${targetCode.slice(0, 500)}
1665
+ \`\`\`
1666
+
1667
+ The user wants to write the TARGET code into the editor.
1668
+
1669
+ TASK: Compare these two code versions. Are they semantically the same (same logic, same algorithm)? Ignore whitespace, formatting, variable naming differences. Focus on LOGIC.
1670
+
1671
+ Return EXACTLY one JSON object:
1672
+ - {"decision":"skip","reason":"..."} — if the current code is already correct (same logic)
1673
+ - {"decision":"partial","reason":"..."} — if mostly correct, only small changes needed
1674
+ - {"decision":"full","reason":"..."} — if completely different code is needed
1675
+
1626
1676
  Return ONLY the JSON. No other text.`;
1627
1677
  try {
1628
1678
  const body = {
@@ -1780,25 +1830,25 @@ async function insertIntoCodeEditor(page, selector, text, charDelayMs) {
1780
1830
  });
1781
1831
  await sleep(100);
1782
1832
  // Disable ALL auto-formatting to prevent cascading indentation
1783
- await cdpEval(page, `
1784
- (() => {
1785
- const monacoNs = typeof monaco !== 'undefined' ? monaco : window.monaco;
1786
- monacoNs?.editor?.getEditors?.().forEach(e => e.updateOptions({
1787
- autoIndent: false,
1788
- formatOnType: false,
1789
- autoClosingBrackets: 'never',
1790
- autoSurround: 'never',
1791
- tabSize: 2,
1792
- detectIndentation: false,
1793
- }));
1794
- const cm5 = document.querySelector('.CodeMirror')?.CodeMirror;
1795
- if (cm5) cm5.setOption('electricChars', false);
1796
- const ace = document.querySelector('.ace_editor')?.env?.editor;
1797
- if (ace) {
1798
- ace.setOption('autoScrollEditorIntoView', false);
1799
- ace.setOption(' behavioursEnabled', false);
1800
- }
1801
- })()
1833
+ await cdpEval(page, `
1834
+ (() => {
1835
+ const monacoNs = typeof monaco !== 'undefined' ? monaco : window.monaco;
1836
+ monacoNs?.editor?.getEditors?.().forEach(e => e.updateOptions({
1837
+ autoIndent: false,
1838
+ formatOnType: false,
1839
+ autoClosingBrackets: 'never',
1840
+ autoSurround: 'never',
1841
+ tabSize: 2,
1842
+ detectIndentation: false,
1843
+ }));
1844
+ const cm5 = document.querySelector('.CodeMirror')?.CodeMirror;
1845
+ if (cm5) cm5.setOption('electricChars', false);
1846
+ const ace = document.querySelector('.ace_editor')?.env?.editor;
1847
+ if (ace) {
1848
+ ace.setOption('autoScrollEditorIntoView', false);
1849
+ ace.setOption(' behavioursEnabled', false);
1850
+ }
1851
+ })()
1802
1852
  `);
1803
1853
  // Smart retry: check if editor already has the correct code
1804
1854
  const currentContent = await readEditorContent(page);
@@ -1860,19 +1910,19 @@ async function insertIntoCodeEditor(page, selector, text, charDelayMs) {
1860
1910
  // Post-typing: detect and remove extra trailing brackets
1861
1911
  await removeTrailingBrackets(page);
1862
1912
  // Re-enable auto-formatting
1863
- await cdpEval(page, `
1864
- (() => {
1865
- const monacoNs = typeof monaco !== 'undefined' ? monaco : window.monaco;
1866
- monacoNs?.editor?.getEditors?.().forEach(e => e.updateOptions({
1867
- autoIndent: true,
1868
- formatOnType: true,
1869
- autoClosingBrackets: 'languageDefined',
1870
- autoSurround: 'languageDefined',
1871
- detectIndentation: true,
1872
- }));
1873
- const cm5 = document.querySelector('.CodeMirror')?.CodeMirror;
1874
- if (cm5) cm5.setOption('electricChars', true);
1875
- })()
1913
+ await cdpEval(page, `
1914
+ (() => {
1915
+ const monacoNs = typeof monaco !== 'undefined' ? monaco : window.monaco;
1916
+ monacoNs?.editor?.getEditors?.().forEach(e => e.updateOptions({
1917
+ autoIndent: true,
1918
+ formatOnType: true,
1919
+ autoClosingBrackets: 'languageDefined',
1920
+ autoSurround: 'languageDefined',
1921
+ detectIndentation: true,
1922
+ }));
1923
+ const cm5 = document.querySelector('.CodeMirror')?.CodeMirror;
1924
+ if (cm5) cm5.setOption('electricChars', true);
1925
+ })()
1876
1926
  `);
1877
1927
  console.log(`[browse] Typing complete (${text.length} chars)`);
1878
1928
  }
@@ -1933,85 +1983,85 @@ async function insertIntoGenericInput(page, el, text, charDelayMs) {
1933
1983
  * This gives the LLM full visibility into what's on screen.
1934
1984
  */
1935
1985
  async function extractPageContext(page) {
1936
- const js = `
1937
- (() => {
1938
- const ctx = [];
1939
- // Title + URL
1940
- ctx.push('Title: ' + (document.title || 'unknown'));
1941
- ctx.push('URL: ' + window.location.href);
1942
- // Headings
1943
- const headings = [...document.querySelectorAll('h1,h2,h3,h4,h5,h6')]
1944
- .map(h => h.textContent?.trim())
1945
- .filter(Boolean)
1946
- .slice(0, 10);
1947
- if (headings.length) ctx.push('Headings: ' + JSON.stringify(headings));
1948
- // Errors / alerts / toasts / notifications
1949
- const errSels = [
1950
- '[role="alert"]', '[role="status"]',
1951
- '[class*="error"]', '[class*="Error"]',
1952
- '[class*="toast"]', '[class*="Toast"]',
1953
- '[class*="alert"]', '[class*="Alert"]',
1954
- '[class*="banner"]', '[class*="Banner"]',
1955
- '[class*="notification"]', '[class*="Notification"]',
1956
- '[class*="danger"]', '[class*="warning"]',
1957
- '[class*="inline-error"]', '[class*="field-error"]',
1958
- '[class*="form-error"]', '[class*="validation-error"]',
1959
- '[data-sonner-toaster] [data-sonner-toast]',
1960
- '.alert-danger', '.alert-warning', '.alert-error',
1961
- '.text-red', '.text-danger', '.text-error',
1962
- '[class*="err-msg"]', '[class*="error-msg"]', '[class*="error-message"]',
1963
- ];
1964
- const errors = [];
1965
- document.querySelectorAll(errSels.join(',')).forEach(el => {
1966
- const text = el.innerText?.trim().replace(/\\s+/g, ' ').slice(0, 200);
1967
- if (text && text.length > 2) errors.push(text);
1968
- });
1969
- if (errors.length) {
1970
- ctx.push('Errors on screen:');
1971
- errors.slice(0, 5).forEach(e => ctx.push(' - ' + e));
1972
- }
1973
- // Code editor content
1974
- let editorContent = null;
1975
- // Monaco
1976
- const monacoNs = typeof monaco !== 'undefined' ? monaco : window.monaco;
1977
- const editors = monacoNs?.editor?.getEditors?.() || [];
1978
- if (editors.length > 0) editorContent = editors[0].getValue();
1979
- // CodeMirror 6
1980
- if (!editorContent) {
1981
- const cm6 = document.querySelector('.cm-content');
1982
- if (cm6?.cmView?.view) editorContent = cm6.cmView.view.state.doc.toString();
1983
- }
1984
- // CodeMirror 5
1985
- if (!editorContent) {
1986
- const cm5 = document.querySelector('.CodeMirror')?.CodeMirror;
1987
- if (cm5) editorContent = cm5.getValue();
1988
- }
1989
- // Ace
1990
- if (!editorContent) {
1991
- const ace = document.querySelector('.ace_editor')?.env?.editor;
1992
- if (ace) editorContent = ace.getValue();
1993
- }
1994
- if (editorContent && editorContent.trim().length > 0) {
1995
- ctx.push('Editor content (' + editorContent.length + ' chars):');
1996
- ctx.push(editorContent.slice(0, 1500));
1997
- if (editorContent.length > 1500) ctx.push('... (truncated)');
1998
- }
1999
- // Problem description (LeetCode, HackerRank, etc.)
2000
- const descSels = [
2001
- '[class*="problem-description"]', '[class*="question-text"]',
2002
- '[data-track-load="description_content"]', '.markdown-body',
2003
- '[class*="content"]', '[class*="prose"]', '[class*="description"]',
2004
- ];
2005
- for (const sel of descSels) {
2006
- const el = document.querySelector(sel);
2007
- if (el && el.innerText && el.innerText.trim().length > 20) {
2008
- ctx.push('Problem description:');
2009
- ctx.push(el.innerText.trim().slice(0, 3000));
2010
- break;
2011
- }
2012
- }
2013
- return ctx.join('\\n');
2014
- })()
1986
+ const js = `
1987
+ (() => {
1988
+ const ctx = [];
1989
+ // Title + URL
1990
+ ctx.push('Title: ' + (document.title || 'unknown'));
1991
+ ctx.push('URL: ' + window.location.href);
1992
+ // Headings
1993
+ const headings = [...document.querySelectorAll('h1,h2,h3,h4,h5,h6')]
1994
+ .map(h => h.textContent?.trim())
1995
+ .filter(Boolean)
1996
+ .slice(0, 10);
1997
+ if (headings.length) ctx.push('Headings: ' + JSON.stringify(headings));
1998
+ // Errors / alerts / toasts / notifications
1999
+ const errSels = [
2000
+ '[role="alert"]', '[role="status"]',
2001
+ '[class*="error"]', '[class*="Error"]',
2002
+ '[class*="toast"]', '[class*="Toast"]',
2003
+ '[class*="alert"]', '[class*="Alert"]',
2004
+ '[class*="banner"]', '[class*="Banner"]',
2005
+ '[class*="notification"]', '[class*="Notification"]',
2006
+ '[class*="danger"]', '[class*="warning"]',
2007
+ '[class*="inline-error"]', '[class*="field-error"]',
2008
+ '[class*="form-error"]', '[class*="validation-error"]',
2009
+ '[data-sonner-toaster] [data-sonner-toast]',
2010
+ '.alert-danger', '.alert-warning', '.alert-error',
2011
+ '.text-red', '.text-danger', '.text-error',
2012
+ '[class*="err-msg"]', '[class*="error-msg"]', '[class*="error-message"]',
2013
+ ];
2014
+ const errors = [];
2015
+ document.querySelectorAll(errSels.join(',')).forEach(el => {
2016
+ const text = el.innerText?.trim().replace(/\\s+/g, ' ').slice(0, 200);
2017
+ if (text && text.length > 2) errors.push(text);
2018
+ });
2019
+ if (errors.length) {
2020
+ ctx.push('Errors on screen:');
2021
+ errors.slice(0, 5).forEach(e => ctx.push(' - ' + e));
2022
+ }
2023
+ // Code editor content
2024
+ let editorContent = null;
2025
+ // Monaco
2026
+ const monacoNs = typeof monaco !== 'undefined' ? monaco : window.monaco;
2027
+ const editors = monacoNs?.editor?.getEditors?.() || [];
2028
+ if (editors.length > 0) editorContent = editors[0].getValue();
2029
+ // CodeMirror 6
2030
+ if (!editorContent) {
2031
+ const cm6 = document.querySelector('.cm-content');
2032
+ if (cm6?.cmView?.view) editorContent = cm6.cmView.view.state.doc.toString();
2033
+ }
2034
+ // CodeMirror 5
2035
+ if (!editorContent) {
2036
+ const cm5 = document.querySelector('.CodeMirror')?.CodeMirror;
2037
+ if (cm5) editorContent = cm5.getValue();
2038
+ }
2039
+ // Ace
2040
+ if (!editorContent) {
2041
+ const ace = document.querySelector('.ace_editor')?.env?.editor;
2042
+ if (ace) editorContent = ace.getValue();
2043
+ }
2044
+ if (editorContent && editorContent.trim().length > 0) {
2045
+ ctx.push('Editor content (' + editorContent.length + ' chars):');
2046
+ ctx.push(editorContent.slice(0, 1500));
2047
+ if (editorContent.length > 1500) ctx.push('... (truncated)');
2048
+ }
2049
+ // Problem description (LeetCode, HackerRank, etc.)
2050
+ const descSels = [
2051
+ '[class*="problem-description"]', '[class*="question-text"]',
2052
+ '[data-track-load="description_content"]', '.markdown-body',
2053
+ '[class*="content"]', '[class*="prose"]', '[class*="description"]',
2054
+ ];
2055
+ for (const sel of descSels) {
2056
+ const el = document.querySelector(sel);
2057
+ if (el && el.innerText && el.innerText.trim().length > 20) {
2058
+ ctx.push('Problem description:');
2059
+ ctx.push(el.innerText.trim().slice(0, 3000));
2060
+ break;
2061
+ }
2062
+ }
2063
+ return ctx.join('\\n');
2064
+ })()
2015
2065
  `;
2016
2066
  try {
2017
2067
  const result = await cdpEval(page, js);
@@ -2026,27 +2076,27 @@ async function extractPageContext(page) {
2026
2076
  * Returns the editor type and the CSS selector to focus.
2027
2077
  */
2028
2078
  async function detectEditor(page) {
2029
- const js = `
2030
- (() => {
2031
- // Order matters — most specific first
2032
- if (document.querySelector('.monaco-editor textarea.inputarea'))
2033
- return { type: 'monaco', selector: '.monaco-editor textarea.inputarea' };
2034
- if (document.querySelector('.CodeMirror textarea'))
2035
- return { type: 'cm5', selector: '.CodeMirror textarea' };
2036
- if (document.querySelector('.cm-content[contenteditable="true"]'))
2037
- return { type: 'cm6', selector: '.cm-content[contenteditable="true"]' };
2038
- if (document.querySelector('.ace_editor textarea.ace_text-input'))
2039
- return { type: 'ace', selector: '.ace_editor textarea.ace_text-input' };
2040
- if (document.querySelector('.ql-editor'))
2041
- return { type: 'quill', selector: '.ql-editor' };
2042
- if (document.querySelector('.ProseMirror[contenteditable="true"]'))
2043
- return { type: 'prosemirror', selector: '.ProseMirror[contenteditable="true"]' };
2044
- if (document.querySelector('.DraftEditor-root [contenteditable="true"]'))
2045
- return { type: 'draft', selector: '.DraftEditor-root [contenteditable="true"]' };
2046
- if (document.querySelector('.ck-editor__editable[contenteditable="true"]'))
2047
- return { type: 'ck5', selector: '.ck-editor__editable[contenteditable="true"]' };
2048
- return null;
2049
- })()
2079
+ const js = `
2080
+ (() => {
2081
+ // Order matters — most specific first
2082
+ if (document.querySelector('.monaco-editor textarea.inputarea'))
2083
+ return { type: 'monaco', selector: '.monaco-editor textarea.inputarea' };
2084
+ if (document.querySelector('.CodeMirror textarea'))
2085
+ return { type: 'cm5', selector: '.CodeMirror textarea' };
2086
+ if (document.querySelector('.cm-content[contenteditable="true"]'))
2087
+ return { type: 'cm6', selector: '.cm-content[contenteditable="true"]' };
2088
+ if (document.querySelector('.ace_editor textarea.ace_text-input'))
2089
+ return { type: 'ace', selector: '.ace_editor textarea.ace_text-input' };
2090
+ if (document.querySelector('.ql-editor'))
2091
+ return { type: 'quill', selector: '.ql-editor' };
2092
+ if (document.querySelector('.ProseMirror[contenteditable="true"]'))
2093
+ return { type: 'prosemirror', selector: '.ProseMirror[contenteditable="true"]' };
2094
+ if (document.querySelector('.DraftEditor-root [contenteditable="true"]'))
2095
+ return { type: 'draft', selector: '.DraftEditor-root [contenteditable="true"]' };
2096
+ if (document.querySelector('.ck-editor__editable[contenteditable="true"]'))
2097
+ return { type: 'ck5', selector: '.ck-editor__editable[contenteditable="true"]' };
2098
+ return null;
2099
+ })()
2050
2100
  `;
2051
2101
  try {
2052
2102
  const result = await cdpEval(page, js);
@@ -2316,27 +2366,27 @@ async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, eleme
2316
2366
  }
2317
2367
  if (!screenChanged) {
2318
2368
  try {
2319
- const flagResult = await cdpEval(page, `
2320
- (() => {
2321
- const syms = Object.getOwnPropertySymbols(window);
2322
- for (const s of syms) {
2323
- if (s.description === '__ts_obs') return window[s]?.changed || 0;
2324
- }
2325
- return 0;
2326
- })()
2369
+ const flagResult = await cdpEval(page, `
2370
+ (() => {
2371
+ const syms = Object.getOwnPropertySymbols(window);
2372
+ for (const s of syms) {
2373
+ if (s.description === '__ts_obs') return window[s]?.changed || 0;
2374
+ }
2375
+ return 0;
2376
+ })()
2327
2377
  `);
2328
2378
  const flagTs = flagResult?.result?.value;
2329
2379
  if (flagTs && flagTs > startTime) {
2330
2380
  screenChanged = true;
2331
2381
  console.log(`[browse] DOM mutation detected (${elapsed}s) — aborting LLM`);
2332
2382
  // Reset flag
2333
- await cdpEval(page, `
2334
- (() => {
2335
- const syms = Object.getOwnPropertySymbols(window);
2336
- for (const s of syms) {
2337
- if (s.description === '__ts_obs') { window[s].changed = 0; return; }
2338
- }
2339
- })()
2383
+ await cdpEval(page, `
2384
+ (() => {
2385
+ const syms = Object.getOwnPropertySymbols(window);
2386
+ for (const s of syms) {
2387
+ if (s.description === '__ts_obs') { window[s].changed = 0; return; }
2388
+ }
2389
+ })()
2340
2390
  `);
2341
2391
  }
2342
2392
  }
@@ -2448,14 +2498,14 @@ async function analyzeScreenshotWithPrompt(baseUrl, apiKey, model, screenshotBas
2448
2498
  const heartbeat = setInterval(async () => {
2449
2499
  const elapsed = Math.round((Date.now() - startTime) / 1000);
2450
2500
  try {
2451
- const flagResult = await cdpEval(page, `
2452
- (() => {
2453
- const syms = Object.getOwnPropertySymbols(window);
2454
- for (const s of syms) {
2455
- if (s.description === '__ts_obs') return window[s]?.changed || 0;
2456
- }
2457
- return 0;
2458
- })()
2501
+ const flagResult = await cdpEval(page, `
2502
+ (() => {
2503
+ const syms = Object.getOwnPropertySymbols(window);
2504
+ for (const s of syms) {
2505
+ if (s.description === '__ts_obs') return window[s]?.changed || 0;
2506
+ }
2507
+ return 0;
2508
+ })()
2459
2509
  `);
2460
2510
  const flagTs = flagResult?.result?.value;
2461
2511
  if (flagTs && flagTs > startTime) {