Files
gravity_web/extension/src/extension.ts

289 lines
9.0 KiB
TypeScript

/**
* Gravity Web Bridge Extension
*
* Antigravity IDE 내부에서 동작하여:
* 1. antigravity-sdk로 대화/상태 접근
* 2. 로컬 HTTP/WS 서버를 열어 Gravity Web 서버와 통신
*
* API:
* GET /api/sessions → 대화 목록
* GET /api/session/:id → 대화 상세
* POST /api/send → 메시지 전송
* POST /api/accept → 스텝 승인
* POST /api/reject → 스텝 거절
* WS /ws → 실시간 이벤트 스트림
*/
import * as vscode from 'vscode';
// antigravity-sdk는 런타임에 로드 (VS Code Extension 환경에서만 동작)
let AntigravitySDK: any;
const BRIDGE_PORT = 9850;
export async function activate(context: vscode.ExtensionContext) {
const output = vscode.window.createOutputChannel('Gravity Web Bridge');
output.appendLine('[Bridge] Extension 활성화...');
// antigravity-sdk 로드
try {
const sdkModule = require('antigravity-sdk');
AntigravitySDK = sdkModule.AntigravitySDK;
} catch (err: any) {
output.appendLine(`[Bridge] antigravity-sdk 로드 실패: ${err.message}`);
vscode.window.showErrorMessage('Gravity Web Bridge: antigravity-sdk를 찾을 수 없습니다.');
return;
}
// SDK 초기화
const sdk = new AntigravitySDK(context);
try {
await sdk.initialize();
output.appendLine('[Bridge] SDK 초기화 완료');
} catch (err: any) {
output.appendLine(`[Bridge] SDK 초기화 실패: ${err.message}`);
return;
}
// Express + WS 서버
const express = require('express');
const http = require('http');
const { WebSocketServer } = require('ws');
const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server, path: '/ws' });
app.use(express.json());
// CORS 허용
app.use((_req: any, res: any, next: any) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
next();
});
// --- REST API ---
app.get('/api/sessions', async (_req: any, res: any) => {
try {
const sessions = await sdk.cascade.getSessions();
res.json({ sessions });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/send', async (req: any, res: any) => {
try {
const { message, sessionId } = req.body;
if (sessionId) {
await sdk.cascade.focusSession(sessionId);
}
await sdk.cascade.sendPrompt(message);
res.json({ success: true });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/accept', async (_req: any, res: any) => {
try {
await sdk.cascade.acceptStep();
res.json({ success: true });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/reject', async (_req: any, res: any) => {
try {
await sdk.cascade.rejectStep();
res.json({ success: true });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/accept-terminal', async (_req: any, res: any) => {
try {
await sdk.cascade.acceptTerminalCommand();
res.json({ success: true });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.get('/api/preferences', async (_req: any, res: any) => {
try {
const prefs = await sdk.cascade.getPreferences();
res.json({ preferences: prefs });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.get('/api/health', (_req: any, res: any) => {
res.json({ status: 'ok', port: BRIDGE_PORT, version: '0.1.0' });
});
// --- WebSocket 이벤트 스트림 ---
const wsClients = new Set<any>();
wss.on('connection', (ws: any) => {
wsClients.add(ws);
output.appendLine('[Bridge WS] 클라이언트 연결');
ws.on('close', () => {
wsClients.delete(ws);
output.appendLine('[Bridge WS] 클라이언트 해제');
});
// 초기 세션 목록 전송
sdk.cascade.getSessions().then((sessions: any) => {
ws.send(JSON.stringify({ type: 'sessions', sessions }));
}).catch(() => { });
});
function broadcast(data: any) {
const json = JSON.stringify(data);
for (const ws of wsClients) {
if (ws.readyState === 1) { // WebSocket.OPEN
ws.send(json);
}
}
}
// SDK 모니터 이벤트 → WS 브로드캐스트
sdk.monitor.onStepCountChanged((e: any) => {
broadcast({
type: 'step_changed',
title: e.title,
delta: e.delta,
newCount: e.newCount,
sessionId: e.sessionId,
});
});
sdk.monitor.onActiveSessionChanged((e: any) => {
broadcast({
type: 'session_changed',
title: e.title,
sessionId: e.sessionId,
});
});
sdk.monitor.onNewConversation(() => {
broadcast({ type: 'new_conversation' });
// 세션 목록 갱신
sdk.cascade.getSessions().then((sessions: any) => {
broadcast({ type: 'sessions', sessions });
}).catch(() => { });
});
sdk.monitor.onStateChanged((e: any) => {
broadcast({
type: 'state_changed',
key: e.key,
previousSize: e.previousSize,
newSize: e.newSize,
});
});
sdk.monitor.start(3000, 5000); // USS 3초, trajectory 5초 폴링
// --- LS Bridge (Language Server 직접 접근) ---
app.get('/api/cascades', async (_req: any, res: any) => {
try {
const cascades = await sdk.ls.listCascades();
res.json({ cascades });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
// 개별 대화 전체 내용 (스텝, 메시지, 도구 호출 등)
app.get('/api/conversation/:id', async (req: any, res: any) => {
try {
const conversation = await sdk.ls.getConversation(req.params.id);
res.json(conversation);
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
// 임의 LS RPC 호출 (개발/디버그)
app.post('/api/ls/rpc', async (req: any, res: any) => {
try {
const { method, payload } = req.body;
const result = await sdk.ls.rawRPC(method, payload || {});
res.json(result);
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.post('/api/ls/send', async (req: any, res: any) => {
try {
const { cascadeId, text, model } = req.body;
await sdk.ls.sendMessage({ cascadeId, text, model });
res.json({ success: true });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
// --- 승인/거절 액션 (Gravity Web에서 직접 조작) ---
app.post('/api/action', async (req: any, res: any) => {
try {
const { action } = req.body;
const commandMap: Record<string, string> = {
acceptStep: 'antigravity.agent.acceptAgentStep',
rejectStep: 'antigravity.agent.rejectAgentStep',
acceptCommand: 'antigravity.command.accept',
rejectCommand: 'antigravity.command.reject',
acceptTerminal: 'antigravity.terminalCommand.accept',
rejectTerminal: 'antigravity.terminalCommand.reject',
};
const cmd = commandMap[action];
if (!cmd) {
res.status(400).json({ error: `Unknown action: ${action}` });
return;
}
await vscode.commands.executeCommand(cmd);
output.appendLine(`[Action] ${action}${cmd} 실행`);
res.json({ success: true, action });
} catch (err: any) {
output.appendLine(`[Action] 실패: ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// --- 서버 시작 ---
server.listen(BRIDGE_PORT, () => {
output.appendLine(`[Bridge] 서버 시작: http://localhost:${BRIDGE_PORT}`);
vscode.window.showInformationMessage(
`Gravity Web Bridge 활성화 — localhost:${BRIDGE_PORT}`
);
});
// 정리
context.subscriptions.push(sdk);
context.subscriptions.push({
dispose: () => {
server.close();
sdk.monitor.stop();
output.appendLine('[Bridge] 서버 종료');
},
});
}
export function deactivate() { }