"use strict"; /** * Gravity Bridge — VS Code Extension (SDK Edition) * * Uses antigravity-sdk for: * - Real-time step/conversation monitoring via EventMonitor * - Full conversation content via LSBridge.getConversation() * - Message sending via CascadeManager.sendPrompt() * - Accept/Reject via CascadeManager.acceptStep()/rejectStep() * * Communication with Discord via file-based bridge protocol. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.activate = activate; exports.deactivate = deactivate; const vscode = __importStar(require("vscode")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const os = __importStar(require("os")); const cp = __importStar(require("child_process")); // ─── File-based logging (AI can read directly) ─── function logToFile(msg) { const ts = new Date().toISOString().replace('T', ' ').substring(0, 19); const line = `${ts} ${msg}`; console.log(`Gravity Bridge: ${msg}`); try { if (!bridgePath) return; const logFile = path.join(bridgePath, 'extension.log'); fs.appendFileSync(logFile, line + '\n', 'utf-8'); } catch (e) { console.error(`Gravity Bridge LOG WRITE FAIL: ${e.message}`); } } // antigravity-sdk embedded locally (src/sdk/) let AntigravitySDK; let sdk; let statusBar; let bridgePath; let projectName; let isActive = false; let watcher = null; let commandsWatcher = null; const sentPendingIds = new Set(); // ─── Project Detection ─── function detectProjectName() { const config = vscode.workspace.getConfiguration('gravityBridge'); const configName = config.get('projectName'); if (configName) { return configName; } const folders = vscode.workspace.workspaceFolders; if (folders && folders.length > 0) { const cwd = folders[0].uri.fsPath; try { const remoteUrl = cp.execSync('git remote get-url origin', { cwd, encoding: 'utf-8', timeout: 3000 }).trim(); const match = remoteUrl.match(/\/([^\/]+?)(?:\.git)?$/); if (match && match[1]) { return match[1].toLowerCase().replace(/[\s\-]+/g, '_'); } } catch { } return path.basename(cwd).toLowerCase().replace(/[\s\-]+/g, '_'); } return 'default'; } // ─── Bridge File I/O ─── function ensureBridgeDir() { const dirs = ['', 'response', 'commands', 'chat_snapshots']; for (const d of dirs) { const p = path.join(bridgePath, d); if (!fs.existsSync(p)) { fs.mkdirSync(p, { recursive: true }); } } } // 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 const snapshotDir = path.join(bridgePath, 'chat_snapshots'); if (!fs.existsSync(snapshotDir)) { fs.mkdirSync(snapshotDir, { recursive: true }); } const id = Date.now().toString(); const data = { id, project_name: projectName, content: text, timestamp: Date.now() / 1000, }; 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}`); } } // ─── Command File Watcher (Discord → Antigravity) ─── function processCommandFile(filePath) { try { const content = fs.readFileSync(filePath, 'utf-8'); const cmd = JSON.parse(content); // Skip already consumed commands if (cmd.consumed) { try { fs.unlinkSync(filePath); } catch { } return; } // Ignore commands for other projects if (cmd.project_name && cmd.project_name !== projectName) { console.log(`Gravity Bridge: skipping command for "${cmd.project_name}" (we are "${projectName}")`); return; } // Bot writes 'text' field, not 'message' const text = cmd.text || cmd.message || ''; const action = cmd.action || ''; console.log(`Gravity Bridge: command — text="${text}" action="${action}"`); if (action === 'approve' && sdk) { sdk.cascade.acceptStep().catch((e) => console.log(`Gravity Bridge: approve error: ${e.message}`)); } else if (action === 'reject' && sdk) { sdk.cascade.rejectStep().catch((e) => console.log(`Gravity Bridge: reject error: ${e.message}`)); } else if (action === 'approve_terminal' && sdk) { sdk.cascade.acceptTerminalCommand().catch((e) => console.log(`Gravity Bridge: approve_terminal error: ${e.message}`)); } else if (text === '!stop') { // Cancel current operation vscode.commands.executeCommand('antigravity.agent.rejectAgentStep') .then(() => console.log('Gravity Bridge: ✅ stop sent'), () => { }); } else if (text.startsWith('!auto ')) { // Auto-approve mode toggle const mode = text.includes('on') ? 'true' : 'false'; console.log(`Gravity Bridge: auto-approve → ${mode}`); } else if (text) { // Send message to Antigravity — use VS Code command (most reliable) vscode.commands.executeCommand('antigravity.sendPromptToAgentPanel', text) .then(() => console.log(`Gravity Bridge: ✅ sent "${text.substring(0, 50)}" via sendPromptToAgentPanel`), (e) => console.log(`Gravity Bridge: sendPrompt failed: ${e.message}`)); } // Remove processed command file try { fs.unlinkSync(filePath); } catch { } } catch (e) { console.log(`Gravity Bridge: command processing error: ${e.message}`); } } function watchCommandsDir() { const cmdDir = path.join(bridgePath, 'commands'); // Process existing files try { for (const f of fs.readdirSync(cmdDir)) { if (f.endsWith('.json')) { processCommandFile(path.join(cmdDir, f)); } } } catch { } // Watch for new files try { commandsWatcher = fs.watch(cmdDir, (event, filename) => { if (filename && filename.endsWith('.json') && event === 'rename') { const fp = path.join(cmdDir, filename); if (fs.existsSync(fp)) { setTimeout(() => processCommandFile(fp), 200); } } }); } catch { } } // ─── SDK Integration ─── async function initSDK(context) { try { const sdkModule = require('./sdk/index'); AntigravitySDK = sdkModule.AntigravitySDK; } catch (err) { console.log(`Gravity Bridge: antigravity-sdk load failed: ${err.message}`); return false; } try { sdk = new AntigravitySDK(context); await sdk.initialize(); console.log('Gravity Bridge: ✅ SDK initialized'); return true; } catch (err) { console.log(`Gravity Bridge: SDK init failed: ${err.message}`); return false; } } // ─── Approval Observer via SDK IntegrationManager ─── async function setupApprovalObserver() { if (!sdk) { logToFile('[OBSERVER] no SDK'); return; } try { const integration = sdk.integration; if (!integration) { logToFile('[OBSERVER] sdk.integration unavailable'); return; } // 1. Start HTTP bridge server in Extension Host const bridgePort = await startObserverHttpBridge(); if (!bridgePort) { logToFile('[OBSERVER] HTTP bridge failed'); return; } // 2. Register a TOP_BAR button so build() works try { integration.register({ id: 'gravity_bridge_status', point: 'topBar', icon: '🌉', tooltip: 'Gravity Bridge Active', }); } catch { /* already registered */ } // 3. Write renderer script with HTTP fetch() approach const observerJS = generateApprovalObserverScript(bridgePort); const patcher = integration._patcher; if (patcher && typeof patcher.getScriptPath === 'function') { let baseScript = ''; try { baseScript = integration.build(); } catch { baseScript = ''; } const combinedScript = baseScript + '\n' + observerJS; const scriptPath = patcher.getScriptPath(); fs.writeFileSync(scriptPath, combinedScript, 'utf8'); logToFile(`[OBSERVER] script written → ${scriptPath} (port=${bridgePort})`); if (!integration.isInstalled()) { patcher.install(combinedScript); logToFile('[OBSERVER] workbench.html patched (needs reload)'); } // Also patch workbench-jetski-agent.html (Antigravity's actual entry point!) const scriptDir = path.dirname(scriptPath); const jetskiHtml = path.join(scriptDir, 'workbench-jetski-agent.html'); const scriptBasename = path.basename(scriptPath); try { if (fs.existsSync(jetskiHtml)) { let html = fs.readFileSync(jetskiHtml, 'utf8'); if (!html.includes(scriptBasename)) { html = html.replace('', `\n\n\n\n`); fs.writeFileSync(jetskiHtml, html, 'utf8'); logToFile('[OBSERVER] workbench-jetski-agent.html PATCHED'); } else { logToFile('[OBSERVER] workbench-jetski-agent.html already has script tag'); } } } catch (e) { logToFile(`[OBSERVER] jetski patch error: ${e.message}`); } } try { integration.enableAutoRepair(); } catch { } setInterval(() => { try { integration.signalActive(); } catch { } }, 30_000); logToFile(`[OBSERVER] setup complete (HTTP bridge on port ${bridgePort})`); console.log(`Gravity Bridge: ✅ Approval observer installed (port ${bridgePort})`); } catch (err) { logToFile(`[OBSERVER] setup error: ${err.message}`); } } // ─── HTTP Bridge Server (Extension Host → Renderer communication) ─── let observerHttpServer = null; const pendingResponses = new Map(); function startObserverHttpBridge() { return new Promise((resolve) => { try { const http = require('http'); const server = http.createServer((req, res) => { // CORS headers for renderer fetch() res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } const url = new URL(req.url, `http://127.0.0.1`); // POST /pending — renderer reports a detected approval button if (req.method === 'POST' && url.pathname === '/pending') { let body = ''; req.on('data', (c) => body += c); req.on('end', () => { try { const data = JSON.parse(body); const rid = data.request_id || Date.now().toString(); // Write pending file for Discord bot const pendingDir = path.join(bridgePath, 'pending'); if (!fs.existsSync(pendingDir)) fs.mkdirSync(pendingDir, { recursive: true }); const pending = { ...data, request_id: rid, timestamp: Date.now() / 1000, status: 'pending', project_name: projectName, auto_detected: true, source: 'dom_observer', }; fs.writeFileSync(path.join(pendingDir, `${rid}.json`), JSON.stringify(pending, null, 2)); logToFile(`[HTTP] pending created: ${rid} cmd="${data.command}" ctx="${(data.description || '').substring(0, 50)}"`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, request_id: rid })); } catch (e) { logToFile(`[HTTP] pending error: ${e.message}`); res.writeHead(400); res.end(JSON.stringify({ error: e.message })); } }); return; } // GET /response/:rid — renderer polls for Discord approval if (req.method === 'GET' && url.pathname.startsWith('/response/')) { const rid = url.pathname.split('/')[2]; const respFile = path.join(bridgePath, 'response', `${rid}.json`); if (fs.existsSync(respFile)) { try { const data = JSON.parse(fs.readFileSync(respFile, 'utf8')); fs.unlinkSync(respFile); logToFile(`[HTTP] response sent: ${rid} approved=${data.approved}`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data)); } catch { res.writeHead(200); res.end(JSON.stringify({ waiting: true })); } } else { res.writeHead(200); res.end(JSON.stringify({ waiting: true })); } return; } // GET /ping — health check if (url.pathname === '/ping') { res.writeHead(200); res.end('pong'); return; } res.writeHead(404); res.end('not found'); }); // Listen on random port server.listen(0, '127.0.0.1', () => { const port = server.address().port; observerHttpServer = server; logToFile(`[HTTP] bridge server started on port ${port}`); // Write port to shared ports JSON (multi-bridge support) const patcher = sdk.integration?._patcher; if (patcher && typeof patcher.getWorkbenchDir === 'function') { 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); }); server.on('error', (e) => { logToFile(`[HTTP] server error: ${e.message}`); resolve(0); }); } catch (e) { logToFile(`[HTTP] server failed: ${e.message}`); resolve(0); } }); } // ─── Renderer Script (uses fetch() — no Node.js APIs) ─── function generateApprovalObserverScript(_port) { // Port is hardcoded as fallback, but renderer also reads ag-bridge-ports.json for multi-bridge return ` // ── Gravity Bridge v2: Approval Observer (MutationObserver-first, throttled) ── (function(){ 'use strict'; 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('v2 Script loaded — discovering bridge port...'); // ── Multi-Port Discovery: reads ag-bridge-ports.json, tries ALL bridges ── function discoverPort(cb){ var attempts=0; var timer=setInterval(function(){ attempts++; if(attempts>60){clearInterval(timer);log('Port discovery timeout after 2min');return;} try{ var xhr=new XMLHttpRequest(); xhr.open('GET','./ag-bridge-ports.json?t='+Date.now(),false); xhr.send(); if(xhr.status===200){ var ports=JSON.parse(xhr.responseText); var keys=Object.keys(ports); for(var i=0;i0&&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){} },2000); } discoverPort(function(port){ BASE='http://127.0.0.1:'+port; fetch(BASE+'/ping').then(function(r){return r.text();}).then(function(t){ if(t==='pong'){log('Bridge connected on port '+port);_ready=true;startObserver();} 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=[ {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'}, ]; // 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;i0)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;imaxPolls){ 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(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=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;iCLEANUP_MS){ log('Cleanup stale entry: '+keys[i]); delete _sent[keys[i]]; } } },60000); // ── Start observation ── function startObserver(){ if(_obs)return; // PRIMARY: MutationObserver — reacts instantly to DOM changes new MutationObserver(function(mutations){ // Only scan if mutations contain added nodes (new buttons potentially) for(var i=0;i0){ 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'); } })(); `; } // Track last seen step per session to avoid re-fetching const lastSeenStep = new Map(); const lastSnapshotText = new Map(); const registeredSessions = new Set(); // track which sessions have been registered /** * Write a registration file for the Bot to discover session → project mapping. * Called automatically on first step event per session. */ function writeRegistration(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(regFile, JSON.stringify(data, null, 2), 'utf-8'); console.log(`Gravity Bridge: registered session ${sessionId.substring(0, 8)} → ${projectName}`); } catch (e) { console.log(`Gravity Bridge: registration write error: ${e.message}`); } } function setupMonitor() { if (!sdk) { return; } // NOTE: SDK EventMonitor DISABLED to prevent ERR_CONNECTION_REFUSED spam. // Root cause: EventMonitor polls GetCascadeTrajectorySteps every 2s via rawRPC, // which has a 775-step hard limit and generates connection errors. // ALL relay is now handled by the GetAllCascadeTrajectories POLL below. console.log('Gravity Bridge: SDK monitor DISABLED (using GetAllCascadeTrajectories POLL instead)'); // ══════════════════════════════════════════════════════════════════════ // PRIMARY RELAY: GetAllCascadeTrajectories (THE CORRECT API!) // // PROVEN VIA DIRECT RPC TESTING: // - GetCascadeTrajectorySteps: 775-step hard limit, startStepIndex IGNORED // - getDiagnostics.lastStepIndex: stale (can lag behind) // - GetAllCascadeTrajectories: // stepCount: REAL-TIME (verified 1413→1429 live) // latestNotifyUserStep: contains FULL notificationContent // latestTaskBoundaryStep: contains FULL taskName/Status/Summary // stepIndex on each → perfect for dedup // ══════════════════════════════════════════════════════════════════════ let pollCount = 0; // activeSessionId is module-level (for writeChatSnapshot lazy registration) let activeSessionTitle = ''; let lastKnownStepCount = 0; let lastNotifyStepIndex = -1; let lastTaskStepIndex = -1; let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step let consecutiveIdleCount = 0; // debounce: require N consecutive stall polls let lastPendingTime = 0; // cooldown: minimum gap between pendings let sawRunningAfterPending = true; // gate: must see delta>0 before next pending let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval setInterval(async () => { pollCount++; if (pollCount <= 3 || pollCount % 12 === 0) { logToFile(`[POLL#${pollCount}] alive`); } try { const allTraj = await sdk.ls.rawRPC('GetAllCascadeTrajectories', {}); if (!allTraj?.trajectorySummaries) { if (pollCount <= 3) 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; bestSessionId = sid; bestModTime = modTime; } } if (!bestSession) return; const currentCount = bestSession.stepCount || 0; const currentTitle = (bestSession.summary || 'Untitled').substring(0, 50); const isRunning = String(bestSession.status || '').includes('RUNNING'); // Session changed? if (bestSessionId !== activeSessionId) { activeSessionId = bestSessionId; activeSessionTitle = currentTitle; lastKnownStepCount = currentCount; lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1; lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1; lastPendingStepIndex = -1; // 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; } const delta = currentCount - lastKnownStepCount; lastKnownStepCount = currentCount; if (delta > 0) { console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`); } // Log session state on EVERY poll for diagnostics const statusStr = String(bestSession.status || 'UNKNOWN'); if (pollCount <= 10 || pollCount % 6 === 0 || delta > 0) { logToFile(`[POLL#${pollCount}] status=${statusStr} steps=${currentCount} delta=${delta}`); } // ── Stall-based approval detection ── // INSIGHT: Both thinking and approval show RUNNING+delta=0. // DIFFERENTIATOR: lastModifiedTime // - Thinking: lastModifiedTime KEEPS CHANGING (server actively processing) // - Approval wait: lastModifiedTime FROZEN (server idle, waiting for user) // DEBUG: dump session keys on first poll to find modTime field if (pollCount === 1) { const keys = Object.keys(bestSession).filter(k => !['latestNotifyUserStep', 'latestTaskBoundaryStep', 'latestToolCallStep'].includes(k)); logToFile(`[DEBUG] session keys: ${keys.join(', ')}`); logToFile(`[DEBUG] lastModifiedTime=${bestSession.lastModifiedTime}, lastModifiedTimestamp=${bestSession.lastModifiedTimestamp}, modifiedTime=${bestSession.modifiedTime}`); } const currentModTime = bestSession.lastModifiedTime || bestSession.lastModifiedTimestamp || bestSession.modifiedTime || ''; const modTimeChanged = currentModTime !== lastModTime; const isStall = isRunning && delta === 0; // Log modTime on stalls for debugging if (isStall && consecutiveIdleCount < 8) { logToFile(`[STALL-DBG] idle=${consecutiveIdleCount} modTime='${currentModTime}' changed=${modTimeChanged}`); } if (delta > 0) { consecutiveIdleCount = 0; sawRunningAfterPending = true; lastModTime = currentModTime; } else if (isStall) { if (modTimeChanged) { // lastModifiedTime is still changing = AI is thinking, NOT approval consecutiveIdleCount = 0; // Reset! if (pollCount <= 10 || pollCount % 12 === 0) { logToFile(`[THINK] step=${currentCount} modTime changing → not stall`); } } else { // lastModifiedTime frozen = real stall (approval waiting) consecutiveIdleCount++; } lastModTime = currentModTime; const now = Date.now(); const cooldownOk = (now - lastPendingTime) > 60_000; if (consecutiveIdleCount >= 6 && sawRunningAfterPending && cooldownOk) { // 6 polls × 5s = 30 seconds of FROZEN stall = approval waiting lastPendingStepIndex = currentCount; lastPendingTime = now; sawRunningAfterPending = false; const command = `Stall at step ${currentCount}`; const description = `승인 대기 감지 (${consecutiveIdleCount * 5}초 정지), Title: "${currentTitle}"`; logToFile(`[STALL] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`); writePendingApproval({ conversation_id: activeSessionId, command, description }); } else if (consecutiveIdleCount === 6) { const reasons = []; if (!sawRunningAfterPending) reasons.push('needDelta>0'); if (!cooldownOk) reasons.push(`cooldown(${Math.round((60000 - (now - lastPendingTime)) / 1000)}s)`); if (reasons.length > 0) logToFile(`[STALL] SKIP: ${reasons.join(', ')}`); } } else if (!isRunning) { consecutiveIdleCount = 0; lastModTime = currentModTime; } // ── Process latestNotifyUserStep ── const notifyStep = bestSession.latestNotifyUserStep; if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) { lastNotifyStepIndex = notifyStep.stepIndex; const content = notifyStep.step?.notifyUser?.notificationContent || ''; if (content.length > 10) { writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`); console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`); } } // ── Process latestTaskBoundaryStep ── const taskStep = bestSession.latestTaskBoundaryStep; if (taskStep && taskStep.stepIndex > lastTaskStepIndex) { lastTaskStepIndex = taskStep.stepIndex; const tb = taskStep.step?.taskBoundary; if (tb?.taskName) { const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : ''; writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`); console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`); } } } catch (e) { if (pollCount <= 5 || pollCount % 20 === 0) { console.log(`Gravity Bridge: [POLL#${pollCount}] error: ${e.message}`); } } }, 5000); } // ─── Response Watcher (Discord approval → Antigravity RPC) ─── let responseWatcher = null; function setupResponseWatcher() { const responseDir = path.join(bridgePath, 'response'); if (!fs.existsSync(responseDir)) { fs.mkdirSync(responseDir, { recursive: true }); } try { responseWatcher = fs.watch(responseDir, (event, filename) => { 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); } } }); console.log('Gravity Bridge: response watcher started'); } catch (e) { console.log(`Gravity Bridge: response watcher failed: ${e.message}`); } } async function processResponseFile(filePath) { try { const content = fs.readFileSync(filePath, 'utf-8'); const resp = JSON.parse(content); const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved}`; console.log(`Gravity Bridge: ${msg}`); logToFile(msg); // 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 ═══ // 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; 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 { // 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('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)); } 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 (${isDomObserver ? 'dom' : 'stall'})`); // Cleanup response file (but NOT pending — renderer still polls it) try { fs.unlinkSync(filePath); } catch { } } catch (e) { const log = `[RESPONSE] error: ${e.message}`; console.log(`Gravity Bridge: ${log}`); logToFile(log); } } /** * Extract AI text from a PLANNER_RESPONSE step. * Known structure: {type, status, metadata, plannerResponse, ephemeralMessage, ...} * ephemeralMessage = system prompt (SKIP), plannerResponse = AI content */ function extractPlannerText(step) { if (!step) { return null; } // Fields to SKIP — not user-facing content const SKIP_FIELDS = new Set([ 'thinking', 'thinkingSignature', 'stopReason', 'type', 'status', 'metadata', 'ephemeralMessage', 'generatorModel', 'requestedModel', 'executionId', 'sourceTrajectoryStepInfo', 'stepIndex', 'viewableAt', 'createdAt', 'finishedGeneratingAt', 'lastCompletedChunkAt', 'source', 'stepGenerationVersion' ]); // plannerResponse can be string or object const pr = step.plannerResponse; if (typeof pr === 'string' && pr.length > 10) { return filterEphemeral(pr); } if (pr && typeof pr === 'object') { // Try known content fields first (NOT thinking/stopReason) const text = pr.content || pr.text || pr.summary || pr.message || pr.response || pr.output; if (typeof text === 'string' && text.length > 10) { return filterEphemeral(text); } // Search other fields, but skip non-content ones for (const key of Object.keys(pr)) { if (SKIP_FIELDS.has(key)) continue; const val = pr[key]; if (typeof val === 'string' && val.length > 50) { // Higher threshold const filtered = filterEphemeral(val); if (filtered) { console.log(`Gravity Bridge: [DEBUG] planner text in plannerResponse.${key} (${filtered.length} chars)`); return filtered; } } } } // Try other step fields (skip known non-content) for (const key of Object.keys(step)) { if (SKIP_FIELDS.has(key) || key === 'plannerResponse') continue; const val = step[key]; if (typeof val === 'string' && val.length > 50) { const filtered = filterEphemeral(val); if (filtered) { console.log(`Gravity Bridge: [DEBUG] planner text in step.${key}`); return filtered; } } } return null; } /** Filter out system ephemeral messages and non-content strings. */ function filterEphemeral(text) { if (!text || text.length < 10) { return null; } // Skip system prompt metadata if (text.includes('') || text.includes('')) { return null; } if (text.includes('artifact_reminder') || text.includes('active_task_reminder')) { return null; } if (text.includes('no_active_task_reminder')) { return null; } // Skip base64/crypto strings (no spaces, mostly alphanumeric) if (!text.includes(' ') && /^[A-Za-z0-9+/=_\-]{50,}$/.test(text)) { return null; } return text; } /** Write a pending approval file matching Bot's ApprovalRequest dataclass. */ function writePendingApproval(data) { try { const pendingDir = path.join(bridgePath, 'pending'); if (!fs.existsSync(pendingDir)) { fs.mkdirSync(pendingDir, { recursive: true }); } const id = Date.now().toString(); const payload = { request_id: id, conversation_id: data.conversation_id, command: data.command, description: data.description, timestamp: Date.now() / 1000, status: 'pending', discord_message_id: 0, project_name: projectName, }; 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}`); } } // ─── Activation ─── async function activate(context) { console.log('Gravity Bridge: activating...'); // Project detection projectName = detectProjectName(); console.log(`Gravity Bridge: project "${projectName}"`); // Bridge path const config = vscode.workspace.getConfiguration('gravityBridge'); const configPath = config.get('bridgePath'); bridgePath = configPath || path.join(os.homedir(), '.gemini', 'antigravity', 'bridge'); ensureBridgeDir(); console.log(`Gravity Bridge: bridge path: ${bridgePath}`); // Status bar statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); statusBar.text = '$(sync~spin) Bridge'; statusBar.tooltip = `Gravity Bridge: ${projectName}`; statusBar.show(); context.subscriptions.push(statusBar); // Initialize SDK const sdkReady = await initSDK(context); if (sdkReady) { setupMonitor(); // Now just logs that monitor is disabled setupApprovalObserver(); // DOM observer via SDK IntegrationManager statusBar.text = '$(check) Bridge'; statusBar.tooltip = `Gravity Bridge: ${projectName} (POLL + Observer active)`; // Register SDK-powered commands context.subscriptions.push(vscode.commands.registerCommand('gravityBridge.approve', async () => { try { await sdk.cascade.acceptStep(); vscode.window.showInformationMessage('Gravity Bridge: Step approved'); } catch (e) { vscode.window.showErrorMessage(`Approve failed: ${e.message}`); } }), vscode.commands.registerCommand('gravityBridge.reject', async () => { try { await sdk.cascade.rejectStep(); vscode.window.showInformationMessage('Gravity Bridge: Step rejected'); } catch (e) { vscode.window.showErrorMessage(`Reject failed: ${e.message}`); } })); } else { statusBar.text = '$(warning) Bridge (no SDK)'; console.log('Gravity Bridge: SDK not available, file-based mode only'); } // Watch commands directory watchCommandsDir(); // Watch response directory for approval interactions setupResponseWatcher(); // Register basic commands context.subscriptions.push(vscode.commands.registerCommand('gravityBridge.start', () => { isActive = true; statusBar.text = sdkReady ? '$(check) Bridge SDK' : '$(sync~spin) Bridge'; vscode.window.showInformationMessage(`Gravity Bridge started for "${projectName}"`); }), vscode.commands.registerCommand('gravityBridge.stop', () => { isActive = false; // SDK monitor is disabled, no need to stop statusBar.text = '$(circle-slash) Bridge OFF'; vscode.window.showInformationMessage('Gravity Bridge stopped'); }), vscode.commands.registerCommand('gravityBridge.connect', async () => { if (!sdk) { vscode.window.showErrorMessage('SDK not initialized'); return; } try { const sessions = await sdk.cascade.getSessions(); const items = sessions.map((s) => ({ label: s.title || 'Untitled', description: `step ${s.stepCount} • ${s.id?.substring(0, 8)}`, sessionId: s.id, })); const pick = await vscode.window.showQuickPick(items, { placeHolder: 'Select a conversation to connect' }); if (pick) { await sdk.cascade.focusSession(pick.sessionId); vscode.window.showInformationMessage(`Connected to: ${pick.label}`); } } catch (e) { vscode.window.showErrorMessage(`Connect failed: ${e.message}`); } })); // Cleanup context.subscriptions.push({ dispose: () => { if (sdk) { try { sdk.dispose(); } catch { } } if (watcher) { watcher.close(); } if (commandsWatcher) { commandsWatcher.close(); } } }); console.log('Gravity Bridge: ✅ activated'); isActive = true; } function deactivate() { if (sdk) { try { sdk.dispose(); } catch { } } } //# sourceMappingURL=extension.js.map