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?$` 명시적 스킵
|
2. `scan()`에서 `^Running\\s*\\d+\\s*commands?$` 명시적 스킵
|
||||||
3. `extractContextFromNearby()` 추가: `data-step-index` 없이 DOM 트리를 20레벨까지 올라가며 `pre`/`code` 블록에서 실제 명령어 추출
|
3. `extractContextFromNearby()` 추가: `data-step-index` 없이 DOM 트리를 20레벨까지 올라가며 `pre`/`code` 블록에서 실제 명령어 추출
|
||||||
4. `collectSiblingButtons()` 범위를 parent → grandparent → great-grandparent로 확대, 그룹 헤더 스킵
|
4. `collectSiblingButtons()` 범위를 parent → grandparent → great-grandparent로 확대, 그룹 헤더 스킵
|
||||||
|
|||||||
@@ -4,3 +4,4 @@
|
|||||||
|-------|-------|----------|-----------|----------|
|
|-------|-------|----------|-----------|----------|
|
||||||
| 001 | 09:50 | Observer v8 검증 — Extension POLL 확인, HTML 패치 확인, V8 캐시 삭제(24MB), BEACON 미수신(AG 재시작 필요) | 없음 | 🔧 |
|
| 001 | 09:50 | Observer v8 검증 — Extension POLL 확인, HTML 패치 확인, V8 캐시 삭제(24MB), BEACON 미수신(AG 재시작 필요) | 없음 | 🔧 |
|
||||||
| 002 | 12:34 | DOM Observer 데이터 품질 검증 + UTF-8 인코딩 수정 + noise 필터 강화 (v0.5.39) | `pending` | ✅ |
|
| 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",
|
"name": "gravity-bridge",
|
||||||
"displayName": "Gravity Bridge",
|
"displayName": "Gravity Bridge",
|
||||||
"description": "Discord-based unified approval system for Antigravity AI interactions.",
|
"description": "Discord-based unified approval system for Antigravity AI interactions.",
|
||||||
"version": "0.5.39",
|
"version": "0.5.40",
|
||||||
"publisher": "variet",
|
"publisher": "variet",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.100.0"
|
"vscode": "^1.100.0"
|
||||||
|
|||||||
@@ -267,14 +267,16 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// "Run" button → step_probe handles these with full command detail
|
// "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 (/^(?:Always\s*)?Run\b/i.test(cmd)) {
|
||||||
if (!ctx.sessionStalled || ctx.lastPendingStepIndex >= 0) {
|
if (ctx.activeSessionId && (!ctx.sessionStalled || ctx.lastPendingStepIndex >= 0)) {
|
||||||
ctx.logToFile(`[HTTP] filtered "Run" — ${!ctx.sessionStalled ? 'not stalled' : 'step_probe pending exists'}`);
|
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.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ ok: false, filtered: true }));
|
res.end(JSON.stringify({ ok: false, filtered: true }));
|
||||||
return;
|
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();
|
const rid = data.request_id || Date.now().toString();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export function generateApprovalObserverScript(_port: number): string {
|
export function generateApprovalObserverScript(_port: number): string {
|
||||||
return `
|
return `
|
||||||
// ── Gravity Bridge v8: Full-DOM AG Native Parser ──
|
// ── Gravity Bridge v9: Context-Aware AG Native Parser ──
|
||||||
// Full body dump + step-aware parsing — no hardcoded selector dependency
|
// v9: Fixed 'Running N commands' false trigger + DOM-climbing context extraction
|
||||||
(function(){
|
(function(){
|
||||||
'use strict';
|
'use strict';
|
||||||
var BASE='',_obs=false,_sent={},_ready=false;
|
var BASE='',_obs=false,_sent={},_ready=false;
|
||||||
@@ -10,7 +10,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
var CLEANUP_MS=300000;
|
var CLEANUP_MS=300000;
|
||||||
|
|
||||||
function log(m){console.log('[GB Observer] '+m);}
|
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
|
// DIAGNOSTIC BEACON: immediate POST to confirm script execution in renderer
|
||||||
try {
|
try {
|
||||||
@@ -109,9 +109,47 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
return null;
|
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) {
|
function extractStepContext(btn) {
|
||||||
var stepEl = getStepContainer(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') || '?';
|
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++) {
|
for(var i=0; i<ACTION_WORDS.length; i++) {
|
||||||
if(txt.indexOf(ACTION_WORDS[i]) !== -1) return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
function isRejectBtn(txt) {
|
function isRejectBtn(txt) {
|
||||||
@@ -173,16 +211,32 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
|
|
||||||
function collectSiblingButtons(container,triggerBtn){
|
function collectSiblingButtons(container,triggerBtn){
|
||||||
if(!container)return [];
|
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=[];
|
var result=[];
|
||||||
|
var seen={};
|
||||||
|
for(var ci=0;ci<containers.length;ci++){
|
||||||
|
var siblings=containers[ci].querySelectorAll('button');
|
||||||
for(var i=0;i<siblings.length;i++){
|
for(var i=0;i<siblings.length;i++){
|
||||||
var sb=siblings[i];
|
var sb=siblings[i];
|
||||||
if(sb.disabled||sb.hidden||(!sb.offsetParent&&sb.style.display!=='fixed'))continue;
|
if(sb.disabled||sb.hidden||(!sb.offsetParent&&sb.style.display!=='fixed'))continue;
|
||||||
var stxt = cleanButtonText(sb);
|
var stxt = cleanButtonText(sb);
|
||||||
if(stxt.length <= 1) continue;
|
if(stxt.length <= 1) continue;
|
||||||
|
// Skip group headers
|
||||||
|
if (/^Running\\s*\\d+\\s*commands?$/i.test(stxt)) continue;
|
||||||
if(!isActionBtn(stxt) && !isRejectBtn(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)});
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,13 +578,16 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
var txt=cleanButtonText(b);
|
var txt=cleanButtonText(b);
|
||||||
if(txt.length <= 1) continue;
|
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;
|
if(!isActionBtn(txt)) continue;
|
||||||
// Skip inline code lens buttons
|
// Skip inline code lens buttons
|
||||||
if (b.closest('.codelens-decoration') && !txt.includes('Accept')) {
|
if (b.closest('.codelens-decoration') && !txt.includes('Accept')) {
|
||||||
continue;
|
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
|
// v7: Use step-index for more unique group key
|
||||||
var stepContainer = getStepContainer(b);
|
var stepContainer = getStepContainer(b);
|
||||||
|
|||||||
Reference in New Issue
Block a user