Compare commits

...
13 Commits
Author SHA1 Message Date
Hanzo Dev b883a23cfd bump: v1.7.17 2026-03-06 03:56:08 -08:00
Hanzo Dev fb68636da9 feat(browser): add cookie, storage, IndexedDB, WebGPU, and performance APIs
Adds setCookie, deleteCookie, session/localStorage access, IndexedDB
query/list, clearSiteData, cache storage, WebGPU detection, hard reload,
service worker inspection, and performance metrics to BrowserControl.
2026-03-06 03:55:59 -08:00
Hanzo Dev d2a8547910 fix(browser): wire wait_for_selector, query_selector_all, get_element_info, get_page_info 2026-03-06 03:49:13 -08:00
Hanzo Dev d9d506e707 fix(browser): wire all hanzo.* commands end-to-end, zero gaps
- Forward hanzo.dblclick/hover/type/clear/select/check/uncheck through
  chrome.runtime to browser-control
- Add browser.dblclick, browser.clear, browser.check, browser.uncheck,
  browser.focus, browser.blur, browser.drag, browser.scrollIntoView
  to handleBrowserAction
- Add actionMap for naming mismatches (url→getURL, title→getTitle, etc.)
- Verified: 0 gaps in server→client→browser-control chain
2026-03-06 03:45:54 -08:00
Hanzo Dev 04a42286c8 feat(browser): navigation control, fetch through browser, history access
- Add goBack, goForward, reload, getURL, getTitle, getTabInfo,
  waitForNavigation, getHistory, createTab, closeTab actions
- Add browserFetch: fetch through page context with cookies/auth
- Wire all hanzo.* commands through CDP bridge client via
  chrome.runtime.sendMessage forwarding
- Wire navigation/fetch/history through CDP bridge server HTTP API
2026-03-06 03:43:45 -08:00
Hanzo Dev b4e34b2849 feat(browser): full DOM control, fix evaluate/screenshot result passthrough
- Fix CDP bridge HTTP layer double-wrapping results (evaluate returned
  {success,source} without actual data)
- Fix screenshot data not flowing through to MCP tool response
- Unwrap CDP Runtime.evaluate envelope to return actual JS values
- Add 20+ DOM manipulation methods: getHTML/setHTML, getText/setText,
  get/set/removeAttribute, setStyle, add/removeClass, insertElement,
  removeElement, observeMutations, getComputedStyles, getBoundingRects,
  injectScript/CSS, localStorage, cookies
- Wire all new methods through browser-control, CDP bridge, and MCP
- Add cookies permission to manifest
- Bump to v1.7.16
2026-03-06 03:33:18 -08:00
Hanzo Dev da91976f3e bump: v1.7.15 2026-03-06 00:55:58 -08:00
Hanzo Dev e6b369a5d2 chore: fix CI release artifacts, modernize sidebar tools, bump MCP to 2.2.2
- ci.yml: scope find pattern to hanzo-ai-* to prevent stale/duplicate release assets
- sidebar.ts: consolidate 10 tool categories into 5 (Core, Intelligence, Workflow, Dev, Cloud)
- mcp/package.json: bump to 2.2.2 with updated description
2026-03-06 00:39:28 -08:00
Hanzo Dev 6b31b22d0d fix(auth): update OAuth endpoints across all auth modules
Align with IAM backend route changes:
- Authorization: /login/oauth/authorize → /oauth/authorize
- Token: /api/login/oauth/access_token → /oauth/token

Applied consistently across browser extension (Chrome + Firefox),
CLI tools, and VS Code extension auth modules.
2026-03-06 00:37:10 -08:00
Hanzo Dev 21e012047b bump: v1.7.14 2026-03-05 19:14:30 -08:00
Hanzo Dev 6b8431a09b Redesign chat composer (Grok-style), fix Enter key submit
- Grok-style composer: single rounded box with textarea, model selector,
  RAG/Tab toggles, and circular send button all integrated
- Textarea expands naturally, no separate input row
- Model select is compact inline pill, not full-width dropdown
- Send button is small circle (not big white rectangle)
- Fix Enter key: double-check auth.status if this.authenticated is stale
- All controls inside one .composer-box with focus glow
2026-03-05 19:14:25 -08:00
Hanzo Dev 114d3c92d8 bump: v1.7.13 2026-03-05 19:06:16 -08:00
Hanzo Dev 966ec0fd60 v1.7.13: Wire up account display, add built-in tools, rename MCP to Bot
- Fix account panel: setUser() crashed on non-existent headerAvatar/userBadge
  elements, preventing name/email/avatar from displaying after login
- Account panel hidden by default, shown only when authenticated
- Rename "Start MCP" to "Start Bot" (extension natively supports MCP)
- Add "MCP Servers" panel showing ZAP-connected servers with status
- Add "Available Tools" panel with all 38 built-in @hanzo/mcp tools
  grouped by category (file-ops, search, shell, edit, AI, AST, vector, todo, modes)
