Files
gravity_control/extension/out/extension.js

1078 lines
46 KiB
JavaScript

"use strict";
/**
* Gravity Bridge — VS Code Extension
*
* Bridges Antigravity IDE approval dialogs to the Discord bot via file-based protocol.
*
* Multi-project routing:
* - Each workspace has a project name (from settings or workspace folder name)
* - Extension only processes commands/responses matching its project_name
* - Pending approvals include project_name for Discord channel routing
*/
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.writePendingApproval = writePendingApproval;
exports.deactivate = deactivate;
const vscode = __importStar(require("vscode"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
let watcher = null;
let commandsWatcher = null;
let statusBar;
let bridgePath;
let projectName;
let isActive = false;
// Track pending approvals we've already sent
const sentPendingIds = new Set();
const cp = __importStar(require("child_process"));
/**
* Detect project name from workspace.
* Priority: settings > git remote repo name > workspace folder name
*/
function detectProjectName() {
const config = vscode.workspace.getConfiguration('gravityBridge');
const configName = config.get('projectName');
if (configName) {
return configName;
}
// Try git remote URL → extract repo name
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();
// "https://gitea.example.com/Variet/gravity_control.git" → "gravity_control"
// "git@github.com:user/repo.git" → "repo"
const match = remoteUrl.match(/\/([^\/]+?)(?:\.git)?$/);
if (match && match[1]) {
const repoName = match[1].toLowerCase().replace(/[\s\-]+/g, '_');
console.log(`Gravity Bridge: project from git remote → "${repoName}"`);
return repoName;
}
}
catch {
// No git or no remote — fall through
}
// Fallback: workspace folder name
return folders[0].name.toLowerCase().replace(/[\s\-]+/g, '_');
}
return 'unknown_project';
}
function activate(context) {
projectName = detectProjectName();
console.log(`Gravity Bridge: activating for project "${projectName}"...`);
// Determine bridge path
const config = vscode.workspace.getConfiguration('gravityBridge');
const configPath = config.get('bridgePath');
bridgePath = configPath || path.join(os.homedir(), '.gemini', 'antigravity', 'bridge');
// Ensure bridge directories exist
const dirs = ['pending', 'response', 'commands', 'register'];
for (const dir of dirs) {
const dirPath = path.join(bridgePath, dir);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
// Status bar
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
statusBar.command = 'gravityBridge.start';
statusBar.text = `$(radio-tower) ${projectName}: Off`;
statusBar.tooltip = `Gravity Bridge — ${projectName}`;
statusBar.show();
context.subscriptions.push(statusBar);
// Register commands
context.subscriptions.push(vscode.commands.registerCommand('gravityBridge.start', startBridge), vscode.commands.registerCommand('gravityBridge.stop', stopBridge), vscode.commands.registerCommand('gravityBridge.connect', connectSession), vscode.commands.registerCommand('gravityBridge.approve', () => handleManualAction(true)), vscode.commands.registerCommand('gravityBridge.reject', () => handleManualAction(false)));
// === LS ConnectRPC Bridge: Relay AI responses to Discord ===
let lsPort = null;
let lsCsrf = '';
let lsPid = null;
let lsUseTls = false; // track detected protocol
let lastStepIndex = {}; // cascadeId → last known step index
async function discoverLS() {
return new Promise((resolve) => {
// Phase 1: Find LS process → PID + CSRF token
cp.exec('powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object {$_.Name -eq \'language_server_exe.exe\' -or ($_.CommandLine -and $_.CommandLine -like \'*language_server*\' -and $_.CommandLine -notlike \'*powershell*\')} | Select-Object ProcessId, CommandLine | ConvertTo-Json"', { maxBuffer: 2 * 1024 * 1024 }, (err, stdout) => {
if (err || !stdout.trim()) {
console.log(`Gravity Bridge: [LS] process not found`);
resolve(false);
return;
}
try {
let procs = JSON.parse(stdout.trim());
if (!Array.isArray(procs)) {
procs = [procs];
}
// Find a process with csrf_token in command line
for (const proc of procs) {
const cmd = proc.CommandLine || '';
const csrfM = cmd.match(/--csrf_token[= ]([^\s"]+)/);
if (csrfM) {
lsPid = proc.ProcessId;
lsCsrf = csrfM[1];
console.log(`Gravity Bridge: [LS] PID=${lsPid}, CSRF=${lsCsrf.substring(0, 12)}...`);
break;
}
}
}
catch (e) {
console.log(`Gravity Bridge: [LS] parse error: ${e}`);
}
if (!lsPid || !lsCsrf) {
resolve(false);
return;
}
// Phase 2: netstat → find LS listening ports
cp.exec(`netstat -ano | findstr "LISTENING" | findstr " ${lsPid}"`, { maxBuffer: 512 * 1024 }, async (err2, stdout2) => {
if (err2 || !stdout2.trim()) {
console.log(`Gravity Bridge: [LS] no listening ports found for PID ${lsPid}`);
resolve(false);
return;
}
// Parse ports
const ports = [];
for (const line of stdout2.split('\n')) {
const m = line.match(/:(\d+)\s+.*LISTENING/);
if (m) {
ports.push(parseInt(m[1]));
}
}
const uniquePorts = [...new Set(ports)].sort((a, b) => a - b);
console.log(`Gravity Bridge: [LS] ports for PID ${lsPid}: ${uniquePorts.join(', ')}`);
// Try ConnectRPC probe on each port (HTTP first, then HTTPS)
for (const port of uniquePorts) {
const ok = await probeLSPort(port);
if (ok) {
lsPort = port;
console.log(`Gravity Bridge: [LS] ✅ ConnectRPC active on port ${port}`);
resolve(true);
return;
}
}
console.log(`Gravity Bridge: [LS] no ConnectRPC port responded`);
resolve(false);
});
});
});
}
function probeLSPort(port) {
return new Promise((resolve) => {
const http = require('http');
const https = require('https');
// Try HTTP first (extension_server uses HTTP)
const tryProto = (proto, useTls) => {
const req = proto.request({
hostname: '127.0.0.1',
port,
path: '/exa.language_server_pb.LanguageServerService/Heartbeat',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-codeium-csrf-token': lsCsrf,
},
rejectUnauthorized: false,
timeout: 2000,
}, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
console.log(`Gravity Bridge: [LS] port ${port} (${useTls ? 'https' : 'http'}) status=${res.statusCode} body=${data.substring(0, 200)}`);
// If HTTP got "HTTPS server" response, retry with HTTPS
if (!useTls && data.includes('HTTPS server')) {
tryProto(https, true);
return;
}
if (res.statusCode !== 404) {
lsUseTls = useTls; // remember which protocol worked
}
resolve(res.statusCode !== 404);
});
});
req.on('error', () => {
if (!useTls) {
// Try HTTPS
tryProto(https, true);
}
else {
resolve(false);
}
});
req.on('timeout', () => { req.destroy(); resolve(false); });
req.write('{}');
req.end();
};
tryProto(http, false);
});
}
async function lsRPC(method, payload = {}) {
if (!lsPort || !lsCsrf) {
return null;
}
return new Promise((resolve) => {
const http = require('http');
const https = require('https');
const proto = lsUseTls ? https : http; // use detected protocol
const body = JSON.stringify(payload);
const req = proto.request({
hostname: '127.0.0.1',
port: lsPort,
path: `/exa.language_server_pb.LanguageServerService/${method}`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-codeium-csrf-token': lsCsrf,
'Content-Length': Buffer.byteLength(body),
},
rejectUnauthorized: false,
timeout: 5000,
}, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
resolve(JSON.parse(data));
}
catch {
resolve(data);
}
});
});
req.on('error', (e) => {
console.log(`Gravity Bridge: [LS RPC] ${method} error: ${e.message}`);
resolve(null);
});
req.on('timeout', () => { req.destroy(); resolve(null); });
req.write(body);
req.end();
});
}
let pollFailCount = 0;
let pollCount = 0;
async function pollConversations() {
if (!lsPort) {
return;
}
if (pollFailCount > 10) {
return;
} // stop after repeated failures
pollCount++;
try {
// Use getDiagnostics to get cascade-level conversation IDs
const diag = await vscode.commands.executeCommand('antigravity.getDiagnostics');
if (!diag) {
return;
}
const parsed = typeof diag === 'string' ? JSON.parse(diag) : diag;
const trajectories = parsed.recentTrajectories || [];
if (!Array.isArray(trajectories) || trajectories.length === 0) {
return;
}
const isFirstPoll = Object.keys(lastStepIndex).length === 0;
if (isFirstPoll) {
console.log(`Gravity Bridge: [LS] ${trajectories.length} trajectories from getDiagnostics`);
}
// Periodic debug every ~1 min (12 * 5s)
if (pollCount % 12 === 0) {
const summary = trajectories.map((t) => {
const id = (t.googleAgentId || '').substring(0, 8);
return `${id}:s${t.lastStepIndex ?? '?'}`;
}).join(', ');
console.log(`Gravity Bridge: [LS] poll#${pollCount}${trajectories.length} trajs: [${summary}]`);
}
// Check ALL trajectories for step count changes
for (const traj of trajectories) {
const agentId = traj.googleAgentId || '';
const trajId = traj.trajectoryId || '';
const stepIdx = traj.lastStepIndex ?? 0;
const summary = traj.summary || '';
if (!agentId && !trajId) {
continue;
}
const key = agentId || trajId;
const prev = lastStepIndex[key];
if (prev === undefined) {
// First time seeing this trajectory — initialize
lastStepIndex[key] = stepIdx;
if (isFirstPoll) {
console.log(`Gravity Bridge: [LS] init ${key.substring(0, 8)} at step ${stepIdx} "${summary.substring(0, 30)}"`);
}
else if (stepIdx > 0 && summary) {
// New conversation with AI response!
console.log(`Gravity Bridge: [LS] NEW conversation ${key.substring(0, 8)} at step ${stepIdx} "${summary.substring(0, 40)}"`);
// Try to extract AI text from extensionLogs
const aiText = extractFromLogs(parsed);
if (aiText) {
writeChatSnapshot(aiText);
console.log(`Gravity Bridge: [LS] → relayed AI text (${aiText.length} chars) from logs`);
}
else {
writeChatSnapshot(`**${summary}**\n\n(새 대화, step ${stepIdx})`);
console.log(`Gravity Bridge: [LS] → summary fallback to Discord`);
}
lastStepIndex[key + '_summary'] = summary;
}
continue;
}
if (stepIdx > prev) {
// Existing conversation has new steps
console.log(`Gravity Bridge: [LS] ${key.substring(0, 8)} steps: ${prev}${stepIdx} "${summary.substring(0, 40)}"`);
// Try to extract AI text from extensionLogs
const aiText = extractFromLogs(parsed);
if (aiText) {
writeChatSnapshot(aiText);
console.log(`Gravity Bridge: [LS] → relayed AI text (${aiText.length} chars)`);
}
else if (summary && summary !== lastStepIndex[key + '_summary']) {
// Summary changed = new topic
writeChatSnapshot(`**${summary}**\n\n(step ${prev}${stepIdx})`);
console.log(`Gravity Bridge: [LS] → summary change relayed`);
}
lastStepIndex[key + '_summary'] = summary;
lastStepIndex[key] = stepIdx;
}
}
}
catch (e) {
console.log(`Gravity Bridge: [LS poll] error: ${e}`);
}
}
function extractAndRelaySteps(steps) {
const messages = [];
for (const step of steps) {
// Try every possible way to find AI text in a step
const type = step.type || step.stepType || step.step_type || '';
const content = step.content || step.summary || step.text || step.message || '';
// PlannerResponse = AI's text output to user
if (content && (type.includes('Response') || type.includes('response') ||
type.includes('Message') || type.includes('message') ||
type.includes('Notify') || type.includes('notify'))) {
messages.push(content);
continue;
}
// Check nested data/content
if (step.data?.content && typeof step.data.content === 'string') {
messages.push(step.data.content);
continue;
}
// Check role-based (assistant messages)
if ((step.role === 'assistant' || step.role === 'model') && content) {
messages.push(content);
continue;
}
// Catch-all: any string content longer than 20 chars that's not a tool call
if (typeof content === 'string' && content.length > 20 &&
!type.includes('Tool') && !type.includes('tool') &&
!type.includes('Command') && !type.includes('command')) {
messages.push(content);
}
}
if (messages.length > 0) {
const combined = messages.join('\n\n---\n\n');
writeChatSnapshot(combined);
console.log(`Gravity Bridge: [LS] relayed ${messages.length} response(s) (${combined.length} chars) to Discord`);
}
}
function extractFromLogs(diagData) {
// Try to find AI response text in extension logs
const logs = diagData.extensionLogs || diagData.extension_logs || '';
if (!logs || typeof logs !== 'string' || logs.length < 10) {
return null;
}
// Look for patterns that indicate AI response text
// Pattern 1: notify_user Message content
const notifyMatch = logs.match(/notify_user.*?"Message"\s*:\s*"([^"]{20,})"/s);
if (notifyMatch) {
return notifyMatch[1];
}
// Pattern 2: "content": "..." blocks from assistant role
const contentMatches = logs.match(/"content"\s*:\s*"([^"]{50,})"/g);
if (contentMatches && contentMatches.length > 0) {
const lastContent = contentMatches[contentMatches.length - 1];
const m = lastContent.match(/"content"\s*:\s*"(.+)"/);
if (m) {
return m[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
}
}
// Pattern 3: Look for Korean text blocks (likely user-facing response)
const koreanBlocks = logs.match(/[\uAC00-\uD7A3]{10,}[^"]{0,200}/g);
if (koreanBlocks && koreanBlocks.length > 0) {
return koreanBlocks[koreanBlocks.length - 1].substring(0, 500);
}
return null;
}
// ========== Trial D2: Streaming RPC with protocol_version=1 ==========
setTimeout(async () => {
if (!lsPort || !lsCsrf) {
console.log('Gravity Bridge: [Trial D2] Skipped — no LS');
return;
}
console.log(`Gravity Bridge: [Trial D2] Streaming RPCs with proto_version=1...`);
const http = require('http');
function tryProtoRPC(method, protoBody, timeout = 12000) {
return new Promise((resolve) => {
// ConnectRPC frame: [flag(1)] [length(4 big-endian)] [message]
const frame = Buffer.alloc(5 + protoBody.length);
frame[0] = 0x00; // no compression
frame.writeUInt32BE(protoBody.length, 1);
protoBody.copy(frame, 5);
const req = http.request({
hostname: '127.0.0.1', port: lsPort,
path: `/exa.language_server_pb.LanguageServerService/${method}`,
method: 'POST',
headers: {
'Content-Type': 'application/connect+proto',
'Connect-Protocol-Version': '1',
'x-codeium-csrf-token': lsCsrf,
},
timeout: timeout
}, (res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
const buf = Buffer.concat(chunks);
// Skip ConnectRPC frame header (5 bytes) to get proto/JSON content
const text = buf.toString('utf-8');
resolve(text);
});
});
req.on('error', (e) => resolve(`err:${e.message}`));
req.on('timeout', () => { req.destroy(); resolve('timeout(stream open)'); });
req.write(frame);
req.end();
});
}
// Get latest trajectory ID first, then try streams WITH cascadeId
try {
const dRaw = await vscode.commands.executeCommand('antigravity.getDiagnostics');
const d = typeof dRaw === 'string' ? JSON.parse(dRaw) : dRaw;
const ts = d?.recentTrajectories || [];
if (ts.length > 0) {
const latest = ts[ts.length - 1];
const gid = latest.googleAgentId || '';
console.log(`Gravity Bridge: [Trial D3] Using cascade ${gid.substring(0, 8)} step=${latest.lastStepIndex}`);
const gidBuf = Buffer.from(gid, 'utf-8');
// Attempt 1: field1=version(varint=1), field2=cascadeId(string)
// 0x08 0x01 0x12 <len> <cascadeId>
const proto1 = Buffer.alloc(2 + 2 + gidBuf.length);
proto1[0] = 0x08;
proto1[1] = 0x01; // field1 varint = 1
proto1[2] = 0x12;
proto1[3] = gidBuf.length; // field2 string
gidBuf.copy(proto1, 4);
let r = await tryProtoRPC('StreamCascadeReactiveUpdates', proto1);
console.log(`Gravity Bridge: [Trial D3] StreamCascade(v1+cascade): ${r.substring(0, 800)}`);
// Attempt 2: field1=cascadeId(string), field2=version(varint=1)
// 0x0A <len> <cascadeId> 0x10 0x01
const proto2 = Buffer.alloc(2 + gidBuf.length + 2);
proto2[0] = 0x0A;
proto2[1] = gidBuf.length; // field1 string
gidBuf.copy(proto2, 2);
proto2[2 + gidBuf.length] = 0x10; // field2 varint
proto2[3 + gidBuf.length] = 0x01;
r = await tryProtoRPC('StreamCascadeReactiveUpdates', proto2);
console.log(`Gravity Bridge: [Trial D3] StreamCascade(cascade+v1): ${r.substring(0, 800)}`);
// Attempt 3: StreamCascadeSummariesReactiveUpdates with version+cascadeId
r = await tryProtoRPC('StreamCascadeSummariesReactiveUpdates', proto1);
console.log(`Gravity Bridge: [Trial D3] StreamSummaries(v1+cascade): ${r.substring(0, 800)}`);
// Attempt 4: GetCascadeTrajectorySteps with proto
const stepIdx = Math.max(0, latest.lastStepIndex - 1);
const stepsProto = Buffer.alloc(2 + gidBuf.length + 2);
stepsProto[0] = 0x0A;
stepsProto[1] = gidBuf.length;
gidBuf.copy(stepsProto, 2);
stepsProto[2 + gidBuf.length] = 0x10;
stepsProto[3 + gidBuf.length] = stepIdx;
r = await tryProtoRPC('GetCascadeTrajectorySteps', stepsProto);
console.log(`Gravity Bridge: [Trial D3] Steps(proto): ${r.substring(0, 1000)}`);
}
}
catch (e) {
console.log(`Gravity Bridge: [Trial D3] err: ${e.message}`);
}
}, 15000);
// Start LS bridge after a delay
setTimeout(async () => {
const found = await discoverLS();
if (found) {
console.log(`Gravity Bridge: [LS] bridge active — polling every 5s`);
// Initialize step counts
await pollConversations();
// Start polling loop
setInterval(pollConversations, 5000);
}
else {
console.log(`Gravity Bridge: [LS] bridge NOT available — AI responses won't relay`);
}
}, 8000);
// Chat document change listener — captures AI text responses
context.subscriptions.push(vscode.workspace.onDidChangeTextDocument((event) => {
handleChatDocumentChange(event);
}));
// Register @bridge Chat Participant for history relay
try {
const participant = vscode.chat.createChatParticipant('gravity-bridge.gravity', bridgeChatHandler);
participant.iconPath = new vscode.ThemeIcon('radio-tower');
context.subscriptions.push(participant);
console.log('Gravity Bridge: @bridge chat participant registered');
}
catch (err) {
console.log('Gravity Bridge: chat participant API not available (OK)');
}
// Auto-watch brain/ for new conversations → auto-register
watchBrainForNewSessions();
// Auto-start
startBridge();
}
function startBridge() {
if (isActive) {
vscode.window.showInformationMessage(`Gravity Bridge (${projectName}): already running`);
return;
}
isActive = true;
statusBar.text = `$(radio-tower) ${projectName}: On`;
statusBar.tooltip = `Gravity Bridge — ${projectName} (Active)`;
statusBar.command = 'gravityBridge.stop';
// Watch bridge/response/ for Discord user responses
const responsePath = path.join(bridgePath, 'response');
const processedFiles = new Set(); // Debounce
try {
watcher = fs.watch(responsePath, { persistent: false }, (eventType, filename) => {
if (filename && filename.endsWith('.json') && !processedFiles.has(filename)) {
processedFiles.add(filename);
setTimeout(() => processedFiles.delete(filename), 2000);
handleResponse(path.join(responsePath, filename));
}
});
console.log('Gravity Bridge: watching response directory');
}
catch (err) {
console.error('Gravity Bridge: failed to watch response dir', err);
}
// Watch for commands (user text input from Discord)
const commandsPath = path.join(bridgePath, 'commands');
try {
commandsWatcher = fs.watch(commandsPath, { persistent: false }, (eventType, filename) => {
if (filename && filename.endsWith('.json') && !processedFiles.has(filename)) {
processedFiles.add(filename);
setTimeout(() => processedFiles.delete(filename), 2000);
handleCommand(path.join(commandsPath, filename));
}
});
console.log('Gravity Bridge: watching commands directory');
}
catch (err) {
console.error('Gravity Bridge: failed to watch commands dir', err);
}
vscode.window.showInformationMessage(`Gravity Bridge (${projectName}): Started`);
console.log(`Gravity Bridge: started for project "${projectName}", bridge: ${bridgePath}`);
}
function stopBridge() {
if (!isActive) {
return;
}
isActive = false;
statusBar.text = `$(radio-tower) ${projectName}: Off`;
statusBar.tooltip = `Gravity Bridge — ${projectName}`;
statusBar.command = 'gravityBridge.start';
if (watcher) {
watcher.close();
watcher = null;
}
if (commandsWatcher) {
commandsWatcher.close();
commandsWatcher = null;
}
vscode.window.showInformationMessage(`Gravity Bridge (${projectName}): Stopped`);
}
/**
* Handle a response from Discord (approve/reject).
* Only processes responses — no project filtering needed since request_id is unique.
*/
async function handleResponse(filePath) {
try {
await new Promise(resolve => setTimeout(resolve, 200));
if (!fs.existsSync(filePath)) {
return;
}
const content = fs.readFileSync(filePath, 'utf-8');
const response = JSON.parse(content);
if (response.approved === undefined) {
return;
}
console.log(`Gravity Bridge [${projectName}]: response — approved=${response.approved}`);
if (response.approved) {
await simulateApproval();
vscode.window.showInformationMessage(`✅ Approved: ${response.request_id}`);
}
else {
await simulateRejection();
vscode.window.showInformationMessage(`❌ Rejected: ${response.request_id}`);
}
try {
fs.unlinkSync(filePath);
}
catch (e) { /* ignore */ }
}
catch (err) {
console.error('Gravity Bridge: error handling response', err);
}
}
/**
* Handle a text command from Discord.
* ONLY processes commands matching this project's name.
*/
async function handleCommand(filePath) {
try {
await new Promise(resolve => setTimeout(resolve, 200));
if (!fs.existsSync(filePath)) {
return;
}
const content = fs.readFileSync(filePath, 'utf-8');
const command = JSON.parse(content);
if (command.consumed || !command.text) {
return;
}
// ★ PROJECT FILTER — only process commands for THIS project
const cmdProject = command.project_name || '';
if (cmdProject && cmdProject !== projectName) {
console.log(`Gravity Bridge [${projectName}]: skipping command for "${cmdProject}"`);
return; // Not for us — leave file for the correct Extension instance
}
const text = command.text.trim();
console.log(`Gravity Bridge [${projectName}]: command — "${text.substring(0, 50)}"`);
// Special command: !stop — cancel AI work
if (text === '!stop') {
try {
await vscode.commands.executeCommand('workbench.action.chat.stop');
vscode.window.showWarningMessage(`⏹️ [${projectName}] AI 작업 중지됨`);
}
catch {
vscode.window.showErrorMessage('AI 중지 명령 실행 실패');
}
command.consumed = true;
fs.writeFileSync(filePath, JSON.stringify(command, null, 2), 'utf-8');
return;
}
// Special command: auto-approve toggle
if (text === '!auto on' || text === '!auto off') {
const enabled = text === '!auto on';
await toggleAutoApprove(enabled);
command.consumed = true;
fs.writeFileSync(filePath, JSON.stringify(command, null, 2), 'utf-8');
return;
}
// General text: send directly to Antigravity agent panel
try {
await vscode.commands.executeCommand('antigravity.sendPromptToAgentPanel', command.text);
console.log(`Gravity Bridge: ✅ sent via sendPromptToAgentPanel`);
}
catch (e1) {
console.log(`Gravity Bridge: sendPromptToAgentPanel failed: ${e1}`);
// Fallback: try sendChatActionMessage
try {
await vscode.commands.executeCommand('antigravity.sendChatActionMessage', command.text);
console.log(`Gravity Bridge: ✅ sent via sendChatActionMessage`);
}
catch (e2) {
console.log(`Gravity Bridge: sendChatActionMessage failed: ${e2}`);
// Last resort: focus panel + clipboard paste
try {
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
await new Promise(resolve => setTimeout(resolve, 300));
const oldClip = await vscode.env.clipboard.readText();
await vscode.env.clipboard.writeText(command.text);
await vscode.commands.executeCommand('editor.action.clipboardPasteAction');
await vscode.env.clipboard.writeText(oldClip);
console.log('Gravity Bridge: clipboard paste fallback');
}
catch (e3) {
console.error('Gravity Bridge: all methods failed', e3);
}
}
}
// Always mark as consumed
command.consumed = true;
fs.writeFileSync(filePath, JSON.stringify(command, null, 2), 'utf-8');
}
catch (err) {
console.error('Gravity Bridge: error handling command', err);
}
}
/**
* Toggle Antigravity's auto-approve settings.
*/
async function toggleAutoApprove(enabled) {
const config = vscode.workspace.getConfiguration();
try {
await config.update('chat.tools.autoApprove', enabled, vscode.ConfigurationTarget.Global);
await config.update('chat.agent.autoApprove', enabled, vscode.ConfigurationTarget.Global);
if (enabled) {
await config.update('chat.tools.terminal.enableAutoApprove', true, vscode.ConfigurationTarget.Global);
}
await config.update('autoAcceptV2.autoAcceptFileEdits', enabled, vscode.ConfigurationTarget.Global);
statusBar.text = enabled
? `$(radio-tower) ${projectName}: Auto ✅`
: `$(radio-tower) ${projectName}: Manual 🔒`;
const mode = enabled ? '자동 승인 ON 🟢' : '수동 승인 OFF 🔴';
vscode.window.showInformationMessage(`Gravity Bridge (${projectName}): ${mode}`);
const statusPath = path.join(bridgePath, 'commands', `auto-status-${Date.now()}.json`);
fs.writeFileSync(statusPath, JSON.stringify({
id: `auto-status-${Date.now()}`,
project_name: projectName,
text: `[SYSTEM] Auto-approve: ${enabled ? 'ON' : 'OFF'}`,
timestamp: Date.now() / 1000,
consumed: true,
auto_approve: enabled,
}, null, 2), 'utf-8');
}
catch (err) {
console.error('Gravity Bridge: failed to toggle auto-approve', err);
}
}
async function simulateApproval() {
try {
await vscode.commands.executeCommand('workbench.action.acceptSelectedCodeAction');
}
catch {
try {
await vscode.commands.executeCommand('type', { text: '\n' });
}
catch {
await vscode.commands.executeCommand('workbench.action.terminal.focus');
}
}
}
async function simulateRejection() {
try {
await vscode.commands.executeCommand('workbench.action.closeQuickOpen');
}
catch {
try {
await vscode.commands.executeCommand('cancelSelection');
}
catch {
console.log('Gravity Bridge: rejection sent but no active dialog found');
}
}
}
/**
* Manual approve/reject from command palette.
*/
function handleManualAction(approved) {
const requestId = `manual-${Date.now()}`;
const responsePath = path.join(bridgePath, 'response', `${requestId}.json`);
const response = {
request_id: requestId,
approved: approved,
user_input: '',
timestamp: Date.now() / 1000,
};
fs.writeFileSync(responsePath, JSON.stringify(response, null, 2), 'utf-8');
if (approved) {
simulateApproval();
}
else {
simulateRejection();
}
}
/**
* Write a pending approval request to bridge/pending/ for Discord bot to pick up.
* Includes project_name for correct channel routing.
*/
function writePendingApproval(conversationId, command, description) {
const requestId = `req-${Date.now()}`;
const pendingPath = path.join(bridgePath, 'pending', `${requestId}.json`);
const request = {
request_id: requestId,
conversation_id: conversationId,
project_name: projectName, // ★ Project routing
command: command,
description: description,
timestamp: Date.now() / 1000,
status: 'pending',
discord_message_id: 0,
};
fs.writeFileSync(pendingPath, JSON.stringify(request, null, 2), 'utf-8');
sentPendingIds.add(requestId);
console.log(`Gravity Bridge [${projectName}]: pending approval — ${requestId}`);
return requestId;
}
/**
* Register a conversation → project mapping in bridge/register/.
* The bot reads these files to route brain events to the correct channel.
*/
function registerConversation(conversationId) {
const registerDir = path.join(bridgePath, 'register');
if (!fs.existsSync(registerDir)) {
fs.mkdirSync(registerDir, { recursive: true });
}
const filePath = path.join(registerDir, `${conversationId}.json`);
const data = {
conversation_id: conversationId,
project_name: projectName,
timestamp: Date.now() / 1000,
};
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
console.log(`Gravity Bridge [${projectName}]: registered ${conversationId.substring(0, 8)}`);
}
/**
* Read the title (first # heading) from a conversation's task.md or implementation_plan.md.
*/
function getConversationTitle(convDir) {
for (const fname of ['task.md', 'implementation_plan.md']) {
const fpath = path.join(convDir, fname);
if (fs.existsSync(fpath)) {
try {
const lines = fs.readFileSync(fpath, 'utf-8').split('\n').slice(0, 5);
for (const line of lines) {
const match = line.match(/^#\s+(.+)/);
if (match) {
return match[1].trim().substring(0, 50);
}
}
}
catch { /* ignore */ }
}
}
return '';
}
/**
* Manual connect: scan brain/ for recent conversations and let user pick.
* Shows task.md titles for readability. Offers auto-connect for new projects.
*/
async function connectSession() {
const brainPath = path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
if (!fs.existsSync(brainPath)) {
vscode.window.showErrorMessage('Brain 디렉토리를 찾을 수 없습니다.');
return;
}
// Get conversation dirs sorted by modification time (newest first)
const dirs = fs.readdirSync(brainPath)
.filter(d => {
const fullPath = path.join(brainPath, d);
return fs.statSync(fullPath).isDirectory() && d.includes('-');
})
.map(d => {
const fullPath = path.join(brainPath, d);
return {
name: d,
mtime: fs.statSync(fullPath).mtimeMs,
title: getConversationTitle(fullPath),
};
})
.sort((a, b) => b.mtime - a.mtime)
.slice(0, 10);
// Build QuickPick items
const items = [];
// Always offer auto-connect option first
items.push({
label: '$(sync) 새 대화 자동 연결',
description: '다음에 시작하는 대화가 자동으로 이 프로젝트에 연결됩니다',
detail: `프로젝트: ${projectName}`,
});
// Add conversation items with titles
for (const d of dirs) {
const titleLabel = d.title || '(제목 없음)';
const timeStr = new Date(d.mtime).toLocaleString();
items.push({
label: `$(comment-discussion) ${titleLabel}`,
description: d.name.substring(0, 8),
detail: `${d.name} · ${timeStr}`,
convId: d.name,
});
}
const selected = await vscode.window.showQuickPick(items, {
placeHolder: `프로젝트 "${projectName}"에 연결할 세션을 선택하세요`,
});
if (!selected) {
return;
}
if (!('convId' in selected) || !selected.convId) {
// Auto-connect mode
vscode.window.showInformationMessage(`🔄 ${projectName}: 다음 대화가 자동으로 연결됩니다`);
return;
}
registerConversation(selected.convId);
vscode.window.showInformationMessage(`${selected.description}${projectName} 연결됨`);
}
/**
* Auto-watch brain/ for new conversation directories → auto-register.
*/
function watchBrainForNewSessions() {
const brainPath = path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
if (!fs.existsSync(brainPath)) {
return;
}
// Track known dirs
const knownDirs = new Set(fs.readdirSync(brainPath).filter(d => fs.statSync(path.join(brainPath, d)).isDirectory()));
try {
fs.watch(brainPath, { persistent: false }, (eventType, filename) => {
if (!filename || !filename.includes('-')) {
return;
}
const fullPath = path.join(brainPath, filename);
// Check if it's a new directory
if (!knownDirs.has(filename) && fs.existsSync(fullPath) &&
fs.statSync(fullPath).isDirectory()) {
knownDirs.add(filename);
registerConversation(filename);
console.log(`Gravity Bridge [${projectName}]: auto-registered new session ${filename.substring(0, 8)}`);
}
});
console.log(`Gravity Bridge [${projectName}]: watching brain/ for new sessions`);
}
catch (err) {
console.error('Gravity Bridge: failed to watch brain dir', err);
}
}
/**
* Monitor text document changes for chat panel content.
* VS Code chat documents have special URI schemes (vscode-chat-response, etc.).
* We capture significant changes and relay to Discord.
*/
let lastChatContent = '';
let chatDebounceTimer = null;
function handleChatDocumentChange(event) {
const doc = event.document;
const scheme = doc.uri.scheme;
// Log ALL schemes to discover chat-related ones (debug mode)
if (scheme !== 'file' && scheme !== 'git' && scheme !== 'output' &&
scheme !== 'vscode-userdata' && scheme !== 'untitled') {
console.log(`Gravity Bridge [${projectName}]: doc change scheme="${scheme}" uri="${doc.uri.toString().substring(0, 80)}"`);
}
// Capture chat-related documents
// Known chat schemes: vscode-chat-response, vscode-copilot-chat, etc.
const isChatDoc = scheme.includes('chat') || scheme.includes('copilot') ||
scheme.includes('notebook') || doc.uri.path.includes('chat');
if (!isChatDoc) {
return;
}
const content = doc.getText();
if (!content || content === lastChatContent) {
return;
}
// Debounce: wait 2s for content to stabilize (AI streams text)
if (chatDebounceTimer) {
clearTimeout(chatDebounceTimer);
}
chatDebounceTimer = setTimeout(() => {
const finalContent = doc.getText();
if (finalContent && finalContent !== lastChatContent && finalContent.length > 20) {
lastChatContent = finalContent;
writeChatSnapshot(finalContent);
}
}, 2000);
}
/**
* Write a chat content snapshot to bridge for the bot to relay.
*/
function writeChatSnapshot(content) {
const snapshotDir = path.join(bridgePath, 'chat_snapshots');
if (!fs.existsSync(snapshotDir)) {
fs.mkdirSync(snapshotDir, { recursive: true });
}
const id = `chat-${Date.now()}`;
const filePath = path.join(snapshotDir, `${id}.json`);
const data = {
id,
project_name: projectName,
content: content.substring(0, 4000), // Limit size
timestamp: Date.now() / 1000,
};
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
console.log(`Gravity Bridge [${projectName}]: chat snapshot written (${content.length} chars)`);
}
/**
* @bridge Chat Participant handler.
* Reads conversation history and sends to Discord via bridge.
*/
const bridgeChatHandler = async (request, context, stream, token) => {
const command = request.prompt.trim().toLowerCase();
if (command === 'stop' || command === '중지') {
// Cancel current AI work
try {
await vscode.commands.executeCommand('workbench.action.chat.stop');
stream.markdown('⏹️ AI 작업 중지 요청을 보냈습니다.');
}
catch {
stream.markdown('⚠️ 중지 명령을 실행할 수 없습니다.');
}
return;
}
// Collect conversation history
const historyLines = [];
historyLines.push(`# 대화 히스토리 (${projectName})\n`);
for (const entry of context.history) {
if (entry instanceof vscode.ChatRequestTurn) {
historyLines.push(`## 👤 사용자\n${entry.prompt}\n`);
}
else if (entry instanceof vscode.ChatResponseTurn) {
let responseText = '';
for (const part of entry.response) {
if (part instanceof vscode.ChatResponseMarkdownPart) {
responseText += part.value.value;
}
}
if (responseText) {
historyLines.push(`## 🤖 AI\n${responseText}\n`);
}
}
}
if (historyLines.length <= 1) {
stream.markdown('대화 히스토리가 비어있습니다. AI와 대화를 먼저 진행한 후 `@bridge`를 호출하세요.');
return;
}
// Write to bridge for Discord relay
const fullHistory = historyLines.join('\n');
const cmdId = `bridge-history-${Date.now()}`;
const cmdPath = path.join(bridgePath, 'commands', `${cmdId}.json`);
const data = {
id: cmdId,
project_name: projectName,
text: `[HISTORY]\n${fullHistory}`,
timestamp: Date.now() / 1000,
consumed: false,
};
fs.writeFileSync(cmdPath, JSON.stringify(data, null, 2), 'utf-8');
stream.markdown(`✅ 대화 히스토리 (${context.history.length}개 턴)를 Discord에 전송했습니다.`);
console.log(`Gravity Bridge [${projectName}]: sent ${context.history.length} turns to Discord`);
};
function deactivate() {
stopBridge();
}
//# sourceMappingURL=extension.js.map