Files
gravity_control/extension/out/extension.js
CD c3964f8e7a fix(bridge): rawRPC direct polling + SDK analysis docs + trial-and-error log
- Root cause: getDiagnostics.lastStepIndex is stale, SDK EventMonitor cannot detect real-time step changes
- Fix: Direct rawRPC('GetCascadeTrajectorySteps') polling every 5s
- Relay: PLANNER_RESPONSE, NOTIFY_USER, TASK_BOUNDARY, WAITING steps
- Added: docs/discord-bridge-analysis.md (full SDK architecture analysis)
- Added: docs/devlog/entries/20260308-003.md (trial-and-error history)
- Added: antigravity-sdk-main/ source reference
- Vikunja: #252 done, #253 created, #251 commented
2026-03-08 07:08:25 +09:00

862 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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"));
// 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 });
}
}
}
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`);
}
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;
}
}
// 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) {
if (registeredSessions.has(sessionId)) {
return;
}
registeredSessions.add(sessionId);
try {
const regDir = path.join(bridgePath, 'register');
if (!fs.existsSync(regDir)) {
fs.mkdirSync(regDir, { recursive: true });
}
const data = {
conversation_id: sessionId,
project_name: projectName,
timestamp: Date.now() / 1000,
};
fs.writeFileSync(path.join(regDir, `${sessionId}.json`), JSON.stringify(data, null, 2), 'utf-8');
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;
}
// Step count changed → fetch new steps via GetCascadeTrajectorySteps
sdk.monitor.onStepCountChanged(async (e) => {
console.log(`Gravity Bridge: [SDK] step changed: "${e.title}" step ${e.newCount} (+${e.delta})`);
// Auto-register session with Bot on first step event
writeRegistration(e.sessionId);
// ── ONE-TIME FULL STEP TYPE DUMP ──
if (!lastSeenStep.has(e.sessionId)) {
try {
const fullData = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: e.sessionId
});
if (fullData && Array.isArray(fullData.steps)) {
const typeCounts = new Map();
for (const step of fullData.steps) {
const t = (step.type || '').replace('CORTEX_STEP_TYPE_', '');
const s = (step.status || '').replace('CORTEX_STEP_STATUS_', '');
const dataKeys = Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k));
if (!typeCounts.has(t)) {
typeCounts.set(t, { count: 0, statuses: new Set(), keys: new Set(), sample: step });
}
const entry = typeCounts.get(t);
entry.count++;
entry.statuses.add(s);
for (const k of dataKeys) {
entry.keys.add(k);
}
}
console.log(`Gravity Bridge: ══════════════════════════════════════`);
console.log(`Gravity Bridge: FULL STEP TYPE MAP (${fullData.steps.length} total steps)`);
console.log(`Gravity Bridge: ══════════════════════════════════════`);
for (const [type, info] of typeCounts.entries()) {
console.log(`Gravity Bridge: [TYPE] ${type} ×${info.count} statuses=[${[...info.statuses].join(',')}] keys=[${[...info.keys].join(',')}]`);
// Dump ONE sample of each type
const s = info.sample;
const dataKeys = Object.keys(s).filter(k => !['type', 'status', 'metadata'].includes(k));
for (const k of dataKeys) {
const v = s[k];
const vStr = typeof v === 'object' ? JSON.stringify(v).substring(0, 150) : String(v).substring(0, 150);
console.log(`Gravity Bridge: .${k} (${typeof v}): ${vStr}`);
}
}
console.log(`Gravity Bridge: ══════════════════════════════════════`);
}
}
catch (e) {
console.log(`Gravity Bridge: full dump error: ${e.message}`);
}
}
try {
// IMPORTANT: Only fetch NEW steps, never re-fetch history
const fromStep = Math.max(lastSeenStep.get(e.sessionId) ?? e.newCount - e.delta, e.newCount - e.delta);
const stepsData = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: e.sessionId, startStepIndex: fromStep
});
lastSeenStep.set(e.sessionId, e.newCount);
if (stepsData && Array.isArray(stepsData.steps)) {
// API may ignore startStepIndex — only process the last e.delta steps
const allSteps = stepsData.steps;
const newSteps = allSteps.slice(-e.delta);
console.log(`Gravity Bridge: [SDK] processing ${newSteps.length} of ${allSteps.length} steps`);
let lastPlannerText = '';
for (const step of newSteps) {
const sType = step.type || '';
const sStatus = step.status || '';
const shortType = sType.replace('CORTEX_STEP_TYPE_', '');
const shortStatus = sStatus.replace('CORTEX_STEP_STATUS_', '');
// ── DIAGNOSTIC: log ALL step types (minimal) ──
console.log(`Gravity Bridge: [STEP] ${shortType} ${shortStatus}`);
// ══════════════════════════════════════════════
// ANY WAITING step → Pending Approval to Discord
// ══════════════════════════════════════════════
if (sStatus.includes('WAITING')) {
let description = '';
let command = '';
if (sType.includes('RUN_COMMAND')) {
const rc = step.runCommand || {};
command = rc.commandLine || rc.proposedCommandLine || '';
description = `💻 **명령 실행 요청**\n\`\`\`\n${command}\n\`\`\`\ncwd: ${rc.cwd || ''}`;
}
else if (sType.includes('EDIT_FILE') || sType.includes('CODE_EDIT') || sType.includes('CODE_ACTION') || sType.includes('WRITE_FILE') || sType.includes('FILE_EDIT')) {
// File edit/write/code action
const edit = step.codeEdit || step.editFile || step.writeFile || step.codeAction || {};
const filePath = edit.filePath || edit.targetFile || edit.path || '';
const desc = edit.description || edit.instruction || '';
command = `📝 파일 수정: ${filePath}`;
description = `📝 **파일 변경 확인**\n파일: \`${filePath}\`\n${desc ? `설명: ${desc}` : ''}`;
// Full dump for diagnostic
console.log(`Gravity Bridge: [STEP-DETAIL] ${shortType} WAITING keys=${JSON.stringify(Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k)))}`);
for (const k of Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k))) {
const v = step[k];
console.log(`Gravity Bridge: [STEP-DETAIL] .${k} = ${typeof v === 'object' ? JSON.stringify(v).substring(0, 200) : String(v).substring(0, 200)}`);
}
}
else if (sType.includes('FILE_ACCESS') || sType.includes('READ_FILE') || sType.includes('FILE_READ')) {
// File access permission
const fa = step.fileAccess || step.readFile || step.fileRead || {};
const filePath = fa.filePath || fa.path || '';
command = `📖 파일 접근: ${filePath}`;
description = `📖 **파일 접근 권한 요청**\n파일: \`${filePath}\``;
console.log(`Gravity Bridge: [STEP-DETAIL] ${shortType} WAITING keys=${JSON.stringify(Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k)))}`);
for (const k of Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k))) {
const v = step[k];
console.log(`Gravity Bridge: [STEP-DETAIL] .${k} = ${typeof v === 'object' ? JSON.stringify(v).substring(0, 200) : String(v).substring(0, 200)}`);
}
}
else {
// Unknown WAITING step — still relay it with full diagnostic
const stepKeys = Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k));
command = `${shortType}`;
description = `⏳ **대기 중: ${shortType}**\nkeys: ${stepKeys.join(', ')}`;
console.log(`Gravity Bridge: [STEP-DETAIL] UNKNOWN WAITING: ${shortType} keys=${JSON.stringify(stepKeys)}`);
for (const k of stepKeys) {
const v = step[k];
console.log(`Gravity Bridge: [STEP-DETAIL] .${k} = ${typeof v === 'object' ? JSON.stringify(v).substring(0, 200) : String(v).substring(0, 200)}`);
}
}
writePendingApproval({
conversation_id: e.sessionId,
command: command,
description: `${description}\n\n🏷️ ${e.title}`,
});
console.log(`Gravity Bridge: [SDK] ⏳ pending ${shortType}: "${command.substring(0, 80)}"`);
continue;
}
// ══════════════════════════════════════════════
// PLANNER_RESPONSE → AI text relay (COMPLETED/DONE)
// ══════════════════════════════════════════════
if (sType.includes('PLANNER_RESPONSE')) {
if (!sStatus.includes('COMPLETED') && !sStatus.includes('DONE')) {
continue;
}
const pr = step.plannerResponse;
const responseText = pr?.modifiedResponse || pr?.response || '';
if (responseText && typeof responseText === 'string' && responseText.length > 0) {
lastPlannerText = responseText;
console.log(`Gravity Bridge: [SDK] 📝 planner response found (${responseText.length} chars)`);
}
continue;
}
// ══════════════════════════════════════════════
// NOTIFY_USER → also relay as chat snapshot
// ══════════════════════════════════════════════
if (sType.includes('NOTIFY_USER') && (sStatus.includes('COMPLETED') || sStatus.includes('DONE'))) {
const nu = step.notifyUser;
const content = nu?.notificationContent || '';
if (content && content.length > 0) {
// Write NOTIFY_USER as snapshot too
writeChatSnapshot(`📣 **알림**\n\n${content}`);
console.log(`Gravity Bridge: [SDK] 📣 NOTIFY_USER relayed (${content.length} chars)`);
}
continue;
}
}
// Write the LAST planner response as snapshot (with dedup)
if (lastPlannerText && lastPlannerText !== lastSnapshotText.get(e.sessionId)) {
lastSnapshotText.set(e.sessionId, lastPlannerText);
writeChatSnapshot(`🤖 **${e.title}**\n\n${lastPlannerText}`);
console.log(`Gravity Bridge: [SDK] 💬 snapshot written (${lastPlannerText.length} chars)`);
}
return;
}
}
catch (err) {
console.log(`Gravity Bridge: [SDK] steps error: ${err.message}`);
}
// Fallback
lastSeenStep.set(e.sessionId, e.newCount);
});
// New conversation started
sdk.monitor.onNewConversation((e) => {
console.log(`Gravity Bridge: [SDK] new conversation: ${e.title}`);
writeRegistration(e.sessionId || e.id || '');
writeChatSnapshot(`🚀 **${e.title}** — 새 대화 시작`);
});
// Active session changed
sdk.monitor.onActiveSessionChanged((e) => {
console.log(`Gravity Bridge: [SDK] active session: "${e.title}" (${e.sessionId?.substring(0, 8)})`);
writeRegistration(e.sessionId || e.id || '');
});
// State changed (USS update)
sdk.monitor.onStateChanged((e) => {
console.log(`Gravity Bridge: [SDK] state changed: ${e.key}`);
});
// Start monitoring (USS every 3s, trajectory every 2s for faster detection)
sdk.monitor.start(3000, 2000);
console.log('Gravity Bridge: [SDK] monitor started (USS 3s, trajectory 2s)');
// ══════════════════════════════════════════════════════════════════════
// PRIMARY RELAY: Direct step polling via rawRPC (getDiagnostics is stale!)
// getDiagnostics.lastStepIndex does NOT update in real-time.
// Instead, we poll GetCascadeTrajectorySteps directly for the active session.
// ══════════════════════════════════════════════════════════════════════
let pollCount = 0;
let activeSessionId = '';
let activeSessionTitle = '';
let polledStepCount = 0;
setInterval(async () => {
pollCount++;
try {
// Phase 1: Discover active session (first time or periodically)
if (!activeSessionId || pollCount % 12 === 0) {
try {
const raw = await vscode.commands.executeCommand('antigravity.getDiagnostics');
if (raw && typeof raw === 'string') {
const diag = JSON.parse(raw);
if (Array.isArray(diag.recentTrajectories) && diag.recentTrajectories.length > 0) {
const first = diag.recentTrajectories[0];
if (first.googleAgentId && first.googleAgentId !== activeSessionId) {
activeSessionId = first.googleAgentId;
activeSessionTitle = first.summary || 'Untitled';
polledStepCount = first.lastStepIndex || 0;
console.log(`Gravity Bridge: [POLL#${pollCount}] 🎯 active session: ${activeSessionId.substring(0, 8)} "${activeSessionTitle}" steps=${polledStepCount}`);
writeRegistration(activeSessionId);
}
}
}
}
catch (e) {
if (pollCount <= 3) {
console.log(`Gravity Bridge: [POLL#${pollCount}] getDiag error: ${e.message}`);
}
}
if (!activeSessionId)
return;
}
// Phase 2: Fetch latest steps via rawRPC (RELIABLE, not stale!)
const stepsData = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: activeSessionId,
startStepIndex: polledStepCount > 2 ? polledStepCount - 2 : 0
});
if (!stepsData || !Array.isArray(stepsData.steps)) {
if (pollCount <= 3) {
console.log(`Gravity Bridge: [POLL#${pollCount}] no steps data`);
}
return;
}
const allSteps = stepsData.steps;
const currentMax = allSteps.length > 0
? Math.max(...allSteps.map((s) => s.stepIndex ?? s.index ?? 0), allSteps.length)
: polledStepCount;
if (currentMax <= polledStepCount) {
// No new steps — log every 12th poll
if (pollCount % 12 === 0) {
console.log(`Gravity Bridge: [POLL#${pollCount}] no change: ${activeSessionId.substring(0, 8)} steps=${currentMax}`);
}
return;
}
// Phase 3: New steps detected! Process them.
const delta = currentMax - polledStepCount;
console.log(`Gravity Bridge: [POLL#${pollCount}] 🆕 +${delta} steps (${polledStepCount}${currentMax}) "${activeSessionTitle}"`);
const newSteps = allSteps.slice(-delta);
polledStepCount = currentMax;
let lastPlannerText = '';
for (const step of newSteps) {
const sType = String(step.type || '');
const sStatus = String(step.status || '');
// PLANNER_RESPONSE → AI text (main content)
if (sType.includes('PLANNER_RESPONSE') && (sStatus.includes('DONE') || sStatus.includes('COMPLETED'))) {
const pr = step.plannerResponse;
const text = pr?.modifiedResponse || pr?.response || '';
if (text && text.length > 0) {
lastPlannerText = text;
console.log(`Gravity Bridge: [POLL#${pollCount}] 📝 planner ${text.length} chars`);
}
}
// NOTIFY_USER → user notification
if (sType.includes('NOTIFY_USER') && (sStatus.includes('DONE') || sStatus.includes('COMPLETED'))) {
const nu = step.notifyUser;
const content = nu?.notificationContent || '';
if (content && content.length > 0) {
writeChatSnapshot(`📣 **알림**\n\n${content}`);
console.log(`Gravity Bridge: [POLL#${pollCount}] 📣 notify ${content.length} chars`);
}
}
// TASK_BOUNDARY → task status update
if (sType.includes('TASK_BOUNDARY') && (sStatus.includes('DONE') || sStatus.includes('COMPLETED'))) {
const tb = step.taskBoundary;
if (tb?.taskName) {
writeChatSnapshot(`📋 **작업**: ${tb.taskName}\n상태: ${tb.taskStatus || ''}\n${tb.taskSummary || ''}`);
}
}
// WAITING steps → pending approval (any type)
if (sStatus.includes('WAITING')) {
const shortType = sType.replace(/CORTEX_STEP_TYPE_/g, '');
let command = `${shortType}`;
let description = `⏳ **대기 중**: ${shortType}`;
if (sType.includes('RUN_COMMAND')) {
const cmd = step.runCommand?.commandLine || '';
command = `▶️ ${cmd.substring(0, 80)}`;
description = `▶️ **명령 실행 확인**\n\`\`\`\n${cmd}\n\`\`\``;
}
else if (sType.includes('CODE_ACTION') || sType.includes('WRITE')) {
const file = step.codeAction?.filePath || step.writeToFile?.filePath || '';
command = `✏️ 파일 수정: ${file}`;
description = `✏️ **파일 수정 확인**\n파일: \`${file}\``;
}
writePendingApproval({
conversation_id: activeSessionId,
command: command,
description: description,
});
console.log(`Gravity Bridge: [POLL#${pollCount}] ⏳ ${shortType} WAITING`);
}
}
// Write latest planner response as snapshot (dedup)
if (lastPlannerText && lastPlannerText !== lastSnapshotText.get(activeSessionId)) {
lastSnapshotText.set(activeSessionId, lastPlannerText);
writeChatSnapshot(`🤖 **${activeSessionTitle}**\n\n${lastPlannerText}`);
console.log(`Gravity Bridge: [POLL#${pollCount}] 💬 snapshot ${lastPlannerText.length} chars`);
}
}
catch (e) {
if (pollCount <= 5 || pollCount % 12 === 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)) {
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);
console.log(`Gravity Bridge: [RESPONSE] request_id=${resp.request_id} approved=${resp.approved}`);
if (!sdk) {
console.log('Gravity Bridge: [RESPONSE] SDK not available');
return;
}
// Find matching pending request for session_id
const pendingDir = path.join(bridgePath, 'pending');
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
let sessionId = '';
if (fs.existsSync(pendingFile)) {
try {
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
sessionId = pending.conversation_id || '';
}
catch { }
}
if (sessionId && resp.approved) {
try {
await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
cascadeId: sessionId,
approved: true,
});
console.log('Gravity Bridge: [RESPONSE] ✅ approved via HandleCascadeUserInteraction');
}
catch (e) {
console.log(`Gravity Bridge: [RESPONSE] HandleCascadeUserInteraction failed: ${e.message}`);
try {
await sdk.ls.rawRPC('ResolveOutstandingSteps', { cascadeId: sessionId });
console.log('Gravity Bridge: [RESPONSE] ✅ approved via ResolveOutstandingSteps');
}
catch (e2) {
console.log(`Gravity Bridge: [RESPONSE] ResolveOutstandingSteps also failed: ${e2.message}`);
}
}
}
else {
console.log(`Gravity Bridge: [RESPONSE] ${resp.approved ? '✅' : '❌'} (session=${sessionId || 'unknown'})`);
}
try {
fs.unlinkSync(filePath);
}
catch { }
}
catch (e) {
console.log(`Gravity Bridge: [RESPONSE] error: ${e.message}`);
}
}
/**
* 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;
}
/** 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`);
}
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();
statusBar.text = '$(check) Bridge SDK';
statusBar.tooltip = `Gravity Bridge: ${projectName} (SDK 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;
if (sdk) {
sdk.monitor.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) {
sdk.monitor.stop();
sdk.dispose();
}
if (watcher) {
watcher.close();
}
if (commandsWatcher) {
commandsWatcher.close();
}
}
});
console.log('Gravity Bridge: ✅ activated');
isActive = true;
}
function deactivate() {
if (sdk) {
try {
sdk.monitor.stop();
sdk.dispose();
}
catch { }
}
}
//# sourceMappingURL=extension.js.map