Compare commits

...
16 Commits
Author SHA1 Message Date
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
Hanzo Dev 047d3f1843 v1.7.12: Fix sidebar scrolling, form rendering across Firefox/Chrome/Safari
- sidebar-container: width 100% (adapts to panel), not hardcoded 320px
- Add min-height:0 to flex scroll containers (required for overflow-y:auto)
- Solid backgrounds on all form elements (backdrop-filter breaks in Firefox)
- Custom select chevrons, color-scheme:dark for native widgets
- Firefox scrollbar styling (scrollbar-width:thin, scrollbar-color)
- Panel overflow:visible (was clipping content)
2026-03-05 18:50:53 -08:00
Hanzo Dev 64d7782869 v1.7.11: Switch to Authorization Code + PKCE (OAuth 2.1 standard)
Reverts implicit flow workaround now that IAM fork properly reads
OAuth params from the JSON body (hanzoai/iam@951e225a).

All platforms now use response_type=code with PKCE:
- Browser (Chrome/Firefox/Safari): code_challenge in authorize URL
- VS Code: code exchange with code_verifier in callback server
- CLI tools: same PKCE flow

Still handles implicit flow as fallback if code not present.
2026-03-05 16:17:12 -08:00
Hanzo Dev b6dc2c0a6d fix(e2e): skip auth-chat tests in CI (requires live IAM) 2026-03-05 16:03:50 -08:00
Hanzo Dev b0d8a57ea2 v1.7.10: Fix auth flow — use implicit OAuth, fix userinfo, fix e2e
- Switch all platforms to response_type=token (implicit flow)
  Casdoor's code flow requires query params the extension wasn't sending
- Use /api/get-account for user profile (userinfo only returns sub)
- Fix e2e test: #gpu-status → #hanzo-node-status + #hanzo-mcp-status
- Add auth+chat e2e test with real Zen model verification
- Fix tools tsconfig (exclude phantom type defs)
- Chrome, Firefox, Safari, VS Code, CLI tools all updated
2026-03-05 16:00:47 -08:00
Hanzo Dev cdd576ed1f v1.7.9: Fix auth across all platforms, redesign sidebar, add service management
Auth fixes:
- Firefox: fix broken login (wrong endpoints, missing dual-flow support)
- VS Code: fix authorize URL (was iam.hanzo.ai, now hanzo.id), fix refresh endpoint
- Tools: fix authorize path, token exchange, refresh, and userinfo endpoints
- All platforms now consistently use hanzo.id for login UI, iam.hanzo.ai for API

Sidebar redesign:
- Remove redundant header (browser chrome already shows "Hanzo AI")
- Replace broken gear icon with "Settings" text tab
- Add Hanzo Services panel (node status, MCP status, start buttons)
- Add footer with small Hanzo logo linking to hanzo.ai
- Add node port setting, settings sync to ~/.hanzo/extension/config.json

Icons:
- Regenerate all icons with dark background + white H mark
- Fixes invisible icon in Firefox sidebar strip (was white-on-transparent)

Version bump to 1.7.9 across all packages.
2026-03-05 14:01:29 -08:00
Hanzo Dev cd89e9bd60 release: v1.7.8 — all extensions with JetBrains plugin
All six extension artifacts now build and ship:
- Chrome (.zip), Firefox (.zip), Safari (.zip)
- VS Code (.vsix), JetBrains (.zip)
- DXT (Claude Code)
2026-03-05 10:56:51 -08:00
Hanzo Dev daa6a6a38c fix(jetbrains): commit gradle-wrapper.jar for CI builds
The wrapper jar must be in the repo for ./gradlew to work.
Removed the workaround that tried to generate it at CI time.
2026-03-05 10:56:40 -08:00
Hanzo Dev 05ebd0b307 release: v1.7.7 — add JetBrains plugin to release, fix Gradle wrapper
- Bump all packages to v1.7.7
- Generate Gradle wrapper jar in CI when missing
- JetBrains plugin now builds and ships in release artifacts
2026-03-05 10:48:51 -08:00
Hanzo Dev 86e1866d3b fix(ci): generate Gradle wrapper jar when missing for JetBrains build
The gradle-wrapper.jar was not committed to the repo, causing
./gradlew to fail with ClassNotFoundException on CI. Add a step
to generate the wrapper from the system gradle installation.
2026-03-05 10:48:35 -08:00
Hanzo Dev c6487166c9 release: v1.7.6 — align all package versions, fix CI release pipeline
- Align all package versions to 1.7.6 (root, vscode, browser, dxt, tools, jetbrains)
- Fix CI release artifact paths: collect all assets into flat release/ dir
- Allow release to proceed when optional builds (Safari, JetBrains) fail
- Fix flaky claude-integration test: async server close to prevent EADDRINUSE
- Fix repo URL in root package.json (dev.git → extension.git)
2026-03-05 10:41:10 -08:00
Hanzo Dev 8cf8d32ce1 fix(browser): back-merge IAM auth fixes from browser-extension repo
- Split IAM_BASE into IAM_LOGIN (hanzo.id) and IAM_API (iam.hanzo.ai)
- Handle both implicit token and PKCE code flow responses
- Use correct Casdoor endpoints (/api/login/oauth/access_token, /api/userinfo)
- Switch token exchange to JSON body (matching Casdoor format)
- Bump browser extension to v1.7.6

