fix(extension): restore AI Response Content capture by patching DOM extraction, CSP connect-src, and TS regex literal serialization

This commit is contained in:
Variet Worker
2026-04-10 21:10:33 +09:00
parent 58887f6933
commit a99c283656
22 changed files with 744 additions and 592 deletions

View File

@@ -235,3 +235,16 @@
- **원인**: 실시간 텍스트 캡처(`delta > 0`) 조건에 `isRunning &&`이 걸려있어, 상태가 `WAITING`이나 `IDLE`로 즉시 넘어가면 텍스트를 캡처하는 루틴이 전부 스킵됨. 또한 이 순간 `isStall` 조건도 타지 않아 `WAITING` 디텍션도 증발함. - **원인**: 실시간 텍스트 캡처(`delta > 0`) 조건에 `isRunning &&`이 걸려있어, 상태가 `WAITING`이나 `IDLE`로 즉시 넘어가면 텍스트를 캡처하는 루틴이 전부 스킵됨. 또한 이 순간 `isStall` 조건도 타지 않아 `WAITING` 디텍션도 증발함.
- **해결**: 실시간 캡처 로직에서 `isRunning &&` 조건을 제거하고, `delta > 0`일 때 추가된 최신 스텝을 스캔하면서 `PLANNER_RESPONSE`와 `WAITING` 스텝을 모두 처리하도록 수정함. - **해결**: 실시간 캡처 로직에서 `isRunning &&` 조건을 제거하고, `delta > 0`일 때 추가된 최신 스텝을 스캔하면서 `PLANNER_RESPONSE`와 `WAITING` 스텝을 모두 처리하도록 수정함.
- **주의**: LS Backend 10개 Session 제한 버그가 있어, 다른 창에서 수동 채팅(`1fbca84c`)이 IDLE로 남아있으면 자동화 에이전트의 워크스페이스 세션과 헷갈릴 수 있으나, 이 버그는 polling 타이밍 문제였음. - **주의**: 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
View File

View File

@@ -1,3 +1,4 @@
| NNN | 시간 | 작업 설명 | 커밋해시 | 완료여부 | | NNN | 시간 | 작업 설명 | 커밋해시 | 완료여부 |
|---|---|---|---|---| |---|---|---|---|---|
| 001 | 17:11 | step-probe.ts 의 isRunning 조건 누락으로 인한 릴레이 증발 버그 픽스 | COMMITTING | ✅ | | 001 | 17:11 | step-probe.ts 의 isRunning 조건 누락으로 인한 릴레이 증발 버그 픽스 | COMMITTING | ✅ |
| #613 | 21:10 | Bridge Relay AI Chat Body DOM extraction and template literal Regex fix | TBD | ? |

View 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) 빌드 완료 및 릴리스 커밋 패키징 수행.

View File

