Files
gravity_control/extension/out/extension.js
CD 1f63f60280 feat: proto-based RPC approval for Run commands via Discord
Decoded HandleCascadeUserInteractionRequest protobuf schema from AG's
extension.js (message #162, base64 FileDescriptor 78KB).

Working payload (variant PROTO-0):
  cascadeId + interaction.{trajectoryId, stepIndex, runCommand.confirm}

Changes:
- extension.ts: Added Strategy 0-PROTO with decoded proto RPC call
- extension.ts: Fixed processResponseFile to call tryApprovalStrategies()
  instead of direct clickTrigger (was bypassing all strategies)
- extension.ts: Fixed false positive Run detection (sessionStalled reset
  when step_probe confirms no WAITING)
- extension.ts: Moved lastPendingStepIndex to module scope
- extension.ts: Added activeTrajectoryId tracking from session init
- bot.py: Added MERGE detection + Discord message edit for command updates
- bot.py: Added _sent_commands tracking for merge detection

Proto RE methodology:
1. Found schema exports in AG extension.js
2. Located fileDesc() with base64 protobuf descriptor
3. Decoded 58KB raw proto, found message names
4. Extracted CascadeRunCommandInteraction.confirm field
5. Tested camelCase JSON via ConnectRPC = SUCCESS
2026-03-10 07:45:10 +09:00

2413 lines
115 KiB
JavaScript

"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"));
const crypto = __importStar(require("crypto"));
// ─── 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 deterministicPort = 0; // derived from projectName, consistent across restarts
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 = '';
let activeTrajectoryId = '';
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] patcher.install() called (needs reload)');
}
// Patch BOTH HTML files with inline script injection.
// CRITICAL: vscode-file:// does NOT serve custom .js files (silent 404),
// so we MUST inline the script directly into BOTH HTML files.
// workbench.html — loaded by DevTools/standard mode
// workbench-jetski-agent.html — loaded by AG agent mode
const scriptDir = path.dirname(scriptPath);
const htmlFiles = ['workbench.html', 'workbench-jetski-agent.html'];
for (const htmlFileName of htmlFiles) {
const htmlPath = path.join(scriptDir, htmlFileName);
try {
if (!fs.existsSync(htmlPath)) {
logToFile(`[OBSERVER] ${htmlFileName} not found — skipping`);
continue;
}
let html = fs.readFileSync(htmlPath, 'utf8');
// CRITICAL: Patch CSP to allow inline scripts.
// Default CSP has script-src 'self' 'unsafe-eval' blob: — NO 'unsafe-inline'.
// Without 'unsafe-inline', all inline <script> tags are silently blocked.
if (html.includes('script-src') && !html.match(/script-src[^;]*'unsafe-inline'/)) {
html = html.replace(/(script-src\s[^;]*?)('self')/, "$1$2\n\t\t\t\t\t'unsafe-inline'");
logToFile(`[OBSERVER] ${htmlFileName} CSP patched: added 'unsafe-inline' to script-src`);
}
// Remove old external script tag if present (legacy, cannot be served)
const extMarkerStart = '<!-- AG SDK [variet-gravity-bridge] -->';
const extMarkerEnd = '<!-- /AG SDK [variet-gravity-bridge] -->';
if (html.includes(extMarkerStart)) {
const extRe = new RegExp('\\n?' + extMarkerStart.replace(/[[\]]/g, '\\$&') +
'[\\s\\S]*?' +
extMarkerEnd.replace(/[[\]]/g, '\\$&') + '\\n?');
html = html.replace(extRe, '');
logToFile(`[OBSERVER] removed external script tag from ${htmlFileName}`);
}
// Insert or update inline script
const inlineMarkerStart = '<!-- AG SDK INLINE [variet-gravity-bridge] -->';
const inlineMarkerEnd = '<!-- /AG SDK INLINE [variet-gravity-bridge] -->';
if (html.includes(inlineMarkerStart)) {
const re = new RegExp(inlineMarkerStart.replace(/[[\]]/g, '\\$&') +
'[\\s\\S]*?' +
inlineMarkerEnd.replace(/[[\]]/g, '\\$&'));
html = html.replace(re, `${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}`);
logToFile(`[OBSERVER] ${htmlFileName} inline script UPDATED`);
}
else {
html = html.replace('</html>', `\n${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}\n</html>`);
logToFile(`[OBSERVER] ${htmlFileName} inline script INSERTED`);
}
fs.writeFileSync(htmlPath, html, 'utf8');
}
catch (e) {
logToFile(`[OBSERVER] ${htmlFileName} patch error: ${e.message}`);
}
}
}
// 4. Update product.json checksums so vscode-file:// serves our patched files
updateProductChecksums();
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}`);
}
}
// ─── Product.json Checksum Auto-Update ───
// vscode-file:// protocol validates SHA256 checksums in product.json.
// If a file's checksum doesn't match, Electron serves the ORIGINAL cached version.
// This function recalculates checksums for files we modify (HTML files with <script> tags).
function updateProductChecksums() {
try {
// Find product.json (2 levels up from workbench dir: resources/app/product.json)
const patcher = sdk?.integration?._patcher;
if (!patcher || typeof patcher.getWorkbenchDir !== 'function') {
logToFile('[CHECKSUM] no patcher/workbenchDir — skipping');
return;
}
const workbenchDir = patcher.getWorkbenchDir();
// workbenchDir = .../resources/app/out/vs/code/electron-browser/workbench
// product.json = .../resources/app/product.json (5 levels up from workbench)
const appDir = path.resolve(workbenchDir, '..', '..', '..', '..', '..');
const productJsonPath = path.join(appDir, 'product.json');
if (!fs.existsSync(productJsonPath)) {
logToFile(`[CHECKSUM] product.json not found at ${productJsonPath}`);
return;
}
// Read product.json (may have BOM)
let raw = fs.readFileSync(productJsonPath, 'utf8');
if (raw.charCodeAt(0) === 0xFEFF)
raw = raw.substring(1);
const product = JSON.parse(raw);
if (!product.checksums) {
logToFile('[CHECKSUM] no checksums section in product.json');
return;
}
// Files we may modify or create (relative key in product.json → absolute path)
// CRITICAL: vscode-file:// only serves files with valid checksums in product.json.
// Custom JS files MUST be added here or they'll silently 404.
const filesToCheck = {
'vs/code/electron-browser/workbench/workbench.html': path.join(workbenchDir, 'workbench.html'),
'vs/code/electron-browser/workbench/workbench-jetski-agent.html': path.join(workbenchDir, 'workbench-jetski-agent.html'),
'vs/code/electron-browser/workbench/ag-sdk-variet-gravity-bridge.js': path.join(workbenchDir, 'ag-sdk-variet-gravity-bridge.js'),
};
let updated = false;
for (const [key, filePath] of Object.entries(filesToCheck)) {
if (!fs.existsSync(filePath))
continue;
const fileBytes = fs.readFileSync(filePath);
const hash = crypto.createHash('sha256').update(fileBytes).digest('base64').replace(/=+$/, '');
if (product.checksums[key] !== hash) {
logToFile(`[CHECKSUM] updating ${key}: ${product.checksums[key].substring(0, 12)}... → ${hash.substring(0, 12)}...`);
product.checksums[key] = hash;
updated = true;
}
}
if (updated) {
fs.writeFileSync(productJsonPath, JSON.stringify(product, null, '\t'), 'utf8');
logToFile('[CHECKSUM] product.json updated ✅');
}
else {
logToFile('[CHECKSUM] all checksums already match ✅');
}
}
catch (e) {
logToFile(`[CHECKSUM] error: ${e.message}`);
}
}
// ─── HTTP Bridge Server (Extension Host → Renderer communication) ───
let observerHttpServer = null;
const pendingResponses = new Map();
// Click trigger: extension sets this, renderer polls and clicks button
let clickTrigger = null;
let sessionStalled = false; // true when session is stalled waiting for approval
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step (module-level for tryApprovalStrategies)
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
let deepInspectRequested = false;
let deepInspectResult = null;
let deepInspectWaiters = [];
/** Derive a deterministic port from project name (range 10000-60000) */
function getDeterministicPort(name) {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
}
return 10000 + (Math.abs(hash) % 50000);
}
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);
// ── Server-side false positive filter ──
const cmd = (data.command || '').trim();
const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it)$/i;
if (FALSE_POSITIVE_RE.test(cmd)) {
logToFile(`[HTTP] filtered false positive: "${cmd}"`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: false, filtered: true }));
return;
}
// "Run" button → only accept if session is actually stalled (waiting for approval)
if (/^Run/i.test(cmd) && !sessionStalled) {
logToFile(`[HTTP] filtered "Run" — session not stalled`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: false, filtered: true }));
return;
}
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,
conversation_id: activeSessionId || '',
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}" btns=${(data.buttons || []).length} 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'));
logToFile(`[HTTP] response served to renderer: ${rid} approved=${data.approved}`);
// Delay deletion: processResponseFile (response watcher) may need to read it too.
// The watcher fires with 300ms delay, so 2s is safe.
setTimeout(() => {
try {
fs.unlinkSync(respFile);
}
catch { }
}, 2000);
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 /trigger-click — renderer polls to check if extension wants a click
if (req.method === 'GET' && url.pathname === '/trigger-click') {
if (clickTrigger && (Date.now() - clickTrigger.timestamp) < 30000) {
const trigger = clickTrigger;
clickTrigger = null; // consume once
logToFile(`[HTTP] trigger-click consumed: ${trigger.action}`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ action: trigger.action }));
}
else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ action: null }));
}
return;
}
// GET /deep-inspect — trigger deep DOM inspection from renderer
if (req.method === 'GET' && url.pathname === '/deep-inspect') {
deepInspectRequested = true;
logToFile('[HTTP] deep-inspect triggered — waiting for renderer result...');
// Wait up to 10s for renderer to POST result
const timeout = setTimeout(() => {
deepInspectWaiters = deepInspectWaiters.filter(w => w !== waiter);
if (deepInspectResult) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(deepInspectResult));
}
else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'timeout', message: 'Renderer did not respond in 10s. Is the v3 script loaded?' }));
}
}, 10000);
const waiter = (data) => {
clearTimeout(timeout);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
};
deepInspectWaiters.push(waiter);
return;
}
// GET /deep-inspect-trigger — renderer polls this
if (req.method === 'GET' && url.pathname === '/deep-inspect-trigger') {
const requested = deepInspectRequested;
deepInspectRequested = false;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ inspect: requested }));
return;
}
// POST /deep-inspect-result — renderer posts inspection results here
if (req.method === 'POST' && url.pathname === '/deep-inspect-result') {
let body = '';
req.on('data', (c) => body += c);
req.on('end', () => {
try {
const data = JSON.parse(body);
deepInspectResult = data;
logToFile(`[HTTP] deep-inspect result received (${body.length} bytes)`);
// Write to file for reference
const inspectFile = path.join(bridgePath, 'deep-inspect-result.json');
fs.writeFileSync(inspectFile, JSON.stringify(data, null, 2));
// Notify waiters
const waiters = [...deepInspectWaiters];
deepInspectWaiters = [];
waiters.forEach(w => w(data));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
}
catch (e) {
logToFile(`[HTTP] deep-inspect-result parse error: ${e.message}`);
res.writeHead(400);
res.end(JSON.stringify({ error: e.message }));
}
});
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 deterministic port (derived from projectName), fallback to random
deterministicPort = getDeterministicPort(projectName);
const tryListen = (targetPort) => {
server.listen(targetPort, '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) => {
if (e.code === 'EADDRINUSE' && deterministicPort > 0) {
logToFile(`[HTTP] deterministic port ${deterministicPort} in use, trying random...`);
deterministicPort = 0;
const server2 = require('http').createServer(server._events.request);
observerHttpServer = server2;
server2.on('error', (e2) => {
logToFile(`[HTTP] random port also failed: ${e2.message}`);
resolve(0);
});
server2.listen(0, '127.0.0.1', () => {
const port = server2.address().port;
logToFile(`[HTTP] bridge server started on RANDOM port ${port}`);
resolve(port);
});
return;
}
logToFile(`[HTTP] server error: ${e.message}`);
resolve(0);
});
tryListen(deterministicPort);
}
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 v3: Approval Observer (deep DOM traversal — iframes, webviews, shadow DOMs) ──
(function(){
'use strict';
var BASE='',_obs=false,_sent={},_ready=false;
var _scanScheduled=false,_lastScanTs=0;
var THROTTLE_MS=100;
var CLEANUP_MS=300000;
var _domDumped=false;
function log(m){console.log('[GB Observer] '+m);}
log('v3 Script loaded — deep DOM traversal enabled');
// ── Deep DOM Traversal: find buttons across ALL boundaries ──
// Searches: main document → iframes (contentDocument) → webview elements → shadow DOMs
function deepFindButtons(patterns){
var results=[];
// 1. Main document buttons
collectButtons(document,results,patterns,'main');
// 2. 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');
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();
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');
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");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)
function dumpDOMStructure(){runDeepInspect();}
// ── Port Discovery: async fetch()-based (sync XHR blocked in Electron renderer) ──
var HARDCODED_PORT=${_port};
function tryPingAsync(port){
return fetch('http://127.0.0.1:'+port+'/ping?t='+Date.now(),{signal:AbortSignal.timeout(2000)})
.then(function(r){return r.text();})
.then(function(t){return t==='pong';})
.catch(function(){return false;});
}
function discoverPort(cb){
log('Trying hardcoded port '+HARDCODED_PORT+'...');
tryPingAsync(HARDCODED_PORT).then(function(ok){
if(ok){log('Port discovered (hardcoded): '+HARDCODED_PORT);cb(HARDCODED_PORT);return;}
log('Hardcoded port failed, retrying with backoff...');
var attempts=0;
var timer=setInterval(function(){
attempts++;
if(attempts>60){clearInterval(timer);log('Port discovery timeout after 2min');return;}
tryPingAsync(HARDCODED_PORT).then(function(ok2){
if(ok2){clearInterval(timer);log('Port discovered (retry #'+attempts+'): '+HARDCODED_PORT);cb(HARDCODED_PORT);}
});
},2000);
});
}
discoverPort(function(port){
BASE='http://127.0.0.1:'+port;
fetch(BASE+'/ping').then(function(r){return r.text();}).then(function(t){
if(t==='pong'){log('Bridge connected on port '+port);_ready=true;startObserver();setTimeout(dumpDOMStructure,3000);}
else log('Bridge ping failed: '+t);
}).catch(function(e){log('Bridge unreachable: '+e.message);});
});
// ── Button patterns to detect (order matters: first match wins per scan) ──
var PATS=[
{re:/^Run/i, type:'terminal_command'},
{re:/^Accept all$/i, type:'diff_review'},
{re:/^Reject all$/i, type:'diff_review'},
{re:/^Accept$/i, type:'agent_step'},
{re:/^Allow/i, type:'permission'},
{re:/^Approve/i, type:'agent_step'},
{re:/^Deny$/i, type:'permission'},
{re:/^Retry$/i, type:'error_recovery'},
{re:/^Dismiss$/i, type:'error_recovery'},
];
// ALL actionable button patterns (for grouping siblings in same container)
var ALL_ACTION_RE=[/^Run/i,/^Accept/i,/^Reject/i,/^Allow/i,/^Deny/i,/^Approve/i,/^Cancel$/i,/^Retry$/i,/^Dismiss$/i,/^Stop$/i,/^Decline$/i];
// Reject button patterns for finding the counterpart
var REJECT_RE=[/^reject$/i,/^reject all$/i,/^cancel$/i,/^deny$/i,/^stop$/i,/^decline$/i,/^dismiss$/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 common container of related buttons ──
function findButtonContainer(btn){
return btn.closest('[class*="step"]')
||btn.closest('[class*="action"]')
||btn.closest('[class*="tool"]')
||btn.closest('[class*="cascade"]')
||btn.closest('[class*="message"]')
||btn.closest('[class*="dialog"]')
||btn.closest('[class*="notification"]')
||btn.parentElement;
}
// ── Collect all actionable sibling buttons from a container ──
function collectSiblingButtons(container,triggerBtn){
if(!container)return [];
var siblings=container.querySelectorAll('button');
var result=[];
for(var i=0;i<siblings.length;i++){
var sb=siblings[i];
if(sb.disabled||sb.hidden)continue;
try{if(!sb.offsetParent&&sb.style.display!=='fixed')continue;}catch(e){}
var stxt=(sb.textContent||'').trim();
stxt=stxt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/,'').trim();
if(!stxt)continue;
// Check if this button matches any actionable pattern
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;
result.push({btn:sb,text:stxt,isPrimary:(sb===triggerBtn)});
}
return result;
}
// ── Find the React app container (Antigravity's main UI root) ──
function findPanel(){
// Priority order of panel selectors (most specific first)
var selectors=[
'.antigravity-agent-side-panel',
'#jetski-agent-panel',
'.react-app-container',
'[class*="agent-panel"]',
'[class*="agentPanel"]',
];
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 ──
// Groups related buttons from same container into a single pending
function scan(){
if(!_ready)return;
var now=Date.now();
var panel=findPanel();
// Expand search: panel-scoped first, then full body for review bars
var searchRoots=[];
if(panel)searchRoots.push(panel);
// Always also scan body for diff review bar (Accept all/Reject all)
// which lives outside the agent panel in the editor notification area
if(document.body)searchRoots.push(document.body);
if(!searchRoots.length)return;
var seen={}; // dedupe buttons across search roots
for(var r=0;r<searchRoots.length;r++){
var allBtns=searchRoots[r].querySelectorAll('button');
if(!allBtns.length)continue;
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;
// Strip keyboard shortcut suffixes (e.g. "RunAlt+↵" → "Run")
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/,'').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 for the GROUP (use container-based key)
var container=findButtonContainer(b);
var groupKey=matchedType+'|group|'+(container?(container.textContent||'').substring(0,40).replace(/\\s+/g,' '):'none');
if(_sent[groupKey])continue;
// Collect ALL related buttons from the same container
var siblings=collectSiblingButtons(container,b);
if(siblings.length===0)siblings=[{btn:b,text:txt,isPrimary:true}];
// Build buttons array for multi-choice support
var buttonsArr=[];
var btnRefs=[];
var bidList=[];
for(var si=0;si<siblings.length;si++){
var sb=siblings[si];
var sbid=btnId(sb.btn,matchedType);
buttonsArr.push({text:sb.text,index:si,is_primary:sb.isPrimary});
btnRefs.push(sb.btn);
bidList.push(sbid);
}
// Extract context from trigger button
var desc=extractContext(b);
var rid=now.toString()+'_'+Math.random().toString(36).substring(2,6);
// Mark entire group as sent
_sent[groupKey]={rid:rid,ts:now};
for(var mk=0;mk<bidList.length;mk++){_sent[bidList[mk]]={rid:rid,ts:now};}
log('DETECTED GROUP '+matchedType+': '+buttonsArr.map(function(x){return '"'+x.text+'"';}).join(', ')+' → pending to bridge');
// Send to bridge (closure to capture refs)
(function(rid2,btnRefs2,bidList2,groupKey2,txt2,desc2,type2,buttonsArr2){
var payload={
request_id:rid2,
command:txt2,
description:desc2,
step_type:type2,
buttons:buttonsArr2
};
fetch(BASE+'/pending',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(payload)
}).then(function(r){return r.json();}).then(function(d){
log('Pending created: '+d.request_id+' for group ['+buttonsArr2.map(function(x){return x.text;}).join(', ')+']');
pollResponseGroup(d.request_id,btnRefs2,bidList2,groupKey2);
}).catch(function(e){
log('POST error: '+e.message);
delete _sent[groupKey2];
for(var di=0;di<bidList2.length;di++){delete _sent[bidList2[di]];}
});
})(rid,btnRefs,bidList,groupKey,txt,desc,matchedType,buttonsArr);
// Process ONE button GROUP per scan cycle (avoid flooding)
return;
}
} // end searchRoots loop
}
// ── Poll for Discord response (multi-button group aware) ──
function pollResponseGroup(rid,btnRefs,bidList,groupKey){
var polls=0;
var maxPolls=600; // 5 minutes at 500ms interval
var timer=setInterval(function(){
polls++;
// Check if ANY button in the group is still in DOM
var anyAlive=false;
for(var ai=0;ai<btnRefs.length;ai++){
if(document.body.contains(btnRefs[ai])){anyAlive=true;break;}
}
if(!anyAlive){
log('All buttons removed from DOM — stopping poll for '+rid);
clearInterval(timer);
delete _sent[groupKey];
for(var ci=0;ci<bidList.length;ci++){delete _sent[bidList[ci]];}
return;
}
if(polls>maxPolls){
log('Poll timeout for '+rid);
clearInterval(timer);
delete _sent[groupKey];
for(var ti=0;ti<bidList.length;ti++){delete _sent[bidList[ti]];}
return;
}
fetch(BASE+'/response/'+rid).then(function(r){return r.json();}).then(function(d){
if(d.waiting)return;
clearInterval(timer);
var btnIdx=(typeof d.button_index==='number'&&d.button_index>=0)?d.button_index:-1;
if(btnIdx>=0&&btnIdx<btnRefs.length){
// Multi-choice: click specific button by index
var targetBtn=btnRefs[btnIdx];
var targetTxt=(targetBtn.textContent||'').trim();
log((d.approved?'✅':'❌')+' CHOICE '+rid+' → clicking button['+btnIdx+'] "'+targetTxt+'"');
targetBtn.click();
} else if(d.approved){
// Legacy single-button: click first (primary) button
var primaryBtn=btnRefs[0];
log('✅ APPROVED '+rid+' → clicking "'+((primaryBtn.textContent||'').trim())+'"');
primaryBtn.click();
} else {
// Legacy reject: find and click reject/deny button
log('❌ REJECTED '+rid+' → finding reject button');
clickRejectButton(btnRefs[0]);
}
delete _sent[groupKey];
for(var ri=0;ri<bidList.length;ri++){delete _sent[bidList[ri]];}
}).catch(function(){});
},500);
}
// Legacy pollResponse for backward compatibility (single button)
function pollResponse(rid,btn,bid){
pollResponseGroup(rid,[btn],[bid],bid);
}
// ── Find and click the reject/cancel counterpart button ──
function clickRejectButton(approveBtn){
// Walk up to find the container, then search for reject buttons
var container=approveBtn.closest('[class*="step"]')
||approveBtn.closest('[class*="action"]')
||approveBtn.closest('[class*="tool"]')
||approveBtn.parentElement;
if(!container){log('No container for reject');return;}
var siblings=container.querySelectorAll('button');
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;
// 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);
// ── DEEP-INSPECT POLLING: curl→Bridge→Renderer→Results ──
setInterval(function(){
if(!_ready||!BASE)return;
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(){});
},2000);
// ── 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
setInterval(function(){
if(!_ready||!BASE)return;
fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
if(!d.action)return;
log('🔔 TRIGGER-CLICK received: action='+d.action);
var approveRe=[/^Run$/i,/^Run /i,/^Accept/i,/^Allow/i,/^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 emoji=(d.action==='approve')?'✅':'❌';
// Phase 1: deepFindButtons in main doc + accessible iframes + shadow DOMs
var found=deepFindButtons(patterns);
if(found.length>0){
log(emoji+' TRIGGER-CLICK: clicking "'+found[0].text+'" from '+found[0].source);
found[0].btn.click();
return;
}
// Phase 2: Try <webview>.executeJavaScript for inaccessible webviews
var webviews=document.querySelectorAll('webview');
if(webviews.length>0){
log('TRIGGER-CLICK: trying '+webviews.length+' webview(s) via executeJavaScript...');
var patternsStr=patterns.map(function(re){return re.source;}).join('|');
var clickScript='(function(){'+
'var re=new RegExp("'+patternsStr+'","i");'+
'var btns=document.querySelectorAll("button");'+
'for(var i=0;i<btns.length;i++){'+
'var b=btns[i];if(b.disabled||b.hidden)continue;'+
'var t=(b.textContent||"").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');
for(var bi=0;bi<ibtns.length;bi++){
var ib=ibtns[bi];
if(ib.disabled||ib.hidden)continue;
var itxt=(ib.textContent||'').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');
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(){});
},1000);
_obs=true;
log('v3 Observer active — deep DOM traversal + MutationObserver + trigger-click polling');
}
})();
`;
}
// 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 (used by writeChatSnapshot for lazy registration)
let activeSessionTitle = '';
let lastKnownStepCount = 0;
let lastNotifyStepIndex = -1;
let lastTaskStepIndex = -1;
// lastPendingStepIndex is module-level (above sessionStalled)
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
let stallProbed = false; // prevent repeated step probes during same stall
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
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;
activeTrajectoryId = bestSession.trajectoryId || '';
activeSessionTitle = currentTitle;
lastKnownStepCount = currentCount;
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
lastPendingStepIndex = -1;
stallProbed = false;
// Don't register here — registration happens lazily in writeChatSnapshot/writePendingApproval
// to avoid race conditions between multiple extension instances
// Dump session keys + trajectoryMetadata on session change
const allKeys = Object.keys(bestSession);
logToFile(`[SESSION-INIT] id=${activeSessionId.substring(0, 8)} keys=[${allKeys.join(',')}]`);
const trajMeta = bestSession.trajectoryMetadata;
if (trajMeta) {
logToFile(`[SESSION-INIT] trajectoryMetadata=${JSON.stringify(trajMeta).substring(0, 500)}`);
}
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}`);
}
// ── PRIMARY: Step-probe-based approval detection ──
// On stall (idle=1, ~5s), probe GetCascadeTrajectorySteps to check WAITING.
// 775-step limit: probe fails for long sessions → faster stall fallback.
// ── STALL-BASED approval detection with step probe ──
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) {
sessionStalled = false;
// Steps progressed — if we had a pending approval, it was handled in AG directly
if (!sawRunningAfterPending && lastPendingStepIndex >= 0) {
// Mark pending as auto_resolved so bot can update Discord message
try {
const pendingFiles = fs.readdirSync(path.join(bridgePath, 'pending'))
.filter((f) => f.endsWith('.json'));
for (const pf of pendingFiles) {
const pfPath = path.join(bridgePath, 'pending', pf);
const pd = JSON.parse(fs.readFileSync(pfPath, 'utf-8'));
if (pd.status === 'pending' && pd.step_index === lastPendingStepIndex) {
pd.status = 'auto_resolved';
fs.writeFileSync(pfPath, JSON.stringify(pd, null, 2), 'utf-8');
logToFile(`[AUTO-RESOLVE] step=${lastPendingStepIndex} progressed → marked ${pf}`);
break;
}
}
}
catch (e) {
logToFile(`[AUTO-RESOLVE] error: ${e.message}`);
}
lastPendingStepIndex = -1;
}
consecutiveIdleCount = 0;
sawRunningAfterPending = true;
stallProbed = false; // allow re-probe on next stall
lastModTime = currentModTime;
}
else if (isStall) {
if (modTimeChanged) {
// lastModifiedTime is still changing = AI is thinking, NOT approval
consecutiveIdleCount = 0; // Reset!
stallProbed = false;
if (pollCount <= 10 || pollCount % 12 === 0) {
logToFile(`[THINK] step=${currentCount} modTime changing → not stall`);
}
}
else {
// lastModifiedTime frozen = real stall (approval waiting)
consecutiveIdleCount++;
if (consecutiveIdleCount >= 1)
sessionStalled = true;
}
lastModTime = currentModTime;
// ── Step probe: on stall, fetch latest step via cascadeId (retry until WAITING found) ──
// CONFIRMED: param='cascadeId', id=sessionId (map key from trajectorySummaries)
// Retries every 2 polls (~10s) because RUN_COMMAND step may not be created yet
if (consecutiveIdleCount >= 1 && !stallProbed) {
try {
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: bestSessionId,
});
if (stepsResp?.steps?.length > 0) {
const steps = stepsResp.steps;
// Diagnostic: compare returned steps vs trajectory stepCount
logToFile(`[STEP-PROBE] returned=${steps.length} vs trajectory.stepCount=${currentCount}`);
if (steps.length < currentCount) {
logToFile(`[STEP-PROBE] ⚠️ 775-limit hit! steps=${steps.length} < stepCount=${currentCount}`);
}
// Scan last 5 steps backwards to find WAITING (RUN_COMMAND may not be last)
let foundWaiting = false;
for (let si = steps.length - 1; si >= Math.max(0, steps.length - 5); si--) {
const step = steps[si];
const stepStatus = step?.status || '';
const stepType = step?.type || '';
if (stepStatus === 'CORTEX_STEP_STATUS_WAITING') {
foundWaiting = true;
// Extract command from metadata.toolCall or direct fields
const toolCall = step?.metadata?.toolCall;
const toolName = toolCall?.name || stepType.replace('CORTEX_STEP_TYPE_', '').toLowerCase();
let command = toolName;
// Parse argumentsJson for command details
if (toolCall?.argumentsJson) {
try {
const args = JSON.parse(toolCall.argumentsJson);
if (args.CommandLine) {
command = `${toolName}: ${args.CommandLine.substring(0, 150)}`;
}
else if (args.TargetFile) {
command = `${toolName}: ${args.TargetFile.split(/[\\/]/).pop()}`;
}
else {
command = `${toolName}: ${Object.keys(args).join(', ')}`;
}
}
catch {
command = toolName;
}
}
const description = `Step #${si} (${stepType.replace('CORTEX_STEP_TYPE_', '')})`;
logToFile(`[STEP-PROBE] ★ WAITING! step=${si} type=${stepType} cmd='${command}'`);
if (si !== lastPendingStepIndex) {
stallProbed = true; // found WAITING — stop retrying
lastPendingStepIndex = si;
lastPendingTime = Date.now();
sawRunningAfterPending = false;
writePendingApproval({
conversation_id: activeSessionId,
command,
description,
step_type: toolName,
step_index: si,
source: 'step_probe',
});
}
break;
}
}
if (!foundWaiting) {
const lastStep = steps[steps.length - 1];
logToFile(`[STEP-PROBE] lastStep status=${lastStep?.status} type=${lastStep?.type} (not waiting)`);
// CRITICAL: Reset sessionStalled when step_probe confirms no WAITING.
// Without this, sessionStalled stays true during long AI generations
// (PLANNER_RESPONSE with delta=0), causing false positive "Run" detections.
sessionStalled = false;
}
}
}
catch (e) {
logToFile(`[STEP-PROBE] error: ${e.message}`);
}
}
// Stall fallback REMOVED — step probe is sole fallback source
// (stall fallback was generating false positives and is now redundant)
}
else if (!isRunning) {
// ── Error detection: probe when session transitions from RUNNING→idle ──
if (consecutiveIdleCount > 0 && !stallProbed) {
// Was running, now idle — possible error. Probe once.
try {
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: bestSessionId,
});
if (stepsResp?.steps?.length > 0) {
const steps = stepsResp.steps;
// Check last 3 steps for error/failed status
for (let si = steps.length - 1; si >= Math.max(0, steps.length - 3); si--) {
const step = steps[si];
const stepStatus = step?.status || '';
const stepType = step?.type || '';
if (stepStatus.includes('ERROR') || stepStatus.includes('FAILED')) {
const toolCall = step?.metadata?.toolCall;
const toolName = toolCall?.name || stepType.replace('CORTEX_STEP_TYPE_', '').toLowerCase();
let command = `⚠️ Error: ${toolName}`;
if (toolCall?.argumentsJson) {
try {
const args = JSON.parse(toolCall.argumentsJson);
if (args.CommandLine)
command = `⚠️ Error: ${args.CommandLine.substring(0, 100)}`;
else if (args.TargetFile)
command = `⚠️ Error: ${args.TargetFile.split(/[\\/]/).pop()}`;
}
catch { }
}
const description = `Step #${si} ${stepStatus} — Retry?`;
logToFile(`[STEP-PROBE] ★ ERROR! step=${si} status=${stepStatus} type=${stepType}`);
if (si !== lastPendingStepIndex) {
stallProbed = true;
lastPendingStepIndex = si;
writePendingApproval({
conversation_id: activeSessionId,
command,
description,
step_type: 'error_recovery',
step_index: si,
source: 'step_probe_error',
});
}
break;
}
}
}
}
catch (e) {
logToFile(`[STEP-PROBE-ERR] error check: ${e.message}`);
}
}
consecutiveIdleCount = 0;
lastModTime = currentModTime;
}
// ── Process latestNotifyUserStep ──
const notifyStep = bestSession.latestNotifyUserStep;
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
lastNotifyStepIndex = notifyStep.stepIndex;
const content = notifyStep.step?.notifyUser?.notificationContent || '';
// Filter: only relay meaningful notifications (skip trivial ones)
if (content.length > 50) {
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`);
}
else if (content.length > 0) {
logToFile(`[POLL] NOTIFY skipped (too short: ${content.length} chars): ${content.substring(0, 80)}`);
}
}
// ── 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_', '') : '';
// Filter: skip status-only updates with same task name (noise)
const taskText = `${tb.taskName}|${tb.taskStatus || ''}`;
if (taskText !== lastRelayedTaskText) {
lastRelayedTaskText = taskText;
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
}
else {
logToFile(`[POLL] TASK skipped (duplicate): "${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 {
// Gracefully handle files already consumed by HTTP handler
if (!fs.existsSync(filePath)) {
// HTTP GET /response/:rid already served and deleted this file — skip silently
return;
}
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 { }
}
// ═══ MULTI-STRATEGY APPROVAL (v2.1) ═══
// Tries multiple methods sequentially with detailed logging.
// DOM observer: renderer handles clicking via pollResponse
// Step probe/stall: try RPC → VS Code commands → log results
const approved = resp.approved;
if (isDomObserver) {
// DOM observer path: renderer polls /response/:rid and clicks directly
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
}
else {
// Step probe path: run ALL approval strategies (5 vectors → 30+ methods)
logToFile(`[RESPONSE] step_probe → running tryApprovalStrategies(${approved}, ${activeSessionId.substring(0, 8)})`);
const strategyResult = await tryApprovalStrategies(approved, activeSessionId);
logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
}
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
// Cleanup response file
// CRITICAL: DOM observer responses must NOT be deleted here!
// The renderer polls GET /response/:rid to discover the approval.
// If we delete the file before the renderer polls, it gets ENOENT.
// The HTTP handler (/response/:rid) deletes after serving to renderer.
if (!isDomObserver) {
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('<EPHEMERAL_MESSAGE>') || text.includes('<ephemeral_message>')) {
return null;
}
if (text.includes('artifact_reminder') || text.includes('active_task_reminder')) {
return null;
}
if (text.includes('no_active_task_reminder')) {
return null;
}
// Skip base64/crypto strings (no spaces, mostly alphanumeric)
if (!text.includes(' ') && /^[A-Za-z0-9+/=_\-]{50,}$/.test(text)) {
return null;
}
return text;
}
/** Extract human-readable command from a tool call step's data. */
function extractToolCommand(stepData) {
// Try common step data shapes from protobuf
if (stepData.runCommand) {
return stepData.runCommand.commandLine || stepData.runCommand.command || 'Run Command';
}
if (stepData.writeToFile) {
const target = stepData.writeToFile.targetFile || stepData.writeToFile.filePath || 'file';
return `Write: ${target.split(/[\\/]/).pop()}`;
}
if (stepData.codeAction) {
const fp = stepData.codeAction.filePath || '';
return `Edit: ${fp.split(/[\\/]/).pop() || 'file'}`;
}
if (stepData.replaceFileContent || stepData.multiReplaceFileContent) {
const d = stepData.replaceFileContent || stepData.multiReplaceFileContent;
const fp = d.targetFile || d.filePath || '';
return `Edit: ${fp.split(/[\\/]/).pop() || 'file'}`;
}
if (stepData.sendCommandInput) {
return `Send Input: ${(stepData.sendCommandInput.input || '').substring(0, 50)}`;
}
// Generic fallback: use first key name
const keys = Object.keys(stepData).filter(k => k !== 'status' && k !== 'stepStatus');
return keys.length > 0 ? keys[0] : 'Unknown tool call';
}
/** Extract description from a tool call step for Discord display. */
function extractToolDescription(stepData, sessionTitle, stepIndex) {
const parts = [`Step #${stepIndex}`, `Session: "${sessionTitle}"`];
// Try to get code/command content for context
if (stepData.runCommand) {
const cmd = stepData.runCommand.commandLine || stepData.runCommand.command || '';
if (cmd)
parts.push(`Command: ${cmd.substring(0, 200)}`);
}
if (stepData.writeToFile?.targetFile) {
parts.push(`File: ${stepData.writeToFile.targetFile}`);
}
if (stepData.codeAction?.filePath) {
parts.push(`File: ${stepData.codeAction.filePath}`);
}
return parts.join('\n');
}
/** 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 });
}
// ── Dedup: if DOM observer already created a "Run"-only pending, MERGE detailed info into it ──
const nowMs = Date.now();
const DEDUP_WINDOW_MS = 15_000; // 15 second dedup window
try {
const existingFiles = fs.readdirSync(pendingDir).filter((f) => f.endsWith('.json'));
for (const ef of existingFiles) {
const efPath = path.join(pendingDir, ef);
const existing = JSON.parse(fs.readFileSync(efPath, 'utf-8'));
if (existing.source === 'dom_observer' && existing.status === 'pending') {
const age = nowMs - (existing.timestamp * 1000);
if (age < DEDUP_WINDOW_MS && age >= 0) {
// MERGE: update DOM observer pending with detailed step_probe info
existing.command = data.command;
existing.description = data.description;
if (data.step_type)
existing.step_type = data.step_type;
if (data.step_index !== undefined)
existing.step_index = data.step_index;
existing.source = 'dom_observer+step_probe'; // mark as merged
fs.writeFileSync(efPath, JSON.stringify(existing, null, 2), 'utf-8');
logToFile(`[DEDUP] MERGED step_probe info into DOM pending: ${ef} cmd="${data.command.substring(0, 60)}"`);
return;
}
}
}
}
catch (dedupErr) {
logToFile(`[DEDUP] check error (non-fatal): ${dedupErr.message}`);
}
const id = nowMs.toString();
const payload = {
request_id: id,
conversation_id: data.conversation_id,
command: data.command,
description: data.description,
timestamp: nowMs / 1000,
status: 'pending',
discord_message_id: 0,
project_name: projectName,
...(data.step_type ? { step_type: data.step_type } : {}),
...(data.step_index !== undefined ? { step_index: data.step_index } : {}),
...(data.source ? { source: data.source } : {}),
};
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}`);
}
}
// ─── Multi-Strategy Approval Execution ───
/**
* Try multiple approval methods sequentially.
* Returns a string describing which method succeeded (or all failed).
*
* Strategy order (most reliable first):
* 1. HandleCascadeUserInteraction RPC (cross-platform, no focus)
* 2. VS Code accept/reject commands (focus-dependent)
* 3. Log failure for manual intervention
*/
async function tryApprovalStrategies(approved, sessionId) {
const action = approved ? 'APPROVE' : 'REJECT';
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)}`);
// ── Dynamic Command Discovery (log what's available during WAITING state) ──
let approvalCmdList = [];
try {
const allCmds = await vscode.commands.getCommands(true);
const agCmds = allCmds.filter((c) => c.startsWith('antigravity.'));
approvalCmdList = agCmds.filter((c) => {
const lower = c.toLowerCase();
return lower.includes('accept') || lower.includes('reject') || lower.includes('approve')
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step')
|| lower.includes('cascade') || lower.includes('action');
});
logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmdList.length} approval-related:`);
for (const c of approvalCmdList) {
logToFile(`[APPROVAL-CMD-CHECK] → ${c}`);
}
}
catch (e) {
logToFile(`[APPROVAL-CMD-CHECK] error: ${e.message}`);
}
// ══════════════════════════════════════════════════════════
// STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source)
// HandleCascadeUserInteractionRequest:
// cascade_id: string
// interaction: CascadeUserInteraction {
// trajectory_id, step_index,
// oneof: { run_command: CascadeRunCommandInteraction { confirm: bool } }
// }
// ConnectRPC uses camelCase for JSON encoding of snake_case proto fields
// ══════════════════════════════════════════════════════════
if (sdk && approved) {
const protoVariants = [
// Variant A: camelCase JSON (ConnectRPC default)
{
cascadeId: sessionId,
interaction: {
trajectoryId: activeTrajectoryId || sessionId,
stepIndex: lastPendingStepIndex,
runCommand: { confirm: true },
},
},
// Variant B: snake_case JSON (proto native)
{
cascade_id: sessionId,
interaction: {
trajectory_id: activeTrajectoryId || sessionId,
step_index: lastPendingStepIndex,
run_command: { confirm: true },
},
},
// Variant C: camelCase, without trajectoryId (maybe optional)
{
cascadeId: sessionId,
interaction: {
stepIndex: lastPendingStepIndex,
runCommand: { confirm: true },
},
},
// Variant D: camelCase, confirm only (minimal)
{
cascadeId: sessionId,
interaction: {
runCommand: { confirm: true },
},
},
// Variant E: snake_case minimal
{
cascade_id: sessionId,
interaction: {
run_command: { confirm: true },
},
},
];
for (let i = 0; i < protoVariants.length; i++) {
try {
const payload = protoVariants[i];
logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 200)})`);
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
logToFile(`[APPROVAL-PROTO-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
return `RPC-PROTO-${i}:HandleCascadeUserInteraction`;
}
catch (e) {
// Capture FULL error message (critical for diagnostics)
logToFile(`[APPROVAL-PROTO-${i}] ❌ ${e.message.substring(0, 300)}`);
}
}
}
// ══════════════════════════════════════════════════════════
// STRATEGY 0A: executeCascadeAction
// ══════════════════════════════════════════════════════════
const cascadeActionVariants = [
{ action: approved ? 'accept' : 'reject' },
{ action: approved ? 'ACCEPT' : 'REJECT' },
{ action: approved ? 'approve' : 'deny' },
{ action: approved ? 'run' : 'reject' },
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
{ cascadeId: sessionId, type: approved ? 'accept' : 'reject' },
approved ? 'accept' : 'reject', // plain string arg
approved ? 1 : 0, // numeric arg
];
for (let i = 0; i < cascadeActionVariants.length; i++) {
try {
const arg = cascadeActionVariants[i];
logToFile(`[APPROVAL-0A-${i}] executeCascadeAction(${JSON.stringify(arg)})`);
const result = await vscode.commands.executeCommand('antigravity.executeCascadeAction', arg);
logToFile(`[APPROVAL-0A-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
return `CMD-0A:executeCascadeAction(variant=${i})`;
}
catch (e) {
logToFile(`[APPROVAL-0A-${i}] ❌ ${e.message.substring(0, 100)}`);
}
}
// ══════════════════════════════════════════════════════════
// STRATEGY 0B: Direct approval commands (brute-force try all 7 SDK commands)
// ══════════════════════════════════════════════════════════
const commandsToTry = approved
? [
'antigravity.terminalCommand.run',
'antigravity.terminalCommand.accept',
'antigravity.agent.acceptAgentStep',
'antigravity.command.accept',
]
: [
'antigravity.terminalCommand.reject',
'antigravity.agent.rejectAgentStep',
'antigravity.command.reject',
];
for (const cmd of commandsToTry) {
// Try with no args, then with sessionId
for (const args of [[], [sessionId], [{ cascadeId: sessionId }]]) {
try {
logToFile(`[APPROVAL-0B] ${cmd}(${JSON.stringify(args)})`);
const result = await vscode.commands.executeCommand(cmd, ...args);
logToFile(`[APPROVAL-0B] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
return `CMD-0B:${cmd}`;
}
catch (e) {
logToFile(`[APPROVAL-0B] ❌ ${e.message.substring(0, 80)}`);
}
}
}
// ══════════════════════════════════════════════════════════
// STRATEGY 0C: Electron webContents access (pierce webview isolation)
// ══════════════════════════════════════════════════════════
try {
logToFile(`[APPROVAL-0C] Attempting Electron webContents access...`);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const electron = require('electron');
const remote = electron.remote;
if (remote) {
logToFile(`[APPROVAL-0C] electron.remote available!`);
const allWC = remote.webContents.getAllWebContents();
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
for (const wc of allWC) {
const wcUrl = wc.getURL() || '';
logToFile(`[APPROVAL-0C] wc: ${wcUrl.substring(0, 100)}`);
if (wcUrl.includes('vscode-webview://') || wcUrl.includes('webview')) {
const clickScript = approved
? `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Run|Accept|Allow/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`
: `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Reject|Deny|Cancel/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`;
const clickResult = await wc.executeJavaScript(clickScript);
logToFile(`[APPROVAL-0C] executeJavaScript result: ${clickResult}`);
if (clickResult && clickResult.startsWith('clicked:')) {
return `ELECTRON-0C:webContents(${clickResult})`;
}
}
}
}
else {
logToFile(`[APPROVAL-0C] electron.remote NOT available`);
// Try without remote (main process context)
const wc = electron.webContents;
if (wc && typeof wc.getAllWebContents === 'function') {
logToFile(`[APPROVAL-0C] Direct electron.webContents available!`);
const allWC = wc.getAllWebContents();
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
}
else {
logToFile(`[APPROVAL-0C] No webContents access from Extension Host`);
}
}
}
catch (e) {
logToFile(`[APPROVAL-0C] ❌ ${e.message.substring(0, 150)}`);
}
// ══════════════════════════════════════════════════════════
// STRATEGY 0D: sendChatActionMessage (may route to agent)
// ══════════════════════════════════════════════════════════
const chatActionVariants = [
{ action: approved ? 'accept' : 'reject', cascadeId: sessionId },
{ message: approved ? 'accept' : 'reject', conversationId: sessionId },
approved ? 'accept' : 'reject',
];
for (let i = 0; i < chatActionVariants.length; i++) {
try {
logToFile(`[APPROVAL-0D-${i}] sendChatActionMessage(${JSON.stringify(chatActionVariants[i])})`);
const result = await vscode.commands.executeCommand('antigravity.sendChatActionMessage', chatActionVariants[i]);
logToFile(`[APPROVAL-0D-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
return `CMD-0D:sendChatActionMessage(variant=${i})`;
}
catch (e) {
logToFile(`[APPROVAL-0D-${i}] ❌ ${e.message.substring(0, 80)}`);
}
}
// ══════════════════════════════════════════════════════════
// STRATEGY 1: HandleCascadeUserInteraction RPC (expanded variants)
// ══════════════════════════════════════════════════════════
if (sdk) {
const rpcVariants = [
// Original variants
{ cascadeId: sessionId, approved: approved },
{ cascadeId: sessionId, stepAction: approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT' },
{ cascadeId: sessionId, userAction: approved ? 'USER_ACTION_APPROVED' : 'USER_ACTION_REJECTED' },
// New variants — ConnectRPC protobuf field naming conventions
{ cascade_id: sessionId, accepted: approved },
{ cascadeId: sessionId, accepted: approved },
{ cascade_id: sessionId, user_action: approved ? 1 : 2 }, // enum as int
{ cascadeId: sessionId, interaction: approved ? 'ACCEPT' : 'REJECT' },
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
// With step index from last known waiting step
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, accepted: approved },
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, approved: approved },
];
for (let i = 0; i < rpcVariants.length; i++) {
try {
logToFile(`[APPROVAL-1-${i}] HandleCascadeUserInteraction(${JSON.stringify(rpcVariants[i]).substring(0, 120)})`);
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', rpcVariants[i]);
logToFile(`[APPROVAL-1-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
return `RPC-1-${i}:HandleCascadeUserInteraction`;
}
catch (e) {
logToFile(`[APPROVAL-1-${i}] ❌ ${e.message.substring(0, 80)}`);
}
}
}
// ── Strategy 2: Renderer DOM Click via HTTP Bridge (kept as fallback) ──
try {
const triggerAction = approved ? 'approve' : 'reject';
logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
clickTrigger = { action: triggerAction, timestamp: Date.now() };
logToFile(`[APPROVAL-2] ✅ clickTrigger set — renderer will poll and click within 2s`);
}
catch (e) {
logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
}
// ── Strategy 3: ResolveOutstandingSteps (REJECT ONLY — this CANCELS!) ──
if (!approved && sdk) {
try {
logToFile(`[APPROVAL-3] ResolveOutstandingSteps (REJECT/CANCEL only!)`);
const rpcResult = await sdk.ls.rawRPC('ResolveOutstandingSteps', {
cascadeId: sessionId,
});
logToFile(`[APPROVAL-3] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
return `RPC-3:ResolveOutstandingSteps(cancel)`;
}
catch (e) {
logToFile(`[APPROVAL-3] ❌ FAIL: ${e.message}`);
}
}
logToFile(`[APPROVAL] ⚠️ ALL strategies attempted — check logs for results`);
return `ALL_ATTEMPTED:${action}`;
}
// ─── 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) {
// ── Command Discovery Diagnostic ──
// Enumerate ALL antigravity.* commands to find correct approval command names
try {
const allCmds = await vscode.commands.getCommands(true);
const agCmds = allCmds.filter((c) => c.startsWith('antigravity.'));
logToFile(`[CMD-DISCOVERY] Total antigravity.* commands: ${agCmds.length}`);
// Log approval-related commands specifically
const approvalKeywords = ['accept', 'reject', 'approve', 'terminal', 'agent', 'cascade', 'step', 'run', 'command.'];
const relevantCmds = agCmds.filter((c) => approvalKeywords.some(kw => c.toLowerCase().includes(kw)));
logToFile(`[CMD-DISCOVERY] Approval-related commands (${relevantCmds.length}):`);
for (const cmd of relevantCmds) {
logToFile(`[CMD-DISCOVERY] → ${cmd}`);
}
// Also dump ALL commands for full reference
logToFile(`[CMD-DISCOVERY] ALL antigravity.* commands:`);
for (const cmd of agCmds) {
logToFile(`[CMD-DISCOVERY] ${cmd}`);
}
}
catch (e) {
logToFile(`[CMD-DISCOVERY] error: ${e.message}`);
}
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