diff --git a/extension/package.json b/extension/package.json index 39935d1..a50fa49 100644 --- a/extension/package.json +++ b/extension/package.json @@ -2,7 +2,7 @@ "name": "gravity-bridge", "displayName": "Gravity Bridge", "description": "Discord-based unified approval system for Antigravity AI interactions.", - "version": "0.5.42", + "version": "0.5.43", "publisher": "variet", "engines": { "vscode": "^1.100.0" diff --git a/extension/src/http-bridge.ts b/extension/src/http-bridge.ts index 5741b7f..7936da4 100644 --- a/extension/src/http-bridge.ts +++ b/extension/src/http-bridge.ts @@ -256,9 +256,28 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) { try { const data = JSON.parse(body); - // ── Server-side false positive filter ── - const cmd = (data.command || '').trim(); - // Removed valid AI buttons (Accept, Reject, Allow, Deny) which are now structurally protected by the observer script + // ── v11: Command enrichment FIRST — extract actual command from description ── + // Must run before filters so "Always run" with useful description isn't filtered out + const rawCmd = (data.command || '').trim(); + const rawDesc = (data.description || '').trim(); + const GENERIC_BTN_RE = /^(?:Always\s*)?(?:Run|Allow|Accept|Approve)$/i; + let enrichedCmd = rawCmd; + let enrichedDesc = rawDesc; + if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) { + // Extract the actual command from description (often includes terminal prompt) + // Pattern: "…\project_name > actual_command" + let extracted = rawDesc; + const promptMatch = rawDesc.match(/[>»]\s*(.+)/); + if (promptMatch && promptMatch[1].trim().length > 3) { + extracted = promptMatch[1].trim(); + } + enrichedCmd = extracted.substring(0, 200); + enrichedDesc = `[${rawCmd}] ${rawDesc}`; + ctx.logToFile(`[HTTP] command enriched: "${rawCmd}" → "${enrichedCmd.substring(0, 60)}"`); + } + + // ── Server-side false positive filter (uses enriched cmd) ── + const cmd = enrichedCmd; const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it|Dismiss)$/i; if (FALSE_POSITIVE_RE.test(cmd)) { ctx.logToFile(`[HTTP] filtered false positive: "${cmd}"`); @@ -267,7 +286,7 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) { return; } // "Run" button → step_probe handles these with full command detail - // Only filter when step_probe IS actively tracking a session + // Only filter when step_probe IS actively tracking AND cmd is still generic button text if (/^(?:Always\s*)?Run\b/i.test(cmd)) { if (ctx.activeSessionId && (!ctx.sessionStalled || ctx.lastPendingStepIndex >= 0)) { ctx.logToFile(`[HTTP] filtered "Run" — ${!ctx.sessionStalled ? 'not stalled' : 'step_probe pending exists'} (session=${ctx.activeSessionId.substring(0, 8)})`); @@ -283,16 +302,20 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) { const pending: Record = { ...data, request_id: rid, + command: enrichedCmd, + description: enrichedDesc, conversation_id: ctx.activeSessionId || '', timestamp: Date.now() / 1000, status: 'pending', project_name: ctx.projectName, auto_detected: true, source: 'dom_observer', + step_type: data.step_type, + buttons: data.buttons, step_index: ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : undefined, }; // File permission: inject multi-choice buttons - const cmdLower = (data.command || '').toLowerCase(); + const cmdLower = enrichedCmd.toLowerCase(); if (cmdLower.includes('allow') && !pending.buttons) { // Dedup: skip if another file_permission pending was created within 10s const nowMs = Date.now(); @@ -311,25 +334,8 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) { ]; pending.step_type = 'file_permission'; // Clean description: remove button labels from text - const rawDesc = (data.description || data.command || '').replace(/Deny|Allow Once|Allow This Conversation/gi, '').trim(); - pending.command = `파일 접근 권한${rawDesc ? ': ' + rawDesc : ''}`; - } - // v11: Command enrichment — if command is just button text but description has actual content, - // swap them so Discord shows what's actually being executed - const descText = (pending.description || data.description || '').trim(); - const cmdText = (pending.command || data.command || '').trim(); - const GENERIC_BTN_RE = /^(?:Always\s*)?(?:Run|Allow|Accept|Approve)$/i; - if (GENERIC_BTN_RE.test(cmdText) && descText.length > 10 && descText !== cmdText) { - // Extract the actual command from description (often includes terminal prompt) - // Pattern: "…\project_name > actual_command" - let enrichedCmd = descText; - const promptMatch = descText.match(/[>»]\s*(.+)/); - if (promptMatch && promptMatch[1].trim().length > 3) { - enrichedCmd = promptMatch[1].trim(); - } - pending.command = enrichedCmd.substring(0, 200); - pending.description = `[${cmdText}] ${descText}`; - ctx.logToFile(`[HTTP] command enriched: "${cmdText}" → "${enrichedCmd.substring(0, 60)}"`); + const cleanDesc = enrichedDesc.replace(/Deny|Allow Once|Allow This Conversation/gi, '').trim(); + pending.command = `파일 접근 권한${cleanDesc ? ': ' + cleanDesc : ''}`; } // WS dispatch if (ctx.wsBridge && ctx.wsBridge.isConnected()) {