Commit graph

54 commits

Author SHA1 Message Date
Anton Soldatov 7305dbe634
AI chat polishing: skeleton workflow, batched tools, visual feedback, stock photos (#111)
* Fix text rendering: always use paragraph wrapping, font fallback, batch tools, severity levels

Rendering:
- renderText: always use paragraph when fontProvider available, not just
  when node-specific font loaded — fixes single-line overflow for Georgia/
  non-Inter fonts
- buildParagraph: add Inter as fallback fontFamily so paragraph wraps text
  even when requested font not yet loaded
- measureTextNode/buildTextPicture: remove isNodeFontLoaded gate — measure
  with fallback font for consistent layout↔rendering
- TEXT_PICTURE_KEYS: add width/height so textPicture invalidates on resize
- renderText drawText fallback: clip to node bounds
- onAfterExecute: invalidate textPictures after font load

Layout:
- estimateTextSize: accept maxWidth, compute multi-line height
- toJSON: depth parameter with childCount fallback

AI tools:
- batch_update tool: multiple set_layout/set_layout_child in one call
- describe: accept ids array for multi-node inspection
- Issue severity (error/warning/info) with pattern classification
- checkNestedFlexWithoutFill: cross-axis fill tolerance
- CORE_TOOLS trimmed 34→27, rest moved to EXTENDED
- System prompt: batch_update, describe ids, severity rules, font/text
  wrapping guidance, card grid patterns

* Add Pexels stock photo integration for AI chat

- stock_photo tool: search Pexels by query, download image, apply as
  IMAGE fill on any node. Picks resolution based on node dimensions.
- Pexels API key stored in localStorage, synced to core via
  setPexelsApiKey on change
- ProviderSettings UI: Pexels API key input with link to get free key
- System prompt: stock photo usage guidance
- Added to CORE_TOOLS so AI can use it in every session

* Batch stock_photo + calc, 4-phase workflow, fix wrap false positives

- stock_photo: accepts JSON array of requests, all fetched in parallel.
  Rejects nodes with children (leaf shapes only). One call replaces 14.
- calc: accepts JSON array of expressions or single string. One call
  replaces 3 separate calc invocations.
- System prompt: 4-phase workflow (plan → skeleton → content → polish).
  Batch stock_photo and calc examples. Updated step budget.
- describe-layout-issues: skip 'nested flex may collapse' for wrap
  containers (pill/tag frames are intentionally HUG). Extract
  childNeedsFill helper to reduce complexity.

* Fix nested flex false positive for pills, single-root render rule

- describe-layout-issues: skip 'nested flex may collapse' when a sibling
  already has grow or fill — pill/badge frames next to flex-grow content
  are intentionally small (e.g. BreakingLabel next to ticker text)
- System prompt: render must have exactly one root element. Stock_photo
  401 fallback: tell user, don't eval gradients.

* Rework workflow: render complete sections, not empty frames

Phase 2 now prescribes 5-7 renders, each producing a complete section
(nav, hero, stories, opinions, sidebar, footer) with placeholder images,
real text, proper auto-layout. No more empty frames to fill later.
Removes the fill content phase — skeleton IS the content, just without
real photos. Phase 3 is now just polish (batch stock_photo + fixes).
Step budget: 12-15 steps instead of 15-30.

* Pexels key input: match AI key behavior

Same pattern as the AI provider key — intermediate ref, 'Key saved —
enter new to replace' placeholder, Clear button, save on change. No
longer exposes the raw key in the input field.

* No empty frames in skeleton, select nodes after each tool

- System prompt: explicit rules against empty frames, no pre-creating
  wrapper frames for future content. Each render = complete section with
  visible placeholders. Render 1 = only nav+ticker, not the whole page
  with empty containers.
- tools.ts: set selection to flash node IDs after each mutating tool,
  so user sees which section AI is working on (selection border persists
  vs flash which is transient).

* AI overlay: pulsing blue while working, green flash on done; skeleton workflow

Rendering:
- ai-overlays.ts: new module with pulsing blue border (sinusoidal
  opacity 0.3-0.8 at 1.5s period) for nodes AI is working on, green
  fade-out flash (800ms) when done
- renderer.ts: aiMarkActive/aiMarkDone/aiClearAll methods, drawn after
  regular flashes
- constants.ts: AI_ACTIVE_COLOR, AI_DONE_COLOR, timing constants

Integration:
- ai-adapter.ts: onBeforeExecute now receives args (for target ID)
- tools.ts: marks target node (id or parent_id) active before tool
  executes, marks done via onFlashNodes after completion
- editor.ts: aiMarkActive/aiMarkDone/aiClearAll forwarded to renderer

Workflow:
- System prompt: 3-phase skeleton workflow. Phase 2 renders gray
  placeholder blocks (Rectangle bg=#E2E8F0) for every section — page
  looks like wireframe before any real content. Phase 3 replaces
  skeletons with real content. Phase 4 batch stock_photo + polish.

* render replace_id: atomic skeleton-to-content swap, fix AI overlay lifecycle

render tool:
- New replace_id parameter — renders new content, places it at the
  skeleton's position in parent, then deletes the skeleton. No visual
  gap between skeleton removal and content appearance.

AI overlay fixes:
- Track lastActiveIds across before/after execute — aiMarkDone clears
  the exact nodes that were marked active, not just the result IDs
- aiFlashDone: direct green flash on result nodes (not gated by
  _aiActiveNodes membership)
- replace_id included in target ID detection for active marking

System prompt:
- Phase 3 uses render({ replace_id }) instead of delete_node + render

* Fix gradient fills, nested flex false positives, render insert_index, h=hug prompt

- set_fill: gradient support with color_end + direction (top-bottom,
  left-right, etc). Uses proper Matrix type. No more eval for gradients.
- describe-layout-issues: skip nested flex warning for all non-MIN
  alignments (SPACE_BETWEEN, CENTER, MAX) — not just SPACE_BETWEEN
- render: insert_index parameter for controlling child order
- system prompt: page frame h="hug" not fixed height
- AI overlay: mark active on any tool with target ID (not just
  mutating), clear previous active in onBeforeExecute

* Add desktop business news site example to system prompt

Full workflow example: skeleton with gray placeholders → replace_id
content fills → batch stock_photo. Shows 12-col grid pattern, card
components, section headers, sidebar widgets, h=hug page frame.
Complements the existing mobile app UI example.

* Drop blue active pulse, keep only green done flash

Blue active marking was invisible — tool execution is 20-80ms (one
frame), AI thinking happens between tool calls where no hooks fire.
Green done flash on result nodes works and is visible.

* Blue pulse on skeleton during AI tool streaming

Watch streaming message parts in ChatPanel — when a tool part appears
with state 'call'/'partial-call' and has replace_id or parent_id in
args, mark that node active (blue pulse). When tool transitions to
'output-available', mark done (green flash). This shows the pulsing
blue border on the skeleton wireframe frame while AI generates JSX,
and switches to green when the content replaces it.

* AI overlay debug log section, watch tool parts by toolCallId

- chat-debug.ts: aiOverlayLog array + AI OVERLAY LOG section in debug
  output showing every part/mark-active/mark-done event with timestamps
- ChatPanel.vue: check parts by toolCallId instead of isToolUIPart,
  log all overlay events to aiOverlayLog for debugging

* Fix AI overlay: use 'input' not 'args' for tool part params

Vercel AI SDK UIToolInvocation uses 'input' field (not 'args') for
tool call parameters. The watch was checking p.args which was always
undefined, so no overlay events ever fired. Also use p.type instead
of p.toolName for the log.

* Move AI overlay to onBeforeExecute/onFlashNodes, reorder render params

- onBeforeExecute: mark replace_id/parent_id active (blue pulse)
- onFlashNodes: clear active, flash done on new nodes (green)
- Reorder render tool params: replace_id first so streaming partial
  input reveals target ID before JSX body
- Remove ChatPanel watch — all overlay logic in tools.ts now
- Add aiClearActive() to renderer (clears active without touching
  done flashes)

* Wait 2 rAF frames after marking active before tool execute

onBeforeExecute marks the target node active (blue pulse), but
execute ran immediately after without yielding to the render loop.
The blue frame was never painted because markActive → execute →
clearActive all happened in the same JS task. Now await two
requestAnimationFrame callbacks so the canvas renders at least one
blue-pulse frame before execution starts.

* Add comprehensive AI overlay debug logging to chat debug log

Both streaming watch (ChatPanel) and tool execution hooks (tools.ts)
now log every event to aiOverlayLog with full state: renderer exists,
activeNodes count, hasFlashes, node existence. This will show exactly
what happens and why the blue pulse is or isn't visible.

* Filter partial node IDs in streaming overlay watch

During input-streaming, replace_id may be partially received (e.g.
'0:' without the number part). Guard with isValidNodeId check.

Also refactor watch callback into collectPendingTargets +
syncOverlayState to stay under complexity limit.

Analysis of last debug log:
- Hero: only 235ms active (streaming didn't deliver replace_id early)
- Stories: 14.8s active — should be clearly visible
- Opinions: 12.5s, LatestNews: 11.5s, Stocks: 9.8s — all long enough
- Hero was the only one too fast to notice

* Fix undo race, JSX pragma conflict, partial ID overlay, add deletion log

- Fix undo snapshot race condition: replace shared `let beforeSnapshot`
  with a per-tool stack. Vercel AI SDK runs tool calls from one step via
  Promise.all — concurrent mutating tools overwrote the shared snapshot,
  causing wrong undo entries and potential content loss.

- Fix 'h is not a function' JSX error: rename sucrase pragma from `h`
  to `__h`. When AI writes `.map((h, i) => ...)`, the loop variable
  shadowed the pragma, crashing render.

- Fix partial node ID `0:1` in overlay: strengthen `isValidNodeId` to
  require both parts ≥2 chars and verify node exists in graph. During
  streaming, `replace_id: "0:1811"` arrives as `0:1` → `0:18` → etc.

- Add deletion log to SceneGraph: `_deletionLog` records id, name,
  type, parentId, timestamp, and truncated stack for last 200 deletions.
  Included in debug log as NODE DELETION LOG section to diagnose the
  intermittent frame disappearance bug.

* Reduce find_nodes spam and describe bloat in system prompt

- Instruct AI to reuse IDs from render results and describe output
  instead of calling find_nodes to rediscover them (saves 8+ calls)
- Recommend depth=1 for full-page describe, depth=2 only for sections

* Auto-depth describe + validate-after-render workflow

Describe tool now auto-picks depth based on subtree size when depth
is omitted: ≤15 nodes → depth 4, ≤40 → 3, ≤100 → 2, larger → 1.
Max raised to 5 for explicit overrides.

Prompt updated: after each render replace_id, call describe on that
block (auto depth gives full detail on small sections). Every 2-3
content renders, describe root at depth=1 to catch cross-section
layout issues early.

* Enforce describe-after-every-render in system prompt

Show explicit render → describe → fix pattern with code example.
AI was skipping per-block validation and batching all describes
at the end, causing compounding layout errors.

* Add describe rules: nested text, missing padding, button padding, row centering

Four new layout issue detectors in describe-issues.ts:

- Nested Text (error): Text node inside Text causes overflow.
  Suggestion: split into separate Text elements in flex row.

- Zero padding container (warning): Frame with fill + text children
  but no padding — content touches edges.

- Button without padding (warning): Button-like frame with children
  but zero horizontal padding.

- Row without vertical centering (warning): Horizontal flex with
  items=start, tall container, and short children — suggests
  items=center.

All verified with bun -e tests including false positive checks.

* Fix gradient alpha loss, add undersize child detection

Gradient fix: set_fill with color_end was overwriting alpha to 1
on both gradient stops (`{ ...c, a: 1 }`). Now preserves original
alpha from parsed hex — #00000000 → a:0, #000000AA → a:0.667.
This caused transparent-to-dark overlays to render as solid black.

New describe rule: detect child significantly narrower than parent
in a container without auto-layout (e.g. 100px Rectangle inside
896px Frame). Suggests adding flex + w=fill. Caught the HeroImg
bug that describe previously missed.

* Auto-include issues in render result, no separate describe needed

Render tool now scans the created subtree for errors/warnings and
includes them in the response. AI gets issues automatically without
needing to call describe — eliminates the pattern where AI skips
describe on 'simple' sidebar blocks.

Example response:
  { id: '0:42', name: 'Card', issues: [
    { node: 'Text', id: '0:45', message: 'no color', severity: 'error' }
  ] }

* Run layout before collecting render issues

Issues were checked on raw node sizes (default 100px) before
computeAllLayouts ran in onAfterExecute. Now render calls
computeAllLayouts inline before collectSubtreeIssues, so issues
reflect actual computed dimensions.

This fixes hundreds of false 'X 100px overflows parent' warnings
that made AI chase nonexistent problems.

* Revert auto-issues in render and layout-before-issues

Reverts 09a9b89 and 70c116a. Auto-including issues in render
result caused false positives (100px default sizes before layout)
and made AI chase nonexistent problems, degrading output quality.

* Clean up AI chat PR: remove debug scaffolding, add stock photo provider adapter

- Delete _deletionLog (stack traces on every node deletion)
- Delete aiOverlayLog and all debug push() calls
- Delete succesful_prompt_diffucult_ux.md (raw debug dump) and tokens.md (empty)
- Remove double-rAF hack from ai-adapter (added 32ms to every tool call)
- Restore simple beforeSnapshot for undo (was matching by tool name)
- Remove 70-line overlay state tracker from ChatPanel.vue
- Remove Math.* regex hack from calc (expr-eval has builtins)
- Refactor stock-photo into provider adapter: Pexels + Unsplash
- Add Unsplash key management to settings UI and use-chat composable

---------

Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-03-15 15:42:41 +03:00
Danila Poyarkov 7ac4c4c1a9 Remove Math.* regex hack from calc tool
expr-eval already has floor/ceil/round/abs/sqrt/min/max as builtins.
Drop the .replace(/Math\./g, '') that mangled arbitrary strings and
add a note to the AI prompt's Prohibited section instead.
2026-03-15 13:48:25 +03:00
mcdmags c847eab75c
feat(mcp): add export_image_file tool and fix headless PNG rendering (#108)
* feat(mcp): add export_image_file tool — saves PNG/JPG/WEBP to disk

The existing export_image tool returns base64 inline, which MCP clients
display in chat but don't save to disk. This new tool writes the image
directly to a file path, making exports usable for documentation,
mockup reviews, and CI pipelines.

Parameters:
- path: absolute file path to save (required)
- ids: node IDs to export (optional, defaults to all top-level nodes)
- format: PNG | JPG | WEBP (default: PNG)
- scale: 0.1–4 (default: 2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(mcp): add export_image_file tool and fix headless PNG rendering

**New tool: `export_image_file`**
Saves rendered PNG/JPG/WEBP images directly to a file path on disk.
The existing `export_image` returns base64 inline — useful for chat
but doesn't produce files. AI agents and CI pipelines need actual files.

Path validation runs before the expensive render call. The tool
respects `fileRoot` restrictions.

**Fix: headless PNG rendering now matches the editor**

1. `computeAllLayouts()` called after `render` creates nodes via JSX.
2. Bundled Inter font loaded from disk — tries `../fonts/` (installed
   package) then `../../public/` (project root). The web-based
   `fetch('/Inter-Regular.ttf')` fails in Node.js.
3. Text measurement uses `renderer.measureTextNode()` — the same
   ParagraphBuilder the renderer uses to draw text. Measurement and
   rendering use the same SkiaRenderer instance, same fontProvider,
   same font shaping. Zero hacks.

Export pipeline:
  ensureInterFont() → SkiaRenderer + loadFonts() →
  setTextMeasurer(renderer.measureTextNode) →
  computeAllLayouts() → renderNodesToImage()

**Tests added** (6 new tests in mcp-server.test.ts):
- export_image_file produces valid PNG after render
- export_image_file without ids exports all page children
- export_image_file without document returns error
- export_image_file with out-of-root path returns error
- JPG and WEBP formats produce non-empty output
- export_image_file is listed in tools

* Remove .cue directory and revert .gitignore changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Move computeAllLayouts into renderTree, reuse exportImage in MCP

computeAllLayouts was in the render tool but renderJSX has multiple
callers (render, replace_node, automation server). Moving it into
renderTree ensures layout is always computed after JSX rendering.

export_image_file in MCP server was duplicating export_image logic.
Now delegates to exportImage.execute and just handles the file write.

---------

Co-authored-by: Michael Magsuci <mcdmag@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-03-14 18:43:50 +03:00
Danila Poyarkov d7945694dd Fix set_radius returning unserializable Symbol for mixed corners
When corners have different radii, node.cornerRadius returns
Symbol('mixed') which can't be serialized to JSON, breaking AI SDK
tool results. Return individual radius values instead.

Also remove unused initFontService import from MCP server.

Co-authored-by: Anton Soldatov <eddclyde@yandex.ru>
Closes #109
2026-03-14 11:35:51 +03:00
Danila Poyarkov ec741a9388 Remove dead files: codegen.md duplicate and planning doc
codegen.md is identical to codegen-prompt.ts (the TS export is what's
actually used — no code references codegen.md).

design-to-code-overview.md is a planning doc superseded by the actual
implementation. Same category as mcp-codegen-plan.md which was already
moved to discussion #99.
2026-03-13 22:29:14 +03:00
Anton A S aa6a8fa5a9 Guard set_layout on NONE frames, remove implicit auto-layout on childCount >= 2
- set_layout returns error if direction omitted on layoutMode: NONE frame
- Remove shouldEnableAutoLayout childCount >= 2 heuristic — require explicit flex prop
2026-03-13 14:54:12 +03:00
Anton A S d688d04bef Address PR #94 review: dedup, replace hand-rolled code, cleanup
- Replace calc.ts (150 lines) with expr-eval library
- Replace rgbLuminance with culori wcagLuminance
- Use @iconify/utils iconToSVG for viewBox normalization in iconify.ts
- Extract describe-shared.ts: findAncestorBackground, looksLikeButton, CONTAINER_TYPES
- Extract icon-render.ts: shared createIconFromPaths for create.ts and renderer.ts
- Inline codegen prompt as TS constant, remove runtime fs.readFile from MCP server
- Fix blur effect color alpha (a:1 → a:0)
- Encapsulate module-level mutable state in src/ai/tools.ts into RunState class
- Add network check to iconify tests (skip gracefully offline)
- Remove Caddyfile.dev and components.d.ts from tracking, add to .gitignore
2026-03-13 14:46:18 +03:00
Anton A S 933511f43e Improve AI design quality: auto-infer flex, reduce false warnings, prompt patterns
Renderer: Frames with 2+ children auto-infer flex="col" when no explicit
flex/grid prop is set. Children with x/y keep ABSOLUTE positioning.
Extract shouldEnableAutoLayout() to stay under complexity limit.

Describe issues: suppress false positives that waste AI steps—
- checkNestedFlexWithoutFill: skip when parent uses justify=between/center
- checkGrowInHug/checkHugCollapse: skip when node will get concrete size
  from its own parent (grow>0, STRETCH, or effectively filling)
- checkSiblingHeightConsistency: skip when parent uses items=center

System prompt: add mandatory flex rule for multi-child frames,
tab-bar pattern, remove 'nested flex may collapse' from fix list.
2026-03-13 12:21:10 +03:00
Anton A S b5fd31b557 Fix Kiwi encodeColor crash on colors missing alpha field
Defensive safeColor() guard at serialization boundary — fills, strokes,
effects, and gradient stops all go through it before Kiwi encoding.
Also fix const→let for execResult in AI adapter (reassigned by step budget).
2026-03-13 11:48:43 +03:00
Anton A S d46b4b11a4 Fix layout axis mismatch, reduce describe noise, add bail-out strategy
Engine:
- layoutSizingVertical/Horizontal setters now use child's own layout
  direction (not parent's) to map primary/counter axis — matches Yoga
- FILL on cross-axis uses alignSelf:Stretch instead of flexGrow
  (flexGrow only works on main axis, caused even distribution bug)
- FILL on main axis auto-sets layoutGrow=1 for leaf nodes

This fixes the 20-step debugging spiral where AI set FIXED heights
on HORIZONTAL children inside VERTICAL parent but Yoga ignored them
(wrote primaryAxisSizing via parent axis, Yoga read counterAxisSizing
via child axis — never saw the FIXED value).

Describe noise reduction:
- Suppress 'absolutely positioned inside flex' — intentional for
  decorative overlays (selection handles, badges)
- Suppress 'justify=between with HUG' when node fills parent width
- Suppress 'fixed size + grow' when sizing is FILL (standard pattern)
- 'Inconsistent padding' requires 3+ siblings (not 2)

Prompt:
- Bail-out: delete and re-render after 2 failed set_* attempts
- eval restricted to operations, not layout debugging
2026-03-13 10:21:46 +03:00
Anton A S 417aaedcb3 Fix set_layout padding reset, enforce final describe after fixes
- set_layout: only change spacing/align/padding when explicitly passed.
  Previously spacing/align/padding defaulted to 0/MIN, wiping existing
  values when AI only wanted to change one property.
- search_icons: client-side slice to enforce limit (API ignores small
  limits, returned 31 results for limit=1)
- system-prompt: issues from describe are mandatory fixes, not advisory.
  Final describe after set_* calls is required workflow step.
2026-03-13 10:20:57 +03:00
Anton A S 1454afc036 Optimize AI token usage: ~61% reduction in per-step context
- System prompt: 19.5KB → 6.3KB (67% smaller), removed redundant
  rules, condensed spacing/typography/layout docs
- Tool tiers: CORE_TOOLS (34) vs EXTENDED_TOOLS (64), AI chat uses
  core only (~2.8K tokens vs ~6.5K). ALL_TOOLS for MCP/CLI unchanged.
- <Icon> element in render JSX: <Icon name="lucide:heart" size={20}
  color="#FFF" /> fetches and renders inline. Eliminates the
  search→fetch→insert×N→delete×N chain (~8 tool calls per design).
- search_icons default limit 20 → 5 (~300 fewer tokens per query)
- get_page_tree returns compact tree (id, type, name, w, h) instead
  of full recursive nodeToResult dump
- eval returns { ok: true } instead of null, saves a follow-up
  describe call
- renderTree/renderJSX/renderTreeNode now async to support Icon fetch
2026-03-13 10:20:56 +03:00
Anton A S fed6ea5734 Fix icon SVG primitives, harden describe issues, ban justify=evenly
- iconify: convert <circle>, <rect>, <ellipse>, <line>, <polygon>,
  <polyline> to path d-strings. Fixes lucide:search (missing circle),
  lucide:user (missing head), lucide:mail (missing envelope), etc.
- describe: show justify/items/sizing in layout description, detect
  icon role, recurse issues into children at every depth level
- describe-issues: split layout checks into describe-layout-issues.ts
- New checks: fill without flex parent, absolute in flex, nested flex
  without fill, duplicate sibling names, stroke color/weight mismatch,
  circular frame without clip, excessive nesting, same fill as parent,
  image placeholder without content, sibling height inconsistency,
  text wrapping detection, alignment issues (between+HUG, between<2,
  stretch with all fixed, equal children packed without gap)
- system-prompt: remove justify="evenly" — not supported
2026-03-13 10:20:55 +03:00
Anton A S 160d5474c9 Fix lint errors: eliminate all any casts, fix complexity and nested ternaries 2026-03-13 10:20:54 +03:00
Anton A S 9daf7ce6ea Fix icon vector positioning: zero x/y after appendChild 2026-03-13 10:18:47 +03:00
Anton A S 4d3a5341cd Fix lint errors: split describe.ts, fix TS errors, clean up type guards
- Extract issue detection from describe.ts into describe-issues.ts (609→167 lines)
- Fix graph not in scope in detectStructuralIssues (TS2304)
- Fix child.characters → child.text (TS2339, SceneNode uses text not characters)
- Remove unused DEFAULT_TEXT_LUMINANCE
- Split detectLayoutIssues (complexity 86→<20) into 8 focused helpers
- Fix non-null assertions in chat-debug.ts
- Fix nested ternary in chat-debug.ts
- Fix unnecessary optional chains in use-chat.ts, export-jsx.ts
- Use Map for MATH_FUNCTIONS in calc.ts (fixes no-unnecessary-condition)
- Use Partial<Record> for SKIP_ARGS and TokenEntry.values (honest nullable types)
- Remove redundant null checks in codegen.ts type guards

Errors: 32 → 1 (pre-existing applyTextOverrides complexity)
Warnings: 47 → 42
2026-03-13 10:18:45 +03:00
Anton A S 774cf6b90a Address PR #94 review feedback
- Remove docs/mcp-codegen-plan.md (internal planning doc, not for repo)
- Remove lol.fig test fixture left in repo root
- Fix inline type imports in codegen.ts — use top-level SceneGraph import
- Move get_codegen_prompt out of core/ALL_TOOLS into MCP server only
  (core must be browser-safe, node:fs is not available in browser)
- Remove skill from packages/mcp/skills/ — content saved to #97
- Slim down codegen.md overlap note (canonical reference stays there)

Closes review comments. Skill npm publishing tracked in #97.
2026-03-13 10:18:44 +03:00
Anton A S dd91b682d1 Fix icon positioning after insert, add emoji guidance to system prompt
- Zero icon frame x/y after appendChild to parent — fixes icons appearing
  at viewport origin instead of inside their parent container
- Add name→names fallback to insert_icon for backward compatibility with
  single-icon calls (name param still works, returns legacy format)
- Add emoji section to system prompt: emoji show as □ tofu on canvas,
  use insert_icon for UI iconography, emoji only in user content/reactions
- Strengthen flex requirement in layout docs: every parent with fill
  children MUST have flex='col'|'row'
2026-03-13 10:18:43 +03:00
Anton A S dec0a67e4f Batch insert_icon and search_icons, fetch_icons prefetch tool 2026-03-13 10:18:41 +03:00
Anton A S 733321fbb5 Add step budget warnings and Continue button for AI chat
- Track per-run step count, warn AI when 5 steps remain via _warning
  field injected into tool results
- Show 'Continue' button in chat when step limit (50) is exhausted
- Add step budget section to system prompt so AI plans ahead
- Reset run step counter on each new agent call via prepareCall
2026-03-13 10:18:20 +03:00
Anton A S 530b56acc5 Add calc tool for layout arithmetic, make set_layout direction optional
- Add calc tool (recursive descent parser) so AI uses exact math
  instead of error-prone mental arithmetic for layout sizing
- Make set_layout direction param optional — keeps current layoutMode
  when only changing spacing/padding (eliminates ~50% of NO-OP mutations)
- Add set_layout to SKIP_ARGS in NO-OP detection
- Update system prompt: calc in workflow, mandatory before rendering
  into fixed-size containers
2026-03-13 10:17:50 +03:00
Anton A S 94160168ec AI chat: debug log with tool loop detection, prompt caching, system prompt tuning
Debug log (chat-debug.ts, ai-adapter.ts):
- Before/after node snapshots detect no-op mutations
- Duplicate tool call detection (mutating tools only)
- Per-step token usage with cache hit/miss breakdown
- TOKEN USAGE & CACHING section with hit rate and cost savings

Prompt caching (use-chat.ts):
- Enable Anthropic automatic caching via providerOptions for
  anthropic, anthropic-compatible, and OpenRouter anthropic models
- Track step usage via onStepFinish with cache read/write tokens

Provider fixes:
- anthropic-compatible now treated as custom provider in ChatInput
  (shows customModelID instead of stale modelID from previous provider)

System prompt:
- Sizing rule: calculate available space before rendering skeleton
- Concise final summary (2-3 lines, no section-by-section listing)
2026-03-13 10:17:47 +03:00
Anton A S 8300ebb807 Fix text w="fill" exported as w={computed}, add wrap rowGap check
export-jsx: Text with layoutAlignSelf=STRETCH (w="fill" in col parent) or
layoutGrow>0 (w="fill" in row parent) was emitting w={computedPx} instead
of letting the fill/grow detection below handle it. Now skips fixed w
emission when text stretches or grows.

describe: wrap layout without rowGap flagged — wrapped rows stick together.

prompt: wrap always needs rowGap.

Tests: 2 new round-trip tests for text w="fill" in col and row parents.
2026-03-13 10:17:15 +03:00
Anton A S ce5433da86 Add progress bar pattern, divider placement rules, w+grow conflict check
Prompt:
- Progress bar pattern with grow bar bg + overflow hidden
- Don't mix w={N} and grow={N} on same node
- Don't wrap Text in Frame just for sizing
- Vertical dividers must be inside flex=row with h=fill
- Horizontal dividers in flex=col

Describe:
- w + grow conflict detection
- Child Rectangle overflowing parent without clipping
- Vertical divider in column layout (wrong parent)
- Horizontal divider in row layout (wrong parent)
2026-03-13 10:17:14 +03:00
Anton A S 3904b987d8 Add text overflow and wrap layout rules to prompt and describe
Prompt:
- Text inside fixed-width containers must have explicit w
- Wrap layout column width formula: count chars × 0.6 × fontSize
- Don't guess widths in wrap grids

Describe:
- Text wider than parent available width flagged with fix suggestion
2026-03-13 10:17:12 +03:00
Anton A S 633469f61f Add text props (lineHeight, letterSpacing, textCase, textDecoration, maxLines), spacing checks, update prompt
Renderer + export-jsx:
- lineHeight={N}, letterSpacing={N}
- textDecoration="underline"|"strikethrough"
- textCase="upper"|"lower"|"title"
- maxLines={N} (auto-enables truncation), truncate (standalone)

Describe:
- Spacing consistency checks (inconsistent padding/gap among siblings)
- Off-grid spacing values (not on 4px grid)
- Gap larger than padding warning
- Corner radius: child radius vs parent − padding rule

Prompt:
- Complete props reference with explicit "nothing else exists" guard
- Typography: type scale, line height, letter spacing, hierarchy rules
- Spacing: 4px grid, Gestalt proximity, consistency rule
- All new text props documented

Tests:
- 7 round-trip tests for new text props (render → SceneNode → export JSX)
2026-03-13 10:17:11 +03:00
Anton A S 5f74908a35 Add typography and corner radius rules to prompt and describe
Prompt — full typography guide:
- Type scale (6-8 sizes, ratio ~1.25)
- Weight hierarchy (max 2-3 per design)
- Color hierarchy for light/dark backgrounds
- Line height rules (tight for headings, relaxed for body)
- Line length (45-75 chars), spacing between text elements
- Uppercase only for small labels/overlines
- Alignment guidelines

Corner radius — nested radius rule:
- inner = outer − padding, 0 if padding ≥ outer

Describe checks:
- Child radius exceeding parent or not matching inner rule
- Uppercase text at sizes > 13px
2026-03-13 10:17:10 +03:00
Anton A S 927bee4270 Add layout and visibility checks to describe tool, fix prompts
describe now catches:
- Text without color (invisible — fills is empty)
- Shapes without fill/stroke (invisible)
- Icon-sized frames with no visible content
- Dark text on dark background
- grow inside HUG parent (no effect)
- Children overflowing fixed parent
- HUG container where all children use grow/stretch (collapses)
- Cross-axis overflow

Prompt changes:
- Text always requires color="#hex" — no fill = invisible
- Shapes/icons always need bg or stroke
- Button icons highlighted as #1 invisible element source
2026-03-13 10:17:09 +03:00
Anton A S 31dbe9690c Add MCP codegen pipeline: tools, prompt, and plan 2026-03-13 10:17:06 +03:00
Danila Poyarkov e97a4cfda4 Fix lint warnings: eliminate any types, split oversized files
- Replace all no-explicit-any with proper types or justified eslint-disable
- Extract scene-graph-hit-test.ts (hit testing), scene-graph-instances.ts
  (component/instance sync) from scene-graph.ts
- Extract renderer/text.ts (text rendering), renderer/pen-overlay.ts
  (vector pen tool overlay) from renderer.ts
- Extract figma-api-proxy.ts (FigmaNodeProxy class) from figma-api.ts
- Extract kiwi-convert-overrides.ts from kiwi-convert.ts
- Add eslint-disable with justification for remaining large class files
- Fix unused imports from splits
- Warnings: 42 → 2 (only unavoidable no-implied-eval in vendored code)
2026-03-12 15:13:30 +03:00
Danila Poyarkov 6072ce57c1 Fix lint warnings: non-null assertions, useless spreads, unused code, type safety
- Replace non-null assertions with guards/optional chaining
- Replace useless spreads with Array.from() where mutation-safe
- Remove unused imports, variables, and functions
- Fix no-base-to-string with proper type narrowing
- Fix sort without comparator, new Array() pattern
- Prefix unused parameter with underscore

Warnings: 76 → 42 (remaining are max-lines, justified any casts, vendored code)
2026-03-11 13:33:54 +03:00
Anton Soldatov 37da83aec3
Image drag-and-drop, clipboard paste, and paste-at-cursor (#92)
* Image drag-and-drop and clipboard paste onto canvas

* Paste images at cursor position, track canvas cursor in state

* Paste nodes at cursor position (Figma and internal clipboard)

* Fix review: center images at cursor, fix hasImageFiles, clean up hash/undo

* Paste at viewport center when cursor is outside canvas

* Fix Figma paste positioning: center nodes at cursor after import

* Image support: clipboard, export, drag-and-drop, paste, renderer, tools, UI

- Clipboard: embed image bytes (base64) in OpenPencil clipboard payload so
  copy/paste between documents preserves image fills
- Fig export: write images/ folder to .fig zip (both fflate and Tauri paths)
- Tauri: extend build_fig_file to accept image entries
- Renderer: implement CROP (with imageTransform) and TILE (TileMode.Repeat)
  scale modes, fix FIT to center the fitted image
- FigmaAPI: add createImage(bytes) with sync FNV-1a hash
- Tools: add set_image_fill tool for AI/MCP
- FillPicker: replace placeholder with file picker, preview, scale mode selector
- Drag-and-drop: new use-image-drop composable creates image nodes from dropped files
- Paste: keyboard paste handler detects image clipboard items
- Shared utils: extract hashImageBytes and getImageDimensions to src/utils/image.ts

* Add image tests and Yjs image sync for collaboration

Tests (18 new):
- FigmaAPI.createImage: deterministic hash, storage, format
- set_image_fill tool: all scale modes, error handling, storage
- Clipboard roundtrip: image bytes preserved, multiple images, children
- Fig export/import: zip contains images/, full round-trip

Collab:
- Add yimages Y.Map to sync graph.images via Yjs
- Observer applies remote image adds/deletes to local graph
- syncNodeToYjs pushes referenced image data alongside node props
- syncAllNodesToYjs bulk-syncs all images on room share

* Fix FillPicker: remove deleted utils/image import, use SHA-1 inline

* Extract storeImage() on editor store, use in FillPicker and placeImageNode

* Unify image hash: use sync FNV-1a everywhere, export computeImageHash

Editor store's hashBytes (async SHA-1) produced different hashes than
FigmaAPI.createImage (sync FNV-1a) for the same bytes. This meant
drag-and-drop images couldn't be deduplicated against AI tool images.

Replace hashBytes with computeImageHash from core. storeImage() is
now sync.

* Skip drawing IMAGE fills when image data is missing

When pasting from Figma, image fills reference a CDN hash but no pixel
data is included in the clipboard. Previously this rendered as a solid
black rectangle because applyImageFill bailed without setting a shader,
leaving stale paint state.

applyFill now returns false when the fill can't be applied, and callers
skip the draw call. The node still exists with the correct imageHash —
if the image data is later provided (e.g. via file re-open), it will
render correctly.

* Warn when Figma paste has missing image data

Show amber warning toast when pasted nodes reference image fills
without available bytes (Figma clipboard limitation).

Add 'warning' toast variant with tailwind-variants, extract toast
styles to src/components/ui/toast.ts.

* Use useFileDialog and useObjectUrl in FillPicker

Replace manual file input ref, click(), createObjectURL/revokeObjectURL
with vueuse composables. Remove hidden <input type=file> from template.

---------

Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-03-11 07:52:04 +03:00
Anton Soldatov e939025f7d
Fix auto-layout overflow, grid layout, AI chat UX (#86)
* Fix auto-layout overflow: MeasureFunc, min/max, absolute positioning, text wrap

Layout engine (layout.ts):
- Use Yoga MeasureFunc for text nodes instead of static pre-measurement,
  so text wraps correctly when width is determined by flex layout
- Add min/max width/height constraint support (minWidth, maxWidth, etc.)
- Translate counterAxisAlignContent to Yoga setAlignContent for wrap layouts
- Extract configureFlexContainer to share between root and nested frames

JSX renderer (render/renderer.ts):
- Auto-set layoutPositioning ABSOLUTE for elements with x/y inside auto-layout
- Fix text auto-resize: set HEIGHT when text fills parent in auto-layout

Text measurer (renderer/renderer.ts):
- Accept optional maxWidth parameter for constraint-based measurement

* AI prompt: prefer describe over export_image for post-render verification

* Strip TS casts from AI-generated JSX before sucrase parse

AI models sometimes emit `as any`, `as const` etc. in JSX props.
Sucrase with jsx-only transform can't handle these, causing parse errors.

- Strip TypeScript cast expressions before passing to sucrase
- Add forbidden patterns to AI prompt: as any, template literals for
  sizes, Math.random()

* Cache text MeasureFunc results to avoid repeated buildParagraph calls

Yoga calls MeasureFunc multiple times per node during calculateLayout.
Each call was creating a full CanvasKit Paragraph — expensive for layouts
with many text nodes. Cache by rounded constraint width.

* AI chat: fix provider settings popover, add max output tokens, fix paste in chat input

- Fix ProviderSettings popover not visible (remove tooltip/popover trigger
  conflict, add collision-padding, isolate z-[51])
- Add configurable max output tokens (default 16384) in provider settings
- Pass maxOutputTokens to ToolLoopAgent to prevent truncated tool calls
- Add system prompt size limits: split render calls at ~40 elements
- Fix paste/copy/cut in chat input (stop event propagation to canvas handler)

* Fix 6 layout engine bugs: hidden size, FILL basis, SPACE_EVENLY, alignSelf range, grid stretch, absolute children

Layout engine (layout.ts):
- Guard applyYogaLayout to preserve hidden children dimensions
- Add setFlexBasis(0) for FILL sizing so children share space from zero
- Add SPACE_EVENLY to mapJustify via Justify.SpaceEvenly
- Replace hardcoded STRETCH checks with mapAlignSelf for full range
- Use setWidthStretch/setHeightStretch for grid children instead of flexGrow
- Insert absolute children in Yoga tree via configureAbsoluteChild with
  PositionType.Absolute, keeping manual x/y (no write-back)

Types (scene-graph.ts):
- Add SPACE_EVENLY to LayoutAlign union
- New LayoutAlignSelf type: AUTO | MIN | CENTER | MAX | STRETCH | BASELINE

Import (kiwi-convert.ts):
- Map SPACE_EVENLY correctly instead of collapsing to SPACE_BETWEEN
- New mapAlignSelf function for full StackCounterAlign range

Tools (modify.ts):
- Expand set_layout align enum with SPACE_EVENLY
- Expand set_layout_child align_self enum with MIN/CENTER/MAX/BASELINE

Tests: 86 pass (+13 new covering all 6 fixes)

* Grid layout support in JSX renderer, auto-height grids, flex children stretch in cells

Renderer:
- Parse grid/columns/rows/columnGap/rowGap/gap props
- Parse grid child positioning: colStart/col/rowStart/row/colSpan/rowSpan
- Grid prop takes precedence over padding-triggered auto-layout
- SPACE_EVENLY added to ALIGN_MAP
- Numeric columns shorthand (columns={3} → 3×1fr)
- Auto-height: grid without rows sets height=0 for Yoga auto-sizing
- fill sizing in grid children → layoutAlignSelf: STRETCH
- Component builders accept variadic children: Frame(props, ...children)

Layout:
- Grid auto-height: skip setHeight when no gridTemplateRows
- Write back computed height for auto-height grids
- Grid children with layoutMode stretch width to fill cell
- recomputeGridChild: re-run flex layout after grid assigns cell size
  (temporarily sets sizing to FIXED so HUG doesn't override grid width)

Export:
- Skip height export for auto-height grids (no rows template)

Tests: 18 new grid render tests covering all paths

* Auto-enable flex for justify/items props, discourage export_image in AI prompt

* Address review: use sucrase typescript transform, don't reset chat on maxOutputTokens change

* Fix lint errors: extract applyFrameSize, wrap nested ternary, remove unnecessary cast

* Remove SPACE_EVENLY — not a valid Figma layout value

* CI: debug LFS pull from R2

* Apply maxOutputTokens dynamically via prepareCall instead of at agent creation

* Fix heavy .fig parse: restore GUID/defID guards, fix optional types

Commit f3eac5a removed null guards from fig-import and
instance-overrides as 'unnecessary conditions', but Kiwi-decoded data
can have NodeChanges without GUIDs and ComponentPropAssignments/Refs
without defIDs. Made the types optional to match reality.

Also fix CI LFS: add git lfs install --force before pull (R2 endpoint
needs the filter registered first).

* Fix Google AI model IDs: gemini-3.1-pro → gemini-3.1-pro-preview

Closes #91

---------

Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-03-10 15:43:04 +03:00
Danila Poyarkov e3ab8dab38 AI chat rendering, undo, visual verification, and provider compatibility
Rendering & layout:
- Fix COUNTER_ALIGN_MAP stretch mapping
- Direction-aware fill sizing based on parent flex direction
- Text without explicit width defaults to WIDTH_AND_HEIGHT auto-resize
- Padding auto-enables vertical auto-layout
- clipsContent propagated as Overflow.Hidden to Yoga
- Text height measurement for HEIGHT auto-resize mode
- Export x/y for absolute children, text w/h per textAutoResize
- Export STRETCH as fill on cross axis

Undo system:
- Undo for auto-layout and layer tree reorder
- Atomic undo for drag + reparent
- Page snapshot undo for AI tool operations
- mouseleave no longer terminates active drag

AI tools & vision:
- Add get_jsx, diff_jsx, and describe tools
- export_image returns image via toModelOutput
- Chunked uint8ArrayToBase64 (stack overflow fix)
- computeAllLayouts after AI tool execution
- onBeforeExecute/onAfterExecute receive ToolDef

Provider & UI:
- Add anthropic-compatible provider
- API type toggle for OpenAI-compatible
- Debug toolbar (dev only)
- Preserve chat on UI toggle
- Enhanced system prompt with JSX reference

Co-authored-by: Anton A S <eddclyde@yandex.ru>
2026-03-09 15:49:29 +03:00
Danila Poyarkov f2ef9c976d
XPath query for node selection (#82)
* Add XPath query for node selection

- packages/core/src/xpath.ts: XPath engine using fontoxpath with custom
  DOM facade over SceneGraph nodes
- RPC command: 'query' with selector, page, limit params
- CLI command: 'open-pencil query <file> <xpath>' with --json, --page, --limit
- AI/MCP tool: 'query_nodes' with XPath examples in description
- Queryable attributes: name, width, height, x, y, visible, opacity,
  cornerRadius, fontSize, fontFamily, fontWeight, layoutMode, itemSpacing,
  padding*, strokeWeight, rotation, locked, blendMode, text, lineHeight,
  letterSpacing

* Add tests and docs for XPath query

- 9 unit tests for queryByXPath/matchByXPath core functions
- 6 tool tests for query_nodes AI/MCP tool
- README: query examples in CLI section
- VitePress docs: patterns, queryable attributes, example output
- CHANGELOG: feature entry

* Lazy-load fontoxpath to avoid bundling in desktop app

- Dynamic import() instead of static import for fontoxpath
- queryByXPath/matchByXPath now async (they were sync before)
- RpcCommand.execute allows R | Promise<R> for async commands
- Fix 2 lint errors (unnecessary optional chain, always-truthy)
- Update all tests and CLI to await results
2026-03-09 15:32:27 +03:00
Danila Poyarkov 7f5a388e2b Code review: svg-export/ folder, restore comments, cleanup
- Move svg-export-{defs,paths}.ts into svg-export/ folder
  (defs.ts, paths.ts, index.ts) instead of prefix-based split
- Remove duplicate getChildren lambda in fig-import.ts importPages
- Remove dead dashPattern local in kiwi-convert.ts nodeChangeToProps
- Restore useful explanatory comments in instance-overrides.ts
  (DSD propagation, direct vs cloned assignments, seed skipping)
- Rename applyEffectOverrides → applyShapeAndEffectOverrides
  (handles polygon props + shadow + blur, not just effects)
- Format src/ with oxfmt
2026-03-09 15:22:14 +03:00
Danila Poyarkov 59981709ba Reduce cyclomatic complexity below 20 in all core functions
Refactor 24 complex functions across 14 files by extracting
dispatch branches and property-group handlers into focused helpers.

kiwi/kiwi-convert.ts (4 functions, was 22-81):
- nodeChangeToProps → 6 property-group converters
- convertOverrideToProps → 5 override applicators
- importStyleRuns → 3 style-run helpers
- mapNodeType → NODE_TYPE_MAP lookup table

kiwi/instance-overrides.ts (3 functions, was 28-44):
- applyDerivedSymbolData → resolveDsd* helpers
- applyComponentProperties → assignment/override applicators
- propagateOverridesTransitively → clone/sync helpers

kiwi/fig-import.ts (2 functions, was 34-40):
- importNodeChanges → buildChangeMaps, importPages, etc.
- importVariables → importCollections, resolveVariableType, etc.

render/export-jsx.ts (2 functions, was 70-85):
- collectProps → property-category extractors
- collectTailwindClasses → Tailwind class-group extractors

render/renderer.ts: propsToOverrides (78) → style-category helpers

renderer/scene.ts: renderNode (46), renderShapeUncached (30) →
  phase extractors (transforms, content, children, strokes)

renderer/renderer.ts: buildParagraph (34) → buildTruncateOpts,
  addStyledRuns

svg-export.ts: split into svg-export.ts + svg-export-defs.ts +
  svg-export-paths.ts; renderNode/renderTextNode/nodeShapeElements
  all reduced via helper extraction

kiwi-serialize.ts: sceneNodeToKiwi (44) → 6 property serializers
clipboard.ts: importClipboardNodes (36) → 5 focused helpers
scene-graph.ts: hitTestChildren (31) → containsPoint,
  hitTestOpaqueContainer, hitTestTransparentContainer
tools/analyze.ts: serializeNodeProps (23) → 4 prop serializers
2026-03-09 13:40:57 +03:00
Danila Poyarkov f3eac5ac8b Lint all packages: fix 193 errors across core, mcp, acp
Extend lint coverage from src/ to all packages (178 files, 140 rules).

Core (37 files):
- Remove 98 unnecessary conditions (dead ?./?? on non-nullish values)
- Remove 38 unnecessary type assertions (! and as casts)
- Convert 12 patterns to optional chain (a && a.b → a?.b)
- Fix 10 consistent-type-imports (merge duplicates, inline type keyword)
- Add parentheses to 16 nested ternaries
- Convert 4 indexed for-loops to for-of
- Merge 4 duplicate import statements

MCP (3 files):
- Replace typeof undefined check with 'in' operator
- Capture graph in local const to avoid ! assertions

ACP (1 file):
- Fix 5 TypeScript errors (type predicates, object shapes, dead code)
- Remove unnecessary template expression

Vendored kiwi-schema gets broader rule overrides to avoid touching it.
Core complexity stays as warning (24 functions over limit).
2026-03-09 13:18:08 +03:00
Danila Poyarkov 2f06c65c7c Revert lint plugin to .js (oxlint uses Node.js internally), add new rules
Oxlint's JS plugin loader uses Node.js, not Bun — .ts fails in CI
with ERR_UNKNOWN_FILE_EXTENSION.

New rules:
- no-hand-rolled-color: flags rgba()/rgb() in template literals outside
  color.ts — use colorToCSS()/colorToHex() helpers instead
- typescript/no-unnecessary-boolean-literal-compare
- typescript/no-unnecessary-template-expression
- typescript/no-unnecessary-type-arguments
- unicorn/no-useless-fallback-in-spread
- unicorn/no-length-as-slice-end
- unicorn/no-unnecessary-await
- unicorn/prefer-string-starts-ends-with

Fixed violations: visible !== false → fill.visible in analyze.ts,
removed ?? {} fallbacks in style-runs.ts spread.
2026-03-09 10:05:18 +03:00
Danila Poyarkov c51f34689f
Multi-provider AI, sucrase JSX, Figma zoom shortcuts (#72)
* Add multi-provider AI support (Anthropic, OpenAI, Google AI, OpenAI-compatible)

- Add AI_PROVIDERS registry with per-provider model lists, key placeholders, and URLs
- Refactor use-chat composable: provider factory creates the right AI SDK model
- Per-provider API key storage in localStorage with automatic legacy migration
- New ProviderSetup.vue replaces APIKeySetup.vue with provider selector
- New ProviderSettings.vue popover accessible from gear icon in chat input
- OpenAI-compatible provider with custom base URL and model ID fields
- Install @ai-sdk/anthropic and @ai-sdk/google dependencies
- Update E2E tests for new provider setup flow

* Fix reactivity, key masking, missing model ID field, and chat reset on model change

- Use ref instead of computed for apiKey — computed getter over localStorage
  wasn't triggering reactivity when the key was set
- Load stored key for new provider in providerId watcher
- Reset chat on modelId and customModelId change, not just provider switch
- Add custom model ID field to ProviderSetup.vue for OpenAI-compatible
- Simplify ProviderSettings key field — empty input with contextual placeholder
  instead of fragile dot-masking that broke on partial edits
- Use data-test-id locator for model selector in E2E test

* Use useLocalStorage from vueuse instead of manual ref + watch + localStorage

Replaces 5 hand-rolled ref/watch/localStorage sync pairs with useLocalStorage.
API key uses a computed storage key that rebinds when the provider changes.

* Use useLocalStorage for collab name persistence

* Review fixes: changelog placement, remove dead class, cursor-pointer

* Extract ProviderSelect component and uiInput helper

- ProviderSelect.vue: shared select dropdown for AI providers
- ui/input.ts: shared input styling (sm/md sizes)
- Remove 7 duplicated input class strings across chat components

* Use uppercase acronyms: keyURL, customBaseURL, supportsCustomBaseURL

* Match select dropdown width to trigger via --reka-select-trigger-width

Move min-w-[var(--reka-select-trigger-width)] into selectContent base
style so all selects get it by default. Remove per-component overrides.

* Uppercase acronyms in variable names, improve setup form layout

- providerID, modelID, customModelID, setAPIKey, AIProviderID
- Keep apiKey lowercase at start (standard JS convention)
- Compact setup form: single column, full-width Connect button
- Fix grammar: 'a OpenRouter' → 'an OpenRouter'
- Shorter promo text

* Allow text selection in chat panel

* Fix chat E2E test: Save → Connect button text

* Disable mermaid in chat markdown renderer

Alias mermaid and beautiful-mermaid to empty shims in Vite config,
preventing vue-stream-markdown from attempting to load them.

* Interleave text and tool calls in chat messages

Render message parts in order instead of grouping all tool calls
first then all text. Use SDK's isToolUIPart/isTextUIPart/getToolName
instead of custom type guards. Fix error state: output-error, not error.

* Implement figma.viewport.scrollAndZoomIntoView()

Figma Plugin API method that centers the viewport on given nodes.
Reuses the same bounding box logic as the viewport_zoom_to_fit tool.

* Catch tool execution errors and return them to the AI

Instead of crashing with an unhandled exception, tool errors are
caught and returned as { error: message } so the AI can retry or
explain the failure. UI detects error outputs and shows them in red
with the error message when expanded.

* Replace esbuild with sucrase for JSX transform

Sucrase is a pure JS transform (201 KB / 46 KB gzip) that works in
both Node/Bun and the browser. Replaces esbuild (13 MB WASM) which
only worked in Node/Bun.

- buildComponent() and renderJSX() are now synchronous
- render tool works in browser AI chat (no more 'esbuild required')
- Handles full JS expressions (map, ternaries, Array.from, etc.)

* Update system prompt to prioritize render tool with JSX

Document available tags, props, layout, text, and sizing options.
Instruct the AI to use full JS expressions in JSX for complex layouts.

* Fix zoom shortcuts to match Figma

Cmd+0: Zoom to 100% (was incorrectly mapped to Zoom to fit)
Cmd+1: Zoom to fit
Cmd+2: Zoom to selection
Shift+1 / Shift+2: same as Cmd+1 / Cmd+2

Add zoomTo100() and zoomToSelection() to editor store.
Refactor zoomToFit() to use shared zoomToBounds() helper.

* Refactor keyboard shortcuts to useMagicKeys

Replace manual keydown handler with VueUse useMagicKeys + whenever
for declarative shortcut registration.

- mod() helper for cross-platform Meta/Control shortcuts
- plain() helper for modifier-free keys
- Proper modifier exclusion (⌘G vs ⌘⇧G no longer conflict)
- Add E2E tests: duplicate, zoom (⌘0/⌘1/⌘2/⇧1/⇧2), auto-layout (⇧A)
- All 26 keyboard shortcut tests pass

* Fix scrollAndZoomIntoView to actually zoom

Match Figma Plugin API behavior (equivalent to Shift-1): compute
zoom level that fits all nodes with padding, capped at 100%.
Previously only set center without adjusting zoom.
2026-03-08 22:05:18 +03:00
Danila Poyarkov bd5cdc8eee Centralize all color utilities in color.ts
- Add colorToHex8 (hex with alpha via culori formatHex8)
- Add colorToCSSCompact (no-space rgba for Tailwind shadow classes)
- Add normalizeColor (partial Color → full Color with defaults)
- Add colorDistance (Euclidean RGB via culori differenceEuclidean)
- Replace colorToCSS body with culori formatRgb
- Change rgba255ToColor to positional args (r, g, b, a)
- Remove duplicate formatColor from svg-export.ts and export-jsx.ts
- Remove duplicate colorDistance from tools/analyze.ts
- Remove duplicate convertColor from kiwi/kiwi-convert.ts
- Replace hand-rolled hex+alpha in HsvColorArea.vue with colorToHex8
- Replace hand-rolled rgba255→Color in HsvColorArea.vue with rgba255ToColor
- Expand color.test.ts from 16 to 34 tests
2026-03-07 10:02:11 +03:00
Danila Poyarkov 9784544f1f Clean up tools/schema, add geometry.ts, isMixed helper, fix imports, add tests
- Strip tools/schema.ts from 1831 to 65 lines (remove duplicate tool defs)
- Fix tools/index.ts to export ALL_TOOLS from registry instead of schema
- Fix renderJsx → renderJSX typo in tools/create.ts and structure.ts
- Add geometry.ts with shared rotation/coordinate math utilities
- Add isArrayMixed() helper to use-multi-props.ts, deduplicate FillSection/StrokeSection/EffectsSection
- Fix broken test imports: use @open-pencil/core instead of relative paths into packages/core/src/
- Merge duplicate import statements across all test files
- Export profiler internals and style-run helpers from core index.ts
- Add 6 new test files: color, undo, snap, vector, style-runs, text-editor (98 new tests)
- Update renderer-effects test for renderer/ directory split
2026-03-06 22:33:10 +03:00
Danila Poyarkov 31326e8e5e Eliminate as Record<string, unknown> casts
- Add index signature to NodeChange for Kiwi codec fields
- Add blobs field to FigmaMessage
- Use 'in' type guards instead of Record casts in ai-adapter
- Use Object.fromEntries for Partial<SceneNode> property copying
- Use satisfies instead of as for handle position map
- Type node() to accept { children?: unknown; [key: string]: unknown }
- Remove ext() helper in kiwi-convert (direct property access)

37 occurrences → 1 (Kiwi codec boundary only).
2026-03-06 21:43:32 +03:00
Danila Poyarkov 23be8e3950 Fix missing parseColor and DEFAULT_SHADOW_COLOR imports in schema.ts 2026-03-06 18:29:53 +03:00
Danila Poyarkov 5ae6234e40
Tailwind CSS v4 JSX export (#54)
* Add Tailwind CSS v4 JSX export format

- Add JSXFormat type ('openpencil' | 'tailwind') to sceneNodeToJSX/selectionToJSX
- Tailwind resolver (tailwind.ts): px→spacing (v4 multiplier), hex→color class,
  fontSize/fontWeight/borderRadius named lookups with arbitrary value fallback
- Export: FRAME→div, TEXT→p, SECTION→section, className with TW utility classes
- CLI: export --format jsx --style tailwind
- CodePanel: format toggle button (OpenPencil / Tailwind)
- Rename Jsx→JSX in all public APIs (sceneNodeToJSX, selectionToJSX, renderJSX, etc.)
- 29 new Tailwind export tests, all 43 export tests pass

* Extract escapeJSXText — replace nested ternary with entity map lookup

* Deduplicate export-jsx: extract shared padding, corner radius, and node context helpers

* Update changelog

* Tighten Tailwind JSX export fidelity
2026-03-06 18:04:11 +03:00
Danila Poyarkov a2d7b89709 Adopt motion-v for mobile drawer and toolbar animations
- MobileDrawer: spring-animated height via motion.div :animate + @pan,
  replaces useSwipe + manual rAF. Tab state always set (never null),
  content stays rendered when drawer is closed.
- Toolbar: AnimatePresence with directional slide variants and
  layout-animated width via motion.div layout, replaces manual
  scrollWidth measuring + inline CSS transitions.
- Add SWIPE_VELOCITY_THRESHOLD constant.
- activeRibbonTab type narrowed from union | null to union.
2026-03-06 08:50:38 +03:00
Danila Poyarkov 661b83e749 Port analyze/diff tools from figma-use, split tools into domain files
Analyze: colors, typography, spacing, clusters
Diff: diff_create (tree diff), diff_show (preview changes)
Utility: get_components, get_current_page, arrange, node_to_component

Split schema.ts (2600 lines) into read, create, modify, structure,
variables, vector, analyze, registry — each under 600 lines.

Clean up inline types (use Color, Vector, SceneNode from existing defs).
Update AGENTS.md and CONTRIBUTING.md with code quality guidelines.
2026-03-05 19:16:23 +03:00
Danila Poyarkov ed61445e2d
SVG export (#52)
* Add SVG export

Scene graph → SVG serializer with support for rectangles, ellipses,
lines, stars, polygons, vectors (fill/stroke geometry and vector
networks), text with style runs, gradients (linear, radial, angular),
image fills, effects (drop shadow, inner shadow, blur), opacity,
rotation, flips, blend modes, clip paths, and nested groups.

- packages/core: svg-node.ts (minimal XML builder), svg-export.ts
  (serializer), ExportFormat now includes 'SVG', computeContentBounds
  extracted for reuse
- packages/cli: export command accepts --format svg
- UI: SVG added to ExportSection format picker (scale hidden for SVG),
  context menu 'Export as SVG' option
- 47 new tests covering SVGNode builder, geometry blob → path, vector
  network → path, and full node export for all shape types

* Update changelog

* Add export_svg tool to ToolDefs

Available via MCP, AI chat, and CLI eval command.

* Replace standalone export items with Copy/Paste as submenu

Figma-style submenu: Copy as text, Copy as SVG, Copy as PNG (⇧⌘C),
Copy as JSX. Keeps Export as PNG (⇧⌘E) as a separate item.

* Remove standalone Export as PNG from context menu

* Update changelog
2026-03-05 10:31:02 +03:00
Danila Poyarkov 3199abb4e6 Fix render tool on Windows: add .js extension to dynamic imports
ESM dynamic imports require explicit file extensions for resolution on
Windows + Bun. The extensionless '../render/render-jsx' path worked on
macOS/Linux but failed on Windows with 'Cannot find module' error.

Fixes #43
2026-03-05 09:19:15 +03:00
Danila Poyarkov 4ee3a2dcc3 Add set_text_properties and set_layout_child tools (from #11)
Cherry-picked the 2 unique tools from PR #11:
- set_text_properties: alignment, auto-resize, decoration
- set_layout_child: sizing, grow, align_self, positioning

The other 10 tools in the PR were already covered by the
port-tools merge (73 → 75 tools).
2026-03-02 21:12:44 +03:00