Root cause: GetCascadeTrajectorySteps has 775-step hard limit, startStepIndex parameter is completely ignored (verified via direct RPC). Solution: GetAllCascadeTrajectories returns: - stepCount: real-time (verified 1413->1457 live) - latestNotifyUserStep: full notificationContent - latestTaskBoundaryStep: full taskName/Status/Summary - stepIndex on each for dedup E2E verified: Python script -> RPC -> snapshot -> Bot -> Discord
872 lines
41 KiB
JavaScript
872 lines
41 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"));
|
||
// 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: 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;
|
||
let activeSessionId = '';
|
||
let activeSessionTitle = '';
|
||
let lastKnownStepCount = 0;
|
||
let lastNotifyStepIndex = -1;
|
||
let lastTaskStepIndex = -1;
|
||
setInterval(async () => {
|
||
pollCount++;
|
||
try {
|
||
// Single RPC: GetAllCascadeTrajectories
|
||
const allTraj = await sdk.ls.rawRPC('GetAllCascadeTrajectories', {});
|
||
if (!allTraj?.trajectorySummaries)
|
||
return;
|
||
// Find the most recently modified session (or current active)
|
||
let bestSession = null;
|
||
let bestSessionId = '';
|
||
let bestModTime = '';
|
||
for (const [sid, data] of Object.entries(allTraj.trajectorySummaries)) {
|
||
const modTime = data.lastModifiedTime || '';
|
||
if (!bestSession || modTime > bestModTime) {
|
||
bestSession = data;
|
||
bestSessionId = sid;
|
||
bestModTime = modTime;
|
||
}
|
||
}
|
||
if (!bestSession)
|
||
return;
|
||
const currentCount = bestSession.stepCount || 0;
|
||
const currentTitle = (bestSession.summary || 'Untitled').substring(0, 50);
|
||
const isRunning = String(bestSession.status || '').includes('RUNNING');
|
||
// Session changed?
|
||
if (bestSessionId !== activeSessionId) {
|
||
activeSessionId = bestSessionId;
|
||
activeSessionTitle = currentTitle;
|
||
lastKnownStepCount = currentCount;
|
||
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
|
||
writeRegistration(activeSessionId);
|
||
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
|
||
return;
|
||
}
|
||
// No change in step count?
|
||
if (currentCount <= lastKnownStepCount && pollCount > 1) {
|
||
if (pollCount % 20 === 0) {
|
||
console.log(`Gravity Bridge: [POLL#${pollCount}] idle: ${activeSessionId.substring(0, 8)} steps=${currentCount}`);
|
||
}
|
||
return;
|
||
}
|
||
const delta = currentCount - lastKnownStepCount;
|
||
lastKnownStepCount = currentCount;
|
||
if (delta > 0) {
|
||
console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`);
|
||
}
|
||
// ── Process latestNotifyUserStep ──
|
||
const notifyStep = bestSession.latestNotifyUserStep;
|
||
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
|
||
lastNotifyStepIndex = notifyStep.stepIndex;
|
||
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
||
if (content.length > 10) {
|
||
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
||
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`);
|
||
}
|
||
}
|
||
// ── Process latestTaskBoundaryStep ──
|
||
const taskStep = bestSession.latestTaskBoundaryStep;
|
||
if (taskStep && taskStep.stepIndex > lastTaskStepIndex) {
|
||
lastTaskStepIndex = taskStep.stepIndex;
|
||
const tb = taskStep.step?.taskBoundary;
|
||
if (tb?.taskName) {
|
||
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
||
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
||
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
|
||
}
|
||
}
|
||
// ── Check for WAITING status (pending user approval) ──
|
||
if (isRunning) {
|
||
// Check lastUserInputStepIndex — if it's far behind stepCount,
|
||
// AI might be waiting for user input
|
||
const lastUserInput = bestSession.lastUserInputStepIndex || 0;
|
||
const gap = currentCount - lastUserInput;
|
||
// If gap is small and status is RUNNING, the AI might have a pending step
|
||
// We can check via GetCascadeTrajectorySteps for the last few steps (within 775 limit)
|
||
if (delta > 0 && gap < 50) {
|
||
try {
|
||
const stepsData = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||
cascadeId: activeSessionId
|
||
});
|
||
if (stepsData?.steps) {
|
||
const last5 = stepsData.steps.slice(-5);
|
||
for (const step of last5) {
|
||
const sType = String(step.type || '');
|
||
const sStatus = String(step.status || '');
|
||
if (sStatus.includes('WAITING')) {
|
||
const shortType = sType.replace(/CORTEX_STEP_TYPE_/g, '');
|
||
let cmd = `⏳ ${shortType}`;
|
||
let desc = `⏳ **대기 중**: ${shortType}`;
|
||
if (sType.includes('RUN_COMMAND')) {
|
||
const cmdLine = step.runCommand?.commandLine || '';
|
||
cmd = `▶️ ${cmdLine.substring(0, 80)}`;
|
||
desc = `▶️ **명령 실행 확인**\n\`\`\`\n${cmdLine}\n\`\`\``;
|
||
}
|
||
else if (sType.includes('CODE_ACTION') || sType.includes('WRITE')) {
|
||
const file = step.codeAction?.filePath || step.writeToFile?.filePath || '';
|
||
cmd = `✏️ 파일: ${file}`;
|
||
desc = `✏️ **파일 수정 확인**\n파일: \`${file}\``;
|
||
}
|
||
writePendingApproval({
|
||
conversation_id: activeSessionId,
|
||
command: cmd,
|
||
description: desc,
|
||
});
|
||
console.log(`Gravity Bridge: [POLL#${pollCount}] WAITING ${shortType}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (rpcErr) {
|
||
// GetCascadeTrajectorySteps failed — not critical
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (e) {
|
||
if (pollCount <= 5 || pollCount % 20 === 0) {
|
||
console.log(`Gravity Bridge: [POLL#${pollCount}] error: ${e.message}`);
|
||
}
|
||
}
|
||
}, 3000);
|
||
}
|
||
// ─── 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
|