diff --git a/bot.py b/bot.py index 3dc3502..2138722 100644 --- a/bot.py +++ b/bot.py @@ -724,15 +724,55 @@ class GravityBot(commands.Bot): desc_parts = [] if header: desc_parts.append(header) - desc_parts.append(f"**명령:** `{request.command[:200]}`") + + # Clean command text (remove "Running2" artifacts → "Running 2") + cmd_text = request.command[:200] + import re + cmd_text = re.sub(r'Running(\d)', r'Running \1', cmd_text) + desc_parts.append(f"**명령:** `{cmd_text}`") + if buttons: btn_names = [b.get("text", "?") for b in buttons] desc_parts.append(f"**선택지:** {' / '.join(btn_names)}") - if request.description: - desc_parts.append(request.description[:500]) + + # Clean description: strip noise headers and garbage + desc_raw = request.description or "" + # Remove old-style headers + desc_raw = re.sub(r'\[AI 본문 요약\]\s*', '', desc_raw) + desc_raw = re.sub(r'\[결행 명령\]\s*', '', desc_raw) + # Remove lines that are clearly noise + desc_lines = desc_raw.split('\n') + clean_desc_lines = [] + for dline in desc_lines: + dline_stripped = dline.strip() + if not dline_stripped: + continue + # Skip UI artifacts + if dline_stripped in ('chevron_right', 'chevron_left', 'close', 'check', + 'content_copy', 'expand_more', 'expand_less', + 'Show more', 'Show less', 'Copy', 'Edit', 'Copied!'): + continue + # Skip "Thought for Xs" + if re.match(r'^Thought for \d+', dline_stripped): + continue + # Skip TypeScript declarations and file paths + if re.match(r'^(declare|import|export)\s+(class|function|interface|type|enum|const)', dline_stripped): + continue + if re.search(r'\.ts:\d+:', dline_stripped): + continue + if re.search(r'extension.*src.*sdk', dline_stripped, re.IGNORECASE): + continue + clean_desc_lines.append(dline_stripped) + + clean_desc = '\n'.join(clean_desc_lines).strip() + if clean_desc and len(clean_desc) > 3: + # Truncate and wrap in code block for readability + if len(clean_desc) > 300: + clean_desc = clean_desc[:300] + '…' + desc_parts.append(f"```\n{clean_desc}\n```") embed = discord.Embed( - title="⚠️ 승인 요청", + title=f"⚠️ 승인 요청 — {request.step_type or 'action'}", description="\n".join(desc_parts), color=discord.Color.orange(), timestamp=datetime.now(timezone.utc), diff --git a/extension/src/observer-script.ts b/extension/src/observer-script.ts index e220c2a..f876376 100644 --- a/extension/src/observer-script.ts +++ b/extension/src/observer-script.ts @@ -1,6 +1,6 @@ export function generateApprovalObserverScript(_port: number): string { return ` -// ── Gravity Bridge v5: Context-First DOM Extraction ── +// ── Gravity Bridge v6: Clean Context Extraction ── (function(){ 'use strict'; var BASE='',_obs=false,_sent={},_ready=false; @@ -9,7 +9,7 @@ export function generateApprovalObserverScript(_port: number): string { var CLEANUP_MS=300000; function log(m){console.log('[GB Observer] '+m);} - log('v5 Script loaded — Context-First Tailored Extraction'); + log('v6 Script loaded — Clean Context Extraction'); // React-Compatible Synthetic Clicker function dispatchReactClick(el){ @@ -25,16 +25,58 @@ export function generateApprovalObserverScript(_port: number): string { } } + // ── Noise filter: lines that are UI artifacts, not real content ── + var NOISE_PATTERNS = [ + /^chevron_right$/i, + /^chevron_left$/i, + /^arrow_/i, + /^Thought for \\\\d+/i, + /^expand_/i, + /^close$/i, + /^more_/i, + /^content_copy$/i, + /^check$/i, + /^\\\\d+ lines?$/i, + /^Show more$/i, + /^Show less$/i, + /^Copy$/i, + /^Edit$/i, + /^Copied!$/i, + /^\\\\s*$/, + /^declare\\\\s+(class|function|interface|type|enum|const|var|let)\\\\s/, // TypeScript declarations + /^(import|export|from)\\\\s/, // JS imports + /^\\\\s*[{}()\\\\[\\\\];]\\\\s*$/, // lone brackets + /\\\\.ts:\\\\d+:/, // file:line references + /extension.*src.*sdk/i, // SDK file paths + ]; + function isNoiseLine(line) { + if (!line || line.trim().length < 2) return true; + var trimmed = line.trim(); + for (var i = 0; i < NOISE_PATTERNS.length; i++) { + if (NOISE_PATTERNS[i].test(trimmed)) return true; + } + return false; + } + function cleanLines(text) { + if (!text) return ''; + var lines = text.split('\\\\n'); + var clean = []; + for (var i = 0; i < lines.length; i++) { + if (!isNoiseLine(lines[i])) clean.push(lines[i].trim()); + } + return clean.join('\\\\n').trim(); + } + function cleanButtonText(btn) { if (!btn) return ''; var clone = btn.cloneNode(true); - var icons = clone.querySelectorAll('.google-symbols, .codicon'); + var icons = clone.querySelectorAll('.google-symbols, .codicon, [class*="icon"], svg'); for(var i=0; i 0) { - var t = titleSpans[0].getAttribute('title'); - if (t && t.length > 5) return t.substring(0, 800); - } - var preEls = container.querySelectorAll('pre'); - if (preEls && preEls.length > 0) { - var t2 = (preEls[preEls.length-1].textContent || '').trim(); - if (t2.length > 2) return t2.substring(0, 800); - } - var fallback = (container.textContent || '').replace(cleanButtonText(b), '').trim(); - return fallback.substring(0, 500); - } + // ── Context extraction: TIGHT scope — only button's immediate context ── + // v6 FIX: Never climb more than 4 parents. Never grab editor/sidebar content. - function extractChatContextFromNode(botTurn) { - if (!botTurn) return ''; - - var res = ''; - // Use innerText if available on the markdown container (preserves spacing perfectly) - var md = botTurn.querySelector('.markdown-body') || botTurn.querySelector('.prose'); - if (md && md.innerText && md.innerText.trim().length > 10) { - res = md.innerText.trim(); - return res.substring(0, 3500); + function extractCommandContext(b) { + // Strategy 1: aria-label or title on button itself + var ariaLabel = b.getAttribute('aria-label') || b.getAttribute('title') || ''; + if (ariaLabel && ariaLabel.length > 5 && ariaLabel.length < 500) { + return ariaLabel; } - - var toolContainer = botTurn.querySelector('.bg-agent-convo-background') || botTurn.querySelector('.bg-ide-background-color'); - var textParts = []; - function walk(node) { - if (toolContainer && node === toolContainer) return; - if (node.id === 'antigravity.agentSidePanelInputBox') return; - if (node.nodeType === 1) { - var tag = node.tagName.toUpperCase(); - if (tag==='BUTTON' || tag==='SVG' || tag==='STYLE' || tag==='SCRIPT') return; - // Skip tool action blocks aggressively if they masquerade as normal divs - if (node !== botTurn && node.classList && (node.classList.contains('bg-ide-background-color') || node.classList.contains('bg-agent-convo-background'))) return; - } - if (node.nodeType === 3) { - var val = node.nodeValue; - if (val && val.trim()) textParts.push(val.trim()); - } else { - if (node.childNodes && node.childNodes.length > 0) { - for(var i=0; i 2 && preText.length < 500 && !isNoiseLine(preText)) { + return preText.substring(0, 400); + } } - return extractChatContextFromNode(botTurn); - } catch(e) { - return ''; + // Check for span with title attribute containing command info + var titleSpans = el.querySelectorAll('span[title]'); + for (var ti = 0; ti < titleSpans.length; ti++) { + var spanTitle = titleSpans[ti].getAttribute('title') || ''; + if (spanTitle.length > 5 && spanTitle.length < 500) { + return spanTitle.substring(0, 400); + } + } + el = el.parentElement; } + + // Strategy 3: Immediate parent's text only (NOT full page) + var immediateParent = b.parentElement; + if (immediateParent) { + var parentText = ''; + var children = immediateParent.childNodes; + for (var ci = 0; ci < children.length; ci++) { + var child = children[ci]; + if (child.nodeType === 3 && child.nodeValue && child.nodeValue.trim()) { + parentText += child.nodeValue.trim() + ' '; + } else if (child.nodeType === 1 && child.tagName !== 'BUTTON' && child.tagName !== 'SVG') { + var childText = child.textContent || ''; + if (childText.trim().length > 2 && childText.trim().length < 200) { + parentText += childText.trim() + ' '; + } + } + } + parentText = parentText.trim(); + if (parentText.length > 3 && parentText.length < 300) { + return cleanLines(parentText).substring(0, 300); + } + } + + return ''; } function extractContext(b) { - var cmd = extractCommandContext(b); - var chat = extractChatContext(b); - if (!chat && !cmd) return ""; - var combined = ""; - if (chat && chat.length > 5) combined += "[AI 본문 요약]\\n" + chat + "\\n\\n"; - if (cmd && cmd.length > 2) combined += "[결행 명령]\\n" + cmd; - return combined.trim(); + var cmd = cleanButtonText(b); + var detail = extractCommandContext(b); + if (!detail) return cmd; + // Deduplicate: if detail contains button text, just show detail + if (detail.includes(cmd)) return cleanLines(detail); + return cmd + ': ' + cleanLines(detail); } - var ACTION_WORDS = ['Allow', 'Run', 'Approve', 'Accept', '결행', '수락', '반영', '허용', '승인']; + var ACTION_WORDS = ['Allow', 'Run', 'Approve', 'Accept', 'Always allow', '결행', '수락', '반영', '허용', '승인']; var REJECT_WORDS = ['Reject', 'Cancel', 'Deny', 'Stop', 'Decline', 'Dismiss', '거절', '거부', '취소', '차단', '정지', '무시']; function isActionBtn(txt) { for(var i=0; i 0) { var text = items[0].getAttribute('aria-label') || items[0].getAttribute('title') || ''; - var m = text.match(/port:(\\d+)/); + var m = text.match(/port:(\\\\d+)/); if (m && m[1]) { clearInterval(timer); tryPingAsync(parseInt(m[1], 10)).then(function(ok){ cb(ok ? parseInt(m[1],10) : HARDCODED_PORT); }); @@ -194,13 +222,33 @@ export function generateApprovalObserverScript(_port: number): string { startObserver(); }); + // ── Chat body scanning (for Discord relay of AI responses) ── var _lastText = ""; var _lastTextTime = 0; var _lastTextSent = false; + function extractCleanChatText(container) { + if (!container) return ''; + // Try markdown body first + var md = container.querySelector('.markdown-body') || container.querySelector('.prose'); + var rawText = ''; + if (md && md.innerText && md.innerText.trim().length > 10) { + rawText = md.innerText.trim(); + } else { + rawText = (container.innerText || container.textContent || '').trim(); + } + // Clean the text + return cleanLines(rawText).substring(0, 3500); + } + function scanChatBodies() { if(!_ready)return; - var botTurns = document.querySelectorAll('.text-ide-message-block-bot-color'); + // Find bot response containers — try multiple selectors for compatibility + var botTurns = document.querySelectorAll( + '.text-ide-message-block-bot-color, ' + + '[data-testid*="bot"], [data-testid*="assistant"], ' + + '[class*="agent-convo"], [class*="bot-message"]' + ); if (botTurns.length === 0) return; var lastTurn = botTurns[botTurns.length - 1]; @@ -217,7 +265,7 @@ export function generateApprovalObserverScript(_port: number): string { if (Date.now() - _lastTextTime > 3000) { _lastTextSent = true; lastTurn.dataset.agChatScraped = "pending"; - var finalTxt = extractChatContextFromNode(lastTurn); + var finalTxt = extractCleanChatText(lastTurn); if (finalTxt && finalTxt.length > 5 && finalTxt !== "Review Changes") { fetch(BASE+'/chat', { method: 'POST', @@ -250,13 +298,13 @@ export function generateApprovalObserverScript(_port: number): string { if(txt.length <= 1) continue; if(!isActionBtn(txt)) continue; - // Skip inline code lens buttons unless they actually match the pattern properly + // Skip inline code lens buttons if (b.closest('.codelens-decoration') && !txt.includes('Accept') && !txt.includes('수락') && !txt.includes('반영')) { continue; } - var matchedType = txt.includes('Accept') ? 'diff_review' : (txt.includes('Run') ? 'command' : 'permission'); - var container=b.closest('.p-1') || b.parentElement.parentElement; + var matchedType = txt.includes('Accept') ? 'diff_review' : (txt.includes('Run') || /Running\\\\d/.test(txt) ? 'command' : 'permission'); + var container=b.parentElement; var groupKey=matchedType+'|'+btnId(b,matchedType); if(_sent[groupKey])continue; @@ -275,27 +323,21 @@ export function generateApprovalObserverScript(_port: number): string { } var desc=extractContext(b); - var is_dom_dummy = false; - if (!desc || desc.trim().length <= 2) { - desc = "MISSING_DESCRIPTION_FROM_DOM_FALLBACK_TO_STEP_PROBE"; - is_dom_dummy = true; - } var rid=now.toString()+'_'+Math.random().toString(36).substring(2,6); _sent[groupKey]={rid:rid,ts:now}; for(var mk=0;mk