feat(sidebar): element inspector at top, services at bottom, CDP bridge as MCP
- Add Pick Element, Screenshot, Audit buttons at top of Tools tab - Pick Element injects overlay into active tab for visual element selection - Screenshot captures visible tab and copies to clipboard - Audit checks accessibility (missing alt, unlabeled buttons/inputs, etc.) - Move Hanzo Services panel to bottom (less important for daily frontend work) - Show CDP bridge and HTTP bridge (port 9224) in MCP Servers list - Sync manifest versions to 1.7.20, add cookies permission to Firefox
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Hanzo AI",
|
||||
"version": "1.7.14",
|
||||
"version": "1.7.20",
|
||||
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
@@ -9,7 +9,8 @@
|
||||
"storage",
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"scripting"
|
||||
"scripting",
|
||||
"cookies"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://localhost/*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Hanzo AI",
|
||||
"version": "1.7.16",
|
||||
"version": "1.7.20",
|
||||
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
||||
@@ -873,6 +873,89 @@ html {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/* Element Inspector Panel */
|
||||
.inspector-panel {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.inspector-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.inspector-btn {
|
||||
flex: 1;
|
||||
padding: 8px 6px;
|
||||
font-size: 11px;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
background: var(--glass-strong);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.inspector-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.inspector-btn.active {
|
||||
background: rgba(255,255,255,0.12);
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 8px rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.inspector-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inspector-result {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||
color: var(--text-secondary);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255,255,255,0.08) transparent;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.inspector-result .element-tag {
|
||||
color: #7cc4f8;
|
||||
}
|
||||
|
||||
.inspector-result .element-id {
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.inspector-result .element-class {
|
||||
color: #c3e88d;
|
||||
}
|
||||
|
||||
.inspector-result .element-size {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.inspector-result img {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tool-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -71,7 +71,80 @@
|
||||
<!-- Tools Tab -->
|
||||
<div id="tab-tools" class="tab-content hidden">
|
||||
<div class="tools-scroll">
|
||||
<!-- Hanzo Services -->
|
||||
<!-- Element Inspector (top priority for frontend work) -->
|
||||
<div class="panel inspector-panel">
|
||||
<div class="inspector-actions">
|
||||
<button class="action-btn inspector-btn" id="pick-element" title="Select element on page">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" width="14" height="14">
|
||||
<path d="M3 3l4 10 2-4 4-2L3 3z" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M9 9l4 4" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
Pick Element
|
||||
</button>
|
||||
<button class="action-btn inspector-btn" id="take-screenshot" title="Capture screenshot">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" width="14" height="14">
|
||||
<rect x="2" y="3" width="12" height="10" rx="1" stroke-width="1.5"/>
|
||||
<circle cx="8" cy="8" r="2" stroke-width="1.5"/>
|
||||
</svg>
|
||||
Screenshot
|
||||
</button>
|
||||
<button class="action-btn inspector-btn" id="run-audit" title="Accessibility & performance audit">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" width="14" height="14">
|
||||
<path d="M8 2v4l3 2" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="8" cy="8" r="6" stroke-width="1.5"/>
|
||||
</svg>
|
||||
Audit
|
||||
</button>
|
||||
</div>
|
||||
<div id="inspector-result" class="inspector-result hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- Connected MCP Servers -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>MCP Servers</h3>
|
||||
<span id="mcp-server-count" class="badge">0</span>
|
||||
</div>
|
||||
<div id="mcp-server-list" class="mcp-server-list">
|
||||
<div class="empty-state">No MCP servers connected</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Built-in + Discovered Tools -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Available Tools</h3>
|
||||
<span id="tool-count-badge" class="badge">0</span>
|
||||
</div>
|
||||
<div id="builtin-tool-list" class="tool-list"></div>
|
||||
<div id="mcp-tool-list" class="tool-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Active Agents -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Active Agents</h3>
|
||||
<span id="agent-count" class="badge">0</span>
|
||||
</div>
|
||||
<div id="agent-list" class="agent-list">
|
||||
<div class="empty-state">No active agents</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Filesystem -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Tab Filesystem</h3>
|
||||
<button class="icon-btn small" id="refresh-tabs" title="Refresh">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
||||
<path d="M14 2v5h-5M2 7a6 6 0 0111.5-2.5L14 5M2 14v-5h5M14 9a6 6 0 01-11.5 2.5L2 11" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="tab-fs" class="tab-filesystem"></div>
|
||||
</div>
|
||||
|
||||
<!-- Hanzo Services (moved to bottom) -->
|
||||
<div class="panel">
|
||||
<h3>Hanzo Services</h3>
|
||||
<div class="mcp-status">
|
||||
@@ -117,27 +190,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connected MCP Servers -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>MCP Servers</h3>
|
||||
<span id="mcp-server-count" class="badge">0</span>
|
||||
</div>
|
||||
<div id="mcp-server-list" class="mcp-server-list">
|
||||
<div class="empty-state">No MCP servers connected</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Built-in + Discovered Tools -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Available Tools</h3>
|
||||
<span id="tool-count-badge" class="badge">0</span>
|
||||
</div>
|
||||
<div id="builtin-tool-list" class="tool-list"></div>
|
||||
<div id="mcp-tool-list" class="tool-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Usage -->
|
||||
<div class="panel">
|
||||
<h3>Usage (1h)</h3>
|
||||
@@ -165,30 +217,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Agents -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Active Agents</h3>
|
||||
<span id="agent-count" class="badge">0</span>
|
||||
</div>
|
||||
<div id="agent-list" class="agent-list">
|
||||
<div class="empty-state">No active agents</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Filesystem -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Tab Filesystem</h3>
|
||||
<button class="icon-btn small" id="refresh-tabs" title="Refresh">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
||||
<path d="M14 2v5h-5M2 7a6 6 0 0111.5-2.5L14 5M2 14v-5h5M14 9a6 6 0 01-11.5 2.5L2 11" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="tab-fs" class="tab-filesystem"></div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="actions">
|
||||
<button id="launch-agent" class="action-btn">
|
||||
|
||||
@@ -78,6 +78,11 @@ class HanzoSidebar {
|
||||
tabFs: document.getElementById('tab-fs'),
|
||||
refreshTabs: document.getElementById('refresh-tabs'),
|
||||
launchAgent: document.getElementById('launch-agent'),
|
||||
// Inspector
|
||||
pickElement: document.getElementById('pick-element'),
|
||||
takeScreenshot: document.getElementById('take-screenshot'),
|
||||
runAudit: document.getElementById('run-audit'),
|
||||
inspectorResult: document.getElementById('inspector-result'),
|
||||
hanzoNodeStatus: document.getElementById('hanzo-node-status'),
|
||||
hanzoMcpStatus: document.getElementById('hanzo-mcp-status'),
|
||||
startHanzoNode: document.getElementById('start-hanzo-node'),
|
||||
@@ -150,6 +155,11 @@ class HanzoSidebar {
|
||||
this.el.startHanzoNode?.addEventListener('click', () => this.startHanzoService('node'));
|
||||
this.el.startHanzoMcp?.addEventListener('click', () => this.startHanzoService('bot'));
|
||||
|
||||
// Inspector
|
||||
this.el.pickElement?.addEventListener('click', () => this.pickElement());
|
||||
this.el.takeScreenshot?.addEventListener('click', () => this.takeScreenshot());
|
||||
this.el.runAudit?.addEventListener('click', () => this.runAudit());
|
||||
|
||||
// Settings
|
||||
this.el.logoutBtn.addEventListener('click', () => this.logout());
|
||||
this.el.saveSettings.addEventListener('click', () => this.saveSettings());
|
||||
@@ -846,6 +856,266 @@ class HanzoSidebar {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Element Inspector
|
||||
// ===========================================================================
|
||||
|
||||
async pickElement() {
|
||||
const btn = this.el.pickElement;
|
||||
if (!btn) return;
|
||||
btn.classList.toggle('active');
|
||||
|
||||
if (btn.classList.contains('active')) {
|
||||
// Inject picker script into active tab
|
||||
try {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (!tab?.id) return;
|
||||
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => {
|
||||
// Remove any existing picker
|
||||
document.getElementById('__hanzo_picker_overlay')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = '__hanzo_picker_overlay';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:2147483647;cursor:crosshair;';
|
||||
|
||||
const highlight = document.createElement('div');
|
||||
highlight.style.cssText = 'position:fixed;border:2px solid #4CAF50;background:rgba(76,175,80,0.1);pointer-events:none;transition:all 0.1s;z-index:2147483647;border-radius:2px;';
|
||||
overlay.appendChild(highlight);
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.style.cssText = 'position:fixed;background:#222;color:#fff;font:11px/1.4 monospace;padding:4px 8px;border-radius:4px;z-index:2147483647;pointer-events:none;max-width:300px;word-break:break-all;';
|
||||
overlay.appendChild(label);
|
||||
|
||||
let lastTarget: Element | null = null;
|
||||
|
||||
overlay.addEventListener('mousemove', (e) => {
|
||||
overlay.style.pointerEvents = 'none';
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
overlay.style.pointerEvents = '';
|
||||
if (!el || el === overlay) return;
|
||||
lastTarget = el;
|
||||
const rect = el.getBoundingClientRect();
|
||||
highlight.style.left = rect.left + 'px';
|
||||
highlight.style.top = rect.top + 'px';
|
||||
highlight.style.width = rect.width + 'px';
|
||||
highlight.style.height = rect.height + 'px';
|
||||
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const id = el.id ? `#${el.id}` : '';
|
||||
const cls = el.className && typeof el.className === 'string' ? '.' + el.className.trim().split(/\s+/).join('.') : '';
|
||||
const size = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
||||
label.textContent = `<${tag}${id}${cls}> ${size}`;
|
||||
label.style.left = Math.min(e.clientX + 12, window.innerWidth - 310) + 'px';
|
||||
label.style.top = Math.min(e.clientY + 12, window.innerHeight - 30) + 'px';
|
||||
});
|
||||
|
||||
overlay.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
overlay.remove();
|
||||
|
||||
if (!lastTarget) return;
|
||||
const el = lastTarget;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const computed = getComputedStyle(el);
|
||||
const info = {
|
||||
tag: el.tagName.toLowerCase(),
|
||||
id: el.id || null,
|
||||
classes: el.className && typeof el.className === 'string' ? el.className.trim().split(/\s+/) : [],
|
||||
text: (el.textContent || '').trim().substring(0, 100),
|
||||
href: (el as HTMLAnchorElement).href || null,
|
||||
src: (el as HTMLImageElement).src || null,
|
||||
rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
|
||||
styles: {
|
||||
color: computed.color,
|
||||
backgroundColor: computed.backgroundColor,
|
||||
fontSize: computed.fontSize,
|
||||
fontWeight: computed.fontWeight,
|
||||
display: computed.display,
|
||||
position: computed.position,
|
||||
},
|
||||
selector: (() => {
|
||||
if (el.id) return `#${el.id}`;
|
||||
let s = el.tagName.toLowerCase();
|
||||
if (el.className && typeof el.className === 'string') s += '.' + el.className.trim().split(/\s+/).join('.');
|
||||
return s;
|
||||
})(),
|
||||
};
|
||||
|
||||
// Send back to extension
|
||||
window.postMessage({ type: '__hanzo_pick_result', info }, '*');
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// ESC to cancel
|
||||
const escHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', escHandler); window.postMessage({ type: '__hanzo_pick_result', info: null }, '*'); }
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
},
|
||||
});
|
||||
|
||||
// Listen for result from content script
|
||||
const resultHandler = (message: any) => {
|
||||
if (message.action === 'inspector.pickResult') {
|
||||
chrome.runtime.onMessage.removeListener(resultHandler);
|
||||
btn.classList.remove('active');
|
||||
this.showInspectorResult(message.info);
|
||||
}
|
||||
};
|
||||
chrome.runtime.onMessage.addListener(resultHandler);
|
||||
|
||||
// Also inject a message relay
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => {
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.data?.type === '__hanzo_pick_result') {
|
||||
chrome.runtime.sendMessage({ action: 'inspector.pickResult', info: e.data.info });
|
||||
}
|
||||
}, { once: true });
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
debugLog('Pick element failed:', error);
|
||||
btn.classList.remove('active');
|
||||
this.showNotification('Cannot inspect this page');
|
||||
}
|
||||
} else {
|
||||
// Cancel picker — remove overlay from active tab
|
||||
try {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (tab?.id) {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => document.getElementById('__hanzo_picker_overlay')?.remove(),
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
showInspectorResult(info: any) {
|
||||
const el = this.el.inspectorResult;
|
||||
if (!el) return;
|
||||
|
||||
if (!info) {
|
||||
el.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = `<span class="element-tag"><${this.escapeHtml(info.tag)}></span>`;
|
||||
const id = info.id ? `<span class="element-id">#${this.escapeHtml(info.id)}</span>` : '';
|
||||
const cls = info.classes?.length ? `<span class="element-class">.${info.classes.map((c: string) => this.escapeHtml(c)).join('.')}</span>` : '';
|
||||
const size = `<span class="element-size">${info.rect?.w}x${info.rect?.h} @ (${info.rect?.x}, ${info.rect?.y})</span>`;
|
||||
const selector = info.selector ? `\nSelector: ${this.escapeHtml(info.selector)}` : '';
|
||||
const text = info.text ? `\nText: "${this.escapeHtml(info.text.substring(0, 60))}"` : '';
|
||||
const styles = info.styles ? `\nFont: ${info.styles.fontSize} ${info.styles.fontWeight}\nColor: ${info.styles.color}\nBg: ${info.styles.backgroundColor}` : '';
|
||||
|
||||
el.innerHTML = `${tag}${id}${cls}\n${size}${selector}${text}${styles}`;
|
||||
el.classList.remove('hidden');
|
||||
}
|
||||
|
||||
async takeScreenshot() {
|
||||
try {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (!tab?.id) return;
|
||||
|
||||
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' });
|
||||
const el = this.el.inspectorResult;
|
||||
if (el) {
|
||||
el.innerHTML = `<img src="${dataUrl}" alt="Screenshot" />\n<span class="element-size">Captured ${new Date().toLocaleTimeString()}</span>`;
|
||||
el.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Also copy to clipboard
|
||||
try {
|
||||
const response = await fetch(dataUrl);
|
||||
const blob = await response.blob();
|
||||
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
||||
this.showNotification('Screenshot copied to clipboard');
|
||||
} catch {
|
||||
this.showNotification('Screenshot captured');
|
||||
}
|
||||
} catch (error) {
|
||||
debugLog('Screenshot failed:', error);
|
||||
this.showNotification('Cannot capture this page');
|
||||
}
|
||||
}
|
||||
|
||||
async runAudit() {
|
||||
try {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (!tab?.id) return;
|
||||
|
||||
const el = this.el.inspectorResult;
|
||||
if (el) {
|
||||
el.textContent = 'Running audit...';
|
||||
el.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const [result] = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => {
|
||||
const audit: any = {};
|
||||
|
||||
// Images without alt
|
||||
const imgs = document.querySelectorAll('img:not([alt])');
|
||||
audit.missingAlt = imgs.length;
|
||||
|
||||
// Buttons without labels
|
||||
const btns = document.querySelectorAll('button:not([aria-label]):not([title])');
|
||||
const unlabeledBtns = Array.from(btns).filter(b => !(b.textContent || '').trim());
|
||||
audit.unlabeledButtons = unlabeledBtns.length;
|
||||
|
||||
// Links without href
|
||||
const emptyLinks = document.querySelectorAll('a:not([href]), a[href=""], a[href="#"]');
|
||||
audit.emptyLinks = emptyLinks.length;
|
||||
|
||||
// Form inputs without labels
|
||||
const inputs = document.querySelectorAll('input:not([type=hidden]):not([aria-label]):not([title])');
|
||||
const unlabeled = Array.from(inputs).filter(i => !document.querySelector(`label[for="${i.id}"]`));
|
||||
audit.unlabeledInputs = unlabeled.length;
|
||||
|
||||
// Contrast — check text readability
|
||||
audit.totalElements = document.querySelectorAll('*').length;
|
||||
audit.headings = document.querySelectorAll('h1,h2,h3,h4,h5,h6').length;
|
||||
audit.forms = document.querySelectorAll('form').length;
|
||||
|
||||
// Performance hints
|
||||
audit.scripts = document.querySelectorAll('script').length;
|
||||
audit.stylesheets = document.querySelectorAll('link[rel=stylesheet]').length;
|
||||
audit.inlineStyles = document.querySelectorAll('[style]').length;
|
||||
|
||||
return audit;
|
||||
},
|
||||
});
|
||||
|
||||
if (el && result?.result) {
|
||||
const a = result.result;
|
||||
const issues: string[] = [];
|
||||
if (a.missingAlt) issues.push(`${a.missingAlt} images missing alt text`);
|
||||
if (a.unlabeledButtons) issues.push(`${a.unlabeledButtons} buttons without labels`);
|
||||
if (a.emptyLinks) issues.push(`${a.emptyLinks} empty/hash links`);
|
||||
if (a.unlabeledInputs) issues.push(`${a.unlabeledInputs} inputs without labels`);
|
||||
|
||||
el.innerHTML =
|
||||
`<b>Page Audit</b>\n` +
|
||||
`Elements: ${a.totalElements} | Headings: ${a.headings} | Forms: ${a.forms}\n` +
|
||||
`Scripts: ${a.scripts} | Stylesheets: ${a.stylesheets} | Inline styles: ${a.inlineStyles}\n\n` +
|
||||
(issues.length ? `<b>Issues (${issues.length})</b>\n` + issues.map(i => ` - ${i}`).join('\n') : '<span style="color:var(--success)">No accessibility issues found</span>');
|
||||
}
|
||||
} catch (error) {
|
||||
debugLog('Audit failed:', error);
|
||||
this.showNotification('Cannot audit this page');
|
||||
}
|
||||
}
|
||||
|
||||
async refreshUsageMetrics() {
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({ action: 'usage.metrics' });
|
||||
@@ -903,6 +1173,8 @@ class HanzoSidebar {
|
||||
}
|
||||
|
||||
// Check hanzo bot/MCP via ZAP status
|
||||
const mcpServers: Array<{ id: string; name: string; tools: string[] }> = [];
|
||||
|
||||
try {
|
||||
const zapResp = await chrome.runtime.sendMessage({ action: 'zap.status' });
|
||||
const hasTools = zapResp?.success && zapResp.zap?.connected;
|
||||
@@ -916,18 +1188,49 @@ class HanzoSidebar {
|
||||
if (hasTools) this.el.startHanzoMcp.textContent = 'Running';
|
||||
}
|
||||
|
||||
// Update connected MCP servers list
|
||||
// Collect ZAP-discovered MCP servers
|
||||
if (zapResp?.success && zapResp.zap?.mcps?.length) {
|
||||
this.updateMCPServerList(zapResp.zap.mcps);
|
||||
} else {
|
||||
this.updateMCPServerList([]);
|
||||
mcpServers.push(...zapResp.zap.mcps);
|
||||
}
|
||||
} catch {
|
||||
if (this.el.hanzoMcpStatus) {
|
||||
this.el.hanzoMcpStatus.innerHTML = `<span class="status-indicator disconnected"></span> Stopped`;
|
||||
}
|
||||
this.updateMCPServerList([]);
|
||||
}
|
||||
|
||||
// Check CDP bridge (hanzo-mcp browser tools connection)
|
||||
try {
|
||||
const bridgeResp = await chrome.runtime.sendMessage({ action: 'bridge.status' });
|
||||
if (bridgeResp?.success && bridgeResp.connected) {
|
||||
mcpServers.push({
|
||||
id: 'cdp-bridge',
|
||||
name: 'Hanzo MCP (CDP Bridge)',
|
||||
tools: ['browser', 'navigate', 'screenshot', 'click', 'evaluate', 'fill', 'url', 'title'],
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Also probe hanzo-mcp HTTP bridge directly (port 9224)
|
||||
try {
|
||||
const bridgeHttp = await fetch('http://localhost:9224', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'status' }),
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
if (bridgeHttp.ok) {
|
||||
const data = await bridgeHttp.json();
|
||||
if (data.success && !mcpServers.find(m => m.id === 'cdp-bridge')) {
|
||||
mcpServers.push({
|
||||
id: 'hanzo-mcp-http',
|
||||
name: 'Hanzo MCP (HTTP :9224)',
|
||||
tools: ['browser', 'navigate', 'screenshot', 'evaluate', 'click'],
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
this.updateMCPServerList(mcpServers);
|
||||
}
|
||||
|
||||
async startHanzoService(service: string) {
|
||||
|
||||
Reference in New Issue
Block a user