Files
zeekayandHanzo Dev b26758673d Tests read the user turn by index, as the lines above them do
`pnpm typecheck` failed in canvas-lms, figma, imanage, procore and
workday with TS2550: Property 'at' does not exist on type
'ChatCompletionMessage[]'. Their tsconfigs load lib ES2020, which has no
Array.prototype.at. @types/node 20 declared it, and 09997dba moved these
packages to @types/node 26, which does not.

Each failing line asked for `.at(1)!`, the second message, which the test
a few lines up in the same file reads as `msgs[1]`. They read it the same
way now. The source these packages ship was already ES2020, so the lib
stays.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 08:54:57 -07:00
..
2026-08-28 12:18:54 -07:00

Hanzo AI for Figma

A Figma & FigJam plugin that puts Hanzo AI next to your canvas. Select layers or a frame and:

  • Improve copy β€” tighten and clarify the selected text layers.
  • Translate β€” localize every text layer in the selection to a target language (placeholders like {name} / %s preserved).
  • Fill content β€” replace lorem/placeholder text with realistic, on-brand copy.
  • Rename layers β€” give the selected layers sensible, consistent names.
  • Critique + a11y β€” a plain-language design critique plus an accessibility review (color-contrast risks from the actual layer colors, WCAG AA notes).
  • Variants β€” generate options, e.g. "3 headline options" or "dark-mode copy", and insert any of them onto the canvas.

The AI runs on the api.hanzo.ai gateway through the published @hanzo/ai headless client. Your Hanzo API key (sk-…) is pasted once, validated against /v1/models, and stored only inside the plugin (figma.clientStorage). Nothing is sent anywhere but api.hanzo.ai.

How it works (two threads)

A Figma plugin is a main thread (code.js β€” full figma.* document access, no DOM, no network) plus an iframe UI (ui.html β€” DOM and network, no document access). They talk over postMessage.

 selection  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ main thread (code.ts) ─────────────┐
 change ───▢│ serialize selection β†’ SelectionSnapshot (data)   β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚ postMessage: { selection }
                                    β–Ό
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ iframe UI (ui.ts) ─────────────────┐
 chip ─────▢│ design-core.stream(action, snapshot, note)       β”‚
 click      β”‚   β†’ @hanzo/ai β†’ live tokens                      β”‚
            β”‚ design-core.parseResult(action, snapshot, text)  β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚ apply-edits        β”‚ insert-text (variants)
                    β–Ό                    β–Ό
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ main thread (code.ts) ─────────────┐
            β”‚ load fonts β†’ setCharacters / rename / createText β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The AI call happens only in the iframe (the main thread has no fetch). The main thread only reads the selection and writes results back onto nodes. All the pure logic — the action catalog, prompt building, selection→context assembly and truncation, and response parsing — lives in design-core.ts and is unit-tested.

Develop locally

Prerequisites: Node β‰₯ 18, pnpm, and the Figma desktop app.

pnpm install                      # from the repo root
pnpm --filter @hanzo/figma build  # β†’ packages/figma/dist/{code.js, ui.html, manifest.json}
# or, from packages/figma:
pnpm build      # one-shot
pnpm watch      # rebuild on change
pnpm test       # vitest (design-core + messaging)
pnpm typecheck  # tsc --noEmit

Import into Figma:

  1. Open the Figma desktop app (browser Figma cannot load a local plugin).
  2. Menu β†’ Plugins β†’ Development β†’ Import plugin from manifest…
  3. Select packages/figma/dist/manifest.json.
  4. Run it from Plugins β†’ Development β†’ Hanzo AI on any file (or a FigJam board). Paste your sk- key on first run.

With pnpm watch running, re-run the plugin after a change to pick up the new build (Figma reloads the manifest's code.js/ui.html each launch).

Publish to the Figma Community

  1. pnpm --filter @hanzo/figma build.
  2. In the Figma desktop app, right-click the plugin under Plugins β†’ Development β†’ Publish… (or Manage plugins in development).
  3. Fill in the name, description, icon, and cover art, then submit for review. The manifest.json already declares editorType: ["figma", "figjam"] and networkAccess.allowedDomains: ["https://api.hanzo.ai"] with a reason, which the Community review requires.

Layout

packages/figma/
β”œβ”€β”€ manifest.json          Figma plugin manifest (main β†’ code.js, ui β†’ ui.html)
β”œβ”€β”€ build.js               esbuild: code.js (IIFE) + ui.html (JS+CSS inlined)
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ code.ts            MAIN thread: serialize selection, write back nodes
β”‚   β”œβ”€β”€ ui.ts              IFRAME: the panel; the only place @hanzo/ai is called
β”‚   β”œβ”€β”€ ui.html / ui.css   the panel shell (single inlined file at build time)
β”‚   β”œβ”€β”€ design-core.ts     PURE: action catalog, prompts, context, parsing, run
β”‚   β”œβ”€β”€ messaging.ts       typed postMessage protocol (main ↔ iframe)
β”‚   └── config.ts          gateway base, default model, storage keys, budget
└── tests/                 vitest over design-core + messaging

Notes

  • Context budget. A big frame won't fit a model window, so the selection is serialized (text layers first, then structure, with ids and colors) and truncated to a character budget β€” with an honest note in the prompt and the panel when anything was dropped.
  • Write-back safety. Rewrite/translate/fill/rename ask the model for a strict JSON id β†’ value map; only ids that are actually in the current selection are applied, so the model cannot mutate nodes it wasn't shown.
  • Fonts. Setting text requires the node's fonts to be loaded first; the main thread loads every font range before writing.
  • Theme. The panel uses Figma's injected theme variables, so it matches the editor's light/dark mode with no toggle.

MIT Β© Hanzo AI