@@ -1,12 +1,12 @@
{ {
"name": "gravity-bridge", "name": "gravity-bridge",
"version": "0.5.4", "version": "0.5.25",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gravity-bridge", "name": "gravity-bridge",
"version": "0.5.4", "version": "0.5.25",
"dependencies": { "dependencies": {
"ws": "^8.19.0" "ws": "^8.19.0"
}, },

View File

@@ -1,8 +1,8 @@
{ {
"name": "gravity-bridge", "name": "gravity-bridge",
"displayName": "Gravity Bridge", "displayName": "Gravity Bridge",
"description": "Antigravity ↔ Discord 브리지 연동 확장", "description": "Discord-based unified approval system for Antigravity AI interactions.",
"version": "0.5.23", "version": "0.5.27",
"publisher": "variet", "publisher": "variet",
"engines": { "engines": {
"vscode": "^1.100.0" "vscode": "^1.100.0"
@@ -86,4 +86,4 @@
"dependencies": { "dependencies": {
"ws": "^8.19.0" "ws": "^8.19.0"
} }
} }

View File

@@ -246,6 +246,22 @@ function _patchHtmlFiles(scriptDir: string, combinedScript: string, logToFile: (
logToFile(`[OBSERVER] ${spec.name} CSP patched: added 'unsafe-inline' to script-src`); 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) // Remove old external script tag if present (legacy, cannot be served)
const extMarkerStart = '<!-- AG SDK [variet-gravity-bridge] -->'; const extMarkerStart = '<!-- AG SDK [variet-gravity-bridge] -->';
const extMarkerEnd = '<!-- /AG SDK [variet-gravity-bridge] -->'; const extMarkerEnd = '<!-- /AG SDK [variet-gravity-bridge] -->';

View File

@@ -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 { export function generateApprovalObserverScript(_port: number): string {
// Port is hardcoded as fallback, but renderer also reads ag-bridge-ports.json for multi-bridge
return ` return `
// ── Gravity Bridge v3: Approval Observer (deep DOM traversal — iframes, webviews, shadow DOMs) ── // ── Gravity Bridge v4: React Tailwind UI Observer ──
(function(){ (function(){
'use strict'; 'use strict';
var BASE='',_obs=false,_sent={},_ready=false; var BASE='',_obs=false,_sent={},_ready=false;
var _scanScheduled=false,_lastScanTs=0; var _scanScheduled=false,_lastScanTs=0;
var THROTTLE_MS=100; var THROTTLE_MS=500;
var CLEANUP_MS=300000; var CLEANUP_MS=300000;
var _domDumped=false;
function log(m){console.log('[GB Observer] '+m);} 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 ── // React-Compatible Synthetic Clicker
// Searches: main document → iframes (contentDocument) → webview elements → shadow DOMs function dispatchReactClick(el){
function deepFindButtons(patterns){ if (!el) return;
var results=[]; try {
// 1. Prioritize Agent panel el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, view:window, composed:true}));
var panel=findPanel(); el.dispatchEvent(new MouseEvent('mousedown', {bubbles:true, cancelable:true, view:window, composed:true}));
if(panel){ el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, view:window, composed:true}));
collectButtons(panel,results,patterns,'panel'); el.dispatchEvent(new MouseEvent('mouseup', {bubbles:true, cancelable:true, view:window, composed:true}));
if(results.length>0) return results; el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, view:window, composed:true}));
} } catch(e) {
// 2. Prioritize VS Code Toasts & Dialogs el.click(); // fallback
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);});
} }
} }
// Auto-dump on startup (3s delay) // ── Find common container for the step ──
function dumpDOMStructure(){runDeepInspect();} function findButtonContainer(btn){
return btn.closest('.p-1')
// ── Port Discovery: async fetch()-based (sync XHR blocked in Electron renderer) ── || btn.closest('.bg-agent-convo-background')
var HARDCODED_PORT=${_port}; || btn.closest('[class*="border-gray-500/10"]')
|| btn.closest('.monaco-list-row')
function tryPingAsync(port){ || btn.parentElement;
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){ function cleanButtonText(btn) {
log('Waiting for Gravity Bridge status bar item to appear in DOM...'); if (!btn) return '';
var attempts=0; // if internal truncate span, use it
var timer=setInterval(function(){ var tr = btn.querySelector('.truncate');
attempts++; var txt = (tr ? tr.textContent : btn.textContent) || '';
// Search for our specific port injected by the extension host for THIS window. return txt.trim().replace(/(Alt|Ctrl|Shift|Meta)\\\\+.*/i,'').trim();
// 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){ // ── Stable button fingerprint ──
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){ function btnId(b,type){
// Use: type + button text + parent's first 40 chars of text content var txt = cleanButtonText(b);
var txt=(b.textContent||'').trim(); var parent = b.parentElement;
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; var idx=0;
if(parent){ 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;}} 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 ── // ── Context extraction — target BOTH chat history and command payload ──
function extractContext(b){ function extractCommandContext(b){
var curr = b.parentElement; var container = findButtonContainer(b);
var bestDesc = ''; if (!container) return "";
var btnText = (b.innerText || b.textContent || '').trim();
// Debug: Dump the container's raw HTML to bridge for analysis var titleSpans = container.querySelectorAll('span[title^="command("]');
try { if (titleSpans && titleSpans.length > 0) {
var dumpContainer = b.closest('[class*="message"]') || b.closest('[class*="chat"]') || b.closest('.monaco-list-row') || b.parentElement.parentElement; var t = titleSpans[0].getAttribute('title');
if (dumpContainer && dumpContainer.outerHTML) { if (t && t.length > 5) return t.substring(0, 800);
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 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 extractChatContext(b) {
function findButtonContainer(btn){ try {
return btn.closest('[class*="step"]') var botTurn = b.closest('.bg-agent-convo-background') || b.closest('.text-ide-message-block-bot-color');
||btn.closest('[class*="action"]') if (!botTurn) {
||btn.closest('[class*="tool"]') var container = findButtonContainer(b);
||btn.closest('[class*="cascade"]') botTurn = container ? container.parentElement : null;
||btn.closest('[class*="message"]') }
||btn.closest('[class*="dialog"]') if (!botTurn) return '';
||btn.closest('[class*="notification"]')
||btn.parentElement; 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){ function collectSiblingButtons(container,triggerBtn){
if(!container)return []; if(!container)return [];
var siblings=container.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button'); var siblings=container.querySelectorAll('button');
var result=[]; var result=[];
for(var i=0;i<siblings.length;i++){ for(var i=0;i<siblings.length;i++){
var sb=siblings[i]; var sb=siblings[i];
if(sb.disabled||sb.hidden)continue; if(sb.disabled||sb.hidden||(!sb.offsetParent&&sb.style.display!=='fixed'))continue;
try{if(!sb.offsetParent&&sb.style.display!=='fixed')continue;}catch(e){}
var stxt=(sb.textContent||'').trim(); var stxt = cleanButtonText(sb);
stxt=stxt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim(); if(stxt.length <= 1) continue; // Ignore icon buttons
stxt=stxt.replace(/^[^a-zA-Z0-9]+/, '').trim(); // Strip leading icons/symbols
if(!stxt)continue;
// Check if this button matches any actionable pattern
var isAction=false; var isAction=false;
for(var a=0;a<ALL_ACTION_RE.length;a++){ for(var a=0;a<ALL_ACTION_RE.length;a++){
if(ALL_ACTION_RE[a].test(stxt)){isAction=true;break;} if(ALL_ACTION_RE[a].test(stxt)){isAction=true;break;}
@@ -382,100 +164,84 @@ export function generateApprovalObserverScript(_port: number): string {
return result; return result;
} }
// ── Find the React app container (Antigravity's main UI root) ── var HARDCODED_PORT=${_port};
function findPanel(){
// Priority order of panel selectors (most specific first) function tryPingAsync(port){
var selectors=[ return fetch('http://127.0.0.1:'+port+'/ping?t='+Date.now(),{signal:AbortSignal.timeout(2000)})
'.antigravity-agent-side-panel', .then(function(r){return r.text();})
'#jetski-agent-panel', .then(function(t){return t==='pong';})
'.react-app-container', .catch(function(){return false;});
'[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;
} }
// ── Core scan — finds actionable buttons and reports to bridge ── function discoverPort(cb){
// Groups related buttons from same container into a single pending 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(){ function scan(){
if(!_ready)return; if(!_ready)return;
var now=Date.now(); var now=Date.now();
var allBtns=document.querySelectorAll('button');
var panel=findPanel(); if(!allBtns.length)return;
// 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;
for(var j=0;j<allBtns.length;j++){ for(var j=0;j<allBtns.length;j++){
var b=allBtns[j]; var b=allBtns[j];
if(b.disabled||b.hidden)continue; if(b.disabled||b.hidden||(!b.offsetParent&&b.style.display!=='fixed'))continue;
// Check visibility (offsetParent null = hidden via CSS)
if(!b.offsetParent&&b.style.display!=='fixed')continue;
var txt=(b.innerText || b.textContent||'').trim(); var txt=cleanButtonText(b);
if(!txt)continue; console.log("[JSDOM] Button scan:", txt);
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim(); if(txt.length <= 1) continue; // Icon
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 isBodyRoot = (searchRoots[r] === document.body);
var isVSCodeMainWindow = !!document.querySelector('.monaco-workbench');
// Match against patterns
var matchedType=null; var matchedType=null;
for(var p=0;p<PATS.length;p++){ for(var p=0;p<PATS.length;p++){
if(PATS[p].re.test(txt)){ 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') { if (b.closest('.codelens-decoration') && PATS[p].type !== 'diff_review' && PATS[p].type !== 'permission') {
continue; continue;
} }
// Prevent duplicates if already scanned via panel root
if (isBodyRoot && panel && panel.contains(b)) {
continue;
}
matchedType=PATS[p].type; matchedType=PATS[p].type;
break; break;
} }
} }
if(!matchedType)continue; if(!matchedType){
console.log("[JSDOM] NOT MATCHED:", txt);
// Generate stable ID for the GROUP (use container-based key) continue;
}
var container=findButtonContainer(b); 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; 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); var siblings=collectSiblingButtons(container,b);
if(siblings.length===0)siblings=[{btn:b,text:txt,isPrimary:true}]; if(siblings.length===0)siblings=[{btn:b,text:txt,isPrimary:true}];
// Build buttons array for multi-choice support
var buttonsArr=[]; var buttonsArr=[];
var btnRefs=[]; var btnRefs=[];
var bidList=[]; var bidList=[];
@@ -487,24 +253,29 @@ export function generateApprovalObserverScript(_port: number): string {
bidList.push(sbid); bidList.push(sbid);
} }
// Extract context from trigger button
var desc=extractContext(b); 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); var rid=now.toString()+'_'+Math.random().toString(36).substring(2,6);
// Mark entire group as sent
_sent[groupKey]={rid:rid,ts:now}; _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,isDummy2){
(function(rid2,btnRefs2,bidList2,groupKey2,txt2,desc2,type2,buttonsArr2){
var payload={ var payload={
request_id:rid2, request_id:rid2,
command:txt2, command:txt2,
description:desc2, description:desc2,
step_type:type2, step_type:type2,
buttons:buttonsArr2 buttons:buttonsArr2,
is_dom_dummy: isDummy2
}; };
fetch(BASE+'/pending',{ fetch(BASE+'/pending',{
method:'POST', method:'POST',
@@ -512,49 +283,33 @@ export function generateApprovalObserverScript(_port: number): string {
body:JSON.stringify(payload) body:JSON.stringify(payload)
}).then(function(r){return r.json();}).then(function(d){ }).then(function(r){return r.json();}).then(function(d){
if (!d.ok || d.filtered) { if (!d.ok || d.filtered) {
log('Pending rejected/filtered for group ['+buttonsArr2.map(function(x){return x.text;}).join(', ')+']');
delete _sent[groupKey2]; 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; return;
} }
log('Pending created: '+d.request_id+' for group ['+buttonsArr2.map(function(x){return x.text;}).join(', ')+']');
pollResponseGroup(d.request_id,btnRefs2,bidList2,groupKey2); pollResponseGroup(d.request_id,btnRefs2,bidList2,groupKey2);
}).catch(function(e){ }).catch(function(e){
log('POST error: '+e.message);
delete _sent[groupKey2]; 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) break;
return;
} }
} // end searchRoots loop
} }
// ── Poll for Discord response (multi-button group aware) ──
function pollResponseGroup(rid,btnRefs,bidList,groupKey){ function pollResponseGroup(rid,btnRefs,bidList,groupKey){
var polls=0; var polls=0, maxPolls=200;
var maxPolls=200; // 5 minutes at 1500ms interval
var timer=setInterval(function(){ var timer=setInterval(function(){
polls++; polls++;
// Check if ANY button in the group is still in DOM
var anyAlive=false; var anyAlive=false;
for(var ai=0;ai<btnRefs.length;ai++){ for(var ai=0;ai<btnRefs.length;ai++){
if(document.body.contains(btnRefs[ai])){anyAlive=true;break;} if(document.body.contains(btnRefs[ai])){anyAlive=true;break;}
} }
if(!anyAlive){ if(!anyAlive || polls>maxPolls){
log('All buttons removed from DOM — stopping poll for '+rid);
clearInterval(timer); clearInterval(timer);
delete _sent[groupKey]; delete _sent[groupKey];
for(var ci=0;ci<bidList.length;ci++){delete _sent[bidList[ci]];} 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]];}
return; return;
} }
fetch(BASE+'/response/'+rid).then(function(r){return r.json();}).then(function(d){ 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); clearInterval(timer);
var btnIdx=(typeof d.button_index==='number'&&d.button_index>=0)?d.button_index:-1; var btnIdx=(typeof d.button_index==='number'&&d.button_index>=0)?d.button_index:-1;
if(btnIdx>=0&&btnIdx<btnRefs.length){ if(btnIdx>=0&&btnIdx<btnRefs.length){
// Multi-choice: click specific button by index log((d.approved?'✅':'❌')+' CHOICE '+rid+' → clicking button['+btnIdx+']');
var targetBtn=btnRefs[btnIdx]; dispatchReactClick(btnRefs[btnIdx]);
var targetTxt=(targetBtn.textContent||'').trim();
log((d.approved?'✅':'❌')+' CHOICE '+rid+' → clicking button['+btnIdx+'] "'+targetTxt+'"');
targetBtn.click();
} else if(d.approved){ } else if(d.approved){
// Legacy single-button: click first (primary) button log('✅ APPROVED '+rid+' → clicking primary');
var primaryBtn=btnRefs[0]; dispatchReactClick(btnRefs[0]);
log('✅ APPROVED '+rid+' → clicking "'+((primaryBtn.textContent||'').trim())+'"');
primaryBtn.click();
} else { } else {
// Legacy reject: find and click reject/deny button
log('❌ REJECTED '+rid+' → finding reject button'); log('❌ REJECTED '+rid+' → finding reject button');
clickRejectButton(btnRefs[0]); clickRejectButton(btnRefs[0]);
} }
delete _sent[groupKey]; 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(){}); }).catch(function(){});
},1500); },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){ function clickRejectButton(approveBtn){
// Walk up to find the container, then search for reject buttons var container=findButtonContainer(approveBtn);
var container=approveBtn.closest('[class*="step"]') if(!container)return;
||approveBtn.closest('[class*="action"]') var siblings=container.querySelectorAll('button');
||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');
for(var i=0;i<siblings.length;i++){ 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++){ for(var r=0;r<REJECT_RE.length;r++){
if(REJECT_RE[r].test(t)){ if(REJECT_RE[r].test(t)){
log('Clicking reject: "'+t+'"'); log('Clicking reject: '+t);
siblings[i].click(); dispatchReactClick(siblings[i]);
return; return;
} }
} }
} }
log('No reject button found near approve button');
} }
// ── Throttled scan — leading-edge: fires immediately, then locks ──
function scheduleScan(){ function scheduleScan(){
if(!_ready)return; if(!_ready)return;
var now=Date.now(); var now=Date.now();
@@ -628,25 +364,20 @@ export function generateApprovalObserverScript(_port: number): string {
} }
} }
// ── Periodic cleanup of stale _sent entries ──
setInterval(function(){ setInterval(function(){
var now=Date.now(); var now=Date.now();
var keys=Object.keys(_sent); var keys=Object.keys(_sent);
for(var i=0;i<keys.length;i++){ for(var i=0;i<keys.length;i++){
var entry=_sent[keys[i]]; var entry=_sent[keys[i]];
if(entry&&entry.ts&&(now-entry.ts)>CLEANUP_MS){ if(entry&&entry.ts&&(now-entry.ts)>CLEANUP_MS){
log('Cleanup stale entry: '+keys[i]);
delete _sent[keys[i]]; delete _sent[keys[i]];
} }
} }
},60000); },60000);
// ── Start observation ──
function startObserver(){ function startObserver(){
if(_obs)return; if(_obs)return;
// PRIMARY: MutationObserver — reacts instantly to DOM changes
new MutationObserver(function(mutations){ new MutationObserver(function(mutations){
// Only scan if mutations contain added nodes (new buttons potentially)
for(var i=0;i<mutations.length;i++){ for(var i=0;i<mutations.length;i++){
if(mutations[i].addedNodes.length>0){ if(mutations[i].addedNodes.length>0){
scheduleScan(); scheduleScan();
@@ -654,128 +385,38 @@ export function generateApprovalObserverScript(_port: number): string {
} }
} }
}).observe(document.body,{childList:true,subtree:true}); }).observe(document.body,{childList:true,subtree:true});
// FALLBACK: periodic scan every 3s for any missed mutations
setInterval(scheduleScan,3000); setInterval(scheduleScan,3000);
// ── Adaptive idle detection for HTTP polls ── // ── TRIGGER-CLICK POLLING (Fallback for missed pushes) ──
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
(function pollTriggerClick(){ (function pollTriggerClick(){
if(_ready&&BASE){ if(_ready&&BASE){
fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){ fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
if(!d.action)return; if(!d.action)return;
log('🔔 TRIGGER-CLICK received: action='+d.action); 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 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 patterns=(d.action==='approve')?approveRe:rejectRe; var patterns=(d.action==='approve')?approveRe:rejectRe;
var emoji=(d.action==='approve')?'✅':'❌';
var btns = document.querySelectorAll('button');
// Phase 1: deepFindButtons in main doc + accessible iframes + shadow DOMs for(var i=0;i<btns.length;i++){
var found=deepFindButtons(patterns); var bx = btns[i];
if(found.length>0){ if(bx.disabled||bx.hidden||(!bx.offsetParent&&bx.style.display!=='fixed'))continue;
log(emoji+' TRIGGER-CLICK: clicking "'+found[0].text+'" from '+found[0].source); var t = cleanButtonText(bx);
found[0].btn.click(); if(t.length <= 1) continue;
return; for(var pi=0;pi<patterns.length;pi++){
} if(patterns[pi].test(t)){
log('Fallback TRIGGER-CLICK on "' + t + '"');
// Phase 2: Try <webview>.executeJavaScript for inaccessible webviews dispatchReactClick(bx);
var webviews=document.querySelectorAll('webview'); return;
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);
} }
}).catch(function(){}); }).catch(function(){});
} }
setTimeout(pollTriggerClick,getAdaptiveInterval()); setTimeout(pollTriggerClick, 2000);
})(); })();
_obs=true; _obs=true;
log('v3 Observer active — deep DOM traversal + MutationObserver + trigger-click polling');
} }
})(); })();
`; `;
} }

