refactor(bridge): migrate gravity bridge to pure websocket gateway architecture, deleting legacy local file scanners and dependencies

This commit is contained in:
Variet Worker
2026-04-11 13:06:38 +09:00
parent 5e697cd919
commit 072f83bf25
20 changed files with 756 additions and 1537 deletions

View File

@@ -1,6 +1,6 @@
export function generateApprovalObserverScript(_port: number): string {
return `
// ── Gravity Bridge v4: React Tailwind UI Observer ──
// ── Gravity Bridge v5: Context-First DOM Extraction ──
(function(){
'use strict';
var BASE='',_obs=false,_sent={},_ready=false;
@@ -9,7 +9,7 @@ export function generateApprovalObserverScript(_port: number): string {
var CLEANUP_MS=300000;
function log(m){console.log('[GB Observer] '+m);}
log('v4 Script loaded — deep Tailwind DOM traversal enabled');
log('v5 Script loaded — Context-First Tailored Extraction');
// React-Compatible Synthetic Clicker
function dispatchReactClick(el){
@@ -21,19 +21,10 @@ export function generateApprovalObserverScript(_port: number): string {
el.dispatchEvent(new MouseEvent('mouseup', {bubbles:true, cancelable:true, view:window, composed:true}));
el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, view:window, composed:true}));
} catch(e) {
el.click(); // fallback
el.click();
}
}
// ── Find common container for the step ──
function findButtonContainer(btn){
return btn.closest('.p-1')
|| btn.closest('.bg-agent-convo-background')
|| btn.closest('[class*="border-gray-500/10"]')
|| btn.closest('.monaco-list-row')
|| btn.parentElement;
}
function cleanButtonText(btn) {
if (!btn) return '';
var clone = btn.cloneNode(true);
@@ -43,10 +34,9 @@ export function generateApprovalObserverScript(_port: number): string {
}
var tr = clone.querySelector('.truncate');
var txt = (tr ? tr.textContent : clone.textContent) || '';
return txt.trim().replace(/^[\s\u200B-\u200D\uFEFF\u00A0]+/, '').replace(/(Alt|Ctrl|Shift|Meta)\\+.*/i,'').trim();
return txt.trim().replace(/^[\s\u200B-\u200D\uFEFF\u00A0]+/, '').replace(/(Alt|Ctrl|Shift|Meta)\\\\+.*/i,'').trim();
}
// ── Stable button fingerprint ──
function btnId(b,type){
var txt = cleanButtonText(b);
var parent = b.parentElement;
@@ -58,104 +48,78 @@ export function generateApprovalObserverScript(_port: number): string {
return type+'|'+txt+'|'+idx;
}
// ── Context extraction — target BOTH chat history and command payload ──
function extractCommandContext(b){
var container = findButtonContainer(b);
var container = b.closest('.p-1') || b.parentElement.parentElement;
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 extractChatContextFromNode(botTurn) {
if (!botTurn) return '';
var res = '';
// Use innerText if available on the markdown container (preserves spacing perfectly)
var md = botTurn.querySelector('.markdown-body') || botTurn.querySelector('.prose');
if (md && md.innerText && md.innerText.trim().length > 10) {
res = md.innerText.trim();
return res.substring(0, 3500);
}
var toolContainer = botTurn.querySelector('.bg-agent-convo-background') || botTurn.querySelector('.bg-ide-background-color');
var textParts = [];
function walk(node) {
if (toolContainer && node === toolContainer) return;
if (node.id === 'antigravity.agentSidePanelInputBox') return;
if (node.nodeType === 1) {
var tag = node.tagName.toUpperCase();
if (tag==='BUTTON' || tag==='SVG' || tag==='STYLE' || tag==='SCRIPT') return;
// Skip tool action blocks aggressively if they masquerade as normal divs
if (node !== botTurn && node.classList && (node.classList.contains('bg-ide-background-color') || node.classList.contains('bg-agent-convo-background'))) return;
}
if (node.nodeType === 3) {
var val = node.nodeValue;
if (val && val.trim()) textParts.push(val.trim());
} else {
if (node.childNodes && node.childNodes.length > 0) {
for(var i=0; i<node.childNodes.length; i++) {
walk(node.childNodes[i]);
}
}
}
if (node.nodeType === 1) {
var tg = node.tagName.toUpperCase();
if (tg==='P' || tg==='DIV' || tg==='BR' || tg==='LI' || tg==='PRE') textParts.push('\\n');
}
}
walk(botTurn);
res = textParts.join(' ').replace(/ \\n /g, '\\n').replace(/\\n+/g, '\\n').trim();
return res.substring(0, 3500);
}
function extractChatContext(b) {
try {
var botTurn = b.closest('.bg-agent-convo-background') || b.closest('.text-ide-message-block-bot-color');
var botTurn = b.closest('.text-ide-message-block-bot-color') || b.closest('.bg-agent-convo-background');
if (!botTurn) {
var container = findButtonContainer(b);
var container = b.closest('.p-1') || b.parentElement;
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);
return extractChatContextFromNode(botTurn);
} catch(e) {
return '';
}
}
function extractChatContextFromNode(botTurn) {
if (!botTurn) return '';
var toolContainer = botTurn.querySelector('.bg-ide-background-color'); // Stop at tool blocks
var textParts = [];
function walk(node) {
if (toolContainer && node === toolContainer) return true;
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, 3500);
}
function extractContext(b) {
var cmd = extractCommandContext(b);
var chat = extractChatContext(b);
@@ -166,15 +130,21 @@ export function generateApprovalObserverScript(_port: number): string {
return combined.trim();
}
// ── Action Buttons Patterns (EN / KO) ──
var PATS = [
{ type: 'command', re: /^(?:Always\\s*)?(?:Run\\b|결행사양\\s*항상|결행)/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];
var ACTION_WORDS = ['Allow', 'Run', 'Approve', 'Accept', '결행', '수락', '반영', '허용', '승인'];
var REJECT_WORDS = ['Reject', 'Cancel', 'Deny', 'Stop', 'Decline', 'Dismiss', '거절', '거부', '취소', '차단', '정지', '무시'];
function isActionBtn(txt) {
for(var i=0; i<ACTION_WORDS.length; i++) {
if(txt.indexOf(ACTION_WORDS[i]) !== -1) return true;
}
return false;
}
function isRejectBtn(txt) {
for(var i=0; i<REJECT_WORDS.length; i++) {
if(txt.indexOf(REJECT_WORDS[i]) !== -1) return true;
}
return false;
}
function collectSiblingButtons(container,triggerBtn){
if(!container)return [];
@@ -183,110 +153,86 @@ export function generateApprovalObserverScript(_port: number): string {
for(var i=0;i<siblings.length;i++){
var sb=siblings[i];
if(sb.disabled||sb.hidden||(!sb.offsetParent&&sb.style.display!=='fixed'))continue;
var stxt = cleanButtonText(sb);
if(stxt.length <= 1) continue; // 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;}
}
if(!isAction)continue;
if(stxt.length <= 1) continue;
if(!isActionBtn(stxt) && !isRejectBtn(stxt)) continue;
result.push({btn:sb,text:stxt,isPrimary:(sb===triggerBtn)});
}
return result;
}
var HARDCODED_PORT=${_port};
function tryPingAsync(port){
return fetch('http://127.0.0.1:'+port+'/ping?t='+Date.now(),{signal:AbortSignal.timeout(2000)})
.then(function(r){return r.text();})
.then(function(t){return t==='pong';})
.catch(function(){return false;});
.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...');
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+)/);
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);
});
tryPingAsync(parseInt(m[1], 10)).then(function(ok){ cb(ok ? parseInt(m[1],10) : 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!
tryPingAsync(HARDCODED_PORT).then(function(){ cb(HARDCODED_PORT); });
}
},500); // Wait 500ms * 2 = 1 second total
},500);
}
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){});
_ready=true;
startObserver();
});
var _chatSnapshots = [];
var _firstChatScan = true;
var _lastText = "";
var _lastTextTime = 0;
var _lastTextSent = false;
function scanChatBodies() {
if(!_ready)return;
var botTurns = document.querySelectorAll('.text-ide-message-block-bot-color');
for (var i = 0; i < botTurns.length; i++) {
var turn = botTurns[i];
if (turn.dataset.agChatScraped === "true" || turn.dataset.agChatScraped === "pending") continue;
if (_firstChatScan) {
turn.dataset.agChatScraped = "true";
continue;
}
var currentText = turn.textContent || '';
var found = -1;
for (var j = 0; j < _chatSnapshots.length; j++) {
if (_chatSnapshots[j].node === turn) { found = j; break; }
}
if (found === -1) {
_chatSnapshots.push({ node: turn, text: currentText, lastChanged: Date.now() });
} else {
if (_chatSnapshots[found].text !== currentText) {
_chatSnapshots[found].text = currentText;
_chatSnapshots[found].lastChanged = Date.now();
if (botTurns.length === 0) return;
var lastTurn = botTurns[botTurns.length - 1];
if (lastTurn.dataset.agChatScraped === "true" || lastTurn.dataset.agChatScraped === "pending") return;
var currentText = lastTurn.textContent || '';
if (currentText.length < 5) return;
if (_lastText !== currentText) {
_lastText = currentText;
_lastTextTime = Date.now();
_lastTextSent = false;
} else if (!_lastTextSent) {
if (Date.now() - _lastTextTime > 3000) {
_lastTextSent = true;
lastTurn.dataset.agChatScraped = "pending";
var finalTxt = extractChatContextFromNode(lastTurn);
if (finalTxt && finalTxt.length > 5 && finalTxt !== "Review Changes") {
fetch(BASE+'/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: finalTxt })
}).then(function(){
lastTurn.dataset.agChatScraped = "true";
}).catch(function(){
lastTurn.dataset.agChatScraped = "false";
});
} else {
if (Date.now() - _chatSnapshots[found].lastChanged > 3500) {
turn.dataset.agChatScraped = "pending"; // prevent re-entry
var finalTxt = extractChatContextFromNode(turn);
if (finalTxt && finalTxt.length > 5) {
fetch(BASE+'/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: finalTxt })
}).then(function(){
turn.dataset.agChatScraped = "true";
}).catch(function(){
turn.dataset.agChatScraped = "false"; // retry
});
} else {
turn.dataset.agChatScraped = "true";
}
}
lastTurn.dataset.agChatScraped = "true";
}
}
}
_firstChatScan = false;
}
function scan(){
@@ -301,26 +247,17 @@ export function generateApprovalObserverScript(_port: number): string {
if(b.disabled||b.hidden||(!b.offsetParent&&b.style.display!=='fixed'))continue;
var txt=cleanButtonText(b);
console.log("[JSDOM] Button scan:", txt);
if(txt.length <= 1) continue; // Icon
if(txt.length <= 1) continue;
var matchedType=null;
for(var p=0;p<PATS.length;p++){
if(PATS[p].re.test(txt)){
if (b.closest('.codelens-decoration') && PATS[p].type !== 'diff_review' && PATS[p].type !== 'permission') {
continue;
}
matchedType=PATS[p].type;
break;
}
if(!isActionBtn(txt)) continue;
// Skip inline code lens buttons unless they actually match the pattern properly
if (b.closest('.codelens-decoration') && !txt.includes('Accept') && !txt.includes('수락') && !txt.includes('반영')) {
continue;
}
if(!matchedType){
console.log("[JSDOM] NOT MATCHED:", txt);
continue;
}
var container=findButtonContainer(b);
var matchedType = txt.includes('Accept') ? 'diff_review' : (txt.includes('Run') ? 'command' : 'permission');
var container=b.closest('.p-1') || b.parentElement.parentElement;
var groupKey=matchedType+'|'+btnId(b,matchedType);
console.log("[JSDOM] MATCHED:", matchedType, "KEY:", groupKey, "SENT:", !!_sent[groupKey]);
if(_sent[groupKey])continue;
var siblings=collectSiblingButtons(container,b);
@@ -338,7 +275,6 @@ export function generateApprovalObserverScript(_port: number): string {
}
var desc=extractContext(b);
var is_dom_dummy = false;
if (!desc || desc.trim().length <= 2) {
desc = "MISSING_DESCRIPTION_FROM_DOM_FALLBACK_TO_STEP_PROBE";
@@ -417,17 +353,15 @@ export function generateApprovalObserverScript(_port: number): string {
}
function clickRejectButton(approveBtn){
var container=findButtonContainer(approveBtn);
var container=approveBtn.closest('.p-1') || approveBtn.parentElement.parentElement;
if(!container)return;
var siblings=container.querySelectorAll('button');
for(var i=0;i<siblings.length;i++){
var t=cleanButtonText(siblings[i]);
for(var r=0;r<REJECT_RE.length;r++){
if(REJECT_RE[r].test(t)){
log('Clicking reject: '+t);
dispatchReactClick(siblings[i]);
return;
}
if(isRejectBtn(t)){
log('Clicking reject: '+t);
dispatchReactClick(siblings[i]);
return;
}
}
}
@@ -476,22 +410,17 @@ export function generateApprovalObserverScript(_port: number): string {
if(_ready&&BASE){
fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
if(!d.action)return;
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 isApprove = (d.action==='approve');
var btns = document.querySelectorAll('button');
for(var i=0;i<btns.length;i++){
var bx = btns[i];
if(bx.disabled||bx.hidden||(!bx.offsetParent&&bx.style.display!=='fixed'))continue;
var t = cleanButtonText(bx);
if(t.length <= 1) continue;
for(var pi=0;pi<patterns.length;pi++){
if(patterns[pi].test(t)){
log('Fallback TRIGGER-CLICK on "' + t + '"');
dispatchReactClick(bx);
return;
}
if (isApprove ? isActionBtn(t) : isRejectBtn(t)) {
log('Fallback TRIGGER-CLICK on "' + t + '"');
dispatchReactClick(bx);
return;
}
}
}).catch(function(){});