feat(action): direct accept/reject buttons — no mirror tab needed, Extension /api/action endpoint

This commit is contained in:
2026-03-08 01:32:29 +09:00
parent 8568985e7a
commit 79c2b1b6d9
7 changed files with 83 additions and 23 deletions

View File

@@ -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<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, () => { server.listen(BRIDGE_PORT, () => {

View File

@@ -526,6 +526,19 @@ body {
background: var(--accent-primary); 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 { .msg-status {
text-align: center; text-align: center;

View File

@@ -230,9 +230,10 @@
if (nu.isBlocking) { if (nu.isBlocking) {
messages.push({ messages.push({
type: 'actions', type: 'actions',
label: '⚠️ Antigravity에서 리뷰가 필요합니다', label: '⚠️ 사용자 승인 대기 중',
buttons: [ 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' },
], ],
}); });
} }

View File

@@ -314,12 +314,34 @@ class ChatPanel {
for (const btn of (msg.buttons || [])) { for (const btn of (msg.buttons || [])) {
const el = document.createElement('button'); 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.textContent = btn.label || btn;
el.style.cursor = 'pointer'; el.style.cursor = 'pointer';
// action 기반 처리 // api_call: 직접 API 호출 (승인/거절 등)
if (btn.action === 'switch_mirror') { 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', () => { el.addEventListener('click', () => {
document.getElementById('tabMirror')?.click(); document.getElementById('tabMirror')?.click();
}); });

View File

@@ -64,24 +64,11 @@ class BridgeClient {
} }
/** /**
* 코드 편집 승인 * 승인/거절 액션 (통합)
* action: 'acceptStep' | 'rejectStep' | 'acceptCommand' | 'rejectCommand' | 'acceptTerminal' | 'rejectTerminal'
*/ */
async acceptStep() { async sendAction(action) {
return this._post('/api/accept', {}); return this._post('/api/action', { action });
}
/**
* 코드 편집 거절
*/
async rejectStep() {
return this._post('/api/reject', {});
}
/**
* 터미널 명령 승인
*/
async acceptTerminal() {
return this._post('/api/accept-terminal', {});
} }
/** /**

View File

@@ -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) => { bridge.connectWs((msg) => {
broadcastToAll({ type: 'bridge_event', ...msg }); broadcastToAll({ type: 'bridge_event', ...msg });
}); });