fix(observer): v9 - stop treating Running N commands as approval button, add DOM-climbing context extraction
This commit is contained in:
@@ -16,6 +16,19 @@
|
||||
> 뵒踰꾧퉭씠굹 援ы쁽 쟾뿉 **諛섎뱶떆** 씠 뙆뙆씪쓣 솗씤븯꽭슂.
|
||||
> 꽭뀡 醫낅즺 떆 깉濡 諛쒓껄맂 씠뒋瑜 씠 뙆뙆씪뿉 異붽빀땲떎.
|
||||
|
||||
> [!TIP]
|
||||
> 빐寃 셿猷뚮맂 怨쇨굅 씠뒋뒗 [`known-issues-archive.md`](file:///c:/Users/Variet-Worker/Desktop/gravity_control/.agents/references/known-issues-archive.md)뿉 蹂닿릺뼱 엳뒿땲떎.
|
||||
> 鍮꾩듂븳 臾몄젣媛 옱諛쒗븯硫 archive뿉꽌 寃깋븯꽭슂.
|
||||
|
||||
---
|
||||
|
||||
### [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로 확대, 그룹 헤더 스킵
|
||||
|
||||
@@ -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` | 🔧 |
|
||||
|
||||
28
docs/devlog/entries/20260413-003.md
Normal file
28
docs/devlog/entries/20260413-003.md
Normal file
@@ -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 검증 (실제 명령어/코드 내용 표시 확인)
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<ACTION_WORDS.length; i++) {
|
||||
if(txt.indexOf(ACTION_WORDS[i]) !== -1) return true;
|
||||
}
|
||||
if (/Running\\s*\\d*\\s*command/i.test(txt)) return true;
|
||||
// v9: Removed "Running N commands" — it's a group header, not an approval button
|
||||
return false;
|
||||
}
|
||||
function isRejectBtn(txt) {
|
||||
@@ -173,15 +211,31 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
|
||||
function collectSiblingButtons(container,triggerBtn){
|
||||
if(!container)return [];
|
||||
var siblings=container.querySelectorAll('button');
|
||||
// v9: Try multiple container levels (parent → grandparent → great-grandparent)
|
||||
// to find all related approval buttons in wider DOM context
|
||||
var containers = [container];
|
||||
if (container.parentElement) containers.push(container.parentElement);
|
||||
if (container.parentElement && container.parentElement.parentElement)
|
||||
containers.push(container.parentElement.parentElement);
|
||||
var result=[];
|
||||
for(var i=0;i<siblings.length;i++){
|
||||
var sb=siblings[i];
|
||||
if(sb.disabled||sb.hidden||(!sb.offsetParent&&sb.style.display!=='fixed'))continue;
|
||||
var stxt = cleanButtonText(sb);
|
||||
if(stxt.length <= 1) continue;
|
||||
if(!isActionBtn(stxt) && !isRejectBtn(stxt)) continue;
|
||||
result.push({btn:sb,text:stxt,isPrimary:(sb===triggerBtn)});
|
||||
var seen={};
|
||||
for(var ci=0;ci<containers.length;ci++){
|
||||
var siblings=containers[ci].querySelectorAll('button');
|
||||
for(var i=0;i<siblings.length;i++){
|
||||
var sb=siblings[i];
|
||||
if(sb.disabled||sb.hidden||(!sb.offsetParent&&sb.style.display!=='fixed'))continue;
|
||||
var stxt = cleanButtonText(sb);
|
||||
if(stxt.length <= 1) continue;
|
||||
// Skip group headers
|
||||
if (/^Running\\s*\\d+\\s*commands?$/i.test(stxt)) continue;
|
||||
if(!isActionBtn(stxt) && !isRejectBtn(stxt)) continue;
|
||||
// Dedup by text
|
||||
if(seen[stxt])continue;
|
||||
seen[stxt]=true;
|
||||
result.push({btn:sb,text:stxt,isPrimary:(sb===triggerBtn)});
|
||||
}
|
||||
// If we found action buttons at this level, don't go wider
|
||||
if(result.length > 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);
|
||||
|
||||
Reference in New Issue
Block a user