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\\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 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;jmaxPolls){ clearInterval(timer); delete _sent[groupKey]; for(var ci=0;ci=0)?d.button_index:-1; if(btnIdx>=0&&btnIdx 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=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;iCLEANUP_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;i0){ 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 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; } })(); `; }