|
|
|
|
@@ -1,377 +1,159 @@
|
|
|
|
|
/**
|
|
|
|
|
* Approval Observer Script — injected into AG's renderer process.
|
|
|
|
|
*
|
|
|
|
|
* This is a self-contained JavaScript string template that runs in the
|
|
|
|
|
* browser context (no Node.js APIs). It scans the DOM for approval buttons,
|
|
|
|
|
* reports them to the HTTP bridge, and handles trigger clicks.
|
|
|
|
|
*
|
|
|
|
|
* Extracted from extension.ts for maintainability.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export function generateApprovalObserverScript(_port: number): string {
|
|
|
|
|
// Port is hardcoded as fallback, but renderer also reads ag-bridge-ports.json for multi-bridge
|
|
|
|
|
return `
|
|
|
|
|
// ── Gravity Bridge v3: Approval Observer (deep DOM traversal — iframes, webviews, shadow DOMs) ──
|
|
|
|
|
// ── Gravity Bridge v4: React Tailwind UI Observer ──
|
|
|
|
|
(function(){
|
|
|
|
|
'use strict';
|
|
|
|
|
var BASE='',_obs=false,_sent={},_ready=false;
|
|
|
|
|
var _scanScheduled=false,_lastScanTs=0;
|
|
|
|
|
var THROTTLE_MS=100;
|
|
|
|
|
var THROTTLE_MS=500;
|
|
|
|
|
var CLEANUP_MS=300000;
|
|
|
|
|
var _domDumped=false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function log(m){console.log('[GB Observer] '+m);}
|
|
|
|
|
log('v3 Script loaded — deep DOM traversal enabled');
|
|
|
|
|
log('v4 Script loaded — deep Tailwind DOM traversal enabled');
|
|
|
|
|
|
|
|
|
|
// ── Deep DOM Traversal: find buttons across ALL boundaries ──
|
|
|
|
|
// Searches: main document → iframes (contentDocument) → webview elements → shadow DOMs
|
|
|
|
|
function deepFindButtons(patterns){
|
|
|
|
|
var results=[];
|
|
|
|
|
// 1. Prioritize Agent panel
|
|
|
|
|
var panel=findPanel();
|
|
|
|
|
if(panel){
|
|
|
|
|
collectButtons(panel,results,patterns,'panel');
|
|
|
|
|
if(results.length>0) return results;
|
|
|
|
|
}
|
|
|
|
|
// 2. Prioritize VS Code Toasts & Dialogs
|
|
|
|
|
var toasts=document.querySelectorAll('.notifications-toasts, .monaco-dialog-box');
|
|
|
|
|
for(var t=0;t<toasts.length;t++){
|
|
|
|
|
collectButtons(toasts[t],results,patterns,'toast');
|
|
|
|
|
}
|
|
|
|
|
if(results.length>0) return results;
|
|
|
|
|
// 3. Main document fallback
|
|
|
|
|
collectButtons(document,results,patterns,'main');
|
|
|
|
|
// 4. Iframe traversal (try contentDocument — works if same-origin or webSecurity off)
|
|
|
|
|
var iframes=document.querySelectorAll('iframe');
|
|
|
|
|
for(var i=0;i<iframes.length;i++){
|
|
|
|
|
try{
|
|
|
|
|
var idoc=iframes[i].contentDocument||iframes[i].contentWindow.document;
|
|
|
|
|
if(idoc){collectButtons(idoc,results,patterns,'iframe#'+i+'('+iframes[i].className.substring(0,30)+')');}
|
|
|
|
|
}catch(e){
|
|
|
|
|
// Cross-origin — can't access. Log only on first dom dump
|
|
|
|
|
if(!_domDumped)log('iframe#'+i+' cross-origin: '+e.message.substring(0,60));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 3. Webview elements (Electron <webview> tag — has executeJavaScript)
|
|
|
|
|
var webviews=document.querySelectorAll('webview');
|
|
|
|
|
for(var w=0;w<webviews.length;w++){
|
|
|
|
|
try{
|
|
|
|
|
var wvDoc=webviews[w].contentDocument;
|
|
|
|
|
if(wvDoc){collectButtons(wvDoc,results,patterns,'webview#'+w);}
|
|
|
|
|
}catch(e){
|
|
|
|
|
if(!_domDumped)log('webview#'+w+' access error: '+e.message.substring(0,60));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return results;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function collectButtons(doc,results,patterns,source){
|
|
|
|
|
if(!doc||!doc.querySelectorAll)return;
|
|
|
|
|
var btns=doc.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
|
|
|
|
|
for(var i=0;i<btns.length;i++){
|
|
|
|
|
var b=btns[i];
|
|
|
|
|
if(b.disabled||b.hidden)continue;
|
|
|
|
|
try{if(!b.offsetParent&&b.style.display!=='fixed')continue;}catch(e){}
|
|
|
|
|
var txt=(b.textContent||'').trim();
|
|
|
|
|
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim();
|
|
|
|
|
txt=txt.replace(/^[^a-zA-Z0-9]+/, '').trim(); // Strip leading icons/symbols
|
|
|
|
|
if(!txt)continue;
|
|
|
|
|
for(var p=0;p<patterns.length;p++){
|
|
|
|
|
if(patterns[p].test(txt)){
|
|
|
|
|
results.push({btn:b,text:txt,source:source});
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 4. Recurse into shadow DOMs
|
|
|
|
|
try{
|
|
|
|
|
var allEls=doc.querySelectorAll('*');
|
|
|
|
|
for(var j=0;j<allEls.length;j++){
|
|
|
|
|
var sr=allEls[j].shadowRoot;
|
|
|
|
|
if(sr)collectButtons(sr,results,patterns,source+'>shadow');
|
|
|
|
|
}
|
|
|
|
|
}catch(e){}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Deep DOM Inspector (recursive, POSTs results to bridge) ──
|
|
|
|
|
function runDeepInspect(){
|
|
|
|
|
var result={timestamp:new Date().toISOString(),windowURL:window.location.href,windowOrigin:window.location.origin,windowProtocol:window.location.protocol,framesCount:window.frames.length,nodes:[]};
|
|
|
|
|
log('DEEP-INSPECT: starting recursive DOM analysis...');
|
|
|
|
|
|
|
|
|
|
function inspectDoc(doc,depth,label){
|
|
|
|
|
var node={label:label,depth:depth,accessible:true,url:'',buttons:[],roleBtns:[],iframes:[],webviews:[],shadowDOMs:0,totalElements:0};
|
|
|
|
|
if(!doc){node.accessible=false;node.error='null document';result.nodes.push(node);return;}
|
|
|
|
|
try{node.url=(doc.URL||doc.documentURI||'unknown').substring(0,200);}catch(e){node.url='blocked';}
|
|
|
|
|
try{node.title=(doc.title||'').substring(0,100);}catch(e){}
|
|
|
|
|
try{node.readyState=doc.readyState;}catch(e){}
|
|
|
|
|
|
|
|
|
|
// CSP
|
|
|
|
|
try{
|
|
|
|
|
var csp=doc.querySelectorAll('meta[http-equiv="Content-Security-Policy"]');
|
|
|
|
|
if(csp.length>0){node.csp=[];for(var c=0;c<csp.length;c++){node.csp.push((csp[c].content||'').substring(0,200));}}
|
|
|
|
|
}catch(e){}
|
|
|
|
|
|
|
|
|
|
try{
|
|
|
|
|
var allEls=doc.querySelectorAll('*');
|
|
|
|
|
node.totalElements=allEls.length;
|
|
|
|
|
// Buttons
|
|
|
|
|
var btns=doc.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
|
|
|
|
|
for(var i=0;i<btns.length;i++){
|
|
|
|
|
var b=btns[i];
|
|
|
|
|
var txt=(b.textContent||'').trim().substring(0,80);
|
|
|
|
|
if(!txt)continue;
|
|
|
|
|
var cls=(b.className||'').substring(0,60);
|
|
|
|
|
var disabled=b.disabled;
|
|
|
|
|
var hidden=b.hidden||false;
|
|
|
|
|
try{if(!b.offsetParent&&b.style.display!=='fixed')hidden=true;}catch(e){}
|
|
|
|
|
var aria=b.getAttribute('aria-label')||'';
|
|
|
|
|
var ttl=b.getAttribute('title')||'';
|
|
|
|
|
node.buttons.push({text:txt,class:cls,disabled:disabled,hidden:hidden,aria:aria,title:ttl});
|
|
|
|
|
}
|
|
|
|
|
// role=button
|
|
|
|
|
var rbs=doc.querySelectorAll('[role="button"]');
|
|
|
|
|
for(var r=0;r<rbs.length;r++){
|
|
|
|
|
if(rbs[r].tagName==='BUTTON')continue;
|
|
|
|
|
var rtxt=(rbs[r].textContent||'').trim().substring(0,60);
|
|
|
|
|
node.roleBtns.push({tag:rbs[r].tagName.toLowerCase(),text:rtxt});
|
|
|
|
|
}
|
|
|
|
|
// Shadow DOMs
|
|
|
|
|
for(var s=0;s<allEls.length;s++){
|
|
|
|
|
var sr=allEls[s].shadowRoot;
|
|
|
|
|
if(sr){node.shadowDOMs++;inspectDoc(sr,depth+1,'shadow(<'+allEls[s].tagName.toLowerCase()+' class="'+(allEls[s].className||'').substring(0,30)+'">)');}
|
|
|
|
|
}
|
|
|
|
|
// Iframes
|
|
|
|
|
var ifs=doc.querySelectorAll('iframe');
|
|
|
|
|
for(var fi=0;fi<ifs.length;fi++){
|
|
|
|
|
var f=ifs[fi];
|
|
|
|
|
var finfo={index:fi,class:(f.className||'').substring(0,60),src:(f.src||'').substring(0,150),id:f.id||'',sandbox:f.getAttribute('sandbox')||'',allow:f.getAttribute('allow')||'',accessible:false,cwExists:false,cwFrames:0};
|
|
|
|
|
try{
|
|
|
|
|
var idoc=f.contentDocument||(f.contentWindow&&f.contentWindow.document);
|
|
|
|
|
if(idoc){finfo.accessible=true;inspectDoc(idoc,depth+1,'iframe#'+fi+'('+finfo.class.substring(0,30)+')');
|
|
|
|
|
}else{finfo.error='contentDocument=null';}
|
|
|
|
|
}catch(e){finfo.error=e.message.substring(0,80);}
|
|
|
|
|
try{var cw=f.contentWindow;if(cw){finfo.cwExists=true;finfo.cwFrames=cw.length;try{finfo.cwLocation=cw.location.href;}catch(e2){finfo.cwLocation='blocked: '+e2.message.substring(0,40);}}}
|
|
|
|
|
catch(e){}
|
|
|
|
|
node.iframes.push(finfo);
|
|
|
|
|
}
|
|
|
|
|
// Webviews
|
|
|
|
|
var wvs=doc.querySelectorAll('webview');
|
|
|
|
|
for(var wi=0;wi<wvs.length;wi++){
|
|
|
|
|
var wv=wvs[wi];
|
|
|
|
|
var winfo={index:wi,src:(wv.src||'').substring(0,150),class:(wv.className||'').substring(0,60),partition:wv.getAttribute('partition')||'',preload:wv.getAttribute('preload')||'',nodeintegration:wv.getAttribute('nodeintegration')||'',webpreferences:wv.getAttribute('webpreferences')||'',hasExecJS:typeof wv.executeJavaScript==='function',contentDocAccessible:false};
|
|
|
|
|
try{var wdoc=wv.contentDocument;if(wdoc){winfo.contentDocAccessible=true;inspectDoc(wdoc,depth+1,'webview#'+wi+'.contentDocument');}}catch(e){winfo.contentDocError=e.message.substring(0,60);}
|
|
|
|
|
node.webviews.push(winfo);
|
|
|
|
|
}
|
|
|
|
|
}catch(e){node.error=e.message;}
|
|
|
|
|
result.nodes.push(node);
|
|
|
|
|
return node;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inspectDoc(document,0,'MainDocument');
|
|
|
|
|
|
|
|
|
|
// Webview executeJavaScript probe (async)
|
|
|
|
|
var webviews=document.querySelectorAll('webview');
|
|
|
|
|
var probesPending=webviews.length;
|
|
|
|
|
result.webviewProbes=[];
|
|
|
|
|
if(probesPending===0)postResults();
|
|
|
|
|
for(var pw=0;pw<webviews.length;pw++){
|
|
|
|
|
(function(wv,idx){
|
|
|
|
|
if(typeof wv.executeJavaScript!=='function'){result.webviewProbes.push({index:idx,error:'executeJavaScript not available'});probesPending--;if(probesPending<=0)postResults();return;}
|
|
|
|
|
try{
|
|
|
|
|
wv.executeJavaScript('(function(){var btns=document.querySelectorAll("button, [role=\"button\"], vscode-button, .monaco-text-button");var allEls=document.querySelectorAll("*");var ifs=document.querySelectorAll("iframe");var wvs=document.querySelectorAll("webview");var btnArr=[];for(var i=0;i<btns.length;i++){var b=btns[i];var txt=(b.textContent||"").trim();var cls=(b.className||"").substring(0,50);var dis=b.disabled;var hid=b.hidden||!b.offsetParent;btnArr.push({text:txt.substring(0,60),class:cls,disabled:dis,hidden:hid,aria:b.getAttribute("aria-label")||"",title:b.getAttribute("title")||""});}var rbs=document.querySelectorAll("[role=button]");var rbArr=[];for(var j=0;j<rbs.length;j++){if(rbs[j].tagName!=="BUTTON")rbArr.push({tag:rbs[j].tagName.toLowerCase(),text:(rbs[j].textContent||"").trim().substring(0,40)});}var sc=0;for(var k=0;k<allEls.length;k++){if(allEls[k].shadowRoot)sc++;}return JSON.stringify({url:document.URL,title:document.title,totalElements:allEls.length,buttons:btnArr,roleBtns:rbArr,iframes:ifs.length,webviews:wvs.length,shadowDOMs:sc});})()')
|
|
|
|
|
.then(function(r){
|
|
|
|
|
try{var d=JSON.parse(r);result.webviewProbes.push({index:idx,success:true,data:d});log('DEEP-INSPECT: webview#'+idx+' probe OK: '+d.buttons.length+' buttons, '+d.totalElements+' elements');}catch(e){result.webviewProbes.push({index:idx,parseError:e.message,raw:r});}
|
|
|
|
|
probesPending--;if(probesPending<=0)postResults();
|
|
|
|
|
})
|
|
|
|
|
.catch(function(e){
|
|
|
|
|
result.webviewProbes.push({index:idx,execError:e.message});
|
|
|
|
|
log('DEEP-INSPECT: webview#'+idx+' execJS error: '+e.message);
|
|
|
|
|
probesPending--;if(probesPending<=0)postResults();
|
|
|
|
|
});
|
|
|
|
|
}catch(e){
|
|
|
|
|
result.webviewProbes.push({index:idx,callError:e.message});
|
|
|
|
|
probesPending--;if(probesPending<=0)postResults();
|
|
|
|
|
}
|
|
|
|
|
})(webviews[pw],pw);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function postResults(){
|
|
|
|
|
var summary='nodes='+result.nodes.length;
|
|
|
|
|
var totalBtns=0;for(var n=0;n<result.nodes.length;n++)totalBtns+=result.nodes[n].buttons.length;
|
|
|
|
|
summary+=' totalButtons='+totalBtns+' webviewProbes='+result.webviewProbes.length;
|
|
|
|
|
log('DEEP-INSPECT complete: '+summary);
|
|
|
|
|
// Also log buttons from each node
|
|
|
|
|
for(var n2=0;n2<result.nodes.length;n2++){
|
|
|
|
|
var nd=result.nodes[n2];
|
|
|
|
|
if(nd.buttons.length>0){
|
|
|
|
|
log(' '+nd.label+': '+nd.buttons.length+' buttons');
|
|
|
|
|
for(var bi=0;bi<Math.min(15,nd.buttons.length);bi++){
|
|
|
|
|
log(' ['+bi+'] "'+nd.buttons[bi].text+'"'+(nd.buttons[bi].disabled?' DISABLED':'')+(nd.buttons[bi].hidden?' HIDDEN':''));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// POST to bridge
|
|
|
|
|
fetch(BASE+'/deep-inspect-result',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(result)})
|
|
|
|
|
.then(function(){log('DEEP-INSPECT results posted to bridge');})
|
|
|
|
|
.catch(function(e){log('DEEP-INSPECT post error: '+e.message);});
|
|
|
|
|
// 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(); // fallback
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Auto-dump on startup (3s delay)
|
|
|
|
|
function dumpDOMStructure(){runDeepInspect();}
|
|
|
|
|
|
|
|
|
|
// ── Port Discovery: async fetch()-based (sync XHR blocked in Electron renderer) ──
|
|
|
|
|
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;});
|
|
|
|
|
// ── Find common container for the step ──
|
|
|
|
|
function findButtonContainer(btn){
|
|
|
|
|
return btn.closest('.p-1')
|
|
|
|
|
|| btn.closest('.bg-agent-convo-background')
|
|
|
|
|
|| btn.closest('[class*="border-gray-500/10"]')
|
|
|
|
|
|| btn.closest('.monaco-list-row')
|
|
|
|
|
|| btn.parentElement;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function discoverPort(cb){
|
|
|
|
|
log('Waiting for Gravity Bridge status bar item to appear in DOM...');
|
|
|
|
|
var attempts=0;
|
|
|
|
|
var timer=setInterval(function(){
|
|
|
|
|
attempts++;
|
|
|
|
|
// Search for our specific port injected by the extension host for THIS window.
|
|
|
|
|
// This prevents cross-project leakage by ignoring the hardcoded port from the shared HTML file.
|
|
|
|
|
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]) {
|
|
|
|
|
var domPort = parseInt(m[1], 10);
|
|
|
|
|
log('Determined correct window port from DOM: ' + domPort);
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
tryPingAsync(domPort).then(function(ok){
|
|
|
|
|
if(ok){ cb(domPort); } else { log('Ping failed on DOM port ' + domPort); cb(HARDCODED_PORT); }
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback if status bar never appears
|
|
|
|
|
if(attempts>150){
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
log('DOM discovery timeout after 5 min. Falling back to hardcoded.');
|
|
|
|
|
tryPingAsync(HARDCODED_PORT).then(function(ok){
|
|
|
|
|
if(ok){ cb(HARDCODED_PORT); }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},2000);
|
|
|
|
|
function cleanButtonText(btn) {
|
|
|
|
|
if (!btn) return '';
|
|
|
|
|
// if internal truncate span, use it
|
|
|
|
|
var tr = btn.querySelector('.truncate');
|
|
|
|
|
var txt = (tr ? tr.textContent : btn.textContent) || '';
|
|
|
|
|
return txt.trim().replace(/(Alt|Ctrl|Shift|Meta)\\\\+.*/i,'').trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
discoverPort(function(port){
|
|
|
|
|
BASE='http://127.0.0.1:'+port;
|
|
|
|
|
fetch(BASE+'/ping').then(function(r){return r.text();}).then(function(t){
|
|
|
|
|
if(t==='pong'){log('Bridge connected on port '+port);_ready=true;startObserver();setTimeout(dumpDOMStructure,3000);}
|
|
|
|
|
else log('Bridge ping failed: '+t);
|
|
|
|
|
}).catch(function(e){log('Bridge unreachable: '+e.message);});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Button patterns to detect (order matters: first match wins per scan) ──
|
|
|
|
|
// ONLY positive triggers should initiate a pending request group.
|
|
|
|
|
// Negative/secondary buttons (Deny, Reject, Dismiss) will be collected as siblings.
|
|
|
|
|
var PATS=[
|
|
|
|
|
// ALL PATS removed to prevent UI-level False Positives and "Empty Body" payloads.
|
|
|
|
|
// 100% of pending detection is now handled by step-probe.ts which has full RPC context.
|
|
|
|
|
// The DOM observer remains strictly for 'trigger-click' (executing physical clicks on fallback).
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// ALL actionable button patterns (for grouping siblings in same container)
|
|
|
|
|
var ALL_ACTION_RE=[/^(?:Always\s*)?Run\b/i,/^(?:Always\s*)?Accept\b/i,/^Reject\b/i,/^(?:Always\s*)?Allow\b/i,/^Deny\b/i,/^(?:Always\s*)?Approve\b/i,/^Cancel\b/i,/^Retry\b/i,/^Dismiss\b/i,/^Stop\b/i,/^Decline\b/i];
|
|
|
|
|
|
|
|
|
|
// Reject button patterns for finding the counterpart
|
|
|
|
|
var REJECT_RE=[/^reject\b/i,/^cancel\b/i,/^deny\b/i,/^stop\b/i,/^decline\b/i,/^dismiss\b/i];
|
|
|
|
|
|
|
|
|
|
// ── Stable button fingerprint (no getBoundingClientRect — scroll-safe) ──
|
|
|
|
|
// ── Stable button fingerprint ──
|
|
|
|
|
function btnId(b,type){
|
|
|
|
|
// Use: type + button text + parent's first 40 chars of text content
|
|
|
|
|
var txt=(b.textContent||'').trim();
|
|
|
|
|
var parent=b.parentElement;
|
|
|
|
|
var pctx=parent?(parent.textContent||'').substring(0,40).replace(/\\s+/g,' '):'';
|
|
|
|
|
// Also use DOM position: nth-child among sibling buttons
|
|
|
|
|
var txt = cleanButtonText(b);
|
|
|
|
|
var parent = b.parentElement;
|
|
|
|
|
var idx=0;
|
|
|
|
|
if(parent){
|
|
|
|
|
var siblings=parent.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
|
|
|
|
|
var siblings=parent.querySelectorAll('button');
|
|
|
|
|
for(var i=0;i<siblings.length;i++){if(siblings[i]===b){idx=i;break;}}
|
|
|
|
|
}
|
|
|
|
|
return type+'|'+txt+'|'+idx+'|'+pctx.substring(0,20);
|
|
|
|
|
return type+'|'+txt+'|'+idx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Context extraction — walk up DOM to find command/code description ──
|
|
|
|
|
function extractContext(b){
|
|
|
|
|
var curr = b.parentElement;
|
|
|
|
|
var bestDesc = '';
|
|
|
|
|
var btnText = (b.innerText || b.textContent || '').trim();
|
|
|
|
|
// ── Context extraction — target BOTH chat history and command payload ──
|
|
|
|
|
function extractCommandContext(b){
|
|
|
|
|
var container = findButtonContainer(b);
|
|
|
|
|
if (!container) return "";
|
|
|
|
|
|
|
|
|
|
// Debug: Dump the container's raw HTML to bridge for analysis
|
|
|
|
|
try {
|
|
|
|
|
var dumpContainer = b.closest('[class*="message"]') || b.closest('[class*="chat"]') || b.closest('.monaco-list-row') || b.parentElement.parentElement;
|
|
|
|
|
if (dumpContainer && dumpContainer.outerHTML) {
|
|
|
|
|
fetch(BASE + '/dump-html', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {'Content-Type':'application/json'},
|
|
|
|
|
body: JSON.stringify({ html: dumpContainer.outerHTML, btnText: btnText })
|
|
|
|
|
}).catch(function(e){});
|
|
|
|
|
}
|
|
|
|
|
} catch(e){}
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < 8 && curr; i++) {
|
|
|
|
|
var codeEl = curr.querySelector('pre, code, [class*="command"], [class*="terminal"], [class*="code"]');
|
|
|
|
|
if (codeEl && codeEl !== b && !b.contains(codeEl)) {
|
|
|
|
|
var codeText = (codeEl.innerText || codeEl.textContent || '').trim();
|
|
|
|
|
if (codeText.length > 0 && codeText !== btnText) {
|
|
|
|
|
return codeText.substring(0, 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var full = (curr.innerText || curr.textContent || '');
|
|
|
|
|
var btnRawText = (b.textContent || '');
|
|
|
|
|
var desc = full.replace(btnRawText, '').trim();
|
|
|
|
|
if (desc.length > 5 && desc !== btnText && bestDesc.length < desc.length) {
|
|
|
|
|
bestDesc = desc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var cname = curr.className;
|
|
|
|
|
if (typeof cname === 'string' && (cname.includes('message') || cname.includes('step') || cname.includes('markdown'))) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
curr = curr.parentElement;
|
|
|
|
|
var titleSpans = container.querySelectorAll('span[title^="command("]');
|
|
|
|
|
if (titleSpans && titleSpans.length > 0) {
|
|
|
|
|
var t = titleSpans[0].getAttribute('title');
|
|
|
|
|
if (t && t.length > 5) return t.substring(0, 800);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var preEls = container.querySelectorAll('pre');
|
|
|
|
|
if (preEls && preEls.length > 0) {
|
|
|
|
|
var t2 = (preEls[preEls.length-1].textContent || '').trim();
|
|
|
|
|
if (t2.length > 2) return t2.substring(0, 800);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var codeText = '';
|
|
|
|
|
var codes = container.querySelectorAll('code, [class*="command"]');
|
|
|
|
|
for(var i=0; i<codes.length; i++) {
|
|
|
|
|
codeText += (codes[i].textContent || '').trim() + ' ';
|
|
|
|
|
}
|
|
|
|
|
if (codeText.length > 2) return codeText.trim().substring(0, 800);
|
|
|
|
|
|
|
|
|
|
return bestDesc.substring(0, 500);
|
|
|
|
|
var fallback = (container.textContent || '').replace(cleanButtonText(b), '').trim();
|
|
|
|
|
return fallback.substring(0, 500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Find common container of related buttons ──
|
|
|
|
|
function findButtonContainer(btn){
|
|
|
|
|
return btn.closest('[class*="step"]')
|
|
|
|
|
||btn.closest('[class*="action"]')
|
|
|
|
|
||btn.closest('[class*="tool"]')
|
|
|
|
|
||btn.closest('[class*="cascade"]')
|
|
|
|
|
||btn.closest('[class*="message"]')
|
|
|
|
|
||btn.closest('[class*="dialog"]')
|
|
|
|
|
||btn.closest('[class*="notification"]')
|
|
|
|
|
||btn.parentElement;
|
|
|
|
|
function extractChatContext(b) {
|
|
|
|
|
try {
|
|
|
|
|
var botTurn = b.closest('.bg-agent-convo-background') || b.closest('.text-ide-message-block-bot-color');
|
|
|
|
|
if (!botTurn) {
|
|
|
|
|
var container = findButtonContainer(b);
|
|
|
|
|
botTurn = container ? container.parentElement : null;
|
|
|
|
|
}
|
|
|
|
|
if (!botTurn) return '';
|
|
|
|
|
|
|
|
|
|
var toolContainer = findButtonContainer(b) || b;
|
|
|
|
|
var textParts = [];
|
|
|
|
|
|
|
|
|
|
function walk(node) {
|
|
|
|
|
if (node === toolContainer) return true; // Stop traversal at the tool box
|
|
|
|
|
if (node.nodeType === 1) {
|
|
|
|
|
var tag = node.tagName.toUpperCase();
|
|
|
|
|
if (tag==='BUTTON' || tag==='SVG' || tag==='STYLE' || tag==='SCRIPT') return false;
|
|
|
|
|
}
|
|
|
|
|
if (node.nodeType === 3) {
|
|
|
|
|
var val = node.nodeValue;
|
|
|
|
|
if (val && val.trim()) textParts.push(val.trim());
|
|
|
|
|
} else {
|
|
|
|
|
for(var i=0; i<node.childNodes.length; i++) {
|
|
|
|
|
if (walk(node.childNodes[i])) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (node.nodeType === 1) {
|
|
|
|
|
var tg = node.tagName.toUpperCase();
|
|
|
|
|
if (tg==='P' || tg==='DIV' || tg==='BR' || tg==='LI' || tg==='PRE') textParts.push('\\n');
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
walk(botTurn);
|
|
|
|
|
var result = textParts.join(' ').replace(/ \\n /g, '\\n').replace(/\\n+/g, '\\n').trim();
|
|
|
|
|
return result.substring(0, 1500);
|
|
|
|
|
} catch(e) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Collect all actionable sibling buttons from a container ──
|
|
|
|
|
function extractContext(b) {
|
|
|
|
|
var cmd = extractCommandContext(b);
|
|
|
|
|
var chat = extractChatContext(b);
|
|
|
|
|
if (!chat && !cmd) return "";
|
|
|
|
|
var combined = "";
|
|
|
|
|
if (chat && chat.length > 5) combined += "[AI 본문 요약]\\n" + chat + "\\n\\n";
|
|
|
|
|
if (cmd && cmd.length > 2) combined += "[결행 명령]\\n" + cmd;
|
|
|
|
|
return combined.trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Action Buttons Patterns ──
|
|
|
|
|
var PATS = [
|
|
|
|
|
{ type: 'command', re: /^(?:Always\\s*)?Run\\b/i },
|
|
|
|
|
{ type: 'permission', re: /^(?:Always\\s*)?Allow\\b/i },
|
|
|
|
|
{ type: 'permission', re: /^(?:Always\\s*)?Approve\\b/i },
|
|
|
|
|
{ type: 'diff_review', re: /^(?:Always\\s*)?Accept\\b/i },
|
|
|
|
|
];
|
|
|
|
|
var ALL_ACTION_RE=[/^(?:Always\\s*)?Run\\b/i,/^(?:Always\\s*)?Accept\\b/i,/^Reject\\b/i,/^(?:Always\\s*)?Allow\\b/i,/^Deny\\b/i,/^(?:Always\\s*)?Approve\\b/i,/^Cancel\\b/i,/^Retry\\b/i,/^Dismiss\\b/i,/^Stop\\b/i,/^Decline\\b/i];
|
|
|
|
|
var REJECT_RE=[/^Reject\\b/i,/^Cancel\\b/i,/^Deny\\b/i,/^Stop\\b/i,/^Decline\\b/i,/^Dismiss\\b/i];
|
|
|
|
|
|
|
|
|
|
function collectSiblingButtons(container,triggerBtn){
|
|
|
|
|
if(!container)return [];
|
|
|
|
|
var siblings=container.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
|
|
|
|
|
var siblings=container.querySelectorAll('button');
|
|
|
|
|
var result=[];
|
|
|
|
|
for(var i=0;i<siblings.length;i++){
|
|
|
|
|
var sb=siblings[i];
|
|
|
|
|
if(sb.disabled||sb.hidden)continue;
|
|
|
|
|
try{if(!sb.offsetParent&&sb.style.display!=='fixed')continue;}catch(e){}
|
|
|
|
|
var stxt=(sb.textContent||'').trim();
|
|
|
|
|
stxt=stxt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim();
|
|
|
|
|
stxt=stxt.replace(/^[^a-zA-Z0-9]+/, '').trim(); // Strip leading icons/symbols
|
|
|
|
|
if(!stxt)continue;
|
|
|
|
|
// Check if this button matches any actionable pattern
|
|
|
|
|
if(sb.disabled||sb.hidden||(!sb.offsetParent&&sb.style.display!=='fixed'))continue;
|
|
|
|
|
|
|
|
|
|
var stxt = cleanButtonText(sb);
|
|
|
|
|
if(stxt.length <= 1) continue; // Ignore icon buttons
|
|
|
|
|
|
|
|
|
|
var isAction=false;
|
|
|
|
|
for(var a=0;a<ALL_ACTION_RE.length;a++){
|
|
|
|
|
if(ALL_ACTION_RE[a].test(stxt)){isAction=true;break;}
|
|
|
|
|
@@ -382,100 +164,84 @@ export function generateApprovalObserverScript(_port: number): string {
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Find the React app container (Antigravity's main UI root) ──
|
|
|
|
|
function findPanel(){
|
|
|
|
|
// Priority order of panel selectors (most specific first)
|
|
|
|
|
var selectors=[
|
|
|
|
|
'.antigravity-agent-side-panel',
|
|
|
|
|
'#jetski-agent-panel',
|
|
|
|
|
'.react-app-container',
|
|
|
|
|
'[class*="agent-panel"]',
|
|
|
|
|
'[class*="agentPanel"]',
|
|
|
|
|
'.chat-body',
|
|
|
|
|
'.interactive-session',
|
|
|
|
|
'[class*="sidebar"]',
|
|
|
|
|
];
|
|
|
|
|
for(var i=0;i<selectors.length;i++){
|
|
|
|
|
var el=document.querySelector(selectors[i]);
|
|
|
|
|
if(el)return el;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
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;});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Core scan — finds actionable buttons and reports to bridge ──
|
|
|
|
|
// Groups related buttons from same container into a single pending
|
|
|
|
|
function discoverPort(cb){
|
|
|
|
|
log('Waiting for Gravity Bridge status...');
|
|
|
|
|
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]) {
|
|
|
|
|
var domPort = parseInt(m[1], 10);
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
tryPingAsync(domPort).then(function(ok){
|
|
|
|
|
if(ok) cb(domPort); else cb(HARDCODED_PORT);
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If we are in the webview, the status bar is invisible. Skip quickly.
|
|
|
|
|
if(attempts>1){
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
tryPingAsync(HARDCODED_PORT).then(function(ok){ cb(HARDCODED_PORT); }); // Assume HARDCODED_PORT works!
|
|
|
|
|
}
|
|
|
|
|
},500); // Wait 500ms * 2 = 1 second total
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
discoverPort(function(port){
|
|
|
|
|
BASE='http://127.0.0.1:'+port;
|
|
|
|
|
fetch(BASE+'/ping').then(function(r){return r.text();}).then(function(t){
|
|
|
|
|
if(t==='pong'){_ready=true;startObserver();}
|
|
|
|
|
}).catch(function(e){});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function scan(){
|
|
|
|
|
if(!_ready)return;
|
|
|
|
|
var now=Date.now();
|
|
|
|
|
|
|
|
|
|
var panel=findPanel();
|
|
|
|
|
// Expand search: panel-scoped first, then full body for review bars
|
|
|
|
|
var searchRoots=[];
|
|
|
|
|
if(panel)searchRoots.push(panel);
|
|
|
|
|
// Always also scan body for diff review bar (Accept all/Reject all)
|
|
|
|
|
// which lives outside the agent panel in the editor notification area
|
|
|
|
|
if(document.body)searchRoots.push(document.body);
|
|
|
|
|
if(!searchRoots.length)return;
|
|
|
|
|
|
|
|
|
|
var seen={}; // dedupe buttons across search roots
|
|
|
|
|
for(var r=0;r<searchRoots.length;r++){
|
|
|
|
|
var allBtns=searchRoots[r].querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
|
|
|
|
|
if(!allBtns.length)continue;
|
|
|
|
|
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)continue;
|
|
|
|
|
// Check visibility (offsetParent null = hidden via CSS)
|
|
|
|
|
if(!b.offsetParent&&b.style.display!=='fixed')continue;
|
|
|
|
|
if(b.disabled||b.hidden||(!b.offsetParent&&b.style.display!=='fixed'))continue;
|
|
|
|
|
|
|
|
|
|
var txt=(b.innerText || b.textContent||'').trim();
|
|
|
|
|
if(!txt)continue;
|
|
|
|
|
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim();
|
|
|
|
|
txt=txt.replace(/([a-zA-Z])(\d+)/g, '$1 $2').replace(/(\d+)([a-zA-Z])/g, '$1 $2').trim();
|
|
|
|
|
txt=txt.replace(/^[^a-zA-Z0-9]+/, '').trim(); // Strip leading icons/symbols
|
|
|
|
|
if(!txt)continue;
|
|
|
|
|
var txt=cleanButtonText(b);
|
|
|
|
|
console.log("[JSDOM] Button scan:", txt);
|
|
|
|
|
if(txt.length <= 1) continue; // Icon
|
|
|
|
|
|
|
|
|
|
var isBodyRoot = (searchRoots[r] === document.body);
|
|
|
|
|
var isVSCodeMainWindow = !!document.querySelector('.monaco-workbench');
|
|
|
|
|
|
|
|
|
|
// Match against patterns
|
|
|
|
|
var matchedType=null;
|
|
|
|
|
for(var p=0;p<PATS.length;p++){
|
|
|
|
|
if(PATS[p].re.test(txt)){
|
|
|
|
|
// STRUCTURAL CONSTRAINT: To prevent freezing on CodeLens 'Run' or 'Accept' false positives within editor files,
|
|
|
|
|
// ignore these if found inside a CodeLens container.
|
|
|
|
|
if (b.closest('.codelens-decoration') && PATS[p].type !== 'diff_review' && PATS[p].type !== 'permission') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// Prevent duplicates if already scanned via panel root
|
|
|
|
|
if (isBodyRoot && panel && panel.contains(b)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
matchedType=PATS[p].type;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if(!matchedType)continue;
|
|
|
|
|
|
|
|
|
|
// Generate stable ID for the GROUP (use container-based key)
|
|
|
|
|
if(!matchedType){
|
|
|
|
|
console.log("[JSDOM] NOT MATCHED:", txt);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
var container=findButtonContainer(b);
|
|
|
|
|
var groupKey=matchedType+'|group|'+(container?(container.textContent||'').substring(0,40).replace(/\s+/g,' '):'none');
|
|
|
|
|
var groupKey=matchedType+'|'+btnId(b,matchedType);
|
|
|
|
|
console.log("[JSDOM] MATCHED:", matchedType, "KEY:", groupKey, "SENT:", !!_sent[groupKey]);
|
|
|
|
|
if(_sent[groupKey])continue;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (txt.indexOf('Run') === 0 && Array.from(document.body.querySelectorAll('button, [role="button"]')).length < 500) {
|
|
|
|
|
fetch(BASE + '/dump-html', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: document.body.innerHTML
|
|
|
|
|
}).catch(function(){});
|
|
|
|
|
}
|
|
|
|
|
} catch(e) {}
|
|
|
|
|
|
|
|
|
|
// Collect ALL related buttons from the same container
|
|
|
|
|
var siblings=collectSiblingButtons(container,b);
|
|
|
|
|
if(siblings.length===0)siblings=[{btn:b,text:txt,isPrimary:true}];
|
|
|
|
|
|
|
|
|
|
// Build buttons array for multi-choice support
|
|
|
|
|
var buttonsArr=[];
|
|
|
|
|
var btnRefs=[];
|
|
|
|
|
var bidList=[];
|
|
|
|
|
@@ -487,24 +253,29 @@ export function generateApprovalObserverScript(_port: number): string {
|
|
|
|
|
bidList.push(sbid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract context from trigger button
|
|
|
|
|
var desc=extractContext(b);
|
|
|
|
|
|
|
|
|
|
var is_dom_dummy = false;
|
|
|
|
|
if (!desc || desc.trim().length <= 2) {
|
|
|
|
|
desc = "MISSING_DESCRIPTION_FROM_DOM_FALLBACK_TO_STEP_PROBE";
|
|
|
|
|
is_dom_dummy = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var rid=now.toString()+'_'+Math.random().toString(36).substring(2,6);
|
|
|
|
|
|
|
|
|
|
// Mark entire group as sent
|
|
|
|
|
_sent[groupKey]={rid:rid,ts:now};
|
|
|
|
|
for(var mk=0;mk<bidList.length;mk++){_sent[bidList[mk]]={rid:rid,ts:now};}
|
|
|
|
|
for(var mk=0;mk<bidList.length;mk++)_sent[bidList[mk]]={rid:rid,ts:now};
|
|
|
|
|
|
|
|
|
|
log('DETECTED GROUP '+matchedType+': '+buttonsArr.map(function(x){return '"'+x.text+'"';}).join(', ')+' → pending to bridge');
|
|
|
|
|
log('DETECTED GROUP '+matchedType+': '+buttonsArr.map(function(x){return x.text;}).join(', '));
|
|
|
|
|
|
|
|
|
|
// Send to bridge (closure to capture refs)
|
|
|
|
|
(function(rid2,btnRefs2,bidList2,groupKey2,txt2,desc2,type2,buttonsArr2){
|
|
|
|
|
(function(rid2,btnRefs2,bidList2,groupKey2,txt2,desc2,type2,buttonsArr2,isDummy2){
|
|
|
|
|
var payload={
|
|
|
|
|
request_id:rid2,
|
|
|
|
|
command:txt2,
|
|
|
|
|
description:desc2,
|
|
|
|
|
step_type:type2,
|
|
|
|
|
buttons:buttonsArr2
|
|
|
|
|
buttons:buttonsArr2,
|
|
|
|
|
is_dom_dummy: isDummy2
|
|
|
|
|
};
|
|
|
|
|
fetch(BASE+'/pending',{
|
|
|
|
|
method:'POST',
|
|
|
|
|
@@ -512,49 +283,33 @@ export function generateApprovalObserverScript(_port: number): string {
|
|
|
|
|
body:JSON.stringify(payload)
|
|
|
|
|
}).then(function(r){return r.json();}).then(function(d){
|
|
|
|
|
if (!d.ok || d.filtered) {
|
|
|
|
|
log('Pending rejected/filtered for group ['+buttonsArr2.map(function(x){return x.text;}).join(', ')+']');
|
|
|
|
|
delete _sent[groupKey2];
|
|
|
|
|
for(var di=0;di<bidList2.length;di++){delete _sent[bidList2[di]];}
|
|
|
|
|
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
log('Pending created: '+d.request_id+' for group ['+buttonsArr2.map(function(x){return x.text;}).join(', ')+']');
|
|
|
|
|
pollResponseGroup(d.request_id,btnRefs2,bidList2,groupKey2);
|
|
|
|
|
}).catch(function(e){
|
|
|
|
|
log('POST error: '+e.message);
|
|
|
|
|
delete _sent[groupKey2];
|
|
|
|
|
for(var di=0;di<bidList2.length;di++){delete _sent[bidList2[di]];}
|
|
|
|
|
for(var di=0;di<bidList2.length;di++)delete _sent[bidList2[di]];
|
|
|
|
|
});
|
|
|
|
|
})(rid,btnRefs,bidList,groupKey,txt,desc,matchedType,buttonsArr);
|
|
|
|
|
|
|
|
|
|
// Process ONE button GROUP per scan cycle (avoid flooding)
|
|
|
|
|
return;
|
|
|
|
|
})(rid,btnRefs,bidList,groupKey,txt,desc,matchedType,buttonsArr,is_dom_dummy);
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} // end searchRoots loop
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Poll for Discord response (multi-button group aware) ──
|
|
|
|
|
function pollResponseGroup(rid,btnRefs,bidList,groupKey){
|
|
|
|
|
var polls=0;
|
|
|
|
|
var maxPolls=200; // 5 minutes at 1500ms interval
|
|
|
|
|
var polls=0, maxPolls=200;
|
|
|
|
|
var timer=setInterval(function(){
|
|
|
|
|
polls++;
|
|
|
|
|
// Check if ANY button in the group is still in DOM
|
|
|
|
|
var anyAlive=false;
|
|
|
|
|
for(var ai=0;ai<btnRefs.length;ai++){
|
|
|
|
|
if(document.body.contains(btnRefs[ai])){anyAlive=true;break;}
|
|
|
|
|
}
|
|
|
|
|
if(!anyAlive){
|
|
|
|
|
log('All buttons removed from DOM — stopping poll for '+rid);
|
|
|
|
|
if(!anyAlive || polls>maxPolls){
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
delete _sent[groupKey];
|
|
|
|
|
for(var ci=0;ci<bidList.length;ci++){delete _sent[bidList[ci]];}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if(polls>maxPolls){
|
|
|
|
|
log('Poll timeout for '+rid);
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
delete _sent[groupKey];
|
|
|
|
|
for(var ti=0;ti<bidList.length;ti++){delete _sent[bidList[ti]];}
|
|
|
|
|
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){
|
|
|
|
|
@@ -562,56 +317,37 @@ export function generateApprovalObserverScript(_port: number): string {
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
var btnIdx=(typeof d.button_index==='number'&&d.button_index>=0)?d.button_index:-1;
|
|
|
|
|
if(btnIdx>=0&&btnIdx<btnRefs.length){
|
|
|
|
|
// Multi-choice: click specific button by index
|
|
|
|
|
var targetBtn=btnRefs[btnIdx];
|
|
|
|
|
var targetTxt=(targetBtn.textContent||'').trim();
|
|
|
|
|
log((d.approved?'✅':'❌')+' CHOICE '+rid+' → clicking button['+btnIdx+'] "'+targetTxt+'"');
|
|
|
|
|
targetBtn.click();
|
|
|
|
|
log((d.approved?'✅':'❌')+' CHOICE '+rid+' → clicking button['+btnIdx+']');
|
|
|
|
|
dispatchReactClick(btnRefs[btnIdx]);
|
|
|
|
|
} else if(d.approved){
|
|
|
|
|
// Legacy single-button: click first (primary) button
|
|
|
|
|
var primaryBtn=btnRefs[0];
|
|
|
|
|
log('✅ APPROVED '+rid+' → clicking "'+((primaryBtn.textContent||'').trim())+'"');
|
|
|
|
|
primaryBtn.click();
|
|
|
|
|
log('✅ APPROVED '+rid+' → clicking primary');
|
|
|
|
|
dispatchReactClick(btnRefs[0]);
|
|
|
|
|
} else {
|
|
|
|
|
// Legacy reject: find and click reject/deny button
|
|
|
|
|
log('❌ REJECTED '+rid+' → finding reject button');
|
|
|
|
|
clickRejectButton(btnRefs[0]);
|
|
|
|
|
}
|
|
|
|
|
delete _sent[groupKey];
|
|
|
|
|
for(var ri=0;ri<bidList.length;ri++){delete _sent[bidList[ri]];}
|
|
|
|
|
for(var ri=0;ri<bidList.length;ri++)delete _sent[bidList[ri]];
|
|
|
|
|
}).catch(function(){});
|
|
|
|
|
},1500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Legacy pollResponse for backward compatibility (single button)
|
|
|
|
|
function pollResponse(rid,btn,bid){
|
|
|
|
|
pollResponseGroup(rid,[btn],[bid],bid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Find and click the reject/cancel counterpart button ──
|
|
|
|
|
function clickRejectButton(approveBtn){
|
|
|
|
|
// Walk up to find the container, then search for reject buttons
|
|
|
|
|
var container=approveBtn.closest('[class*="step"]')
|
|
|
|
|
||approveBtn.closest('[class*="action"]')
|
|
|
|
|
||approveBtn.closest('[class*="tool"]')
|
|
|
|
|
||approveBtn.parentElement;
|
|
|
|
|
if(!container){log('No container for reject');return;}
|
|
|
|
|
|
|
|
|
|
var siblings=container.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
|
|
|
|
|
var container=findButtonContainer(approveBtn);
|
|
|
|
|
if(!container)return;
|
|
|
|
|
var siblings=container.querySelectorAll('button');
|
|
|
|
|
for(var i=0;i<siblings.length;i++){
|
|
|
|
|
var t=(siblings[i].textContent||'').trim();
|
|
|
|
|
var t=cleanButtonText(siblings[i]);
|
|
|
|
|
for(var r=0;r<REJECT_RE.length;r++){
|
|
|
|
|
if(REJECT_RE[r].test(t)){
|
|
|
|
|
log('Clicking reject: "'+t+'"');
|
|
|
|
|
siblings[i].click();
|
|
|
|
|
log('Clicking reject: '+t);
|
|
|
|
|
dispatchReactClick(siblings[i]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
log('No reject button found near approve button');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Throttled scan — leading-edge: fires immediately, then locks ──
|
|
|
|
|
function scheduleScan(){
|
|
|
|
|
if(!_ready)return;
|
|
|
|
|
var now=Date.now();
|
|
|
|
|
@@ -628,25 +364,20 @@ export function generateApprovalObserverScript(_port: number): string {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Periodic cleanup of stale _sent entries ──
|
|
|
|
|
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){
|
|
|
|
|
log('Cleanup stale entry: '+keys[i]);
|
|
|
|
|
delete _sent[keys[i]];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},60000);
|
|
|
|
|
|
|
|
|
|
// ── Start observation ──
|
|
|
|
|
function startObserver(){
|
|
|
|
|
if(_obs)return;
|
|
|
|
|
// PRIMARY: MutationObserver — reacts instantly to DOM changes
|
|
|
|
|
new MutationObserver(function(mutations){
|
|
|
|
|
// Only scan if mutations contain added nodes (new buttons potentially)
|
|
|
|
|
for(var i=0;i<mutations.length;i++){
|
|
|
|
|
if(mutations[i].addedNodes.length>0){
|
|
|
|
|
scheduleScan();
|
|
|
|
|
@@ -654,128 +385,38 @@ export function generateApprovalObserverScript(_port: number): string {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).observe(document.body,{childList:true,subtree:true});
|
|
|
|
|
|
|
|
|
|
// FALLBACK: periodic scan every 3s for any missed mutations
|
|
|
|
|
setInterval(scheduleScan,3000);
|
|
|
|
|
|
|
|
|
|
// ── Adaptive idle detection for HTTP polls ──
|
|
|
|
|
var _lastActivity=Date.now();
|
|
|
|
|
var _idleThreshold=60000; // 60s without DOM changes → slow mode
|
|
|
|
|
new MutationObserver(function(){_lastActivity=Date.now();}).observe(document.body,{childList:true,subtree:true,attributes:true});
|
|
|
|
|
function getAdaptiveInterval(){return (Date.now()-_lastActivity>_idleThreshold)?10000:2000;}
|
|
|
|
|
|
|
|
|
|
// ── DEEP-INSPECT POLLING: curl→Bridge→Renderer→Results ──
|
|
|
|
|
(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){log('🔍 Deep inspect triggered via HTTP');runDeepInspect();}
|
|
|
|
|
}).catch(function(){});
|
|
|
|
|
}
|
|
|
|
|
setTimeout(pollDeepInspect,getAdaptiveInterval());
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
// ── TRIGGER-CLICK: Extension→Renderer bridge for programmatic button clicks ──
|
|
|
|
|
// Extension sets clickTrigger via tryApprovalStrategies → renderer polls and clicks
|
|
|
|
|
// v3: uses deepFindButtons() to traverse iframes, webviews, shadow DOMs
|
|
|
|
|
// ── TRIGGER-CLICK POLLING (Fallback for missed pushes) ──
|
|
|
|
|
(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;
|
|
|
|
|
log('🔔 TRIGGER-CLICK received: action='+d.action);
|
|
|
|
|
|
|
|
|
|
var approveRe=[/^(?:Always\s*)?Run/i,/^(?:Always\s*)?Accept/i,/^(?:Always\s*)?Accept all/i,/^(?:Always\s*)?Allow/i,/^(?:Always\s*)?Approve/i,/^Continue$/i,/^Proceed$/i,/^Retry$/i];
|
|
|
|
|
var rejectRe=[/^Reject/i,/^Cancel$/i,/^Deny$/i,/^Stop$/i,/^Decline$/i,/^Dismiss$/i];
|
|
|
|
|
var approveRe=[/^(?:Always\s*)?Run\b/i,/^(?:Always\s*)?Accept\b/i,/^(?:Always\s*)?Accept all\b/i,/^(?:Always\s*)?Allow\b/i,/^(?:Always\s*)?Approve\b/i];
|
|
|
|
|
var rejectRe=[/^Reject\b/i,/^Cancel\b/i,/^Deny\b/i,/^Stop\b/i,/^Decline\b/i,/^Dismiss\b/i];
|
|
|
|
|
var patterns=(d.action==='approve')?approveRe:rejectRe;
|
|
|
|
|
var emoji=(d.action==='approve')?'✅':'❌';
|
|
|
|
|
|
|
|
|
|
// Phase 1: deepFindButtons in main doc + accessible iframes + shadow DOMs
|
|
|
|
|
var found=deepFindButtons(patterns);
|
|
|
|
|
if(found.length>0){
|
|
|
|
|
log(emoji+' TRIGGER-CLICK: clicking "'+found[0].text+'" from '+found[0].source);
|
|
|
|
|
found[0].btn.click();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Phase 2: Try <webview>.executeJavaScript for inaccessible webviews
|
|
|
|
|
var webviews=document.querySelectorAll('webview');
|
|
|
|
|
if(webviews.length>0){
|
|
|
|
|
log('TRIGGER-CLICK: trying '+webviews.length+' webview(s) via executeJavaScript...');
|
|
|
|
|
var patternsStr=patterns.map(function(re){return re.source;}).join('|');
|
|
|
|
|
var clickScript='(function(){'+
|
|
|
|
|
'var re=new RegExp("'+patternsStr+'","i");'+
|
|
|
|
|
'var btns=document.querySelectorAll("button, [role=\"button\"], vscode-button, .monaco-text-button");'+
|
|
|
|
|
'for(var i=0;i<btns.length;i++){'+
|
|
|
|
|
'var b=btns[i];if(b.disabled||b.hidden)continue;'+
|
|
|
|
|
'var t=(b.textContent||"").trim().replace(/(Alt|Ctrl|Shift|Meta)\\+.*/i,"").replace(/^[^a-zA-Z0-9]+/,"").trim();'+
|
|
|
|
|
'if(re.test(t)){b.click();return "CLICKED:"+t;}'+
|
|
|
|
|
'}'+
|
|
|
|
|
'return "NOT_FOUND:"+btns.length+"_buttons";'+
|
|
|
|
|
'})()';
|
|
|
|
|
for(var w=0;w<webviews.length;w++){
|
|
|
|
|
(function(wv,idx){
|
|
|
|
|
try{
|
|
|
|
|
if(typeof wv.executeJavaScript==='function'){
|
|
|
|
|
wv.executeJavaScript(clickScript).then(function(result){
|
|
|
|
|
log(emoji+' TRIGGER-CLICK webview#'+idx+': '+result);
|
|
|
|
|
}).catch(function(e){
|
|
|
|
|
log('TRIGGER-CLICK webview#'+idx+' execJS error: '+e.message);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}catch(e){
|
|
|
|
|
log('TRIGGER-CLICK webview#'+idx+' error: '+e.message);
|
|
|
|
|
}
|
|
|
|
|
})(webviews[w],w);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Phase 3: Try iframes via postMessage (cross-origin fallback)
|
|
|
|
|
var iframes=document.querySelectorAll('iframe');
|
|
|
|
|
if(iframes.length>0){
|
|
|
|
|
log('TRIGGER-CLICK: trying '+iframes.length+' iframe(s) — checking accessibility...');
|
|
|
|
|
var clickedAny=false;
|
|
|
|
|
for(var fi=0;fi<iframes.length;fi++){
|
|
|
|
|
try{
|
|
|
|
|
var idoc=iframes[fi].contentDocument||iframes[fi].contentWindow.document;
|
|
|
|
|
if(!idoc)continue;
|
|
|
|
|
var ibtns=idoc.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
|
|
|
|
|
for(var bi=0;bi<ibtns.length;bi++){
|
|
|
|
|
var ib=ibtns[bi];
|
|
|
|
|
if(ib.disabled||ib.hidden)continue;
|
|
|
|
|
var itxt=(ib.textContent||'').trim();
|
|
|
|
|
itxt=itxt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim();
|
|
|
|
|
itxt=itxt.replace(/^[^a-zA-Z0-9]+/, '').trim();
|
|
|
|
|
for(var pi=0;pi<patterns.length;pi++){
|
|
|
|
|
if(patterns[pi].test(itxt)){
|
|
|
|
|
log(emoji+' TRIGGER-CLICK iframe#'+fi+': clicking "'+itxt+'"');
|
|
|
|
|
ib.click();
|
|
|
|
|
clickedAny=true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}catch(e){}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!found.length){
|
|
|
|
|
// Log what we DID find for debugging
|
|
|
|
|
var allBtns=document.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
|
|
|
|
|
var btnTexts=[];
|
|
|
|
|
for(var di=0;di<Math.min(10,allBtns.length);di++){
|
|
|
|
|
btnTexts.push('"'+((allBtns[di].textContent||'').trim()).substring(0,30)+'"');
|
|
|
|
|
}
|
|
|
|
|
log('⚠️ TRIGGER-CLICK: no '+d.action+' button found. Main DOM has '+allBtns.length+' btns: ['+btnTexts.join(',')+']');
|
|
|
|
|
log('⚠️ iframes='+document.querySelectorAll('iframe').length+' webviews='+document.querySelectorAll('webview').length);
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
for(var pi=0;pi<patterns.length;pi++){
|
|
|
|
|
if(patterns[pi].test(t)){
|
|
|
|
|
log('Fallback TRIGGER-CLICK on "' + t + '"');
|
|
|
|
|
dispatchReactClick(bx);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).catch(function(){});
|
|
|
|
|
}
|
|
|
|
|
setTimeout(pollTriggerClick,getAdaptiveInterval());
|
|
|
|
|
setTimeout(pollTriggerClick, 2000);
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
_obs=true;
|
|
|
|
|
log('v3 Observer active — deep DOM traversal + MutationObserver + trigger-click polling');
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|