fix(extension): support vscode native notification UI and Always Allow buttons for DOM observer (#514)

This commit is contained in:
Variet Worker
2026-03-24 13:58:21 +09:00
parent f13bcc871c
commit 7b6cd59801
5 changed files with 45 additions and 21 deletions

View File

@@ -27,7 +27,18 @@
--- ---
## 공통 이슈 ## 🔴 Active/Recent Issues
### [2026-03-24] DOM Observer — VS Code Native UI Blind Spot
- **증상**: "Always Allow" 및 일반 "Allow Alt+↵" 권한 알림 버튼이 디스코드 권한 센싱에서 완전히 누락됨.
- **원인**: VS Code 네이티브 알림 및 채팅 패널 내의 버튼은 `<button>` 태그 대신 `<a role="button">`, `<vscode-button>` 등을 사용하는데, 기존 DOM scan 로직이 `querySelectorAll('button')`으로 하드코딩되어 노드를 아예 찾지 못함. (추가로 Always Allow 정규식 누락)
- **해결** (v0.5.9): DOM scan, 리슨 훅 등 모든 탐색 로직 셀렉터를 `button, [role="button"], vscode-button, .monaco-text-button` 으로 전면 개편. 정규식을 `/^(?:Always )?Allow/i`로 수정.
### [2026-03-24] Python Hub — 좀비 커넥션 및 UI 프리징
- **증상**: `npm run` 명령이 `실행 정책` 관련 오류로 실패
- **원인**: PowerShell 스크립트 실행 정책이 제한적
- **해결**: `cmd /c npm run dev` 형식으로 cmd를 통해 실행
- **주의**: npm 관련 명령은 항상 `cmd /c` 접두어 사용 권장
### [2026-03-08] PowerShell curl — Invoke-WebRequest 충돌 ### [2026-03-08] PowerShell curl — Invoke-WebRequest 충돌
- **증상**: `curl` 명령이 예상과 다른 응답 형식을 반환 - **증상**: `curl` 명령이 예상과 다른 응답 형식을 반환
@@ -35,12 +46,6 @@
- **해결**: **`curl.exe`**를 명시적으로 사용 - **해결**: **`curl.exe`**를 명시적으로 사용
- **주의**: HTTP 관련 모든 명령에서 `curl.exe` 사용 필수 - **주의**: HTTP 관련 모든 명령에서 `curl.exe` 사용 필수
### [2026-03-08] PowerShell npm — 실행 정책 오류
- **증상**: `npm run` 명령이 `실행 정책` 관련 오류로 실패
- **원인**: PowerShell 스크립트 실행 정책이 제한적
- **해결**: `cmd /c npm run dev` 형식으로 cmd를 통해 실행
- **주의**: npm 관련 명령은 항상 `cmd /c` 접두어 사용 권장
--- ---
## 미해결 이슈 ## 미해결 이슈

View File

@@ -3,3 +3,4 @@
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 | | NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|-----|-------|----------|-----------|-----------| |-----|-------|----------|-----------|-----------|
| 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) | `TBD` | ✅ |

View File

@@ -0,0 +1,18 @@
# DOM Observer VS Code 네이티브 알림 UI 캡처 블라인드 스팟 해결 (v0.5.9)
- **시간**: 2026-03-24 12:00~13:00
- **Commit**: `TBD`
- **Vikunja**: TBD
## 결정 사항
- **문제**: "Always Allow" 및 "Allow Alt+↵" (단축키 포함) 권한 알람이 Discord로 전송되지 않는 문제가 발생했습니다. (v0.5.8)
- **근본 원인 확인**:
- Regex 실패: `Always Allow``^Allow` 정규식을 통과하지 못합니다.
- CSS Selector 실패: `observer-script.ts`의 스캔 엔진이 오직 `document.querySelectorAll('button')`에만 의존하여 렌더링 노드를 찾고 있었습니다. VS Code 네이티브 권한 프롬프트(토스트 알림 및 채팅 패널)는 `<a role="button" class="monaco-text-button">` 또는 `<vscode-button>`을 활용하므로 애초에 찾지도 못하고 스킵되었습니다.
- **해결책**:
1. `observer-script.ts` 내의 모든 DOM 쿼리를 `button, [role="button"], vscode-button, .monaco-text-button` 으로 확장.
2. 허용 권한 토큰 관련 정규식을 `/^(?:Always )?Allow/i` 로 상향 패치.
3. `v0.5.9` 로 빌드 및 VSIX 설치 완료 후 정상 동작 검증 완료.
## 미완료
- 없음.

