"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 D: Streaming RPC + JSON retry ========== setTimeout(async () => { if (!lsPort || !lsCsrf) { console.log('Gravity Bridge: [Trial D] Skipped — no LS'); return; } console.log(`Gravity Bridge: [Trial D] Probing streaming RPCs on port ${lsPort}...`); const http = require('http'); function tryRPC(method, bodyStr) { return new Promise((resolve) => { const isJson = !!bodyStr; const body = isJson ? Buffer.from(bodyStr) : Buffer.from([0, 0, 0, 0, 0]); const req = http.request({ hostname: '127.0.0.1', port: lsPort, path: `/exa.language_server_pb.LanguageServerService/${method}`, method: 'POST', headers: { 'Content-Type': isJson ? 'application/json' : 'application/connect+proto', 'Connect-Protocol-Version': '1', 'x-codeium-csrf-token': lsCsrf }, timeout: 8000 }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); }); req.on('error', (e) => resolve(`err:${e.message}`)); req.on('timeout', () => { req.destroy(); resolve('timeout'); }); req.write(body); req.end(); }); } for (const m of ['StreamCascadeReactiveUpdates', 'StreamCascadeSummariesReactiveUpdates', 'StreamAgentStateUpdates', 'GetBrowserOpenConversation']) { const r = await tryRPC(m); console.log(`Gravity Bridge: [Trial D] ${m}: ${r.substring(0, 400)}`); } 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 t = ts[ts.length - 1]; const gid = t.googleAgentId || ''; console.log(`Gravity Bridge: [Trial D] Latest: ${gid.substring(0, 8)} step=${t.lastStepIndex}`); const r = await tryRPC('GetCascadeTrajectorySteps', JSON.stringify({ trajectoryId: gid, startStepIndex: Math.max(0, t.lastStepIndex - 1) })); console.log(`Gravity Bridge: [Trial D] Steps(json): ${r.substring(0, 500)}`); } } catch (e) { console.log(`Gravity Bridge: [Trial D] 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