feat: multi-project routing — project_name in bridge, per-project channels, extension filtering
This commit is contained in:
@@ -3,13 +3,10 @@
|
||||
*
|
||||
* Bridges Antigravity IDE approval dialogs to the Discord bot via file-based protocol.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Extension watches for tool approval notifications in VS Code
|
||||
* 2. Writes pending approval to bridge/pending/
|
||||
* 3. Discord bot sends buttons to user
|
||||
* 4. User clicks approve/reject
|
||||
* 5. Bot writes response to bridge/response/
|
||||
* 6. Extension reads response → sends keyboard command to approve/reject
|
||||
* 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
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
@@ -18,15 +15,38 @@ import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
let watcher: fs.FSWatcher | null = null;
|
||||
let commandsWatcher: fs.FSWatcher | null = null;
|
||||
let statusBar: vscode.StatusBarItem;
|
||||
let bridgePath: string;
|
||||
let projectName: string;
|
||||
let isActive = false;
|
||||
|
||||
// Track pending approvals we've already sent
|
||||
const sentPendingIds = new Set<string>();
|
||||
|
||||
/**
|
||||
* Detect project name from workspace.
|
||||
* Priority: settings > workspace folder name > fallback
|
||||
*/
|
||||
function detectProjectName(): string {
|
||||
const config = vscode.workspace.getConfiguration('gravityBridge');
|
||||
const configName = config.get<string>('projectName');
|
||||
if (configName) { return configName; }
|
||||
|
||||
// Use workspace folder name
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (folders && folders.length > 0) {
|
||||
const folderName = folders[0].name;
|
||||
// Convert to snake_case: "antig_web" → "antig_web", "My Project" → "my_project"
|
||||
return folderName.toLowerCase().replace(/[\s\-]+/g, '_');
|
||||
}
|
||||
|
||||
return 'unknown_project';
|
||||
}
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('Gravity Bridge: activating...');
|
||||
projectName = detectProjectName();
|
||||
console.log(`Gravity Bridge: activating for project "${projectName}"...`);
|
||||
|
||||
// Determine bridge path
|
||||
const config = vscode.workspace.getConfiguration('gravityBridge');
|
||||
@@ -45,8 +65,8 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
// Status bar
|
||||
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
||||
statusBar.command = 'gravityBridge.start';
|
||||
statusBar.text = '$(radio-tower) Bridge: Off';
|
||||
statusBar.tooltip = 'Gravity Bridge — Click to start';
|
||||
statusBar.text = `$(radio-tower) ${projectName}: Off`;
|
||||
statusBar.tooltip = `Gravity Bridge — ${projectName}`;
|
||||
statusBar.show();
|
||||
context.subscriptions.push(statusBar);
|
||||
|
||||
@@ -64,18 +84,18 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
function startBridge() {
|
||||
if (isActive) {
|
||||
vscode.window.showInformationMessage('Gravity Bridge is already running');
|
||||
vscode.window.showInformationMessage(`Gravity Bridge (${projectName}): already running`);
|
||||
return;
|
||||
}
|
||||
|
||||
isActive = true;
|
||||
statusBar.text = '$(radio-tower) Bridge: On';
|
||||
statusBar.tooltip = 'Gravity Bridge — Active';
|
||||
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<string>(); // Debounce: prevent double-processing
|
||||
const processedFiles = new Set<string>(); // Debounce
|
||||
try {
|
||||
watcher = fs.watch(responsePath, { persistent: false }, (eventType, filename) => {
|
||||
if (filename && filename.endsWith('.json') && !processedFiles.has(filename)) {
|
||||
@@ -92,7 +112,7 @@ function startBridge() {
|
||||
// Watch for commands (user text input from Discord)
|
||||
const commandsPath = path.join(bridgePath, 'commands');
|
||||
try {
|
||||
fs.watch(commandsPath, { persistent: false }, (eventType, filename) => {
|
||||
commandsWatcher = fs.watch(commandsPath, { persistent: false }, (eventType, filename) => {
|
||||
if (filename && filename.endsWith('.json') && !processedFiles.has(filename)) {
|
||||
processedFiles.add(filename);
|
||||
setTimeout(() => processedFiles.delete(filename), 2000);
|
||||
@@ -104,33 +124,30 @@ function startBridge() {
|
||||
console.error('Gravity Bridge: failed to watch commands dir', err);
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage('Gravity Bridge: Started');
|
||||
console.log(`Gravity Bridge: started, bridge path: ${bridgePath}`);
|
||||
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) Bridge: Off';
|
||||
statusBar.tooltip = 'Gravity Bridge — Click to start';
|
||||
statusBar.text = `$(radio-tower) ${projectName}: Off`;
|
||||
statusBar.tooltip = `Gravity Bridge — ${projectName}`;
|
||||
statusBar.command = 'gravityBridge.start';
|
||||
|
||||
if (watcher) {
|
||||
watcher.close();
|
||||
watcher = null;
|
||||
}
|
||||
if (watcher) { watcher.close(); watcher = null; }
|
||||
if (commandsWatcher) { commandsWatcher.close(); commandsWatcher = null; }
|
||||
|
||||
vscode.window.showInformationMessage('Gravity Bridge: Stopped');
|
||||
vscode.window.showInformationMessage(`Gravity Bridge (${projectName}): Stopped`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a response from Discord (approve/reject).
|
||||
* Reads the response JSON and simulates the appropriate action.
|
||||
* Only processes responses — no project filtering needed since request_id is unique.
|
||||
*/
|
||||
async function handleResponse(filePath: string) {
|
||||
try {
|
||||
// Small delay to ensure file is fully written
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
if (!fs.existsSync(filePath)) { return; }
|
||||
@@ -140,11 +157,9 @@ async function handleResponse(filePath: string) {
|
||||
|
||||
if (response.approved === undefined) { return; }
|
||||
|
||||
console.log(`Gravity Bridge: response received — approved=${response.approved}`);
|
||||
console.log(`Gravity Bridge [${projectName}]: response — approved=${response.approved}`);
|
||||
|
||||
if (response.approved) {
|
||||
// Simulate pressing Enter or clicking approve
|
||||
// Strategy: Use VS Code command to accept suggestion
|
||||
await simulateApproval();
|
||||
vscode.window.showInformationMessage(`✅ Approved: ${response.request_id}`);
|
||||
} else {
|
||||
@@ -152,9 +167,7 @@ async function handleResponse(filePath: string) {
|
||||
vscode.window.showInformationMessage(`❌ Rejected: ${response.request_id}`);
|
||||
}
|
||||
|
||||
// Cleanup: delete the response file after processing
|
||||
try { fs.unlinkSync(filePath); } catch (e) { /* ignore */ }
|
||||
|
||||
} catch (err) {
|
||||
console.error('Gravity Bridge: error handling response', err);
|
||||
}
|
||||
@@ -162,7 +175,7 @@ async function handleResponse(filePath: string) {
|
||||
|
||||
/**
|
||||
* Handle a text command from Discord.
|
||||
* Supports special commands (!auto on/off) and general text relay.
|
||||
* ONLY processes commands matching this project's name.
|
||||
*/
|
||||
async function handleCommand(filePath: string) {
|
||||
try {
|
||||
@@ -175,15 +188,21 @@ async function handleCommand(filePath: string) {
|
||||
|
||||
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: command received — "${text.substring(0, 50)}"`);
|
||||
console.log(`Gravity Bridge [${projectName}]: command — "${text.substring(0, 50)}"`);
|
||||
|
||||
// Special command: auto-approve toggle
|
||||
if (text === '!auto on' || text === '!auto off') {
|
||||
const enabled = text === '!auto on';
|
||||
await toggleAutoApprove(enabled);
|
||||
|
||||
// Mark as consumed
|
||||
command.consumed = true;
|
||||
fs.writeFileSync(filePath, JSON.stringify(command, null, 2), 'utf-8');
|
||||
return;
|
||||
@@ -202,7 +221,7 @@ async function handleCommand(filePath: string) {
|
||||
command.consumed = true;
|
||||
fs.writeFileSync(filePath, JSON.stringify(command, null, 2), 'utf-8');
|
||||
|
||||
vscode.window.showInformationMessage(`📨 Discord input: ${command.text.substring(0, 50)}...`);
|
||||
vscode.window.showInformationMessage(`📨 [${projectName}] Discord: ${command.text.substring(0, 50)}...`);
|
||||
} catch (err) {
|
||||
console.error('Gravity Bridge: error handling command', err);
|
||||
}
|
||||
@@ -215,31 +234,25 @@ async function toggleAutoApprove(enabled: boolean) {
|
||||
const config = vscode.workspace.getConfiguration();
|
||||
|
||||
try {
|
||||
// Core auto-approve settings
|
||||
await config.update('chat.tools.autoApprove', enabled, vscode.ConfigurationTarget.Global);
|
||||
await config.update('chat.agent.autoApprove', enabled, vscode.ConfigurationTarget.Global);
|
||||
|
||||
// Terminal auto-execution
|
||||
if (enabled) {
|
||||
await config.update('chat.tools.terminal.enableAutoApprove', true, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
|
||||
// File edits auto-accept
|
||||
await config.update('autoAcceptV2.autoAcceptFileEdits', enabled, vscode.ConfigurationTarget.Global);
|
||||
|
||||
// Update status bar
|
||||
statusBar.text = enabled
|
||||
? '$(radio-tower) Bridge: Auto ✅'
|
||||
: '$(radio-tower) Bridge: Manual 🔒';
|
||||
? `$(radio-tower) ${projectName}: Auto ✅`
|
||||
: `$(radio-tower) ${projectName}: Manual 🔒`;
|
||||
|
||||
const mode = enabled ? '자동 승인 ON 🟢' : '수동 승인 OFF 🔴';
|
||||
vscode.window.showInformationMessage(`Gravity Bridge: ${mode}`);
|
||||
console.log(`Gravity Bridge: auto-approve set to ${enabled}`);
|
||||
vscode.window.showInformationMessage(`Gravity Bridge (${projectName}): ${mode}`);
|
||||
|
||||
// Write status back to bridge for bot to report
|
||||
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,
|
||||
@@ -248,40 +261,28 @@ async function toggleAutoApprove(enabled: boolean) {
|
||||
|
||||
} catch (err) {
|
||||
console.error('Gravity Bridge: failed to toggle auto-approve', err);
|
||||
vscode.window.showErrorMessage(`Auto-approve toggle failed: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate approval — try multiple strategies.
|
||||
*/
|
||||
async function simulateApproval() {
|
||||
try {
|
||||
// Strategy 1: Try executing the accept command if available
|
||||
await vscode.commands.executeCommand('workbench.action.acceptSelectedCodeAction');
|
||||
} catch {
|
||||
// Strategy 2: Send Enter key via type command
|
||||
try {
|
||||
await vscode.commands.executeCommand('type', { text: '\n' });
|
||||
} catch {
|
||||
// Strategy 3: Focus terminal and send Enter
|
||||
await vscode.commands.executeCommand('workbench.action.terminal.focus');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate rejection — try multiple strategies.
|
||||
*/
|
||||
async function simulateRejection() {
|
||||
try {
|
||||
// Strategy 1: Escape key
|
||||
await vscode.commands.executeCommand('workbench.action.closeQuickOpen');
|
||||
} catch {
|
||||
try {
|
||||
await vscode.commands.executeCommand('cancelSelection');
|
||||
} catch {
|
||||
// Fallback: just notify
|
||||
console.log('Gravity Bridge: rejection sent but no active dialog found');
|
||||
}
|
||||
}
|
||||
@@ -289,7 +290,6 @@ async function simulateRejection() {
|
||||
|
||||
/**
|
||||
* Manual approve/reject from command palette.
|
||||
* Writes a pending request for testing purposes.
|
||||
*/
|
||||
function handleManualAction(approved: boolean) {
|
||||
const requestId = `manual-${Date.now()}`;
|
||||
@@ -304,15 +304,13 @@ function handleManualAction(approved: boolean) {
|
||||
|
||||
fs.writeFileSync(responsePath, JSON.stringify(response, null, 2), 'utf-8');
|
||||
|
||||
if (approved) {
|
||||
simulateApproval();
|
||||
} else {
|
||||
simulateRejection();
|
||||
}
|
||||
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.
|
||||
*/
|
||||
export function writePendingApproval(
|
||||
conversationId: string,
|
||||
@@ -325,6 +323,7 @@ export function writePendingApproval(
|
||||
const request = {
|
||||
request_id: requestId,
|
||||
conversation_id: conversationId,
|
||||
project_name: projectName, // ★ Project routing
|
||||
command: command,
|
||||
description: description,
|
||||
timestamp: Date.now() / 1000,
|
||||
@@ -335,7 +334,7 @@ export function writePendingApproval(
|
||||
fs.writeFileSync(pendingPath, JSON.stringify(request, null, 2), 'utf-8');
|
||||
sentPendingIds.add(requestId);
|
||||
|
||||
console.log(`Gravity Bridge: pending approval written — ${requestId}`);
|
||||
console.log(`Gravity Bridge [${projectName}]: pending approval — ${requestId}`);
|
||||
return requestId;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user