View File

@@ -244,6 +244,45 @@ function setupMonitor() {
if (pollCount <= 3) ctx.logToFile(`[POLL] GetDiagnostics fallback failed: ${e.message}`); 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 (!allTraj?.trajectorySummaries || Object.keys(allTraj.trajectorySummaries).length === 0) {
if (pollCount <= 3) ctx.logToFile('[POLL] no trajectorySummaries found from any source'); if (pollCount <= 3) ctx.logToFile('[POLL] no trajectorySummaries found from any source');
return; return;

View File

@@ -71,13 +71,31 @@ export function extractPlannerText(step: any): string | null {
/** Filter out system ephemeral messages and non-content strings. */ /** Filter out system ephemeral messages and non-content strings. */
export function filterEphemeral(text: string): string | null { export function filterEphemeral(text: string): string | null {
if (!text || text.length < 10) { return null; } if (!text || text.length < 10) { return null; }
// Skip system prompt metadata
if (text.includes('<EPHEMERAL_MESSAGE>') || text.includes('<ephemeral_message>')) { return null; } // Strip ephemeral system blocks entirely without dropping the user-facing text
if (text.includes('artifact_reminder') || text.includes('active_task_reminder')) { return null; } let cleaned = text;
if (text.includes('no_active_task_reminder')) { return null; }
// 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) // Skip base64/crypto strings (no spaces, mostly alphanumeric)
if (!text.includes(' ') && /^[A-Za-z0-9+/=_\-]{50,}$/.test(text)) { return null; } if (!cleaned.includes(' ') && /^[A-Za-z0-9+/=_\-]{50,}$/.test(cleaned)) { return null; }
return text;
return cleaned;
} }
/** Extract human-readable command from a tool call step's data. */ /** Extract human-readable command from a tool call step's data. */

36
generate_mock.js Normal file
View 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

File diff suppressed because one or more lines are too long

8
scratch_btn.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
}