export function generateApprovalObserverScript(_port: number): string { return ` // ── Gravity Bridge v14: Strict Scope + Junk Filter ── // v14: Strict 5-level DOM scope, CSS/source code/icon-glue filters, no fallback (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('v14 Script loaded — Strict Scope + Junk Filter'); // 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) { if (!line || line.trim().length < 2) return true; var trimmed = line.trim(); 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 = []; for (var i = 0; i < lines.length; i++) { if (!isNoiseLine(lines[i])) clean.push(lines[i].trim()); } return clean.join('\\n').trim(); } 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']; 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); } // ══════════════════════════════════════════════════════════════════ // v7: STEP-AWARE CHAT BODY SCANNING // Scans [data-step-index] elements inside [data-testid="conversation-view"] // Extracts AI response text while filtering UI noise // ══════════════════════════════════════════════════════════════════ var _lastScrapedStepIndex = -1; var _lastStepText = ''; var _lastStepTextTime = 0; var _lastStepTextSent = false; 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); // 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 // Look for known markdown container patterns var mdEl = clone.querySelector('.markdown-body, .prose, [class*="markdown"], [class*="rendered"]'); var rawText = ''; if (mdEl && mdEl.innerText && mdEl.innerText.trim().length > 10) { rawText = mdEl.innerText.trim(); } else { // Fallback: get all text but filter aggressively rawText = (clone.innerText || clone.textContent || '').trim(); } // Apply line-by-line noise filter return cleanLines(rawText).substring(0, 3500); } function scanChatBodies() { if (!_ready) return; // One-time DOM dump dumpDOMStructure(); // PRIMARY: Find conversation-view container var 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; } })(); `; }