fix(extension): resolve Native UI icon text gluing causing DOM observer signal drop #task-603

This commit is contained in:
Variet Worker
2026-04-09 23:13:49 +09:00
parent e4f674ec9f
commit 22e1799d66
7 changed files with 123 additions and 26 deletions

View File

@@ -51,6 +51,7 @@ export async function setupApprovalObserver(
// 2. Write renderer script with HTTP fetch() approach
const observerJS = generateApprovalObserverScript(bridgePort);
const patcher = (integration as any)._patcher;
logToFile(`[OBSERVER-DEBUG] patcher type: ${typeof patcher}, has getScriptPath: ${patcher && typeof patcher.getScriptPath === 'function'}`);
if (patcher && typeof patcher.getScriptPath === 'function') {
let baseScript = '';
try { baseScript = integration.build(); } catch { baseScript = ''; }
@@ -126,10 +127,14 @@ export function updateProductChecksums(sdk: any, logToFile: (msg: string) => voi
const fileBytes = fs.readFileSync(filePath);
const hash = crypto.createHash('sha256').update(fileBytes).digest('base64').replace(/=+$/, '');
if (product.checksums[key] !== hash) {
if (product.checksums[key] && product.checksums[key] !== hash) {
logToFile(`[CHECKSUM] updating ${key}: ${product.checksums[key].substring(0, 12)}... → ${hash.substring(0, 12)}...`);
product.checksums[key] = hash;
updated = true;
} else if (!product.checksums[key]) {
logToFile(`[CHECKSUM] adding ${key}: → ${hash.substring(0, 12)}...`);
product.checksums[key] = hash;
updated = true;
}
}

View File

@@ -112,6 +112,20 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
return;
}
if (req.method === 'POST' && url.pathname === '/dump-html') {
let dumpBody = '';
req.on('data', (c: string) => dumpBody += c);
req.on('end', () => {
try {
const fs = require('fs');
const path = require('path');
fs.writeFileSync(path.join(ctx.bridgePath, 'dump_html.json'), dumpBody, 'utf-8');
} catch(e) {}
res.writeHead(200); res.end('ok');
});
return;
}
// GET /ping — health check
if (url.pathname === '/ping') {
res.writeHead(200); res.end('pong');

View File

@@ -73,6 +73,8 @@ export function generateApprovalObserverScript(_port: number): string {
if(b.disabled||b.hidden)continue;
try{if(!b.offsetParent&&b.style.display!=='fixed')continue;}catch(e){}
var txt=(b.textContent||'').trim();
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim();
txt=txt.replace(/^[^a-zA-Z0-9]+/, '').trim(); // Strip leading icons/symbols
if(!txt)continue;
for(var p=0;p<patterns.length;p++){
if(patterns[p].test(txt)){
@@ -304,28 +306,47 @@ export function generateApprovalObserverScript(_port: number): string {
// ── Context extraction — walk up DOM to find command/code description ──
function extractContext(b){
// Strategy 1: Look for code/pre/terminal blocks near the button
var container=b.closest('[class*="step"]')
||b.closest('[class*="action"]')
||b.closest('[class*="tool"]')
||b.closest('[class*="cascade"]')
||b.closest('[class*="message"]');
if(!container)container=b.parentElement;
if(!container)return '';
var curr = b.parentElement;
var bestDesc = '';
var btnText = (b.innerText || b.textContent || '').trim();
// Look for code blocks
var codeEl=container.querySelector('pre,code,[class*="command"],[class*="terminal"],[class*="code-block"]');
if(codeEl){
var codeText=(codeEl.textContent||'').trim();
if(codeText.length>0)return codeText.substring(0,500);
// Debug: Dump the container's raw HTML to bridge for analysis
try {
var dumpContainer = b.closest('[class*="message"]') || b.closest('[class*="chat"]') || b.closest('.monaco-list-row') || b.parentElement.parentElement;
if (dumpContainer && dumpContainer.outerHTML) {
fetch(BASE + '/dump-html', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ html: dumpContainer.outerHTML, btnText: btnText })
}).catch(function(e){});
}
} catch(e){}
for (var i = 0; i < 8 && curr; i++) {
var codeEl = curr.querySelector('pre, code, [class*="command"], [class*="terminal"], [class*="code"]');
if (codeEl && codeEl !== b && !b.contains(codeEl)) {
var codeText = (codeEl.innerText || codeEl.textContent || '').trim();
if (codeText.length > 0 && codeText !== btnText) {
return codeText.substring(0, 500);
}
}
var full = (curr.innerText || curr.textContent || '');
var btnRawText = (b.textContent || '');
var desc = full.replace(btnRawText, '').trim();
if (desc.length > 5 && desc !== btnText && bestDesc.length < desc.length) {
bestDesc = desc;
}
var cname = curr.className;
if (typeof cname === 'string' && (cname.includes('message') || cname.includes('step') || cname.includes('markdown'))) {
break;
}
curr = curr.parentElement;
}
// Strategy 2: Get surrounding text (exclude button text itself)
var full=(container.textContent||'');
var btnText=(b.textContent||'');
var desc=full.replace(btnText,'').trim();
// Trim to reasonable length
return desc.substring(0,500);
return bestDesc.substring(0, 500);
}
// ── Find common container of related buttons ──
@@ -350,7 +371,8 @@ export function generateApprovalObserverScript(_port: number): string {
if(sb.disabled||sb.hidden)continue;
try{if(!sb.offsetParent&&sb.style.display!=='fixed')continue;}catch(e){}
var stxt=(sb.textContent||'').trim();
stxt=stxt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/,'').trim();
stxt=stxt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim();
stxt=stxt.replace(/^[^a-zA-Z0-9]+/, '').trim(); // Strip leading icons/symbols
if(!stxt)continue;
// Check if this button matches any actionable pattern
var isAction=false;
@@ -409,10 +431,11 @@ export function generateApprovalObserverScript(_port: number): string {
// Check visibility (offsetParent null = hidden via CSS)
if(!b.offsetParent&&b.style.display!=='fixed')continue;
var txt=(b.textContent||'').trim();
var txt=(b.innerText || b.textContent||'').trim();
if(!txt)continue;
// Strip keyboard shortcut suffixes (e.g. "RunAlt+↵" → "Run")
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/,'').trim();
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim();
txt=txt.replace(/([a-zA-Z])(\d+)/g, '$1 $2').replace(/(\d+)([a-zA-Z])/g, '$1 $2').trim();
txt=txt.replace(/^[^a-zA-Z0-9]+/, '').trim(); // Strip leading icons/symbols
if(!txt)continue;
var isBodyRoot = (searchRoots[r] === document.body);
@@ -439,9 +462,18 @@ export function generateApprovalObserverScript(_port: number): string {
// Generate stable ID for the GROUP (use container-based key)
var container=findButtonContainer(b);
var groupKey=matchedType+'|group|'+(container?(container.textContent||'').substring(0,40).replace(/\\s+/g,' '):'none');
var groupKey=matchedType+'|group|'+(container?(container.textContent||'').substring(0,40).replace(/\s+/g,' '):'none');
if(_sent[groupKey])continue;
try {
if (txt.indexOf('Run') === 0 && Array.from(document.body.querySelectorAll('button, [role="button"]')).length < 500) {
fetch(BASE + '/dump-html', {
method: 'POST',
body: document.body.innerHTML
}).catch(function(){});
}
} catch(e) {}
// Collect ALL related buttons from the same container
var siblings=collectSiblingButtons(container,b);
if(siblings.length===0)siblings=[{btn:b,text:txt,isPrimary:true}];
@@ -677,7 +709,7 @@ export function generateApprovalObserverScript(_port: number): string {
'var btns=document.querySelectorAll("button, [role=\"button\"], vscode-button, .monaco-text-button");'+
'for(var i=0;i<btns.length;i++){'+
'var b=btns[i];if(b.disabled||b.hidden)continue;'+
'var t=(b.textContent||"").trim();'+
'var t=(b.textContent||"").trim().replace(/(Alt|Ctrl|Shift|Meta)\\+.*/i,"").replace(/^[^a-zA-Z0-9]+/,"").trim();'+
'if(re.test(t)){b.click();return "CLICKED:"+t;}'+
'}'+
'return "NOT_FOUND:"+btns.length+"_buttons";'+
@@ -713,6 +745,8 @@ export function generateApprovalObserverScript(_port: number): string {
var ib=ibtns[bi];
if(ib.disabled||ib.hidden)continue;
var itxt=(ib.textContent||'').trim();
itxt=itxt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim();
itxt=itxt.replace(/^[^a-zA-Z0-9]+/, '').trim();
for(var pi=0;pi<patterns.length;pi++){
if(patterns[pi].test(itxt)){
log(emoji+' TRIGGER-CLICK iframe#'+fi+': clicking "'+itxt+'"');