The package's `npm test` has been broken-by-construction for years:
tsconfig excluded src/test entirely (out/test/runTest.js never existed),
tsconfig.test.json inherited the exclude that negated its include, the
runner pointed at a ./suite/index path that never existed, two suites used
BDD describe/it under the tdd-configured mocha, a vitest file sat in the
mocha glob, `ws` was never declared (it resolved only cross-package), and
154 eslint errors blocked pretest.
Lint 154 → 0 with real fixes: the ban-types autofix damage reverted
properly (the local `Symbol` interface shadowing the ES built-in — the
actual footgun — is now `CodeSymbol` everywhere), case bodies braced via
AST codemod, empty catches annotated, `Function` types replaced with real
signatures, hasOwnProperty via Object.prototype, and
@typescript-eslint/no-var-requires off — CJS lazy requires are the
intentional activation-perf idiom (the rule is retired to stylistic in
typescript-eslint v8).
Harness: tests compile via tsconfig.test.json in pretest; runner path
fixed; BDD→TDD; glob v10 promise API; esModuleInterop default imports.
Two harnesses, one directory each: vitest owns src/test/vitest/ (scoped
vitest.config, excluded from the mocha compile), mocha owns the rest;
posttest runs the vitest side so `npm test` is the whole gate.
Suites modernized against current tool contracts (34 passing / 64 failing
→ 152 passing / 0 failing / 12 pending): web-fetch rewritten against the
real http(s) path via loopback server, mode against list/activate/show/
current, mcp-tools against config-driven getAllTools filtering, extension
pins the shipped manifest contract, shell/process/git-search/filesystem/
config/bash/mcp-runner exercise real commands, files, repos and spawns.
Dead-subject suites removed; network-dependent integration tests gated on
HANZO_E2E_LIVE with visible skip reasons (12 pending). Tests-only — the
one suspected product bug was an unfaithful Memento mock, and a real
cross-suite settings leak was fixed by making suites hermetic.
CI: cli-tools and vscode test steps lose continue-on-error and become
hard gates (vscode under xvfb-run, same as the E2E job).
The first pass pinned each override to its advisory-snapshot floor, but
several packages carry stacked advisories — a pin at one advisory's floor
sits inside the next one's vulnerable range (protobufjs 7.5.5 vs 7.6.3,
tar 7.5.16, tmp 0.2.7, flatted 3.4.2, vite 6.4.3, file-type 21.3.2,
qs 6.15.2, js-yaml 4.2.0). One within-major floor per package now, at the
highest first-patched. Suites: browser 243, ai 31, aci 29, cli-tools 87.
- pnpm overrides pin every vulnerable transitive to its advisory's exact
first-patched version, scoped to the advisory's own vulnerable range so
no dependency is dragged across a major it didn't need (criticals:
form-data 4.0.6, handlebars 4.7.9, protobufjs 7.5.5).
- vitest < 3.2.6 is a critical (CVE fix floor) with no 0.x/1.x backport —
forced major to ^3.2.6 in every package. Test scripts unify on
`vitest run` (aci/ai ran bare watch mode). tools' vscode mock alias
becomes absolute (vitest 3 dropped relative alias resolution).
- packages/ai otel family 1.x → 2.x (core <2.8.0 advisory covers all 1.x):
resourceFromAttributes, ATTR_* semconv, constructor spanProcessors. The
previously-uncollectable telemetry suite now runs: ai 31 tests (was 16).
- direct bumps: vite ^6.4.3 (site), esbuild ^0.25.8 (mcp), uuid ^11.1.1
(tools). packages/ai/package-lock.json deleted — stray npm lockfile in a
pnpm workspace, carried 24 of the alerts.
- ci: test step filtered @hanzo/tools but the package is @hanzo/cli-tools —
the suite never ran in CI. Fixed; 87 tests now gate (behind the existing
continue-on-error). pnpm 9 reads onlyBuiltDependencies from package.json,
not pnpm-workspace.yaml — mirrored so native build scripts stay allowlisted.
Verified: browser 243/243 + e2e 30/30 + build; aci 29/29; ai 31/31;
cli-tools 87/87; site builds on vite 6; mcp builds on esbuild 0.25.
1.9.25 shipped the unregistered app-hanzo client_id, so the PKCE token
exchange failed invalid_client — login has been dead since v1.8.0. The
canonical registered client is hanzo-browser (universe IAM init_data;
redirectUris include https://hanzo.ai/callback). Contracts pinning the
registered client land in shared-config + cross-browser-parity tests.
Adopted from fix/dxt-login-and-ci-guards: the DXT auth tool offered a
password-grant login defaulting to the unregistered app-hanzo client —
removed; whoami/token/account read /v1/iam/oauth/userinfo, refresh
exchanges at /v1/iam/oauth/token, issuance is authorization-code+PKCE
only. dxt build script now actually builds (mcp cli → server.js) instead
of echoing success. PR #6's test-suite restorations are not taken — main
already restored those suites.
Migrate the last llm.hanzo.ai references (vscode MCP llm tool provider URL
+ gateway fallback, auth-chat e2e doc comments) to the canonical
https://api.hanzo.ai/v1 (OpenAI-compatible, IAM JWT at the gateway — see
hanzoai/ai). The browser extension chat paths already pointed at
API_BASE_URL = api.hanzo.ai.
model-hub's HuggingFace tests are real live-API integration tests, which
means they fail on HF's weather: repeated runs rate-limit and the 5s
default timeout reads as 6 test failures with nothing of ours broken.
Probe HF once at module load and describe.skipIf with the status logged
when HF itself refuses service; give live calls a 15s budget. A failure
while HF answers 200 is still a real failure. 243/243 across repeated runs.
Three specs still tested the pre-1.9.3 popup (#open-page-overlay FAB,
'Open Chat Panel' label) — the Extension E2E jobs have been red on them
since the FAB was killed. Repointed at the current contract
(#overlay-enabled + #open-panel, 'Open Chat').
The interaction specs (overlay show, picker tool, console tool) all
funnel through the popup's active-tab lookup, which cannot work when the
harness hosts the popup as a tab: the popup IS the active tab, so the
privileged-tab guard always takes the fallback. Each spec now sends the
exact message its button sends (page.overlay.show / tool.picker.start /
tool.console.start), explicitly targeted at the example.com tab, so the
background relay and content-script surfaces are still exercised end to
end. CI's file set (popup, popup-tools, sidebar): 30/30 locally.
Fix the two failures the 1.9.24 tag surfaced:
- test/claude-integration.test.ts bound a fixed port 3002 in beforeEach
and raced the previous test's teardown — EADDRINUSE as an unhandled
'error', a 10s hook timeout, then ECONNREFUSED (flaked 3 of 4 local
runs). BrowserExtensionServer now exposes ready() (resolves on listen,
rejects on bind failure instead of crashing the process) and a port
getter; the test binds port 0 (OS-assigned), awaits ready(), and
terminates the client so wss.close() reliably fires. Six consecutive
green runs.
- CI Build and Publish packaged with the `zip` binary the arc runner
image doesn't have (Publish red since 1.9.23); Publish also wrote the
zips one directory above where its artifact globs and the Chrome Web
Store upload (which additionally pointed at an unversioned filename)
looked, and CI hardcoded the version from the root package.json.
Package with `npx web-ext build` (already in the job for linting),
version from the browser package.json everywhere, store upload path
fixed, and the Linux job no longer ships a junk safari zip — the real
bundle comes from the macOS job.
Root and browser package.json move in lockstep to 1.9.25 (root was left
at 1.9.23 by the 1.9.24 bump). npm + VS Code Marketplace already carry
1.9.24; the stores' first complete release of this line is 1.9.25.
shared/evaluable.ts is the single cross-browser rule for caller-supplied
JS: pickEvaluable accepts every code-param alias (expression/code/script/
function/js); wrapEvaluable passes bare expressions through and wraps
statement bodies in an async IIFE with the trailing value auto-returned.
Both dispatch paths consume it — browser-dispatch.ts (chrome.debugger
Runtime.evaluate, which also gains the plain 'evaluate' method alias
Firefox already accepted) and background-firefox.ts, whose previous
`return (${code})` wrap was a SyntaxError on any multi-statement body
and silently returned undefined for `expr;`. The Firefox path now always
settles promises in-page and routes rejections over the __hanzo_error
channel so callers see the real page-side error.
Tests: behavioral suite for the wrapper (evaluates wrapped output, not
string-matching); revive the three suites dead since the node bridge was
deleted — parity contracts repointed at browser-dispatch.ts, the IAM
contracts updated to the HIP-0111 canonical /v1/iam/oauth/token shape,
hub-wiring trimmed to the live background.ts surface, and the node-bridge
routing suite removed with its subject. 243 passing (was 186 + 3 dead
files).
connectNativeZap had no singleton guard: state.port was overwritten without closing the
prior port, and every onDisconnect scheduled another connect. Called from startup + the
3s reconnect, ports (+ their native hosts) accumulated into a host-spawn storm once the
router stopped rejecting duplicates. Now: tear down any prior port on entry; a replaced
port (state.port !== port) never reconnects. Exactly one native host per browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build.js stamps the package.json version into every manifest (chrome/firefox/safari)
so the three can never drift. Syncs src manifests to 1.9.21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
connectNativeZap (shared/native-zap.ts) registers the browser as a zapd provider
over native messaging and dispatches inbound ROUTE commands. Renames cdp-bridge.ts
-> browser-dispatch.ts; deletes the cdp-bridge-server.ts WS bridge. No ws://localhost,
no port roulette, no CDP fallback in the default boot path. Patch bump (no 2.0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ZAP discovery is unreliable for local hanzo-mcp: the server binds ephemeral
ports (not the probed [9999..9995]) and mDNS needs the unshipped
ai.hanzo.zap_mdns native helper. Re-enable ENABLE_LEGACY_TRANSPORTS so the
extension connects to the running CDP bridge (ws://localhost:9223/cdp +
HTTP :9224) for driving a logged-in Chrome profile. Builds on the 1.9.16-1.9.19
dispatch/CSP fixes. Bumps chrome+firefox manifests + package.json to 1.9.20.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hanzo.click: removed framework-specific hacks (Drupal AJAX lookup, jQuery
fallback). Those don't generalize — every new framework would need its
own hack. Replaced with a clean realistic-event-sequence dispatch:
mouseover → mouseenter → mousemove → mousedown → focus → mouseup → click
with proper clientX/Y/button/buttons/composed. Events still have
isTrusted=false (a WebExtension can't change that), so frameworks that
explicitly check isTrusted (Drupal AJAX behaviors, some React libs)
will still reject — those need the v1.10.0 BiDi backend on the Python
bridge to produce real trusted browser input via Firefox's
--remote-debugging-port WebDriver BiDi endpoint.
Target.activateTarget: now ALSO calls windows.update(focused:true) so
cross-Firefox-window tab switching actually brings the OS window to
the foreground (not just the tab within its current window).
hanzo.switchTab: new Layer-3 ergonomic alias. Same implementation as
Target.activateTarget but with a human-readable name. Recommended for
everyday tab switching in scripts and the MCP browser_tool wrapper.
Architectural note: hanzo.click is the synthetic path (best-effort for
sites that don't check isTrusted). When the v1.10.0 Python bridge BiDi
backend lands, calls to hanzo.click will auto-route to Input.bidi
.dispatchMouseEvent for trusted browser-input events. That is the
proper decomplected design — backend per trustedness level, ergonomic
alias on top. No per-framework hacks in the extension.
Two bugs caught while driving the SEC TCR form:
1) runFunc silently swallowed page-side errors.
scripting.executeScript returns [{result, error, frameId}] per frame.
We were mapping to r?.result which dropped the `error` field — when a
page-side function threw, callers saw null instead of an error.
Fix: keep the full InjectionResult shape and throw on first.error.
2) hanzo.fill called HTMLInputElement's value setter on a TEXTAREA.
That throws TypeError "Illegal invocation" silently (now visible
thanks to fix#1). The chained `|| HTMLTextAreaElement.prototype...`
never executed because the first ?.set was truthy.
Fix: per-tag dispatch — pick the matching prototype's native setter
(INPUT/TEXTAREA/SELECT each get their own). Wrapped in try/catch with
plain `el.value = value` as last resort.
Verified on www.sec.gov/forms/tcr-external-form:
- hanzo.fill on textarea[name^="in_your_own_words"] now sets the value
- hanzo.clear on same textarea now works
handleExtensionRequest() was returning "Unknown extension action" for
every Page.* / DOM.* / Input.* / hanzo.* method added in 1.9.16. The
handler had only routed monitor.*/audit.* and fell off the end for
everything else.
Fix: fall back to executeMethod (the canonical CDP-shaped dispatcher)
before returning Unknown. Strictly additive — all previously-working
actions continue through their existing branches.
Verified locally on www.sec.gov/forms/tcr-external-form: hanzo.listForm
returns 183 elements with full label association; hanzo.scroll +
hanzo.clickByText + hanzo.fillByLabel are now routable.
Next: a v1.10.x multi-backend Python bridge that adds WebDriver BiDi
(Firefox 129+), CDP (Chrome 124+), and safaridriver (Safari 17+) as
secondary backends for network interception, log streams, and other
capabilities the WebExtension scripting API cannot provide. Same wire
surface, prefix-based routing (BiDi.* / CDP.* / WD.*).
Decomplected three orthogonal layers:
L3: hanzo.* ergonomic aliases
L2: CDP-shape canonical primitives (Page.* DOM.* Input.* ...)
L1: runFunc(tabId, fn, args) → scripting.executeScript({world: MAIN, func: ref})
Every DOM op now passes a real function reference to scripting.executeScript
instead of constructing Function(codeStr)(). The Function() path is blocked
by strict page CSP on sec.gov / github.com / any default-src 'self' SPA.
Direct function refs are exempt from page CSP per the WebExtension spec, so
the new path works everywhere.
Added (all CSP-safe):
• hanzo.clickByText fillByLabel findByText listForm submitForm
• hanzo.press scroll waitForText waitForMutation
• hanzo.uploadFile dialogAccept observe/observeRead/observeStop
• Input.dispatchMouseEvent Input.scrollWheel
• DOM.querySelector DOM.scrollIntoView DOM.focus
• Page.printToPDF
Rewrote with runFunc (CSP-safe):
• hanzo.click fill check uncheck clear getText getHTML
• hanzo.querySelectorAll
Capabilities handshake bumped to advertise the new method names.
Driver-side mapping in hanzo-tools-browser/browser_tool.py updated with
~100 snake_case action → wire-method entries (Python MCP surface).
API reference: dist/browser-extension/firefox/API.md
Reverts violations of the durable rule that current-state docs belong
in LLM.md and history belongs in git log. Removed files were session
handoffs, agent-style "complete success" / "1000%" reports, dated
audit dumps, and stub NOTES.
Fixes the browser ↔ hanzo-mcp ZAP path end to end:
- shared/zap.ts: vendor ZAP_PORTS constant and restore localhost
port-probe fallback in discoverZapServers when the mDNS native
helper isn't available. Sequential probe (stops on first hit) so
the Chrome console stays quiet on the happy path.
- src/manifest.json: add deterministic ``key`` (extension ID
``biingenefmanpecedoafkfajbnlgdmbl``) + ``nativeMessaging``
permission so the ``ai.hanzo.zap_mdns`` helper can be invoked.
- background.ts: configurable HF model source for the WebGPU
runtime via chrome.storage (``hanzo_model_url`` or
``hanzo_model_hf={repo,file,revision}``); falls back through HF →
bundled artifact → remote providers.
- webgpu-ai.ts: cache-first fetch backed by the service-worker
Cache API + progress logs, ``WebGPUAI.huggingFaceURL`` helper, and
``WebGPUAI.evictCache`` for rotating model artefacts. Drop-in for
the existing single-blob loader; lays groundwork for sparse-load
MoE work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns the standalone MCP server's auth path with the canonical IAM_
env contract enforced by hanzo-iam 1.30.0 (python-sdk 3cb6a34) — there
is exactly one prefix, IAM_, with no upstream-brand alias chain.
- HANZO_IAM_ENDPOINT → IAM_ENDPOINT
- HANZO_IAM_LOGIN_URL → IAM_LOGIN_URL
- HANZO_IAM_CLIENT_ID → IAM_CLIENT_ID
- HANZO_IAM_CLIENT_SECRET → IAM_CLIENT_SECRET
Updates docs/BUILD.md and docs/MCP_INSTALLATION.md to match.
Module-load WS connections to ws://localhost:3001/browser-extension
(legacy MCP) and ws://localhost:9223/cdp (legacy CDP bridge) spam
ERR_CONNECTION_REFUSED on every reload when no local hanzo-mcp / CDP
server is running.
ZAP mDNS discovery (startZapDiscoveryLoop / discoverZapServers, added
in 1.9.13–1.9.15) is now the canonical transport, so the legacy paths
are dead-on-load by default.
Adds ENABLE_LEGACY_TRANSPORTS = false (top of background.ts) and gates:
- connectToMCP() body (early-return when off)
- The module-load call site for connectToMCP()
- The module-load call site for cdpBridge.startWebSocketServer(cdpPort)
Flip the flag to true when developing against a local WS server.
ZAP path is untouched.
Firefox 151 warns on `data_collection_permissions` — that key is not
yet in Firefox's accepted MV3 manifest schema. The warning was
sufficient to block clean loading of native-messaging permissions in
some contexts (visible in about:debugging as "Reading manifest:
Warning processing data_collection_permissions"), which broke our
mDNS discovery via runtime.sendNativeMessage with an opaque
"unexpected error".
Removed the field. No data-collection semantics depend on it on the
Firefox side; Chrome MV3 still ignores unknown keys.
Per HIP-0069 — extension discovers MCPs via mDNS only, no port-probe
fallback. The legacy [9999..9995] DEFAULT_ZAP_PORTS array is gone;
non-compliant deployments (no mDNS responder, no native-messaging
helper installed) fail loudly so the operator can fix the install
rather than the extension silently picking the wrong server.
shared/zap.ts:
- Replaced DEFAULT_ZAP_PORTS export with HANZO_SERVICE_TYPE constant
('_hanzo._tcp.local.')
- discoverZapServers: mDNS-only, retries every 10s when the helper
is unavailable or the browse comes back empty
- discoverViaMdns: throws on missing native-messaging API instead of
swallowing — surfaces the install gap
background.ts:
- Removed DEFAULT_ZAP_PORTS / SHARED_ZAP_PORTS imports + zapPorts
storage key from getPortConfig
- Both discovery callsites pass undefined for ports (mDNS routes them)
The 1.9.11 alarms-based wake-up worked but felt fragile — the background
was still idling out between alarms. Switching primary keep-alive to
the canonical MV3 Port pattern: every content script in every loaded
http(s) tab opens a `zap-keepalive` chrome.runtime port back to the
background and pings it every 10s. As long as ANY tab holds a port,
the background event-page (Firefox) / service worker (Chrome) stays
loaded indefinitely. The WebSocket survives without polling.
Alarms remain as belt-and-suspenders for the no-tab edge case (fresh
browser window, only privileged pages open).
Package metadata corrected: monorepo root is now `@hanzo/extension`
with description "Hanzo AI Extension" — preparing the surface for
publishing the bits people actually want (zap shared module,
gui-primitives shim, mDNS package).
Root cause of the "connect once, never reconnect" pattern: Firefox MV3
treats `"background.scripts"` as event pages and Chrome MV3 service
workers idle out after ~30s. Once the background unloads, every
setTimeout (including our connectZap retry) silently dies. The first
connect happens on initial load; once that WS closes the script
suspends and never comes back.
Fix: register a `chrome.alarms` / `browser.alarms` 30s alarm. Alarm
events wake the worker/page even when suspended. The handler is a
no-op when the ZAP connection is healthy and triggers
discoverZapServers() when it isn't — closing the gap that the
WebSocket reconnect chain alone could not.
Adds `alarms` permission to both Firefox and Chrome manifests.
ZAP is a long-lived WebSocket. The OS delivers FIN/RST on real
disconnects, which fires ws.onclose → the reconnect chain shipped in
1.9.7. The 20s/8s PING/PONG dance added in 1.9.9 was both unnecessary
and broken: it overwrote ws.onmessage on every tick, stacking closures
indefinitely until the socket closed.
Reverted. The transport stays simple: connect, stream, on close
reconnect. No polling, no wrapper chain.
shared/zap.ts:
Every successful connectZap now arms a 20-second PING heartbeat. If no
PONG arrives within 8s the socket is force-closed so the existing
reconnect chain (1.9.7) takes over. This catches the "half-open"
failure mode where neither side gets ws.onclose — typical when the
agent process is killed -9, a laptop sleeps mid-WS, or NAT keep-alive
drops a stale connection between LAN hops.
ws.onclose now also clears the heartbeat interval, so we never PING a
socket whose owner is in the middle of being torn down.
Combined with 1.9.7's onerror retry and 1.9.8's MAIN→ISOLATED CSP
fallback + 30s evaluate timeout, the 1.9.x line is rock-solid for
low-latency tab driving across MCP restarts, page CSP, and any
hand-off between agents.
executeScript now tries world: 'MAIN' first (so callers reading
page-internal state still work) and silently falls back to ISOLATED on
the recognisable CSP-blocked-Function() error string. This rescues every
strict 'script-src self' page (banking sites, Porkbun account pages,
GitHub, Google Docs) without forcing the caller to know which world it
needs.
Default Runtime.evaluate timeout bumped from 10s → 30s. Page-walks
against jQuery-rendered domain-management UIs with 1.5k+ DOM nodes
routinely need more than 10s for the first selector pass; caller can
still override via params.timeout for short queries.
Together with 1.9.7's reconnect-chain fix the extension can now drive
any Firefox tab end-to-end: stays glued to its ZAP server through
restarts, evaluates JS regardless of page CSP, and exposes the result
on a clean shape (1.9.2's null-coercion + exception-details wiring).
connectZap previously chained a 3s reconnect on ws.onclose only. When the
WebSocket failed BEFORE a successful handshake (typical case: agent
process restarting and rebinding port 9999), Firefox hit ws.onerror →
resolve(null) → the reconnect chain died after a single attempt.
Now both onclose AND onerror schedule the same idempotent retry (guarded
by a one-shot flag so we never double-fire). Firefox stays glued to its
ZAP server across MCP restarts, server upgrades, and any transient bind
window — exactly what we need for low-latency tab driving.
Discovery (shared/zap.ts):
discoverZapServers now tries mDNS via the native-messaging helper
`ai.hanzo.zap_mdns` first (op=browse, 1.5s timeout), falling back to
the legacy `[9999..9995]` port-probe only when the helper isn't
installed or returns no services. With the helper in place we get
collision-free LAN-wide discovery — the same agent can be found
whether it's on this host or another machine on the network.
See ~/work/zap/mdns/ for the helper + manifest:
python/zap_mdns.py publish() / browse() (zeroconf-backed, no-op
fallback when zeroconf isn't installed)
python/omni.py roles: mcp / browser / host / gateway over
shared `_hanzo._tcp.local.` service-type
ext/helper.py stdio native-messaging host
ext/native-messaging-host.json manifest for Firefox NMH path
Manifest:
Adds `nativeMessaging` permission so the helper can be reached.
Popup:
Footer now displays "Hanzo AI v1.9.6" (read from manifest at runtime
so the chip always tracks the actual loaded build).
The extension no longer depends on @hanzo/ui. The single React consumer
(chat-widget.tsx) now imports from `@hanzo/gui`, which the build aliases
to a small local primitives shim at `src/gui-primitives.tsx`. The shim is
strictly monochrome plain DOM (Button, Textarea, Input, Card*, XStack,
YStack, Text) — sized for content-script bundles. When the full tamagui-
based @hanzo/gui adoption ships in a feature branch, swap the alias
target in build.js and remove the shim.
Why the local shim and not the upstream @hanzo/gui (npm) right now:
- the upstream is tamagui-flavoured: react-native-web + @hanzogui/*
packages + the static compiler — far too heavy for the per-page
content-script + background bundles we ship today;
- @hanzo/brand is strict monochrome anyway, so primitives carry no
colour tokens — sibling CSS (popup.css/sidebar.css) does the styling
with the white-luminance scale committed in 1.9.4.
Surface change: every `@hanzo/ui/primitives-common` import is now
`@hanzo/gui`, build.js no longer probes a sibling @hanzo/ui repo, and
`primitives-common-fallback.tsx` is replaced by the canonical
`gui-primitives.tsx`. Bundle drops ~200KB across firefox + chrome from
not pulling the @hanzo/ui dist.
Also includes the in-flight ZAP-bridge / Firefox auth / sidebar
auto-mount tweaks that were already in working-tree.
@hanzo/brand is strictly monochrome (primaryColor #FFFFFF, no chromatic
accents). Stripped every blue/violet/emerald/amber/red literal across
the in-page overlay, sidebar, and popup; replaced with a luminance scale
keyed off white. Status states (success/warn/error) now distinguished
by opacity, not hue.
On-page overlay (content-script):
- Replaces the brittle `looksLikePromiseResult` blue button with a white
primary action (#fff bg / #0a0a0b text) per brand guidelines.
- Composer is now sticky-bottom: messages flex/scroll, composer
flex-shrink: 0, panel min-height: 0 lets the layout collapse cleanly.
- Adds a pin-mode toggle in the header. `mode=push` reflows page content
(sets html.hanzo-overlay-push-{side} → margin-{side}: var(width));
`mode=overlay` floats over the page (default).
- Adds setMode / setWidth / setEnabled message handlers and persists all
three in chrome.storage.sync alongside the existing overlaySide.
- Picks up overlayWidth (compact 320 / default 420 / wide 560) via
--hanzo-overlay-width CSS var, used by both the panel itself and the
push-mode html margin.
Popup:
- "Open Chat" honours overlayEnabled — falls back to standalone tab
when disabled (or when active tab is privileged).
- Adds On-page chat panel checkbox + Width picker (Compact/Default/Wide)
alongside the existing Pin Left/Right.
CSS / brand cleanup:
- sidebar.css usage bars (.bar-blue/.bar-violet/...) → white opacity scale.
- model-hub badges (.badge-cloud/.badge-local/.badge-hub) → mono-luminance.
- inspector-result element-{tag,id,class} → mono-luminance instead of
blue/violet/green syntax tints.
- --success / --warning / --error in both stylesheets → white shades.
- Remove the round "AI" FAB launcher from the in-page overlay. It rendered
as a 54px circle bottom-right and looked out of place; once toggled on
it was sticky because hide() never reset data-enabled. Removed the
button + CSS + listener entirely.
- Reposition the overlay as a full-height edge-pinned sidebar panel
(top:0; bottom:0; right:0; width:min(420px,100vw)) instead of the old
floating bottom-right card. Slides in via translateX rather than
translateY+scale so it reads as a sidebar.
- Add data-side="left|right" with persisted overlaySide setting in
chrome.storage.sync. Popup gains a Pin Left | Pin Right picker that
flips the side on the fly and stores the preference for next page load.
- Unify the popup's two old buttons (Open Chat Panel + Toggle On-Page
Overlay — both did the same thing in different words) into a single
Open Chat button.
- Register sidebar_action in manifest-firefox.json so Firefox users who
prefer the OS-native sidebar can reach the same chat surface via
View → Sidebar → Hanzo AI.