diff --git a/extension/gravity-web-bridge-0.1.0.vsix b/extension/gravity-web-bridge-0.1.0.vsix index 4e0962f..fb4f95f 100644 Binary files a/extension/gravity-web-bridge-0.1.0.vsix and b/extension/gravity-web-bridge-0.1.0.vsix differ diff --git a/extension/src/extension.ts b/extension/src/extension.ts index e7eb701..6d22c0f 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -236,6 +236,35 @@ export async function activate(context: vscode.ExtensionContext) { } }); + // --- 승인/거절 액션 (Gravity Web에서 직접 조작) --- + + app.post('/api/action', async (req: any, res: any) => { + try { + const { action } = req.body; + const commandMap: Record = { + 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, () => { diff --git a/public/css/style.css b/public/css/style.css index 28ccd22..01fa664 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -526,6 +526,19 @@ body { background: var(--accent-primary); } +.msg-action-danger { + background: #ef4444; +} + +.msg-action-danger:hover { + background: #dc2626; +} + +.msg-action-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + /* --- 상태 메시지 --- */ .msg-status { text-align: center; diff --git a/public/js/app.js b/public/js/app.js index 6ee095e..a4f6a03 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -230,9 +230,10 @@ if (nu.isBlocking) { messages.push({ type: 'actions', - label: '⚠️ Antigravity에서 리뷰가 필요합니다', + label: '⚠️ 사용자 승인 대기 중', buttons: [ - { label: '미러 탭에서 확인', action: 'switch_mirror' }, + { label: '✅ 진행', action: 'api_call', endpoint: '/api/bridge/action', body: { action: 'acceptStep' } }, + { label: '❌ 거절', action: 'api_call', endpoint: '/api/bridge/action', body: { action: 'rejectStep' }, variant: 'danger' }, ], }); } diff --git a/public/js/chat-panel.js b/public/js/chat-panel.js index bf2003d..b527652 100644 --- a/public/js/chat-panel.js +++ b/public/js/chat-panel.js @@ -314,12 +314,34 @@ class ChatPanel { for (const btn of (msg.buttons || [])) { const el = document.createElement('button'); - el.className = 'msg-action-btn msg-action-primary'; + el.className = btn.variant === 'danger' ? 'msg-action-btn msg-action-danger' : 'msg-action-btn msg-action-primary'; el.textContent = btn.label || btn; el.style.cursor = 'pointer'; - // action 기반 처리 - if (btn.action === 'switch_mirror') { + // api_call: 직접 API 호출 (승인/거절 등) + if (btn.action === 'api_call' && btn.endpoint) { + el.addEventListener('click', async () => { + el.disabled = true; + el.textContent = '처리 중...'; + try { + const res = await fetch(btn.endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(btn.body || {}), + }); + if (res.ok) { + el.textContent = '✓ 완료'; + // 모든 형제 버튼 비활성화 + div.querySelectorAll('button').forEach(b => { b.disabled = true; }); + } else { + const err = await res.json().catch(() => ({})); + el.textContent = `실패: ${err.error || res.status}`; + } + } catch (e) { + el.textContent = `오류: ${e.message}`; + } + }); + } else if (btn.action === 'switch_mirror') { el.addEventListener('click', () => { document.getElementById('tabMirror')?.click(); }); diff --git a/server/bridge-client.js b/server/bridge-client.js index 52eb974..1dc7993 100644 --- a/server/bridge-client.js +++ b/server/bridge-client.js @@ -64,24 +64,11 @@ class BridgeClient { } /** - * 코드 편집 승인 + * 승인/거절 액션 (통합) + * action: 'acceptStep' | 'rejectStep' | 'acceptCommand' | 'rejectCommand' | 'acceptTerminal' | 'rejectTerminal' */ - async acceptStep() { - return this._post('/api/accept', {}); - } - - /** - * 코드 편집 거절 - */ - async rejectStep() { - return this._post('/api/reject', {}); - } - - /** - * 터미널 명령 승인 - */ - async acceptTerminal() { - return this._post('/api/accept-terminal', {}); + async sendAction(action) { + return this._post('/api/action', { action }); } /** diff --git a/server/index.js b/server/index.js index bb9d00e..def29f2 100644 --- a/server/index.js +++ b/server/index.js @@ -330,7 +330,15 @@ app.get('/api/bridge/trajectory/:id', async (req, res) => { } }); -// Bridge WS 이벤트 → 프론트엔드 포워딩 +app.post('/api/bridge/action', async (req, res) => { + try { + const { action } = req.body; + const result = await bridge.sendAction(action); + res.json(result); + } catch (e) { + res.status(502).json({ error: e.message }); + } +}); bridge.connectWs((msg) => { broadcastToAll({ type: 'bridge_event', ...msg }); });