diff --git a/.agents/references/known-issues.md b/.agents/references/known-issues.md index 1385c5c..2d8cdbb 100644 --- a/.agents/references/known-issues.md +++ b/.agents/references/known-issues.md @@ -16,6 +16,19 @@ --- +### [2026-04-13] [Extension] Observer v8 "Running N commands" 그룹 헤더를 승인 버튼으로 오인 — Discord 빈 내용+잘못된 버튼 +- **증상**: Discord 승인 요청에 command="Running2 commands", description 비어있음, 버튼도 "Running2 commands / Always run" 형태. 실제 코드/명령어 내용이 전혀 표시되지 않음 +- **원인 1**: `observer-script.ts`의 `isActionBtn()`에 `/Running\\s*\\d*\\s*command/i` 패턴이 있어 AG UI의 그룹 헤더 버튼("Running 3 commands")을 승인 버튼으로 분류. `scan()`이 이 버튼을 먼저 만나고 `break`로 나가 실제 "Always run"/"Cancel" 버튼은 처리 안 됨 +- **원인 2**: `extractStepContext()`가 `data-step-index` 속성 없으면 `cleanButtonText(btn)` = "Running2 commands"를 그대로 반환. AG Native에는 `data-step-index`/`data-testid` 속성이 없음 (DOM 덤프로 확인) +- **원인 3**: `http-bridge.ts`의 "Run/Always run" 필터가 step-probe 미활성(activeSessionId 비어있음) 시에도 DOM observer 신호를 차단 +- **해결**: observer v9 (v0.5.40): + 1. `isActionBtn()`에서 "Running N commands" 패턴 제거 + 2. `scan()`에서 `^Running\\s*\\d+\\s*commands?$` 명시적 스킵 + 3. `extractContextFromNearby()` 추가: `data-step-index` 없이 DOM 트리를 20레벨까지 올라가며 `pre`/`code` 블록에서 실제 명령어 추출 + 4. `collectSiblingButtons()` 범위를 parent → grandparent → great-grandparent로 확대, 그룹 헤더 스킵 + 5. `http-bridge.ts`의 "Run" 필터에 `ctx.activeSessionId` 체크 추가 — step-probe 미활성 시 DOM observer 허용 +- **주의**: AG Native UI의 "Running N commands"는 아코디언/그룹 헤더이며, 실제 승인 버튼은 하위 레벨의 "Run"/"Always run"/"Cancel". DOM 구조상 버튼 탐색 시 그룹 헤더를 반드시 스킵해야 함 + ### [2026-04-13] [Extension] HTTP Bridge UTF-8 인코딩 깨짐 — 한글 description 손실 - **증상**: pending/ 파일의 description 필드에서 한글이 `[AI ]`처럼 깨져서 저장됨. Discord로 전달되는 승인 요청 본문도 깨짐 - **원인**: Node.js HTTP 서버의 `req.on('data', chunk)` 콜백에서 chunk가 Buffer 타입으로 전달되는데, `body += chunk`로 string 결합 시 Buffer의 기본 인코딩(latin1)이 사용되어 multi-byte UTF-8 문자가 손실됨 diff --git a/docs/devlog/2026-04-13.md b/docs/devlog/2026-04-13.md index 11a357c..2a02e7d 100644 --- a/docs/devlog/2026-04-13.md +++ b/docs/devlog/2026-04-13.md @@ -4,3 +4,4 @@ |-------|-------|----------|-----------|----------| | 001 | 09:50 | Observer v8 검증 — Extension POLL 확인, HTML 패치 확인, V8 캐시 삭제(24MB), BEACON 미수신(AG 재시작 필요) | 없음 | 🔧 | | 002 | 12:34 | DOM Observer 데이터 품질 검증 + UTF-8 인코딩 수정 + noise 필터 강화 (v0.5.39) | `pending` | ✅ | +| 003 | 19:26 | Observer v9: "Running N commands" 오인 수정 + DOM-climbing 컨텍스트 추출 + http-bridge 필터 완화 (v0.5.40) | `pending` | 🔧 | diff --git a/docs/devlog/entries/20260413-003.md b/docs/devlog/entries/20260413-003.md new file mode 100644 index 0000000..5c1f899 --- /dev/null +++ b/docs/devlog/entries/20260413-003.md @@ -0,0 +1,28 @@ +# DOM Observer 컨텍스트 추출 수정 — v9 (v0.5.40) + +- **시간**: 2026-04-13 19:26~ +- **Commit**: `pending` +- **Vikunja**: #619, #620 (진행 중) + +## 문제 + +Discord 승인 요청에 내용이 비어있음: +- command = "Running2 commands" (그룹 헤더 버튼을 잘못 캡처) +- description = 비어있거나 UI 노이즈만 포함 +- buttons = "Running2 commands / Always run" (잘못된 구조) + +## 변경 사항 + +### observer-script.ts (v8 → v9) +1. `isActionBtn()`에서 "Running N commands" 패턴 제거 — 이것은 그룹 헤더이며 승인 버튼이 아님 +2. `scan()`에서 `^Running\s*\d+\s*commands?$` 명시적 스킵 +3. `extractContextFromNearby()` 신규 함수 추가 — `data-step-index` 없이 DOM 트리를 20레벨까지 올라가며 `pre`/`code` 블록에서 실제 명령어 추출 +4. `collectSiblingButtons()` 범위를 parent → grandparent → great-grandparent 3레벨로 확대, 그룹 헤더 스킵, 텍스트 기반 dedup 추가 +5. `matchedType` 판별에서 `/Running\d/` 패턴 제거 + +### http-bridge.ts +6. "Run/Always run" 필터에 `ctx.activeSessionId` 체크 추가 — step-probe가 세션 미추적 시 DOM observer 신호 허용 + +## 미완료 +- AG 재시작 후 v0.5.40 적용 검증 +- Discord E2E 검증 (실제 명령어/코드 내용 표시 확인) diff --git a/extension/package.json b/extension/package.json index 6b71098..c02bd1b 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.39", + "version": "0.5.40", "publisher": "variet", "engines": { "vscode": "^1.100.0" diff --git a/extension/src/http-bridge.ts b/extension/src/http-bridge.ts index 3f0d096..0f6aeb0 100644 --- a/extension/src/http-bridge.ts +++ b/extension/src/http-bridge.ts @@ -267,14 +267,16 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) { return; } // "Run" button → step_probe handles these with full command detail - // Only let through if session is stalled AND step_probe hasn't created a pending yet + // Only filter when step_probe IS actively tracking a session if (/^(?:Always\s*)?Run\b/i.test(cmd)) { - if (!ctx.sessionStalled || ctx.lastPendingStepIndex >= 0) { - ctx.logToFile(`[HTTP] filtered "Run" — ${!ctx.sessionStalled ? 'not stalled' : 'step_probe pending exists'}`); + 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)})`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, filtered: true })); return; } + // v9: When step_probe has no active session, let DOM observer handle approval + ctx.logToFile(`[HTTP] allowing "Run" — step_probe has no active session`); } const rid = data.request_id || Date.now().toString(); diff --git a/extension/src/observer-script.ts b/extension/src/observer-script.ts index 8a945cc..a7052ed 100644 --- a/extension/src/observer-script.ts +++ b/extension/src/observer-script.ts @@ -1,7 +1,7 @@ export function generateApprovalObserverScript(_port: number): string { return ` -// ── Gravity Bridge v8: Full-DOM AG Native Parser ── -// Full body dump + step-aware parsing — no hardcoded selector dependency +// ── Gravity Bridge v9: Context-Aware AG Native Parser ── +// v9: Fixed 'Running N commands' false trigger + DOM-climbing context extraction (function(){ 'use strict'; var BASE='',_obs=false,_sent={},_ready=false; @@ -10,7 +10,7 @@ export function generateApprovalObserverScript(_port: number): string { var CLEANUP_MS=300000; function log(m){console.log('[GB Observer] '+m);} - log('v8 Script loaded — Full-DOM AG Native Parser'); + log('v9 Script loaded — Context-Aware AG Native Parser'); // DIAGNOSTIC BEACON: immediate POST to confirm script execution in renderer try { @@ -109,9 +109,47 @@ export function generateApprovalObserverScript(_port: number): string { return null; } + // v9: Climb DOM tree to find pre/code content near the button (no data-step-index needed) + function extractContextFromNearby(btn) { + var node = btn; + for (var depth = 0; depth < 20 && node; depth++) { + if (!node.querySelector) { node = node.parentElement; continue; } + // Look for code/pre blocks (actual command text) + var codeEls = node.querySelectorAll('pre, code, [class*="terminal"]'); + for (var ci = 0; ci < codeEls.length; ci++) { + var codeText = cleanLines((codeEls[ci].textContent || '').trim().substring(0, 500)); + if (codeText && codeText.length > 5 && !/^Running\\s*\\d/i.test(codeText)) { + // Also try to get a header/title near this container + var headerEl = node.querySelector('h1, h2, h3, [class*="header"], [class*="title"], [class*="cursor-pointer"]'); + var headerText = ''; + if (headerEl) { + var hClone = headerEl.cloneNode(true); + var hRem = hClone.querySelectorAll('button, svg, [class*="icon"], .google-symbols'); + for (var hi = 0; hi < hRem.length; hi++) { + if (hRem[hi].parentNode) hRem[hi].parentNode.removeChild(hRem[hi]); + } + headerText = cleanLines((hClone.textContent || '').trim().substring(0, 200)); + } + var parts = []; + if (headerText) parts.push(headerText); + parts.push(codeText); + return parts.join(' — '); + } + } + node = node.parentElement; + } + // Last resort: try aria-label or title on the button + var ariaLabel = btn.getAttribute('aria-label') || btn.getAttribute('title') || ''; + if (ariaLabel && ariaLabel.length > 5) return ariaLabel; + return cleanButtonText(btn); + } + function extractStepContext(btn) { var stepEl = getStepContainer(btn); - if (!stepEl) return cleanButtonText(btn); + if (!stepEl) { + // v9 FALLBACK: no data-step-index — climb DOM for pre/code blocks + return extractContextFromNearby(btn); + } var stepIdx = stepEl.getAttribute('data-step-index') || '?'; @@ -161,7 +199,7 @@ export function generateApprovalObserverScript(_port: number): string { for(var i=0; i 0) break; } return result; } @@ -524,13 +578,16 @@ export function generateApprovalObserverScript(_port: number): string { var txt=cleanButtonText(b); if(txt.length <= 1) continue; + // v9: Skip group header buttons — not approval buttons + if (/^Running\\s*\\d+\\s*commands?$/i.test(txt)) continue; + if(!isActionBtn(txt)) continue; // Skip inline code lens buttons if (b.closest('.codelens-decoration') && !txt.includes('Accept')) { continue; } - var matchedType = txt.includes('Accept') ? 'diff_review' : (txt.includes('Run') || /Running\\d/.test(txt) ? 'command' : 'permission'); + var matchedType = txt.includes('Accept') ? 'diff_review' : (txt.includes('Run') || txt.includes('Allow') ? 'command' : 'permission'); // v7: Use step-index for more unique group key var stepContainer = getStepContainer(b);