Files
gravity_control/extension/src/observer-script.ts
Variet Worker ed90cbf874 fix(observer/bridge): v14 — strict 5-level DOM scope, CSS/code/icon junk filter, auto-version sync (v0.5.47) #task-619
Root causes fixed:
1. extractContextFromNearby depth 20→5 — stops grabbing unrelated UI/editor code
2. JUNK_CODE_RE — rejects CSS rules, JS source code, extension internals
3. ICON_GLUE_RE — rejects Material icon text glued with content
4. Fallback span/div/p collection REMOVED entirely (always grabbed chat text)
5. html-patcher strips old observer from integration.build() cache
6. http-bridge server-side JUNK_CONTENT_RE as last line of defense
2026-04-15 14:55:58 +09:00

865 lines
35 KiB
TypeScript

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<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'];
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);
}
// ══════════════════════════════════════════════════════════════════
// 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;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.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;
}
})();
`;
}