1408 lines
63 KiB
TypeScript
1408 lines
63 KiB
TypeScript
export function generateApprovalObserverScript(_port: number): string {
|
||
return `
|
||
// ?? Gravity Bridge v17: Always Run Auto-Approve + Retry Detection ??
|
||
// v17: "Always run" auto-approve at bridge level + Retry button relay to Discord
|
||
(function(){
|
||
'use strict';
|
||
var BASE='',_obs=false,_sent={},_ready=false;
|
||
var _scanScheduled=false,_lastScanTs=0;
|
||
var THROTTLE_MS=500;
|
||
var CLEANUP_MS=300000;
|
||
|
||
function log(m){
|
||
console.log('[GB Observer] '+m);
|
||
// v19: Relay important logs to extension via HTTP so they appear in extension.log
|
||
if (BASE && (m.indexOf('CV-CLASSES')!==-1 || m.indexOf('CV-CHILDREN')!==-1 || m.indexOf('child[')!==-1 || m.indexOf('CV found')!==-1 || m.indexOf('Conversation view')!==-1 || m.indexOf('BEACON')!==-1 || m.indexOf('ERROR')!==-1 || m.indexOf('chat relay')!==-1 || m.indexOf('user-cls')!==-1 || m.indexOf('CONTEXT')!==-1 || m.indexOf('BTN-DOM')!==-1 || m.indexOf('DEFERRED')!==-1 || m.indexOf('DETECTED')!==-1)) {
|
||
try { fetch(BASE+'/log', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({msg:m.substring(0,2000)})}); } catch(e){}
|
||
}
|
||
}
|
||
log('v17 Script loaded ??Always Run Auto-Approve + Retry Detection');
|
||
|
||
// DIAGNOSTIC BEACON: immediate POST to confirm script execution in renderer
|
||
try {
|
||
fetch('http://127.0.0.1:${_port}/ping?beacon=1&t='+Date.now())
|
||
.then(function(r){log('BEACON ping OK: '+r.status);})
|
||
.catch(function(e){log('BEACON ping FAIL: '+e.message);});
|
||
} catch(e){ log('BEACON error: '+e); }
|
||
|
||
// React-Compatible Synthetic Clicker
|
||
function dispatchReactClick(el){
|
||
if (!el) return;
|
||
try {
|
||
el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, view:window, composed:true}));
|
||
el.dispatchEvent(new MouseEvent('mousedown', {bubbles:true, cancelable:true, view:window, composed:true}));
|
||
el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, view:window, composed:true}));
|
||
el.dispatchEvent(new MouseEvent('mouseup', {bubbles:true, cancelable:true, view:window, composed:true}));
|
||
el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, view:window, composed:true}));
|
||
} catch(e) {
|
||
el.click();
|
||
}
|
||
}
|
||
|
||
// ?? Noise filter: lines that are UI artifacts, not real content ??
|
||
var NOISE_RE = new RegExp(
|
||
'^(' +
|
||
'chevron_right|chevron_left|arrow_drop_down|arrow_drop_up|arrow_right|arrow_left|' +
|
||
'arrow_forward|arrow_back|expand_more|expand_less|close|more_horiz|more_vert|' +
|
||
'content_copy|content_paste|check|check_circle|error|warning|info|' +
|
||
'keyboard_arrow_up|keyboard_arrow_down|keyboard_arrow_left|keyboard_arrow_right|' +
|
||
'Thought for \\\\d+s?|Thought for a few seconds|Show more|Show less|Copy|Copied!|Edit|Cancel|' +
|
||
'Always run|Always allow|Running command|Running \\\\d+ commands?|' +
|
||
'Deny|Allow|Allow Once|Allow This Conversation|' +
|
||
'Run|Send|Stop|Review Changes|Accept all|Reject all|Accept|Reject' +
|
||
')$', 'i'
|
||
);
|
||
var NOISE_CODE_RE = /^(declare\\s+(class|function|interface|type|enum|const|var|let)\\s|(import|export|from)\\s|\\s*[{}()\\[\\];]\\s*$|\\.ts:\\d+:|extension.*src.*sdk)/i;
|
||
|
||
function isNoiseLine(line) {
|
||
var trimmed = line.trim();
|
||
if (trimmed.length < 2 && !/^[-*+>]$|^[0-9]$/.test(trimmed)) return true;
|
||
if (NOISE_RE.test(trimmed)) return true;
|
||
if (NOISE_CODE_RE.test(trimmed)) return true;
|
||
// Single-word Material icon names (all lowercase, no spaces)
|
||
if (/^[a-z_]+$/.test(trimmed) && trimmed.length < 30) return true;
|
||
return false;
|
||
}
|
||
|
||
function cleanLines(text) {
|
||
if (!text) return '';
|
||
// Pre-strip: inline removal of icon names and UI noise that textContent concatenates without newlines
|
||
text = text.replace(/\\b(chevron_right|chevron_left|arrow_drop_down|arrow_drop_up|arrow_right|arrow_left|arrow_forward|arrow_back|expand_more|expand_less|more_horiz|more_vert|content_copy|content_paste|keyboard_arrow_up|keyboard_arrow_down|keyboard_arrow_left|keyboard_arrow_right|slow_motion_video|open_in_new)\\b/g, '\\n');
|
||
text = text.replace(/Thought for \\d+s?/gi, '');
|
||
text = text.replace(/Thought for a few seconds/gi, '');
|
||
var lines = text.split('\\n');
|
||
var clean = [];
|
||
var lastWasEmpty = false;
|
||
for (var i = 0; i < lines.length; i++) {
|
||
var line = lines[i];
|
||
if (line.trim().length === 0) {
|
||
if (!lastWasEmpty && clean.length > 0) {
|
||
clean.push('');
|
||
lastWasEmpty = true;
|
||
}
|
||
continue;
|
||
}
|
||
if (!isNoiseLine(line)) {
|
||
clean.push(line.replace(/\\s+$/, ''));
|
||
lastWasEmpty = false;
|
||
}
|
||
}
|
||
while (clean.length > 0 && clean[clean.length - 1] === '') clean.pop();
|
||
return clean.join('\\n');
|
||
}
|
||
|
||
function cleanButtonText(btn) {
|
||
if (!btn) return '';
|
||
var clone = btn.cloneNode(true);
|
||
var icons = clone.querySelectorAll('.google-symbols, .codicon, [class*="icon"], svg, .material-symbols-outlined, .material-icons');
|
||
for(var i=0; i<icons.length; i++) {
|
||
if(icons[i].parentNode) icons[i].parentNode.removeChild(icons[i]);
|
||
}
|
||
var tr = clone.querySelector('.truncate');
|
||
var txt = (tr ? tr.textContent : clone.textContent) || '';
|
||
return txt.trim().replace(/^[\\s\\u200B-\\u200D\\uFEFF\\u00A0]+/, '').replace(/(Alt|Ctrl|Shift|Meta)\\+.*/i,'').trim();
|
||
}
|
||
|
||
function btnId(b,type){
|
||
var txt = cleanButtonText(b);
|
||
var parent = b.parentElement;
|
||
var idx=0;
|
||
if(parent){
|
||
var siblings=parent.querySelectorAll('button');
|
||
for(var i=0;i<siblings.length;i++){if(siblings[i]===b){idx=i;break;}}
|
||
}
|
||
return type+'|'+txt+'|'+idx;
|
||
}
|
||
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
// v7: STEP-AWARE CONTEXT EXTRACTION
|
||
// Find the closest [data-step-index] ancestor, extract step info
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
|
||
function getStepContainer(el) {
|
||
var node = el;
|
||
for (var depth = 0; depth < 10 && node; depth++) {
|
||
if (node.hasAttribute && node.hasAttribute('data-step-index')) return node;
|
||
node = node.parentElement;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// v14: Climb DOM tree to find context near the button
|
||
// STRICT SCOPE: Only 5 levels up ??beyond that we're in unrelated UI territory.
|
||
// JUNK FILTERS: CSS rules, source code, Material icon gluing are all rejected.
|
||
// NO FALLBACK: span/div/p text collection is removed entirely ??it always grabs chat/UI text.
|
||
var PROMPT_ONLY_RE = /^[^\\n]*[\\/\>\\xbb$#]\\s*$/;
|
||
// v14: Detect CSS rules, JS source code, or extension internals in code text
|
||
var JUNK_CODE_RE = /(!important|::selection|background-color:|var\\(--|font-size:|border-[a-z]|padding:|margin:|display:\\s|\\{[^}]*:[^}]*\\}|===|!==|\\|\\||\\bfunction\\s*\\(|\\bconst\\s+\\w+\\s*=|\\bvar\\s+\\w+\\s*=|\\bif\\s*\\(|\\breturn\\b|\\bimport\\s|\\bexport\\s|\\bclass\\s+\\w|\\bnew\\s+\\w|\\.test\\(|\\.match\\(|\\.replace\\(|_RE[.\\s]|\\brawDesc\\b|\\brawCmd\\b|\\benrichedCmd\\b|\\bquerySelector)/;
|
||
// v14: Detect Material icon text glued with content
|
||
var ICON_GLUE_RE = /(alternate_email|content_copy|content_paste|check_circle|chevron_right|chevron_left|keyboard_arrow|arrow_drop_down|arrow_drop_up|more_horiz|more_vert|expand_more|expand_less)[a-zA-Z]/;
|
||
function extractContextFromNearby(btn) {
|
||
var node = btn;
|
||
var _debugTrail = [];
|
||
var _bestCodeText = '';
|
||
var _bestCodeHeader = '';
|
||
var _sawCodeEls = false;
|
||
var _allSkipped = true;
|
||
// v22: Increased from 5 to 10 ??AG Native command display (SRi) can be many levels up
|
||
for (var depth = 0; depth < 10 && node; depth++) {
|
||
if (!node.querySelector) { _debugTrail.push('d'+depth+':noQS'); node = node.parentElement; continue; }
|
||
// v22: Prioritize pre.font-mono (AG Native command line display from SRi component)
|
||
var codeEls = node.querySelectorAll('pre.font-mono, pre, code, [class*="terminal"]');
|
||
_debugTrail.push('d'+depth+':tag='+((node.tagName||'?').toLowerCase())+',cls='+(((typeof node.className==='string')?node.className:'').substring(0,60))+',codeEls='+codeEls.length);
|
||
for (var ci = 0; ci < codeEls.length; ci++) {
|
||
var codeText = cleanLines((codeEls[ci].textContent || '').trim().substring(0, 500));
|
||
if (!codeText || codeText.length <= 5) continue;
|
||
if (/^Running\\s*\\d/i.test(codeText)) continue;
|
||
_sawCodeEls = true;
|
||
if (PROMPT_ONLY_RE.test(codeText.trim())) {
|
||
_debugTrail.push('skip_prompt_ci='+ci+':'+codeText.substring(0,30));
|
||
continue;
|
||
}
|
||
if (JUNK_CODE_RE.test(codeText)) {
|
||
_debugTrail.push('skip_junk_ci='+ci+':'+codeText.substring(0,30));
|
||
continue;
|
||
}
|
||
if (ICON_GLUE_RE.test(codeText)) {
|
||
_debugTrail.push('skip_iconglue_ci='+ci+':'+codeText.substring(0,30));
|
||
continue;
|
||
}
|
||
_allSkipped = false;
|
||
if (!_bestCodeText || codeText.length > _bestCodeText.length) {
|
||
_bestCodeText = codeText;
|
||
var headerEl = node.querySelector('h1, h2, h3, [class*="header"], [class*="title"], [class*="cursor-pointer"]');
|
||
if (headerEl) {
|
||
var hClone = headerEl.cloneNode(true);
|
||
var hRem = hClone.querySelectorAll('button, svg, [class*="icon"], .google-symbols');
|
||
for (var hi = 0; hi < hRem.length; hi++) {
|
||
if (hRem[hi].parentNode) hRem[hi].parentNode.removeChild(hRem[hi]);
|
||
}
|
||
_bestCodeHeader = cleanLines((hClone.textContent || '').trim().substring(0, 200));
|
||
}
|
||
}
|
||
}
|
||
if (_bestCodeText) {
|
||
var parts = [];
|
||
if (_bestCodeHeader) parts.push(_bestCodeHeader);
|
||
parts.push(_bestCodeText);
|
||
log('CONTEXT-OK d='+depth+' src=code trail='+_debugTrail.join(' > '));
|
||
_lastContextDebug = _debugTrail.join(' > ');
|
||
return parts.join(' \u2014 ');
|
||
}
|
||
// v30: Command text is in plain divs near "Running command" header, not pre/code
|
||
var rcDivs = node.querySelectorAll('div');
|
||
// v30 diagnostic: log what we find at each depth where code was skipped
|
||
if (_sawCodeEls && rcDivs.length > 0 && depth <= 5) {
|
||
var rcSample = [];
|
||
for (var rdi = 0; rdi < Math.min(rcDivs.length, 8); rdi++) {
|
||
var rdt = (rcDivs[rdi].textContent || '').trim().substring(0,40);
|
||
var rdc = rcDivs[rdi].children ? rcDivs[rdi].children.length : 0;
|
||
rcSample.push('ch'+rdc+':"'+rdt+'"');
|
||
}
|
||
log('CONTEXT-v30-SCAN d='+depth+' divs='+rcDivs.length+' ['+rcSample.join(', ')+']');
|
||
}
|
||
for (var rci = 0; rci < rcDivs.length; rci++) {
|
||
var rcEl = rcDivs[rci];
|
||
var rcChildCount = rcEl.children ? rcEl.children.length : 0;
|
||
var rcTxt = (rcEl.textContent || '').trim();
|
||
// Match leaf div with exact or near-exact "Running command" text
|
||
if ((rcTxt === 'Running command' || (rcChildCount === 0 && rcTxt.indexOf('Running command') !== -1 && rcTxt.length < 30)) && rcEl.parentElement) {
|
||
var rcP = rcEl.parentElement;
|
||
log('CONTEXT-v30 found RC header at d'+depth+' siblings='+rcP.children.length);
|
||
for (var rcsi = 0; rcsi < rcP.children.length; rcsi++) {
|
||
if (rcP.children[rcsi] === rcEl) continue;
|
||
var sibT = (rcP.children[rcsi].textContent || '').trim();
|
||
if (sibT.length < 5) continue;
|
||
// Skip button-bar text
|
||
if (/^(Always|Run|Allow|Cancel|Deny|keyboard_arrow)/i.test(sibT)) continue;
|
||
if (sibT.indexOf('Always run') !== -1 && sibT.indexOf('Cancel') !== -1) continue;
|
||
// Try prompt marker extraction
|
||
var pM = sibT.match(/[\\u003e\\u00bb\\u276f]\\s+(.+)/);
|
||
if (pM && pM[1].trim().length > 3) {
|
||
var cmdV = pM[1].trim().substring(0, 300);
|
||
if (/^(Always|Run|Allow|Cancel|Deny)/i.test(cmdV)) continue;
|
||
log('CONTEXT-OK d='+depth+' src=running-cmd cmdV='+cmdV.substring(0,60));
|
||
_lastContextDebug = _debugTrail.join(' ');
|
||
return 'Running command: ' + cmdV;
|
||
}
|
||
// Fallback: use raw sibling text if it looks like a terminal line
|
||
if (sibT.length > 10 && /[\\u276f\\u003e]/.test(sibT)) {
|
||
log('CONTEXT-OK d='+depth+' src=running-cmd-raw sibT='+sibT.substring(0,60));
|
||
_lastContextDebug = _debugTrail.join(' ');
|
||
return sibT.substring(0, 300);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// v23: Also search sibling elements at each level
|
||
// AG Native's command display (pre.font-mono) is a SIBLING of footer, not ancestor
|
||
if (node && node.parentElement) {
|
||
var siblings = node.parentElement.children;
|
||
for (var si = 0; si < siblings.length; si++) {
|
||
if (siblings[si] === node) continue;
|
||
if (!siblings[si].querySelector) continue;
|
||
var sibCodeEls = siblings[si].querySelectorAll('pre.font-mono, pre, code');
|
||
for (var sci = 0; sci < sibCodeEls.length; sci++) {
|
||
var sibCode = cleanLines((sibCodeEls[sci].textContent || '').trim().substring(0, 500));
|
||
if (!sibCode || sibCode.length <= 5) continue;
|
||
if (JUNK_CODE_RE.test(sibCode) || ICON_GLUE_RE.test(sibCode)) continue;
|
||
if (PROMPT_ONLY_RE.test(sibCode.trim())) continue;
|
||
_debugTrail.push('sibling_d'+depth+':tag='+siblings[si].tagName.toLowerCase()+',code='+sibCode.substring(0,40));
|
||
_bestCodeText = sibCode;
|
||
_allSkipped = false;
|
||
// Found in sibling ??return immediately
|
||
var sibParts = [];
|
||
var sibHdr = siblings[si].querySelector('h1, h2, h3, [class*="header"], [class*="title"], [class*="cursor-pointer"]');
|
||
if (sibHdr) sibParts.push(cleanLines((sibHdr.textContent || '').trim().substring(0, 200)));
|
||
sibParts.push(sibCode);
|
||
log('CONTEXT-OK d='+depth+' src=sibling trail='+_debugTrail.join(' > '));
|
||
_lastContextDebug = _debugTrail.join(' > ');
|
||
return sibParts.join(' \u2014 ');
|
||
}
|
||
}
|
||
}
|
||
node = node.parentElement;
|
||
}
|
||
if (_sawCodeEls && _allSkipped) {
|
||
log('CONTEXT-SKIP-ALL trail='+_debugTrail.join(' > '));
|
||
_lastContextDebug = _debugTrail.join(' > ');
|
||
return cleanButtonText(btn);
|
||
}
|
||
var ariaLabel = btn.getAttribute('aria-label') || btn.getAttribute('title') || '';
|
||
log('CONTEXT-FAIL trail='+_debugTrail.join(' > '));
|
||
_lastContextDebug = _debugTrail.join(' > ');
|
||
if (ariaLabel && ariaLabel.length > 5) return ariaLabel;
|
||
return cleanButtonText(btn);
|
||
}
|
||
var _lastContextDebug = '';
|
||
|
||
function extractStepContext(btn) {
|
||
var stepEl = getStepContainer(btn);
|
||
if (!stepEl) {
|
||
// v9 FALLBACK: no data-step-index ??climb DOM for pre/code blocks
|
||
return extractContextFromNearby(btn);
|
||
}
|
||
|
||
var stepIdx = stepEl.getAttribute('data-step-index') || '?';
|
||
|
||
// Get step header text (first line, usually has the tool name/command)
|
||
var header = stepEl.querySelector('[class*="cursor-pointer"]');
|
||
var headerText = '';
|
||
if (header) {
|
||
// Clone and strip icons/buttons
|
||
var hClone = header.cloneNode(true);
|
||
var hRemove = hClone.querySelectorAll('button, svg, [class*="icon"], .google-symbols, .material-symbols-outlined');
|
||
for (var i = 0; i < hRemove.length; i++) {
|
||
if (hRemove[i].parentNode) hRemove[i].parentNode.removeChild(hRemove[i]);
|
||
}
|
||
headerText = (hClone.textContent || '').trim().substring(0, 300);
|
||
// Clean noise
|
||
headerText = cleanLines(headerText);
|
||
}
|
||
|
||
// Try to get code/pre content (command detail)
|
||
var codeEl = stepEl.querySelector('pre, code');
|
||
var codeText = '';
|
||
if (codeEl) {
|
||
codeText = cleanLines((codeEl.textContent || '').trim().substring(0, 400));
|
||
}
|
||
|
||
// Try aria-label on button
|
||
var ariaLabel = btn.getAttribute('aria-label') || btn.getAttribute('title') || '';
|
||
|
||
var parts = [];
|
||
if (headerText) parts.push(headerText);
|
||
if (codeText && !headerText.includes(codeText.substring(0, 20))) parts.push(codeText);
|
||
if (ariaLabel && ariaLabel.length > 5 && !headerText.includes(ariaLabel)) parts.push(ariaLabel);
|
||
|
||
var result = parts.join(' ??');
|
||
if (!result) result = cleanButtonText(btn);
|
||
return 'Step #' + stepIdx + ': ' + result;
|
||
}
|
||
|
||
function extractContext(b) {
|
||
return extractStepContext(b);
|
||
}
|
||
|
||
var ACTION_WORDS = ['Allow', 'Run', 'Approve', 'Accept', 'Always allow', 'Always run', 'Retry'];
|
||
var REJECT_WORDS = ['Reject', 'Cancel', 'Deny', 'Stop', 'Decline', 'Dismiss'];
|
||
|
||
function isActionBtn(txt) {
|
||
for(var i=0; i<ACTION_WORDS.length; i++) {
|
||
if(txt.indexOf(ACTION_WORDS[i]) !== -1) return true;
|
||
}
|
||
// v9: Removed "Running N commands" ??it's a group header, not an approval button
|
||
return false;
|
||
}
|
||
function isRejectBtn(txt) {
|
||
for(var i=0; i<REJECT_WORDS.length; i++) {
|
||
if(txt.indexOf(REJECT_WORDS[i]) !== -1) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function collectSiblingButtons(container,triggerBtn){
|
||
if(!container)return [];
|
||
// v11: Try 5 container levels to find Cancel and other approval buttons
|
||
var containers = [container];
|
||
var cur = container;
|
||
for (var lvl = 0; lvl < 5 && cur.parentElement; lvl++) {
|
||
cur = cur.parentElement;
|
||
containers.push(cur);
|
||
}
|
||
var result=[];
|
||
var seen={};
|
||
for(var ci=0;ci<containers.length;ci++){
|
||
var siblings=containers[ci].querySelectorAll('button');
|
||
for(var i=0;i<siblings.length;i++){
|
||
var sb=siblings[i];
|
||
if(sb.disabled||sb.hidden||(!sb.offsetParent&&sb.style.display!=='fixed'))continue;
|
||
var stxt = cleanButtonText(sb);
|
||
if(stxt.length <= 1) continue;
|
||
// Skip group headers
|
||
if (/^Running\\s*\\d+\\s*commands?$/i.test(stxt)) continue;
|
||
if(!isActionBtn(stxt) && !isRejectBtn(stxt)) continue;
|
||
// Dedup by text
|
||
if(seen[stxt])continue;
|
||
seen[stxt]=true;
|
||
result.push({btn:sb,text:stxt,isPrimary:(sb===triggerBtn)});
|
||
}
|
||
// v11: Only stop if we found BOTH action AND reject buttons at this level
|
||
var hasAction = false, hasReject = false;
|
||
for (var ri=0;ri<result.length;ri++) {
|
||
if (isActionBtn(result[ri].text)) hasAction = true;
|
||
if (isRejectBtn(result[ri].text)) hasReject = true;
|
||
}
|
||
if(hasAction && hasReject) break;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
var HARDCODED_PORT=${_port};
|
||
function tryPingAsync(port){
|
||
return fetch('http://127.0.0.1:'+port+'/ping?t='+Date.now(),{signal:AbortSignal.timeout(2000)})
|
||
.then(function(r){return r.text();}).then(function(t){return t==='pong';}).catch(function(){return false;});
|
||
}
|
||
|
||
function discoverPort(cb){
|
||
var attempts=0;
|
||
var timer=setInterval(function(){
|
||
attempts++;
|
||
var items = document.querySelectorAll('[aria-label^="Gravity Bridge Control"], [title^="Gravity Bridge Control"]');
|
||
if (items.length > 0) {
|
||
var text = items[0].getAttribute('aria-label') || items[0].getAttribute('title') || '';
|
||
var m = text.match(/port:(\\d+)/);
|
||
if (m && m[1]) {
|
||
clearInterval(timer);
|
||
tryPingAsync(parseInt(m[1], 10)).then(function(ok){ cb(ok ? parseInt(m[1],10) : HARDCODED_PORT); });
|
||
return;
|
||
}
|
||
}
|
||
if(attempts>1){
|
||
clearInterval(timer);
|
||
tryPingAsync(HARDCODED_PORT).then(function(){ cb(HARDCODED_PORT); });
|
||
}
|
||
},500);
|
||
}
|
||
|
||
discoverPort(function(port){
|
||
BASE='http://127.0.0.1:'+port;
|
||
_ready=true;
|
||
startObserver();
|
||
});
|
||
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
// v8: FULL DOM STRUCTURE DUMP (unconditional ??no selector dependency)
|
||
// Dumps entire document.body tree to /dump-html for real DOM analysis
|
||
// Auto-triggers at 5s, 15s, 60s after load to capture React-rendered state
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
|
||
var _dumpCount=0;
|
||
var MAX_DUMPS=8;
|
||
var _conversationDumpCount=0;
|
||
|
||
function walkNode(el, depth, maxDepth, maxChildren) {
|
||
if (depth > maxDepth) return {tag:'MAX',text:'depth limit'};
|
||
if (!el || !el.tagName) return null;
|
||
var info = {
|
||
tag: el.tagName ? el.tagName.toLowerCase() : '#text',
|
||
cls: '',
|
||
attrs: {},
|
||
text: '',
|
||
children: []
|
||
};
|
||
// Capture className (string or SVGAnimatedString)
|
||
if (el.className) {
|
||
if (typeof el.className === 'string') info.cls = el.className.substring(0, 300);
|
||
else if (el.className.baseVal) info.cls = el.className.baseVal.substring(0, 300);
|
||
}
|
||
// Capture ALL attributes that might help identify structure
|
||
if (el.attributes) {
|
||
for (var ai = 0; ai < el.attributes.length; ai++) {
|
||
var attr = el.attributes[ai];
|
||
var n = attr.name;
|
||
if (n === 'class' || n === 'style') continue; // class already captured, style is noise
|
||
info.attrs[n] = (attr.value || '').substring(0, 200);
|
||
}
|
||
}
|
||
// For leaf nodes or shallow nodes, capture text content
|
||
if (!el.children || el.children.length === 0) {
|
||
var t = (el.textContent || '').trim();
|
||
if (t.length > 0) info.text = t.substring(0, 200);
|
||
}
|
||
// Recurse children
|
||
if (el.children) {
|
||
var limit = Math.min(el.children.length, maxChildren);
|
||
for (var ci = 0; ci < limit; ci++) {
|
||
var childInfo = walkNode(el.children[ci], depth + 1, maxDepth, maxChildren);
|
||
if (childInfo) info.children.push(childInfo);
|
||
}
|
||
if (el.children.length > limit) {
|
||
info.children.push({tag: 'TRUNC', text: '+' + (el.children.length - limit) + ' more children'});
|
||
}
|
||
}
|
||
return info;
|
||
}
|
||
|
||
function dumpDOMStructure() {
|
||
if (!_ready || _dumpCount >= MAX_DUMPS) return;
|
||
if (!document.body) return;
|
||
|
||
// Only dump if there are enough elements (DOM has actually rendered)
|
||
var totalEls = document.querySelectorAll('*').length;
|
||
if (totalEls < 20) {
|
||
log('DOM dump skipped: only ' + totalEls + ' elements (not rendered yet)');
|
||
return;
|
||
}
|
||
_dumpCount++;
|
||
|
||
// Walk entire body with generous limits
|
||
var structure = walkNode(document.body, 0, 15, 30);
|
||
|
||
// Also gather quick-reference info
|
||
var quickInfo = {
|
||
totalElements: totalEls,
|
||
title: document.title,
|
||
url: window.location.href,
|
||
hasConversationView: !!document.querySelector('[data-testid="conversation-view"]'),
|
||
hasStepIndex: !!document.querySelector('[data-step-index]'),
|
||
hasBotColor: !!document.querySelector('.text-ide-message-block-bot-color'),
|
||
hasMarkdownBody: !!document.querySelector('.markdown-body'),
|
||
hasProse: !!document.querySelector('.prose'),
|
||
buttons: [],
|
||
dataTestIds: [],
|
||
dataAttrs: []
|
||
};
|
||
|
||
// Collect all data-testid values in the page
|
||
var testIdEls = document.querySelectorAll('[data-testid]');
|
||
for (var ti = 0; ti < testIdEls.length; ti++) {
|
||
var tid = testIdEls[ti].getAttribute('data-testid');
|
||
if (quickInfo.dataTestIds.indexOf(tid) === -1) quickInfo.dataTestIds.push(tid);
|
||
}
|
||
|
||
// Collect all unique data-* attribute names
|
||
var allEls = document.querySelectorAll('*');
|
||
var seenAttrs = {};
|
||
for (var ei = 0; ei < Math.min(allEls.length, 2000); ei++) {
|
||
var elAttrs = allEls[ei].attributes;
|
||
if (!elAttrs) continue;
|
||
for (var ai2 = 0; ai2 < elAttrs.length; ai2++) {
|
||
var aName = elAttrs[ai2].name;
|
||
if (aName.startsWith('data-') && !seenAttrs[aName]) {
|
||
seenAttrs[aName] = true;
|
||
quickInfo.dataAttrs.push(aName + '=' + (elAttrs[ai2].value || '').substring(0, 50));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Collect visible buttons
|
||
var allBtns = document.querySelectorAll('button, [role="button"]');
|
||
for (var bi2 = 0; bi2 < Math.min(allBtns.length, 50); bi2++) {
|
||
var btnEl = allBtns[bi2];
|
||
var btnText = (btnEl.textContent || '').trim().substring(0, 80);
|
||
if (btnText.length > 0) {
|
||
quickInfo.buttons.push({
|
||
text: btnText,
|
||
tag: btnEl.tagName.toLowerCase(),
|
||
cls: ((btnEl.className && typeof btnEl.className === 'string') ? btnEl.className : '').substring(0, 150),
|
||
visible: !!(btnEl.offsetParent || btnEl.style.display === 'fixed')
|
||
});
|
||
}
|
||
}
|
||
|
||
var payload = JSON.stringify({
|
||
timestamp: new Date().toISOString(),
|
||
source: 'v8_full_body_dump',
|
||
dumpIndex: _dumpCount,
|
||
quickInfo: quickInfo,
|
||
body: structure
|
||
});
|
||
log('DOM dump #' + _dumpCount + ': ' + totalEls + ' elements, ' + payload.length + ' bytes');
|
||
fetch(BASE + '/dump-html', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: payload
|
||
}).catch(function(e) { log('DOM dump failed: ' + e.message); });
|
||
}
|
||
|
||
// Auto-dump at multiple intervals to catch React rendering stages
|
||
function scheduleAutoDumps() {
|
||
setTimeout(function(){ log('Auto-dump @5s'); dumpDOMStructure(); }, 5000);
|
||
setTimeout(function(){ log('Auto-dump @15s'); dumpDOMStructure(); }, 15000);
|
||
setTimeout(function(){ log('Auto-dump @60s'); dumpDOMStructure(); }, 60000);
|
||
}
|
||
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
// v15: AG-NATIVE + CASCADE DUAL CHAT BODY SCANNING
|
||
// AG Native: #conversation > ... > .leading-relaxed.select-text
|
||
// Cascade: [data-testid="conversation-view"] > [data-step-index]
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
|
||
var _lastScrapedStepIndex = -1;
|
||
var _lastStepText = '';
|
||
var _lastStepTextTime = 0;
|
||
var _lastStepTextSent = false;
|
||
var _lastResponseBlockCount = 0; // track number of response blocks for AG Native
|
||
|
||
function convertNodeToMarkdown(node) {
|
||
if (!node) return '';
|
||
if (node.nodeType === 3) return node.textContent; // Text node
|
||
if (node.nodeType !== 1) return ''; // Skip other node types
|
||
|
||
var tag = node.tagName.toLowerCase();
|
||
|
||
// Skip hidden or UI elements
|
||
if (tag === 'style' || tag === 'script' || tag === 'noscript' || tag === 'button' || tag === 'svg') return '';
|
||
var cls = '';
|
||
if (typeof node.className === 'string') cls = node.className;
|
||
else if (node.className && node.className.baseVal) cls = node.className.baseVal;
|
||
|
||
if (cls && (cls.indexOf('google-symbols') !== -1 || cls.indexOf('material-icons') !== -1 || cls.indexOf('copy') !== -1 || cls.indexOf('codicon') !== -1)) return '';
|
||
|
||
var childrenMd = '';
|
||
for (var i = 0; i < node.childNodes.length; i++) {
|
||
childrenMd += convertNodeToMarkdown(node.childNodes[i]);
|
||
}
|
||
|
||
// TABLE: Discord doesn't support markdown tables, so convert to fixed-width code block
|
||
if (tag === 'table') {
|
||
var rows = node.querySelectorAll('tr');
|
||
if (!rows || rows.length === 0) return childrenMd;
|
||
var grid = [];
|
||
var colWidths = [];
|
||
for (var ri = 0; ri < rows.length; ri++) {
|
||
var cells = rows[ri].querySelectorAll('th, td');
|
||
var row = [];
|
||
for (var ci = 0; ci < cells.length; ci++) {
|
||
var cellText = (cells[ci].textContent || '').trim();
|
||
row.push(cellText);
|
||
if (!colWidths[ci] || cellText.length > colWidths[ci]) colWidths[ci] = cellText.length;
|
||
}
|
||
grid.push(row);
|
||
}
|
||
// Build fixed-width text
|
||
var tbl = '';
|
||
for (var ri2 = 0; ri2 < grid.length; ri2++) {
|
||
var line = '';
|
||
for (var ci2 = 0; ci2 < colWidths.length; ci2++) {
|
||
var cell = grid[ri2][ci2] || '';
|
||
var pad = colWidths[ci2] - cell.length;
|
||
var padding = '';
|
||
for (var pi = 0; pi < pad; pi++) padding += ' ';
|
||
line += (ci2 > 0 ? ' | ' : '') + cell + padding;
|
||
}
|
||
tbl += line + '\\n';
|
||
// Add separator after header row (first row)
|
||
if (ri2 === 0) {
|
||
var sep = '';
|
||
for (var si2 = 0; si2 < colWidths.length; si2++) {
|
||
var dashes = '';
|
||
for (var di = 0; di < colWidths[si2]; di++) dashes += '-';
|
||
sep += (si2 > 0 ? '-+-' : '') + dashes;
|
||
}
|
||
tbl += sep + '\\n';
|
||
}
|
||
}
|
||
return '\\n' + String.fromCharCode(96,96,96) + '\\n' + tbl + String.fromCharCode(96,96,96) + '\\n';
|
||
}
|
||
|
||
switch (tag) {
|
||
case 'h1': return '\\n# ' + childrenMd.trim() + '\\n';
|
||
case 'h2': return '\\n## ' + childrenMd.trim() + '\\n';
|
||
case 'h3': return '\\n### ' + childrenMd.trim() + '\\n';
|
||
case 'h4': return '\\n#### ' + childrenMd.trim() + '\\n';
|
||
case 'p': return '\\n' + childrenMd.trim() + '\\n';
|
||
case 'div':
|
||
// Treat specific divs as blocks if they end up behaving like paragraphs
|
||
if (cls.indexOf('block') !== -1 || cls.indexOf('message') !== -1) return '\\n' + childrenMd.trim() + '\\n';
|
||
return childrenMd;
|
||
case 'br': return '\\n';
|
||
case 'strong':
|
||
case 'b': return '**' + childrenMd + '**';
|
||
case 'em':
|
||
case 'i': return '*' + childrenMd + '*';
|
||
case 'a':
|
||
var href = node.getAttribute('href') || '';
|
||
return '[' + childrenMd + '](' + href + ')';
|
||
case 'code': return (node.parentNode && node.parentNode.tagName === 'PRE') ? childrenMd : (String.fromCharCode(96) + childrenMd + String.fromCharCode(96));
|
||
case 'pre': return '\\n' + String.fromCharCode(96,96,96) + '\\n' + childrenMd.trim() + '\\n' + String.fromCharCode(96,96,96) + '\\n';
|
||
case 'li':
|
||
var prefix = '- ';
|
||
if (node.parentNode && node.parentNode.tagName.toLowerCase() === 'ol') {
|
||
var idx = 1;
|
||
var curr = node.previousSibling;
|
||
while(curr) { if (curr.nodeType === 1 && curr.tagName.toLowerCase() === 'li') idx++; curr = curr.previousSibling; }
|
||
prefix = idx + '. ';
|
||
}
|
||
return '\\n' + prefix + childrenMd.trim();
|
||
case 'ul':
|
||
case 'ol': return '\\n' + childrenMd + '\\n';
|
||
case 'blockquote': return '\\n> ' + childrenMd.trim().split('\\n').join('\\n> ') + '\\n';
|
||
// Table sub-elements: already handled by the table case above via querySelectorAll
|
||
case 'thead':
|
||
case 'tbody':
|
||
case 'tfoot':
|
||
case 'tr':
|
||
case 'th':
|
||
case 'td': return '';
|
||
default: return childrenMd;
|
||
}
|
||
}
|
||
|
||
function extractCleanStepText(stepEl) {
|
||
if (!stepEl) return '';
|
||
|
||
// Clone the step element so we can strip UI elements without affecting the DOM
|
||
var clone = stepEl.cloneNode(true);
|
||
|
||
// v16: Remove style/script/noscript elements FIRST
|
||
var styleEls = clone.querySelectorAll('style, script, noscript, link[rel="stylesheet"]');
|
||
for (var si = 0; si < styleEls.length; si++) {
|
||
if (styleEls[si].parentNode) styleEls[si].parentNode.removeChild(styleEls[si]);
|
||
}
|
||
|
||
// Remove all buttons (Run, Allow, Cancel, etc.)
|
||
var buttons = clone.querySelectorAll('button');
|
||
for (var bi = 0; bi < buttons.length; bi++) {
|
||
if (buttons[bi].parentNode) buttons[bi].parentNode.removeChild(buttons[bi]);
|
||
}
|
||
|
||
// Remove all SVGs and icon elements
|
||
var icons = clone.querySelectorAll('svg, .google-symbols, .material-symbols-outlined, .material-icons, [class*="codicon"], [class*="icon"]');
|
||
for (var ii = 0; ii < icons.length; ii++) {
|
||
if (icons[ii].parentNode) icons[ii].parentNode.removeChild(icons[ii]);
|
||
}
|
||
|
||
// Remove copy buttons and their containers
|
||
var copyBtns = clone.querySelectorAll('[class*="copy"], [aria-label*="copy"], [title*="Copy"]');
|
||
for (var ci = 0; ci < copyBtns.length; ci++) {
|
||
if (copyBtns[ci].parentNode) copyBtns[ci].parentNode.removeChild(copyBtns[ci]);
|
||
}
|
||
|
||
// Try to get text from markdown rendering area first
|
||
// AG Native uses .leading-relaxed.select-text, Cascade uses .markdown-body/.prose
|
||
var mdEl = clone.querySelector('.markdown-body, .prose, [class*="markdown"], [class*="rendered"]') || clone;
|
||
|
||
// Use our custom DOM-to-Markdown parser instead of innerText
|
||
var rawText = convertNodeToMarkdown(mdEl).trim();
|
||
|
||
// v18 FIX: DO NOT apply cleanLines to full markdown content, it destroys valid code blocks
|
||
// Safely remove "Thought for X" lines only
|
||
rawText = rawText.replace(/Thought for \\d+s?/gi, '');
|
||
rawText = rawText.replace(/Thought for a few seconds/gi, '');
|
||
|
||
// Cleanup multiple empty lines
|
||
var lines = rawText.split('\\n');
|
||
var finalLines = [];
|
||
var lastEmpty = false;
|
||
for (var i = 0; i < lines.length; i++) {
|
||
var line = lines[i].replace(/\\s+$/, '');
|
||
if (line.length === 0) {
|
||
if (!lastEmpty && finalLines.length > 0) {
|
||
finalLines.push('');
|
||
lastEmpty = true;
|
||
}
|
||
} else {
|
||
finalLines.push(line);
|
||
lastEmpty = false;
|
||
}
|
||
}
|
||
|
||
// v19: Post-process ??wrap markdown table patterns in code blocks for Discord
|
||
// AG Native renders tables as divs, not <table> HTML, so DOM-level handler can't catch them.
|
||
// Detect consecutive lines with pipe separators (| col1 | col2 |) and wrap in code block fences
|
||
var bt = String.fromCharCode(96, 96, 96);
|
||
var result = [];
|
||
var tableBlock = [];
|
||
var inCodeBlock = false;
|
||
for (var fi = 0; fi < finalLines.length; fi++) {
|
||
var fl = finalLines[fi];
|
||
// Track existing code blocks to avoid double-wrapping
|
||
if (fl.trim().indexOf(bt) === 0) {
|
||
inCodeBlock = !inCodeBlock;
|
||
// Flush any pending table block before code block marker
|
||
if (tableBlock.length > 0) {
|
||
result.push(bt);
|
||
for (var ti = 0; ti < tableBlock.length; ti++) result.push(tableBlock[ti]);
|
||
result.push(bt);
|
||
tableBlock = [];
|
||
}
|
||
result.push(fl);
|
||
continue;
|
||
}
|
||
if (inCodeBlock) {
|
||
result.push(fl);
|
||
continue;
|
||
}
|
||
// Detect table row: has at least 2 pipe characters and content between them
|
||
var pipeCount = 0;
|
||
for (var pc = 0; pc < fl.length; pc++) { if (fl.charAt(pc) === '|') pipeCount++; }
|
||
var isTableRow = pipeCount >= 2 && fl.trim().charAt(0) === '|';
|
||
var isSeparator = isTableRow && /^[\\s|:-]+$/.test(fl.trim());
|
||
if (isTableRow) {
|
||
tableBlock.push(fl);
|
||
} else {
|
||
// Flush table block if it had enough rows (header + separator + data)
|
||
if (tableBlock.length >= 2) {
|
||
result.push(bt);
|
||
for (var ti2 = 0; ti2 < tableBlock.length; ti2++) result.push(tableBlock[ti2]);
|
||
result.push(bt);
|
||
} else {
|
||
// Not a real table, push lines back normally
|
||
for (var ti3 = 0; ti3 < tableBlock.length; ti3++) result.push(tableBlock[ti3]);
|
||
}
|
||
tableBlock = [];
|
||
result.push(fl);
|
||
}
|
||
}
|
||
// Flush trailing table block
|
||
if (tableBlock.length >= 2) {
|
||
result.push(bt);
|
||
for (var ti4 = 0; ti4 < tableBlock.length; ti4++) result.push(tableBlock[ti4]);
|
||
result.push(bt);
|
||
} else {
|
||
for (var ti5 = 0; ti5 < tableBlock.length; ti5++) result.push(tableBlock[ti5]);
|
||
}
|
||
|
||
return result.join('\\n').substring(0, 3500);
|
||
}
|
||
|
||
function scanChatBodies() {
|
||
if (!_ready) return;
|
||
|
||
// One-time DOM dump
|
||
dumpDOMStructure();
|
||
|
||
// ?? STRATEGY 1: AG Native ??#conversation or .antigravity-agent-side-panel ??
|
||
var cv = document.querySelector('#conversation');
|
||
if (!cv) {
|
||
cv = document.querySelector('.antigravity-agent-side-panel');
|
||
}
|
||
// v19: Fallback ??find conversation by tracing from known content elements
|
||
if (!cv) {
|
||
var probe = document.querySelector('.leading-relaxed.select-text') || document.querySelector('.text-ide-message-block-bot-color');
|
||
if (probe) {
|
||
// Walk up to find a reasonable container (has overflow-y or is big enough)
|
||
var p = probe.parentElement;
|
||
for (var pi2 = 0; pi2 < 10 && p && p !== document.body; pi2++) {
|
||
var pCls = (typeof p.className === 'string') ? p.className : '';
|
||
if (pCls.indexOf('overflow') !== -1 || p.children.length > 3) {
|
||
cv = p;
|
||
break;
|
||
}
|
||
p = p.parentElement;
|
||
}
|
||
if (!cv && probe.parentElement) cv = probe.parentElement.parentElement || probe.parentElement;
|
||
}
|
||
}
|
||
if (cv) {
|
||
// v20: Dump CV structure for first 3 scans to ensure we capture it (even with stale HTML cache)
|
||
if (_conversationDumpCount < 3) {
|
||
_conversationDumpCount++;
|
||
log('CV found via: ' + (cv.id || (typeof cv.className === 'string' ? cv.className : cv.tagName) || 'unknown').substring(0, 100));
|
||
// Log all unique class names under #conversation for selector discovery
|
||
var allCvEls = cv.querySelectorAll('*');
|
||
var clsSet = {};
|
||
for (var ci2 = 0; ci2 < allCvEls.length; ci2++) {
|
||
var cn = allCvEls[ci2].className;
|
||
if (typeof cn === 'string' && cn.length > 0) {
|
||
var parts = cn.split(/\\s+/);
|
||
for (var pi = 0; pi < parts.length; pi++) {
|
||
if (parts[pi].length > 3 && !clsSet[parts[pi]]) clsSet[parts[pi]] = true;
|
||
}
|
||
}
|
||
}
|
||
var clsList = Object.keys(clsSet).sort().join(', ');
|
||
log('CV-CLASSES (' + Object.keys(clsSet).length + '): ' + clsList.substring(0, 1500));
|
||
// v19: Log direct children to discover message block structure
|
||
var cvKids = cv.children;
|
||
log('CV-CHILDREN (' + cvKids.length + '):');
|
||
for (var ck = 0; ck < Math.min(cvKids.length, 15); ck++) {
|
||
var kid = cvKids[ck];
|
||
var kidCls = (typeof kid.className === 'string') ? kid.className : '';
|
||
var kidText = (kid.textContent || '').trim().substring(0, 60);
|
||
log(' child[' + ck + '] tag=' + kid.tagName + ' cls=' + kidCls.substring(0, 120) + ' text=' + kidText);
|
||
}
|
||
// v22: Deep-dive into gap-8 container to find individual message blocks
|
||
var msgContainer = cv.querySelector('.gap-8') || cv.children[0];
|
||
if (msgContainer) {
|
||
var msgKids = msgContainer.children;
|
||
log('MSG-BLOCKS (' + msgKids.length + '):');
|
||
for (var mk = 0; mk < Math.min(msgKids.length, 30); mk++) {
|
||
var mb = msgKids[mk];
|
||
var mbCls = (typeof mb.className === 'string') ? mb.className : '';
|
||
var mbText = (mb.textContent || '').trim().substring(0, 80);
|
||
var hasLeadingRelaxed = mb.querySelector('.leading-relaxed.select-text') ? 'Y' : 'N';
|
||
var firstChildCls = (mb.children[0] && typeof mb.children[0].className === 'string') ? mb.children[0].className : '';
|
||
log(' msg[' + mk + '] cls=' + mbCls.substring(0, 120) + ' lr=' + hasLeadingRelaxed + ' fc=' + firstChildCls.substring(0, 80) + ' text=' + mbText.substring(0, 60));
|
||
}
|
||
}
|
||
// Force a dump with conversation context
|
||
dumpDOMStructure();
|
||
}
|
||
|
||
// v22: AI response = .leading-relaxed.select-text, User message = .select-text.rounded-lg (Esn component, msn class)
|
||
// Source: jetskiAgent/main.js ??msn="bg-gray-500/10 border border-gray-500/20 p-2 rounded-lg w-full text-sm select-text"
|
||
var responseBlocks = cv.querySelectorAll('.leading-relaxed.select-text, .select-text.rounded-lg');
|
||
|
||
if (responseBlocks.length > 0) {
|
||
// v22: Filter out thinking/reasoning blocks ??they have ancestor with max-h-[200px]
|
||
// These are internal AI reasoning and should NOT be relayed to Discord
|
||
var filteredBlocks = [];
|
||
for (var fbi = 0; fbi < responseBlocks.length; fbi++) {
|
||
var isThinking = false;
|
||
var ancestor = responseBlocks[fbi].parentElement;
|
||
for (var depth = 0; ancestor && depth < 5; depth++) {
|
||
var aCls = (typeof ancestor.className === 'string') ? ancestor.className : '';
|
||
if (aCls.indexOf('max-h-[200px]') !== -1 || aCls.indexOf('max-h-[150px]') !== -1) {
|
||
isThinking = true;
|
||
break;
|
||
}
|
||
ancestor = ancestor.parentElement;
|
||
}
|
||
if (!isThinking) filteredBlocks.push(responseBlocks[fbi]);
|
||
}
|
||
if (filteredBlocks.length === 0) return;
|
||
|
||
// Process the LAST (most recent) non-thinking response block
|
||
var lastBlock = filteredBlocks[filteredBlocks.length - 1];
|
||
|
||
// Skip if already scraped
|
||
if (lastBlock.dataset.agChatScraped === 'true' || lastBlock.dataset.agChatScraped === 'pending') {
|
||
// Check for NEW blocks since last scrape
|
||
if (filteredBlocks.length > _lastResponseBlockCount) {
|
||
// New block appeared ??process it
|
||
for (var rbi = filteredBlocks.length - 1; rbi >= 0; rbi--) {
|
||
if (filteredBlocks[rbi].dataset.agChatScraped !== 'true' && filteredBlocks[rbi].dataset.agChatScraped !== 'pending') {
|
||
lastBlock = filteredBlocks[rbi];
|
||
break;
|
||
}
|
||
}
|
||
if (lastBlock.dataset.agChatScraped === 'true' || lastBlock.dataset.agChatScraped === 'pending') return;
|
||
} else {
|
||
return; // Already scraped, no new blocks
|
||
}
|
||
}
|
||
|
||
var blockText = extractCleanStepText(lastBlock);
|
||
|
||
var clsStr = (typeof lastBlock.className === 'string') ? lastBlock.className : '';
|
||
// v19: Log block classes for user message selector discovery
|
||
var parentCls = lastBlock.parentElement ? ((typeof lastBlock.parentElement.className === 'string') ? lastBlock.parentElement.className : '') : '';
|
||
var grandCls = (lastBlock.parentElement && lastBlock.parentElement.parentElement) ? ((typeof lastBlock.parentElement.parentElement.className === 'string') ? lastBlock.parentElement.parentElement.className : '') : '';
|
||
log('user-cls-debug block=' + clsStr.substring(0, 150) + ' | parent=' + parentCls.substring(0, 150) + ' | grand=' + grandCls.substring(0, 150) + ' | text=' + (blockText||'').substring(0, 50));
|
||
// v22: Detect user message: has select-text + rounded-lg but NOT leading-relaxed
|
||
var isUser = (clsStr.indexOf('rounded-lg') !== -1 && clsStr.indexOf('leading-relaxed') === -1) || clsStr.indexOf('user-color') !== -1;
|
||
var role = isUser ? 'user' : 'bot';
|
||
|
||
// Bot messages often start empty and stream in. User messages are usually immediate.
|
||
if (blockText && (blockText.length > 30 || isUser && blockText.length > 0)) {
|
||
// QUALITY CHECK: Skip if the text is mostly short lines (UI artifacts), BUT skip this check for user messages
|
||
if (!isUser) {
|
||
var lines = blockText.split('\\n').filter(function(l) { return l.trim().length > 0; });
|
||
var longLines = lines.filter(function(l) { return l.trim().length > 20; });
|
||
if (longLines.length === 0) {
|
||
log('AG-Native: skipped (no long lines, likely UI noise)');
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Wait for content to stabilize (3s no change)
|
||
if (_lastStepText !== blockText) {
|
||
_lastStepText = blockText;
|
||
_lastStepTextTime = Date.now();
|
||
_lastStepTextSent = false;
|
||
return; // Wait for next scan cycle
|
||
}
|
||
|
||
if (_lastStepTextSent) return;
|
||
// Bot needs 3s to stabilize, User just needs 500ms
|
||
var waitTime = isUser ? 500 : 3000;
|
||
if (Date.now() - _lastStepTextTime < waitTime) return; // Still waiting
|
||
|
||
// v21: DOM-based chat relay RE-ENABLED ??GetCascadeTrajectorySteps does NOT
|
||
// return steps for in-progress cascades, making Step Probe RT-CAPTURE useless.
|
||
// Observer DOM extraction is the ONLY real-time path for AI response relay.
|
||
_lastStepTextSent = true;
|
||
_lastResponseBlockCount = filteredBlocks.length;
|
||
lastBlock.dataset.agChatScraped = 'pending';
|
||
|
||
log('AG-Native chat relay [' + role + ']: blocks=' + filteredBlocks.length + ' text=' + blockText.length + ' chars');
|
||
(function(el, txt, count, r) {
|
||
fetch(BASE + '/chat', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text: txt, source: 'ag_native_block_' + count, block_index: count, role: r })
|
||
}).then(function() { el.dataset.agChatScraped = 'true'; log('AG-Native chat sent OK'); })
|
||
.catch(function(e) { el.dataset.agChatScraped = 'false'; log('AG-Native chat send error: ' + e.message); });
|
||
})(lastBlock, blockText, filteredBlocks.length, role);
|
||
}
|
||
return; // AG Native path handled ??don't fall through to Cascade path
|
||
}
|
||
}
|
||
|
||
// ?? STRATEGY 2: Cascade ??[data-testid="conversation-view"] ??
|
||
cv = document.querySelector('[data-testid="conversation-view"]');
|
||
if (!cv) {
|
||
// FALLBACK: Try older selectors
|
||
cv = document.querySelector('[class*="conversation"], [class*="chat-container"]');
|
||
if (!cv) return;
|
||
}
|
||
|
||
// Find ALL step elements within the conversation
|
||
var stepEls = cv.querySelectorAll('[data-step-index]');
|
||
if (stepEls.length === 0) {
|
||
// FALLBACK: Try text-ide-message-block-bot-color directly
|
||
var botMsgs = cv.querySelectorAll('.text-ide-message-block-bot-color');
|
||
if (botMsgs.length === 0) return;
|
||
// Use the last bot message as a pseudo-step
|
||
var lastBot = botMsgs[botMsgs.length - 1];
|
||
if (lastBot.dataset.agChatScraped === 'true' || lastBot.dataset.agChatScraped === 'pending') return;
|
||
var botText = extractCleanStepText(lastBot);
|
||
if (botText && botText.length > 20) {
|
||
if (_lastStepText !== botText) {
|
||
_lastStepText = botText;
|
||
_lastStepTextTime = Date.now();
|
||
_lastStepTextSent = false;
|
||
} else if (!_lastStepTextSent && (Date.now() - _lastStepTextTime > 3000)) {
|
||
_lastStepTextSent = true;
|
||
lastBot.dataset.agChatScraped = 'pending';
|
||
fetch(BASE + '/chat', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text: botText, source: 'fallback_bot_msg' })
|
||
}).then(function() { lastBot.dataset.agChatScraped = 'true'; })
|
||
.catch(function() { lastBot.dataset.agChatScraped = 'false'; });
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Find the LATEST step that has meaningful text content (AI response)
|
||
// Iterate backwards to find the most recent unscraped step
|
||
for (var si = stepEls.length - 1; si >= Math.max(0, stepEls.length - 5); si--) {
|
||
var stepEl = stepEls[si];
|
||
var stepIdx = parseInt(stepEl.getAttribute('data-step-index') || '-1', 10);
|
||
|
||
// Skip already scraped
|
||
if (stepEl.dataset.agChatScraped === 'true' || stepEl.dataset.agChatScraped === 'pending') continue;
|
||
|
||
// Skip steps we've already processed
|
||
if (stepIdx >= 0 && stepIdx <= _lastScrapedStepIndex) continue;
|
||
|
||
// Check if this step has substantial text content (not just a tool call header)
|
||
var stepText = extractCleanStepText(stepEl);
|
||
if (!stepText || stepText.length < 30) continue;
|
||
|
||
// QUALITY CHECK: Skip if the text is mostly short lines (UI artifacts)
|
||
var lines = stepText.split('\\n').filter(function(l) { return l.trim().length > 0; });
|
||
var longLines = lines.filter(function(l) { return l.trim().length > 20; });
|
||
if (longLines.length === 0) {
|
||
log('Step ' + stepIdx + ': skipped (no long lines, likely UI noise)');
|
||
continue;
|
||
}
|
||
|
||
// Wait for content to stabilize (3s no change)
|
||
if (_lastStepText !== stepText) {
|
||
_lastStepText = stepText;
|
||
_lastStepTextTime = Date.now();
|
||
_lastStepTextSent = false;
|
||
break; // Wait for next scan cycle
|
||
}
|
||
|
||
if (_lastStepTextSent) continue;
|
||
if (Date.now() - _lastStepTextTime < 3000) break; // Still waiting
|
||
|
||
// Content is stable ??send it
|
||
_lastStepTextSent = true;
|
||
_lastScrapedStepIndex = stepIdx;
|
||
stepEl.dataset.agChatScraped = 'pending';
|
||
|
||
log('Chat relay: step=' + stepIdx + ' text=' + stepText.length + ' chars');
|
||
(function(el, txt, idx) {
|
||
fetch(BASE + '/chat', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text: txt, source: 'step_' + idx, step_index: idx })
|
||
}).then(function() { el.dataset.agChatScraped = 'true'; })
|
||
.catch(function() { el.dataset.agChatScraped = 'false'; });
|
||
})(stepEl, stepText, stepIdx);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
// BUTTON SCANNING (approval detection)
|
||
// ?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧?먥븧
|
||
|
||
function scan(){
|
||
if(!_ready)return;
|
||
scanChatBodies();
|
||
var now=Date.now();
|
||
var allBtns=document.querySelectorAll('button, [role="button"], a.monaco-button, .monaco-text-button, vscode-button');
|
||
if(!allBtns.length)return;
|
||
|
||
// v25: One-shot debug ??find Accept/Reject elements in ANY tag (run once per 30s)
|
||
if (!scan._lastAcceptScan || now - scan._lastAcceptScan > 30000) {
|
||
scan._lastAcceptScan = now;
|
||
var allEls = document.querySelectorAll('button, a, div, span, [role="button"]');
|
||
for (var ai = 0; ai < allEls.length; ai++) {
|
||
var aTxt = (allEls[ai].textContent || '').trim();
|
||
if (aTxt.length > 2 && aTxt.length < 30 && /Accept|Reject all/i.test(aTxt)) {
|
||
log('ACCEPT-SCAN tag=' + allEls[ai].tagName + ' cls=' + (allEls[ai].className || '').substring(0,80) + ' txt=' + aTxt.substring(0,40) + ' oP=' + !!allEls[ai].offsetParent + ' dis=' + allEls[ai].disabled + ' hid=' + allEls[ai].hidden);
|
||
}
|
||
}
|
||
}
|
||
|
||
for(var j=0;j<allBtns.length;j++){
|
||
var b=allBtns[j];
|
||
// v24: Visibility check moved after txt extraction (see isDiffReviewBtn below)
|
||
|
||
var txt=cleanButtonText(b);
|
||
if(txt.length <= 1) continue;
|
||
|
||
// v9: Skip group header buttons ??not approval buttons
|
||
if (/^Running\\s*\\d+\\s*commands?$/i.test(txt)) continue;
|
||
|
||
// v24: Relaxed visibility check ??Accept all/Reject all buttons in AG Native
|
||
// editor bottom bar may have offsetParent===null (different rendering layer)
|
||
var isDiffReviewBtn = txt.includes('Accept') || txt === 'Reject all';
|
||
if(!isDiffReviewBtn && (b.disabled||b.hidden||(!b.offsetParent&&b.style.display!=='fixed')))continue;
|
||
|
||
if(!isActionBtn(txt)) continue;
|
||
// Skip inline code lens buttons
|
||
if (b.closest('.codelens-decoration') && !txt.includes('Accept')) {
|
||
continue;
|
||
}
|
||
|
||
var txtLow = txt.toLowerCase();
|
||
var matchedType = txt.includes('Accept') ? 'diff_review' : (txt === 'Retry' ? 'retry' : (txtLow.includes('run') || txtLow.includes('allow') ? 'command' : 'permission'));
|
||
|
||
// v7: Use step-index for more unique group key
|
||
var stepContainer = getStepContainer(b);
|
||
var stepIdx = stepContainer ? stepContainer.getAttribute('data-step-index') : 'none';
|
||
var groupKey = matchedType + '|' + stepIdx + '|' + btnId(b, matchedType);
|
||
if(_sent[groupKey])continue;
|
||
|
||
var container=b.parentElement;
|
||
var siblings=collectSiblingButtons(container,b);
|
||
if(siblings.length===0)siblings=[{btn:b,text:txt,isPrimary:true}];
|
||
|
||
var buttonsArr=[];
|
||
var btnRefs=[];
|
||
var bidList=[];
|
||
for(var si=0;si<siblings.length;si++){
|
||
var sb=siblings[si];
|
||
var sbid=btnId(sb.btn,matchedType);
|
||
buttonsArr.push({text:sb.text,index:si,is_primary:sb.isPrimary});
|
||
btnRefs.push(sb.btn);
|
||
bidList.push(sbid);
|
||
}
|
||
|
||
var desc=extractContext(b);
|
||
|
||
// v28: One-shot DOM structure dump for button context analysis
|
||
if (!window._btnDomDumped && txtLow.includes('run')) {
|
||
window._btnDomDumped = true;
|
||
var dumpLines = [];
|
||
var cur = b;
|
||
for (var dd = 0; dd < 10 && cur; dd++) {
|
||
var childSummary = [];
|
||
if (cur.children) {
|
||
for (var ci2 = 0; ci2 < Math.min(cur.children.length, 8); ci2++) {
|
||
var ch = cur.children[ci2];
|
||
var chTag = (ch.tagName || '?').toLowerCase();
|
||
var chCls = (typeof ch.className === 'string' ? ch.className : '').substring(0, 40);
|
||
var chText = (ch.textContent || '').substring(0, 30).replace(/\\n/g, ' ');
|
||
childSummary.push(chTag + '.' + chCls.split(' ')[0] + '=' + chText);
|
||
}
|
||
}
|
||
var sibSummary = [];
|
||
if (cur.parentElement) {
|
||
for (var si2 = 0; si2 < Math.min(cur.parentElement.children.length, 6); si2++) {
|
||
var sib = cur.parentElement.children[si2];
|
||
var sibTag = (sib.tagName || '?').toLowerCase();
|
||
var sibCls = (typeof sib.className === 'string' ? sib.className : '').substring(0, 30);
|
||
sibSummary.push(sibTag + '.' + sibCls.split(' ')[0] + (sib === cur ? '*' : ''));
|
||
}
|
||
}
|
||
dumpLines.push('d' + dd + ':' + (cur.tagName || '?').toLowerCase() + ' cls=' + (typeof cur.className === 'string' ? cur.className : '').substring(0, 50) + ' | children=[' + childSummary.join(', ') + '] | siblings=[' + sibSummary.join(', ') + ']');
|
||
cur = cur.parentElement;
|
||
}
|
||
log('BTN-DOM-DUMP txt=' + txt + ' desc=' + desc.substring(0, 40));
|
||
for (var ddi = 0; ddi < dumpLines.length; ddi++) {
|
||
log('BTN-DOM-DUMP ' + dumpLines[ddi]);
|
||
}
|
||
}
|
||
|
||
var rid=now.toString()+'_'+Math.random().toString(36).substring(2,6);
|
||
|
||
_sent[groupKey]={rid:rid,ts:now};
|
||
for(var mk=0;mk<bidList.length;mk++)_sent[bidList[mk]]={rid:rid,ts:now};
|
||
|
||
// v26: Deferred context (string match, not regex)
|
||
var _isGenericDesc = function(d) {
|
||
var t = d.trim().toLowerCase();
|
||
return t === 'always run' || t === 'run' || t === 'allow' || t === 'accept' || t === 'retry' || t === txt.toLowerCase();
|
||
};
|
||
// v26: Deferred context ??if desc is generic ("Always run", button text only),
|
||
// delay 500ms and re-extract to allow DOM rendering to complete
|
||
var isGenericDesc = _isGenericDesc(desc);
|
||
if (isGenericDesc && matchedType === 'command') {
|
||
log('DEFERRED-CONTEXT: desc="' + desc.substring(0,30) + '" ??waiting 500ms for DOM render');
|
||
(function(b2, rid2, btnRefs2, bidList2, groupKey2, txt2, type2, buttonsArr2) {
|
||
setTimeout(function() {
|
||
var retryDesc = extractContext(b2);
|
||
var finalDesc = _isGenericDesc(retryDesc) ? desc : retryDesc;
|
||
log('DEFERRED-RESULT: "' + finalDesc.substring(0,80) + '"');
|
||
var payload = {
|
||
request_id: rid2,
|
||
command: txt2,
|
||
description: finalDesc,
|
||
step_type: type2,
|
||
buttons: buttonsArr2,
|
||
_debug_trail: _lastContextDebug || ''
|
||
};
|
||
fetch(BASE+'/pending',{
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify(payload)
|
||
}).then(function(r){return r.json();}).then(function(d){
|
||
if (!d.ok || d.filtered) {
|
||
delete _sent[groupKey2];
|
||
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
|
||
return;
|
||
}
|
||
pollResponseGroup(d.request_id,btnRefs2,bidList2,groupKey2);
|
||
}).catch(function(e){
|
||
delete _sent[groupKey2];
|
||
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
|
||
});
|
||
}, 500);
|
||
})(b, rid, btnRefs, bidList, groupKey, txt, matchedType, buttonsArr);
|
||
} else {
|
||
// Original immediate send path
|
||
log('DETECTED '+matchedType+': '+txt+' ['+desc.substring(0,80)+'] step='+stepIdx);
|
||
|
||
(function(rid2,btnRefs2,bidList2,groupKey2,txt2,desc2,type2,buttonsArr2){
|
||
var payload={
|
||
request_id:rid2,
|
||
command:txt2,
|
||
description:desc2,
|
||
step_type:type2,
|
||
buttons:buttonsArr2,
|
||
_debug_trail:_lastContextDebug||''
|
||
};
|
||
fetch(BASE+'/pending',{
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify(payload)
|
||
}).then(function(r){return r.json();}).then(function(d){
|
||
if (!d.ok || d.filtered) {
|
||
delete _sent[groupKey2];
|
||
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
|
||
return;
|
||
}
|
||
pollResponseGroup(d.request_id,btnRefs2,bidList2,groupKey2);
|
||
}).catch(function(e){
|
||
delete _sent[groupKey2];
|
||
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
|
||
});
|
||
})(rid,btnRefs,bidList,groupKey,txt,desc,matchedType,buttonsArr);
|
||
} // end else (immediate send)
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
function pollResponseGroup(rid,btnRefs,bidList,groupKey){
|
||
var polls=0, maxPolls=200;
|
||
var timer=setInterval(function(){
|
||
polls++;
|
||
var anyAlive=false;
|
||
for(var ai=0;ai<btnRefs.length;ai++){
|
||
if(document.body.contains(btnRefs[ai])){anyAlive=true;break;}
|
||
}
|
||
if(!anyAlive || polls>maxPolls){
|
||
clearInterval(timer);
|
||
delete _sent[groupKey];
|
||
for(var ci=0;ci<bidList.length;ci++)delete _sent[bidList[ci]];
|
||
return;
|
||
}
|
||
fetch(BASE+'/response/'+rid).then(function(r){return r.json();}).then(function(d){
|
||
if(d.waiting)return;
|
||
clearInterval(timer);
|
||
var btnIdx=(typeof d.button_index==='number'&&d.button_index>=0)?d.button_index:-1;
|
||
if(btnIdx>=0&&btnIdx<btnRefs.length){
|
||
log((d.approved?'OK':'NO')+' CHOICE '+rid+' -> clicking button['+btnIdx+']');
|
||
dispatchReactClick(btnRefs[btnIdx]);
|
||
} else if(d.approved){
|
||
log('OK APPROVED '+rid+' -> clicking primary');
|
||
dispatchReactClick(btnRefs[0]);
|
||
} else {
|
||
log('NO REJECTED '+rid+' -> finding reject button');
|
||
clickRejectButton(btnRefs[0]);
|
||
}
|
||
delete _sent[groupKey];
|
||
for(var ri=0;ri<bidList.length;ri++)delete _sent[bidList[ri]];
|
||
}).catch(function(){});
|
||
},1500);
|
||
}
|
||
|
||
function clickRejectButton(approveBtn){
|
||
var container=approveBtn.parentElement;
|
||
if(!container) container = approveBtn.parentElement && approveBtn.parentElement.parentElement;
|
||
if(!container)return;
|
||
var siblings=container.querySelectorAll('button');
|
||
for(var i=0;i<siblings.length;i++){
|
||
var t=cleanButtonText(siblings[i]);
|
||
if(isRejectBtn(t)){
|
||
log('Clicking reject: '+t);
|
||
dispatchReactClick(siblings[i]);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
function scheduleScan(){
|
||
if(!_ready)return;
|
||
var now=Date.now();
|
||
if(now-_lastScanTs>=THROTTLE_MS){
|
||
_lastScanTs=now;
|
||
scan();
|
||
} else if(!_scanScheduled){
|
||
_scanScheduled=true;
|
||
setTimeout(function(){
|
||
_scanScheduled=false;
|
||
_lastScanTs=Date.now();
|
||
scan();
|
||
},THROTTLE_MS-(now-_lastScanTs));
|
||
}
|
||
}
|
||
|
||
setInterval(function(){
|
||
var now=Date.now();
|
||
var keys=Object.keys(_sent);
|
||
for(var i=0;i<keys.length;i++){
|
||
var entry=_sent[keys[i]];
|
||
if(entry&&entry.ts&&(now-entry.ts)>CLEANUP_MS){
|
||
delete _sent[keys[i]];
|
||
}
|
||
}
|
||
},60000);
|
||
|
||
function startObserver(){
|
||
if(_obs)return;
|
||
log('startObserver() ??scheduling auto-dumps and mutation observer');
|
||
scheduleAutoDumps();
|
||
new MutationObserver(function(mutations){
|
||
for(var i=0;i<mutations.length;i++){
|
||
if(mutations[i].addedNodes.length>0){
|
||
scheduleScan();
|
||
return;
|
||
}
|
||
}
|
||
}).observe(document.body,{childList:true,subtree:true});
|
||
setInterval(scheduleScan,3000);
|
||
|
||
// ?? TRIGGER-CLICK POLLING ??
|
||
(function pollTriggerClick(){
|
||
if(_ready&&BASE){
|
||
fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
|
||
if(!d.action)return;
|
||
var isApprove = (d.action==='approve');
|
||
var btns = document.querySelectorAll('button');
|
||
for(var i=0;i<btns.length;i++){
|
||
var bx = btns[i];
|
||
if(bx.disabled||bx.hidden||(!bx.offsetParent&&bx.style.display!=='fixed'))continue;
|
||
var t = cleanButtonText(bx);
|
||
if(t.length <= 1) continue;
|
||
if (isApprove ? isActionBtn(t) : isRejectBtn(t)) {
|
||
log('Fallback TRIGGER-CLICK on "' + t + '"');
|
||
dispatchReactClick(bx);
|
||
return;
|
||
}
|
||
}
|
||
}).catch(function(){});
|
||
}
|
||
setTimeout(pollTriggerClick, 2000);
|
||
})();
|
||
|
||
// ?? DEEP-INSPECT POLLING (v8: full body dump) ??
|
||
(function pollDeepInspect(){
|
||
if(_ready&&BASE){
|
||
fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
|
||
if(!d.inspect)return;
|
||
log('Deep inspect triggered ??full body dump');
|
||
// Force a fresh DOM dump
|
||
_dumpCount = Math.max(0, _dumpCount - 1); // allow one more dump
|
||
dumpDOMStructure();
|
||
// Also send the result to deep-inspect-result endpoint
|
||
var bodyTree = walkNode(document.body, 0, 12, 25);
|
||
var result = {
|
||
timestamp: new Date().toISOString(),
|
||
windowURL: window.location.href,
|
||
title: document.title,
|
||
totalElements: document.body ? document.querySelectorAll('*').length : 0,
|
||
hasConversationView: !!document.querySelector('[data-testid="conversation-view"]'),
|
||
hasStepIndex: !!document.querySelector('[data-step-index]'),
|
||
dataTestIds: [],
|
||
buttons: [],
|
||
bodyTree: bodyTree
|
||
};
|
||
var testIdEls = document.querySelectorAll('[data-testid]');
|
||
for (var dti = 0; dti < testIdEls.length; dti++) {
|
||
var dtid = testIdEls[dti].getAttribute('data-testid');
|
||
if (result.dataTestIds.indexOf(dtid) === -1) result.dataTestIds.push(dtid);
|
||
}
|
||
var allBtns = document.querySelectorAll('button, [role="button"]');
|
||
for (var bi = 0; bi < Math.min(allBtns.length, 50); bi++) {
|
||
var btn = allBtns[bi];
|
||
var btxt = (btn.textContent || '').trim().substring(0, 80);
|
||
if (btxt.length > 0) {
|
||
result.buttons.push({
|
||
text: btxt,
|
||
tag: btn.tagName.toLowerCase(),
|
||
cls: ((btn.className && typeof btn.className === 'string') ? btn.className : '').substring(0, 150),
|
||
visible: !!(btn.offsetParent || btn.style.display === 'fixed'),
|
||
});
|
||
}
|
||
}
|
||
fetch(BASE+'/deep-inspect-result', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(result)
|
||
}).catch(function(){});
|
||
}).catch(function(){});
|
||
}
|
||
setTimeout(pollDeepInspect, 3000);
|
||
})();
|
||
|
||
_obs=true;
|
||
}
|
||
})();
|
||
`;
|
||
}
|
||
|