Back-ported from hanzoai/browser-extension commits fdf644d, a7fe010, b0dbbdc.
2026-03-05 10:34:49 -08:00
Hanzo Dev 664e602886 fix(auth): stabilize login and add signup flow; harden CI fallback builds 2026-03-05 02:18:25 -08:00
32 changed files with 1719 additions and 848 deletions
+9 -8
View File
@@ -216,7 +216,6 @@ jobs:
with:
distribution: 'temurin'
java-version: '17'
- uses: gradle/gradle-build-action@v2
- name: Build plugin
working-directory: packages/jetbrains
@@ -229,12 +228,13 @@ jobs:
name: jetbrains
path: packages/jetbrains/build/distributions/*.zip
# ─── GitHub Release (only when all builds pass on tag) ───
# ─── GitHub Release (on tag, after core build passes) ───
release:
name: Release
runs-on: ubuntu-latest
needs: [build, build-safari, build-jetbrains]
if: >-
always() &&
github.ref_type == 'tag' &&
needs.build.result == 'success'
steps:
@@ -254,17 +254,18 @@ jobs:
echo "tag=$VERSION" >> $GITHUB_OUTPUT
echo "name=Release $VERSION" >> $GITHUB_OUTPUT
- name: Collect release assets
run: |
mkdir -p release
find artifacts -type f \( -name '*.zip' -o -name '*.vsix' -o -name '*.dxt' \) -exec cp {} release/ \;
echo "Release assets:" && ls -la release/
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.info.outputs.tag }}
name: ${{ steps.info.outputs.name }}
generate_release_notes: true
files: |
artifacts/extensions/browser/hanzo-ai-chrome-*.zip
artifacts/extensions/browser/hanzo-ai-firefox-*.zip
artifacts/extensions/vscode/*.vsix
artifacts/safari/hanzo-ai-safari-*.zip
artifacts/jetbrains/*.zip
files: release/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -211,7 +211,7 @@ jobs:
with:
java-version: '17'
distribution: 'temurin'
- uses: gradle/gradle-build-action@v2
- name: Build and publish
working-directory: packages/jetbrains
run: ./gradlew publishPlugin
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "hanzo-ai-monorepo",
"version": "1.6.3",
"version": "1.7.14",
"private": true,
"description": "Hanzo AI Development Platform Monorepo",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/hanzoai/dev.git"
"url": "https://github.com/hanzoai/extension.git"
},
"packageManager": "pnpm@10.12.4",
"engines": {
@@ -30,4 +30,4 @@
"package:vscode": "pnpm --filter @hanzo/extension run package",
"package:dxt": "pnpm --filter @hanzo/dxt run package"
}
}
}
+173
View File
@@ -0,0 +1,173 @@
import { test, expect } from './fixtures';
/**
* End-to-end tests for the login + chat flow.
*
* These tests exercise the real Casdoor OAuth2 flow using the z@hanzo.ai
* test account. They verify:
* 1. Login tab opens correctly
* 2. Password auth succeeds
* 3. Token is stored and user info is fetched
* 4. Chat sends a message to a Zen model via llm.hanzo.ai
*
* Requirements:
* - IAM at iam.hanzo.ai / hanzo.id must be reachable
* - LLM gateway at llm.hanzo.ai must be reachable
* - Extension must be built: npm run build
*/
const TEST_USER = 'z';
const TEST_EMAIL = 'z@hanzo.ai';
// These tests require live IAM/LLM access — skip in CI
const descFn = process.env.CI ? test.describe.skip : test.describe;
descFn('Auth + Chat', () => {
test('login via tab-based OAuth flow and verify user stored', async ({ context, extensionId }) => {
const sidebar = await context.newPage();
await sidebar.goto(`chrome-extension://${extensionId}/sidebar.html`);
await sidebar.waitForTimeout(500);
// Switch to chat tab — should show login prompt
await sidebar.locator('[data-tab="chat"]').click();
const loginPrompt = sidebar.locator('#chat-login-prompt');
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
const [authPage] = await Promise.all([
context.waitForEvent('page'),
sidebar.locator('#auth-btn').click(),
]);
// Wait for the Casdoor login page to load
await authPage.waitForLoadState('networkidle');
const authUrl = authPage.url();
expect(authUrl).toContain('hanzo.id');
// Fill in credentials on the Casdoor login page
// Casdoor uses a form with username + password inputs
const usernameInput = authPage.locator('input[name="username"], input#input');
const passwordInput = authPage.locator('input[name="password"], input[type="password"]');
if (await usernameInput.isVisible({ timeout: 5000 }).catch(() => false)) {
await usernameInput.fill(TEST_USER);
await passwordInput.fill('IloveHanzo2026!!!');
// Submit the form
const submitBtn = authPage.locator('button[type="submit"], .login-button, button:has-text("Sign In"), button:has-text("Sign in")');
await submitBtn.click();
// Wait for redirect — the background script should catch the callback URL
// and close this tab automatically
await authPage.waitForEvent('close', { timeout: 30_000 }).catch(() => {
// Tab might not close if redirect is to a real page
});
} else {
// If no form visible, Casdoor might have a different UI — skip
test.skip(true, 'Casdoor login form not found');
}
// Back on sidebar — verify auth state
await sidebar.waitForTimeout(2000);
// Check storage for the access token
const tokenStored = await sidebar.evaluate(() => {
return new Promise(resolve => {
chrome.storage.local.get(['hanzo_iam_access_token', 'hanzo_iam_user'], (result) => {
resolve({
hasToken: !!result.hanzo_iam_access_token,
user: result.hanzo_iam_user,
});
});
});
}) as any;
expect(tokenStored.hasToken).toBe(true);
expect(tokenStored.user).toBeTruthy();
// /api/get-account should have returned name and email
if (tokenStored.user) {
expect(tokenStored.user.name).toBeTruthy();
expect(tokenStored.user.email).toBe(TEST_EMAIL);
}
});
test('chat with Zen model after login', async ({ context, extensionId }) => {
const sidebar = await context.newPage();
await sidebar.goto(`chrome-extension://${extensionId}/sidebar.html`);
// Inject a mock token to skip the login flow
await sidebar.evaluate(() => {
return new Promise<void>(resolve => {
// We need a real token for this test
chrome.storage.local.set({
hanzo_iam_access_token: 'test-will-be-replaced',
hanzo_iam_user: { name: 'Test', email: 'z@hanzo.ai' },
}, resolve);
});
});
// Get a real token via direct API call
const tokenResult = await sidebar.evaluate(async () => {
try {
const resp = await fetch('https://iam.hanzo.ai/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
application: 'app-hanzo',
organization: 'hanzo',
username: 'z',
password: 'IloveHanzo2026!!!',
type: 'token',
}),
});
const data = await resp.json();
if (data.status === 'ok' && data.data) {
// Store the real token
await new Promise<void>(resolve => {
chrome.storage.local.set({ hanzo_iam_access_token: data.data }, resolve);
});
return { success: true };
}
return { success: false, error: data.msg };
} catch (e: any) {
return { success: false, error: e.message };
}
});
expect(tokenResult.success).toBe(true);
// Reload to pick up the token
await sidebar.reload();
await sidebar.waitForTimeout(1000);
// Switch to chat tab
await sidebar.locator('[data-tab="chat"]').click();
// Login prompt should be hidden, composer should be visible
const composer = sidebar.locator('#chat-composer');
await expect(composer).not.toHaveClass(/hidden/, { timeout: 5000 });
// Select a Zen model
const modelSelect = sidebar.locator('#model-select');
const options = await modelSelect.locator('option').allTextContents();
const zenOption = options.find(o => o.toLowerCase().includes('zen'));
if (zenOption) {
await modelSelect.selectOption({ label: zenOption });
}
// Type a message and send
const chatInput = sidebar.locator('#chat-input');
await chatInput.fill('Hello, say "pong" and nothing else.');
await sidebar.locator('#send-btn').click();
// Wait for a response (the assistant message should appear)
const assistantMsg = sidebar.locator('.message.assistant');
await expect(assistantMsg.first()).toBeVisible({ timeout: 30_000 });
// Verify response contains text
const responseText = await assistantMsg.first().textContent();
expect(responseText).toBeTruthy();
expect(responseText!.length).toBeGreaterThan(0);
});
});
+6 -1
View File
@@ -41,6 +41,10 @@ test.describe('Sidebar', () => {
await expect(authBtn).toBeVisible();
await expect(authBtn).toContainText('Sign in');
const signupBtn = page.locator('#signup-btn');
await expect(signupBtn).toBeVisible();
await expect(signupBtn).toContainText('Create account');
// Should have link to docs
const docsLink = loginPrompt.locator('a[href="https://docs.hanzo.ai"]');
await expect(docsLink).toBeAttached();
@@ -127,7 +131,8 @@ test.describe('Sidebar', () => {
await expect(page.locator('#mcp-tool-list')).toBeAttached();
await expect(page.locator('#agent-count')).toBeAttached();
await expect(page.locator('#tab-fs')).toBeAttached();
await expect(page.locator('#gpu-status')).toBeAttached();
await expect(page.locator('#hanzo-node-status')).toBeAttached();
await expect(page.locator('#hanzo-mcp-status')).toBeAttached();
});
test('settings tab has account and connection config', async ({ context, extensionId }) => {
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hanzo/browser-extension",
"version": "1.7.4",
"version": "1.7.14",
"description": "Hanzo AI Browser Extension",
"main": "dist/background.js",
"scripts": {
@@ -26,6 +26,8 @@
"@playwright/test": "^1.52.0",
"esbuild": "^0.25.6",
"jsdom": "^26.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.8.3",
"vitest": "^0.34.6",
"ws": "^8.18.3"
+85 -33
View File
@@ -49,7 +49,9 @@ function generateState(): string {
// Configuration
// ---------------------------------------------------------------------------
const IAM_BASE = 'https://hanzo.id';
// Login UI lives on hanzo.id; Casdoor API is on iam.hanzo.ai
const IAM_LOGIN = 'https://hanzo.id';
const IAM_API = 'https://iam.hanzo.ai';
const CLIENT_ID = 'app-hanzo';
const SCOPES = 'openid profile email';
@@ -134,7 +136,8 @@ export async function login(): Promise<UserInfo> {
const state = generateState();
const redirectUri = getRedirectUri();
const authorizeUrl = new URL(`${IAM_BASE}/login/oauth/authorize`);
// Authorization Code + PKCE (OAuth 2.1 standard for public clients)
const authorizeUrl = new URL(`${IAM_LOGIN}/login/oauth/authorize`);
authorizeUrl.searchParams.set('client_id', CLIENT_ID);
authorizeUrl.searchParams.set('response_type', 'code');
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
@@ -150,40 +153,58 @@ export async function login(): Promise<UserInfo> {
const returnedState = url.searchParams.get('state');
if (returnedState !== state) throw new Error('State mismatch — possible CSRF');
// Handle both code flow and implicit token flow responses.
// The login page may return tokens directly (type=token) or an auth code (type=code).
const code = url.searchParams.get('code');
if (!code) {
const error = url.searchParams.get('error_description') || url.searchParams.get('error') || 'No authorization code';
const directToken = url.searchParams.get('access_token');
if (directToken) {
// Implicit flow — tokens returned directly in redirect URL
const tokens: TokenData = {
access_token: directToken,
refresh_token: url.searchParams.get('refresh_token') || undefined,
token_type: 'Bearer',
};
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`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
}),
});
if (!tokenResponse.ok) {
const errText = await tokenResponse.text();
throw new Error(`Token exchange failed: ${errText}`);
}
const tokens: TokenData = await tokenResponse.json();
await storeTokens(tokens);
} else {
const error = url.searchParams.get('error_description') || url.searchParams.get('error') || 'No authorization code or token';
throw new Error(error);
}
// Exchange code for tokens
const tokenResponse = await fetch(`${IAM_BASE}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
}),
});
if (!tokenResponse.ok) {
const errText = await tokenResponse.text();
throw new Error(`Token exchange failed: ${errText}`);
}
const tokens: TokenData = await tokenResponse.json();
await storeTokens(tokens);
// Fetch user info
const user = await fetchUserInfo(tokens.access_token);
// Fetch user info using the stored token
const { accessToken } = await getStoredTokens();
if (!accessToken) throw new Error('Login succeeded but no token was stored');
const user = await fetchUserInfo(accessToken);
await chrome.storage.local.set({ [STORAGE_KEYS.user]: user });
return user;
}
export async function signup(): Promise<void> {
await openExternalTab(`${IAM_LOGIN}/signup`);
}
/**
* Open a browser tab for OAuth login and wait for the redirect.
* Returns the full callback URL with authorization code.
@@ -237,6 +258,18 @@ function openAuthTab(authorizeUrl: string, redirectUriPrefix: string): Promise<s
});
}
function openExternalTab(url: string): Promise<void> {
return new Promise((resolve, reject) => {
chrome.tabs.create({ url }, (tab) => {
if (chrome.runtime.lastError || !tab?.id) {
reject(new Error(chrome.runtime.lastError?.message || 'Failed to open tab'));
return;
}
resolve();
});
});
}
/**
* Log out — clear all stored tokens.
*/
@@ -276,10 +309,10 @@ 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_BASE}/oauth/token`, {
const response = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
client_id: CLIENT_ID,
refresh_token: refreshToken,
@@ -295,11 +328,30 @@ async function refreshAccessToken(refreshToken: string): Promise<string> {
/**
* Fetch user info from IAM.
* /api/userinfo only returns sub/iss/aud; /api/get-account has full profile.
*/
async function fetchUserInfo(accessToken: string): Promise<UserInfo> {
const response = await fetch(`${IAM_BASE}/oauth/userinfo`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const headers = { Authorization: `Bearer ${accessToken}` };
// Try /api/get-account first (returns name, email, avatar, etc.)
try {
const acctResp = await fetch(`${IAM_API}/api/get-account`, { headers });
if (acctResp.ok) {
const acctJson = await acctResp.json();
const acct = acctJson.data || acctJson;
if (acct.name || acct.email) {
return {
sub: acct.id || acct.sub,
name: acct.displayName || acct.name,
email: acct.email,
picture: acct.avatar || undefined,
};
}
}
} catch { /* fall through */ }
// Fallback to standard /api/userinfo
const response = await fetch(`${IAM_API}/api/userinfo`, { headers });
if (!response.ok) throw new Error('Failed to fetch user info');
return response.json();
}
+67 -20
View File
@@ -393,7 +393,9 @@ class HanzoFirefoxExtension {
// Uses browser.identity.launchWebAuthFlow for OAuth
// =============================================================================
const IAM_BASE = 'https://hanzo.id';
// Login UI lives on hanzo.id; Casdoor API is on iam.hanzo.ai
const IAM_LOGIN = 'https://hanzo.id';
const IAM_API = 'https://iam.hanzo.ai';
const API_BASE = 'https://api.hanzo.ai';
const CLIENT_ID = 'app-hanzo';
const SCOPES = 'openid profile email';
@@ -525,7 +527,7 @@ async function firefoxLogin(): Promise<any> {
const state = generateRandomString(32);
const redirectUri = 'https://hanzo.ai/callback';
const authorizeUrl = new URL(`${IAM_BASE}/login/oauth/authorize`);
const authorizeUrl = new URL(`${IAM_LOGIN}/login/oauth/authorize`);
authorizeUrl.searchParams.set('client_id', CLIENT_ID);
authorizeUrl.searchParams.set('response_type', 'code');
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
@@ -567,29 +569,65 @@ async function firefoxLogin(): Promise<any> {
const url = new URL(callbackUrl);
if (url.searchParams.get('state') !== state) throw new Error('State mismatch');
// Handle both code flow and implicit token flow responses
const code = url.searchParams.get('code');
if (!code) throw new Error(url.searchParams.get('error_description') || 'No code');
const directToken = url.searchParams.get('access_token');
const tokenResponse = await fetch(`${IAM_BASE}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code', client_id: CLIENT_ID,
code, redirect_uri: redirectUri, code_verifier: codeVerifier,
}),
});
if (directToken) {
// Implicit flow — tokens returned directly in redirect URL
const data: any = { [STORAGE_KEYS.accessToken]: directToken };
const refreshToken = url.searchParams.get('refresh_token');
if (refreshToken) data[STORAGE_KEYS.refreshToken] = refreshToken;
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`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
}),
});
if (!tokenResponse.ok) throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
const tokens = await tokenResponse.json();
if (!tokenResponse.ok) throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
const tokens = await tokenResponse.json();
const data: any = { [STORAGE_KEYS.accessToken]: tokens.access_token };
if (tokens.refresh_token) data[STORAGE_KEYS.refreshToken] = tokens.refresh_token;
if (tokens.id_token) data[STORAGE_KEYS.idToken] = tokens.id_token;
if (tokens.expires_in) data[STORAGE_KEYS.expiresAt] = Date.now() + tokens.expires_in * 1000;
await browser.storage.local.set(data);
const data: any = { [STORAGE_KEYS.accessToken]: tokens.access_token };
if (tokens.refresh_token) data[STORAGE_KEYS.refreshToken] = tokens.refresh_token;
if (tokens.id_token) data[STORAGE_KEYS.idToken] = tokens.id_token;
if (tokens.expires_in) data[STORAGE_KEYS.expiresAt] = Date.now() + tokens.expires_in * 1000;
await browser.storage.local.set(data);
} else {
const error = url.searchParams.get('error_description') || url.searchParams.get('error') || 'No authorization code or token';
throw new Error(error);
}
const userResp = await fetch(`${IAM_BASE}/oauth/userinfo`, { headers: { Authorization: `Bearer ${tokens.access_token}` } });
const user = userResp.ok ? await userResp.json() : {};
// Fetch user info
const stored = await browser.storage.local.get([STORAGE_KEYS.accessToken]);
const accessToken = stored[STORAGE_KEYS.accessToken];
if (!accessToken) throw new Error('Login succeeded but no token was stored');
// /api/userinfo only returns sub/iss/aud; /api/get-account has full profile
let user: any = {};
try {
const acctResp = await fetch(`${IAM_API}/api/get-account`, { headers: { Authorization: `Bearer ${accessToken}` } });
if (acctResp.ok) {
const acctJson = await acctResp.json();
const acct = acctJson.data || acctJson;
if (acct.name || acct.email) {
user = { sub: acct.id || acct.sub, name: acct.displayName || acct.name, email: acct.email, picture: acct.avatar || undefined };
}
}
} catch { /* fall through */ }
if (!user.name && !user.email) {
const userResp = await fetch(`${IAM_API}/api/userinfo`, { headers: { Authorization: `Bearer ${accessToken}` } });
if (userResp.ok) user = await userResp.json();
}
await browser.storage.local.set({ [STORAGE_KEYS.user]: user });
return user;
}
@@ -606,6 +644,15 @@ browser.runtime.onMessage.addListener((request: any, sender: any, sendResponse:
}
break;
case 'auth.signup':
try {
await browser.tabs.create({ url: `${IAM_LOGIN}/signup` });
sendResponse({ success: true });
} catch (e: any) {
sendResponse({ success: false, error: e.message });
}
break;
case 'auth.logout':
await browser.storage.local.remove(Object.values(STORAGE_KEYS));
sendResponse({ success: true });
+20
View File
@@ -934,6 +934,15 @@ async function handleMessage(request: any, sender: chrome.runtime.MessageSender,
}
break;
case 'auth.signup':
try {
await auth.signup();
sendResponse({ success: true });
} catch (error: any) {
sendResponse({ success: false, error: error.message });
}
break;
case 'auth.logout':
try {
await auth.logout();
@@ -1163,6 +1172,17 @@ async function handleMessage(request: any, sender: chrome.runtime.MessageSender,
break;
}
case 'settings.sync': {
// Sync all settings to ~/.hanzo/extension/config.json via CDP bridge
if (request.settings && typeof request.settings === 'object') {
for (const [key, value] of Object.entries(request.settings)) {
cdpBridge.sendConfig(key, value);
}
}
sendResponse({ success: true });
break;
}
// --- Takeover messages (forwarded from CDP bridge to content script) ---
case 'hanzo.takeover.start': {
const takeoverTabId = await resolveControlTabId(request.tabId);
+20 -10
View File
@@ -12,22 +12,25 @@ async function build() {
const hanzoUiPrimitives = path.join(hanzoUiRoot, 'pkg/ui/dist/primitives/index-common.js');
const localReact = path.join(hanzoUiRoot, 'node_modules/react');
const localReactDom = path.join(hanzoUiRoot, 'node_modules/react-dom');
const forceLocalUiShim = process.env.HANZO_FORCE_LOCAL_UI_SHIM === '1';
const canUseSharedUi =
!forceLocalUiShim &&
fs.existsSync(hanzoUiPrimitives) &&
fs.existsSync(localReact) &&
fs.existsSync(localReactDom);
if (!canUseSharedUi) {
throw new Error(
if (canUseSharedUi) {
console.log('Using shared @hanzo/ui primitives from sibling repo:', hanzoUiPrimitives);
} else {
console.warn(
[
'Shared @hanzo/ui dependencies were not found.',
'Shared @hanzo/ui dependencies were not found. Falling back to local primitives shim.',
`Expected: ${hanzoUiPrimitives}`,
`Expected: ${localReact}`,
`Expected: ${localReactDom}`,
].join('\n')
);
}
console.log('Using shared @hanzo/ui primitives from sibling repo:', hanzoUiPrimitives);
// Ensure dist directories exist
fs.mkdirSync('dist/browser-extension', { recursive: true });
@@ -78,11 +81,15 @@ async function build() {
});
// Build sidebar + popup UI scripts (TypeScript-first, with JS fallback)
const sidebarAliases = {
'@hanzo/ui/primitives-common': hanzoUiPrimitives,
react: localReact,
'react-dom': localReactDom,
};
const sidebarAliases = canUseSharedUi
? {
'@hanzo/ui/primitives-common': hanzoUiPrimitives,
react: localReact,
'react-dom': localReactDom,
}
: {
'@hanzo/ui/primitives-common': path.join(__dirname, 'primitives-common-fallback.tsx'),
};
await esbuild.build({
entryPoints: [fs.existsSync('src/sidebar.ts') ? 'src/sidebar.ts' : 'src/sidebar.js'],
@@ -314,4 +321,7 @@ server.on('elementSelected', (data) => {
console.log('\nTo publish to npm: cd dist/browser-extension && npm publish');
}
build().catch(console.error);
build().catch((error) => {
console.error(error);
process.exit(1);
});
+3
View File
@@ -22,6 +22,9 @@ function LoginPrompt() {
<Button id="auth-btn" className="chat-modern-auth-btn" type="button">
Sign in
</Button>
<Button id="signup-btn" className="secondary-btn" type="button">
Create account
</Button>
<p className="auth-note">
Browser tools work without sign-in.{' '}
<a href="https://docs.hanzo.ai" target="_blank" rel="noreferrer">
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 B

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 B

After

Width:  |  Height:  |  Size: 1.6 KiB

+23 -8
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "1.7.4",
"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",
@@ -21,13 +21,19 @@
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content-script.js"],
"matches": [
"<all_urls>"
],
"js": [
"content-script.js"
],
"run_at": "document_idle"
}
],
"background": {
"scripts": ["background.js"],
"scripts": [
"background.js"
],
"type": "module"
},
"action": {
@@ -48,8 +54,13 @@
},
"web_accessible_resources": [
{
"resources": ["ai-worker.js", "models/*"],
"matches": ["<all_urls>"]
"resources": [
"ai-worker.js",
"models/*"
],
"matches": [
"<all_urls>"
]
}
],
"browser_specific_settings": {
@@ -60,7 +71,11 @@
"gecko_android": {}
},
"data_collection_permissions": {
"purposes": ["functionality"],
"data_categories": ["technical_data"]
"purposes": [
"functionality"
],
"data_categories": [
"technical_data"
]
}
}
+14 -5
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "1.7.4",
"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",
@@ -23,8 +23,12 @@
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content-script.js"],
"matches": [
"<all_urls>"
],
"js": [
"content-script.js"
],
"run_at": "document_idle"
}
],
@@ -50,8 +54,13 @@
},
"web_accessible_resources": [
{
"resources": ["ai-worker.js", "models/*"],
"matches": ["<all_urls>"]
"resources": [
"ai-worker.js",
"models/*"
],
"matches": [
"<all_urls>"
]
}
]
}
@@ -0,0 +1,45 @@
import React from 'react';
type Classy = { className?: string; children?: React.ReactNode };
function cx(base: string, extra?: string): string {
return extra ? `${base} ${extra}` : base;
}
export function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
const { className, children, ...rest } = props;
return (
<button {...rest} className={cx('hanzo-ui-btn', className)}>
{children}
</button>
);
}
export function Textarea(props: React.TextareaHTMLAttributes<HTMLTextAreaElement>) {
const { className, children, ...rest } = props;
return (
<textarea {...rest} className={cx('hanzo-ui-textarea', className)}>
{children}
</textarea>
);
}
export function Card({ className, children }: Classy) {
return <div className={cx('hanzo-ui-card', className)}>{children}</div>;
}
export function CardHeader({ className, children }: Classy) {
return <div className={cx('hanzo-ui-card-header', className)}>{children}</div>;
}
export function CardTitle({ className, children }: Classy) {
return <h3 className={cx('hanzo-ui-card-title', className)}>{children}</h3>;
}
export function CardDescription({ className, children }: Classy) {
return <p className={cx('hanzo-ui-card-description', className)}>{children}</p>;
}
export function CardContent({ className, children }: Classy) {
return <div className={cx('hanzo-ui-card-content', className)}>{children}</div>;
}
+302 -114
View File
@@ -39,11 +39,18 @@ body {
color: var(--text-primary);
font-size: 13px;
line-height: 1.6;
overflow-x: hidden;
overflow: hidden;
width: 100%;
height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
overflow: hidden;
height: 100vh;
}
.hidden { display: none !important; }
/* ==================== Shimmer Keyframes ==================== */
@@ -86,13 +93,15 @@ body {
/* ==================== Layout ==================== */
.sidebar-container {
width: 320px;
width: 100%;
min-width: 0;
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-secondary);
border-left: 1px solid var(--border);
position: relative;
overflow: hidden;
}
/* Ambient glow behind sidebar */
@@ -274,6 +283,11 @@ body {
margin-bottom: 14px;
}
.chat-modern-login-card #signup-btn {
width: 100%;
margin-bottom: 14px;
}
.chat-modern-login-card .auth-note {
font-size: 11px;
color: var(--text-tertiary);
@@ -348,6 +362,7 @@ body {
.tab-content {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
@@ -357,22 +372,30 @@ body {
.tools-scroll,
.settings-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.08) transparent;
}
/* ==================== Chat ==================== */
.chat-messages {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.08) transparent;
}
.chat-welcome {
@@ -467,138 +490,147 @@ body {
.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 {
display: flex;
flex-direction: column;
gap: 8px;
}
.model-selector {
margin-bottom: 8px;
}
.model-selector select {
width: 100%;
padding: 6px 10px;
background: var(--glass-strong);
.composer-box {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-size: 12px;
cursor: pointer;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
transition: border-color 0.2s;
border-radius: 16px;
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
}
.model-selector select:focus {
.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;
}
.chat-flags {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.flag-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;
font-size: 11px;
.composer-box textarea::placeholder {
color: var(--text-tertiary);
}
.input-row {
.composer-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px 8px 12px;
gap: 8px;
align-items: flex-end;
}
.input-row textarea {
.composer-controls {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
padding: 10px 14px;
min-width: 0;
overflow-x: auto;
}
.composer-model {
padding: 3px 22px 3px 8px;
background: var(--glass-strong);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text-primary);
font-size: 13px;
border-radius: 6px;
color: var(--text-secondary);
font-size: 11px;
font-family: inherit;
line-height: 1.4;
resize: none;
outline: none;
max-height: 120px;
transition: border-color 0.2s, box-shadow 0.2s;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
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='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 6px center;
white-space: nowrap;
max-width: 140px;
transition: border-color 0.2s;
}
.input-row textarea:focus {
.composer-model:focus {
border-color: var(--border-strong);
box-shadow: 0 0 0 1px rgba(255,255,255,0.08);
outline: none;
}
.chat-modern-input {
min-height: 40px;
.composer-toggle {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 11px;
color: var(--text-tertiary);
user-select: none;
cursor: pointer;
white-space: nowrap;
}
.send-btn {
width: 36px;
height: 36px;
.composer-toggle input[type="checkbox"] {
width: 12px;
height: 12px;
accent-color: var(--text-secondary);
cursor: pointer;
}
.composer-toggle:has(input:checked) {
color: var(--text-secondary);
}
.composer-status {
font-size: 10px;
color: var(--text-tertiary);
white-space: nowrap;
margin-left: auto;
}
.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 ==================== */
@@ -676,21 +708,19 @@ body {
.secondary-btn {
width: 100%;
padding: 8px 16px;
background: var(--glass-strong);
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: 8px;
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: all 0.2s;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.secondary-btn:hover {
background: var(--glass);
background: var(--bg-hover);
border-color: var(--border-strong);
box-shadow: 0 0 8px rgba(255,255,255,0.05);
}
.action-btn {
@@ -700,22 +730,20 @@ body {
justify-content: center;
gap: 8px;
padding: 10px;
background: var(--glass-strong);
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 12px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: all 0.25s ease;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.action-btn:hover {
background: var(--glass);
background: var(--bg-hover);
border-color: var(--border-strong);
box-shadow: 0 0 12px rgba(255,255,255,0.06);
}
.action-btn svg {
@@ -726,15 +754,13 @@ body {
/* ==================== Glass Panels ==================== */
.panel {
background: var(--glass-strong);
backdrop-filter: blur(16px) saturate(150%);
-webkit-backdrop-filter: blur(16px) saturate(150%);
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
transition: border-color 0.3s, box-shadow 0.3s;
transition: border-color 0.3s;
position: relative;
overflow: hidden;
overflow: visible;
}
/* Subtle top highlight on glass panels */
@@ -830,25 +856,102 @@ body {
font-size: 11px;
}
.tool-list-wrap {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border);
.service-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.tool-list-header {
color: var(--text-secondary);
.action-btn.small {
padding: 6px 10px;
font-size: 11px;
margin-bottom: 6px;
gap: 4px;
}
.action-btn.small svg {
width: 10px;
height: 10px;
}
.tool-list {
max-height: 90px;
max-height: 200px;
overflow-y: auto;
font-size: 11px;
line-height: 1.4;
color: var(--text-tertiary);
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
white-space: pre-wrap;
word-break: break-all;
scrollbar-width: thin;
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 ==================== */
@@ -922,10 +1025,12 @@ body {
/* ==================== Tab Filesystem ==================== */
.tab-filesystem {
max-height: 200px;
max-height: 240px;
overflow-y: auto;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
font-size: 12px;
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.08) transparent;
}
.tab-item {
@@ -951,12 +1056,15 @@ body {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 0;
}
.setting-item span {
.setting-item > span {
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
flex-shrink: 0;
}
.setting-item input[type="text"],
@@ -964,14 +1072,26 @@ body {
.setting-item select {
width: 140px;
padding: 6px 10px;
background: var(--glass-strong);
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-size: 12px;
font-family: inherit;
transition: border-color 0.2s;
}
.setting-item select {
-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-repeat: no-repeat;
background-position: right 8px center;
padding-right: 28px;
cursor: pointer;
}
.setting-item input:focus,
.setting-item select:focus {
border-color: var(--border-strong);
@@ -992,14 +1112,21 @@ body {
.setting-item input[type="number"] {
width: 70px;
padding: 6px 10px;
background: var(--glass-strong);
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-size: 12px;
font-family: inherit;
-moz-appearance: textfield;
transition: border-color 0.2s;
}
.setting-item input[type="number"]::-webkit-inner-spin-button,
.setting-item input[type="number"]::-webkit-outer-spin-button {
opacity: 0.5;
}
.setting-item input[type="number"]:focus {
border-color: var(--border-strong);
outline: none;
@@ -1008,10 +1135,22 @@ body {
.setting-item input[type="checkbox"] {
width: 16px;
height: 16px;
min-width: 16px;
cursor: pointer;
accent-color: var(--accent);
flex-shrink: 0;
}
/* Firefox: style option elements in dropdowns */
select option {
background: var(--bg-tertiary);
color: var(--text-primary);
padding: 4px 8px;
}
/* Color scheme hint for native widgets */
:root { color-scheme: dark; }
.settings-links {
display: flex;
justify-content: center;
@@ -1073,6 +1212,13 @@ body {
/* ==================== Scrollbar ==================== */
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.08) transparent;
}
/* Chrome / Safari */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 3px; }
@@ -1082,6 +1228,43 @@ body {
.loading { animation: pulse 1.5s infinite; }
/* ==================== Footer ==================== */
.sidebar-footer {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
border-top: 1px solid var(--border);
background: rgba(0,0,0,0.4);
flex-shrink: 0;
position: relative;
z-index: 10;
}
.footer-brand {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-tertiary);
text-decoration: none;
font-size: 11px;
transition: color 0.2s;
}
.footer-brand:hover {
color: var(--text-secondary);
}
.footer-brand svg {
opacity: 0.5;
transition: opacity 0.2s;
}
.footer-brand:hover svg {
opacity: 0.8;
}
/* ==================== Agent Launcher Modal ==================== */
.modal-overlay {
@@ -1147,7 +1330,7 @@ body {
.modal-body textarea {
width: 100%;
background: var(--glass-strong);
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
@@ -1155,6 +1338,11 @@ body {
font-family: inherit;
font-size: 12px;
resize: vertical;
outline: none;
}
.modal-body textarea:focus {
border-color: var(--border-strong);
}
.modal-footer {
+104 -81
View File
@@ -8,38 +8,14 @@
</head>
<body>
<div class="sidebar-container">
<!-- Header -->
<header class="sidebar-header">
<div class="brand">
<svg class="logo" viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="currentColor"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="currentColor"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="currentColor"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="currentColor"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="currentColor"/>
</svg>
<span>Hanzo AI</span>
</div>
<div class="header-right">
<div id="user-badge" class="user-badge hidden" title="Account">
<img id="header-avatar" class="header-avatar" src="" alt="">
</div>
</div>
</header>
<!-- Auth section removed — login prompt is now inside Chat tab -->
<!-- Auth section (hidden — login prompt is inside Chat tab) -->
<section id="auth-section" class="hidden"></section>
<!-- Tab Bar (always visible) -->
<nav id="tab-bar" class="tab-bar hidden">
<button class="tab active" data-tab="chat">Chat</button>
<button class="tab" data-tab="tools">Tools</button>
<button class="tab" data-tab="settings">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" width="14" height="14">
<path d="M8 10a2 2 0 100-4 2 2 0 000 4z" stroke-width="1.5"/>
<path d="M13 8a5 5 0 01-.4 2l1 1.7a7 7 0 01-1.3.8l-1-1.7a5 5 0 01-2 .4v2a7 7 0 01-1.5 0v-2a5 5 0 01-2-.4l-1 1.7a7 7 0 01-1.3-.8l1-1.7A5 5 0 013 8a5 5 0 01.4-2l-1-1.7a7 7 0 011.3-.8l1 1.7a5 5 0 012-.4V2.8a7 7 0 011.5 0v2a5 5 0 012 .4l1-1.7a7 7 0 011.3.8l-1 1.7A5 5 0 0113 8z" stroke-width="1.5"/>
</svg>
</button>
<button class="tab" data-tab="settings">Settings</button>
</nav>
<!-- Chat Tab -->
@@ -50,6 +26,7 @@
<p class="auth-headline">Chat with Zen AI models</p>
<p class="auth-sub">Claude, GPT-4o, Zen Coder Flash &amp; more</p>
<button id="auth-btn" class="primary-btn">Sign in</button>
<button id="signup-btn" class="secondary-btn">Create account</button>
<p class="auth-note">Browser tools work without sign-in. <a href="https://docs.hanzo.ai" target="_blank">Learn more</a></p>
</div>
</div>
@@ -58,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>
@@ -94,14 +71,28 @@
<!-- Tools Tab -->
<div id="tab-tools" class="tab-content hidden">
<div class="tools-scroll">
<!-- Connection Status -->
<!-- Hanzo Services -->
<div class="panel">
<h3>Connections</h3>
<h3>Hanzo Services</h3>
<div class="mcp-status">
<div class="status-row">
<span>Hanzo Node</span>
<span id="hanzo-node-status" class="status-value">
<span class="status-indicator disconnected"></span>
Checking...
</span>
</div>
<div class="status-row">
<span>Hanzo Bot</span>
<span id="hanzo-mcp-status" class="status-value">
<span class="status-indicator disconnected"></span>
Checking...
</span>
</div>
<div class="status-row">
<span>ZAP Protocol</span>
<span id="mcp-status" class="status-value">
<span class="status-indicator connected"></span>
<span class="status-indicator disconnected"></span>
Discovering...
</span>
</div>
@@ -109,13 +100,45 @@
<span>Tools Available</span>
<span id="mcp-tools" class="status-value">0</span>
</div>
<div class="tool-list-wrap">
<div class="tool-list-header">Discovered Tools</div>
<div id="mcp-tool-list" class="tool-list">No tools discovered yet.</div>
</div>
</div>
<div class="service-actions">
<button class="action-btn small" id="start-hanzo-node" title="Start hanzo node">
<svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12">
<path d="M4 2l10 6-10 6V2z"/>
</svg>
Start Node
</button>
<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 Bot
</button>
</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>
<div class="mcp-status">
@@ -166,24 +189,6 @@
<div id="tab-fs" class="tab-filesystem"></div>
</div>
<!-- WebGPU Status -->
<div class="panel">
<h3>WebGPU AI</h3>
<div class="gpu-status">
<div class="status-row">
<span>Status</span>
<span id="gpu-status" class="status-value">
<span class="status-indicator warning"></span>
Checking...
</span>
</div>
<div class="status-row">
<span>Model</span>
<span id="gpu-model" class="status-value mono">-</span>
</div>
</div>
</div>
<!-- Actions -->
<div class="actions">
<button id="launch-agent" class="action-btn">
@@ -199,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="">
@@ -227,6 +232,10 @@
<span>ZAP Ports</span>
<input type="text" id="zap-ports-setting" value="9999,9998,9997,9996,9995" placeholder="Comma-separated">
</label>
<label class="setting-item">
<span>Node Port</span>
<input type="number" id="node-port-setting" value="52415" min="1024" max="65535">
</label>
</div>
<!-- AI Settings -->
@@ -301,6 +310,20 @@
</div>
</div>
</div>
<!-- Footer -->
<footer class="sidebar-footer">
<a href="https://hanzo.ai" target="_blank" class="footer-brand" title="hanzo.ai">
<svg viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg" width="14" height="14">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="currentColor"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="currentColor"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="currentColor"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="currentColor"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="currentColor"/>
</svg>
<span>hanzo.ai</span>
</a>
</footer>
</div>
<script src="sidebar.js"></script>
+233 -36
View File
@@ -30,6 +30,7 @@ class HanzoSidebar {
this.requestEpoch = 0;
this.lastInputTokens = 0;
this.lastOutputTokens = 0;
this.authInFlight = false;
this.refreshTabFilesystemDebounced = debounce(() => {
if (this.currentTab === 'tools') {
void this.refreshTabFilesystem();
@@ -52,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'),
@@ -70,13 +69,19 @@ 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'),
refreshTabs: document.getElementById('refresh-tabs'),
gpuStatus: document.getElementById('gpu-status'),
gpuModel: document.getElementById('gpu-model'),
launchAgent: document.getElementById('launch-agent'),
hanzoNodeStatus: document.getElementById('hanzo-node-status'),
hanzoMcpStatus: document.getElementById('hanzo-mcp-status'),
startHanzoNode: document.getElementById('start-hanzo-node'),
startHanzoMcp: document.getElementById('start-hanzo-mcp'),
usageChatRequests: document.getElementById('usage-chat-requests'),
usageInputTokens: document.getElementById('usage-input-tokens'),
usageOutputTokens: document.getElementById('usage-output-tokens'),
@@ -85,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'),
@@ -101,8 +107,20 @@ class HanzoSidebar {
}
setupEventListeners() {
// Auth
this.el.authBtn.addEventListener('click', () => this.login());
// Auth (delegated so lazy-mounted React login card keeps working)
document.addEventListener('click', (event) => {
const target = event.target as HTMLElement | null;
if (!target) return;
if (target.closest('#auth-btn')) {
event.preventDefault();
void this.login();
return;
}
if (target.closest('#signup-btn')) {
event.preventDefault();
void this.signup();
}
});
// Tabs
document.querySelectorAll('.tab').forEach(tab => {
@@ -129,6 +147,8 @@ class HanzoSidebar {
// Tools
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('bot'));
// Settings
this.el.logoutBtn.addEventListener('click', () => this.logout());
@@ -172,8 +192,14 @@ class HanzoSidebar {
}
async login() {
this.el.authBtn.disabled = true;
this.el.authBtn.textContent = 'Signing in...';
if (this.authInFlight) return;
this.authInFlight = true;
const authBtn = document.getElementById('auth-btn') as HTMLButtonElement | null;
if (authBtn) {
authBtn.disabled = true;
authBtn.textContent = 'Signing in...';
}
try {
const response = await chrome.runtime.sendMessage({ action: 'auth.login' });
@@ -186,16 +212,30 @@ class HanzoSidebar {
} catch (error) {
this.showError('Sign in failed');
} finally {
this.el.authBtn.disabled = false;
this.el.authBtn.textContent = 'Sign in with Hanzo';
this.authInFlight = false;
const latestAuthBtn = document.getElementById('auth-btn') as HTMLButtonElement | null;
if (latestAuthBtn) {
latestAuthBtn.disabled = false;
latestAuthBtn.textContent = 'Sign in with Hanzo';
}
}
}
async signup() {
const response = await chrome.runtime.sendMessage({ action: 'auth.signup' });
if (!response?.success) {
this.showError(response?.error || 'Unable to open signup');
} else {
this.showNotification('Signup opened in a new tab');
}
}
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');
}
@@ -204,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();
@@ -223,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() {
@@ -283,9 +332,10 @@ class HanzoSidebar {
ensureToolsInitialized() {
if (this.toolsInitialized) return;
this.toolsInitialized = true;
this.renderBuiltinTools();
void this.connectToMCP();
void this.refreshTabFilesystem();
void this.checkWebGPU();
void this.checkHanzoServices();
void this.refreshUsageMetrics();
}
@@ -321,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) {
@@ -719,17 +779,75 @@ class HanzoSidebar {
this.el.mcpTools.textContent = toolCount;
}
// Built-in @hanzo/mcp tool categories (always available)
static BUILTIN_TOOLS = [
{ category: 'File Operations', tools: ['read_file', 'write_file', 'list_files', 'get_file_info', 'directory_tree'] },
{ category: 'Search', tools: ['grep', 'find_files', 'search'] },
{ category: 'Shell', tools: ['bash', 'run_command', 'run_background', 'list_processes', 'kill_process'] },
{ category: 'Edit', tools: ['edit_file', 'multi_edit', 'create_file', 'delete_file', 'move_file'] },
{ category: 'AI Tools', tools: ['think', 'critic', 'consensus', 'agent'] },
{ category: 'AST Search', tools: ['ast_search', 'find_symbol', 'analyze_dependencies'] },
{ category: 'Vector Search', tools: ['vector_index', 'vector_search', 'vector_stats'] },
{ category: 'Todo', tools: ['todo_add', 'todo_list', 'todo_update', 'todo_delete', 'todo_stats'] },
{ category: 'Modes', tools: ['mode_switch', 'mode_list', 'preset_select', 'preset_list', 'shortcut'] },
];
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() {
@@ -771,22 +889,79 @@ class HanzoSidebar {
return title.replace(/[^a-zA-Z0-9-_]/g, '_').toLowerCase().substring(0, 30);
}
async checkWebGPU() {
async checkHanzoServices() {
// Check hanzo node (default port 52415)
const nodePort = parseInt(document.getElementById('node-port-setting')?.value || '52415') || 52415;
try {
const response = await chrome.runtime.sendMessage({ action: 'checkWebGPU' });
if (response?.available) {
this.el.gpuStatus.innerHTML = `
<span class="status-indicator connected"></span>
Available
`;
this.el.gpuModel.textContent = response.adapter || 'Unknown';
const resp = await fetch(`http://localhost:${nodePort}/health`, { signal: AbortSignal.timeout(2000) });
if (resp.ok) {
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.gpuStatus.innerHTML = `
<span class="status-indicator disconnected"></span>
Not Available
`;
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 {
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 bot/MCP via ZAP status
try {
const zapResp = await chrome.runtime.sendMessage({ action: 'zap.status' });
const hasTools = zapResp?.success && zapResp.zap?.connected;
if (this.el.hanzoMcpStatus) {
this.el.hanzoMcpStatus.innerHTML = hasTools
? `<span class="status-indicator connected"></span> Running`
: `<span class="status-indicator disconnected"></span> Stopped`;
}
if (this.el.startHanzoMcp) {
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) {
const commands = {
node: 'hanzo node --port 52415',
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 native messaging or CDP bridge first
try {
const response = await chrome.runtime.sendMessage({
action: 'cdp.exec',
command: cmd,
});
if (response?.success) {
this.showNotification(`Starting ${label}...`);
setTimeout(() => this.checkHanzoServices(), 3000);
return;
}
} catch {}
// 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() {
@@ -944,6 +1119,7 @@ class HanzoSidebar {
'mcpPort',
'cdpPort',
'zapPorts',
'nodePort',
'hanzo_default_model',
'hanzo_rag_endpoint',
'hanzo_rag_api_key',
@@ -957,10 +1133,12 @@ class HanzoSidebar {
const mcpPort = document.getElementById('mcp-port-setting');
const cdpPort = document.getElementById('cdp-port-setting');
const zapPorts = document.getElementById('zap-ports-setting');
const nodePort = document.getElementById('node-port-setting');
if (mcpPort) mcpPort.value = result.mcpPort || 3001;
if (cdpPort) cdpPort.value = result.cdpPort || 9223;
if (zapPorts) zapPorts.value = (result.zapPorts || [9999,9998,9997,9996,9995]).join(',');
if (nodePort) nodePort.value = result.nodePort || 52415;
if (result.hanzo_default_model && this.el.defaultModel) {
this.el.defaultModel.value = result.hanzo_default_model;
}
@@ -983,9 +1161,12 @@ class HanzoSidebar {
const zapPorts = (document.getElementById('zap-ports-setting')?.value || '')
.split(',').map(p => parseInt(p.trim())).filter(p => p > 0 && p < 65536);
const nodePort = parseInt(document.getElementById('node-port-setting')?.value) || 52415;
await chrome.storage.local.set({
mcpPort,
cdpPort,
nodePort,
zapPorts: zapPorts.length ? zapPorts : [9999,9998,9997,9996,9995],
hanzo_default_model: this.el.defaultModel?.value || 'auto',
'safe-mode': document.getElementById('safe-mode')?.checked,
@@ -1001,6 +1182,21 @@ class HanzoSidebar {
hanzo_chat_tab_context_enabled: !!this.el.tabContextEnabled?.checked,
});
// Sync settings to ~/.hanzo/extension/config.json via CDP bridge
chrome.runtime.sendMessage({
action: 'settings.sync',
settings: {
mcpPort,
cdpPort,
nodePort,
zapPorts: zapPorts.length ? zapPorts : [9999,9998,9997,9996,9995],
defaultModel: this.el.defaultModel?.value || 'auto',
safeMode: !!document.getElementById('safe-mode')?.checked,
maxAgents: parseInt(document.getElementById('max-agents')?.value) || 3,
enableWebGPU: !!document.getElementById('enable-webgpu')?.checked,
},
}).catch(() => {});
this.showNotification('Settings saved');
}
@@ -1015,6 +1211,7 @@ class HanzoSidebar {
setInterval(() => {
if (this.currentTab !== 'tools') return;
void this.connectToMCP();
void this.checkHanzoServices();
void this.refreshUsageMetrics();
}, 15000);
}
@@ -21,9 +21,13 @@ describe('Claude Code Browser Extension Integration', () => {
await new Promise(resolve => ws.on('open', resolve));
});
afterEach(() => {
afterEach(async () => {
ws.close();
server.close();
await new Promise<void>((resolve) => {
server.close(() => resolve());
// Fallback if close callback never fires
setTimeout(resolve, 500);
});
});
describe('React source-map integration', () => {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@hanzo/dxt",
"version": "1.5.7",
"version": "1.7.14",
"description": "Hanzo AI DXT Bundle",
"main": "dist/index.js",
"scripts": {
@@ -11,4 +11,4 @@
"archiver": "^7.0.1"
},
"license": "MIT"
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ pluginGroup = ai.hanzo
pluginName = Hanzo AI
pluginRepositoryUrl = https://github.com/hanzoai/hanzo-ai-jetbrains
# SemVer format -> https://semver.org
pluginVersion = 0.1.0
pluginVersion = 1.7.11
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 233
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@hanzo/cli-tools",
"version": "1.5.7",
"version": "1.7.14",
"description": "Hanzo AI CLI Tools Integration",
"main": "dist/index.js",
"scripts": {
@@ -34,4 +34,4 @@
"node": ">=18.0.0"
},
"license": "MIT"
}
}
+78 -53
View File
@@ -8,7 +8,8 @@ import * as crypto from 'crypto';
export interface HanzoAuthConfig {
apiUrl: string;
iamUrl: string;
iamUrl: string; // Casdoor API (iam.hanzo.ai)
iamLoginUrl: string; // Login UI (hanzo.id)
clientId: string;
scope: string;
configPath?: string;
@@ -44,6 +45,7 @@ export class HanzoAuth extends EventEmitter {
this.config = {
apiUrl: config.apiUrl || 'https://api.hanzo.ai',
iamUrl: config.iamUrl || 'https://iam.hanzo.ai',
iamLoginUrl: config.iamLoginUrl || 'https://hanzo.id',
clientId: config.clientId || 'hanzo-dev-cli',
scope: config.scope || 'api:access tools:manage',
configPath: config.configPath || path.join(os.homedir(), '.hanzo')
@@ -100,9 +102,8 @@ export class HanzoAuth extends EventEmitter {
const url = new URL(req.url!, `http://localhost:${port}`);
if (url.pathname === '/callback') {
const code = url.searchParams.get('code');
const returnedState = url.searchParams.get('state');
if (returnedState !== state) {
res.writeHead(400);
res.end('Invalid state parameter');
@@ -110,55 +111,69 @@ export class HanzoAuth extends EventEmitter {
reject(new Error('Invalid state parameter'));
return;
}
if (code) {
// Exchange code for tokens
try {
await this.exchangeCodeForTokens(code, codeVerifier);
// Success response
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<html>
<head>
<title>Hanzo Dev - Login Successful</title>
<style>
body { font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #1a1a1a; color: white; }
.container { text-align: center; }
.success { color: #4ade80; font-size: 48px; margin-bottom: 20px; }
</style>
</head>
<body>
<div class="container">
<div class="success">✓</div>
<h1>Login Successful!</h1>
<p>You can now close this window and return to your terminal.</p>
</div>
<script>setTimeout(() => window.close(), 3000);</script>
</body>
</html>
`);
this.server?.close();
resolve(true);
} catch (error) {
res.writeHead(500);
res.end('Failed to exchange code for tokens');
this.server?.close();
reject(error);
}
// Implicit flow: access_token comes directly in URL params
const accessToken = url.searchParams.get('access_token');
// Code flow fallback
const code = url.searchParams.get('code');
const successHtml = `<html><head><title>Hanzo Dev - Login Successful</title>
<style>body{font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#1a1a1a;color:white}.container{text-align:center}.success{color:#4ade80;font-size:48px;margin-bottom:20px}</style>
</head><body><div class="container"><div class="success">✓</div><h1>Login Successful!</h1>
<p>You can now close this window.</p></div>
<script>setTimeout(()=>window.close(),3000)</script></body></html>`;
if (accessToken) {
// Implicit flow — token received directly
const refreshToken = url.searchParams.get('refresh_token');
const expiresIn = url.searchParams.get('expires_in');
this.credentials = {
accessToken,
refreshToken: refreshToken || undefined,
expiresAt: expiresIn ? Date.now() + parseInt(expiresIn, 10) * 1000 : Date.now() + 168 * 3600 * 1000,
...this.credentials,
};
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(successHtml);
this.server?.close();
// Fetch user info and save
this.fetchUserInfo().then(() => this.syncAPIKeys()).then(() => {
this.saveCredentials();
this.emit('login:success', this.credentials);
}).catch(() => {});
resolve(true);
} else if (code) {
// Authorization code flow fallback
(async () => {
try {
await this.exchangeCodeForTokens(code, codeVerifier);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(successHtml);
this.server?.close();
resolve(true);
} catch (error) {
res.writeHead(500);
res.end('Failed to exchange code for tokens');
this.server?.close();
reject(error);
}
})();
} else {
res.writeHead(400);
res.end('No authorization code received');
res.end('No token or authorization code received');
this.server?.close();
reject(new Error('No authorization code received'));
reject(new Error('No token received'));
}
}
});
this.server.listen(port, () => {
// Build authorization URL
const authUrl = new URL('/oauth/authorize', this.config.iamUrl);
// Authorization Code + PKCE (OAuth 2.1 standard)
const authUrl = new URL('/login/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`);
@@ -185,7 +200,7 @@ export class HanzoAuth extends EventEmitter {
}
private async exchangeCodeForTokens(code: string, codeVerifier: string): Promise<void> {
const response = await fetch(`${this.config.iamUrl}/oauth/token`, {
const response = await fetch(`${this.config.iamUrl}/api/login/oauth/access_token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -224,16 +239,26 @@ export class HanzoAuth extends EventEmitter {
private async fetchUserInfo(): Promise<void> {
if (!this.credentials?.accessToken) return;
const response = await fetch(`${this.config.iamUrl}/api/user`, {
headers: {
'Authorization': `Bearer ${this.credentials.accessToken}`
const headers = { 'Authorization': `Bearer ${this.credentials.accessToken}` };
// /api/get-account returns full profile; /api/userinfo only returns sub
try {
const acctResp = await fetch(`${this.config.iamUrl}/api/get-account`, { headers });
if (acctResp.ok) {
const acctJson = await acctResp.json() as any;
const acct = acctJson.data || acctJson;
if (acct.name || acct.email) {
this.credentials.userId = acct.id || acct.sub;
this.credentials.email = acct.email;
return;
}
}
});
} catch { /* fall through */ }
const response = await fetch(`${this.config.iamUrl}/api/userinfo`, { headers });
if (response.ok) {
const user = await response.json() as any;
this.credentials.userId = user.id;
this.credentials.userId = user.id || user.sub;
this.credentials.email = user.email;
}
}
@@ -293,7 +318,7 @@ export class HanzoAuth extends EventEmitter {
}
try {
const response = await fetch(`${this.config.iamUrl}/oauth/token`, {
const response = await fetch(`${this.config.iamUrl}/api/login/oauth/access_token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
+2 -1
View File
@@ -11,7 +11,8 @@
"esModuleInterop": true,
"resolveJsonModule": true,
"lib": ["ES2020", "DOM"],
"skipLibCheck": true
"skipLibCheck": true,
"types": []
},
"include": ["src/index.ts"],
"exclude": ["node_modules", "dist"]
+398 -398
View File
@@ -1,399 +1,399 @@
{
"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.1",
"publisher": "hanzo-ai",
"private": false,
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/hanzoai/dev.git"
},
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Other"
],
"keywords": [
"productivity",
"AI",
"IDE",
"project management",
"context management",
"change tracking",
"knowledge base",
"documentation",
"specification",
"analysis"
],
"icon": "images/icon.png",
"galleryBanner": {
"color": "#C80000",
"theme": "dark"
},
"activationEvents": [
"onStartupFinished",
"onCommand:hanzo.openManager",
"onCommand:hanzo.openWelcomeGuide",
"onCommand:hanzo.reanalyzeProject",
"onCommand:hanzo.triggerReminder",
"onCommand:hanzo.login",
"onCommand:hanzo.logout",
"onCommand:hanzo.debug.authState",
"onCommand:hanzo.debug.clearAuth",
"onCommand:hanzo.checkMetrics",
"onCommand:hanzo.resetMetrics"
],
"main": "./out/extension.js",
"contributes": {
"chatParticipants": [
{
"id": "hanzo",
"name": "Hanzo",
"description": "Ultimate AI engineering toolkit: 200+ LLMs, 4000+ MCP servers, 53+ dev tools",
"isSticky": true
}
],
"commands": [
{
"command": "hanzo.openManager",
"title": "Hanzo: Open Project Manager"
},
{
"command": "hanzo.openWelcomeGuide",
"title": "Hanzo: View Getting Started Guide"
},
{
"command": "hanzo.reanalyzeProject",
"title": "Hanzo: Analyze Project",
"icon": "$(refresh)"
},
{
"command": "hanzo.triggerReminder",
"title": "Hanzo: Show Reminder",
"icon": "$(bell)"
},
{
"command": "hanzo.checkMetrics",
"title": "Hanzo Debug: Check Impact Metrics",
"icon": "$(graph)"
},
{
"command": "hanzo.resetMetrics",
"title": "Hanzo Debug: Reset Impact Metrics",
"icon": "$(trash)"
},
{
"command": "hanzo.login",
"title": "Hanzo: Login"
},
{
"command": "hanzo.logout",
"title": "Hanzo: Logout"
},
{
"command": "hanzo.debug.authState",
"title": "Hanzo Debug: Check Auth State",
"enablement": "isDevelopment"
},
{
"command": "hanzo.debug.clearAuth",
"title": "Hanzo Debug: Clear Auth Data",
"enablement": "isDevelopment"
}
],
"configuration": {
"title": "Hanzo AI Context Manager",
"properties": {
"hanzo.ide": {
"type": "string",
"enum": [
"cursor",
"copilot",
"continue",
"codium"
],
"enumDescriptions": [
"Using Cursor IDE (.cursorrules)",
"Using GitHub Copilot (.github/copilot-instructions.md)",
"Using Continue (.continuerules)",
"Using Codium (.windsurfrules)"
],
"default": "cursor",
"description": "Select which IDE to generate rules for"
},
"hanzo.mcp.enabled": {
"type": "boolean",
"default": true,
"description": "Enable Model Context Protocol (MCP) server integration"
},
"hanzo.mcp.transport": {
"type": "string",
"enum": [
"stdio",
"tcp"
],
"default": "stdio",
"description": "Transport method for MCP server communication"
},
"hanzo.mcp.port": {
"type": "number",
"default": 3000,
"description": "Port for MCP server when using TCP transport"
},
"hanzo.mcp.backend": {
"type": "string",
"enum": [
"auto",
"python",
"typescript",
"rust",
"local-node"
],
"enumDescriptions": [
"Auto-detect: prefer Python (uvx) if available, fallback to TypeScript",
"Python MCP via uvx (recommended, most comprehensive tools)",
"Built-in TypeScript MCP server",
"Rust MCP binary (hanzo-mcp)",
"Connect to local hanzod node"
],
"default": "auto",
"description": "MCP backend to use. Python via uvx is recommended for the most comprehensive tool support."
},
"hanzo.mcp.pythonCommand": {
"type": "string",
"default": "uvx hanzo-mcp",
"description": "Command to run Python MCP server (e.g., 'uvx hanzo-mcp' or 'python -m hanzo_mcp')"
},
"hanzo.mcp.rustBinary": {
"type": "string",
"default": "hanzo-mcp",
"description": "Path to Rust MCP binary (when backend is 'rust')"
},
"hanzo.mcp.localNodeUrl": {
"type": "string",
"default": "http://localhost:8080",
"description": "URL of local hanzod node (when backend is 'local-node')"
},
"hanzo.mcp.allowedPaths": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "Paths that MCP tools are allowed to access"
},
"hanzo.mcp.disableWriteTools": {
"type": "boolean",
"default": false,
"description": "Disable all write operations in MCP tools"
},
"hanzo.mcp.disableSearchTools": {
"type": "boolean",
"default": false,
"description": "Disable all search operations in MCP tools"
},
"hanzo.mcp.disableBrowserTool": {
"type": "boolean",
"default": false,
"description": "Disable the built-in browser tool (useful when using Antigravity's native browser)"
},
"hanzo.mcp.enabledTools": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "List of explicitly enabled MCP tools"
},
"hanzo.mcp.disabledTools": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "List of explicitly disabled MCP tools"
},
"hanzo.api.endpoint": {
"type": "string",
"default": "https://api.hanzo.ai/ext/v1",
"description": "API endpoint for Hanzo services"
},
"hanzo.debug": {
"type": "boolean",
"default": false,
"description": "Enable debug logging"
},
"hanzo.llm.provider": {
"type": "string",
"enum": [
"hanzo",
"lmstudio",
"ollama",
"openai",
"anthropic"
],
"default": "hanzo",
"description": "LLM provider to use for AI features"
},
"hanzo.llm.hanzo.apiKey": {
"type": "string",
"description": "API key for Hanzo AI Gateway"
},
"hanzo.llm.lmstudio.endpoint": {
"type": "string",
"default": "http://localhost:1234/v1",
"description": "LM Studio API endpoint"
},
"hanzo.llm.lmstudio.model": {
"type": "string",
"description": "Model to use in LM Studio"
},
"hanzo.llm.ollama.endpoint": {
"type": "string",
"default": "http://localhost:11434",
"description": "Ollama API endpoint"
},
"hanzo.llm.ollama.model": {
"type": "string",
"default": "llama2",
"description": "Model to use in Ollama"
},
"hanzo.llm.openai.apiKey": {
"type": "string",
"description": "OpenAI API key"
},
"hanzo.llm.openai.model": {
"type": "string",
"default": "gpt-4",
"description": "OpenAI model to use"
},
"hanzo.llm.anthropic.apiKey": {
"type": "string",
"description": "Anthropic API key"
},
"hanzo.llm.anthropic.model": {
"type": "string",
"default": "claude-3-opus-20240229",
"description": "Anthropic model to use"
}
}
},
"menus": {
"editor/title": [
{
"command": "hanzo.reanalyzeProject",
"group": "navigation",
"when": "resourceScheme != extension"
}
]
}
},
"scripts": {
"vscode:prepublish": "node scripts/compile-main.js || true && npm run build:mcp",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"lint": "eslint .",
"test": "node ./out/test/runTest.js",
"test:simple": "node test-simple.js",
"test:pm": "cross-env MOCHA_TEST_FILE=\"ProjectManager Test Suite\" node ./out/test/runTest.js",
"test:auth": "cross-env MOCHA_TEST_FILE=\"Auth and API Test Suite\" node ./out/test/runTest.js",
"test:file-collection": "cross-env MOCHA_TEST_FILE=\"FileCollectionService Test Suite\" node ./out/test/runTest.js",
"test:mcp": "cross-env MOCHA_TEST_FILE=\"MCP.*Tool Test Suite\" node ./out/test/runTest.js",
"test:mcp-proxy": "cross-env MOCHA_TEST_FILE=\"MCP.*Proxy Test\" node ./out/test/runTest.js",
"test:mcp-integration": "cross-env MOCHA_TEST_FILE=\"MCP Proxy Integration\" node ./out/test/runTest.js",
"test:coverage": "c8 npm test",
"test:vitest": "vitest",
"test:vitest:coverage": "vitest --coverage",
"test:vitest:run": "vitest run --coverage",
"build:mcp": "node scripts/build-mcp-standalone.js",
"build:claude-desktop": "npm run build:mcp && node scripts/build-claude-desktop.js",
"build": "tsc -p ./",
"build:dxt": "node scripts/build-dxt.js",
"build:npm": "node scripts/build-mcp-npm.js",
"build:cursor": "node scripts/build-cursor.js",
"build:windsurf": "node scripts/build-windsurf.js",
"build:all": "node scripts/build-all-platforms.js",
"build:browser-extension": "node src/browser-extension/build.js",
"package": "npm run build:all && sed -i '' 's/\"name\": \"@hanzo\\/extension\"/\"name\": \"hanzo-extension\"/' package.json && vsce package --no-dependencies; sed -i '' 's/\"name\": \"hanzo-extension\"/\"name\": \"@hanzo\\/extension\"/' package.json",
"package:claude": "npm run build:claude-desktop",
"package:dxt": "npm run build:dxt",
"publish": "npm run package && npx vsce publish && ovsx publish",
"publish:npm": "npm run build:claude-desktop && cd dist/claude-desktop && npm publish",
"dev": "cross-env VSCODE_ENV=development npm run watch",
"dev:mcp": "cross-env MCP_TRANSPORT=stdio node ./out/mcp-server-standalone.js",
"start:prod": "cross-env VSCODE_ENV=production npm run watch",
"preview": "npx serve . -p 3000",
"deploy": "vercel --prod",
"deploy:preview": "vercel"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/inquirer": "^9.0.8",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^21.1.7",
"@types/lodash": "^4.17.16",
"@types/minimatch": "^5.1.2",
"@types/mocha": "^10.0.10",
"@types/node": "^16.18.126",
"@types/node-fetch": "^2.6.12",
"@types/sinon": "^17.0.4",
"@types/sinonjs__fake-timers": "^8.1.5",
"@types/uuid": "^10.0.0",
"@types/vscode": "^1.85.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitest/coverage-v8": "^0.34.6",
"@vscode/test-cli": "^0.0.11",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^2.24.0",
"archiver": "^7.0.1",
"better-sqlite3": "^12.2.0",
"c8": "^10.1.3",
"cross-env": "^7.0.3",
"esbuild": "^0.25.6",
"eslint": "^8.26.0",
"glob": "^10.3.10",
"jsdom": "^26.1.0",
"mocha": "^11.0.1",
"node-fetch": "^3.3.2",
"sinon": "^21.0.0",
"typescript": "^5.2.2",
"vitest": "^0.34.6"
},
"dependencies": {
"@lancedb/lancedb": "^0.21.0",
"@modelcontextprotocol/sdk": "^1.14.0",
"@supabase/supabase-js": "^2.48.1",
"@typescript-eslint/typescript-estree": "^8.35.1",
"axios": "^1.6.2",
"ignore": "^7.0.3",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"minimatch": "^10.0.1",
"rxdb": "^16.15.0",
"rxjs": "^7.8.2",
"tree-sitter": "^0.21.1",
"tree-sitter-javascript": "^0.23.1",
"tree-sitter-typescript": "^0.23.2",
"zod": "^3.25.71"
},
"__metadata": {
"id": "fd0ca99f-75f0-4318-af8c-660c5e883c16",
"publisherId": "c3a25647-333f-4954-a3a7-a5487b656f72",
"publisherDisplayName": "hanzo-ai",
"targetPlatform": "undefined",
"isApplicationScoped": false,
"isPreReleaseVersion": false,
"hasPreReleaseVersion": false,
"installedTimestamp": 1743812550788,
"pinned": false,
"preRelease": false,
"source": "gallery",
"size": 15245886
}
}
"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.14",
"publisher": "hanzo-ai",
"private": false,
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/hanzoai/dev.git"
},
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Other"
],
"keywords": [
"productivity",
"AI",
"IDE",
"project management",
"context management",
"change tracking",
"knowledge base",
"documentation",
"specification",
"analysis"
],
"icon": "images/icon.png",
"galleryBanner": {
"color": "#C80000",
"theme": "dark"
},
"activationEvents": [
"onStartupFinished",
"onCommand:hanzo.openManager",
"onCommand:hanzo.openWelcomeGuide",
"onCommand:hanzo.reanalyzeProject",
"onCommand:hanzo.triggerReminder",
"onCommand:hanzo.login",
"onCommand:hanzo.logout",
"onCommand:hanzo.debug.authState",
"onCommand:hanzo.debug.clearAuth",
"onCommand:hanzo.checkMetrics",
"onCommand:hanzo.resetMetrics"
],
"main": "./out/extension.js",
"contributes": {
"chatParticipants": [
{
"id": "hanzo",
"name": "Hanzo",
"description": "Ultimate AI engineering toolkit: 200+ LLMs, 4000+ MCP servers, 53+ dev tools",
"isSticky": true
}
],
"commands": [
{
"command": "hanzo.openManager",
"title": "Hanzo: Open Project Manager"
},
{
"command": "hanzo.openWelcomeGuide",
"title": "Hanzo: View Getting Started Guide"
},
{
"command": "hanzo.reanalyzeProject",
"title": "Hanzo: Analyze Project",
"icon": "$(refresh)"
},
{
"command": "hanzo.triggerReminder",
"title": "Hanzo: Show Reminder",
"icon": "$(bell)"
},
{
"command": "hanzo.checkMetrics",
"title": "Hanzo Debug: Check Impact Metrics",
"icon": "$(graph)"
},
{
"command": "hanzo.resetMetrics",
"title": "Hanzo Debug: Reset Impact Metrics",
"icon": "$(trash)"
},
{
"command": "hanzo.login",
"title": "Hanzo: Login"
},
{
"command": "hanzo.logout",
"title": "Hanzo: Logout"
},
{
"command": "hanzo.debug.authState",
"title": "Hanzo Debug: Check Auth State",
"enablement": "isDevelopment"
},
{
"command": "hanzo.debug.clearAuth",
"title": "Hanzo Debug: Clear Auth Data",
"enablement": "isDevelopment"
}
],
"configuration": {
"title": "Hanzo AI Context Manager",
"properties": {
"hanzo.ide": {
"type": "string",
"enum": [
"cursor",
"copilot",
"continue",
"codium"
],
"enumDescriptions": [
"Using Cursor IDE (.cursorrules)",
"Using GitHub Copilot (.github/copilot-instructions.md)",
"Using Continue (.continuerules)",
"Using Codium (.windsurfrules)"
],
"default": "cursor",
"description": "Select which IDE to generate rules for"
},
"hanzo.mcp.enabled": {
"type": "boolean",
"default": true,
"description": "Enable Model Context Protocol (MCP) server integration"
},
"hanzo.mcp.transport": {
"type": "string",
"enum": [
"stdio",
"tcp"
],
"default": "stdio",
"description": "Transport method for MCP server communication"
},
"hanzo.mcp.port": {
"type": "number",
"default": 3000,
"description": "Port for MCP server when using TCP transport"
},
"hanzo.mcp.backend": {
"type": "string",
"enum": [
"auto",
"python",
"typescript",
"rust",
"local-node"
],
"enumDescriptions": [
"Auto-detect: prefer Python (uvx) if available, fallback to TypeScript",
"Python MCP via uvx (recommended, most comprehensive tools)",
"Built-in TypeScript MCP server",
"Rust MCP binary (hanzo-mcp)",
"Connect to local hanzod node"
],
"default": "auto",
"description": "MCP backend to use. Python via uvx is recommended for the most comprehensive tool support."
},
"hanzo.mcp.pythonCommand": {
"type": "string",
"default": "uvx hanzo-mcp",
"description": "Command to run Python MCP server (e.g., 'uvx hanzo-mcp' or 'python -m hanzo_mcp')"
},
"hanzo.mcp.rustBinary": {
"type": "string",
"default": "hanzo-mcp",
"description": "Path to Rust MCP binary (when backend is 'rust')"
},
"hanzo.mcp.localNodeUrl": {
"type": "string",
"default": "http://localhost:8080",
"description": "URL of local hanzod node (when backend is 'local-node')"
},
"hanzo.mcp.allowedPaths": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "Paths that MCP tools are allowed to access"
},
"hanzo.mcp.disableWriteTools": {
"type": "boolean",
"default": false,
"description": "Disable all write operations in MCP tools"
},
"hanzo.mcp.disableSearchTools": {
"type": "boolean",
"default": false,
"description": "Disable all search operations in MCP tools"
},
"hanzo.mcp.disableBrowserTool": {
"type": "boolean",
"default": false,
"description": "Disable the built-in browser tool (useful when using Antigravity's native browser)"
},
"hanzo.mcp.enabledTools": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "List of explicitly enabled MCP tools"
},
"hanzo.mcp.disabledTools": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "List of explicitly disabled MCP tools"
},
"hanzo.api.endpoint": {
"type": "string",
"default": "https://api.hanzo.ai/ext/v1",
"description": "API endpoint for Hanzo services"
},
"hanzo.debug": {
"type": "boolean",
"default": false,
"description": "Enable debug logging"
},
"hanzo.llm.provider": {
"type": "string",
"enum": [
"hanzo",
"lmstudio",
"ollama",
"openai",
"anthropic"
],
"default": "hanzo",
"description": "LLM provider to use for AI features"
},
"hanzo.llm.hanzo.apiKey": {
"type": "string",
"description": "API key for Hanzo AI Gateway"
},
"hanzo.llm.lmstudio.endpoint": {
"type": "string",
"default": "http://localhost:1234/v1",
"description": "LM Studio API endpoint"
},
"hanzo.llm.lmstudio.model": {
"type": "string",
"description": "Model to use in LM Studio"
},
"hanzo.llm.ollama.endpoint": {
"type": "string",
"default": "http://localhost:11434",
"description": "Ollama API endpoint"
},
"hanzo.llm.ollama.model": {
"type": "string",
"default": "llama2",
"description": "Model to use in Ollama"
},
"hanzo.llm.openai.apiKey": {
"type": "string",
"description": "OpenAI API key"
},
"hanzo.llm.openai.model": {
"type": "string",
"default": "gpt-4",
"description": "OpenAI model to use"
},
"hanzo.llm.anthropic.apiKey": {
"type": "string",
"description": "Anthropic API key"
},
"hanzo.llm.anthropic.model": {
"type": "string",
"default": "claude-3-opus-20240229",
"description": "Anthropic model to use"
}
}
},
"menus": {
"editor/title": [
{
"command": "hanzo.reanalyzeProject",
"group": "navigation",
"when": "resourceScheme != extension"
}
]
}
},
"scripts": {
"vscode:prepublish": "node scripts/compile-main.js || true && npm run build:mcp",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"lint": "eslint .",
"test": "node ./out/test/runTest.js",
"test:simple": "node test-simple.js",
"test:pm": "cross-env MOCHA_TEST_FILE=\"ProjectManager Test Suite\" node ./out/test/runTest.js",
"test:auth": "cross-env MOCHA_TEST_FILE=\"Auth and API Test Suite\" node ./out/test/runTest.js",
"test:file-collection": "cross-env MOCHA_TEST_FILE=\"FileCollectionService Test Suite\" node ./out/test/runTest.js",
"test:mcp": "cross-env MOCHA_TEST_FILE=\"MCP.*Tool Test Suite\" node ./out/test/runTest.js",
"test:mcp-proxy": "cross-env MOCHA_TEST_FILE=\"MCP.*Proxy Test\" node ./out/test/runTest.js",
"test:mcp-integration": "cross-env MOCHA_TEST_FILE=\"MCP Proxy Integration\" node ./out/test/runTest.js",
"test:coverage": "c8 npm test",
"test:vitest": "vitest",
"test:vitest:coverage": "vitest --coverage",
"test:vitest:run": "vitest run --coverage",
"build:mcp": "node scripts/build-mcp-standalone.js",
"build:claude-desktop": "npm run build:mcp && node scripts/build-claude-desktop.js",
"build": "tsc -p ./",
"build:dxt": "node scripts/build-dxt.js",
"build:npm": "node scripts/build-mcp-npm.js",
"build:cursor": "node scripts/build-cursor.js",
"build:windsurf": "node scripts/build-windsurf.js",
"build:all": "node scripts/build-all-platforms.js",
"build:browser-extension": "node src/browser-extension/build.js",
"package": "npm run build:all && sed -i '' 's/\"name\": \"@hanzo\\/extension\"/\"name\": \"hanzo-extension\"/' package.json && vsce package --no-dependencies; sed -i '' 's/\"name\": \"hanzo-extension\"/\"name\": \"@hanzo\\/extension\"/' package.json",
"package:claude": "npm run build:claude-desktop",
"package:dxt": "npm run build:dxt",
"publish": "npm run package && npx vsce publish && ovsx publish",
"publish:npm": "npm run build:claude-desktop && cd dist/claude-desktop && npm publish",
"dev": "cross-env VSCODE_ENV=development npm run watch",
"dev:mcp": "cross-env MCP_TRANSPORT=stdio node ./out/mcp-server-standalone.js",
"start:prod": "cross-env VSCODE_ENV=production npm run watch",
"preview": "npx serve . -p 3000",
"deploy": "vercel --prod",
"deploy:preview": "vercel"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/inquirer": "^9.0.8",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^21.1.7",
"@types/lodash": "^4.17.16",
"@types/minimatch": "^5.1.2",
"@types/mocha": "^10.0.10",
"@types/node": "^16.18.126",
"@types/node-fetch": "^2.6.12",
"@types/sinon": "^17.0.4",
"@types/sinonjs__fake-timers": "^8.1.5",
"@types/uuid": "^10.0.0",
"@types/vscode": "^1.85.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitest/coverage-v8": "^0.34.6",
"@vscode/test-cli": "^0.0.11",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^2.24.0",
"archiver": "^7.0.1",
"better-sqlite3": "^12.2.0",
"c8": "^10.1.3",
"cross-env": "^7.0.3",
"esbuild": "^0.25.6",
"eslint": "^8.26.0",
"glob": "^10.3.10",
"jsdom": "^26.1.0",
"mocha": "^11.0.1",
"node-fetch": "^3.3.2",
"sinon": "^21.0.0",
"typescript": "^5.2.2",
"vitest": "^0.34.6"
},
"dependencies": {
"@lancedb/lancedb": "^0.21.0",
"@modelcontextprotocol/sdk": "^1.14.0",
"@supabase/supabase-js": "^2.48.1",
"@typescript-eslint/typescript-estree": "^8.35.1",
"axios": "^1.6.2",
"ignore": "^7.0.3",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"minimatch": "^10.0.1",
"rxdb": "^16.15.0",
"rxjs": "^7.8.2",
"tree-sitter": "^0.21.1",
"tree-sitter-javascript": "^0.23.1",
"tree-sitter-typescript": "^0.23.2",
"zod": "^3.25.71"
},
"__metadata": {
"id": "fd0ca99f-75f0-4318-af8c-660c5e883c16",
"publisherId": "c3a25647-333f-4954-a3a7-a5487b656f72",
"publisherDisplayName": "hanzo-ai",
"targetPlatform": "undefined",
"isApplicationScoped": false,
"isPreReleaseVersion": false,
"hasPreReleaseVersion": false,
"installedTimestamp": 1743812550788,
"pinned": false,
"preRelease": false,
"source": "gallery",
"size": 15245886
}
}
+93 -66
View File
@@ -12,7 +12,8 @@ interface AuthToken {
}
interface CasdoorConfig {
endpoint: string;
endpoint: string; // Casdoor API (iam.hanzo.ai)
loginUrl: string; // Login UI (hanzo.id)
clientId: string;
clientSecret?: string;
applicationName: string;
@@ -38,9 +39,10 @@ export class StandaloneAuthManager {
this.tokenFile = path.join(this.configDir, 'auth.json');
this.deviceIdFile = path.join(this.configDir, 'device.json');
// Casdoor configuration for iam.hanzo.ai
// Casdoor configuration: login UI on hanzo.id, API on iam.hanzo.ai
this.casdoorConfig = {
endpoint: process.env.HANZO_IAM_ENDPOINT || 'https://iam.hanzo.ai',
loginUrl: process.env.HANZO_IAM_LOGIN_URL || 'https://hanzo.id',
clientId: process.env.HANZO_IAM_CLIENT_ID || 'hanzo-mcp',
clientSecret: process.env.HANZO_IAM_CLIENT_SECRET,
applicationName: 'hanzo-mcp',
@@ -102,7 +104,7 @@ export class StandaloneAuthManager {
private async refreshToken(refreshToken: string): Promise<AuthToken | null> {
try {
const response = await axios.post(`${this.casdoorConfig.endpoint}/api/refresh-token`, {
const response = await axios.post(`${this.casdoorConfig.endpoint}/api/login/oauth/access_token`, {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.casdoorConfig.clientId,
@@ -160,50 +162,44 @@ export class StandaloneAuthManager {
const deviceId = this.getDeviceId();
const state = crypto.randomBytes(16).toString('hex');
// Build OAuth URL for Casdoor
const authUrl = new URL(`${this.casdoorConfig.endpoint}/login/oauth/authorize`);
// Build OAuth URL — Authorization Code + PKCE (OAuth 2.1 standard)
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`);
authUrl.searchParams.append('client_id', this.casdoorConfig.clientId);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('redirect_uri', 'http://localhost:8765/callback');
authUrl.searchParams.append('scope', 'read write');
authUrl.searchParams.append('state', state);
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
authUrl.searchParams.append('device_id', deviceId);
// Start local callback server
const callbackServer = await this.startCallbackServer(state);
const callbackResult = await this.startCallbackServer(state, codeVerifier);
// Open browser
console.log('Opening browser for authentication...');
this.openBrowser(authUrl.toString());
// Wait for callback
const authCode = await callbackServer;
if (!authCode) {
console.error('Authentication failed: No authorization code received');
const result = await callbackResult;
if (!result) {
console.error('Authentication failed: No token received');
return false;
}
// Exchange code for token
const tokenResponse = await axios.post(`${this.casdoorConfig.endpoint}/api/login/oauth/access_token`, {
grant_type: 'authorization_code',
code: authCode,
client_id: this.casdoorConfig.clientId,
client_secret: this.casdoorConfig.clientSecret,
redirect_uri: 'http://localhost:8765/callback'
});
const token: AuthToken = {
token: result.accessToken,
refreshToken: result.refreshToken,
expiresAt: result.expiresAt || Date.now() + 168 * 3600 * 1000,
};
if (tokenResponse.data?.access_token) {
const token: AuthToken = {
token: tokenResponse.data.access_token,
refreshToken: tokenResponse.data.refresh_token,
expiresAt: Date.now() + (tokenResponse.data.expires_in || 3600) * 1000
};
await this.storeToken(token);
console.log('\n✅ Authentication successful!\n');
return true;
}
await this.storeToken(token);
console.log('\n✅ Authentication successful!\n');
return true;
} catch (error) {
console.error('Authentication failed:', error);
}
@@ -211,54 +207,85 @@ export class StandaloneAuthManager {
return false;
}
private async startCallbackServer(expectedState: string): Promise<string | null> {
private async startCallbackServer(expectedState: string, codeVerifier?: string): Promise<{ accessToken: string; refreshToken?: string; expiresAt?: number } | null> {
const endpoint = this.casdoorConfig.endpoint;
const clientId = this.casdoorConfig.clientId;
const clientSecret = this.casdoorConfig.clientSecret;
return new Promise((resolve) => {
const http = require('http');
const url = require('url');
const server = http.createServer((req: any, res: any) => {
const successHtml = `<html><body style="font-family:system-ui;text-align:center;padding:50px">
<h1 style="color:#10b981">Authentication Successful!</h1>
<p>You can close this window.</p>
<script>setTimeout(()=>window.close(),3000)</script></body></html>`;
const server = http.createServer(async (req: any, res: any) => {
const parsedUrl = url.parse(req.url, true);
if (parsedUrl.pathname === '/callback') {
const code = parsedUrl.query.code;
const state = parsedUrl.query.state;
if (state === expectedState && code) {
if (parsedUrl.pathname !== '/callback') return;
const state = parsedUrl.query.state;
if (state !== expectedState) {
res.writeHead(400, { 'Content-Type': 'text/html' });
res.end('<h1>Authentication Failed</h1><p>Invalid state.</p>');
server.close();
resolve(null);
return;
}
const code = parsedUrl.query.code;
const directToken = parsedUrl.query.access_token;
if (code && codeVerifier) {
// Authorization Code + PKCE exchange
try {
const body: Record<string, string> = {
grant_type: 'authorization_code',
client_id: clientId,
code: code as string,
redirect_uri: 'http://localhost:8765/callback',
code_verifier: codeVerifier,
};
if (clientSecret) body.client_secret = clientSecret;
const tokenResp = await axios.post(`${endpoint}/api/login/oauth/access_token`, body);
const tokens = tokenResp.data;
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<html>
<head>
<title>Authentication Successful</title>
<style>
body { font-family: system-ui; text-align: center; padding: 50px; }
h1 { color: #10b981; }
</style>
</head>
<body>
<h1>✅ Authentication Successful!</h1>
<p>You can now close this window and return to your terminal.</p>
<script>window.setTimeout(() => window.close(), 3000);</script>
</body>
</html>
`);
res.end(successHtml);
server.close();
resolve(code as string);
} else {
res.writeHead(400, { 'Content-Type': 'text/html' });
res.end('<h1>Authentication Failed</h1><p>Invalid state or missing code.</p>');
resolve({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined,
});
} catch (err: any) {
res.writeHead(500, { 'Content-Type': 'text/html' });
res.end(`<h1>Token Exchange Failed</h1><p>${err.message}</p>`);
server.close();
resolve(null);
}
} else if (directToken) {
// Implicit flow fallback
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(successHtml);
server.close();
const expiresIn = parsedUrl.query.expires_in ? parseInt(parsedUrl.query.expires_in, 10) : undefined;
resolve({
accessToken: directToken as string,
refreshToken: parsedUrl.query.refresh_token as string | undefined,
expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : undefined,
});
} else {
res.writeHead(400, { 'Content-Type': 'text/html' });
res.end('<h1>Authentication Failed</h1><p>No code or token received.</p>');
server.close();
resolve(null);
}
});
server.listen(8765, 'localhost');
// Timeout after 5 minutes
setTimeout(() => {
server.close();
resolve(null);
}, 300000);
setTimeout(() => { server.close(); resolve(null); }, 300000);
});
}
@@ -134,7 +134,7 @@ export class BrowserExtensionServer extends EventEmitter {
return null;
}
public close() {
this.wss.close();
public close(callback?: () => void) {
this.wss.close(callback);
}
}
+24
View File
@@ -154,6 +154,12 @@ importers:
jsdom:
specifier: ^26.1.0
version: 26.1.0
react:
specifier: ^18.3.1
version: 18.3.1
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
typescript:
specifier: ^5.8.3
version: 5.8.3
@@ -4779,6 +4785,11 @@ packages:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
react: ^18.3.1
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
@@ -4937,6 +4948,9 @@ packages:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true
@@ -10494,6 +10508,12 @@ snapshots:
minimist: 1.2.8
strip-json-comments: 2.0.1
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
react-is@18.3.1: {}
react@18.3.1:
@@ -10716,6 +10736,10 @@ snapshots:
dependencies:
xmlchars: 2.2.0
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
semver@5.7.2: {}
semver@7.7.2: {}