Compare commits

...
Author SHA1 Message Date
hanzo-dev 2177c0a3ca refactor(sdk): remove dead streaming subsystem + fully-commented connect-qr page (~1.2k LOC, red-team #13/#42/#56) — build green 2026-06-22 06:01:30 +00:00
hanzo-devandGitHub f53dc2513a Merge pull request #7 from hanzoai/fix/onboarding-welcome-brand-crash
fix(chat): stream reasoning_content so thinking models reply (#19)
2026-06-21 20:53:56 -07:00
hanzo-dev f892023d4a Merge remote-tracking branch 'origin/main' into fix/onboarding-welcome-brand-crash 2026-06-22 03:53:29 +00:00
hanzo-dev 38b22e7265 fix(chat): stream reasoning_content too so thinking models show a reply (red-team #19/#55) 2026-06-22 03:53:28 +00:00
hanzo-devandGitHub aeed548966 Merge pull request #6 from hanzoai/fix/onboarding-welcome-brand-crash
fix(sdk): default-agents hang + tsc-breaking logo type + 5 GGUF zen models
2026-06-21 20:47:25 -07:00
hanzo-dev 0483a5b099 Merge remote-tracking branch 'origin/main' into fix/onboarding-welcome-brand-crash 2026-06-22 03:46:35 +00:00
hanzo-dev cf084837a5 fix(sdk): bound default-agents polling so it cannot hang 60s + drop dead HanzoAIProps.logo override that broke tsc for all 3 apps + 5 verified GGUF zen models 2026-06-22 03:46:07 +00:00
hanzo-devandGitHub 2a62e59c1c Merge pull request #5 from hanzoai/fix/onboarding-welcome-brand-crash
fix(onboarding): per-app /app-logo.png on landing (stop Hanzo H leaking onto zoo/lux)
2026-06-21 20:02:27 -07:00
hanzo-dev b5217aca35 Merge remote-tracking branch 'origin/main' into fix/onboarding-welcome-brand-crash 2026-06-22 03:00:48 +00:00
hanzo-dev 350f9f2c6e fix(onboarding): use per-app /app-logo.png on landing — stop Hanzo H leaking onto zoo/lux 2026-06-22 02:57:17 +00:00
hanzo-dev 3f7b90359c fix(brand): node-manager window + notifications use per-app /app-logo.png (was hardcoded Hanzo icon — leaked onto zoo/lux) 2026-06-22 00:14:29 +00:00
hanzo-dev 13c4dbf660 fix(ai): show compact app logo in collapsed sidebar 2026-06-21 22:11:56 +00:00
hanzo-devandGitHub 4be2d12e2e Merge pull request #4 from hanzoai/fix/onboarding-welcome-brand-crash
Merge fix/onboarding-welcome-brand-crash
2026-06-21 13:54:24 -07:00
hanzo-dev 45adbc8349 feat(models): Download-all per segment (DRY) + Featured community models (Qwen/Llama/Gemma/Mistral/DeepSeek, verified-public HF GGUF) 2026-06-21 20:51:04 +00:00
hanzo-dev bbe5b9ece6 chore(assets): remove dead Shinkai octopus logo + video (2.3MB, unreferenced) 2026-06-21 20:51:04 +00:00
hanzo-dev 689b811db9 feat(models): native Hanzo engine + Zen catalog (HF download), one Local Models gallery (engine choice Hanzo/Ollama/LMStudio), cosmetic fixes (real Ollama icon, per-brand sidebar logo, cloud scroll) 2026-06-21 20:51:04 +00:00
hanzo-dev 085d2ca295 fix(chat): resolve bare 'default' alias to the real model name (Zen Nano 0.6B) 2026-06-21 20:51:04 +00:00
hanzo-dev bd97e87ab3 fix(chat): show actual model name (Zen Nano 0.6B) in selector, not provider name (Zen Engine) 2026-06-21 20:51:03 +00:00
hanzo-dev 854b1d59d5 fix(onboarding): unblock Skip — inject sha512 for noble-ed25519 v2, node addr from brand config, reset(false), /v1/node paths 2026-06-21 20:51:03 +00:00
hanzo-dev 76247c6366 feat(ai): add optional node field to published BrandConfig surface (matches src/types.ts) 2026-06-21 20:51:03 +00:00
hanzo-dev 0cb5be28a2 feat(@hanzo/ai): BrandConfig.node field + brand-neutral node_* invokes
- BrandConfig.node { apiPort, enginePort } — single source for on-device node address
- brand-neutral node_* command invokes (was hardcoded hanzo_node_spawn)
2026-06-21 20:51:03 +00:00
hanzo-devandClaude Opus 4.8 c480b014b4 fix(@hanzo/ai): export ./ai.css subpath for consumer stylesheet import
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 20:51:03 +00:00
hanzo-dev 9fd1bd7b65 remove brand-hardcode gate; brand is data, not a gate 2026-06-21 20:51:03 +00:00
hanzo-devandClaude Opus 4.8 782cff59e6 feat(brand): brand-hardcode guard + brand-neutral i18n (decomplect brand=data)
- scripts/check-no-hardcoded-brand.mjs: fails if a brand literal (Hanzo/Zoo/Lux/Shinkai) appears in user-facing i18n copy; brand must come from {{appName}}/brandConfig
- net-i18n: 62 brand literals -> {{appName}}; tidy 2 stray 'shinkai' comments

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 20:51:03 +00:00
hanzo-devandClaude Opus 4.8 11c79d969d build(@hanzo/ai): restore @tailwindcss/vite + globals.css in SDK lib build and fix i18n en-US resource load so consumer apps render styled + translated
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 20:51:03 +00:00
hanzo-devandClaude Opus 4.8 f6c03b9f63 fix(ai): resolve brand at render time in onboarding so welcome stops crashing blank on Zoo/Lux
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 20:51:03 +00:00
hanzo-devandClaude Opus 4.8 75f734cba3 fix(ai): onboarding welcome uses each brand's own logo (no hardcoded Hanzo H / tagline)
Read brandConfig.logo inside OnboardingLayout (was a module-level useBrand()
frozen on the default brand, forcing visor.svg's Hanzo "H" onto Zoo/Lux).
Drop the broken right-column hero preview; tagline is now opt-in via
brandConfig.tagline (empty default — removes the old hardcoded "the way, the
truth, the life"). Welcome title uses {{appName}} so no brand name leaks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 20:51:02 +00:00
hanzo-dev f22c35d920 fix(build): unbreak docker build:app (tsconfig refs, recipes deep-subpath stubs, react-navigation skew, gui->hanzogui imports)
Four root causes blocked the one/vxrn (rolldown) build of apps/gui.hanzo.ai:

1. tsconfig project references: single-level pkgs/*/tsconfig.json used ../../
   (depth-2) refs that resolved to /work/core/* and /work/ui/gui instead of
   pkgs/core/* and pkgs/ui/hanzogui. Fixed the depth (../) and the stale
   gui->hanzogui rename across 9 tsconfigs.

2. @hanzogui/recipes deep subpaths (component/*) were UNLOADABLE: the no-recipes
   path still pulls the showcase tree, which imports @hanzogui/recipes/component/*
   via named re-export / named import / namespace member access. The old
   virtual-module stub could not satisfy statically-validated named exports.
   generate-recipes-proxy.mjs now scans the app and emits concrete per-subpath
   stub modules with the exact named exports (hooks return a proxy object so
   destructuring is safe); vite.config aliases each deep subpath (longest-first)
   to its stub ahead of the catch-all @hanzogui/recipes alias.

3. @react-navigation version skew: expo-router pulled bottom-tabs@7.16.2 which
   imports NavigationProvider (only in native >=7.2.5), conflicting with one's
   ~7.1.33 peer. Pinned bottom-tabs to 7.10.1 and bumped native 7.1.9->7.1.34
   (latest 7.1.x), keeping the @react-navigation family internally consistent.

4. Stale gui->hanzogui imports: SelectDemo.tsx and TestSelect.tsx imported from
   gui/linear-gradient instead of hanzogui/linear-gradient.

Validated locally: bun run build:app completes (build complete, full dist emitted).
2026-06-20 08:09:39 +00:00
hanzo-dev 357f9f9690 fix(build): static-extract components must be hanzogui not gui
The config-bundler did require("gui") for each entry in
gui.build.ts components, but no workspace package is named gui
(the components package is hanzogui, pkgs/ui/hanzogui). This
threw Cannot find module \x27gui\x27 -> Must provide components in
the gui-extract step of build:prod (static extract), so gui
never published an image. Rename the stale tamagui->gui leftover
to hanzogui in components and the matching optimizeDeps include.
2026-06-20 06:40:26 +00:00
hanzo-dev cd9cc63361 fix(build): pin vxrn to version exposing ./worker (unbreak build:app)
The gui.hanzo.ai site (the only app shipped in the GHCR image) runs on
one@1.12.5, whose vxrn@1.12.5 self-imports vxrn/worker. kitchen-sink and
sandbox pull one@1.15.10 -> vxrn@1.15.10, which bun hoists to the root
node_modules/vxrn. vxrn@1.15.10 dropped the ./worker export, so the
hoisted copy breaks vxrn@1.12.5s self-referential import:
  Package subpath ./worker is not defined by exports in vxrn/package.json

Pin the whole one/vxrn family to 1.12.5 via root overrides so a single
vxrn (with ./worker) exists in the tree. One version, no hoist collision.
2026-06-20 02:56:44 +00:00
b888ef8e2a ci: containerize + arcd GHCR semver build (DOKS deploy) (#3)
Add .github/workflows/build.yml that builds+pushes ghcr.io/hanzoai/gui on
v* tags via the canonical hanzoai/.github docker-build reusable workflow
on the self-hosted arcd fleet (amd64+arm64, no GitHub-hosted runners).
Wires the existing working Dockerfile (gui.hanzo.ai, bun build, serves on
:3000) to GHCR. Semver-only tags. The npm publish-gui*.yml workflows are
unaffected.

Co-authored-by: hanzo-dev <z@hanzo.ai>
2026-06-19 16:22:49 -07:00
Antje WorringandClaude Opus 4.8 ea427bc66f docs: tidy LLM.md indexes; CLAUDE.md -> LLM.md symlink convention
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 10:04:45 -07:00
zeekay 926589c982 ci: route to canonical native arcd labels [self-hosted, linux, <arch>]
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.
2026-06-10 20:15:30 -07:00
hanzo-devandClaude Opus 4.8 40921f9a3d ci: targeted gui-surface publish (skip whole-workspace fan-out)
release.ts --force-publish-all crashed packing unrelated @hanzo_network/*
packages. Replace with publish-gui-surface.mjs which publishes only the
@hanzogui/* closure + hanzogui + @hanzo/gui alias, rewriting workspace:*
to concrete versions and publishing the npm pack tarball directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:20:10 +00:00
hanzo-devandClaude Opus 4.8 15f130db5f ci: add publish-gui-all workflow to republish all gui packages with dist
Fixes broken 7.0.0 publish where @hanzogui/* primitives shipped without
dist/, breaking standalone @hanzo/gui installs. Builds then runs
release.ts --republish --force-publish-all using the same KMS->secret
NPM_TOKEN resolution as publish-gui.yml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:09:27 +00:00
hanzo-devandClaude Opus 4.8 07ae7ee84a ci(publish-gui): run on spark runner (evo offline) to unblock publish
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:34:46 +00:00
hanzo-devandClaude Opus 4.8 3109fc14c2 perf(@hanzo/ai): optimize SDK build — skip minify/compressed-size in lib mode
CI OOM'd bundling the app-as-SDK. Skip minify (consumers minify in their own
build), skip compressed-size reporting, target esnext, and raise heap to 12GB.
Build ~89s, no OOM. (Lib output is unminified by design; downstream apps minify.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:04:31 +00:00
Antje WorringandClaude Opus 4.8 1ae9e1579b ci: target native arcd runners (evo/spark)
Replace retired self-hosted labels with native host arcd daemons:
evo (linux/amd64), spark (linux/arm64).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:55:49 -07:00
hanzo-devandClaude Opus 4.8 1f3d8df185 build(@hanzo/ai): raise Node heap to 8GB for the SDK vite build (CI OOM)
CI runner OOM'd bundling @hanzo/ai (exit 134). Add --max-old-space-size=8192.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:40:16 +00:00
hanzo-devandClaude Opus 4.8 e213be30bc release(@hanzo/ai): 0.1.1 — brand-agnostic SDK + workspace build fixes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:58:25 +00:00
hanzo-devandClaude Opus 4.8 7c1765359c build: declare missing external deps across workspace packages
The monorepo relied on hoisting and never declared many externals; a clean CI
install (bun, no hoisting guarantees) failed to resolve them when bundling
@hanzo/ai. Declare them on the owning packages (net-ui +49, net-artifacts +33,
net-state, net-i18n, net-message-ts, …). @hanzogui/* workspace deps untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:36:46 +00:00
hanzo-devandClaude Opus 4.8 7087215033 build(@hanzo/ai): declare missing runtime deps (immer, zod, ethers, …)
@hanzo/ai imported ~25 externals (immer, zod, ethers, framer-motion, axios,
react-hook-form, sonner, pyodide, …) not in its package.json — a clean install
(CI) failed to resolve them (local hoisting masked it). Declare them; move
test-only deps (vitest, testing-library, react-scan) to devDependencies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:25:43 +00:00
hanzo-devandClaude Opus 4.8 416d87c08f build: fix workspace build (green 163/163)
- net-i18n: useChain/@hanzo_network/chain-config -> useBrand/brand-config
  (chain-config was renamed to brand-config; this import was never updated).
- loader: drop now-unused @ts-expect-error (TS2578) in GuiPlugin.ts.
Pre-existing breakage that gated the @hanzo/ai publish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:18:09 +00:00
hanzo-dev d164901c6f rename: claude.md → CLAUDE.md
Canonical casing per durable rule. Filesystem is case-insensitive
on macOS but git tracks the lowercase variant; uppercase is the
project convention.
2026-06-07 13:38:06 -07:00
hanzo-devandClaude Opus 4.8 ab3fe3d70c build: add missing per-package tsconfig.json (unblocks workspace build/publish)
10 leaf TS packages (recipes, net-brand-config, examples, *-test) lacked a
committed tsconfig.json, so the workspace build died with ENOENT on the first
one (pkgs/recipes). Add them on the standard pattern (extends root + composite).
Pre-existing breakage, gates the @hanzo/ai publish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:27:45 +00:00
hanzo-devandClaude Opus 4.8 2d663eba93 refactor(brand): brand-agnostic SDK — no hardcoded brands, apps own config
- net-brand-config bundles ZERO brands: registry (registerBrands) + injection
  (setBrand/useBrand/getBrand); add company + machinesEnabled to BrandConfig.
- Brand type: drop 'hanzo'|'zoo'|'lux' union -> string (open/extensible).
- Delete hardcoded BRAND/CHAIN consts -> proxy the active brand (16 call sites
  unchanged, now read the live brand).
- isMachinesEnabled: config field, not a hardcoded brand check.
- Examples each ship their own brand.config.ts (apps do the same downstream).
- Neutral test (fixture brands, no bundled assumptions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:45:16 +00:00
hanzo-devandClaude Opus 4.8 6f179ac4f2 refactor(iam): brand-neutral login hook (config-driven, PKCE, per-brand)
Replace hardcoded-hanzo useHanzoLogin (hanzo.id, placeholder clientId, shipped
client secret, wrong oauth-deep-link event) with brand-neutral useIamLogin that
reads the active brand's iam config (baseUrl/clientId/redirectUri/callbackEvent)
from @hanzo_network/brand-config and uses public-client PKCE. Adding a brand is
now a config entry; nothing is hardcoded to one brand.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:46:40 +00:00
hanzo-devandClaude Opus 4.8 bce9d6f602 fix(iam): real OAuth client IDs (hanzo-app/lux-app/zoo-app) for desktop login
Per-brand iam used placeholder clientIds not registered in Hanzo IAM. Domains
(hanzo.id/lux.id/zoolabs.id), redirect URIs, and callbackEvents were already
correct. Pairs with desktop redirects added to IAM init_data.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:28:36 +00:00
hanzo-devandClaude Opus 4.8 8b0e6b0d59 feat(@hanzo/ai): desktop build variant — converged frontend with full native
Add `@hanzo/ai/desktop` (vite.desktop.config.ts → dist-desktop): same converged
app as the web build, but it EXTERNALIZES @tauri-apps (14 modules) instead of
aliasing them to web shims — so a desktop consumer gets the REAL native APIs
(window/fs/dialog/updater/node-sidecar), full parity with the standalone forks.

This is the reusable foundation for converging the desktops locally: a desktop
shim is `import HanzoAI, { getBrand } from '@hanzo/ai/desktop'; <HanzoAI
{...getBrand()}/>` (VITE_BRAND selects hanzo/zoo/lux), loaded by the repo's Tauri
shell (which provides @tauri-apps + the node-manager handlers). hanzo/zoo have
full shells; lux's bespoke shell needs those handlers added.

build now emits both dist (web) + dist-desktop; exports['./desktop'] added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:38:48 +00:00
hanzo-devandClaude Opus 4.8 36fc4a2fd4 fix(@hanzo/ai web): per-brand document title + favicon (was hardcoded Hanzo)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:15:39 +00:00
hanzo-devandClaude Opus 4.8 41901783a8 build(@hanzo/ai): make publishable — drop workspace deps, public types, access
Prep for `npm publish @hanzo/ai` (the @hanzo scope):
- Drop the `@hanzo_network/* : workspace:*` runtime deps — they're bundled into
  the self-contained dist (aliased to src at build), and workspace:* can't be
  installed by consumers. Real 3rd-party deps stay.
- `publishConfig.access = public` (scoped package).
- Ship a hand-authored, self-contained public-API `dist/index.d.ts`
  (types/public.d.ts; the build copies it). Raw tsc would emit a broken tree of
  internal/@hanzo_network imports for a bundled lib.

Verified: `npm pack --dry-run` → valid tarball with dist + index.d.ts.
Note: 14 MB (bundles streamdown→shiki's ~200 grammars); a follow-up can
externalize shiki to slim it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 06:45:23 +00:00
hanzo-devandClaude Opus 4.8 c9ecfb0df5 ci(@hanzo/ai): run the SDK tests in CI; fix libsodium path portability
- Add `test:web` to @hanzo/ai so the existing Checks `unit-tests` job
  (`turbo run test:web`) includes the 63 SDK unit/regression tests
  (verified: `turbo run test:web --filter=@hanzo/ai` → 63 passed).
- Replace the hardcoded /home/z libsodium alias in web/vite.config.ts with a
  __dirname-relative resolve so the web build works off this machine (CI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 06:37:52 +00:00
hanzo-devandClaude Opus 4.8 1485697c4d fix(@hanzo/ai): clean the web console — host shims, CORS, auth timing, motion
Every console error/warning from the running web app, fixed and guarded by the
e2e (which now asserts the console stays clean):

- react-query "Query data cannot be undefined": web host `invoke` returns null
  (not undefined) for no-op native calls.
- "isPermissionGranted is not a function": complete the notification host shim
  (isPermissionGranted/requestPermission/sendNotification via the browser API).
- CORS on direct :36900/v1/engine/models: route the hanzo-engine client through
  a same-origin `/engine-api` vite proxy (set VITE_ENGINE_BASE_URL=/engine-api).
- "ws://localhost:1501 failed": the app derives ws port = httpPort+1; add
  local-runtime/ws-proxy.js (:1501 → node WS :3701).
- 401 /v1/node/available_models: gate useGetLLMProviders on auth — it mounted
  during onboarding and fired with an empty Bearer.
- framer-motion `motion()` deprecation → `motion.create()` (2 sites).

Tests: unit 63/63, e2e 2/2 (now also asserts no CORS / "is not a function" /
react-query-undefined / available_models / :1501 in the console). Remaining: a
rare intermittent static-asset 404 + a node-side embedding warning — neither
affects chat (documented in CHAT.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 06:33:23 +00:00
hanzo-devandClaude Opus 4.8 8bb89b0163 test(@hanzo/ai): vitest unit/regression + Playwright e2e + TDD setup
Establish the test harness and lock in every bug found this session as a
regression test (TDD going forward — see TESTING.md):

- vitest.config.ts: jsdom + the web build's alias graph (@/, @tauri-apps→host
  shims, @hanzo_network→src), streamdown stub, @testing-library/jest-dom setup.
  `bun run --cwd pkgs/ai test` → 61 tests green (4 new + the 7 pre-existing).
- src/__tests__/ regressions:
  • brand-store — useBrand/getBrand are plain getters callable outside render
    (the invalid-hook-call that blocked the web app)
  • host-shims — getCurrentWindow().emit etc. exist; host adapter web↔tauri
  • resizable — react-resizable-panels v3 API present + net-ui renders
  • no-missing-imports — critical screens import every <Component> they use
    (the dropped Box/Boxes/Coins lucide imports)
- e2e/chat.e2e.test.ts (vitest.e2e.config.ts): Playwright drives the real
  browser — onboarding smoke + connect→register→send→assert zen-coder reply.
  Skips cleanly when the local runtime isn't up. `test:e2e` → 2 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 05:38:57 +00:00
hanzo-devandClaude Opus 4.8 9b6fb6b759 feat(@hanzo/ai): chat works end-to-end in the browser (verified local)
The web app now chats with zen-coder-24b through a real hanzo-node, fully in the
browser (Playwright e2e: "Reply with exactly: E2E CHAT OK" → zen-coder generated
exactly that). Fixes uncovered by driving the real UI:

- main-layout.tsx: restore missing lucide imports Box/Boxes/Coins (the chat
  screens crashed with "Box is not defined").
- react-resizable-panels: the merge pulled v4 (Group/Separator), but the app
  uses the v3 API (PanelGroup/PanelResizeHandle) — pin ^3.0.2 in net-ui so the
  chat conversation view renders.
- web/vite.config.ts: proxy /v1 (the node's API is /v1/node/*, 135 endpoints) in
  addition to /v2 + /ws, so the browser reaches the node same-origin (no CORS).

local-runtime/ holds the reproducible inference wiring (see CHAT.md):
- responses-proxy.py: the node's OpenAI provider POSTs /v1/engine/responses, but
  hanzo-engine serves the Responses API at /v1/responses — rewrite the path
  (+ enable_thinking:false). This was the "AI Provider API Error" root cause.
- run-node.sh: a standalone /v1 node wired to the engine + embeddings. Uses
  127.0.0.1 everywhere (localhost → IPv6 ::1 → instant connection failure).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 05:03:49 +00:00
hanzo-devandClaude Opus 4.8 4d18c6d07b Merge origin/main into @hanzo/ai SDK — land the AI app on main
Reconciles the 140 upstream commits with the @hanzo/ai SDK (pkgs/ai +
pkgs/net-*, purely additive). Only bun.lock conflicted; regenerated fresh from
the merged package.json. The markdown dep skew that this exposed is fixed by
the prior commit (shiki@^3 override + dev streamdown stub).

Verified post-merge: fresh `bun install` exit 0, turbo build passes, web prod
build (rollup, 13MB) renders the Hanzo onboarding screen, dev server renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 00:49:52 +00:00
hanzo-devandClaude Opus 4.8 ae9c3a8e5b fix(@hanzo/ai): harden markdown deps — clean install now renders
The committed lock had a latent skew that only surfaced on a clean `bun install`
(earlier session renders used an ephemeral node_modules):

- shiki: `@vxrn/mdx` pins shiki 1.3.0 *exact*; bun hoisted it over
  `@streamdown/code`'s `shiki@^3` (no `engine/javascript` export → build fails).
  Fix: `overrides.shiki = ^3.19.0`. Deterministic; @vxrn/mdx is docs-only, not
  in the app graph. This alone makes the PROD and LIB builds (rollup) clean —
  rollup resolves the rest of the nested micromark stack correctly.

- micromark: a v1↔v4 generation skew (16 v1 consumers from the remark/MDX docs
  stack vs 1 v2 consumer = streamdown). The two APIs (util@1 subpath exports vs
  util@2 root) must coexist nested, and rollup handles it — but esbuild's DEV
  pre-bundle flattens the bare `micromark-util-symbol` to the hoisted @1
  ("Missing '.' specifier"). Fix: a DEV-ONLY (command==='serve') stub of the
  streamdown stack (streamdown-stub.tsx); the prod build uses the real packages
  so chat markdown/code/math/mermaid are unaffected in production.

Verified clean: web dev server renders the onboarding screen, web prod build
(rollup, 13MB) renders, self-contained lib dist builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 00:46:14 +00:00
hanzo-devandClaude Opus 4.8 b512973819 feat(@hanzo/ai): web→node proxy + chat wiring recipe
Add an env-configurable same-origin dev proxy (/v2 + /ws → VITE_NODE_API/
VITE_NODE_WS) so the browser reaches a hanzo-node without CORS: the app's
nodeAddress becomes its own origin and vite forwards to the node.

CHAT.md documents the verified state: the zen engine answers on :36900
(zen-coder-24b), and a healthy node (:2000, v1.1.20) already has the zen_engine
provider wired to it. Remaining blocker is API-version alignment — the app
client speaks /v1/node/*, the healthy node serves /v2/* only, and the
/v1-compatible desktop node (:3690) is currently hung. Recipe for both paths
included.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 00:11:54 +00:00
hanzo-devandClaude Opus 4.8 8c454dd150 docs(@hanzo/ai): CONVERGENCE.md — one surface, web⇄desktop parity, verification matrix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 23:54:57 +00:00
hanzo-devandClaude Opus 4.8 b559401d63 feat(@hanzo/ai): self-contained dist + brand re-export → thin shims work
Make @hanzo/ai consumable as a published APP-as-SDK so each app collapses to
a ~10-line shim (the luxfi/exchange model, for AI):

- Lib build now bundles EVERYTHING (the app, net-* libs, @tauri-apps host
  shims, all 3rd-party deps) and externalizes ONLY react/react-dom as peers.
  Previously every dep was external, so a consumer installing just @hanzo/ai
  was missing libsodium/framer-motion/radix/etc. (pinned libsodium's CJS build
  in the lib config too, now that it's bundled rather than external).
- Re-export getBrand/getBrandFromHostname from the package, so a shim needs no
  separate brand package for the default case:
    import HanzoAI, { getBrand } from '@hanzo/ai'
- Shims (hanzo/zoo/lux/web) aligned to the proven pattern: brand is a build
  axis (VITE_BRAND), platform an injected prop (host={tauriHost}), app is
  @hanzo/ai. Added a runnable web shim (index.html + vite.config) that imports
  @hanzo/ai from its built dist providing only react.

Verified: the external web shim (only react/react-dom provided, everything
else from dist) renders the full Hanzo onboarding screen on :1503.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 23:54:10 +00:00
hanzo-devandClaude Opus 4.8 699e8217a8 fix(@hanzo/ai): web app renders — break the dist self-import cycle
The web app threw `Cannot read properties of null (reading 'useContext')`
(invalid hook call) and never painted. Root cause was NOT React duplication
from a dep: 7 migrated app modules imported `useBrand` from the `@hanzo/ai`
PACKAGE name, which resolves to the built `dist/index.js`. That pulled the
whole minified library (a second React + a second copy of the app) back into
the source graph — a module cycle with a dual React dispatcher.

Fix: the injectable brand store lives in `@hanzo_network/brand-config`
(`setBrand`/`useBrand`, plain module getters — not hooks — so the app's
non-component call sites are safe), aliased to src so there is ONE copy and
no cycle. The 7 app files now import `useBrand` from there; `@hanzo/ai`'s
`brand-context` just re-exports it. Rule: app code never imports the
`@hanzo/ai` package by name.

Also completed `src/host/window.ts` to the full Tauri Window surface
(`emit`/`listen`/`once`/`onCloseRequested`/geometry) — the missing `emit`
was the next crash after the cycle was broken. Production web entry cleaned
up with a real ErrorBoundary.

Verified: dev server on :1500 paints the real Hanzo onboarding screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 23:46:13 +00:00
hanzo-devandClaude Opus 4.8 1d818ebb88 feat(@hanzo/ai): decomplect the AI app into one declarative SDK
The entire shinkai-fork AI app (280 files) + its 7 @hanzo_network/* libs,
lifted into @hanzo/ai as <HanzoAI {...brand} host={}/> — brand is a prop,
platform is an injected HostAdapter (web / tauri / expo). One unified surface
for hanzo/zoo/lux desktop + mobile + web (hanzo.chat). Library build green
(2.8MB) and production web build green (14MB). Shims in examples/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:31:02 +00:00
1848 changed files with 1645028 additions and 2161 deletions
+25
View File
@@ -0,0 +1,25 @@
name: Build image
# Builds + pushes ghcr.io/hanzoai/gui (the gui.hanzo.ai site, served on
# :3000 by the repo's existing Dockerfile) on a v* semver tag via the
# canonical reusable workflow, natively on the self-hosted arcd fleet
# (NO GitHub-hosted runners). Semver-only tags. Deployable on DOKS.
#
# This is the CONTAINER-image path. The repo's publish-gui*.yml workflows
# publish the npm library packages and are unaffected.
on:
push:
tags: ['v*']
workflow_dispatch:
permissions:
contents: read
packages: write
id-token: write
jobs:
docker:
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
with:
image: ghcr.io/hanzoai/gui
secrets: inherit
+1 -1
View File
@@ -8,7 +8,7 @@ on:
jobs:
tagged-release:
name: Changelog
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
# needs: release
steps:
+4 -4
View File
@@ -19,7 +19,7 @@ concurrency:
jobs:
checks:
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
steps:
- name: Checkout
uses: actions/checkout@v5
@@ -37,7 +37,7 @@ jobs:
run: bun run lint
unit-tests:
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
env:
NODE_OPTIONS: '--max-old-space-size=6144'
steps:
@@ -66,7 +66,7 @@ jobs:
run: bun turbo run test:web --filter='!@hanzogui/kitchen-sink' --concurrency=1
changes:
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
outputs:
integration-relevant: ${{ steps.filter.outputs.integration-relevant }}
steps:
@@ -90,7 +90,7 @@ jobs:
integration-tests:
needs: changes
if: needs.changes.outputs.integration-relevant == 'true'
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
strategy:
fail-fast: false
matrix:
+108
View File
@@ -0,0 +1,108 @@
name: Publish gui (all packages)
# Publishes the FULL gui surface — every @hanzogui/* primitive AND the
# `hanzogui` + `@hanzo/gui` umbrellas — at the current on-disk version
# (7.x), with freshly built `dist/`. This is the fix for the broken
# 7.0.0 publish, where the primitives shipped without `dist/` and
# `@hanzo/gui` therefore failed to resolve for standalone consumers.
#
# `publish-gui.yml` only ships the umbrella; this ships the primitives it
# depends on so a clean `npm i @hanzo/gui` works end-to-end.
#
# Trigger: manual (workflow_dispatch, type "publish" to confirm) or by
# pushing a `release/gui-all-v*` tag.
#
# NPM_TOKEN resolution mirrors publish-gui.yml:
# 1. KMS via Universal Auth (KMS_CLIENT_ID/SECRET → short-lived token)
# 2. KMS via long-lived HANZO_API_KEY (legacy bootstrap)
# 3. Direct repo secret NPM_TOKEN (fallback)
on:
workflow_dispatch:
inputs:
confirm:
description: 'Type "publish" to publish ALL gui packages at the current version'
required: true
default: ''
push:
tags:
- 'release/gui-all-v*'
permissions:
contents: read
id-token: write
jobs:
publish-all:
name: Build + publish all gui packages
runs-on: [self-hosted, linux, arm64]
if: github.event_name == 'push' || github.event.inputs.confirm == 'publish'
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install
uses: ./.github/actions/install
- name: Build all packages
# Fresh checkout has no dist; --republish skips release.ts's own
# build, so build here explicitly. `npm pack` (inside release.ts)
# will fail loudly if any dist tree is missing.
run: bun run build
- name: Resolve NPM_TOKEN (KMS → fallback to repo secret)
id: token
env:
DIRECT_NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
HANZO_API_KEY: ${{ secrets.HANZO_API_KEY }}
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
KMS_WORKSPACE_ID: ${{ vars.KMS_WORKSPACE_ID_GUI || 'e1359bf4-31b4-4dfa-bb90-323e2c298ad8' }}
run: |
set -eu
npm_token=""
if [ -n "${KMS_CLIENT_ID:-}" ] && [ -n "${KMS_CLIENT_SECRET:-}" ]; then
HANZO_API_KEY=$(curl -sf "${KMS_ENDPOINT}/api/v1/auth/universal-auth/login" \
-H "Content-Type: application/json" \
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
| jq -r '.accessToken') || true
fi
if [ -n "${HANZO_API_KEY:-}" ]; then
response=$(curl -sf \
"${KMS_ENDPOINT}/api/v3/secrets/raw?workspaceId=${KMS_WORKSPACE_ID}&secretPath=/publish&environment=prod" \
-H "Authorization: Bearer ${HANZO_API_KEY}" 2>/dev/null) || true
if [ -n "${response:-}" ]; then
npm_token=$(echo "$response" | jq -r '.secrets[] | select(.secretKey=="NPM_TOKEN") | .secretValue // empty')
fi
fi
if [ -z "$npm_token" ]; then
npm_token="${DIRECT_NPM_TOKEN:-}"
if [ -n "$npm_token" ]; then
echo "Using NPM_TOKEN from repo secret (KMS path unavailable)."
fi
fi
if [ -z "$npm_token" ]; then
echo "::error::No NPM_TOKEN available — need KMS_CLIENT_ID/SECRET, HANZO_API_KEY, or NPM_TOKEN repo secret."
exit 1
fi
echo "::add-mask::${npm_token}"
echo "npm_token=${npm_token}" >> "$GITHUB_OUTPUT"
- name: Configure npm registry
env:
NPM_TOKEN: ${{ steps.token.outputs.npm_token }}
run: |
echo 'registry=https://registry.npmjs.org/' > ~/.npmrc
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrc
- name: Publish gui surface (primitives + umbrella + @hanzo/gui alias)
# Targeted publish of just the @hanzogui/* closure + hanzogui +
# @hanzo/gui, at the current on-disk version. Avoids release.ts's
# whole-workspace fan-out (which fails packing unrelated
# @hanzo_network/* packages) and its fragile tarball-rename step.
# Idempotent — skips any name@version already on npm.
env:
NPM_TOKEN: ${{ steps.token.outputs.npm_token }}
NODE_AUTH_TOKEN: ${{ steps.token.outputs.npm_token }}
run: node ./scripts/publish-gui-surface.mjs
+1 -1
View File
@@ -28,7 +28,7 @@ permissions:
jobs:
publish:
name: Build, smoke-test, publish
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, arm64]
steps:
- uses: actions/checkout@v5
with:
+2 -2
View File
@@ -10,7 +10,7 @@ on:
jobs:
release:
name: Release
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
steps:
- name: Checkout Repo
uses: actions/checkout@v5
@@ -36,7 +36,7 @@ jobs:
tagged-release:
name: Tagged Release
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
# needs: release
steps:
+2 -2
View File
@@ -179,7 +179,7 @@ jobs:
name: Build Android App
# Only run on main/v* branches (not PRs)
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/v') || github.ref == 'refs/heads/rn82'
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
timeout-minutes: 45
outputs:
android-cache-key: ${{ steps.final-cache-key.outputs.cache_key }}
@@ -446,7 +446,7 @@ jobs:
needs: build-android
# Only run on main/v* branches (not PRs)
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/v') || github.ref == 'refs/heads/rn82'
runs-on: hanzo-build-linux-amd64
runs-on: [self-hosted, linux, amd64]
timeout-minutes: 45
# Android emulator in CI is flaky with window focus issues - don't block on failures
continue-on-error: true
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
-1
View File
@@ -1 +0,0 @@
CLAUDE.md
+136
View File
@@ -0,0 +1,136 @@
Please read ./CONTRIBUTING.md as well
Note you need to re-build packages (`bun run build` in the package directory) as you change them, unless you or someone is running a `bun run watch` at root.
FOR LONG RUNNNING DEBUGGING run `bun run watch` in the background its faster and rebuilds all packages.
keep commits to one line, add a trailing "Fixes #" if associated with a GH issue, and start with a convential commit style - UNLESS its a change that shouldn't go into the changelog, in those cases you can do things like `docs: ` or `site: `.
# Hanzo GUI Testing Guide
## Running Tests
### Kitchen Sink Tests
The kitchen-sink package contains the main integration tests for Hanzo GUI components. To run these tests:
1. **Start the web server** (in the background):
```bash
cd apps/kitchen-sink
bun run start:web
```
To open a specific test case in the browser:
```bash
open "http://localhost:9000/?test=YourTestCaseName"
```
Test case names match the file names in `apps/kitchen-sink/src/usecases/` (e.g., `SelectFocusScopeCase`).
To open a component demo:
```bash
open "http://localhost:9000/?demo=Select"
```
Demo names match files in `apps/demos/src/` without the `Demo` suffix (e.g., `Select` for `SelectDemo.tsx`).
2. **Run all web tests** with different animation drivers:
```bash
bun run test:web
```
This uses `run-tests-parallel.ts` which first runs `default` + `webkit` projects sequentially, then runs all four animated driver projects (`css`, `native`, `reanimated`, `motion`) in parallel against a single shared dev server.
3. **Run tests with a specific animation driver**:
```bash
# Using env var + playwright --project flag
cd apps/kitchen-sink
NODE_ENV=test HANZO_GUI_TEST_ANIMATION_DRIVER=css npx playwright test --project=animated-css
# Available projects: animated-css, animated-native, animated-reanimated, animated-motion
```
4. **Run a specific test file**:
```bash
# Using playwright directly
cd apps/kitchen-sink
npx playwright test tests/PopoverFocusScope.test.tsx
# Or with a specific driver
NODE_ENV=test HANZO_GUI_TEST_ANIMATION_DRIVER=css npx playwright test tests/YourTest.animated.test.tsx --project=animated-css
```
5. **Debug tests**:
```bash
bun run test:web:debug
# or
npx playwright test --debug
```
### Test Structure
Tests are located in `apps/kitchen-sink/tests/` and follow these naming conventions:
- `ComponentName.test.tsx` - Standard tests that run ONCE with the default animation driver
- `ComponentName.animated.test.tsx` - Animation-dependent tests that run with ALL animation drivers (css, native, reanimated, motion)
This separation significantly speeds up the test suite since most tests don't need to run 4x across all animation drivers. Only use `.animated.test.tsx` for tests that specifically verify animation behavior across different drivers.
### Writing Tests
When writing tests for focus behavior or component interactions:
1. Use appropriate wait times for animations and focus changes
2. Be aware that `trapFocus` behavior depends on the component's open state
3. Test both trapped and non-trapped focus scenarios
4. Consider browser focus behavior when `trapFocus` is false
### Common Issues
- If tests fail due to timing, add appropriate `waitForTimeout` calls
- For focus tests, ensure elements are visible before testing focus state
- When testing popover/dialog components, wait for animations to complete
## Commit Message Conventions
- Use `site:` prefix (not `fix(site):`) for gui.hanzo.ai changes since they don't go in the changelog
- Use `ci:` prefix (not `fix(ci):`) for CI/workflow changes since they don't go in the changelog
- Keep commit messages to a single line
## iOS Development
See [docs/using-ios.md](./docs/using-ios.md) for iOS native development and Detox testing tips.
## gui.hanzo.ai API Authentication
When making authenticated API calls from the client side in gui.hanzo.ai, always use the `authFetch` helper:
```ts
import { authFetch } from '~/features/api/authFetch'
const response = await authFetch('/api/some-endpoint', {
method: 'POST',
body: JSON.stringify({ ... }),
})
```
**Why this matters:** Cookies alone are not reliable for auth in production due to cross-origin/SameSite issues. The `authFetch` helper automatically includes the Authorization header with the user's access token. All payment/subscription endpoints require this.
---
## Additional notes (merged from LLM.md)
# gui — AI Assistant Context
# Hanzo GUI
<h3 align="center">
Style library, design system, composable components, and more.
</h3>
+1 -1
View File
@@ -3,7 +3,7 @@ import React from 'react'
import type { FontSizeTokens, SelectProps } from 'hanzogui'
import { Adapt, Label, Select, Sheet, Theme, XStack, YStack, getFontSize } from 'hanzogui'
import { LinearGradient } from 'gui/linear-gradient'
import { LinearGradient } from 'hanzogui/linear-gradient'
export function SelectDemo() {
return (
+1 -1
View File
@@ -1,7 +1,7 @@
import type { GuiBuildOptions } from 'hanzogui'
export default {
components: ['gui'],
components: ['hanzogui'],
logTimings: true,
config: '@hanzogui/dev-config',
outputCSS: './gui.generated.css',
@@ -1,10 +1,149 @@
import { mkdirSync } from 'node:fs'
import { mkdirSync, readdirSync, statSync } from 'node:fs'
import { existsSync, writeFileSync, readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const RECIPES_COMPONENT_PREFIX = '@hanzogui/recipes/component/'
/**
* Recursively collect .ts/.tsx source files under a directory, skipping
* node_modules and build output.
* @param {string} dir
* @param {string[]} acc
* @returns {string[]}
*/
function collectSourceFiles(dir, acc = []) {
let entries
try {
entries = readdirSync(dir)
} catch {
return acc
}
for (const name of entries) {
if (name === 'node_modules' || name === 'dist' || name === 'recipes-output') continue
const full = join(dir, name)
let s
try {
s = statSync(full)
} catch {
continue
}
if (s.isDirectory()) {
collectSourceFiles(full, acc)
} else if (name.endsWith('.ts') || name.endsWith('.tsx')) {
acc.push(full)
}
}
return acc
}
/**
* Scan the app for deep `@hanzogui/recipes/component/*` imports and the named
* bindings each subpath must provide, so concrete stub modules can be generated
* when the (optional, pro-only) recipes repo is absent.
*
* Three import shapes are detected:
* 1. namespace -> import * as NS from '<subpath>' (then NS.Member usages)
* 2. named -> import { A, B } from '<subpath>'
* 3. re-export -> export { A } from '<subpath>'
*
* @param {string} appDir
* @returns {Record<string, string[]>} subpath -> sorted unique export names
*/
function scanRecipesSubpaths(appDir) {
const subpaths = {}
for (const file of collectSourceFiles(appDir)) {
let src
try {
src = readFileSync(file, 'utf-8')
} catch {
continue
}
if (!src.includes(RECIPES_COMPONENT_PREFIX)) continue
// namespace imports + member accesses
for (const m of src.matchAll(/import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g)) {
const [, ns, sp] = m
if (!sp.startsWith(RECIPES_COMPONENT_PREFIX)) continue
subpaths[sp] ??= new Set()
const memberRe = new RegExp('\\b' + ns + '\\.([A-Za-z_][A-Za-z0-9_]*)', 'g')
for (const u of src.matchAll(memberRe)) subpaths[sp].add(u[1])
}
// named imports and re-exports
for (const m of src.matchAll(/(?:import|export)\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g)) {
const [, names, sp] = m
if (!sp.startsWith(RECIPES_COMPONENT_PREFIX)) continue
subpaths[sp] ??= new Set()
for (const raw of names.split(',')) {
const name = raw.trim().split(/\s+as\s+/)[0].trim()
if (name) subpaths[sp].add(name)
}
}
}
return Object.fromEntries(
Object.entries(subpaths).map(([k, v]) => [k, [...v].sort()])
)
}
/**
* Generate concrete stub modules for deep `@hanzogui/recipes/component/*`
* subpaths used by the showcase tree, returning a map from subpath -> absolute
* stub file path. Each stub exports a no-op component (carrying a `.fileName`
* field so `Member.fileName` reads don't throw) as the default and as every
* named binding the app imports — concrete on-disk named exports are required
* because rolldown statically validates `export { X } from` / `import { X }`
* and does not transform virtual `\0`-modules through commonjs interop.
*
* @param {Object} options
* @param {string} options.appDir - app root (dir containing components/, vite.config.ts)
* @param {string} options.outDir - directory to write stub modules into
* @returns {Record<string, string>} subpath -> absolute stub path
*/
export function generateSubpathStubs({ appDir, outDir }) {
const map = scanRecipesSubpaths(appDir)
mkdirSync(outDir, { recursive: true })
const result = {}
for (const [subpath, names] of Object.entries(map)) {
const flat = subpath.slice(RECIPES_COMPONENT_PREFIX.length).replace(/[^A-Za-z0-9]+/g, '_')
const stubPath = resolve(outDir, `${flat}.tsx`)
// Hook-shaped exports (useFoo) are called, not rendered, and their result is
// often destructured (e.g. `const { sm } = useGroupMedia(...)`), so they must
// return a (proxy) object rather than the null-rendering component stub.
const namedExports = names
.filter((n) => n !== 'default')
.map((n) =>
/^use[A-Z0-9]/.test(n)
? `export const ${n} = recipesSubpathHook`
: `export const ${n} = RecipesSubpathStub`
)
.join('\n')
writeFileSync(
stubPath,
`// AUTO-GENERATED stub for ${subpath}
// Recipes is optional (pro users only); the real components live in the
// separate recipes repo. Regenerate via scripts/generate-recipes-proxy.mjs.
function RecipesSubpathStub() {
return null
}
RecipesSubpathStub.fileName = ''
// Hook stub: returns an object that yields undefined for any destructured key,
// so callers like \`const { sm } = useGroupMedia(...)\` never throw.
const recipesSubpathHook = () =>
new Proxy({}, { get: () => undefined }) as any
export default RecipesSubpathStub
${namedExports}
`
)
result[subpath] = stubPath
}
return result
}
/**
* Generate recipes proxy files based on whether the recipes repo is available.
* Can be called from vite config or run directly as a script.
@@ -19,11 +158,23 @@ export function generateRecipesProxy(options = {}) {
const RECIPES_PATH = resolve(basePath, '../../../../recipes')
const HELPERS_DIST_PATH = resolve(basePath, '../helpers/dist')
const APP_DIR = resolve(basePath, '..')
const hasRecipes = existsSync(RECIPES_PATH)
// Ensure dist exists
mkdirSync(HELPERS_DIST_PATH, { recursive: true })
// When recipes is absent, generate concrete stub modules for every deep
// `@hanzogui/recipes/component/*` subpath the app imports, so the showcase
// tree (which is always in the build graph via RecipesComponentSection) can
// resolve its named imports/re-exports.
const subpathStubs = hasRecipes
? {}
: generateSubpathStubs({
appDir: APP_DIR,
outDir: resolve(HELPERS_DIST_PATH, 'recipes-subpath-stubs'),
})
const proxyPath = resolve(HELPERS_DIST_PATH, 'recipes-proxy.ts')
const existingContent = existsSync(proxyPath) ? readFileSync(proxyPath, 'utf-8') : ''
const isStub = existingContent.includes('Stub file for when recipes is not available')
@@ -31,7 +182,7 @@ export function generateRecipesProxy(options = {}) {
// Skip if already generated with correct state and has all exports
if (existingContent && isStub === !hasRecipes && hasCurrentRouteProvider) {
return { hasRecipes }
return { hasRecipes, subpathStubs }
}
if (!hasRecipes) {
@@ -87,7 +238,7 @@ export * as Sections from '../../components/recipes-showcase/sections'
}
}
return { hasRecipes }
return { hasRecipes, subpathStubs }
}
// Run if called directly (postinstall script)
+17 -50
View File
@@ -37,8 +37,10 @@ if (!existsSync(vitePluginDist) || !existsSync(staticDist)) {
}
}
// Generate recipes proxy files (creates stubs if recipes repo not found)
const { hasRecipes } = generateRecipesProxy({
// Generate recipes proxy files (creates stubs if recipes repo not found).
// When recipes is absent, subpathStubs maps each deep
// `@hanzogui/recipes/component/*` import to a concrete generated stub module.
const { hasRecipes, subpathStubs } = generateRecipesProxy({
basePath: pathResolve(import.meta.dirname, 'scripts'),
silent: false,
})
@@ -61,7 +63,7 @@ const include = [
'swr/mutation',
'mdx-bundler/client',
// core hanzo-gui packages must be pre-bundled together to avoid duplicate instances
'gui',
'hanzogui',
'@hanzogui/core',
'@hanzogui/web',
// existing
@@ -119,17 +121,19 @@ export default {
resolve: {
preserveSymlinks: false,
alias: [
// Regex-based alias for recipes components when not available
// When recipes is absent, map each deep @hanzogui/recipes/component/*
// import to its concrete generated stub. These MUST precede the catch-all
// '@hanzogui/recipes' alias below, otherwise that alias swallows the
// subpath (resolving to recipes-proxy/component/...) and the build fails
// with UNLOADABLE_DEPENDENCY. Stubs carry the exact named exports the
// showcase tree imports/re-exports, which rolldown validates statically.
...(!hasRecipes
? [
{
find: /^@gui\/recipes\/component\/.+$/,
replacement: pathResolve(
import.meta.dirname,
'./helpers/dist/recipes-component-stub.tsx'
),
},
]
? Object.entries(subpathStubs)
// longest subpath first: vite matches `id === find || id.startsWith(find + '/')`,
// so a parent like `.../user/preferences` must not shadow its child
// `.../user/preferences/LocationNotification`.
.sort(([a], [b]) => b.length - a.length)
.map(([find, replacement]) => ({ find, replacement }))
: []),
// Standard string-based aliases
{
@@ -204,43 +208,6 @@ export default {
},
plugins: [
// Plugin to stub recipes component imports when recipes repo is not available
!hasRecipes && {
name: 'stub-recipes-components',
enforce: 'pre', // Run before other plugins including alias resolution
resolveId(id: string) {
// Intercept imports from @hanzogui/recipes/component/*
if (id.startsWith('@hanzogui/recipes/component/')) {
// Return a virtual module ID
return '\0recipes-component-stub:' + id
}
},
load(id: string) {
// Handle the virtual module
if (id.startsWith('\0recipes-component-stub:')) {
// Return stub component code
return `
import { YStack, Paragraph } from 'hanzogui'
export default function RecipesComponentStub() {
if (process.env.NODE_ENV === 'production') {
return null
}
return (
<YStack p="$4" bc="$borderColor" br="$4">
<Paragraph size="$2" color="$color10">
Recipes component not available
</Paragraph>
</YStack>
)
}
// Export as default and named for compatibility
export const LocationNotification = RecipesComponentStub
`
}
},
},
guiPlugin({
// see gui.build.ts
disable: process.env.NODE_ENV !== 'production',
+1 -1
View File
@@ -3,7 +3,7 @@ import React from 'react'
import type { FontSizeTokens, SelectProps } from 'hanzogui'
import { Adapt, Label, Select, Sheet, XStack, YStack, getFontSize } from 'hanzogui'
import { LinearGradient } from 'gui/linear-gradient'
import { LinearGradient } from 'hanzogui/linear-gradient'
export function SelectDemo() {
return (
+2944 -1859
View File
File diff suppressed because it is too large Load Diff
-136
View File
@@ -1,136 +0,0 @@
Please read ./CONTRIBUTING.md as well
Note you need to re-build packages (`bun run build` in the package directory) as you change them, unless you or someone is running a `bun run watch` at root.
FOR LONG RUNNNING DEBUGGING run `bun run watch` in the background its faster and rebuilds all packages.
keep commits to one line, add a trailing "Fixes #" if associated with a GH issue, and start with a convential commit style - UNLESS its a change that shouldn't go into the changelog, in those cases you can do things like `docs: ` or `site: `.
# Hanzo GUI Testing Guide
## Running Tests
### Kitchen Sink Tests
The kitchen-sink package contains the main integration tests for Hanzo GUI components. To run these tests:
1. **Start the web server** (in the background):
```bash
cd apps/kitchen-sink
bun run start:web
```
To open a specific test case in the browser:
```bash
open "http://localhost:9000/?test=YourTestCaseName"
```
Test case names match the file names in `apps/kitchen-sink/src/usecases/` (e.g., `SelectFocusScopeCase`).
To open a component demo:
```bash
open "http://localhost:9000/?demo=Select"
```
Demo names match files in `apps/demos/src/` without the `Demo` suffix (e.g., `Select` for `SelectDemo.tsx`).
2. **Run all web tests** with different animation drivers:
```bash
bun run test:web
```
This uses `run-tests-parallel.ts` which first runs `default` + `webkit` projects sequentially, then runs all four animated driver projects (`css`, `native`, `reanimated`, `motion`) in parallel against a single shared dev server.
3. **Run tests with a specific animation driver**:
```bash
# Using env var + playwright --project flag
cd apps/kitchen-sink
NODE_ENV=test HANZO_GUI_TEST_ANIMATION_DRIVER=css npx playwright test --project=animated-css
# Available projects: animated-css, animated-native, animated-reanimated, animated-motion
```
4. **Run a specific test file**:
```bash
# Using playwright directly
cd apps/kitchen-sink
npx playwright test tests/PopoverFocusScope.test.tsx
# Or with a specific driver
NODE_ENV=test HANZO_GUI_TEST_ANIMATION_DRIVER=css npx playwright test tests/YourTest.animated.test.tsx --project=animated-css
```
5. **Debug tests**:
```bash
bun run test:web:debug
# or
npx playwright test --debug
```
### Test Structure
Tests are located in `apps/kitchen-sink/tests/` and follow these naming conventions:
- `ComponentName.test.tsx` - Standard tests that run ONCE with the default animation driver
- `ComponentName.animated.test.tsx` - Animation-dependent tests that run with ALL animation drivers (css, native, reanimated, motion)
This separation significantly speeds up the test suite since most tests don't need to run 4x across all animation drivers. Only use `.animated.test.tsx` for tests that specifically verify animation behavior across different drivers.
### Writing Tests
When writing tests for focus behavior or component interactions:
1. Use appropriate wait times for animations and focus changes
2. Be aware that `trapFocus` behavior depends on the component's open state
3. Test both trapped and non-trapped focus scenarios
4. Consider browser focus behavior when `trapFocus` is false
### Common Issues
- If tests fail due to timing, add appropriate `waitForTimeout` calls
- For focus tests, ensure elements are visible before testing focus state
- When testing popover/dialog components, wait for animations to complete
## Commit Message Conventions
- Use `site:` prefix (not `fix(site):`) for gui.hanzo.ai changes since they don't go in the changelog
- Use `ci:` prefix (not `fix(ci):`) for CI/workflow changes since they don't go in the changelog
- Keep commit messages to a single line
## iOS Development
See [docs/using-ios.md](./docs/using-ios.md) for iOS native development and Detox testing tips.
## gui.hanzo.ai API Authentication
When making authenticated API calls from the client side in gui.hanzo.ai, always use the `authFetch` helper:
```ts
import { authFetch } from '~/features/api/authFetch'
const response = await authFetch('/api/some-endpoint', {
method: 'POST',
body: JSON.stringify({ ... }),
})
```
**Why this matters:** Cookies alone are not reliable for auth in production due to cross-origin/SameSite issues. The `authFetch` helper automatically includes the Authorization header with the user's access token. All payment/subscription endpoints require this.
---
## Additional notes (merged from LLM.md)
# gui — AI Assistant Context
# Hanzo GUI
<h3 align="center">
Style library, design system, composable components, and more.
</h3>
+32 -3
View File
@@ -17,7 +17,14 @@
"./apps/kitchen-sink-shared",
"./apps/sandbox",
"./apps/tests/**/*",
"./templates/*"
"./templates/*",
"./pkgs/ai",
"./pkgs/net-ui",
"./pkgs/net-state",
"./pkgs/net-i18n",
"./pkgs/net-message-ts",
"./pkgs/net-brand-config",
"./pkgs/net-logo"
],
"scripts": {
"postinstall": "bun ./scripts/postinstall.ts",
@@ -113,6 +120,8 @@
"devDependencies": {
"@manypkg/cli": "^0.23.0",
"@playwright/test": "^1.49.1",
"@tailwindcss/typography": "^0.5.20",
"@tailwindcss/vite": "^4",
"@testing-library/dom": "^10.4.0",
"dotenv-cli": "^7.4.2",
"esbuild": "^0.27.2",
@@ -127,6 +136,7 @@
"p-map": "^7.0.3",
"prompts": "2.1.0",
"react-native-screens": "~4.23.0",
"tailwindcss": "^4",
"turbo": "^2.1.3",
"typescript": "~5.9.2",
"ultra-runner": "^3.10.5"
@@ -134,14 +144,33 @@
"overrides": {
"@react-navigation/core": "7.9.2",
"@react-navigation/elements": "2.8.1",
"@react-navigation/native": "7.1.9",
"@react-navigation/native": "7.1.34",
"@react-navigation/native-stack": "7.6.2",
"@react-navigation/routers": "7.4.0",
"@swc/core": "1.14.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.83.2",
"whatwg-url": "14.1.1"
"whatwg-url": "14.1.1",
"shiki": "^3.19.0",
"react-resizable-panels": "^3.0.2",
"one": "1.12.5",
"vxrn": "1.12.5",
"@vxrn/compiler": "1.12.5",
"@vxrn/resolve": "1.12.5",
"@vxrn/color-scheme": "1.12.5",
"@vxrn/mdx": "1.12.5",
"@vxrn/safe-area": "1.12.5",
"@vxrn/utils": "1.12.5",
"@vxrn/debug": "1.12.5",
"@vxrn/vendor": "1.12.5",
"@vxrn/query-string": "1.12.5",
"@vxrn/url-parse": "1.12.5",
"@vxrn/vite-flow": "1.12.5",
"@vxrn/vite-plugin-metro": "1.12.5",
"@vxrn/tslib-lite": "1.12.5",
"create-vxrn": "1.12.5",
"@react-navigation/bottom-tabs": "7.10.1"
},
"packageManager": "bun@1.3.9",
"manypkg": {
+49
View File
@@ -0,0 +1,49 @@
# @hanzo/ai — build / migration plan
Goal: the Hanzo AI app as **one declarative SDK component** (`<HanzoAI {...brand} />`),
so `hanzo/zoo/lux` desktop+mobile apps become ~80-line shims — exactly how
`zooai/exchange` / `luxfi/exchange` consume `@luxfi/exchange`.
```
@hanzo/ai (this pkg — the app) @luxfi/exchange (the analog)
▲ ▲
hanzoai/desktop · zooai/app · luxfi/app zooai/exchange · luxfi/exchange
<HanzoAI {...brand}/> <Exchange {...brand}/>
```
## Status
- [x] Props contract — `BrandConfig` lifted from `hanzoai/desktop/libs/brand-config``src/types.ts`.
- [x] `<HanzoAI>` component + `BrandProvider`/`useBrand` (prop-driven; replaces `getBrand()` env lookup).
- [ ] **Move the app in** (the big rock).
- [ ] **Decouple Tauri** (44 files).
- [ ] **Bundle + publish** `@hanzo/ai` and its libs.
- [ ] **Shims**: rewrite `hanzoai/desktop`, `zooai/app`, `luxfi/app` to the shim.
## The move (~955 source files)
From `hanzoai/desktop`:
| Source | Files | Destination |
|---|---|---|
| `apps/hanzo-desktop/src/**` | 263 | `pkgs/ai/src/app/**` (App.tsx → `./app/App`) |
| `libs/hanzo-node-state/src` | 460 | `pkgs/state``@hanzo_network/hanzo-node-state` |
| `libs/hanzo-ui/src` | 192 | `pkgs/hanzo-ui``@hanzo_network/hanzo-ui` |
| `libs/hanzo-message-ts/src` | 32 | `pkgs/message-ts``@hanzo_network/hanzo-message-ts` |
| `libs/hanzo-i18n/src` | 8 | `pkgs/hanzo-i18n``@hanzo_network/hanzo-i18n` |
Mechanical, but two rewrites are required during the move:
1. `getBrand()` / `import.meta.env.VITE_BRAND``useBrand()` (from `./brand-context`).
2. The **44** `@tauri-apps/*` import sites → the injected `host: HostAdapter` prop
(`host.invoke` / `host.listen`), so the SDK runs on plain web *and* Tauri *and* Expo.
On web with no `host`, those features degrade gracefully.
## Then the shim (per app) — the whole app:
```tsx
// luxfi/app — apps/web/src/main.tsx
import HanzoAI from '@hanzo/ai'
import brand from '@luxfi/brand'
import { tauriHost } from './host' // desktop adapter
createRoot(root).render(
<HanzoAI {...brand} host={tauriHost}
features={{ chat:true, wallet:true, mining:true }} />)
```
Mobile = `apps/mobile` (Expo) with the same `<HanzoAI {...brand} host={expoHost} />`.
Build via Dockerfile from the pinned `@hanzo/ai`, brand overlaid — like the exchanges.
+59
View File
@@ -0,0 +1,59 @@
# Live chat — VERIFIED working end-to-end (web, local) 2026-06-05
The web app at `:1500` chats with **zen-coder-24b** through a real **hanzo-node**,
fully in the browser. Verified: prompt "Reply with exactly: E2E CHAT OK" →
zen-coder generated `E2E CHAT OK` (7 tokens), streamed back to the chat UI.
The full path:
```
browser :1500 ─(vite proxy /v1,/v2,/ws)→ hanzo-node :3700 ─job→ zen_engine provider
→ responses-proxy :36906 (rewrites /v1/engine/responses → /v1/responses)
→ hanzo-engine :36902 (zen-coder-24b) → "E2E CHAT OK" → streamed back to the UI
```
## The non-obvious fixes that made it work
1. **The node speaks the OpenAI _Responses_ API, the engine serves it at a
different path.** The node's `OpenAI` provider POSTs to
`<url>/v1/engine/responses` (`{input:[...], max_output_tokens, ...}`), but
hanzo-engine serves the Responses API at **`/v1/responses`** (`/v1/engine/...`
→ 404 → the node reports "AI Provider API Error: Unknown error"). A tiny proxy
rewrites the path (and injects `enable_thinking:false`). See
`local-runtime/responses-proxy.py`.
2. **Use `127.0.0.1`, never `localhost`.** `localhost` resolves to IPv6 `::1`
here; the engine/embeddings bind IPv4 only → instant connection failure.
3. **CORS:** the vite dev server proxies the app's own origin (`/v1`,`/v2`,`/ws`)
to the node, so the app's `nodeAddress` is just `http://<host>:1500`.
4. **Two migration import bugs** had to be fixed for the chat screens to render:
`main-layout.tsx` was missing `Box`/`Boxes`/`Coins` lucide imports, and the
merge pulled `react-resizable-panels@4` (renamed exports) — pinned to `^3.0.2`
in `pkgs/net-ui` (the app uses `PanelGroup`/`PanelResizeHandle`).
## Reproduce it
```bash
# 1. engine + embeddings already running: :36902 (zen-coder), :11436 (embed)
# 2. responses-proxy (path rewrite):
python3 pkgs/ai/local-runtime/responses-proxy.py & # :36906 → :36902
# 3. WS proxy (the app derives ws://host:1501 behind the vite proxy):
node pkgs/ai/local-runtime/ws-proxy.js & # :1501 → node WS :3701
# 4. a /v1 hanzo-node wired to the proxy (fresh storage, no reg code):
bash pkgs/ai/local-runtime/run-node.sh & # API :3700, zen_engine → :36906
# 5. web app pointed at the node + engine (engine-api proxy avoids CORS):
cd pkgs/ai/web && VITE_NODE_API=http://127.0.0.1:3700 VITE_NODE_WS=ws://127.0.0.1:3701 \
VITE_ENGINE_BASE_URL=/engine-api VITE_ENGINE_API=http://127.0.0.1:36906 \
bun x vite --config vite.config.ts # :1500
# 6. open http://spark.local:1500 → agree → Quick Connect (node address = same
# origin http://localhost:1500) → /home → chat. Reply streams from zen-coder.
```
## Console — clean (the e2e asserts it)
The earlier web console errors are fixed and guarded by `e2e/chat.e2e.test.ts`:
react-query "data cannot be undefined" (web `invoke` returns null), the
`isPermissionGranted`/notification gap, the `:36900/v1/engine/models` CORS
(routed via the `/engine-api` proxy), the `ws://…:1501` failure (ws-proxy), the
`available_models` 401 (gated `useGetLLMProviders` on auth), and the framer
`motion()` deprecation. Remaining: a rare, intermittent static-asset `404` and a
node-side `Embedding "Query is not read-only"` warning — neither affects chat.
+80
View File
@@ -0,0 +1,80 @@
# @hanzo/ai — one app surface, every platform
The Hanzo / Zoo / Lux AI app is **one** thing: `@hanzo/ai`. Web, desktop and
mobile are not three apps — they are the same app with two axes injected.
```
┌────────────────────────── @hanzo/ai ──────────────────────────┐
│ src/app/ (the migrated shinkai-fork app — ONE copy, one place) │
│ + net-* libs (ui, state, i18n, message-ts, brand-config, logo, │
│ artifacts) + src/host/* (the @tauri-apps shim surface) │
└──────────────────────────────────────────────────────────────────┘
▲ ▲ ▲
host = web (default) host = tauriHost host = expoHost
brand = getBrand() brand = getBrand() brand = getBrand()
│ │ │
hanzo.app / .chat hanzoai/desktop @hanzo/gui
zoo.cloud / lux.cloud zooai/app · luxfi/app (Expo mobile)
```
## The two orthogonal axes (decomplected)
| Axis | What it is | How it's injected | Default |
|---|---|---|---|
| **Brand** | hanzo / zoo / lux identity, cloud endpoints, chain, IAM | `getBrand()` (driven by `VITE_BRAND` or hostname) **or** spread as props `<HanzoAI {...brand}/>` | `getBrand()` → hostname → HANZO |
| **Platform** | how native calls (`invoke`, `listen`, fs, window…) resolve | `host` prop — a `HostAdapter` | web no-ops |
Everything else — the entire app — is shared. A new app is a ~10-line shim:
```tsx
import HanzoAI, { getBrand } from '@hanzo/ai';
import { tauriHost } from './tauri-host'; // desktop only; web omits it
createRoot(root).render(
<HanzoAI {...getBrand()} host={tauriHost} features={{ chat, wallet, agents }} />
);
```
## Web ⇄ desktop parity
The web shim and the desktop shim are **byte-identical except one prop**
(`host={tauriHost}`). They import the same `@hanzo/ai`, which is the same
`src/app`. So the rendered React tree — every screen, route and component — is
identical. The only runtime difference is what `HostAdapter` does:
| Call | web (default host) | desktop (tauriHost) |
|---|---|---|
| `invoke(cmd, args)` | no-op, returns `undefined` (logs in dev) | Tauri IPC → Rust |
| `listen(event, cb)` | no-op unlisten | Tauri event bus |
| `getCurrentWindow()` | no-op Window (emit/listen/geometry) | real Tauri window |
| fs / shell / process / updater | no-op / empty | Tauri plugins |
So on the web the app renders 1:1 with desktop; native-only actions (open a
folder, auto-update, tray) simply do nothing instead of crashing. Cloud
inference + chat work on both (they go over HTTP to the brand's
`inferenceEndpoint`, not through `invoke`).
## Verification (2026-06-04)
| Surface | Build | Render |
|---|---|---|
| web dev server (`pkgs/ai/web`, imports `src`) | ✅ | ✅ Hanzo onboarding |
| web prod (unminified, 55M) | ✅ 40s | ✅ |
| web prod (minified, 13M / gzip 3.75M) | ✅ 44s | ✅ |
| library `dist` (self-contained, react external) | ✅ | — |
| **external shim** (`examples/web`, only react provided, app from `dist`) | ✅ | ✅ — proves SDK consumption |
The render blocker that had to be cleared first: app modules imported
`useBrand` from the **`@hanzo/ai` package name**, which resolved to the built
`dist` — pulling a second React + a second app copy into the source graph (a
cycle → `useContext` of null). Fix: the brand store lives in
`@hanzo_network/brand-config` (plain getters, not hooks), aliased to `src`;
app code never imports the `@hanzo/ai` package by name.
## Turning the real repos into shims
`luxfi/app`, `zooai/app`, `hanzoai/desktop`, `hanzoai/app` each become the
shim above. Per repo: depend on `@hanzo/ai` (+ `react`, `react-dom`, and for
desktop `@tauri-apps/api`), set `VITE_BRAND`, drop all app source. The
Dockerfile/Tauri config builds from the pinned SDK — exactly how
`zooai/exchange` builds from `@luxfi/exchange`. (`@hanzo/ai` is not yet
published to npm; until then the shims consume it via the workspace.)
+51
View File
@@ -0,0 +1,51 @@
# Testing @hanzo/ai — and TDD going forward
## Run
```bash
bun run --cwd pkgs/ai test # unit + regression (vitest, jsdom) — fast, no external deps
bun run --cwd pkgs/ai test:watch # vitest watch mode (use while writing a fix)
bun run --cwd pkgs/ai test:e2e # Playwright e2e — needs the local runtime up (skips if not)
```
`test` runs everything under `src/**/*.{test,spec}.{ts,tsx}` in jsdom. Config:
`vitest.config.ts` mirrors the web build's alias graph (so `@/`, `@tauri-apps/*`
→ host shims, `@hanzo_network/*` → src resolve), stubs the streamdown markdown
stack (esbuild can't resolve its nested micromark), and loads
`@testing-library/jest-dom` via `src/__tests__/setup.ts`. Pyodide /
python-code-runner tests are excluded (30 MB wasm + network).
## What's covered
**Unit + regression** (`src/__tests__/`) — each guards a bug this stack actually hit:
- `brand-store.test.ts``useBrand`/`getBrand` are plain getters callable
outside render (the invalid-hook-call that blocked the whole web app).
- `host-shims.test.ts``getCurrentWindow().emit` etc. exist; the injected host
adapter switches web↔tauri (the "emit is not a function" mount crash).
- `resizable.test.tsx``react-resizable-panels` v3 API present, net-ui
resizable renders (the v4 "Element type is invalid" chat-view crash).
- `no-missing-imports.test.ts` — critical screens import every `<Component>`
they use (the dropped `Box`/`Boxes`/`Coins` lucide imports → "Box is not defined").
**App tests** (pre-existing, `src/app/**`) — engine client, machine-state queries,
mining page, playground utils. **E2E** (`e2e/chat.e2e.test.ts`) — drives the real
browser: onboarding renders, then connect → register → send → assert zen-coder's
reply (and assert no "is not defined" / "Element type is invalid" page errors).
## TDD workflow (going forward)
Every fix and feature starts with a failing test:
1. **Red** — write a test that reproduces the bug or specifies the behavior, in
`src/__tests__/` (unit) or `e2e/` (flow). Run `test:watch`; confirm it fails.
2. **Green** — make the smallest change that passes it.
3. **Refactor** — clean up with the test green.
4. Run `bun run --cwd pkgs/ai test` before committing; keep it green.
Most bugs this session were import/dep/runtime issues that *static render checks
missed but a test would have caught* — prefer a regression test over a manual
re-check. For anything touching the chat path, add/extend the e2e.
The local runtime for `test:e2e` is in `pkgs/ai/local-runtime/` (see `CHAT.md`):
restart `run-node.sh` (fresh storage) before a run so Quick Connect registers as
the first device.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+270
View File
@@ -0,0 +1,270 @@
'use strict';
const index = require('./index-BSSKeTjn.cjs');
function arcInnerRadius(d) {
return d.innerRadius;
}
function arcOuterRadius(d) {
return d.outerRadius;
}
function arcStartAngle(d) {
return d.startAngle;
}
function arcEndAngle(d) {
return d.endAngle;
}
function arcPadAngle(d) {
return d && d.padAngle; // Note: optional!
}
function intersect(x0, y0, x1, y1, x2, y2, x3, y3) {
var x10 = x1 - x0, y10 = y1 - y0,
x32 = x3 - x2, y32 = y3 - y2,
t = y32 * x10 - x32 * y10;
if (t * t < index.epsilon) return;
t = (x32 * (y0 - y2) - y32 * (x0 - x2)) / t;
return [x0 + t * x10, y0 + t * y10];
}
// Compute perpendicular offset line of length rc.
// http://mathworld.wolfram.com/Circle-LineIntersection.html
function cornerTangents(x0, y0, x1, y1, r1, rc, cw) {
var x01 = x0 - x1,
y01 = y0 - y1,
lo = (cw ? rc : -rc) / index.sqrt(x01 * x01 + y01 * y01),
ox = lo * y01,
oy = -lo * x01,
x11 = x0 + ox,
y11 = y0 + oy,
x10 = x1 + ox,
y10 = y1 + oy,
x00 = (x11 + x10) / 2,
y00 = (y11 + y10) / 2,
dx = x10 - x11,
dy = y10 - y11,
d2 = dx * dx + dy * dy,
r = r1 - rc,
D = x11 * y10 - x10 * y11,
d = (dy < 0 ? -1 : 1) * index.sqrt(index.max(0, r * r * d2 - D * D)),
cx0 = (D * dy - dx * d) / d2,
cy0 = (-D * dx - dy * d) / d2,
cx1 = (D * dy + dx * d) / d2,
cy1 = (-D * dx + dy * d) / d2,
dx0 = cx0 - x00,
dy0 = cy0 - y00,
dx1 = cx1 - x00,
dy1 = cy1 - y00;
// Pick the closer of the two intersection points.
// TODO Is there a faster way to determine which intersection to use?
if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1;
return {
cx: cx0,
cy: cy0,
x01: -ox,
y01: -oy,
x11: cx0 * (r1 / r - 1),
y11: cy0 * (r1 / r - 1)
};
}
function d3arc() {
var innerRadius = arcInnerRadius,
outerRadius = arcOuterRadius,
cornerRadius = index.constant(0),
padRadius = null,
startAngle = arcStartAngle,
endAngle = arcEndAngle,
padAngle = arcPadAngle,
context = null,
path = index.withPath(arc);
function arc() {
var buffer,
r,
r0 = +innerRadius.apply(this, arguments),
r1 = +outerRadius.apply(this, arguments),
a0 = startAngle.apply(this, arguments) - index.halfPi,
a1 = endAngle.apply(this, arguments) - index.halfPi,
da = index.abs(a1 - a0),
cw = a1 > a0;
if (!context) context = buffer = path();
// Ensure that the outer radius is always larger than the inner radius.
if (r1 < r0) r = r1, r1 = r0, r0 = r;
// Is it a point?
if (!(r1 > index.epsilon)) context.moveTo(0, 0);
// Or is it a circle or annulus?
else if (da > index.tau - index.epsilon) {
context.moveTo(r1 * index.cos(a0), r1 * index.sin(a0));
context.arc(0, 0, r1, a0, a1, !cw);
if (r0 > index.epsilon) {
context.moveTo(r0 * index.cos(a1), r0 * index.sin(a1));
context.arc(0, 0, r0, a1, a0, cw);
}
}
// Or is it a circular or annular sector?
else {
var a01 = a0,
a11 = a1,
a00 = a0,
a10 = a1,
da0 = da,
da1 = da,
ap = padAngle.apply(this, arguments) / 2,
rp = (ap > index.epsilon) && (padRadius ? +padRadius.apply(this, arguments) : index.sqrt(r0 * r0 + r1 * r1)),
rc = index.min(index.abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments)),
rc0 = rc,
rc1 = rc,
t0,
t1;
// Apply padding? Note that since r1 ≥ r0, da1 ≥ da0.
if (rp > index.epsilon) {
var p0 = index.asin(rp / r0 * index.sin(ap)),
p1 = index.asin(rp / r1 * index.sin(ap));
if ((da0 -= p0 * 2) > index.epsilon) p0 *= (cw ? 1 : -1), a00 += p0, a10 -= p0;
else da0 = 0, a00 = a10 = (a0 + a1) / 2;
if ((da1 -= p1 * 2) > index.epsilon) p1 *= (cw ? 1 : -1), a01 += p1, a11 -= p1;
else da1 = 0, a01 = a11 = (a0 + a1) / 2;
}
var x01 = r1 * index.cos(a01),
y01 = r1 * index.sin(a01),
x10 = r0 * index.cos(a10),
y10 = r0 * index.sin(a10);
// Apply rounded corners?
if (rc > index.epsilon) {
var x11 = r1 * index.cos(a11),
y11 = r1 * index.sin(a11),
x00 = r0 * index.cos(a00),
y00 = r0 * index.sin(a00),
oc;
// Restrict the corner radius according to the sector angle. If this
// intersection fails, its probably because the arc is too small, so
// disable the corner radius entirely.
if (da < index.pi) {
if (oc = intersect(x01, y01, x00, y00, x11, y11, x10, y10)) {
var ax = x01 - oc[0],
ay = y01 - oc[1],
bx = x11 - oc[0],
by = y11 - oc[1],
kc = 1 / index.sin(index.acos((ax * bx + ay * by) / (index.sqrt(ax * ax + ay * ay) * index.sqrt(bx * bx + by * by))) / 2),
lc = index.sqrt(oc[0] * oc[0] + oc[1] * oc[1]);
rc0 = index.min(rc, (r0 - lc) / (kc - 1));
rc1 = index.min(rc, (r1 - lc) / (kc + 1));
} else {
rc0 = rc1 = 0;
}
}
}
// Is the sector collapsed to a line?
if (!(da1 > index.epsilon)) context.moveTo(x01, y01);
// Does the sectors outer ring have rounded corners?
else if (rc1 > index.epsilon) {
t0 = cornerTangents(x00, y00, x01, y01, r1, rc1, cw);
t1 = cornerTangents(x11, y11, x10, y10, r1, rc1, cw);
context.moveTo(t0.cx + t0.x01, t0.cy + t0.y01);
// Have the corners merged?
if (rc1 < rc) context.arc(t0.cx, t0.cy, rc1, index.atan2(t0.y01, t0.x01), index.atan2(t1.y01, t1.x01), !cw);
// Otherwise, draw the two corners and the ring.
else {
context.arc(t0.cx, t0.cy, rc1, index.atan2(t0.y01, t0.x01), index.atan2(t0.y11, t0.x11), !cw);
context.arc(0, 0, r1, index.atan2(t0.cy + t0.y11, t0.cx + t0.x11), index.atan2(t1.cy + t1.y11, t1.cx + t1.x11), !cw);
context.arc(t1.cx, t1.cy, rc1, index.atan2(t1.y11, t1.x11), index.atan2(t1.y01, t1.x01), !cw);
}
}
// Or is the outer ring just a circular arc?
else context.moveTo(x01, y01), context.arc(0, 0, r1, a01, a11, !cw);
// Is there no inner ring, and its a circular sector?
// Or perhaps its an annular sector collapsed due to padding?
if (!(r0 > index.epsilon) || !(da0 > index.epsilon)) context.lineTo(x10, y10);
// Does the sectors inner ring (or point) have rounded corners?
else if (rc0 > index.epsilon) {
t0 = cornerTangents(x10, y10, x11, y11, r0, -rc0, cw);
t1 = cornerTangents(x01, y01, x00, y00, r0, -rc0, cw);
context.lineTo(t0.cx + t0.x01, t0.cy + t0.y01);
// Have the corners merged?
if (rc0 < rc) context.arc(t0.cx, t0.cy, rc0, index.atan2(t0.y01, t0.x01), index.atan2(t1.y01, t1.x01), !cw);
// Otherwise, draw the two corners and the ring.
else {
context.arc(t0.cx, t0.cy, rc0, index.atan2(t0.y01, t0.x01), index.atan2(t0.y11, t0.x11), !cw);
context.arc(0, 0, r0, index.atan2(t0.cy + t0.y11, t0.cx + t0.x11), index.atan2(t1.cy + t1.y11, t1.cx + t1.x11), cw);
context.arc(t1.cx, t1.cy, rc0, index.atan2(t1.y11, t1.x11), index.atan2(t1.y01, t1.x01), !cw);
}
}
// Or is the inner ring just a circular arc?
else context.arc(0, 0, r0, a10, a00, cw);
}
context.closePath();
if (buffer) return context = null, buffer + "" || null;
}
arc.centroid = function() {
var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2,
a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - index.pi / 2;
return [index.cos(a) * r, index.sin(a) * r];
};
arc.innerRadius = function(_) {
return arguments.length ? (innerRadius = typeof _ === "function" ? _ : index.constant(+_), arc) : innerRadius;
};
arc.outerRadius = function(_) {
return arguments.length ? (outerRadius = typeof _ === "function" ? _ : index.constant(+_), arc) : outerRadius;
};
arc.cornerRadius = function(_) {
return arguments.length ? (cornerRadius = typeof _ === "function" ? _ : index.constant(+_), arc) : cornerRadius;
};
arc.padRadius = function(_) {
return arguments.length ? (padRadius = _ == null ? null : typeof _ === "function" ? _ : index.constant(+_), arc) : padRadius;
};
arc.startAngle = function(_) {
return arguments.length ? (startAngle = typeof _ === "function" ? _ : index.constant(+_), arc) : startAngle;
};
arc.endAngle = function(_) {
return arguments.length ? (endAngle = typeof _ === "function" ? _ : index.constant(+_), arc) : endAngle;
};
arc.padAngle = function(_) {
return arguments.length ? (padAngle = typeof _ === "function" ? _ : index.constant(+_), arc) : padAngle;
};
arc.context = function(_) {
return arguments.length ? ((context = _ == null ? null : _), arc) : context;
};
return arc;
}
exports.d3arc = d3arc;
+268
View File
@@ -0,0 +1,268 @@
import { P as withPath, Q as pi, S as cos, T as sin, V as constant, W as halfPi, X as epsilon, Y as tau, Z as sqrt, $ as min, a0 as abs, a1 as atan2, a2 as max, a3 as asin, a4 as acos } from './index-BnaE_tRw.js';
function arcInnerRadius(d) {
return d.innerRadius;
}
function arcOuterRadius(d) {
return d.outerRadius;
}
function arcStartAngle(d) {
return d.startAngle;
}
function arcEndAngle(d) {
return d.endAngle;
}
function arcPadAngle(d) {
return d && d.padAngle; // Note: optional!
}
function intersect(x0, y0, x1, y1, x2, y2, x3, y3) {
var x10 = x1 - x0, y10 = y1 - y0,
x32 = x3 - x2, y32 = y3 - y2,
t = y32 * x10 - x32 * y10;
if (t * t < epsilon) return;
t = (x32 * (y0 - y2) - y32 * (x0 - x2)) / t;
return [x0 + t * x10, y0 + t * y10];
}
// Compute perpendicular offset line of length rc.
// http://mathworld.wolfram.com/Circle-LineIntersection.html
function cornerTangents(x0, y0, x1, y1, r1, rc, cw) {
var x01 = x0 - x1,
y01 = y0 - y1,
lo = (cw ? rc : -rc) / sqrt(x01 * x01 + y01 * y01),
ox = lo * y01,
oy = -lo * x01,
x11 = x0 + ox,
y11 = y0 + oy,
x10 = x1 + ox,
y10 = y1 + oy,
x00 = (x11 + x10) / 2,
y00 = (y11 + y10) / 2,
dx = x10 - x11,
dy = y10 - y11,
d2 = dx * dx + dy * dy,
r = r1 - rc,
D = x11 * y10 - x10 * y11,
d = (dy < 0 ? -1 : 1) * sqrt(max(0, r * r * d2 - D * D)),
cx0 = (D * dy - dx * d) / d2,
cy0 = (-D * dx - dy * d) / d2,
cx1 = (D * dy + dx * d) / d2,
cy1 = (-D * dx + dy * d) / d2,
dx0 = cx0 - x00,
dy0 = cy0 - y00,
dx1 = cx1 - x00,
dy1 = cy1 - y00;
// Pick the closer of the two intersection points.
// TODO Is there a faster way to determine which intersection to use?
if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1;
return {
cx: cx0,
cy: cy0,
x01: -ox,
y01: -oy,
x11: cx0 * (r1 / r - 1),
y11: cy0 * (r1 / r - 1)
};
}
function d3arc() {
var innerRadius = arcInnerRadius,
outerRadius = arcOuterRadius,
cornerRadius = constant(0),
padRadius = null,
startAngle = arcStartAngle,
endAngle = arcEndAngle,
padAngle = arcPadAngle,
context = null,
path = withPath(arc);
function arc() {
var buffer,
r,
r0 = +innerRadius.apply(this, arguments),
r1 = +outerRadius.apply(this, arguments),
a0 = startAngle.apply(this, arguments) - halfPi,
a1 = endAngle.apply(this, arguments) - halfPi,
da = abs(a1 - a0),
cw = a1 > a0;
if (!context) context = buffer = path();
// Ensure that the outer radius is always larger than the inner radius.
if (r1 < r0) r = r1, r1 = r0, r0 = r;
// Is it a point?
if (!(r1 > epsilon)) context.moveTo(0, 0);
// Or is it a circle or annulus?
else if (da > tau - epsilon) {
context.moveTo(r1 * cos(a0), r1 * sin(a0));
context.arc(0, 0, r1, a0, a1, !cw);
if (r0 > epsilon) {
context.moveTo(r0 * cos(a1), r0 * sin(a1));
context.arc(0, 0, r0, a1, a0, cw);
}
}
// Or is it a circular or annular sector?
else {
var a01 = a0,
a11 = a1,
a00 = a0,
a10 = a1,
da0 = da,
da1 = da,
ap = padAngle.apply(this, arguments) / 2,
rp = (ap > epsilon) && (padRadius ? +padRadius.apply(this, arguments) : sqrt(r0 * r0 + r1 * r1)),
rc = min(abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments)),
rc0 = rc,
rc1 = rc,
t0,
t1;
// Apply padding? Note that since r1 ≥ r0, da1 ≥ da0.
if (rp > epsilon) {
var p0 = asin(rp / r0 * sin(ap)),
p1 = asin(rp / r1 * sin(ap));
if ((da0 -= p0 * 2) > epsilon) p0 *= (cw ? 1 : -1), a00 += p0, a10 -= p0;
else da0 = 0, a00 = a10 = (a0 + a1) / 2;
if ((da1 -= p1 * 2) > epsilon) p1 *= (cw ? 1 : -1), a01 += p1, a11 -= p1;
else da1 = 0, a01 = a11 = (a0 + a1) / 2;
}
var x01 = r1 * cos(a01),
y01 = r1 * sin(a01),
x10 = r0 * cos(a10),
y10 = r0 * sin(a10);
// Apply rounded corners?
if (rc > epsilon) {
var x11 = r1 * cos(a11),
y11 = r1 * sin(a11),
x00 = r0 * cos(a00),
y00 = r0 * sin(a00),
oc;
// Restrict the corner radius according to the sector angle. If this
// intersection fails, its probably because the arc is too small, so
// disable the corner radius entirely.
if (da < pi) {
if (oc = intersect(x01, y01, x00, y00, x11, y11, x10, y10)) {
var ax = x01 - oc[0],
ay = y01 - oc[1],
bx = x11 - oc[0],
by = y11 - oc[1],
kc = 1 / sin(acos((ax * bx + ay * by) / (sqrt(ax * ax + ay * ay) * sqrt(bx * bx + by * by))) / 2),
lc = sqrt(oc[0] * oc[0] + oc[1] * oc[1]);
rc0 = min(rc, (r0 - lc) / (kc - 1));
rc1 = min(rc, (r1 - lc) / (kc + 1));
} else {
rc0 = rc1 = 0;
}
}
}
// Is the sector collapsed to a line?
if (!(da1 > epsilon)) context.moveTo(x01, y01);
// Does the sectors outer ring have rounded corners?
else if (rc1 > epsilon) {
t0 = cornerTangents(x00, y00, x01, y01, r1, rc1, cw);
t1 = cornerTangents(x11, y11, x10, y10, r1, rc1, cw);
context.moveTo(t0.cx + t0.x01, t0.cy + t0.y01);
// Have the corners merged?
if (rc1 < rc) context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw);
// Otherwise, draw the two corners and the ring.
else {
context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw);
context.arc(0, 0, r1, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), !cw);
context.arc(t1.cx, t1.cy, rc1, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw);
}
}
// Or is the outer ring just a circular arc?
else context.moveTo(x01, y01), context.arc(0, 0, r1, a01, a11, !cw);
// Is there no inner ring, and its a circular sector?
// Or perhaps its an annular sector collapsed due to padding?
if (!(r0 > epsilon) || !(da0 > epsilon)) context.lineTo(x10, y10);
// Does the sectors inner ring (or point) have rounded corners?
else if (rc0 > epsilon) {
t0 = cornerTangents(x10, y10, x11, y11, r0, -rc0, cw);
t1 = cornerTangents(x01, y01, x00, y00, r0, -rc0, cw);
context.lineTo(t0.cx + t0.x01, t0.cy + t0.y01);
// Have the corners merged?
if (rc0 < rc) context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw);
// Otherwise, draw the two corners and the ring.
else {
context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw);
context.arc(0, 0, r0, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), cw);
context.arc(t1.cx, t1.cy, rc0, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw);
}
}
// Or is the inner ring just a circular arc?
else context.arc(0, 0, r0, a10, a00, cw);
}
context.closePath();
if (buffer) return context = null, buffer + "" || null;
}
arc.centroid = function() {
var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2,
a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - pi / 2;
return [cos(a) * r, sin(a) * r];
};
arc.innerRadius = function(_) {
return arguments.length ? (innerRadius = typeof _ === "function" ? _ : constant(+_), arc) : innerRadius;
};
arc.outerRadius = function(_) {
return arguments.length ? (outerRadius = typeof _ === "function" ? _ : constant(+_), arc) : outerRadius;
};
arc.cornerRadius = function(_) {
return arguments.length ? (cornerRadius = typeof _ === "function" ? _ : constant(+_), arc) : cornerRadius;
};
arc.padRadius = function(_) {
return arguments.length ? (padRadius = _ == null ? null : typeof _ === "function" ? _ : constant(+_), arc) : padRadius;
};
arc.startAngle = function(_) {
return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant(+_), arc) : startAngle;
};
arc.endAngle = function(_) {
return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant(+_), arc) : endAngle;
};
arc.padAngle = function(_) {
return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant(+_), arc) : padAngle;
};
arc.context = function(_) {
return arguments.length ? ((context = _ == null ? null : _), arc) : context;
};
return arc;
}
export { d3arc as d };
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
var __viteBrowserExternal = {};
export { __viteBrowserExternal as default };
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
const lang = Object.freeze(JSON.parse("{\"displayName\":\"Berry\",\"name\":\"berry\",\"patterns\":[{\"include\":\"#controls\"},{\"include\":\"#strings\"},{\"include\":\"#comment-block\"},{\"include\":\"#comments\"},{\"include\":\"#keywords\"},{\"include\":\"#function\"},{\"include\":\"#member\"},{\"include\":\"#identifier\"},{\"include\":\"#number\"},{\"include\":\"#operator\"}],\"repository\":{\"comment-block\":{\"begin\":\"#-\",\"end\":\"-#\",\"name\":\"comment.berry\",\"patterns\":[{}]},\"comments\":{\"begin\":\"#\",\"end\":\"\\\\n\",\"name\":\"comment.line.berry\",\"patterns\":[{}]},\"controls\":{\"patterns\":[{\"match\":\"\\\\b(if|elif|else|for|while|do|end|break|continue|return|try|except|raise)\\\\b\",\"name\":\"keyword.control.berry\"}]},\"function\":{\"patterns\":[{\"match\":\"\\\\b([A-Z_a-z][0-9A-Z_a-z]*(?=\\\\s*\\\\())\",\"name\":\"entity.name.function.berry\"}]},\"identifier\":{\"patterns\":[{\"match\":\"\\\\b[A-Z_a-z]\\\\w+\\\\b\",\"name\":\"identifier.berry\"}]},\"keywords\":{\"patterns\":[{\"match\":\"\\\\b(var|static|def|class|true|false|nil|self|super|import|as|_class)\\\\b\",\"name\":\"keyword.berry\"}]},\"member\":{\"patterns\":[{\"captures\":{\"0\":{\"name\":\"entity.other.attribute-name.berry\"}},\"match\":\"\\\\.([A-Z_a-z][0-9A-Z_a-z]*)\"}]},\"number\":{\"patterns\":[{\"match\":\"0x\\\\h+|\\\\d+|(\\\\d+\\\\.?|\\\\.\\\\d)\\\\d*([Ee][-+]?\\\\d+)?\",\"name\":\"constant.numeric.berry\"}]},\"operator\":{\"patterns\":[{\"match\":\"[-\\\\]!%\\\\&(-+./:<=>\\\\[^|~]\",\"name\":\"keyword.operator.berry\"}]},\"strings\":{\"patterns\":[{\"begin\":\"f(?=[\\\"'])\",\"patterns\":[{\"begin\":\"\\\"\",\"end\":\"\\\"\",\"name\":\"string.quoted.other.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"},{\"match\":\"\\\\{\\\\{[^}]*}}\",\"name\":\"string.quoted.other.berry\"},{\"begin\":\"\\\\{\",\"end\":\"}\",\"name\":\"keyword.other.unit.berry\",\"patterns\":[{\"include\":\"#keywords\"},{\"include\":\"#numbers\"},{\"include\":\"#identifier\"},{\"include\":\"#operator\"},{\"include\":\"#member\"},{\"include\":\"#function\"}]}]},{\"begin\":\"'\",\"end\":\"'\",\"name\":\"string.quoted.other.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"},{\"match\":\"\\\\{\\\\{[^}]*}}\",\"name\":\"string.quoted.other.berry\"},{\"begin\":\"\\\\{\",\"end\":\"}\",\"name\":\"keyword.other.unit.berry\",\"patterns\":[{\"include\":\"#keywords\"},{\"include\":\"#numbers\"},{\"include\":\"#identifier\"},{\"include\":\"#operator\"},{\"include\":\"#member\"},{\"include\":\"#function\"}]}]}],\"while\":\"\\\\G|^[\\\\t ]*(?=[\\\"'])\"},{\"begin\":\"([\\\"'])\",\"end\":\"\\\\1\",\"name\":\"string.quoted.double.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"}]}]}},\"scopeName\":\"source.berry\",\"aliases\":[\"be\"]}"));
const berry = [
lang
];
export { berry as default };
+11
View File
@@ -0,0 +1,11 @@
'use strict';
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const lang = Object.freeze(JSON.parse("{\"displayName\":\"Berry\",\"name\":\"berry\",\"patterns\":[{\"include\":\"#controls\"},{\"include\":\"#strings\"},{\"include\":\"#comment-block\"},{\"include\":\"#comments\"},{\"include\":\"#keywords\"},{\"include\":\"#function\"},{\"include\":\"#member\"},{\"include\":\"#identifier\"},{\"include\":\"#number\"},{\"include\":\"#operator\"}],\"repository\":{\"comment-block\":{\"begin\":\"#-\",\"end\":\"-#\",\"name\":\"comment.berry\",\"patterns\":[{}]},\"comments\":{\"begin\":\"#\",\"end\":\"\\\\n\",\"name\":\"comment.line.berry\",\"patterns\":[{}]},\"controls\":{\"patterns\":[{\"match\":\"\\\\b(if|elif|else|for|while|do|end|break|continue|return|try|except|raise)\\\\b\",\"name\":\"keyword.control.berry\"}]},\"function\":{\"patterns\":[{\"match\":\"\\\\b([A-Z_a-z][0-9A-Z_a-z]*(?=\\\\s*\\\\())\",\"name\":\"entity.name.function.berry\"}]},\"identifier\":{\"patterns\":[{\"match\":\"\\\\b[A-Z_a-z]\\\\w+\\\\b\",\"name\":\"identifier.berry\"}]},\"keywords\":{\"patterns\":[{\"match\":\"\\\\b(var|static|def|class|true|false|nil|self|super|import|as|_class)\\\\b\",\"name\":\"keyword.berry\"}]},\"member\":{\"patterns\":[{\"captures\":{\"0\":{\"name\":\"entity.other.attribute-name.berry\"}},\"match\":\"\\\\.([A-Z_a-z][0-9A-Z_a-z]*)\"}]},\"number\":{\"patterns\":[{\"match\":\"0x\\\\h+|\\\\d+|(\\\\d+\\\\.?|\\\\.\\\\d)\\\\d*([Ee][-+]?\\\\d+)?\",\"name\":\"constant.numeric.berry\"}]},\"operator\":{\"patterns\":[{\"match\":\"[-\\\\]!%\\\\&(-+./:<=>\\\\[^|~]\",\"name\":\"keyword.operator.berry\"}]},\"strings\":{\"patterns\":[{\"begin\":\"f(?=[\\\"'])\",\"patterns\":[{\"begin\":\"\\\"\",\"end\":\"\\\"\",\"name\":\"string.quoted.other.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"},{\"match\":\"\\\\{\\\\{[^}]*}}\",\"name\":\"string.quoted.other.berry\"},{\"begin\":\"\\\\{\",\"end\":\"}\",\"name\":\"keyword.other.unit.berry\",\"patterns\":[{\"include\":\"#keywords\"},{\"include\":\"#numbers\"},{\"include\":\"#identifier\"},{\"include\":\"#operator\"},{\"include\":\"#member\"},{\"include\":\"#function\"}]}]},{\"begin\":\"'\",\"end\":\"'\",\"name\":\"string.quoted.other.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"},{\"match\":\"\\\\{\\\\{[^}]*}}\",\"name\":\"string.quoted.other.berry\"},{\"begin\":\"\\\\{\",\"end\":\"}\",\"name\":\"keyword.other.unit.berry\",\"patterns\":[{\"include\":\"#keywords\"},{\"include\":\"#numbers\"},{\"include\":\"#identifier\"},{\"include\":\"#operator\"},{\"include\":\"#member\"},{\"include\":\"#function\"}]}]}],\"while\":\"\\\\G|^[\\\\t ]*(?=[\\\"'])\"},{\"begin\":\"([\\\"'])\",\"end\":\"\\\\1\",\"name\":\"string.quoted.double.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"}]}]}},\"scopeName\":\"source.berry\",\"aliases\":[\"be\"]}"));
const berry = [
lang
];
exports.default = berry;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+14
View File
@@ -0,0 +1,14 @@
'use strict';
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const python = require('./python-R5Doq6Ap.cjs');
const lang = Object.freeze(JSON.parse("{\"displayName\":\"Cairo\",\"name\":\"cairo\",\"patterns\":[{\"begin\":\"\\\\b(if).*\\\\(\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.if\"},\"2\":{\"name\":\"entity.name.condition\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.if\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(with)\\\\s+(.+)\\\\s*\\\\{\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.with\"},\"2\":{\"name\":\"entity.name.identifiers\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.with\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(with_attr)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*[({]\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.with_attr\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.with_attr\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"match\":\"\\\\belse\\\\b\",\"name\":\"keyword.control.else\"},{\"match\":\"\\\\b(call|jmp|ret|abs|rel|if)\\\\b\",\"name\":\"keyword.other.opcode\"},{\"match\":\"\\\\b([af]p)\\\\b\",\"name\":\"keyword.other.register\"},{\"match\":\"\\\\b(const|let|local|tempvar|felt|as|from|import|static_assert|return|assert|cast|alloc_locals|with|with_attr|nondet|dw|codeoffset|new|using|and)\\\\b\",\"name\":\"keyword.other.meta\"},{\"match\":\"\\\\b(SIZE(?:OF_LOCALS|))\\\\b\",\"name\":\"markup.italic\"},{\"match\":\"//[^\\\\n]*\\\\n\",\"name\":\"comment.line.sharp\"},{\"match\":\"\\\\b[A-Z_a-z][0-9A-Z_a-z]*:\\\\s*$\",\"name\":\"entity.name.function\"},{\"begin\":\"\\\\b(func)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*[({]\",\"beginCaptures\":{\"1\":{\"name\":\"storage.type.function.cairo\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"storage.type.function.cairo\"}},\"name\":\"meta.function.cairo\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(struct|namespace)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*\\\\{\",\"beginCaptures\":{\"1\":{\"name\":\"storage.type.function.cairo\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"storage.type.function.cairo\"}},\"name\":\"meta.function.cairo\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"match\":\"\\\\b[-+]?[0-9]+\\\\b\",\"name\":\"constant.numeric.decimal\"},{\"match\":\"\\\\b[-+]?0x\\\\h+\\\\b\",\"name\":\"constant.numeric.hexadecimal\"},{\"match\":\"'[^']*'\",\"name\":\"string.quoted.single\"},{\"match\":\"\\\"[^\\\"]*\\\"\",\"name\":\"string.quoted.double\"},{\"begin\":\"%\\\\{\",\"beginCaptures\":{\"0\":{\"name\":\"punctuation.section.embedded.begin.python\"}},\"contentName\":\"source.python\",\"end\":\"%}\",\"endCaptures\":{\"0\":{\"name\":\"punctuation.section.embedded.end.python\"},\"1\":{\"name\":\"source.python\"}},\"name\":\"meta.embedded.block.python\",\"patterns\":[{\"include\":\"source.python\"}]}],\"scopeName\":\"source.cairo0\",\"embeddedLangs\":[\"python\"]}"));
const cairo = [
...python.default,
lang
];
exports.default = cairo;
+10
View File
@@ -0,0 +1,10 @@
import python from './python-CRy0opiU.js';
const lang = Object.freeze(JSON.parse("{\"displayName\":\"Cairo\",\"name\":\"cairo\",\"patterns\":[{\"begin\":\"\\\\b(if).*\\\\(\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.if\"},\"2\":{\"name\":\"entity.name.condition\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.if\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(with)\\\\s+(.+)\\\\s*\\\\{\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.with\"},\"2\":{\"name\":\"entity.name.identifiers\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.with\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(with_attr)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*[({]\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.with_attr\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.with_attr\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"match\":\"\\\\belse\\\\b\",\"name\":\"keyword.control.else\"},{\"match\":\"\\\\b(call|jmp|ret|abs|rel|if)\\\\b\",\"name\":\"keyword.other.opcode\"},{\"match\":\"\\\\b([af]p)\\\\b\",\"name\":\"keyword.other.register\"},{\"match\":\"\\\\b(const|let|local|tempvar|felt|as|from|import|static_assert|return|assert|cast|alloc_locals|with|with_attr|nondet|dw|codeoffset|new|using|and)\\\\b\",\"name\":\"keyword.other.meta\"},{\"match\":\"\\\\b(SIZE(?:OF_LOCALS|))\\\\b\",\"name\":\"markup.italic\"},{\"match\":\"//[^\\\\n]*\\\\n\",\"name\":\"comment.line.sharp\"},{\"match\":\"\\\\b[A-Z_a-z][0-9A-Z_a-z]*:\\\\s*$\",\"name\":\"entity.name.function\"},{\"begin\":\"\\\\b(func)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*[({]\",\"beginCaptures\":{\"1\":{\"name\":\"storage.type.function.cairo\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"storage.type.function.cairo\"}},\"name\":\"meta.function.cairo\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(struct|namespace)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*\\\\{\",\"beginCaptures\":{\"1\":{\"name\":\"storage.type.function.cairo\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"storage.type.function.cairo\"}},\"name\":\"meta.function.cairo\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"match\":\"\\\\b[-+]?[0-9]+\\\\b\",\"name\":\"constant.numeric.decimal\"},{\"match\":\"\\\\b[-+]?0x\\\\h+\\\\b\",\"name\":\"constant.numeric.hexadecimal\"},{\"match\":\"'[^']*'\",\"name\":\"string.quoted.single\"},{\"match\":\"\\\"[^\\\"]*\\\"\",\"name\":\"string.quoted.double\"},{\"begin\":\"%\\\\{\",\"beginCaptures\":{\"0\":{\"name\":\"punctuation.section.embedded.begin.python\"}},\"contentName\":\"source.python\",\"end\":\"%}\",\"endCaptures\":{\"0\":{\"name\":\"punctuation.section.embedded.end.python\"},\"1\":{\"name\":\"source.python\"}},\"name\":\"meta.embedded.block.python\",\"patterns\":[{\"include\":\"source.python\"}]}],\"scopeName\":\"source.cairo0\",\"embeddedLangs\":[\"python\"]}"));
const cairo = [
...python,
lang
];
export { cairo as default };
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More