- Null-safe all element references in setUser/logout/checkHanzoServices
- All 46 tests pass
2026-03-05 19:06:03 -08:00
20 changed files with 1215 additions and 181 deletions
+2 -1
View File
@@ -257,7 +257,8 @@ jobs:
- name: Collect release assets
run: |
mkdir -p release
find artifacts -type f \( -name '*.zip' -o -name '*.vsix' -o -name '*.dxt' \) -exec cp {} release/ \;
# Only collect artifacts with our expected naming patterns (prevents stale/duplicate files)
find artifacts -type f \( -name 'hanzo-ai-*.zip' -o -name 'hanzo-ai-*.vsix' -o -name 'hanzoai-*.dxt' \) -exec cp {} release/ \;
echo "Release assets:" && ls -la release/
- name: Create Release
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hanzo-ai-monorepo",
"version": "1.7.12",
"version": "1.7.17",
"private": true,
"description": "Hanzo AI Development Platform Monorepo",
"license": "MIT",
+1 -1
View File
@@ -34,7 +34,7 @@ descFn('Auth + Chat', () => {
await expect(loginPrompt).toBeVisible();
// Click "Sign in" — this sends auth.login to the background,
// which opens a new tab to hanzo.id/login/oauth/authorize
// which opens a new tab to hanzo.id/oauth/authorize
const [authPage] = await Promise.all([
context.waitForEvent('page'),
sidebar.locator('#auth-btn').click(),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hanzo/browser-extension",
"version": "1.7.12",
"version": "1.7.14",
"description": "Hanzo AI Browser Extension",
"main": "dist/background.js",
"scripts": {
+3 -3
View File
@@ -137,7 +137,7 @@ export async function login(): Promise<UserInfo> {
const redirectUri = getRedirectUri();
// Authorization Code + PKCE (OAuth 2.1 standard for public clients)
const authorizeUrl = new URL(`${IAM_LOGIN}/login/oauth/authorize`);
const authorizeUrl = new URL(`${IAM_LOGIN}/oauth/authorize`);
authorizeUrl.searchParams.set('client_id', CLIENT_ID);
authorizeUrl.searchParams.set('response_type', 'code');
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
@@ -168,7 +168,7 @@ export async function login(): Promise<UserInfo> {
await storeTokens(tokens);
} else if (code) {
// Authorization code flow — exchange code for tokens via PKCE
const tokenResponse = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
const tokenResponse = await fetch(`${IAM_API}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -309,7 +309,7 @@ export async function getValidAccessToken(): Promise<string | null> {
* Refresh access token using refresh_token grant.
*/
async function refreshAccessToken(refreshToken: string): Promise<string> {
const response = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
const response = await fetch(`${IAM_API}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
+2 -2
View File
@@ -527,7 +527,7 @@ async function firefoxLogin(): Promise<any> {
const state = generateRandomString(32);
const redirectUri = 'https://hanzo.ai/callback';
const authorizeUrl = new URL(`${IAM_LOGIN}/login/oauth/authorize`);
const authorizeUrl = new URL(`${IAM_LOGIN}/oauth/authorize`);
authorizeUrl.searchParams.set('client_id', CLIENT_ID);
authorizeUrl.searchParams.set('response_type', 'code');
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
@@ -582,7 +582,7 @@ async function firefoxLogin(): Promise<any> {
await browser.storage.local.set(data);
} else if (code) {
// Authorization code flow — exchange code for tokens via PKCE
const tokenResponse = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
const tokenResponse = await fetch(`${IAM_API}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
+619
View File
@@ -423,6 +423,425 @@ export class BrowserControl {
`);
}
// ===========================================================================
// DOM Read/Write/Observe
// ===========================================================================
async getHTML(tabId: number, selector: string, outer: boolean = true): Promise<string | null> {
return this.executeScript(tabId, `
(() => {
const el = document.querySelector(${JSON.stringify(selector)});
return el ? (${outer} ? el.outerHTML : el.innerHTML) : null;
})()
`);
}
async setHTML(tabId: number, selector: string, html: string, outer: boolean = false): Promise<boolean> {
return this.executeScript(tabId, `
(() => {
const el = document.querySelector(${JSON.stringify(selector)});
if (!el) return false;
${outer ? `el.outerHTML = ${JSON.stringify(html)};` : `el.innerHTML = ${JSON.stringify(html)};`}
return true;
})()
`);
}
async getText(tabId: number, selector: string): Promise<string | null> {
return this.executeScript(tabId, `
(() => {
const el = document.querySelector(${JSON.stringify(selector)});
return el ? el.textContent : null;
})()
`);
}
async setText(tabId: number, selector: string, text: string): Promise<boolean> {
return this.executeScript(tabId, `
(() => {
const el = document.querySelector(${JSON.stringify(selector)});
if (!el) return false;
el.textContent = ${JSON.stringify(text)};
return true;
})()
`);
}
async getAttribute(tabId: number, selector: string, attr: string): Promise<string | null> {
return this.executeScript(tabId, `
document.querySelector(${JSON.stringify(selector)})?.getAttribute(${JSON.stringify(attr)}) ?? null
`);
}
async setAttribute(tabId: number, selector: string, attr: string, value: string): Promise<boolean> {
return this.executeScript(tabId, `
(() => {
const el = document.querySelector(${JSON.stringify(selector)});
if (!el) return false;
el.setAttribute(${JSON.stringify(attr)}, ${JSON.stringify(value)});
return true;
})()
`);
}
async removeAttribute(tabId: number, selector: string, attr: string): Promise<boolean> {
return this.executeScript(tabId, `
(() => {
const el = document.querySelector(${JSON.stringify(selector)});
if (!el) return false;
el.removeAttribute(${JSON.stringify(attr)});
return true;
})()
`);
}
async setStyle(tabId: number, selector: string, styles: Record<string, string>): Promise<boolean> {
return this.executeScript(tabId, `
(() => {
const el = document.querySelector(${JSON.stringify(selector)});
if (!el) return false;
Object.assign(el.style, ${JSON.stringify(styles)});
return true;
})()
`);
}
async addClass(tabId: number, selector: string, ...classNames: string[]): Promise<boolean> {
return this.executeScript(tabId, `
(() => {
const el = document.querySelector(${JSON.stringify(selector)});
if (!el) return false;
el.classList.add(${classNames.map(c => JSON.stringify(c)).join(',')});
return true;
})()
`);
}
async removeClass(tabId: number, selector: string, ...classNames: string[]): Promise<boolean> {
return this.executeScript(tabId, `
(() => {
const el = document.querySelector(${JSON.stringify(selector)});
if (!el) return false;
el.classList.remove(${classNames.map(c => JSON.stringify(c)).join(',')});
return true;
})()
`);
}
async insertElement(tabId: number, parentSelector: string, html: string, position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' = 'beforeend'): Promise<boolean> {
return this.executeScript(tabId, `
(() => {
const parent = document.querySelector(${JSON.stringify(parentSelector)});
if (!parent) return false;
parent.insertAdjacentHTML(${JSON.stringify(position)}, ${JSON.stringify(html)});
return true;
})()
`);
}
async removeElement(tabId: number, selector: string): Promise<boolean> {
return this.executeScript(tabId, `
(() => {
const el = document.querySelector(${JSON.stringify(selector)});
if (!el) return false;
el.remove();
return true;
})()
`);
}
async observeMutations(tabId: number, selector: string, options: { attributes?: boolean; childList?: boolean; characterData?: boolean; subtree?: boolean } = {}, durationMs: number = 5000): Promise<any[]> {
const opts = { childList: true, subtree: true, attributes: true, ...options };
return this.executeScript(tabId, `
new Promise((resolve) => {
const target = ${selector === 'document' ? 'document.documentElement' : `document.querySelector(${JSON.stringify(selector)})`};
if (!target) { resolve([]); return; }
const mutations = [];
const observer = new MutationObserver((records) => {
for (const r of records) {
mutations.push({
type: r.type,
target: r.target.nodeName + (r.target.id ? '#' + r.target.id : ''),
added: r.addedNodes.length,
removed: r.removedNodes.length,
attribute: r.attributeName || null,
oldValue: r.oldValue?.substring(0, 200) || null,
});
if (mutations.length >= 200) { observer.disconnect(); resolve(mutations); }
}
});
observer.observe(target, ${JSON.stringify(opts)});
setTimeout(() => { observer.disconnect(); resolve(mutations); }, ${durationMs});
})
`);
}
async getComputedStyles(tabId: number, selector: string, properties?: string[]): Promise<Record<string, string> | null> {
return this.executeScript(tabId, `
(() => {
const el = document.querySelector(${JSON.stringify(selector)});
if (!el) return null;
const cs = window.getComputedStyle(el);
${properties ? `return Object.fromEntries(${JSON.stringify(properties)}.map(p => [p, cs.getPropertyValue(p)]));` : `
const props = ['display','visibility','opacity','position','width','height','margin','padding',
'color','background','font-size','font-weight','border','overflow','z-index','transform'];
return Object.fromEntries(props.map(p => [p, cs.getPropertyValue(p)]));`}
})()
`);
}
async getBoundingRects(tabId: number, selector: string): Promise<any[]> {
return this.executeScript(tabId, `
Array.from(document.querySelectorAll(${JSON.stringify(selector)})).slice(0, 50).map(el => {
const r = el.getBoundingClientRect();
return { tag: el.tagName.toLowerCase(), id: el.id, x: r.x, y: r.y, w: r.width, h: r.height };
})
`);
}
async injectScript(tabId: number, url: string): Promise<boolean> {
return this.executeScript(tabId, `
new Promise((resolve) => {
const s = document.createElement('script');
s.src = ${JSON.stringify(url)};
s.onload = () => resolve(true);
s.onerror = () => resolve(false);
document.head.appendChild(s);
})
`);
}
async injectCSS(tabId: number, css: string): Promise<boolean> {
return this.executeScript(tabId, `
(() => {
const style = document.createElement('style');
style.textContent = ${JSON.stringify(css)};
document.head.appendChild(style);
return true;
})()
`);
}
async getLocalStorage(tabId: number, key?: string): Promise<any> {
if (key) {
return this.executeScript(tabId, `localStorage.getItem(${JSON.stringify(key)})`);
}
return this.executeScript(tabId, `JSON.parse(JSON.stringify(localStorage))`);
}
async setLocalStorage(tabId: number, key: string, value: string): Promise<boolean> {
return this.executeScript(tabId, `
(() => { localStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)}); return true; })()
`);
}
async getCookies(tabId: number): Promise<any[]> {
return new Promise((resolve) => {
chrome.tabs.get(tabId, (tab) => {
if (!tab?.url) { resolve([]); return; }
chrome.cookies.getAll({ url: tab.url }, (cookies) => {
resolve(cookies || []);
});
});
});
}
async setCookie(tabId: number, cookie: { name: string; value: string; domain?: string; path?: string; secure?: boolean; httpOnly?: boolean; expirationDate?: number }): Promise<boolean> {
return new Promise((resolve) => {
chrome.tabs.get(tabId, (tab) => {
if (!tab?.url) { resolve(false); return; }
chrome.cookies.set({ url: tab.url, ...cookie }, (c) => resolve(!!c));
});
});
}
async deleteCookie(tabId: number, name: string): Promise<boolean> {
return new Promise((resolve) => {
chrome.tabs.get(tabId, (tab) => {
if (!tab?.url) { resolve(false); return; }
chrome.cookies.remove({ url: tab.url, name }, () => resolve(true));
});
});
}
async getSessionStorage(tabId: number, key?: string): Promise<any> {
if (key) {
return this.executeScript(tabId, `sessionStorage.getItem(${JSON.stringify(key)})`);
}
return this.executeScript(tabId, `JSON.parse(JSON.stringify(sessionStorage))`);
}
async setSessionStorage(tabId: number, key: string, value: string): Promise<boolean> {
return this.executeScript(tabId, `
(() => { sessionStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)}); return true; })()
`);
}
async getIndexedDBDatabases(tabId: number): Promise<any[]> {
return this.executeScript(tabId, `
(async () => {
const dbs = await indexedDB.databases();
return dbs.map(db => ({ name: db.name, version: db.version }));
})()
`);
}
async queryIndexedDB(tabId: number, dbName: string, storeName: string, query?: { key?: string; index?: string; count?: number }): Promise<any[]> {
const count = query?.count || 100;
return this.executeScript(tabId, `
new Promise((resolve, reject) => {
const req = indexedDB.open(${JSON.stringify(dbName)});
req.onerror = () => reject(new Error(req.error?.message || 'Failed to open DB'));
req.onsuccess = () => {
const db = req.result;
try {
const tx = db.transaction(${JSON.stringify(storeName)}, 'readonly');
const store = tx.objectStore(${JSON.stringify(storeName)});
const src = ${query?.index ? `store.index(${JSON.stringify(query.index)})` : 'store'};
const getReq = ${query?.key ? `src.get(${JSON.stringify(query.key)})` : `src.getAll(null, ${count})`};
getReq.onsuccess = () => resolve(${query?.key ? '[getReq.result]' : 'getReq.result'});
getReq.onerror = () => reject(new Error(getReq.error?.message || 'Query failed'));
} catch(e) { reject(e); }
};
})
`);
}
async getIndexedDBStores(tabId: number, dbName: string): Promise<string[]> {
return this.executeScript(tabId, `
new Promise((resolve, reject) => {
const req = indexedDB.open(${JSON.stringify(dbName)});
req.onerror = () => reject(new Error(req.error?.message || 'Failed to open DB'));
req.onsuccess = () => {
resolve(Array.from(req.result.objectStoreNames));
};
})
`);
}
async clearSiteData(tabId: number, what: ('cookies' | 'localStorage' | 'sessionStorage' | 'indexedDB' | 'cache' | 'all')[] = ['all']): Promise<Record<string, boolean>> {
const doAll = what.includes('all');
const results: Record<string, boolean> = {};
if (doAll || what.includes('cookies')) {
await new Promise<void>((resolve) => {
chrome.tabs.get(tabId, (tab) => {
if (!tab?.url) { results.cookies = false; resolve(); return; }
chrome.cookies.getAll({ url: tab.url }, (cookies) => {
Promise.all((cookies || []).map(c =>
new Promise<void>(r => chrome.cookies.remove({ url: tab.url!, name: c.name }, () => r()))
)).then(() => { results.cookies = true; resolve(); });
});
});
});
}
if (doAll || what.includes('localStorage')) {
results.localStorage = await this.executeScript(tabId, `(() => { localStorage.clear(); return true; })()`);
}
if (doAll || what.includes('sessionStorage')) {
results.sessionStorage = await this.executeScript(tabId, `(() => { sessionStorage.clear(); return true; })()`);
}
if (doAll || what.includes('indexedDB')) {
results.indexedDB = await this.executeScript(tabId, `
(async () => {
const dbs = await indexedDB.databases();
await Promise.all(dbs.map(db => new Promise((r, j) => {
const req = indexedDB.deleteDatabase(db.name);
req.onsuccess = () => r(true);
req.onerror = () => r(false);
})));
return true;
})()
`);
}
if (doAll || what.includes('cache')) {
results.cache = await this.executeScript(tabId, `
(async () => {
const keys = await caches.keys();
await Promise.all(keys.map(k => caches.delete(k)));
return true;
})()
`);
}
return results;
}
async getCacheStorageKeys(tabId: number): Promise<string[]> {
return this.executeScript(tabId, `caches.keys()`);
}
async checkWebGPU(tabId: number): Promise<{ available: boolean; adapter?: string; features?: string[]; limits?: Record<string, number> }> {
return this.executeScript(tabId, `
(async () => {
if (!navigator.gpu) return { available: false, reason: 'WebGPU not supported' };
try {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) return { available: false, reason: 'No GPU adapter' };
const info = await adapter.requestAdapterInfo();
return {
available: true,
adapter: info.description || info.device || info.vendor || 'Unknown',
vendor: info.vendor,
architecture: info.architecture,
features: [...adapter.features].sort(),
limits: {
maxBufferSize: adapter.limits.maxBufferSize,
maxTextureDimension2D: adapter.limits.maxTextureDimension2D,
maxComputeWorkgroupSizeX: adapter.limits.maxComputeWorkgroupSizeX,
maxBindGroups: adapter.limits.maxBindGroups,
maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
},
};
} catch(e) { return { available: false, reason: e.message }; }
})()
`);
}
async hardReload(tabId: number): Promise<void> {
chrome.tabs.reload(tabId, { bypassCache: true });
}
async getServiceWorkers(tabId: number): Promise<any[]> {
return this.executeScript(tabId, `
(async () => {
const regs = await navigator.serviceWorker?.getRegistrations() || [];
return regs.map(r => ({
scope: r.scope,
active: r.active ? { state: r.active.state, scriptURL: r.active.scriptURL } : null,
waiting: r.waiting ? { state: r.waiting.state } : null,
installing: r.installing ? { state: r.installing.state } : null,
}));
})()
`);
}
async getPerformanceMetrics(tabId: number): Promise<any> {
return this.executeScript(tabId, `
(() => {
const nav = performance.getEntriesByType('navigation')[0] || {};
const paint = performance.getEntriesByType('paint');
const resources = performance.getEntriesByType('resource');
return {
timing: {
domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
load: Math.round(nav.loadEventEnd - nav.startTime),
ttfb: Math.round(nav.responseStart - nav.startTime),
domInteractive: Math.round(nav.domInteractive - nav.startTime),
},
paint: Object.fromEntries(paint.map(p => [p.name, Math.round(p.startTime)])),
resourceCount: resources.length,
transferSize: resources.reduce((s, r) => s + (r.transferSize || 0), 0),
memory: performance.memory ? {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
} : null,
};
})()
`);
}
// ===========================================================================
// Navigation
// ===========================================================================
@@ -451,6 +870,85 @@ export class BrowserControl {
});
}
async getURL(tabId: number): Promise<string> {
return new Promise((resolve) => {
chrome.tabs.get(tabId, (tab) => resolve(tab?.url || ''));
});
}
async getTitle(tabId: number): Promise<string> {
return new Promise((resolve) => {
chrome.tabs.get(tabId, (tab) => resolve(tab?.title || ''));
});
}
async getTabInfo(tabId: number): Promise<any> {
return new Promise((resolve) => {
chrome.tabs.get(tabId, (tab) => resolve({
id: tab?.id,
url: tab?.url,
title: tab?.title,
status: tab?.status,
active: tab?.active,
index: tab?.index,
pinned: tab?.pinned,
incognito: tab?.incognito,
}));
});
}
async getHistory(tabId: number, maxResults: number = 50): Promise<any[]> {
return this.executeScript(tabId, `
(() => {
const entries = performance.getEntriesByType('navigation');
return {
length: history.length,
scrollRestoration: history.scrollRestoration,
current: location.href,
navigation: entries.map(e => ({
type: e.type,
redirectCount: e.redirectCount,
duration: Math.round(e.duration),
})),
};
})()
`);
}
async browserFetch(tabId: number, url: string, options?: {
method?: string;
headers?: Record<string, string>;
body?: string;
mode?: string;
credentials?: string;
}): Promise<{ status: number; statusText: string; headers: Record<string, string>; body: string; url: string }> {
const opts = {
method: options?.method || 'GET',
headers: options?.headers || {},
body: options?.body,
mode: options?.mode || 'cors',
credentials: options?.credentials || 'include',
};
return this.executeScript(tabId, `
(async () => {
const opts = ${JSON.stringify(opts)};
if (!opts.body) delete opts.body;
const res = await fetch(${JSON.stringify(url)}, opts);
const headers = {};
res.headers.forEach((v, k) => headers[k] = v);
const ct = res.headers.get('content-type') || '';
let body;
if (ct.includes('json')) {
body = JSON.stringify(await res.json());
} else {
body = await res.text();
}
if (body.length > 500000) body = body.substring(0, 500000) + '...(truncated)';
return { status: res.status, statusText: res.statusText, headers, body, url: res.url };
})()
`);
}
async closeTab(tabId: number): Promise<void> {
chrome.tabs.remove(tabId);
}
@@ -692,9 +1190,33 @@ export class BrowserControl {
case 'browser.scroll':
sendResponse({ success: await this.scroll(tabId, request.x, request.y, request.selector) });
break;
case 'browser.dblclick':
sendResponse({ success: await this.doubleClick(tabId, request.selector) });
break;
case 'browser.hover':
sendResponse({ success: await this.hover(tabId, request.selector) });
break;
case 'browser.clear':
sendResponse({ success: await this.fill(tabId, request.selector, '') });
break;
case 'browser.check':
sendResponse({ success: await this.check(tabId, request.selector, true) });
break;
case 'browser.uncheck':
sendResponse({ success: await this.check(tabId, request.selector, false) });
break;
case 'browser.focus':
sendResponse({ success: await this.focus(tabId, request.selector) });
break;
case 'browser.blur':
sendResponse({ success: await this.blur(tabId, request.selector) });
break;
case 'browser.drag':
sendResponse({ success: await this.drag(tabId, request.from || request.selector, request.to) });
break;
case 'browser.scrollIntoView':
sendResponse({ success: await this.scrollIntoView(tabId, request.selector) });
break;
case 'browser.pressKey':
sendResponse({ success: await this.pressKey(tabId, request.key, request.modifiers) });
break;
@@ -708,6 +1230,43 @@ export class BrowserControl {
await this.navigateTo(tabId, request.url);
sendResponse({ success: true });
break;
case 'browser.goBack':
await this.goBack(tabId);
sendResponse({ success: true });
break;
case 'browser.goForward':
await this.goForward(tabId);
sendResponse({ success: true });
break;
case 'browser.reload':
await this.reload(tabId);
sendResponse({ success: true });
break;
case 'browser.getURL':
sendResponse({ success: true, url: await this.getURL(tabId) });
break;
case 'browser.getTitle':
sendResponse({ success: true, title: await this.getTitle(tabId) });
break;
case 'browser.getTabInfo':
sendResponse({ success: true, info: await this.getTabInfo(tabId) });
break;
case 'browser.waitForNavigation':
sendResponse({ success: await this.waitForNavigation(tabId, request.timeout) });
break;
case 'browser.getHistory':
sendResponse({ success: true, history: await this.getHistory(tabId, request.maxResults) });
break;
case 'browser.fetch':
sendResponse({ success: true, response: await this.browserFetch(tabId, request.url, request.options) });
break;
case 'browser.createTab':
sendResponse({ success: true, tabId: await this.createTab(request.url || 'about:blank', request.active !== false) });
break;
case 'browser.closeTab':
await this.closeTab(tabId);
sendResponse({ success: true });
break;
case 'browser.waitForSelector':
sendResponse({ success: await this.waitForSelector(tabId, request.selector, request.timeout) });
break;
@@ -720,6 +1279,66 @@ export class BrowserControl {
case 'browser.querySelectorAll':
sendResponse({ success: true, elements: await this.querySelectorAll(tabId, request.selector) });
break;
case 'browser.getHTML':
sendResponse({ success: true, html: await this.getHTML(tabId, request.selector, request.outer !== false) });
break;
case 'browser.setHTML':
sendResponse({ success: await this.setHTML(tabId, request.selector, request.html, request.outer) });
break;
case 'browser.getText':
sendResponse({ success: true, text: await this.getText(tabId, request.selector) });
break;
case 'browser.setText':
sendResponse({ success: await this.setText(tabId, request.selector, request.text) });
break;
case 'browser.getAttribute':
sendResponse({ success: true, value: await this.getAttribute(tabId, request.selector, request.attr) });
break;
case 'browser.setAttribute':
sendResponse({ success: await this.setAttribute(tabId, request.selector, request.attr, request.value) });
break;
case 'browser.removeAttribute':
sendResponse({ success: await this.removeAttribute(tabId, request.selector, request.attr) });
break;
case 'browser.setStyle':
sendResponse({ success: await this.setStyle(tabId, request.selector, request.styles) });
break;
case 'browser.addClass':
sendResponse({ success: await this.addClass(tabId, request.selector, ...(request.classNames || [])) });
break;
case 'browser.removeClass':
sendResponse({ success: await this.removeClass(tabId, request.selector, ...(request.classNames || [])) });
break;
case 'browser.insertElement':
sendResponse({ success: await this.insertElement(tabId, request.selector, request.html, request.position) });
break;
case 'browser.removeElement':
sendResponse({ success: await this.removeElement(tabId, request.selector) });
break;
case 'browser.observeMutations':
sendResponse({ success: true, mutations: await this.observeMutations(tabId, request.selector || 'document', request.options, request.duration) });
break;
case 'browser.getComputedStyles':
sendResponse({ success: true, styles: await this.getComputedStyles(tabId, request.selector, request.properties) });
break;
case 'browser.getBoundingRects':
sendResponse({ success: true, rects: await this.getBoundingRects(tabId, request.selector) });
break;
case 'browser.injectScript':
sendResponse({ success: await this.injectScript(tabId, request.url) });
break;
case 'browser.injectCSS':
sendResponse({ success: await this.injectCSS(tabId, request.css) });
break;
case 'browser.getLocalStorage':
sendResponse({ success: true, data: await this.getLocalStorage(tabId, request.key) });
break;
case 'browser.setLocalStorage':
sendResponse({ success: await this.setLocalStorage(tabId, request.key, request.value) });
break;
case 'browser.getCookies':
sendResponse({ success: true, cookies: await this.getCookies(tabId) });
break;
default:
sendResponse({ success: false, error: `Unknown action: ${request.action}` });
}
+151 -10
View File
@@ -208,17 +208,20 @@ class CDPBridgeServer {
});
// Screenshots
case 'screenshot':
case 'screenshot': {
const screenshot = await this.sendRaw('hanzo.screenshot', {
format: rest.format || 'png',
fullPage: rest.fullPage
fullPage: rest.fullPage,
...(rest.tabId ? { tabId: rest.tabId } : {}),
...(rest.tabIndex !== undefined ? { tabIndex: rest.tabIndex } : {}),
});
if (rest.filename && screenshot.data) {
if (rest.filename && screenshot?.data) {
const buffer = Buffer.from(screenshot.data, 'base64');
fs.writeFileSync(rest.filename, buffer);
return { saved: rest.filename, bytes: buffer.length };
return { saved: rest.filename, bytes: buffer.length, data: screenshot.data };
}
return screenshot;
}
case 'snapshot':
// Accessibility snapshot
@@ -283,12 +286,139 @@ class CDPBridgeServer {
selector: rest.selector || rest.ref
});
// Evaluate
case 'evaluate':
return this.sendRaw('Runtime.evaluate', {
expression: rest.code || rest.function || rest.expression,
returnByValue: true
// Navigation
case 'go_back':
case 'navigate_back':
return this.sendRaw('Page.goBack');
case 'go_forward':
case 'navigate_forward':
return this.sendRaw('Page.goForward');
case 'get_url':
return this.sendRaw('hanzo.url');
case 'get_title':
return this.sendRaw('hanzo.title');
case 'get_tab_info':
case 'tab_info':
return this.sendRaw('hanzo.tabInfo');
case 'get_history':
case 'history':
return this.sendRaw('hanzo.getHistory', { maxResults: rest.maxResults });
case 'wait_for_navigation':
return this.sendRaw('hanzo.waitForNavigation', { timeout: rest.timeout });
case 'create_tab':
return this.sendRaw('Target.createTarget', { url: rest.url || 'about:blank' });
case 'close_tab':
return this.sendRaw('Target.closeTarget', { targetId: rest.tabId });
// Fetch through browser context (uses page cookies/auth)
case 'fetch': {
const fetchResult = await this.sendRaw('hanzo.fetch', {
url: rest.url,
options: {
method: rest.method || rest.options?.method,
headers: rest.headers || rest.options?.headers,
body: rest.body || rest.options?.body,
mode: rest.mode || rest.options?.mode,
credentials: rest.credentials || rest.options?.credentials,
},
});
return fetchResult;
}
// Selectors / Waiting
case 'wait_for_selector':
return this.sendRaw('hanzo.waitForSelector', { selector: rest.selector, timeout: rest.timeout });
case 'query_selector_all':
return this.sendRaw('hanzo.querySelectorAll', { selector: rest.selector });
case 'get_element_info':
return this.sendRaw('hanzo.getElementInfo', { selector: rest.selector });
case 'get_page_info':
return this.sendRaw('hanzo.getPageInfo', {});
// DOM Read/Write/Observe
case 'get_html':
return this.sendRaw('hanzo.getHTML', { selector: rest.selector, outer: rest.outer });
case 'set_html':
return this.sendRaw('hanzo.setHTML', { selector: rest.selector, html: rest.html, outer: rest.outer });
case 'get_text':
return this.sendRaw('hanzo.getText', { selector: rest.selector });
case 'set_text':
return this.sendRaw('hanzo.setText', { selector: rest.selector, text: rest.text });
case 'get_attribute':
return this.sendRaw('hanzo.getAttribute', { selector: rest.selector, attr: rest.attr });
case 'set_attribute':
return this.sendRaw('hanzo.setAttribute', { selector: rest.selector, attr: rest.attr, value: rest.value });
case 'remove_attribute':
return this.sendRaw('hanzo.removeAttribute', { selector: rest.selector, attr: rest.attr });
case 'set_style':
return this.sendRaw('hanzo.setStyle', { selector: rest.selector, styles: rest.styles });
case 'add_class':
return this.sendRaw('hanzo.addClass', { selector: rest.selector, classNames: rest.classNames });
case 'remove_class':
return this.sendRaw('hanzo.removeClass', { selector: rest.selector, classNames: rest.classNames });
case 'insert_element':
return this.sendRaw('hanzo.insertElement', { selector: rest.selector, html: rest.html, position: rest.position });
case 'remove_element':
return this.sendRaw('hanzo.removeElement', { selector: rest.selector });
case 'observe_mutations':
return this.sendRaw('hanzo.observeMutations', { selector: rest.selector, options: rest.options, duration: rest.duration });
case 'computed_styles':
return this.sendRaw('hanzo.getComputedStyles', { selector: rest.selector, properties: rest.properties });
case 'bounding_rects':
return this.sendRaw('hanzo.getBoundingRects', { selector: rest.selector });
case 'inject_script':
return this.sendRaw('hanzo.injectScript', { url: rest.url });
case 'inject_css':
return this.sendRaw('hanzo.injectCSS', { css: rest.css });
case 'local_storage':
if (rest.value !== undefined) {
return this.sendRaw('hanzo.setLocalStorage', { key: rest.key, value: rest.value });
}
return this.sendRaw('hanzo.getLocalStorage', { key: rest.key });
case 'cookies':
return this.sendRaw('hanzo.getCookies', {});
// Evaluate
case 'evaluate': {
const evalResult = await this.sendRaw('Runtime.evaluate', {
expression: rest.code || rest.function || rest.expression,
returnByValue: true,
...(rest.tabId ? { tabId: rest.tabId } : {}),
...(rest.tabIndex !== undefined ? { tabIndex: rest.tabIndex } : {}),
});
// Unwrap CDP result envelope: {result: {type, value}} → value
const value = evalResult?.result?.value ?? evalResult?.result ?? evalResult;
return { result: value };
}
// Wait
case 'wait':
@@ -358,7 +488,8 @@ async function startJSONRPCServer(bridgeServer: CDPBridgeServer) {
const request = JSON.parse(body);
const result = await bridgeServer.browser(request.params || request);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ result }));
// Return result directly (no extra wrapping — Python expects flat dict)
res.end(JSON.stringify(result && typeof result === 'object' ? result : { result }));
} catch (error: any) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: error.message }));
@@ -377,6 +508,16 @@ async function startJSONRPCServer(bridgeServer: CDPBridgeServer) {
'click', 'dblclick', 'hover', 'type', 'fill', 'clear', 'press_key',
'select_option', 'check', 'uncheck',
'evaluate',
'go_back', 'go_forward', 'get_url', 'get_title', 'get_tab_info',
'wait_for_navigation', 'get_history', 'create_tab', 'close_tab',
'fetch',
'get_html', 'set_html', 'get_text', 'set_text',
'get_attribute', 'set_attribute', 'remove_attribute',
'set_style', 'add_class', 'remove_class',
'insert_element', 'remove_element',
'observe_mutations', 'computed_styles', 'bounding_rects',
'inject_script', 'inject_css',
'local_storage', 'cookies',
'wait', 'wait_for_load',
'tabs', 'new_tab', 'close_tab', 'select_tab',
'console', 'network_requests',
+98
View File
@@ -404,6 +404,104 @@ export class CDPBridge {
result = { data: screenshotData };
break;
// Navigation
case 'Page.goBack':
await chrome.tabs.goBack(tabId);
result = { success: true };
break;
case 'Page.goForward':
await chrome.tabs.goForward(tabId);
result = { success: true };
break;
case 'Page.reload':
await chrome.tabs.reload(tabId);
result = { success: true };
break;
case 'hanzo.url':
result = { result: { value: (await chrome.tabs.get(tabId))?.url || '' } };
break;
case 'hanzo.title':
result = { result: { value: (await chrome.tabs.get(tabId))?.title || '' } };
break;
case 'hanzo.tabInfo': {
const tab = await chrome.tabs.get(tabId);
result = { id: tab?.id, url: tab?.url, title: tab?.title, status: tab?.status, active: tab?.active, index: tab?.index };
break;
}
case 'hanzo.waitForNavigation':
result = await new Promise((resolve) => {
const timeout = setTimeout(() => {
chrome.tabs.onUpdated.removeListener(listener);
resolve({ success: false });
}, params?.timeout || 30000);
const listener = (updatedId: number, info: chrome.tabs.TabChangeInfo) => {
if (updatedId === tabId && info.status === 'complete') {
clearTimeout(timeout);
chrome.tabs.onUpdated.removeListener(listener);
resolve({ success: true });
}
};
chrome.tabs.onUpdated.addListener(listener);
});
break;
// High-level hanzo.* commands — forward to BrowserControl via executeScript
// Forward hanzo.* commands to BrowserControl via chrome.runtime
// (hanzo.click, hanzo.fill, hanzo.screenshot handled above with overlay)
case 'hanzo.dblclick':
case 'hanzo.hover':
case 'hanzo.type':
case 'hanzo.clear':
case 'hanzo.select':
case 'hanzo.check':
case 'hanzo.uncheck':
case 'hanzo.waitForSelector':
case 'hanzo.querySelectorAll':
case 'hanzo.getElementInfo':
case 'hanzo.getPageInfo':
case 'hanzo.getHTML':
case 'hanzo.setHTML':
case 'hanzo.getText':
case 'hanzo.setText':
case 'hanzo.getAttribute':
case 'hanzo.setAttribute':
case 'hanzo.removeAttribute':
case 'hanzo.setStyle':
case 'hanzo.addClass':
case 'hanzo.removeClass':
case 'hanzo.insertElement':
case 'hanzo.removeElement':
case 'hanzo.observeMutations':
case 'hanzo.getComputedStyles':
case 'hanzo.getBoundingRects':
case 'hanzo.injectScript':
case 'hanzo.injectCSS':
case 'hanzo.getLocalStorage':
case 'hanzo.setLocalStorage':
case 'hanzo.getCookies':
case 'hanzo.getHistory':
case 'hanzo.fetch': {
// Map hanzo.X → browser.X and forward via chrome.runtime message
const cmd = method.replace('hanzo.', '');
// Normalize naming: hanzo.select → browser.select, hanzo.dblclick → browser.dblclick, etc.
const actionMap: Record<string, string> = {
'url': 'getURL', 'title': 'getTitle', 'tabInfo': 'getTabInfo',
};
const action = 'browser.' + (actionMap[cmd] || cmd);
result = await new Promise((resolve) => {
chrome.runtime.sendMessage({ action, tabId, ...params }, (response) => {
resolve(response || { error: chrome.runtime.lastError?.message });
});
});
break;
}
// Control overlay management
case 'hanzo.control.start':
case 'hanzo.takeover.start':
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "1.7.12",
"version": "1.7.14",
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
"permissions": [
"activeTab",
+2 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "1.7.12",
"version": "1.7.16",
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
"permissions": [
"activeTab",
@@ -11,6 +11,7 @@
"tabs",
"webNavigation",
"scripting",
"cookies",
"debugger"
],
"host_permissions": [
+155 -81
View File
@@ -490,141 +490,147 @@ html {
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
/* Chat Input */
/* ==================== Composer (Grok-style) ==================== */
.chat-input-area {
border-top: 1px solid var(--border);
padding: 12px 16px;
background: rgba(0,0,0,0.5);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
.composer {
padding: 12px;
flex-shrink: 0;
position: relative;
z-index: 10;
}
.chat-modern-composer {
.composer-box {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 16px;
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
}
.composer-box:focus-within {
border-color: var(--border-strong);
box-shadow: 0 0 0 1px rgba(255,255,255,0.06), 0 4px 16px rgba(0,0,0,0.3);
}
.composer-box textarea {
width: 100%;
padding: 14px 16px 8px;
background: transparent;
border: none;
color: var(--text-primary);
font-size: 14px;
font-family: inherit;
line-height: 1.5;
resize: none;
outline: none;
min-height: 44px;
max-height: 160px;
}
.composer-box textarea::placeholder {
color: var(--text-tertiary);
}
.composer-bar {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 6px 8px 8px 12px;
gap: 8px;
}
.model-selector {
margin-bottom: 8px;
.composer-controls {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
overflow-x: auto;
}
.model-selector select {
width: 100%;
padding: 6px 10px;
background: var(--bg-tertiary);
.composer-model {
padding: 3px 22px 3px 8px;
background: var(--glass-strong);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-size: 12px;
color: var(--text-secondary);
font-size: 11px;
font-family: inherit;
cursor: pointer;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 5l3 3 3-3' stroke='%23A0A0A0' stroke-width='1.5'/%3E%3C/svg%3E");
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10' fill='none'%3E%3Cpath d='M2.5 4l2.5 2.5 2.5-2.5' stroke='%236B6B6B' stroke-width='1.2'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
padding-right: 28px;
background-position: right 6px center;
white-space: nowrap;
max-width: 140px;
transition: border-color 0.2s;
}
.model-selector select:focus {
.composer-model:focus {
border-color: var(--border-strong);
outline: none;
}
.chat-flags {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.flag-toggle {
.composer-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--text-secondary);
user-select: none;
}
.flag-toggle input[type="checkbox"] {
width: 14px;
height: 14px;
accent-color: var(--accent);
}
.rag-status {
margin-left: auto;
gap: 3px;
font-size: 11px;
color: var(--text-tertiary);
user-select: none;
cursor: pointer;
white-space: nowrap;
}
.input-row {
display: flex;
gap: 8px;
align-items: flex-end;
.composer-toggle input[type="checkbox"] {
width: 12px;
height: 12px;
accent-color: var(--text-secondary);
cursor: pointer;
}
.input-row textarea {
flex: 1;
padding: 10px 14px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text-primary);
font-size: 13px;
font-family: inherit;
line-height: 1.4;
resize: none;
outline: none;
max-height: 120px;
transition: border-color 0.2s, box-shadow 0.2s;
.composer-toggle:has(input:checked) {
color: var(--text-secondary);
}
.input-row textarea:focus {
border-color: var(--border-strong);
box-shadow: 0 0 0 1px rgba(255,255,255,0.08);
.composer-status {
font-size: 10px;
color: var(--text-tertiary);
white-space: nowrap;
margin-left: auto;
}
.chat-modern-input {
min-height: 40px;
}
.send-btn {
width: 36px;
height: 36px;
.composer-send {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: var(--accent);
border: none;
border-radius: 10px;
color: #000000;
border-radius: 50%;
color: #000;
cursor: pointer;
flex-shrink: 0;
transition: all 0.2s;
}
.send-btn:hover {
.composer-send:hover {
background: var(--accent-hover);
box-shadow: 0 0 12px rgba(255,255,255,0.15);
box-shadow: 0 0 12px rgba(255,255,255,0.12);
}
.send-btn:disabled {
opacity: 0.3;
.composer-send:disabled {
opacity: 0.2;
cursor: not-allowed;
box-shadow: none;
}
.send-btn svg {
width: 16px;
height: 16px;
.composer-send svg {
width: 14px;
height: 14px;
}
/* ==================== Buttons ==================== */
@@ -868,7 +874,7 @@ html {
}
.tool-list {
max-height: 120px;
max-height: 200px;
overflow-y: auto;
font-size: 11px;
line-height: 1.4;
@@ -880,6 +886,74 @@ html {
scrollbar-color: rgba(255,255,255,0.08) transparent;
}
.tool-category {
padding: 4px 0;
border-bottom: 1px solid var(--border);
}
.tool-category:last-child {
border-bottom: none;
}
.tool-category-name {
color: var(--text-secondary);
font-weight: 600;
font-family: inherit;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.tool-category-tools {
color: var(--text-tertiary);
font-size: 10px;
}
/* MCP Server List */
.mcp-server-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.mcp-server-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
background: var(--glass);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 12px;
transition: border-color 0.2s;
}
.mcp-server-item:hover {
border-color: var(--border-strong);
}
.mcp-server-info {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.mcp-server-name {
font-weight: 500;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mcp-server-tools {
color: var(--text-tertiary);
font-size: 11px;
white-space: nowrap;
flex-shrink: 0;
}
/* ==================== User Profile ==================== */
.user-profile {
+49 -36
View File
@@ -35,35 +35,35 @@
<p>Ask anything. Powered by Hanzo Cloud.</p>
</div>
</div>
<div id="chat-composer" class="chat-input-area">
<div class="model-selector">
<select id="model-select">
<option value="auto">Auto (cost-aware)</option>
<option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
<option value="claude-opus-4-20250514">Claude Opus 4</option>
<option value="gpt-4o">GPT-4o</option>
<option value="zen-coder-flash">Zen Coder Flash</option>
<option value="zen-max">Zen Max</option>
</select>
</div>
<div class="chat-flags">
<label class="flag-toggle">
<input type="checkbox" id="rag-enabled" checked>
<span>RAG</span>
</label>
<label class="flag-toggle">
<input type="checkbox" id="tab-context-enabled" checked>
<span>Tab Context</span>
</label>
<span id="rag-status" class="rag-status">Ready</span>
</div>
<div class="input-row">
<div id="chat-composer" class="composer">
<div class="composer-box">
<textarea id="chat-input" placeholder="Ask anything..." rows="1"></textarea>
<button id="send-btn" class="send-btn" title="Send">
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor">
<path d="M4 10l12-6-6 12-2-6-4-2z" stroke-width="1.5" fill="currentColor"/>
</svg>
</button>
<div class="composer-bar">
<div class="composer-controls">
<select id="model-select" class="composer-model">
<option value="auto">Auto</option>
<option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
<option value="claude-opus-4-20250514">Claude Opus 4</option>
<option value="gpt-4o">GPT-4o</option>
<option value="zen-coder-flash">Zen Coder Flash</option>
<option value="zen-max">Zen Max</option>
</select>
<label class="composer-toggle" title="RAG context">
<input type="checkbox" id="rag-enabled" checked>
<span>RAG</span>
</label>
<label class="composer-toggle" title="Include active tab">
<input type="checkbox" id="tab-context-enabled" checked>
<span>Tab</span>
</label>
<span id="rag-status" class="composer-status">Ready</span>
</div>
<button id="send-btn" class="composer-send" title="Send (Enter)">
<svg viewBox="0 0 16 16" fill="none">
<path d="M3 13V3l10 5-10 5z" fill="currentColor"/>
</svg>
</button>
</div>
</div>
</div>
</div>
@@ -83,7 +83,7 @@
</span>
</div>
<div class="status-row">
<span>MCP Server</span>
<span>Hanzo Bot</span>
<span id="hanzo-mcp-status" class="status-value">
<span class="status-indicator disconnected"></span>
Checking...
@@ -108,21 +108,34 @@
</svg>
Start Node
</button>
<button class="action-btn small" id="start-hanzo-mcp" title="Start hanzo mcp serve">
<button class="action-btn small" id="start-hanzo-mcp" title="Start hanzo bot">
<svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12">
<path d="M4 2l10 6-10 6V2z"/>
</svg>
Start MCP
Start Bot
</button>
</div>
</div>
<!-- MCP Tools -->
<!-- Connected MCP Servers -->
<div class="panel">
<div class="panel-header">
<h3>Discovered Tools</h3>
<h3>MCP Servers</h3>
<span id="mcp-server-count" class="badge">0</span>
</div>
<div id="mcp-tool-list" class="tool-list">No tools discovered yet.</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 -->
@@ -191,8 +204,8 @@
<!-- Settings Tab -->
<div id="tab-settings" class="tab-content hidden">
<div class="settings-scroll">
<!-- Account -->
<div class="panel" id="account-panel">
<!-- Account (shown when authenticated) -->
<div class="panel hidden" id="account-panel">
<h3>Account</h3>
<div class="user-profile">
<img id="user-avatar" class="user-avatar" src="" alt="">
+119 -32
View File
@@ -53,8 +53,6 @@ class HanzoSidebar {
authSection: document.getElementById('auth-section'),
authBtn: document.getElementById('auth-btn'),
tabBar: document.getElementById('tab-bar'),
userBadge: document.getElementById('user-badge'),
headerAvatar: document.getElementById('header-avatar'),
// Chat
tabChat: document.getElementById('tab-chat'),
@@ -71,6 +69,10 @@ class HanzoSidebar {
mcpStatus: document.getElementById('mcp-status'),
mcpTools: document.getElementById('mcp-tools'),
mcpToolList: document.getElementById('mcp-tool-list'),
builtinToolList: document.getElementById('builtin-tool-list'),
toolCountBadge: document.getElementById('tool-count-badge'),
mcpServerList: document.getElementById('mcp-server-list'),
mcpServerCount: document.getElementById('mcp-server-count'),
agentCount: document.getElementById('agent-count'),
agentList: document.getElementById('agent-list'),
tabFs: document.getElementById('tab-fs'),
@@ -88,6 +90,7 @@ class HanzoSidebar {
// Settings
tabSettings: document.getElementById('tab-settings'),
accountPanel: document.getElementById('account-panel'),
userAvatar: document.getElementById('user-avatar'),
userName: document.getElementById('user-name'),
userEmail: document.getElementById('user-email'),
@@ -145,7 +148,7 @@ class HanzoSidebar {
this.el.refreshTabs.addEventListener('click', () => this.refreshTabFilesystem());
this.el.launchAgent.addEventListener('click', () => this.showAgentLauncher());
this.el.startHanzoNode?.addEventListener('click', () => this.startHanzoService('node'));
this.el.startHanzoMcp?.addEventListener('click', () => this.startHanzoService('mcp'));
this.el.startHanzoMcp?.addEventListener('click', () => this.startHanzoService('bot'));
// Settings
this.el.logoutBtn.addEventListener('click', () => this.logout());
@@ -229,9 +232,10 @@ class HanzoSidebar {
async logout() {
await chrome.runtime.sendMessage({ action: 'auth.logout' });
this.el.userBadge.classList.add('hidden');
this.el.userName.textContent = '';
this.el.userEmail.textContent = '';
if (this.el.userName) this.el.userName.textContent = '';
if (this.el.userEmail) this.el.userEmail.textContent = '';
if (this.el.userAvatar) this.el.userAvatar.src = '';
if (this.el.accountPanel) this.el.accountPanel.classList.add('hidden');
this.showChatLoginPrompt();
this.switchTab('tools');
}
@@ -240,13 +244,20 @@ class HanzoSidebar {
this.authenticated = true;
if (user) {
const avatar = user.picture || user.avatar || '';
if (avatar) {
this.el.headerAvatar.src = avatar;
this.el.userBadge.classList.remove('hidden');
if (avatar && this.el.userAvatar) {
this.el.userAvatar.src = avatar;
this.el.userAvatar.style.display = '';
}
if (this.el.userName) {
this.el.userName.textContent = user.name || user.displayName || user.sub || 'User';
}
if (this.el.userEmail) {
this.el.userEmail.textContent = user.email || '';
}
// Show the account panel
if (this.el.accountPanel) {
this.el.accountPanel.classList.remove('hidden');
}
this.el.userName.textContent = user.name || user.displayName || 'User';
this.el.userEmail.textContent = user.email || '';
}
this.loadModels();
this.loadConversation();
@@ -259,6 +270,8 @@ class HanzoSidebar {
if (chatLogin) chatLogin.classList.remove('hidden');
const chatComposer = document.getElementById('chat-composer');
if (chatComposer) chatComposer.classList.add('hidden');
// Hide account panel when not authenticated
if (this.el.accountPanel) this.el.accountPanel.classList.add('hidden');
}
hideChatLoginPrompt() {
@@ -319,6 +332,7 @@ class HanzoSidebar {
ensureToolsInitialized() {
if (this.toolsInitialized) return;
this.toolsInitialized = true;
this.renderBuiltinTools();
void this.connectToMCP();
void this.refreshTabFilesystem();
void this.checkHanzoServices();
@@ -357,11 +371,21 @@ class HanzoSidebar {
}
async sendMessage() {
const text = this.el.chatInput.value.trim();
const text = this.el.chatInput?.value?.trim();
if (!text) return;
if (!this.authenticated) {
this.showError('Please sign in to use chat');
return;
// Double-check auth status in case it was set externally
try {
const status = await chrome.runtime.sendMessage({ action: 'auth.status' });
if (status?.success && status.authenticated) {
this.authenticated = true;
if (status.user) this.setUser(status.user);
}
} catch {}
if (!this.authenticated) {
this.showError('Please sign in to use chat');
return;
}
}
if (this.streaming && this.streamController) {
@@ -755,17 +779,71 @@ class HanzoSidebar {
this.el.mcpTools.textContent = toolCount;
}
// Built-in @hanzo/mcp tool categories (always available)
static BUILTIN_TOOLS = [
{ category: 'Core', tools: ['read', 'write', 'list', 'info', 'tree', 'create', 'delete', 'move', 'edit', 'patch', 'grep', 'find', 'search', 'bash', 'bg', 'ps', 'logs', 'kill'] },
{ category: 'Intelligence', tools: ['ai', 'ast', 'vector'] },
{ category: 'Workflow', tools: ['todo', 'plan', 'memory', 'mode'] },
{ category: 'Dev', tools: ['vcs', 'refactor'] },
{ category: 'Cloud', tools: ['iam', 'kms', 'paas', 'commerce', 'storage', 'auth', 'api'] },
];
renderBuiltinTools() {
if (!this.el.builtinToolList) return;
const totalBuiltin = HanzoSidebar.BUILTIN_TOOLS.reduce((sum, c) => sum + c.tools.length, 0);
this.el.builtinToolList.innerHTML = HanzoSidebar.BUILTIN_TOOLS.map(cat =>
`<div class="tool-category">` +
`<span class="tool-category-name">${this.escapeHtml(cat.category)}</span> ` +
`<span class="tool-category-tools">${cat.tools.join(', ')}</span>` +
`</div>`
).join('');
this.updateToolCount(totalBuiltin);
}
updateToolCount(builtinCount: number) {
const zapCount = parseInt(this.el.mcpTools?.textContent || '0') || 0;
const total = builtinCount + zapCount;
if (this.el.toolCountBadge) this.el.toolCountBadge.textContent = String(total);
}
updateMCPToolList(tools) {
if (!this.el.mcpToolList) return;
if (!tools || !tools.length) {
this.el.mcpToolList.textContent = 'No tools discovered yet.';
this.el.mcpToolList.textContent = '';
return;
}
const names = Array.from(new Set(tools.map((tool) => tool.name))).sort();
const preview = names.slice(0, 20);
const suffix = names.length > preview.length ? `\n... +${names.length - preview.length} more` : '';
this.el.mcpToolList.textContent = `${preview.join('\n')}${suffix}`;
this.el.mcpToolList.textContent = `ZAP: ${preview.join(', ')}${suffix}`;
// Update total count
const builtinCount = HanzoSidebar.BUILTIN_TOOLS.reduce((sum, c) => sum + c.tools.length, 0);
this.updateToolCount(builtinCount);
}
updateMCPServerList(mcps) {
if (!this.el.mcpServerList) return;
if (this.el.mcpServerCount) this.el.mcpServerCount.textContent = String(mcps?.length || 0);
if (!mcps || !mcps.length) {
this.el.mcpServerList.innerHTML = '<div class="empty-state">No MCP servers connected</div>';
return;
}
this.el.mcpServerList.innerHTML = mcps.map(mcp => `
<div class="mcp-server-item">
<div class="mcp-server-info">
<span class="status-indicator connected"></span>
<span class="mcp-server-name">${this.escapeHtml(mcp.name || mcp.id)}</span>
</div>
<span class="mcp-server-tools">${(mcp.tools?.length || 0)} tools</span>
</div>
`).join('');
}
async refreshUsageMetrics() {
@@ -813,19 +891,18 @@ class HanzoSidebar {
try {
const resp = await fetch(`http://localhost:${nodePort}/health`, { signal: AbortSignal.timeout(2000) });
if (resp.ok) {
this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator connected"></span> Running`;
this.el.startHanzoNode.textContent = 'Running';
this.el.startHanzoNode.disabled = true;
if (this.el.hanzoNodeStatus) this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator connected"></span> Running`;
if (this.el.startHanzoNode) { this.el.startHanzoNode.textContent = 'Running'; this.el.startHanzoNode.disabled = true; }
} else {
this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator disconnected"></span> Stopped`;
this.el.startHanzoNode.disabled = false;
if (this.el.hanzoNodeStatus) this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator disconnected"></span> Stopped`;
if (this.el.startHanzoNode) this.el.startHanzoNode.disabled = false;
}
} catch {
this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator disconnected"></span> Stopped`;
if (this.el.hanzoNodeStatus) this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator disconnected"></span> Stopped`;
if (this.el.startHanzoNode) this.el.startHanzoNode.disabled = false;
}
// Check hanzo MCP server (via ZAP status which already probes MCP)
// Check hanzo bot/MCP via ZAP status
try {
const zapResp = await chrome.runtime.sendMessage({ action: 'zap.status' });
const hasTools = zapResp?.success && zapResp.zap?.connected;
@@ -838,39 +915,49 @@ class HanzoSidebar {
this.el.startHanzoMcp.disabled = !!hasTools;
if (hasTools) this.el.startHanzoMcp.textContent = 'Running';
}
// Update connected MCP servers list
if (zapResp?.success && zapResp.zap?.mcps?.length) {
this.updateMCPServerList(zapResp.zap.mcps);
} else {
this.updateMCPServerList([]);
}
} catch {
if (this.el.hanzoMcpStatus) {
this.el.hanzoMcpStatus.innerHTML = `<span class="status-indicator disconnected"></span> Stopped`;
}
this.updateMCPServerList([]);
}
}
async startHanzoService(service: string) {
// Open a new tab with instructions to start the service via terminal
const commands = {
node: 'hanzo node --port 52415',
mcp: 'hanzo mcp serve',
bot: 'npx @hanzo/mcp serve',
};
const labels = { node: 'Hanzo Node', bot: 'Hanzo Bot' };
const cmd = commands[service] || commands.node;
const label = labels[service] || service;
// Try sending command to CDP bridge (if running, it can spawn processes)
// Try native messaging or CDP bridge first
try {
const response = await chrome.runtime.sendMessage({
action: 'cdp.exec',
command: cmd,
});
if (response?.success) {
this.showNotification(`Starting ${service}...`);
// Re-check after a short delay
this.showNotification(`Starting ${label}...`);
setTimeout(() => this.checkHanzoServices(), 3000);
return;
}
} catch {}
// Fallback: open docs page with terminal command
const url = `https://docs.hanzo.ai/cli#${service}`;
chrome.tabs.create({ url });
this.showNotification(`Run: ${cmd}`);
// Fallback: show the command to run with a copy-friendly notification
this.showNotification(`Run in terminal: ${cmd}`);
// Also try to open docs
const docsSection = service === 'bot' ? 'mcp' : service;
chrome.tabs.create({ url: `https://docs.hanzo.ai/cli#${docsSection}` });
}
updateAgentList() {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hanzo/dxt",
"version": "1.7.12",
"version": "1.7.14",
"description": "Hanzo AI DXT Bundle",
"main": "dist/index.js",
"scripts": {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@hanzo/mcp",
"version": "1.0.0",
"description": "Hanzo MCP Server - Model Context Protocol tools for AI development",
"version": "2.2.2",
"description": "Hanzo MCP Server - Unified tool surface with AI, cloud control, vector search, and multi-framework UI",
"main": "dist/index.js",
"type": "module",
"bin": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hanzo/cli-tools",
"version": "1.7.12",
"version": "1.7.14",
"description": "Hanzo AI CLI Tools Integration",
"main": "dist/index.js",
"scripts": {
+3 -3
View File
@@ -173,7 +173,7 @@ export class HanzoAuth extends EventEmitter {
this.server.listen(port, () => {
// Authorization Code + PKCE (OAuth 2.1 standard)
const authUrl = new URL('/login/oauth/authorize', this.config.iamLoginUrl);
const authUrl = new URL('/oauth/authorize', this.config.iamLoginUrl);
authUrl.searchParams.set('client_id', this.config.clientId);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('redirect_uri', `http://localhost:${port}/callback`);
@@ -200,7 +200,7 @@ export class HanzoAuth extends EventEmitter {
}
private async exchangeCodeForTokens(code: string, codeVerifier: string): Promise<void> {
const response = await fetch(`${this.config.iamUrl}/api/login/oauth/access_token`, {
const response = await fetch(`${this.config.iamUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -318,7 +318,7 @@ export class HanzoAuth extends EventEmitter {
}
try {
const response = await fetch(`${this.config.iamUrl}/api/login/oauth/access_token`, {
const response = await fetch(`${this.config.iamUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@hanzo/extension",
"displayName": "Hanzo AI",
"description": "The ultimate meta AI development platform. Manage and run all LLMs and CLI tools (Claude, Codex, Gemini, OpenHands, Aider) in one unified interface. Features MCP integration, async execution, git worktrees, and universal context sync.",
"version": "1.7.12",
"version": "1.7.14",
"publisher": "hanzo-ai",
"private": false,
"license": "MIT",
+3 -3
View File
@@ -104,7 +104,7 @@ export class StandaloneAuthManager {
private async refreshToken(refreshToken: string): Promise<AuthToken | null> {
try {
const response = await axios.post(`${this.casdoorConfig.endpoint}/api/login/oauth/access_token`, {
const response = await axios.post(`${this.casdoorConfig.endpoint}/oauth/token`, {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.casdoorConfig.clientId,
@@ -166,7 +166,7 @@ export class StandaloneAuthManager {
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
const authUrl = new URL(`${this.casdoorConfig.loginUrl}/login/oauth/authorize`);
const authUrl = new URL(`${this.casdoorConfig.loginUrl}/oauth/authorize`);
authUrl.searchParams.append('client_id', this.casdoorConfig.clientId);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('redirect_uri', 'http://localhost:8765/callback');
@@ -249,7 +249,7 @@ export class StandaloneAuthManager {
};
if (clientSecret) body.client_secret = clientSecret;
const tokenResp = await axios.post(`${endpoint}/api/login/oauth/access_token`, body);
const tokenResp = await axios.post(`${endpoint}/oauth/token`, body);
const tokens = tokenResp.data;
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(successHtml);