fix(extension): restore AI Response Content capture by patching DOM extraction, CSP connect-src, and TS regex literal serialization
This commit is contained in:
@@ -235,3 +235,16 @@
|
||||
- **원인**: 실시간 텍스트 캡처(`delta > 0`) 조건에 `isRunning &&`이 걸려있어, 상태가 `WAITING`이나 `IDLE`로 즉시 넘어가면 텍스트를 캡처하는 루틴이 전부 스킵됨. 또한 이 순간 `isStall` 조건도 타지 않아 `WAITING` 디텍션도 증발함.
|
||||
- **해결**: 실시간 캡처 로직에서 `isRunning &&` 조건을 제거하고, `delta > 0`일 때 추가된 최신 스텝을 스캔하면서 `PLANNER_RESPONSE`와 `WAITING` 스텝을 모두 처리하도록 수정함.
|
||||
- **주의**: LS Backend 10개 Session 제한 버그가 있어, 다른 창에서 수동 채팅(`1fbca84c`)이 IDLE로 남아있으면 자동화 에이전트의 워크스페이스 세션과 헷갈릴 수 있으나, 이 버그는 polling 타이밍 문제였음.
|
||||
|
||||
### [2026-04-10] [Extension] AI Response Missing for New Sessions (Session Tracking Failure)
|
||||
- **증상**: 새로운 대화(Session) 시작 시 첫 AI 응답 텍스트가 디스코드에 전혀 전송되지 않는 현상.
|
||||
- **원인**: 백엔드의 `GetAllCascadeTrajectories`가 10개 세션만 반환하여 새 세션이 누락됨. 이를 보완하기 위해 `brain/` 디렉토리를 스캔하는 Fallback 로직이 동작했으나, 신규 세션의 첫 단계에서 `GetCascadeTrajectorySteps`(stepOffset: 0) 호출 시 내부 응답(UTF-8 파싱 등) 에러로 인해 Exception이 발생, `trajectorySummaries`에 세션이 아예 등록되지 않음. 세션이 추적되지 않으니 `delta > 0` 기반의 응답 캡처가 발생하지 않음.
|
||||
- **해결**: `step-probe.ts`의 Fallback 2 `catch` 블록에서 에러가 발생하더라도 강제로 `stepCount: 1`로 세션을 등록하도록 패치하여 세션 인식 유실 방지.
|
||||
- **주의**: API 호출 실패를 조용히 `catch`로 넘기면 전체 파이프라인(여기서는 상태 폴링)이 해당 데이터를 영원히 무시하게 되는 치명적 버그가 발생함. 장애 허용 설계 시 기본값 복원(Fallback State) 설정 필수.
|
||||
|
||||
|
||||
### [2026-04-10] [Extension] Trigger-Click False Positives & Button Matching Failure
|
||||
- **증상**: 디스코드에서 승인(Approve)을 누르면, 에이전트 확장 프로그램이 알맞은 버튼(예: `Always run`)을 누르지 못하거나, 엉뚱한 버튼(예: 상단의 `Running1 command`)을 눌러버려 실제 승인 처리가 누락되는 현상.
|
||||
- **원인**: 1) UI 버튼 텍스트에 `keyboard_arrow_up` 등 머티리얼 아이콘 텍스트가 접착(`Always runkeyboard_arrow_up`)되어 정규식이 실패할 것을 우려해 단어 경계(`\b`)를 제거한 패치가 원인. 단어 경계가 사라지면서 `/Run/i` 패턴이 `Running1 command` 같은 다른 상태 텍스트 버튼에 오탐(False Positive)됨. 2) DOM 순서상 상태 텍스트 버튼이 앞서 있으므로 오탐된 버튼이 우선 클릭됨.
|
||||
- **해결**: `trigger-click` 로직 실행 전 버튼의 `textContent`에서 `keyboard_arrow_up` 등 알려진 꼬리 아이콘 문자열을 명시적으로 제거(strip)하고, 모든 트리거 정규식에 다시 단어 경계(`\b`)를 강제 삽입하여 오탐을 원천 차단함.
|
||||
- **주의**: UI 요소를 DOM에서 긁어올 때는 텍스트에 숨겨진 아이콘/웹폰트 리거쳐(ligatures)가 없는지 검토해야 함. 패턴 매칭 시 꼬리표를 먼저 제거하고 명확한 경계를 부여할 것.
|
||||
|
||||
0
diag_output.txt
Normal file
0
diag_output.txt
Normal file
@@ -1,3 +1,4 @@
|
||||
| NNN | 시간 | 작업 설명 | 커밋해시 | 완료여부 |
|
||||
|---|---|---|---|---|
|
||||
| 001 | 17:11 | step-probe.ts 의 isRunning 조건 누락으로 인한 릴레이 증발 버그 픽스 | COMMITTING | ✅ |
|
||||
| #613 | 21:10 | Bridge Relay AI Chat Body DOM extraction and template literal Regex fix | TBD | ? |
|
||||
|
||||
15
docs/devlog/entries/20260410-613.md
Normal file
15
docs/devlog/entries/20260410-613.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Fix gravity bridge Discord Relay AI Chat Body by patching DOM extraction and Regex literals
|
||||
|
||||
- **시간**: 2026-04-10 20:30~21:10
|
||||
- **Vikunja**: #613 → done
|
||||
|
||||
## 트러블슈팅: Typescript 백틱 안의 정규식 리터럴 파괴 현상
|
||||
- **증상**: JSDOM 가상 모의 환경에서 테스트를 돌려보니, 렌더링 화면이나 타겟 Text가 정확히 매치됨에도 정규식이 조건문에서 `false`를 내뱉으며 Button Matching을 건너뛰는 현상 발생.
|
||||
- **원인**: `observer-script.ts`를 `.js`로 변환할 때, Typescript 컴파일러가 `return \`...\`` 템플릿 리터럴 내부의 `/^(?:Always\s*)?Allow\b/i` 구문을 해석하면서, `\s`를 일반 문자 `s`로, `\b`를 아스키 특수문자 `Backspace(0x08)`로 직렬화하여 클라이언트에 꽂아버리는 문제가 있었음. 이로 인해 정규식 자체가 오염되어 어떠한 버튼도 매칭하지 못하고 있었음.
|
||||
- **해결**: `observer-script` 내부의 정규식 리터럴 내부의 이스케이프 문자(`\s`, `\b` 등)를 전부 이중 백슬래시(`\\s`, `\\b`)로 패치하여 브라우저에서 스크립트가 실행될 때 올바른 정규식 파서가 열리도록 수정 보완함.
|
||||
|
||||
## 결정 사항: 웹뷰 내 로컬 fetch CSP 패치 통과
|
||||
- `html-patcher.ts`에서 웹뷰 렌더링 시점에 CSP를 조작하여 `default-src 'none'` 방어막을 뚫고 `connect-src`에 `http://127.0.0.1:* wss://127.0.0.1:*`를 주입하도록 강제 적용함. 이를 통해 Bridge 서버로의 로컬 HTTP 통신이 활성화됨.
|
||||
|
||||
## 완료 상태
|
||||
VSCode VSIX (0.5.27) 빌드 완료 및 릴리스 커밋 패키징 수행.
|
||||
4
extension/package-lock.json
generated
4
extension/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "gravity-bridge",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.25",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gravity-bridge",
|
||||
"version": "0.5.4",
|
||||
"version": "0.5.25",
|
||||
"dependencies": {
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "gravity-bridge",
|
||||
"displayName": "Gravity Bridge",
|
||||
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
||||
"version": "0.5.23",
|
||||
"description": "Discord-based unified approval system for Antigravity AI interactions.",
|
||||
"version": "0.5.27",
|
||||
"publisher": "variet",
|
||||
"engines": {
|
||||
"vscode": "^1.100.0"
|
||||
|
||||
@@ -246,6 +246,22 @@ function _patchHtmlFiles(scriptDir: string, combinedScript: string, logToFile: (
|
||||
logToFile(`[OBSERVER] ${spec.name} CSP patched: added 'unsafe-inline' to script-src`);
|
||||
}
|
||||
|
||||
// CRITICAL: Patch CSP connect-src to allow HTTP bridge requests from the webview
|
||||
// In Tailwind UI, connect-src is either missing (defaults to 'none') or strict.
|
||||
if (!html.includes('connect-src') && html.includes('default-src')) {
|
||||
html = html.replace(
|
||||
/(default-src\s+'none'\s*;)/,
|
||||
"$1\n\t\t\t\tconnect-src\n\t\t\t\t\t'self'\n\t\t\t\t\thttp://127.0.0.1:*\n\t\t\t\t\thttps://127.0.0.1:*\n\t\t\t\t\twss://127.0.0.1:*\n\t\t\t\t;"
|
||||
);
|
||||
logToFile(`[OBSERVER] ${spec.name} CSP patched: injected connect-src for localhost API`);
|
||||
} else if (html.includes('connect-src') && !html.match(/connect-src[^;]*127\.0\.0\.1/)) {
|
||||
html = html.replace(
|
||||
/(connect-src\s[^;]*?)('self'|vscode-remote-resource:|[a-z-]+:)/i,
|
||||
"$1$2\n\t\t\t\t\thttp://127.0.0.1:*\n\t\t\t\t\thttps://127.0.0.1:*"
|
||||
);
|
||||
logToFile(`[OBSERVER] ${spec.name} CSP patched: added localhost to existing connect-src`);
|
||||
}
|
||||
|
||||
// Remove old external script tag if present (legacy, cannot be served)
|
||||
const extMarkerStart = '<!-- AG SDK [variet-gravity-bridge] -->';
|
||||
const extMarkerEnd = '<!-- /AG SDK [variet-gravity-bridge] -->';
|
||||
|
||||
@@ -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++){
|
||||
// React-Compatible Synthetic Clicker
|
||||
function dispatchReactClick(el){
|
||||
if (!el) return;
|
||||
try {
|
||||
var idoc=iframes[i].contentDocument||iframes[i].contentWindow.document;
|
||||
if(idoc){collectButtons(idoc,results,patterns,'iframe#'+i+'('+iframes[i].className.substring(0,30)+')');}
|
||||
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) {
|
||||
// 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);});
|
||||
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;});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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) ──
|
||||
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 idx=0;
|
||||
if(parent){
|
||||
var siblings=parent.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
|
||||
for(var i=0;i<siblings.length;i++){if(siblings[i]===b){idx=i;break;}}
|
||||
}
|
||||
return type+'|'+txt+'|'+idx+'|'+pctx.substring(0,20);
|
||||
}
|
||||
|
||||
// ── 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();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
return bestDesc.substring(0, 500);
|
||||
}
|
||||
|
||||
// ── Find common container of related buttons ──
|
||||
// ── Find common container for the step ──
|
||||
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"]')
|
||||
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;
|
||||
}
|
||||
|
||||
// ── Collect all actionable sibling buttons from a container ──
|
||||
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();
|
||||
}
|
||||
|
||||
// ── Stable button fingerprint ──
|
||||
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;
|
||||
}
|
||||
|
||||
// ── Context extraction — target BOTH chat history and command payload ──
|
||||
function extractCommandContext(b){
|
||||
var container = findButtonContainer(b);
|
||||
if (!container) return "";
|
||||
|
||||
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);
|
||||
|
||||
var fallback = (container.textContent || '').replace(cleanButtonText(b), '').trim();
|
||||
return fallback.substring(0, 500);
|
||||
}
|
||||
|
||||
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 '';
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
})(rid,btnRefs,bidList,groupKey,txt,desc,matchedType,buttonsArr,is_dom_dummy);
|
||||
|
||||
// Process ONE button GROUP per scan cycle (avoid flooding)
|
||||
return;
|
||||
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();
|
||||
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(itxt)){
|
||||
log(emoji+' TRIGGER-CLICK iframe#'+fi+': clicking "'+itxt+'"');
|
||||
ib.click();
|
||||
clickedAny=true;
|
||||
if(patterns[pi].test(t)){
|
||||
log('Fallback TRIGGER-CLICK on "' + t + '"');
|
||||
dispatchReactClick(bx);
|
||||
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);
|
||||
}
|
||||
}).catch(function(){});
|
||||
}
|
||||
setTimeout(pollTriggerClick,getAdaptiveInterval());
|
||||
setTimeout(pollTriggerClick, 2000);
|
||||
})();
|
||||
|
||||
_obs=true;
|
||||
log('v3 Observer active — deep DOM traversal + MutationObserver + trigger-click polling');
|
||||
}
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -244,6 +244,45 @@ function setupMonitor() {
|
||||
if (pollCount <= 3) ctx.logToFile(`[POLL] GetDiagnostics fallback failed: ${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Fallback 2: The backend API hard-limits to 10 trajectories, often dropping the newest active session.
|
||||
// We physically scan the .gemini/antigravity/brain/ directory to guarantee we track the latest ones.
|
||||
const brainDir = path.resolve(ctx.bridgePath, '..', 'brain');
|
||||
if (fs.existsSync(brainDir)) {
|
||||
const brainDirs = fs.readdirSync(brainDir, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory() && dirent.name.length === 36)
|
||||
.map(dirent => {
|
||||
const stats = fs.statSync(path.join(brainDir, dirent.name));
|
||||
return { name: dirent.name, time: stats.mtimeMs };
|
||||
})
|
||||
.sort((a, b) => b.time - a.time);
|
||||
|
||||
for (let i = 0; i < Math.min(3, brainDirs.length); i++) {
|
||||
const sid = brainDirs[i].name;
|
||||
if (!allTraj.trajectorySummaries[sid]) {
|
||||
try {
|
||||
const stResp = await ctx.sdk.ls.rawRPC('GetCascadeTrajectorySteps', { cascadeId: sid, stepOffset: 0 });
|
||||
if (stResp?.steps) {
|
||||
const len = stResp.steps.length;
|
||||
const ls = len > 0 ? stResp.steps[len - 1] : null;
|
||||
allTraj.trajectorySummaries[sid] = {
|
||||
status: ls?.status || 'CASCADE_RUN_STATUS_RUNNING', // Assume running if we have to force it
|
||||
stepCount: len,
|
||||
lastModifiedTime: new Date(brainDirs[i].time).toISOString(),
|
||||
summary: 'Discovered via brain/ scan',
|
||||
trajectoryMetadata: { workspaces: [{ workspaceFolderAbsoluteUri: ctx.workspaceUri.replace(/\\/g, '/') }] }
|
||||
};
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (pollCount <= 30) ctx.logToFile(`[POLL] Fallback 2 error for sid=${sid}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (pollCount <= 3) ctx.logToFile(`[POLL] brainDir scan fallback failed: ${e.message}`);
|
||||
}
|
||||
|
||||
if (!allTraj?.trajectorySummaries || Object.keys(allTraj.trajectorySummaries).length === 0) {
|
||||
if (pollCount <= 3) ctx.logToFile('[POLL] no trajectorySummaries found from any source');
|
||||
return;
|
||||
|
||||
@@ -71,13 +71,31 @@ export function extractPlannerText(step: any): string | null {
|
||||
/** Filter out system ephemeral messages and non-content strings. */
|
||||
export function filterEphemeral(text: string): string | null {
|
||||
if (!text || text.length < 10) { return null; }
|
||||
// Skip system prompt metadata
|
||||
if (text.includes('<EPHEMERAL_MESSAGE>') || text.includes('<ephemeral_message>')) { return null; }
|
||||
if (text.includes('artifact_reminder') || text.includes('active_task_reminder')) { return null; }
|
||||
if (text.includes('no_active_task_reminder')) { return null; }
|
||||
|
||||
// Strip ephemeral system blocks entirely without dropping the user-facing text
|
||||
let cleaned = text;
|
||||
|
||||
// Target the specific Gemini system prompt injection format:
|
||||
// "The following is an <EPHEMERAL_MESSAGE>... </ephemeral_message>"
|
||||
// Make sure to match case-insensitively and dotAll
|
||||
cleaned = cleaned.replace(/The following is an <EPHEMERAL_MESSAGE>[\s\S]*?(?:<\/ephemeral_message>|<\/EPHEMERAL_MESSAGE>|$)/gi, '');
|
||||
|
||||
// Target standard blocks
|
||||
cleaned = cleaned.replace(/<EPHEMERAL_MESSAGE>[\s\S]*?<\/EPHEMERAL_MESSAGE>/gi, '');
|
||||
cleaned = cleaned.replace(/<ephemeral_message>[\s\S]*?<\/ephemeral_message>/gi, '');
|
||||
|
||||
// Strip other known reminder blocks (if they exist as XML-like tags, just remove them)
|
||||
cleaned = cleaned.replace(/<artifact_reminder>[\s\S]*?<\/artifact_reminder>/gi, '');
|
||||
cleaned = cleaned.replace(/<active_task_reminder>[\s\S]*?<\/active_task_reminder>/gi, '');
|
||||
cleaned = cleaned.replace(/<no_active_task_reminder>[\s\S]*?<\/no_active_task_reminder>/gi, '');
|
||||
|
||||
cleaned = cleaned.trim();
|
||||
if (!cleaned || cleaned.length < 10) { return null; }
|
||||
|
||||
// Skip base64/crypto strings (no spaces, mostly alphanumeric)
|
||||
if (!text.includes(' ') && /^[A-Za-z0-9+/=_\-]{50,}$/.test(text)) { return null; }
|
||||
return text;
|
||||
if (!cleaned.includes(' ') && /^[A-Za-z0-9+/=_\-]{50,}$/.test(cleaned)) { return null; }
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/** Extract human-readable command from a tool call step's data. */
|
||||
|
||||
36
generate_mock.js
Normal file
36
generate_mock.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const fs=require('fs');
|
||||
const {JSDOM}=require('jsdom');
|
||||
const rawDump = JSON.parse(fs.readFileSync('C:/Users/Variet-Worker/.gemini/antigravity/bridge/dump_html.json', 'utf8'));
|
||||
|
||||
// Inject Tailwind script so the mock renders styles properly when opened in a browser
|
||||
const htmlStr = rawDump.html;
|
||||
const dom = new JSDOM("<!DOCTYPE html><html><head><script src='https://cdn.tailwindcss.com'></script></head><body>" + htmlStr + "</body></html>");
|
||||
const document = dom.window.document;
|
||||
|
||||
const titleSpan = document.querySelector('span[title^="command("]');
|
||||
if(titleSpan) {
|
||||
let toolContainer = titleSpan.parentElement;
|
||||
while(toolContainer && !toolContainer.className.includes("border-gray-500/10") && !toolContainer.className.includes("bg-gray-500/10")) {
|
||||
toolContainer = toolContainer.parentElement;
|
||||
}
|
||||
if(!toolContainer) toolContainer = titleSpan.parentElement;
|
||||
|
||||
const aiChat = document.createElement('div');
|
||||
aiChat.className = 'markdown prose';
|
||||
aiChat.innerHTML = '<p>안녕하세요! 시스템을 수정하기 위해 요청하신 작업을 시작합니다. <b>디스코드 릴레이 기능 복구</b>를 위해 스크립트를 실행하겠습니다.</p>';
|
||||
|
||||
const parent = toolContainer.parentElement;
|
||||
const convoWrapper = document.createElement('div');
|
||||
convoWrapper.className = 'bg-agent-convo-background p-4 rounded-lg my-4 bg-gray-800 text-white';
|
||||
parent.insertBefore(convoWrapper, toolContainer);
|
||||
convoWrapper.appendChild(aiChat);
|
||||
convoWrapper.appendChild(toolContainer);
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mt-2';
|
||||
btn.innerHTML = '<span class="truncate">Allow</span>';
|
||||
toolContainer.appendChild(btn);
|
||||
}
|
||||
|
||||
fs.writeFileSync('C:/Users/Variet-Worker/Desktop/gravity_control/mock_output.html', dom.serialize());
|
||||
console.log('Saved to mock_output.html');
|
||||
1
mock_output.html
Normal file
1
mock_output.html
Normal file
File diff suppressed because one or more lines are too long
8
scratch_btn.py
Normal file
8
scratch_btn.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import json; d=json.load(open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', encoding='utf-8', errors='ignore')); print('Total Nodes:', len(d.get('nodes',[])));
|
||||
for n in d.get('nodes', []):
|
||||
if 'agent' in n.get('label','').lower() or n.get('buttons'):
|
||||
print(f"\n[Node] {n.get('label')}")
|
||||
for b in n.get('buttons', []):
|
||||
print(f" BTN: '{b.get('text')}' class='{b.get('class')}' hidden={b.get('hidden')} disabled={b.get('disabled')}")
|
||||
for b in n.get('roleBtns', []):
|
||||
print(f" ROLE-BTN: '{b.get('text')}'")
|
||||
6
scratch_btn2.py
Normal file
6
scratch_btn2.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import json, re; d=json.load(open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', encoding='utf-8', errors='ignore'))
|
||||
approveRe=[re.compile(r'^(?:Always\s*)?Run\b', re.IGNORECASE), re.compile(r'^(?:Always\s*)?Accept\b', re.IGNORECASE)]
|
||||
for b in d['nodes'][0]['buttons']:
|
||||
t = b['text'].strip()
|
||||
t = re.sub(r'(?:keyboard_arrow_up|keyboard_arrow_down)$', '', t, flags=re.IGNORECASE).strip()
|
||||
if any(p.match(t) for p in approveRe): print(f"MATCH: {b['text']} -> {t}")
|
||||
16
scratch_diag_7.py
Normal file
16
scratch_diag_7.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import asyncio
|
||||
import json
|
||||
from mcp_client import MCPClient
|
||||
|
||||
async def main():
|
||||
client = MCPClient()
|
||||
await client.connect()
|
||||
try:
|
||||
# Get raw API response
|
||||
resp = await client.request("EvaluateCascadeLspMethods", {"method": "GetDiagnostics", "params": "{}"})
|
||||
print(json.dumps(resp, indent=2))
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
20
scratch_diag_8.js
Normal file
20
scratch_diag_8.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
async function testDiag() {
|
||||
const bridgePath = process.cwd();
|
||||
// we want to list latest brainDir and check state summary instead.
|
||||
const brainDir = path.resolve(bridgePath, '.gemini/antigravity/brain');
|
||||
if (fs.existsSync(brainDir)) {
|
||||
const brainDirs = fs.readdirSync(brainDir, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory() && dirent.name.length === 36)
|
||||
.map(dirent => {
|
||||
const stats = fs.statSync(path.join(brainDir, dirent.name));
|
||||
return { name: dirent.name, time: stats.mtimeMs };
|
||||
})
|
||||
.sort((a, b) => b.time - a.time);
|
||||
|
||||
console.log(`Latest brain UUIDs:`, brainDirs.slice(0, 3));
|
||||
}
|
||||
}
|
||||
testDiag();
|
||||
5
scratch_diag_9.js
Normal file
5
scratch_diag_9.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const fs = require('fs');
|
||||
const readline = require('readline');
|
||||
// Let's parse extension.log to find the steps! No wait, let's just make a script that uses rawRPC.
|
||||
// I can't use rawRPC from an external script easily because it needs the MCP connection or WS bridge.
|
||||
// Wait! The bot has a bridge!
|
||||
4
scratch_test.js
Normal file
4
scratch_test.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const fs = require('fs');
|
||||
const lsPath = "C:\\Users\\Variet-Worker\\.gemini\\antigravity\\sdk\\ls.js";
|
||||
// Dummy script to just read allTraj.trajectorySummaries by using the SDK directly?
|
||||
// Actually simpler: I can just grep the log if I log it.
|
||||
5
scratch_trigger.ps1
Normal file
5
scratch_trigger.ps1
Normal file
@@ -0,0 +1,5 @@
|
||||
Start-Sleep -Seconds 3
|
||||
$log = Get-Content 'C:\Users\Variet-Worker\.gemini\antigravity\bridge\extension.log' -Tail 500
|
||||
$port = 0
|
||||
foreach ($line in $log) { if ($line -match 'port (\d+)') { $port = $Matches[1] } }
|
||||
if ($port -gt 0) { Invoke-RestMethod -Uri "http://127.0.0.1:$port/deep-inspect"; Write-Host 'Dump success!' }
|
||||
66
test_dom.js
Normal file
66
test_dom.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const fs = require('fs');
|
||||
const { JSDOM } = require("jsdom");
|
||||
|
||||
try {
|
||||
const observerModule = require("./extension/out/observer-script.js");
|
||||
|
||||
const dumpRaw = fs.readFileSync('C:\\Users\\Variet-Worker\\.gemini\\antigravity\\bridge\\dump_html.json', 'utf8');
|
||||
const parseData = JSON.parse(dumpRaw);
|
||||
let htmlStr = parseData.html;
|
||||
|
||||
// Inject fake port discovery node so it passes discoverPort()
|
||||
htmlStr += `<div aria-label="Gravity Bridge Control port:1234"></div>`;
|
||||
|
||||
const dom = new JSDOM(htmlStr, { url: "http://localhost/", runScripts: "dangerously" });
|
||||
const window = dom.window;
|
||||
const document = window.document;
|
||||
|
||||
let testResults = [];
|
||||
|
||||
// Mock fetch for the observer
|
||||
window.fetch = async (url, options) => {
|
||||
if (url.includes('/ping')) {
|
||||
return { text: async () => 'pong' };
|
||||
}
|
||||
if (url.includes('/pending') && options?.method === 'POST') {
|
||||
const body = JSON.parse(options.body);
|
||||
testResults.push("✅ POST /pending intercepted! Payload:");
|
||||
testResults.push(JSON.stringify(body, null, 2));
|
||||
return { json: async () => ({ok: true, request_id: body.request_id}) };
|
||||
}
|
||||
return { json: async () => ({}) };
|
||||
};
|
||||
|
||||
// Fallback overrides
|
||||
window.console.log = (m) => testResults.push(`[Script Log] ${m}`);
|
||||
window.MutationObserver = window.MutationObserver || class { observe(){} };
|
||||
window.AbortSignal = { timeout: () => ({}) };
|
||||
|
||||
let scriptStr = observerModule.generateApprovalObserverScript(1234);
|
||||
// Brutally bypass discoverPort block and force initialization
|
||||
scriptStr = scriptStr.replace(/discoverPort\(function\(port\)\{[\s\S]*?\}\);/, "BASE='http://127.0.0.1:1234';_ready=true;startObserver();");
|
||||
scriptStr = scriptStr.replace("function scan(){", "function scan(){ log('scan() STAGE 1'); log('buttons in DOM: ' + document.querySelectorAll('button').length);");
|
||||
scriptStr = scriptStr.replace("_obs=true;", "_obs=true; log('Forcing scan'); scan();");
|
||||
|
||||
// Run script inside JSDOM
|
||||
const scriptEl = document.createElement("script");
|
||||
scriptEl.textContent = scriptStr;
|
||||
document.body.appendChild(scriptEl);
|
||||
|
||||
// Wait 3 seconds for discoverPort -> ping -> startObserver -> scheduleScan to execute
|
||||
setTimeout(() => {
|
||||
console.log("=== TEST RESULTS ===");
|
||||
console.log(testResults.join("\n"));
|
||||
if (!testResults.some(l => l.includes('POST /pending intercepted'))) {
|
||||
console.error("❌ FAILED: No POST to /pending was made. The DOM scan failed to find the dummy button or extract context.");
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("✅ SUCCESS: The DOM extraction is functioning properly.");
|
||||
process.exit(0);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
} catch (e) {
|
||||
console.error("Test Harness Error:", e);
|
||||
process.exit(1);
|
||||
}
|
||||
144
test_dom_mock.js
Normal file
144
test_dom_mock.js
Normal file
@@ -0,0 +1,144 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const jsdom = require('jsdom');
|
||||
const { JSDOM } = jsdom;
|
||||
|
||||
const observerModule = require('./extension/out/observer-script.js');
|
||||
const observerCode = observerModule.generateApprovalObserverScript(8080);
|
||||
const rawDump = JSON.parse(fs.readFileSync('C:/Users/Variet-Worker/.gemini/antigravity/bridge/dump_html.json', 'utf8'));
|
||||
|
||||
// Instantiate DOM
|
||||
const dom = new JSDOM(rawDump.html, { runScripts: "dangerously", pretendToBeVisual: true, url: "http://localhost/" });
|
||||
const window = dom.window;
|
||||
const document = window.document;
|
||||
|
||||
// Polyfill offsetParent for visibility check
|
||||
Object.defineProperty(window.HTMLElement.prototype, 'offsetParent', {
|
||||
get() { return document.body; }
|
||||
});
|
||||
Object.defineProperty(window.HTMLElement.prototype, 'style', {
|
||||
get() { return { display: 'block' }; }
|
||||
});
|
||||
|
||||
// Mock innerText (JSDOM does not fully support it, but generic walker uses nodeValue / textContent)
|
||||
// Our logic uses nodeValue for TextNodes, so it will work in JSDOM out of the box!
|
||||
|
||||
// Setup the DOM tree to perfectly match a real Chat conversation
|
||||
const titleSpan = document.querySelector('span[title^="command("]');
|
||||
if(!titleSpan) {
|
||||
console.error("COULD NOT FIND command SPAN in dump?!");
|
||||
process.exit(1);
|
||||
}
|
||||
// Find the card container
|
||||
let toolContainer = titleSpan.parentElement;
|
||||
while(toolContainer && !toolContainer.className.includes("border-gray-500/10") && !toolContainer.className.includes("bg-gray-500/10")) {
|
||||
toolContainer = toolContainer.parentElement;
|
||||
}
|
||||
if(!toolContainer) toolContainer = titleSpan.parentElement; // fallback
|
||||
|
||||
// Create an AI text block just above it
|
||||
const aiChat = document.createElement('div');
|
||||
aiChat.className = 'markdown prose';
|
||||
aiChat.innerHTML = '<p>안녕하세요! 시스템을 수정하기 위해 요청하신 작업을 시작합니다. <b>디스코드 릴레이 기능 복구</b>를 위해 스크립트를 실행하겠습니다.</p>';
|
||||
|
||||
// Wrap them up in the turn container
|
||||
const parent = toolContainer.parentElement;
|
||||
const convoWrapper = document.createElement('div');
|
||||
convoWrapper.className = 'bg-agent-convo-background';
|
||||
parent.insertBefore(convoWrapper, toolContainer);
|
||||
convoWrapper.appendChild(aiChat);
|
||||
convoWrapper.appendChild(toolContainer); // Move tool inside the convo wrapper as a sibling to AI chat
|
||||
|
||||
// Add action button to the tool container
|
||||
const btn = document.createElement('button');
|
||||
btn.innerHTML = '<span class="truncate">Allow</span>';
|
||||
toolContainer.appendChild(btn);
|
||||
|
||||
console.log("Mock Button offsetParent:", btn.offsetParent ? btn.offsetParent.tagName : 'null');
|
||||
console.log("Mock Button display:", btn.style.display);
|
||||
console.log("Mock Button text:", btn.textContent);
|
||||
|
||||
// MOCK FETCH
|
||||
const fetchCalls = [];
|
||||
window.fetch = function(url, options) {
|
||||
fetchCalls.push({url, options});
|
||||
if (url.includes('/ping')) {
|
||||
return Promise.resolve({ text: function() { return Promise.resolve('pong'); } });
|
||||
}
|
||||
if (url.includes('/pending')) {
|
||||
return Promise.resolve({ json: function() { return Promise.resolve({ok: true, request_id: 'test-rid'}); } });
|
||||
}
|
||||
return Promise.resolve({ json: function() { return Promise.resolve({}); } });
|
||||
};
|
||||
|
||||
// Polyfill offsetParent for visibility check
|
||||
Object.defineProperty(window.HTMLElement.prototype, 'offsetParent', {
|
||||
get() { return document.body; }
|
||||
});
|
||||
Object.defineProperty(window.HTMLElement.prototype, 'style', {
|
||||
get() { return { display: 'block' }; }
|
||||
});
|
||||
|
||||
const originalLog = console.log;
|
||||
window.console.log = function(...args) {
|
||||
if (args.length > 0 && typeof args[0] === 'string' && args[0].includes("NOT MATCHED")) {
|
||||
let txt = args[1];
|
||||
let codes = [];
|
||||
if (txt) {
|
||||
for(let i=0; i<txt.length; i++) codes.push(txt.charCodeAt(i));
|
||||
}
|
||||
originalLog('[JSDOM-WIN]', args[0], `\nRAW="${txt}"\nCODES=[${codes.join(',')}]`);
|
||||
} else {
|
||||
originalLog('[JSDOM-WIN]', ...args);
|
||||
}
|
||||
};
|
||||
|
||||
// Inject the observer
|
||||
const scriptEl = document.createElement('script');
|
||||
scriptEl.textContent = observerCode;
|
||||
document.body.appendChild(scriptEl);
|
||||
|
||||
console.log("Observer injected, waiting for cycles...");
|
||||
console.log("Total Buttons in DOM:", document.querySelectorAll('button').length);
|
||||
|
||||
// Emulate UI mutation to trigger the MutationObserver and force an instant scan()
|
||||
setTimeout(() => {
|
||||
console.log("Triggering DOM mutation to force scan()...");
|
||||
document.body.appendChild(document.createElement('span'));
|
||||
}, 1500);
|
||||
|
||||
// Give it time to finish scan().
|
||||
setTimeout(() => {
|
||||
console.log("\n====== FETCH CALLS ======");
|
||||
if(fetchCalls.length === 0) console.log("NO FETCH CALLS MADE!");
|
||||
|
||||
fetchCalls.forEach(c => {
|
||||
console.log(`\n[${c.options ? c.options.method || 'GET' : 'GET'}] ${c.url}`);
|
||||
if(c.options && c.options.body) {
|
||||
try {
|
||||
let j = JSON.parse(c.options.body);
|
||||
console.log("[BODY] request_id:", j.request_id);
|
||||
console.log("[BODY] command:", j.command);
|
||||
console.log("[BODY] description:\n" + "=".repeat(40) + "\n" + j.description + "\n" + "=".repeat(40));
|
||||
} catch(e) {
|
||||
console.log("[BODY]", c.options.body);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Determine success
|
||||
const pendingCall = fetchCalls.find(c => c.url.includes('/pending'));
|
||||
if(pendingCall && pendingCall.options && pendingCall.options.body) {
|
||||
const payload = JSON.parse(pendingCall.options.body);
|
||||
if(payload.description.includes("안녕하세요!") && payload.description.includes("METHOD=TITLE_SPAN")) {
|
||||
console.log("\n✅ SUCCESS: Both Chat Body & Tool Command effectively extracted!");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log("\n❌ FAIL: Payload description missing either chat text or command string!");
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log("\n❌ FAIL: /pending never called!");
|
||||
process.exit(1);
|
||||
}
|
||||
}, 4000);
|
||||
98
test_logic.js
Normal file
98
test_logic.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const fs = require('fs');
|
||||
const { JSDOM } = require("jsdom");
|
||||
|
||||
try {
|
||||
const dumpRaw = fs.readFileSync('C:\\Users\\Variet-Worker\\.gemini\\antigravity\\bridge\\dump_html.json', 'utf8');
|
||||
const parseData = JSON.parse(dumpRaw);
|
||||
let htmlStr = parseData.html;
|
||||
|
||||
const dom = new JSDOM(htmlStr);
|
||||
const document = dom.window.document;
|
||||
|
||||
// Direct copy of functions from observer-script.ts
|
||||
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 cleanButtonText(btn) {
|
||||
if (!btn) return '';
|
||||
var tr = btn.querySelector('.truncate');
|
||||
var txt = (tr ? tr.textContent : btn.textContent) || '';
|
||||
return txt.trim().replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim();
|
||||
}
|
||||
|
||||
function extractContext(b){
|
||||
var container = findButtonContainer(b);
|
||||
if (!container) return "ERROR_NO_CONTAINER";
|
||||
|
||||
var titleSpans = container.querySelectorAll('span[title^="command("]');
|
||||
if (titleSpans && titleSpans.length > 0) {
|
||||
var t = titleSpans[0].getAttribute('title');
|
||||
if (t && t.length > 5) return "METHOD=TITLE_SPAN | " + 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 "METHOD=PRE_SPAN | " + 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 "METHOD=CODES | " + codeText.trim().substring(0, 800);
|
||||
|
||||
var fallback = (container.textContent || '').replace(cleanButtonText(b), '').trim();
|
||||
return "METHOD=FALLBACK | " + fallback.substring(0, 500);
|
||||
}
|
||||
|
||||
// RUN TEST
|
||||
const allBtns = document.querySelectorAll('button');
|
||||
console.log(`Total buttons found: ${allBtns.length}`);
|
||||
|
||||
let tested = 0;
|
||||
for(let j=0; j<allBtns.length; j++) {
|
||||
let b = allBtns[j];
|
||||
let txt = cleanButtonText(b);
|
||||
if (txt.length <= 1) continue; // Icon
|
||||
|
||||
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 matchedType=null;
|
||||
for(var p=0;p<PATS.length;p++){
|
||||
if(PATS[p].re.test(txt)){
|
||||
matchedType=PATS[p].type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedType) continue;
|
||||
|
||||
console.log(`\n✅ Matched Button: "${txt}" (Type: ${matchedType})`);
|
||||
console.log(` Extracting Context Data...`);
|
||||
console.log(` -> ` + extractContext(b));
|
||||
tested++;
|
||||
}
|
||||
|
||||
if (tested === 0) {
|
||||
console.log("❌ No actionable buttons matched!");
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("\n✅ SUCCESS: Context fully extracted via DOM script logic.");
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user