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

1088 lines
45 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);}
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;
for (var depth = 0; depth < 5 && node; depth++) {
if (!node.querySelector) { _debugTrail.push('d'+depth+':noQS'); node = node.parentElement; continue; }
var codeEls = node.querySelectorAll('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 ');
}
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=5;
function walkNode(el, depth, maxDepth, maxChildren) {
if (depth > maxDepth) return {tag:'…',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: '…', 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;
}
}
return finalLines.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');
}
if (cv) {
// AG Native path: find AI and User response blocks by class pattern
var responseBlocks = cv.querySelectorAll('.leading-relaxed.select-text, .text-ide-message-block-user-color, .text-ide-message-block-bot-color, .bg-ide-message-block-user-background');
if (responseBlocks.length > 0) {
// Process the LAST (most recent) response block
var lastBlock = responseBlocks[responseBlocks.length - 1];
// Skip if already scraped
if (lastBlock.dataset.agChatScraped === 'true' || lastBlock.dataset.agChatScraped === 'pending') {
// Check for NEW blocks since last scrape
if (responseBlocks.length > _lastResponseBlockCount) {
// New block appeared — process it
for (var rbi = responseBlocks.length - 1; rbi >= 0; rbi--) {
if (responseBlocks[rbi].dataset.agChatScraped !== 'true' && responseBlocks[rbi].dataset.agChatScraped !== 'pending') {
lastBlock = responseBlocks[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 : '';
var isUser = clsStr.indexOf('user-color') !== -1 || clsStr.indexOf('user-background') !== -1 || clsStr.indexOf('user-message') !== -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
// Content is stable — send it
_lastStepTextSent = true;
_lastResponseBlockCount = responseBlocks.length;
lastBlock.dataset.agChatScraped = 'pending';
log('AG-Native chat relay [' + role + ']: blocks=' + responseBlocks.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, responseBlocks.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');
if(!allBtns.length)return;
for(var j=0;j<allBtns.length;j++){
var b=allBtns[j];
if(b.disabled||b.hidden||(!b.offsetParent&&b.style.display!=='fixed'))continue;
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;
if(!isActionBtn(txt)) continue;
// Skip inline code lens buttons
if (b.closest('.codelens-decoration') && !txt.includes('Accept')) {
continue;
}
var matchedType = txt.includes('Accept') ? 'diff_review' : (txt === 'Retry' ? 'retry' : (txt.includes('Run') || txt.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);
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};
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);
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;
}
})();
`;
}