Files
gravity_control/extension/src/observer-script.ts

1398 lines
63 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
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;
}
})();
`;
}