feat(phase3): VS Code Extension 스캐폴드 - bridge 연동 (approve/reject/text relay)
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -24,3 +24,7 @@ Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
extension/out/
|
||||
|
||||
292
extension/out/extension.js
Normal file
292
extension/out/extension.js
Normal file
@@ -0,0 +1,292 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Gravity Bridge — VS Code Extension
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
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 statusBar;
|
||||
let bridgePath;
|
||||
let isActive = false;
|
||||
// Track pending approvals we've already sent
|
||||
const sentPendingIds = new Set();
|
||||
function activate(context) {
|
||||
console.log('Gravity Bridge: activating...');
|
||||
// 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'];
|
||||
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) Bridge: Off';
|
||||
statusBar.tooltip = 'Gravity Bridge — Click to start';
|
||||
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.approve', () => handleManualAction(true)), vscode.commands.registerCommand('gravityBridge.reject', () => handleManualAction(false)));
|
||||
// Auto-start
|
||||
startBridge();
|
||||
}
|
||||
function startBridge() {
|
||||
if (isActive) {
|
||||
vscode.window.showInformationMessage('Gravity Bridge is already running');
|
||||
return;
|
||||
}
|
||||
isActive = true;
|
||||
statusBar.text = '$(radio-tower) Bridge: On';
|
||||
statusBar.tooltip = 'Gravity Bridge — Active';
|
||||
statusBar.command = 'gravityBridge.stop';
|
||||
// Watch bridge/response/ for Discord user responses
|
||||
const responsePath = path.join(bridgePath, 'response');
|
||||
try {
|
||||
watcher = fs.watch(responsePath, { persistent: false }, (eventType, filename) => {
|
||||
if (eventType === 'rename' && filename && filename.endsWith('.json')) {
|
||||
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 {
|
||||
fs.watch(commandsPath, { persistent: false }, (eventType, filename) => {
|
||||
if (eventType === 'rename' && filename && filename.endsWith('.json')) {
|
||||
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: Started');
|
||||
console.log(`Gravity Bridge: started, bridge path: ${bridgePath}`);
|
||||
}
|
||||
function stopBridge() {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
isActive = false;
|
||||
statusBar.text = '$(radio-tower) Bridge: Off';
|
||||
statusBar.tooltip = 'Gravity Bridge — Click to start';
|
||||
statusBar.command = 'gravityBridge.start';
|
||||
if (watcher) {
|
||||
watcher.close();
|
||||
watcher = null;
|
||||
}
|
||||
vscode.window.showInformationMessage('Gravity Bridge: Stopped');
|
||||
}
|
||||
/**
|
||||
* Handle a response from Discord (approve/reject).
|
||||
* Reads the response JSON and simulates the appropriate action.
|
||||
*/
|
||||
async function handleResponse(filePath) {
|
||||
try {
|
||||
// Small delay to ensure file is fully written
|
||||
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: response received — 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 {
|
||||
await simulateRejection();
|
||||
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);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle a text command from Discord.
|
||||
* Types the text into the active editor or chat input.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
console.log(`Gravity Bridge: command received — "${command.text.substring(0, 50)}..."`);
|
||||
// Type into the active text input (chat panel)
|
||||
await vscode.commands.executeCommand('workbench.action.chat.open');
|
||||
// Small delay for chat panel to open
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
// Type the text using clipboard
|
||||
const oldClipboard = await vscode.env.clipboard.readText();
|
||||
await vscode.env.clipboard.writeText(command.text);
|
||||
await vscode.commands.executeCommand('editor.action.clipboardPasteAction');
|
||||
await vscode.env.clipboard.writeText(oldClipboard);
|
||||
// Mark as consumed
|
||||
command.consumed = true;
|
||||
fs.writeFileSync(filePath, JSON.stringify(command, null, 2), 'utf-8');
|
||||
vscode.window.showInformationMessage(`📨 Discord input: ${command.text.substring(0, 50)}...`);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Gravity Bridge: error handling command', 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Manual approve/reject from command palette.
|
||||
* Writes a pending request for testing purposes.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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,
|
||||
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: pending approval written — ${requestId}`);
|
||||
return requestId;
|
||||
}
|
||||
function deactivate() {
|
||||
stopBridge();
|
||||
}
|
||||
//# sourceMappingURL=extension.js.map
|
||||
1
extension/out/extension.js.map
Normal file
1
extension/out/extension.js.map
Normal file
File diff suppressed because one or more lines are too long
58
extension/package-lock.json
generated
Normal file
58
extension/package-lock.json
generated
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "gravity-bridge",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gravity-bridge",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/vscode": "^1.80.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.80.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
|
||||
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/vscode": {
|
||||
"version": "1.109.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz",
|
||||
"integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
56
extension/package.json
Normal file
56
extension/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "gravity-bridge",
|
||||
"displayName": "Gravity Bridge",
|
||||
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
||||
"version": "0.1.0",
|
||||
"publisher": "variet",
|
||||
"engines": {
|
||||
"vscode": "^1.80.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onStartupFinished"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"scripts": {
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.80.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "gravityBridge.start",
|
||||
"title": "Gravity Bridge: Start"
|
||||
},
|
||||
{
|
||||
"command": "gravityBridge.stop",
|
||||
"title": "Gravity Bridge: Stop"
|
||||
},
|
||||
{
|
||||
"command": "gravityBridge.approve",
|
||||
"title": "Gravity Bridge: Approve Pending"
|
||||
},
|
||||
{
|
||||
"command": "gravityBridge.reject",
|
||||
"title": "Gravity Bridge: Reject Pending"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "Gravity Bridge",
|
||||
"properties": {
|
||||
"gravityBridge.bridgePath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Bridge 디렉토리 경로 (기본: ~/.gemini/antigravity/bridge)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
285
extension/src/extension.ts
Normal file
285
extension/src/extension.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Gravity Bridge — VS Code Extension
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
let watcher: fs.FSWatcher | null = null;
|
||||
let statusBar: vscode.StatusBarItem;
|
||||
let bridgePath: string;
|
||||
let isActive = false;
|
||||
|
||||
// Track pending approvals we've already sent
|
||||
const sentPendingIds = new Set<string>();
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('Gravity Bridge: activating...');
|
||||
|
||||
// Determine bridge path
|
||||
const config = vscode.workspace.getConfiguration('gravityBridge');
|
||||
const configPath = config.get<string>('bridgePath');
|
||||
bridgePath = configPath || path.join(os.homedir(), '.gemini', 'antigravity', 'bridge');
|
||||
|
||||
// Ensure bridge directories exist
|
||||
const dirs = ['pending', 'response', 'commands'];
|
||||
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) Bridge: Off';
|
||||
statusBar.tooltip = 'Gravity Bridge — Click to start';
|
||||
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.approve', () => handleManualAction(true)),
|
||||
vscode.commands.registerCommand('gravityBridge.reject', () => handleManualAction(false)),
|
||||
);
|
||||
|
||||
// Auto-start
|
||||
startBridge();
|
||||
}
|
||||
|
||||
function startBridge() {
|
||||
if (isActive) {
|
||||
vscode.window.showInformationMessage('Gravity Bridge is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
isActive = true;
|
||||
statusBar.text = '$(radio-tower) Bridge: On';
|
||||
statusBar.tooltip = 'Gravity Bridge — Active';
|
||||
statusBar.command = 'gravityBridge.stop';
|
||||
|
||||
// Watch bridge/response/ for Discord user responses
|
||||
const responsePath = path.join(bridgePath, 'response');
|
||||
try {
|
||||
watcher = fs.watch(responsePath, { persistent: false }, (eventType, filename) => {
|
||||
if (eventType === 'rename' && filename && filename.endsWith('.json')) {
|
||||
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 {
|
||||
fs.watch(commandsPath, { persistent: false }, (eventType, filename) => {
|
||||
if (eventType === 'rename' && filename && filename.endsWith('.json')) {
|
||||
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: Started');
|
||||
console.log(`Gravity Bridge: started, bridge path: ${bridgePath}`);
|
||||
}
|
||||
|
||||
function stopBridge() {
|
||||
if (!isActive) { return; }
|
||||
|
||||
isActive = false;
|
||||
statusBar.text = '$(radio-tower) Bridge: Off';
|
||||
statusBar.tooltip = 'Gravity Bridge — Click to start';
|
||||
statusBar.command = 'gravityBridge.start';
|
||||
|
||||
if (watcher) {
|
||||
watcher.close();
|
||||
watcher = null;
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage('Gravity Bridge: Stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a response from Discord (approve/reject).
|
||||
* Reads the response JSON and simulates the appropriate action.
|
||||
*/
|
||||
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; }
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const response = JSON.parse(content);
|
||||
|
||||
if (response.approved === undefined) { return; }
|
||||
|
||||
console.log(`Gravity Bridge: response received — 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 {
|
||||
await simulateRejection();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a text command from Discord.
|
||||
* Types the text into the active editor or chat input.
|
||||
*/
|
||||
async function handleCommand(filePath: string) {
|
||||
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; }
|
||||
|
||||
console.log(`Gravity Bridge: command received — "${command.text.substring(0, 50)}..."`);
|
||||
|
||||
// Type into the active text input (chat panel)
|
||||
await vscode.commands.executeCommand('workbench.action.chat.open');
|
||||
// Small delay for chat panel to open
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Type the text using clipboard
|
||||
const oldClipboard = await vscode.env.clipboard.readText();
|
||||
await vscode.env.clipboard.writeText(command.text);
|
||||
await vscode.commands.executeCommand('editor.action.clipboardPasteAction');
|
||||
await vscode.env.clipboard.writeText(oldClipboard);
|
||||
|
||||
// Mark as consumed
|
||||
command.consumed = true;
|
||||
fs.writeFileSync(filePath, JSON.stringify(command, null, 2), 'utf-8');
|
||||
|
||||
vscode.window.showInformationMessage(`📨 Discord input: ${command.text.substring(0, 50)}...`);
|
||||
} catch (err) {
|
||||
console.error('Gravity Bridge: error handling command', 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual approve/reject from command palette.
|
||||
* Writes a pending request for testing purposes.
|
||||
*/
|
||||
function handleManualAction(approved: boolean) {
|
||||
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.
|
||||
*/
|
||||
export function writePendingApproval(
|
||||
conversationId: string,
|
||||
command: string,
|
||||
description: string
|
||||
): string {
|
||||
const requestId = `req-${Date.now()}`;
|
||||
const pendingPath = path.join(bridgePath, 'pending', `${requestId}.json`);
|
||||
|
||||
const request = {
|
||||
request_id: requestId,
|
||||
conversation_id: conversationId,
|
||||
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: pending approval written — ${requestId}`);
|
||||
return requestId;
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
stopBridge();
|
||||
}
|
||||
18
extension/tsconfig.json
Normal file
18
extension/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2022",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"ES2022"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user