fix(extension): restructure DOM observer to prevent false positive triggers (v0.5.10)
This commit is contained in:
@@ -29,6 +29,14 @@
|
|||||||
|
|
||||||
## 🔴 Active/Recent Issues
|
## 🔴 Active/Recent Issues
|
||||||
|
|
||||||
|
### [2026-03-24] DOM Observer /trigger-click 렌더링 순서 오작동 및 False Positive 프리징
|
||||||
|
- **증상**: v0.5.9 패치 이후 코딩 시 Agent 화면이 끊임없이 서명 대기(Pending) 상태로 멈춤. 또는 디스코드에서 `Approve` 시 에디터 내의 엉뚱한 `Run Test`(코드 렌즈)를 클릭함.
|
||||||
|
- **원인**: 텍스트와 정규식(`/^Run/i` 등)에만 의존하여 `querySelectorAll`을 수행할 경우, DOM 트리에 렌더링된 수많은 VS Code 네이티브 코드 렌즈 버튼을 Agent 버튼보다 먼저 찾아버리는 발생 위치(Context)의 한계점.
|
||||||
|
- **해결** (v0.5.10):
|
||||||
|
1. 감지(Scan): `isVSCodeMainWindow` 및 탐색 노드 `isBodyRoot` 확인을 통해, 에디터 본문 영역에서는 "Run", "Approve" 감지를 원천 제거 (오직 패널 내로 한정).
|
||||||
|
2. 클릭(Trigger-click): `deepFindButtons()` 내에서 `findPanel()`(에이전트 패널) -> 알림 Toasts -> Document 본문 순으로 탐색 **우선순위(Priority)**를 강제 적용.
|
||||||
|
- **주의**: 버튼 이벤트 후킹 시 텍스트 매칭에만 의존하지 말고, 반드시 DOM 탐색 우선순위와 컨텍스트 범위를 함께 필터링하여 False Positive를 차단할 것.
|
||||||
|
|
||||||
### [2026-03-24] DOM Observer — VS Code Native UI Blind Spot
|
### [2026-03-24] DOM Observer — VS Code Native UI Blind Spot
|
||||||
- **증상**: "Always Allow" 및 일반 "Allow Alt+↵" 권한 알림 버튼이 디스코드 권한 센싱에서 완전히 누락됨.
|
- **증상**: "Always Allow" 및 일반 "Allow Alt+↵" 권한 알림 버튼이 디스코드 권한 센싱에서 완전히 누락됨.
|
||||||
- **원인**: VS Code 네이티브 알림 및 채팅 패널 내의 버튼은 `<button>` 태그 대신 `<a role="button">`, `<vscode-button>` 등을 사용하는데, 기존 DOM scan 로직이 `querySelectorAll('button')`으로 하드코딩되어 노드를 아예 찾지 못함. (추가로 Always Allow 정규식 누락)
|
- **원인**: VS Code 네이티브 알림 및 채팅 패널 내의 버튼은 `<button>` 태그 대신 `<a role="button">`, `<vscode-button>` 등을 사용하는데, 기존 DOM scan 로직이 `querySelectorAll('button')`으로 하드코딩되어 노드를 아예 찾지 못함. (추가로 Always Allow 정규식 누락)
|
||||||
|
|||||||
@@ -4,3 +4,4 @@
|
|||||||
|-----|-------|----------|-----------|-----------|
|
|-----|-------|----------|-----------|-----------|
|
||||||
| 001 | 07:05 | v0.5.6 좀비 커넥션 패치 회귀 오류 해결 (False Positive 끊김 방지를 위한 타임스탬프 검증 도입 v0.5.8) | `TBD` | ✅ |
|
| 001 | 07:05 | v0.5.6 좀비 커넥션 패치 회귀 오류 해결 (False Positive 끊김 방지를 위한 타임스탬프 검증 도입 v0.5.8) | `TBD` | ✅ |
|
||||||
| 002 | 13:00 | DOM Observer VS Code 네이티브 알림 UI 캡처 블라인드 스팟 해결 (v0.5.9) | `7b6cd59` | ✅ |
|
| 002 | 13:00 | DOM Observer VS Code 네이티브 알림 UI 캡처 블라인드 스팟 해결 (v0.5.9) | `7b6cd59` | ✅ |
|
||||||
|
| 003 | 18:14 | DOM Observer /trigger-click 렌더링 순서 오작동 및 False Positive 프리징 해결 (v0.5.10) | `TBD` | ✅ |
|
||||||
|
|||||||
18
docs/devlog/entries/20260324-003.md
Normal file
18
docs/devlog/entries/20260324-003.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# DOM Observer /trigger-click 렌더링 순서 오작동 및 False Positive 프리징 해결 (v0.5.10)
|
||||||
|
|
||||||
|
- **시간**: 2026-03-24 17:50~18:20
|
||||||
|
- **Commit**: `HEAD` (예정)
|
||||||
|
- **Vikunja**: #514 관련 디버깅/핫픽스
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- **문제**: "0.5.9 패치한 이후 화면이 펜딩되서 움직여지지않아" 라는 증상 확인.
|
||||||
|
- v0.5.9에서 DOM 쿼리를 `[role="button"]` 등으로 확장했으나, 정규식이 `/^Run/i` 등으로 풀어진 상태여서 에디터 뷰의 "Run Test" 등 수많은 CodeLens 버튼들을 Agent의 트리거로 오인함.
|
||||||
|
- 결과적으로 아무 조작도 하지 않았는데 계속 터미널 실행 대기상태(Pending)로 무한 진입하여 UI 화면이 프리징(Freeze)됨.
|
||||||
|
- 특히 디스코드에서 `Approve` 명령을 내렸을 때도, DOM 트리상 상단에 우연히 "Run" CodeLens가 있으면 먼저 캡처되어 진짜 Agent 패널의 버튼을 클릭하지 못하고 엉뚱한 요소를 클릭하는 위험한 순위 불일치 버그까지 있었음.
|
||||||
|
- **해결책 (Structural Context Filtering)**:
|
||||||
|
1. 감지(Scan): 단순 정규식을 빡빡하게 변경하면 동적인 버튼 이름("Run script" 등)이 안 먹히는 부작용이 있으므로 느슨함을 유지하되, **발생 영역(DOM Context)**에 강제 필터를 부여.
|
||||||
|
- `isVSCodeMainWindow` 및 노드 루트가 `document.body`인지를 체크하여, 에디터 본문 영역 안에서는 "Run", "Approve", "Accept" 캡처를 전부 무시.
|
||||||
|
2. 제어(Trigger-click 우선순위): `observer-script.ts`의 `deepFindButtons()` 내부 스캔 트리를 변경하여 `findPanel()`로 안티그래비티 패널을 1순위로 조회, 알림 Toast를 2순위, 본문 Document를 3순위로 탐색하게 강제하여 엉뚱한 버튼 클릭 사고를 100% 방지함.
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- 없음 (빌드 및 검증 완료)
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"displayName": "Gravity Bridge",
|
"displayName": "Gravity Bridge",
|
||||||
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
||||||
"version": "0.5.9",
|
"version": "0.5.10",
|
||||||
"publisher": "variet",
|
"publisher": "variet",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.100.0"
|
"vscode": "^1.100.0"
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
|||||||
}
|
}
|
||||||
// "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 let through if session is stalled AND step_probe hasn't created a pending yet
|
||||||
if (/^Run$/i.test(cmd)) {
|
if (/^Run\b/i.test(cmd)) {
|
||||||
if (!ctx.sessionStalled || ctx.lastPendingStepIndex >= 0) {
|
if (!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'}`);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
|||||||
@@ -27,9 +27,21 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
// Searches: main document → iframes (contentDocument) → webview elements → shadow DOMs
|
// Searches: main document → iframes (contentDocument) → webview elements → shadow DOMs
|
||||||
function deepFindButtons(patterns){
|
function deepFindButtons(patterns){
|
||||||
var results=[];
|
var results=[];
|
||||||
// 1. Main document buttons
|
// 1. Prioritize Agent panel
|
||||||
|
var panel=findPanel();
|
||||||
|
if(panel){
|
||||||
|
collectButtons(panel,results,patterns,'panel');
|
||||||
|
if(results.length>0) return results;
|
||||||
|
}
|
||||||
|
// 2. Prioritize VS Code Toasts & Dialogs
|
||||||
|
var toasts=document.querySelectorAll('.notifications-toasts, .monaco-dialog-box');
|
||||||
|
for(var t=0;t<toasts.length;t++){
|
||||||
|
collectButtons(toasts[t],results,patterns,'toast');
|
||||||
|
}
|
||||||
|
if(results.length>0) return results;
|
||||||
|
// 3. Main document fallback
|
||||||
collectButtons(document,results,patterns,'main');
|
collectButtons(document,results,patterns,'main');
|
||||||
// 2. Iframe traversal (try contentDocument — works if same-origin or webSecurity off)
|
// 4. Iframe traversal (try contentDocument — works if same-origin or webSecurity off)
|
||||||
var iframes=document.querySelectorAll('iframe');
|
var iframes=document.querySelectorAll('iframe');
|
||||||
for(var i=0;i<iframes.length;i++){
|
for(var i=0;i<iframes.length;i++){
|
||||||
try{
|
try{
|
||||||
@@ -262,11 +274,11 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
// Negative/secondary buttons (Deny, Reject, Dismiss) will be collected as siblings.
|
// Negative/secondary buttons (Deny, Reject, Dismiss) will be collected as siblings.
|
||||||
var PATS=[
|
var PATS=[
|
||||||
{re:/^Run/i, type:'terminal_command'},
|
{re:/^Run/i, type:'terminal_command'},
|
||||||
{re:/^Accept all$/i, type:'diff_review'},
|
{re:/^Accept all/i, type:'diff_review'},
|
||||||
{re:/^Accept$/i, type:'agent_step'},
|
{re:/^Accept/i, type:'agent_step'},
|
||||||
{re:/^(?:Always )?Allow/i, type:'permission'},
|
{re:/^(?:Always )?Allow/i, type:'permission'},
|
||||||
{re:/^Approve/i, type:'agent_step'},
|
{re:/^Approve/i, type:'agent_step'},
|
||||||
{re:/^Retry$/i, type:'error_recovery'},
|
{re:/^Retry/i, type:'error_recovery'},
|
||||||
];
|
];
|
||||||
|
|
||||||
// ALL actionable button patterns (for grouping siblings in same container)
|
// ALL actionable button patterns (for grouping siblings in same container)
|
||||||
@@ -400,10 +412,25 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/,'').trim();
|
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/,'').trim();
|
||||||
if(!txt)continue;
|
if(!txt)continue;
|
||||||
|
|
||||||
|
var isBodyRoot = (searchRoots[r] === document.body);
|
||||||
|
var isVSCodeMainWindow = !!document.querySelector('.monaco-workbench');
|
||||||
|
|
||||||
// Match against patterns
|
// Match against patterns
|
||||||
var matchedType=null;
|
var matchedType=null;
|
||||||
for(var p=0;p<PATS.length;p++){
|
for(var p=0;p<PATS.length;p++){
|
||||||
if(PATS[p].re.test(txt)){matchedType=PATS[p].type;break;}
|
if(PATS[p].re.test(txt)){
|
||||||
|
// STRUCTURAL CONSTRAINT: If we are scanning the main VS Code Editor body, reject Agent/Terminal buttons
|
||||||
|
// to prevent freezing on CodeLens 'Run' or 'Accept' false positives.
|
||||||
|
if (isVSCodeMainWindow && isBodyRoot && PATS[p].type !== 'diff_review' && PATS[p].type !== 'permission') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Prevent duplicates if already scanned via panel root
|
||||||
|
if (isBodyRoot && panel && panel.contains(b)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
matchedType=PATS[p].type;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(!matchedType)continue;
|
if(!matchedType)continue;
|
||||||
|
|
||||||
@@ -618,7 +645,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
if(!d.action)return;
|
if(!d.action)return;
|
||||||
log('🔔 TRIGGER-CLICK received: action='+d.action);
|
log('🔔 TRIGGER-CLICK received: action='+d.action);
|
||||||
|
|
||||||
var approveRe=[/^Run$/i,/^Run /i,/^Accept/i,/^(?:Always )?Allow/i,/^Approve/i,/^Continue$/i,/^Proceed$/i,/^Retry$/i];
|
var approveRe=[/^Run/i,/^Accept/i,/^Accept all/i,/^(?:Always )?Allow/i,/^Approve/i,/^Continue$/i,/^Proceed$/i,/^Retry$/i];
|
||||||
var rejectRe=[/^Reject/i,/^Cancel$/i,/^Deny$/i,/^Stop$/i,/^Decline$/i,/^Dismiss$/i];
|
var rejectRe=[/^Reject/i,/^Cancel$/i,/^Deny$/i,/^Stop$/i,/^Decline$/i,/^Dismiss$/i];
|
||||||
var patterns=(d.action==='approve')?approveRe:rejectRe;
|
var patterns=(d.action==='approve')?approveRe:rejectRe;
|
||||||
var emoji=(d.action==='approve')?'✅':'❌';
|
var emoji=(d.action==='approve')?'✅':'❌';
|
||||||
|
|||||||
Reference in New Issue
Block a user