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.
- package/dist/cli/commands/browse.js +660 -610
- package/dist/cli/commands/browse.js.map +1 -1
- package/dist/cli/commands/research.js +110 -14
- package/dist/cli/commands/research.js.map +1 -1
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.js +1 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/lib/api.d.ts +36 -0
- package/dist/lib/api.js +15 -0
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/llm/models.d.ts +25 -0
- package/dist/lib/llm/models.js +69 -1
- package/dist/lib/llm/models.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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') ??
|
|
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
|
-
|
|
309
|
-
const
|
|
310
|
-
const
|
|
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}".
|
|
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]
|
|
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) {
|