fix(bridge): multi-window isolation v0.3.4
This commit is contained in:
@@ -110,6 +110,8 @@ function ensureBridgeDir() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Module-level activeSessionId so writeChatSnapshot can register sessions lazily
|
||||
let activeSessionId = '';
|
||||
function writeChatSnapshot(text) {
|
||||
try {
|
||||
// Write to chat_snapshots/*.json for Bot's chat_snapshot_scanner to pick up
|
||||
@@ -127,6 +129,10 @@ function writeChatSnapshot(text) {
|
||||
const filePath = path.join(snapshotDir, `${id}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
console.log(`Gravity Bridge: chat snapshot written (${text.length} chars) → ${id}.json`);
|
||||
// Lazily register session → project mapping (correct because projectName is per-window)
|
||||
if (activeSessionId) {
|
||||
writeRegistration(activeSessionId);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`Gravity Bridge: snapshot write error: ${e.message}`);
|
||||
@@ -405,12 +411,21 @@ function startObserverHttpBridge() {
|
||||
const port = server.address().port;
|
||||
observerHttpServer = server;
|
||||
logToFile(`[HTTP] bridge server started on port ${port}`);
|
||||
// Write port to workbench dir so renderer can read it via XHR
|
||||
// Write port to shared ports JSON (multi-bridge support)
|
||||
const patcher = sdk.integration?._patcher;
|
||||
if (patcher && typeof patcher.getWorkbenchDir === 'function') {
|
||||
const portFile = path.join(patcher.getWorkbenchDir(), 'ag-bridge-port');
|
||||
fs.writeFileSync(portFile, port.toString(), 'utf8');
|
||||
logToFile(`[HTTP] port written → ${portFile}`);
|
||||
const workbenchDir = patcher.getWorkbenchDir();
|
||||
const portsFile = path.join(workbenchDir, 'ag-bridge-ports.json');
|
||||
let portsData = {};
|
||||
try {
|
||||
if (fs.existsSync(portsFile)) {
|
||||
portsData = JSON.parse(fs.readFileSync(portsFile, 'utf-8'));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
portsData[projectName] = port;
|
||||
fs.writeFileSync(portsFile, JSON.stringify(portsData), 'utf-8');
|
||||
logToFile(`[HTTP] ports JSON updated → ${portsFile} (${projectName}=${port})`);
|
||||
}
|
||||
resolve(port);
|
||||
});
|
||||
@@ -427,32 +442,49 @@ function startObserverHttpBridge() {
|
||||
}
|
||||
// ─── Renderer Script (uses fetch() — no Node.js APIs) ───
|
||||
function generateApprovalObserverScript(_port) {
|
||||
// Port is NOT hardcoded — renderer reads it dynamically from ag-bridge-port file via XHR
|
||||
// Port is hardcoded as fallback, but renderer also reads ag-bridge-ports.json for multi-bridge
|
||||
return `
|
||||
// ── Gravity Bridge: Approval Observer (renderer-side, dynamic port) ──
|
||||
// ── Gravity Bridge v2: Approval Observer (MutationObserver-first, throttled) ──
|
||||
(function(){
|
||||
'use strict';
|
||||
var BASE='',_lastTs=0,_obs=false,_sent={},_ready=false;
|
||||
var BASE='',_obs=false,_sent={},_ready=false;
|
||||
var _scanScheduled=false,_lastScanTs=0;
|
||||
var THROTTLE_MS=100;
|
||||
var CLEANUP_MS=300000;
|
||||
|
||||
function log(m){console.log('[GB Observer] '+m);}
|
||||
log('Script loaded — discovering bridge port...');
|
||||
log('v2 Script loaded — discovering bridge port...');
|
||||
|
||||
// ── Dynamic Port Discovery (like SDK heartbeat) ──
|
||||
// ── Multi-Port Discovery: reads ag-bridge-ports.json, tries ALL bridges ──
|
||||
function discoverPort(cb){
|
||||
var attempts=0;
|
||||
var timer=setInterval(function(){
|
||||
attempts++;
|
||||
if(attempts>30){clearInterval(timer);log('Port discovery timeout');return;}
|
||||
if(attempts>60){clearInterval(timer);log('Port discovery timeout after 2min');return;}
|
||||
try{
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.open('GET','./ag-bridge-port?t='+Date.now(),false);
|
||||
xhr.open('GET','./ag-bridge-ports.json?t='+Date.now(),false);
|
||||
xhr.send();
|
||||
if(xhr.status===200){
|
||||
var port=parseInt(xhr.responseText.trim(),10);
|
||||
if(port>0&&port<65536){
|
||||
clearInterval(timer);
|
||||
log('Port discovered: '+port);
|
||||
cb(port);
|
||||
var ports=JSON.parse(xhr.responseText);
|
||||
var keys=Object.keys(ports);
|
||||
for(var i=0;i<keys.length;i++){
|
||||
var port=ports[keys[i]];
|
||||
if(port>0&&port<65536){
|
||||
// Try ping on each port
|
||||
try{
|
||||
var xhr2=new XMLHttpRequest();
|
||||
xhr2.open('GET','http://127.0.0.1:'+port+'/ping?t='+Date.now(),false);
|
||||
xhr2.timeout=1000;
|
||||
xhr2.send();
|
||||
if(xhr2.status===200&&xhr2.responseText==='pong'){
|
||||
clearInterval(timer);
|
||||
log('Port discovered: '+port+' (project='+keys[i]+')');
|
||||
cb(port);
|
||||
return;
|
||||
}
|
||||
}catch(e2){}
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(e){}
|
||||
@@ -461,82 +493,254 @@ function generateApprovalObserverScript(_port) {
|
||||
|
||||
discoverPort(function(port){
|
||||
BASE='http://127.0.0.1:'+port;
|
||||
// Verify bridge is alive
|
||||
fetch(BASE+'/ping').then(function(r){return r.text();}).then(function(t){
|
||||
if(t==='pong'){log('Bridge connected on port '+port);_ready=true;startObserver();}
|
||||
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) ──
|
||||
var PATS=[
|
||||
{sel:'button',re:/^Run$/i,type:'terminal_command'},
|
||||
{sel:'button',re:/^Accept/i,type:'agent_step'},
|
||||
{sel:'button',re:/^Allow/i,type:'permission'},
|
||||
{sel:'button',re:/^Continue$/i,type:'continue'},
|
||||
{re:/^Run$/i, type:'terminal_command'},
|
||||
{re:/^Accept/i, type:'agent_step'},
|
||||
{re:/^Allow/i, type:'permission'},
|
||||
{re:/^Approve/i, type:'agent_step'},
|
||||
{re:/^Continue$/i, type:'continue'},
|
||||
{re:/^Proceed$/i, type:'continue'},
|
||||
];
|
||||
|
||||
function ctx(b){
|
||||
var p=b.closest('[class*="step"]')||b.closest('[class*="action"]')||b.parentElement;
|
||||
if(!p)return '';
|
||||
var c=p.querySelector('pre,code,[class*="command"],[class*="terminal"]');
|
||||
if(c)return(c.textContent||'').trim().substring(0,200);
|
||||
return(p.textContent||'').replace((b.textContent||''),'').trim().substring(0,200);
|
||||
// Reject button patterns for finding the counterpart
|
||||
var REJECT_RE=[/^reject$/i,/^cancel$/i,/^deny$/i,/^stop$/i,/^decline$/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');
|
||||
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){
|
||||
// Strategy 1: Look for code/pre/terminal blocks near the button
|
||||
var container=b.closest('[class*="step"]')
|
||||
||b.closest('[class*="action"]')
|
||||
||b.closest('[class*="tool"]')
|
||||
||b.closest('[class*="cascade"]')
|
||||
||b.closest('[class*="message"]');
|
||||
if(!container)container=b.parentElement;
|
||||
if(!container)return '';
|
||||
|
||||
// Look for code blocks
|
||||
var codeEl=container.querySelector('pre,code,[class*="command"],[class*="terminal"],[class*="code-block"]');
|
||||
if(codeEl){
|
||||
var codeText=(codeEl.textContent||'').trim();
|
||||
if(codeText.length>0)return codeText.substring(0,500);
|
||||
}
|
||||
|
||||
// Strategy 2: Get surrounding text (exclude button text itself)
|
||||
var full=(container.textContent||'');
|
||||
var btnText=(b.textContent||'');
|
||||
var desc=full.replace(btnText,'').trim();
|
||||
// Trim to reasonable length
|
||||
return desc.substring(0,500);
|
||||
}
|
||||
|
||||
// ── 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"]',
|
||||
];
|
||||
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 scan(){
|
||||
if(!_ready)return;
|
||||
var now=Date.now();if(now-_lastTs<1000)return;
|
||||
var panel=document.querySelector('#jetski-agent-panel,.antigravity-agent-side-panel,[class*="agent-panel"]');
|
||||
var now=Date.now();
|
||||
|
||||
var panel=findPanel();
|
||||
if(!panel)return;
|
||||
for(var i=0;i<PATS.length;i++){
|
||||
var pat=PATS[i],btns=panel.querySelectorAll(pat.sel);
|
||||
for(var j=0;j<btns.length;j++){
|
||||
var b=btns[j],txt=(b.textContent||'').trim();
|
||||
if(!pat.re.test(txt)||b.disabled)continue;
|
||||
var bid=pat.type+'_'+txt+'_'+Math.round(b.getBoundingClientRect().top);
|
||||
if(_sent[bid])continue;
|
||||
var desc=ctx(b),rid=now.toString();
|
||||
_sent[bid]=rid;_lastTs=now;
|
||||
log('FOUND '+pat.type+': "'+txt+'" → sending to bridge');
|
||||
(function(rid2,b2,bid2){
|
||||
fetch(BASE+'/pending',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({request_id:rid2,command:txt,description:desc,step_type:pat.type})
|
||||
}).then(function(r){return r.json();}).then(function(d){
|
||||
log('Pending created: '+d.request_id);
|
||||
pollResp(d.request_id,b2,bid2);
|
||||
}).catch(function(e){log('POST error: '+e.message);delete _sent[bid2];});
|
||||
})(rid,b,bid);
|
||||
return;
|
||||
|
||||
// Find ALL buttons in the panel
|
||||
var allBtns=panel.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;
|
||||
|
||||
var txt=(b.textContent||'').trim();
|
||||
if(!txt)continue;
|
||||
|
||||
// Match against patterns
|
||||
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;
|
||||
|
||||
// Generate stable ID
|
||||
var bid=btnId(b,matchedType);
|
||||
if(_sent[bid])continue;
|
||||
|
||||
// Extract context
|
||||
var desc=extractContext(b);
|
||||
var rid=now.toString()+'_'+Math.random().toString(36).substring(2,6);
|
||||
|
||||
// Mark as sent
|
||||
_sent[bid]={rid:rid,ts:now};
|
||||
log('DETECTED '+matchedType+': "'+txt+'" → pending to bridge');
|
||||
|
||||
// Send to bridge (closure to capture refs)
|
||||
(function(rid2,b2,bid2,txt2,desc2,type2){
|
||||
fetch(BASE+'/pending',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({
|
||||
request_id:rid2,
|
||||
command:txt2,
|
||||
description:desc2,
|
||||
step_type:type2
|
||||
})
|
||||
}).then(function(r){return r.json();}).then(function(d){
|
||||
log('Pending created: '+d.request_id+' for "'+txt2+'"');
|
||||
pollResponse(d.request_id,b2,bid2);
|
||||
}).catch(function(e){
|
||||
log('POST error: '+e.message);
|
||||
delete _sent[bid2];
|
||||
});
|
||||
})(rid,b,bid,txt,desc,matchedType);
|
||||
|
||||
// Process ONE button per scan cycle (avoid flooding)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function pollResp(rid,b,bid){
|
||||
var n=0,t=setInterval(function(){
|
||||
n++;if(n>600){clearInterval(t);delete _sent[bid];return;}
|
||||
// ── Poll for Discord response ──
|
||||
function pollResponse(rid,btn,bid){
|
||||
var polls=0;
|
||||
var maxPolls=600; // 5 minutes at 500ms interval
|
||||
var timer=setInterval(function(){
|
||||
polls++;
|
||||
// Check if button is still in DOM (step may have been resolved by other means)
|
||||
if(!document.body.contains(btn)){
|
||||
log('Button removed from DOM — stopping poll for '+rid);
|
||||
clearInterval(timer);
|
||||
delete _sent[bid];
|
||||
return;
|
||||
}
|
||||
if(polls>maxPolls){
|
||||
log('Poll timeout for '+rid);
|
||||
clearInterval(timer);
|
||||
delete _sent[bid];
|
||||
return;
|
||||
}
|
||||
fetch(BASE+'/response/'+rid).then(function(r){return r.json();}).then(function(d){
|
||||
if(d.waiting)return;
|
||||
clearInterval(t);
|
||||
if(d.approved){log('APPROVED '+rid+' → clicking');b.click();}
|
||||
else{
|
||||
log('REJECTED '+rid);
|
||||
var p=b.closest('[class*="step"]')||b.parentElement;
|
||||
if(p){var rb=p.querySelectorAll('button');
|
||||
for(var k=0;k<rb.length;k++){var rt=(rb[k].textContent||'').trim().toLowerCase();
|
||||
if(rt==='reject'||rt==='cancel'||rt==='deny'){rb[k].click();break;}}}
|
||||
clearInterval(timer);
|
||||
if(d.approved){
|
||||
log('✅ APPROVED '+rid+' → clicking "'+((btn.textContent||'').trim())+'"');
|
||||
btn.click();
|
||||
} else {
|
||||
log('❌ REJECTED '+rid+' → finding reject button');
|
||||
clickRejectButton(btn);
|
||||
}
|
||||
delete _sent[bid];
|
||||
}).catch(function(){});
|
||||
},500);
|
||||
}
|
||||
|
||||
// ── 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');
|
||||
for(var i=0;i<siblings.length;i++){
|
||||
var t=(siblings[i].textContent||'').trim();
|
||||
for(var r=0;r<REJECT_RE.length;r++){
|
||||
if(REJECT_RE[r].test(t)){
|
||||
log('Clicking reject: "'+t+'"');
|
||||
siblings[i].click();
|
||||
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();
|
||||
if(now-_lastScanTs>=THROTTLE_MS){
|
||||
_lastScanTs=now;
|
||||
scan();
|
||||
} else if(!_scanScheduled){
|
||||
_scanScheduled=true;
|
||||
setTimeout(function(){
|
||||
_scanScheduled=false;
|
||||
_lastScanTs=Date.now();
|
||||
scan();
|
||||
},THROTTLE_MS-(now-_lastScanTs));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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;
|
||||
new MutationObserver(function(){scan();}).observe(document.body,{childList:true,subtree:true});
|
||||
setInterval(scan,2000);
|
||||
_obs=true;log('Observer active — watching for approval buttons');
|
||||
// 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();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}).observe(document.body,{childList:true,subtree:true});
|
||||
|
||||
// FALLBACK: periodic scan every 3s for any missed mutations
|
||||
setInterval(scheduleScan,3000);
|
||||
|
||||
_obs=true;
|
||||
log('v2 Observer active — MutationObserver + 3s fallback');
|
||||
}
|
||||
})();
|
||||
`;
|
||||
@@ -550,21 +754,19 @@ const registeredSessions = new Set(); // track which sessions have been register
|
||||
* Called automatically on first step event per session.
|
||||
*/
|
||||
function writeRegistration(sessionId) {
|
||||
if (registeredSessions.has(sessionId)) {
|
||||
return;
|
||||
}
|
||||
registeredSessions.add(sessionId);
|
||||
try {
|
||||
const regDir = path.join(bridgePath, 'register');
|
||||
if (!fs.existsSync(regDir)) {
|
||||
fs.mkdirSync(regDir, { recursive: true });
|
||||
}
|
||||
const regFile = path.join(regDir, `${sessionId}.json`);
|
||||
// Always overwrite — the window that actively writes snapshots/approvals is the correct owner
|
||||
const data = {
|
||||
conversation_id: sessionId,
|
||||
project_name: projectName,
|
||||
timestamp: Date.now() / 1000,
|
||||
};
|
||||
fs.writeFileSync(path.join(regDir, `${sessionId}.json`), JSON.stringify(data, null, 2), 'utf-8');
|
||||
fs.writeFileSync(regFile, JSON.stringify(data, null, 2), 'utf-8');
|
||||
console.log(`Gravity Bridge: registered session ${sessionId.substring(0, 8)} → ${projectName}`);
|
||||
}
|
||||
catch (e) {
|
||||
@@ -593,7 +795,7 @@ function setupMonitor() {
|
||||
// stepIndex on each → perfect for dedup
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
let pollCount = 0;
|
||||
let activeSessionId = '';
|
||||
// activeSessionId is module-level (for writeChatSnapshot lazy registration)
|
||||
let activeSessionTitle = '';
|
||||
let lastKnownStepCount = 0;
|
||||
let lastNotifyStepIndex = -1;
|
||||
@@ -615,10 +817,26 @@ function setupMonitor() {
|
||||
logToFile('[POLL] no trajectorySummaries');
|
||||
return;
|
||||
}
|
||||
// ── Filter to sessions owned by THIS window ──
|
||||
// Each window claims sessions it sees first via writeRegistration().
|
||||
// Only process sessions registered to THIS projectName (or unclaimed ones).
|
||||
let bestSession = null;
|
||||
let bestSessionId = '';
|
||||
let bestModTime = '';
|
||||
const regDir = path.join(bridgePath, 'register');
|
||||
for (const [sid, data] of Object.entries(allTraj.trajectorySummaries)) {
|
||||
// Check if this session is claimed by another project
|
||||
const regFile = path.join(regDir, `${sid}.json`);
|
||||
if (fs.existsSync(regFile)) {
|
||||
try {
|
||||
const reg = JSON.parse(fs.readFileSync(regFile, 'utf-8'));
|
||||
if (reg.project_name && reg.project_name !== projectName) {
|
||||
// Session belongs to another window — skip
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
const modTime = data.lastModifiedTime || '';
|
||||
if (!bestSession || modTime > bestModTime) {
|
||||
bestSession = data;
|
||||
@@ -639,7 +857,8 @@ function setupMonitor() {
|
||||
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
|
||||
lastPendingStepIndex = -1;
|
||||
writeRegistration(activeSessionId);
|
||||
// Don't register here — registration happens lazily in writeChatSnapshot/writePendingApproval
|
||||
// to avoid race conditions between multiple extension instances
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
|
||||
return;
|
||||
}
|
||||
@@ -756,6 +975,19 @@ function setupResponseWatcher() {
|
||||
if (filename && filename.endsWith('.json') && event === 'rename') {
|
||||
const fp = path.join(responseDir, filename);
|
||||
if (fs.existsSync(fp)) {
|
||||
// Check if this response belongs to our project
|
||||
const rid = filename.replace('.json', '');
|
||||
const pendingFile = path.join(bridgePath, 'pending', `${rid}.json`);
|
||||
if (fs.existsSync(pendingFile)) {
|
||||
try {
|
||||
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
||||
if (pending.project_name && pending.project_name !== projectName) {
|
||||
logToFile(`[RESPONSE] skip ${rid} (project=${pending.project_name}, we=${projectName})`);
|
||||
return; // Not our project
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
setTimeout(() => processResponseFile(fp), 300);
|
||||
}
|
||||
}
|
||||
@@ -773,90 +1005,82 @@ async function processResponseFile(filePath) {
|
||||
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved}`;
|
||||
console.log(`Gravity Bridge: ${msg}`);
|
||||
logToFile(msg);
|
||||
// Find matching pending request for session_id
|
||||
// Find matching pending request
|
||||
const pendingDir = path.join(bridgePath, 'pending');
|
||||
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
|
||||
let sessionId = '';
|
||||
let isDomObserver = false;
|
||||
if (fs.existsSync(pendingFile)) {
|
||||
try {
|
||||
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
||||
sessionId = pending.conversation_id || '';
|
||||
isDomObserver = pending.auto_detected === true && pending.source === 'dom_observer';
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
// ═══ APPROVAL STRATEGY (VS Code Commands Only) ═══
|
||||
// Phase 0 ResolveOutstandingSteps: REMOVED — confirmed it CANCELS steps!
|
||||
// Phase 1 HandleCascadeUserInteraction: REMOVED — always gets "socket hang up"
|
||||
// Phase 2: ALL VS Code commands sequentially (no break on "success")
|
||||
// ═══ APPROVAL STRATEGY ═══
|
||||
// DOM observer approvals: renderer handles clicking via pollResponse — skip VS Code commands
|
||||
// Stall-detection approvals: use VS Code commands as fallback (focus-dependent)
|
||||
const approved = resp.approved;
|
||||
// Focus panel with multiple attempts + longer delay
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
|
||||
if (i === 0)
|
||||
logToFile('[RESPONSE] panel focus attempt 1');
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[RESPONSE] panel focus attempt ${i + 1} failed: ${e.message}`);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
// Phase 2: Sequential VS Code commands (MUST try ALL — no break!)
|
||||
// Focus panel first
|
||||
try {
|
||||
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
|
||||
logToFile('[RESPONSE] panel focused');
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[RESPONSE] panel focus failed: ${e.message}`);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
if (approved) {
|
||||
const approveCommands = [
|
||||
'antigravity.interactiveCascade.acceptSuggestedAction',
|
||||
'antigravity.terminalCommand.run',
|
||||
'antigravity.terminalCommand.accept',
|
||||
'antigravity.command.accept',
|
||||
'antigravity.agent.acceptAgentStep',
|
||||
];
|
||||
for (const cmd of approveCommands) {
|
||||
try {
|
||||
await vscode.commands.executeCommand(cmd);
|
||||
logToFile(`[RESPONSE] cmd OK: ${cmd}`);
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[RESPONSE] cmd FAIL: ${cmd} → ${e.message}`);
|
||||
}
|
||||
}
|
||||
if (isDomObserver) {
|
||||
// DOM observer path: renderer polls /response/:rid and clicks the button directly
|
||||
logToFile(`[RESPONSE] DOM observer approval — renderer will handle click (rid=${resp.request_id})`);
|
||||
}
|
||||
else {
|
||||
const rejectCommands = [
|
||||
'antigravity.interactiveCascade.rejectSuggestedAction',
|
||||
'antigravity.terminalCommand.reject',
|
||||
'antigravity.command.reject',
|
||||
'antigravity.agent.rejectAgentStep',
|
||||
];
|
||||
for (const cmd of rejectCommands) {
|
||||
// Stall-detection path: use VS Code commands (legacy, focus-dependent)
|
||||
logToFile(`[RESPONSE] Stall-detection approval — using VS Code commands`);
|
||||
// Focus panel
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await vscode.commands.executeCommand(cmd);
|
||||
logToFile(`[RESPONSE] cmd OK: ${cmd}`);
|
||||
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
|
||||
if (i === 0)
|
||||
logToFile('[RESPONSE] panel focus attempt 1');
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[RESPONSE] cmd FAIL: ${cmd} → ${e.message}`);
|
||||
logToFile(`[RESPONSE] panel focus attempt ${i + 1} failed: ${e.message}`);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
if (approved) {
|
||||
const approveCommands = [
|
||||
'antigravity.terminalCommand.run',
|
||||
'antigravity.terminalCommand.accept',
|
||||
'antigravity.command.accept',
|
||||
'antigravity.agent.acceptAgentStep',
|
||||
];
|
||||
for (const cmd of approveCommands) {
|
||||
try {
|
||||
await vscode.commands.executeCommand(cmd);
|
||||
logToFile(`[RESPONSE] cmd OK: ${cmd}`);
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[RESPONSE] cmd FAIL: ${cmd} → ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const rejectCommands = [
|
||||
'antigravity.terminalCommand.reject',
|
||||
'antigravity.command.reject',
|
||||
'antigravity.agent.rejectAgentStep',
|
||||
];
|
||||
for (const cmd of rejectCommands) {
|
||||
try {
|
||||
await vscode.commands.executeCommand(cmd);
|
||||
logToFile(`[RESPONSE] cmd OK: ${cmd}`);
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[RESPONSE] cmd FAIL: ${cmd} → ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done`);
|
||||
// Cleanup
|
||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'stall'})`);
|
||||
// Cleanup response file (but NOT pending — renderer still polls it)
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
catch { }
|
||||
try {
|
||||
if (fs.existsSync(pendingFile))
|
||||
fs.unlinkSync(pendingFile);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
catch (e) {
|
||||
const log = `[RESPONSE] error: ${e.message}`;
|
||||
@@ -962,6 +1186,10 @@ function writePendingApproval(data) {
|
||||
};
|
||||
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
|
||||
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
|
||||
// Register session → project mapping (correct because projectName is per-window)
|
||||
if (data.conversation_id) {
|
||||
writeRegistration(data.conversation_id);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`Gravity Bridge: pending write error: ${e.message}`);
|
||||
|
||||
Reference in New Issue
Block a user