View File

@@ -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.8", "version": "0.5.9",
"publisher": "variet", "publisher": "variet",
"engines": { "engines": {
"vscode": "^1.100.0" "vscode": "^1.100.0"

View File

@@ -55,7 +55,7 @@ export function generateApprovalObserverScript(_port: number): string {
function collectButtons(doc,results,patterns,source){ function collectButtons(doc,results,patterns,source){
if(!doc||!doc.querySelectorAll)return; if(!doc||!doc.querySelectorAll)return;
var btns=doc.querySelectorAll('button'); var btns=doc.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
for(var i=0;i<btns.length;i++){ for(var i=0;i<btns.length;i++){
var b=btns[i]; var b=btns[i];
if(b.disabled||b.hidden)continue; if(b.disabled||b.hidden)continue;
@@ -101,7 +101,7 @@ export function generateApprovalObserverScript(_port: number): string {
var allEls=doc.querySelectorAll('*'); var allEls=doc.querySelectorAll('*');
node.totalElements=allEls.length; node.totalElements=allEls.length;
// Buttons // Buttons
var btns=doc.querySelectorAll('button'); var btns=doc.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
for(var i=0;i<btns.length;i++){ for(var i=0;i<btns.length;i++){
var b=btns[i]; var b=btns[i];
var txt=(b.textContent||'').trim().substring(0,80); var txt=(b.textContent||'').trim().substring(0,80);
@@ -164,7 +164,7 @@ export function generateApprovalObserverScript(_port: number): string {
(function(wv,idx){ (function(wv,idx){
if(typeof wv.executeJavaScript!=='function'){result.webviewProbes.push({index:idx,error:'executeJavaScript not available'});probesPending--;if(probesPending<=0)postResults();return;} if(typeof wv.executeJavaScript!=='function'){result.webviewProbes.push({index:idx,error:'executeJavaScript not available'});probesPending--;if(probesPending<=0)postResults();return;}
try{ try{
wv.executeJavaScript('(function(){var btns=document.querySelectorAll("button");var allEls=document.querySelectorAll("*");var ifs=document.querySelectorAll("iframe");var wvs=document.querySelectorAll("webview");var btnArr=[];for(var i=0;i<btns.length;i++){var b=btns[i];var txt=(b.textContent||"").trim();var cls=(b.className||"").substring(0,50);var dis=b.disabled;var hid=b.hidden||!b.offsetParent;btnArr.push({text:txt.substring(0,60),class:cls,disabled:dis,hidden:hid,aria:b.getAttribute("aria-label")||"",title:b.getAttribute("title")||""});}var rbs=document.querySelectorAll("[role=button]");var rbArr=[];for(var j=0;j<rbs.length;j++){if(rbs[j].tagName!=="BUTTON")rbArr.push({tag:rbs[j].tagName.toLowerCase(),text:(rbs[j].textContent||"").trim().substring(0,40)});}var sc=0;for(var k=0;k<allEls.length;k++){if(allEls[k].shadowRoot)sc++;}return JSON.stringify({url:document.URL,title:document.title,totalElements:allEls.length,buttons:btnArr,roleBtns:rbArr,iframes:ifs.length,webviews:wvs.length,shadowDOMs:sc});})()') wv.executeJavaScript('(function(){var btns=document.querySelectorAll("button, [role=\"button\"], vscode-button, .monaco-text-button");var allEls=document.querySelectorAll("*");var ifs=document.querySelectorAll("iframe");var wvs=document.querySelectorAll("webview");var btnArr=[];for(var i=0;i<btns.length;i++){var b=btns[i];var txt=(b.textContent||"").trim();var cls=(b.className||"").substring(0,50);var dis=b.disabled;var hid=b.hidden||!b.offsetParent;btnArr.push({text:txt.substring(0,60),class:cls,disabled:dis,hidden:hid,aria:b.getAttribute("aria-label")||"",title:b.getAttribute("title")||""});}var rbs=document.querySelectorAll("[role=button]");var rbArr=[];for(var j=0;j<rbs.length;j++){if(rbs[j].tagName!=="BUTTON")rbArr.push({tag:rbs[j].tagName.toLowerCase(),text:(rbs[j].textContent||"").trim().substring(0,40)});}var sc=0;for(var k=0;k<allEls.length;k++){if(allEls[k].shadowRoot)sc++;}return JSON.stringify({url:document.URL,title:document.title,totalElements:allEls.length,buttons:btnArr,roleBtns:rbArr,iframes:ifs.length,webviews:wvs.length,shadowDOMs:sc});})()')
.then(function(r){ .then(function(r){
try{var d=JSON.parse(r);result.webviewProbes.push({index:idx,success:true,data:d});log('DEEP-INSPECT: webview#'+idx+' probe OK: '+d.buttons.length+' buttons, '+d.totalElements+' elements');}catch(e){result.webviewProbes.push({index:idx,parseError:e.message,raw:r});} try{var d=JSON.parse(r);result.webviewProbes.push({index:idx,success:true,data:d});log('DEEP-INSPECT: webview#'+idx+' probe OK: '+d.buttons.length+' buttons, '+d.totalElements+' elements');}catch(e){result.webviewProbes.push({index:idx,parseError:e.message,raw:r});}
probesPending--;if(probesPending<=0)postResults(); probesPending--;if(probesPending<=0)postResults();
@@ -264,13 +264,13 @@ export function generateApprovalObserverScript(_port: number): string {
{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:/^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)
var ALL_ACTION_RE=[/^Run/i,/^Accept/i,/^Reject/i,/^Allow/i,/^Deny/i,/^Approve/i,/^Cancel$/i,/^Retry$/i,/^Dismiss$/i,/^Stop$/i,/^Decline$/i]; var ALL_ACTION_RE=[/^Run/i,/^Accept/i,/^Reject/i,/^(?:Always )?Allow/i,/^Deny/i,/^Approve/i,/^Cancel$/i,/^Retry$/i,/^Dismiss$/i,/^Stop$/i,/^Decline$/i];
// Reject button patterns for finding the counterpart // Reject button patterns for finding the counterpart
var REJECT_RE=[/^reject$/i,/^reject all$/i,/^cancel$/i,/^deny$/i,/^stop$/i,/^decline$/i,/^dismiss$/i]; var REJECT_RE=[/^reject$/i,/^reject all$/i,/^cancel$/i,/^deny$/i,/^stop$/i,/^decline$/i,/^dismiss$/i];
@@ -284,7 +284,7 @@ export function generateApprovalObserverScript(_port: number): string {
// Also use DOM position: nth-child among sibling buttons // Also use DOM position: nth-child among sibling buttons
var idx=0; var idx=0;
if(parent){ if(parent){
var siblings=parent.querySelectorAll('button'); var siblings=parent.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
for(var i=0;i<siblings.length;i++){if(siblings[i]===b){idx=i;break;}} for(var i=0;i<siblings.length;i++){if(siblings[i]===b){idx=i;break;}}
} }
return type+'|'+txt+'|'+idx+'|'+pctx.substring(0,20); return type+'|'+txt+'|'+idx+'|'+pctx.substring(0,20);
@@ -331,7 +331,7 @@ export function generateApprovalObserverScript(_port: number): string {
// ── Collect all actionable sibling buttons from a container ── // ── Collect all actionable sibling buttons from a container ──
function collectSiblingButtons(container,triggerBtn){ function collectSiblingButtons(container,triggerBtn){
if(!container)return []; if(!container)return [];
var siblings=container.querySelectorAll('button'); var siblings=container.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
var result=[]; var result=[];
for(var i=0;i<siblings.length;i++){ for(var i=0;i<siblings.length;i++){
var sb=siblings[i]; var sb=siblings[i];
@@ -385,7 +385,7 @@ export function generateApprovalObserverScript(_port: number): string {
var seen={}; // dedupe buttons across search roots var seen={}; // dedupe buttons across search roots
for(var r=0;r<searchRoots.length;r++){ for(var r=0;r<searchRoots.length;r++){
var allBtns=searchRoots[r].querySelectorAll('button'); var allBtns=searchRoots[r].querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
if(!allBtns.length)continue; if(!allBtns.length)continue;
for(var j=0;j<allBtns.length;j++){ for(var j=0;j<allBtns.length;j++){
@@ -532,7 +532,7 @@ export function generateApprovalObserverScript(_port: number): string {
||approveBtn.parentElement; ||approveBtn.parentElement;
if(!container){log('No container for reject');return;} if(!container){log('No container for reject');return;}
var siblings=container.querySelectorAll('button'); var siblings=container.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
for(var i=0;i<siblings.length;i++){ for(var i=0;i<siblings.length;i++){
var t=(siblings[i].textContent||'').trim(); var t=(siblings[i].textContent||'').trim();
for(var r=0;r<REJECT_RE.length;r++){ for(var r=0;r<REJECT_RE.length;r++){
@@ -618,7 +618,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,/^Allow/i,/^Approve/i,/^Continue$/i,/^Proceed$/i,/^Retry$/i]; var approveRe=[/^Run$/i,/^Run /i,/^Accept/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')?'✅':'❌';
@@ -638,7 +638,7 @@ export function generateApprovalObserverScript(_port: number): string {
var patternsStr=patterns.map(function(re){return re.source;}).join('|'); var patternsStr=patterns.map(function(re){return re.source;}).join('|');
var clickScript='(function(){'+ var clickScript='(function(){'+
'var re=new RegExp("'+patternsStr+'","i");'+ 'var re=new RegExp("'+patternsStr+'","i");'+
'var btns=document.querySelectorAll("button");'+ 'var btns=document.querySelectorAll("button, [role=\"button\"], vscode-button, .monaco-text-button");'+
'for(var i=0;i<btns.length;i++){'+ 'for(var i=0;i<btns.length;i++){'+
'var b=btns[i];if(b.disabled||b.hidden)continue;'+ 'var b=btns[i];if(b.disabled||b.hidden)continue;'+
'var t=(b.textContent||"").trim();'+ 'var t=(b.textContent||"").trim();'+
@@ -672,7 +672,7 @@ export function generateApprovalObserverScript(_port: number): string {
try{ try{
var idoc=iframes[fi].contentDocument||iframes[fi].contentWindow.document; var idoc=iframes[fi].contentDocument||iframes[fi].contentWindow.document;
if(!idoc)continue; if(!idoc)continue;
var ibtns=idoc.querySelectorAll('button'); var ibtns=idoc.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
for(var bi=0;bi<ibtns.length;bi++){ for(var bi=0;bi<ibtns.length;bi++){
var ib=ibtns[bi]; var ib=ibtns[bi];
if(ib.disabled||ib.hidden)continue; if(ib.disabled||ib.hidden)continue;
@@ -692,7 +692,7 @@ export function generateApprovalObserverScript(_port: number): string {
if(!found.length){ if(!found.length){
// Log what we DID find for debugging // Log what we DID find for debugging
var allBtns=document.querySelectorAll('button'); var allBtns=document.querySelectorAll('button, [role="button"], vscode-button, .monaco-text-button');
var btnTexts=[]; var btnTexts=[];
for(var di=0;di<Math.min(10,allBtns.length);di++){ for(var di=0;di<Math.min(10,allBtns.length);di++){
btnTexts.push('"'+((allBtns[di].textContent||'').trim()).substring(0,30)+'"'); btnTexts.push('"'+((allBtns[di].textContent||'').trim()).substring(0,30)+'"');