Commit graph

432 commits

Author SHA1 Message Date
Danila Poyarkov f44f06f721 Refactor: extract business logic from components, clean composables
Tier 1 — Core API fixes:
- updateNodeWithUndo now calls requestRender internally, eliminating
  15+ redundant requestRender() calls across 6 property panel components
- New editor/variables.ts: 7 undo-able variable CRUD operations
  (renameCollection, addCollection, removeCollection, addVariable,
  removeVariable, renameVariable, updateVariableValue)
- New editor/alignment.ts: alignNodes, flipNodes, rotateNodes —
  moves bounding box geometry out of PositionSection.vue

Tier 2 — Composable cleanup:
- Merged use-node-props + use-multi-props into single useNodeProps()
  with MIXED sentinel, eliminating duplicate computed refs
- Added toolCursor() utility — replaces if/else chain in EditorCanvas
- Renamed use-toast.ts → toast.ts (singleton module, not a composable)

Tier 3 — use-canvas-input.ts split (1497 → 817 lines):
- input/types.ts (88) — DragState variants, HandlePosition, TOOL_TO_NODE
- input/geometry.ts (170) — hit testing, handle positions, rotation cursor
- input/pan-zoom.ts (266) — wheel, touch pinch, Safari gestures
- input/resize.ts (121) — constrainToAspectRatio, applyResize
- input/auto-layout.ts (103) — insert indicator computation

All 955 tests pass, 0 lint errors, 1.48% duplication.
2026-03-16 20:35:17 +03:00
Danila Poyarkov 608be0ea27 Move composables to @open-pencil/vue SDK, unify Editor type
Phase 2 of SDK refactor:

Composables moved to packages/vue/src/composables/:
- use-canvas-input (1497 lines) — mouse/touch/gesture interaction
- use-text-edit (340 lines) — inline text editing
- use-canvas-drop (68 lines) — image drag-and-drop
- use-node-props (55 lines) — single/multi node property editing
- use-multi-props (106 lines) — multi-selection property merging
- use-inline-rename (58 lines) — inline rename input
- use-font-status (24 lines) — font loading status
- use-toast (39 lines) — toast notifications

All retyped from EditorStore to Editor (core type). App composables
are now 1-2 line re-export shims from @open-pencil/vue.

Also moved to core Editor:
- placeImageFiles / placeImageNode / decodeImageDimensions
- Interaction constants (PEN_CLOSE_THRESHOLD, ROTATION_SNAP_DEGREES, etc.)

Removed dead exports: HANDLE_SIZE, DRAG_DEAD_ZONE, ACP_DESIGN_CONTEXT.
2026-03-16 17:41:07 +03:00
Danila Poyarkov 8cdb4db7ee Add @open-pencil/vue headless SDK package
Renderless components (Reka UI-style) exposing editor logic via slots:
- OpenPencilProvider — injects editor via Vue provide/inject
- OpenPencilCanvas — CanvasKit/Skia surface with render loop
- PageList, LayerTree, ToolSelector, NodeProperties — slot-based

Composables:
- useEditor() — access editor from any descendant
- useCanvas() — low-level surface lifecycle (moved from app)

The app's use-canvas.ts is now a 2-line re-export from the SDK.
Includes example app in packages/vue/example/.
2026-03-16 17:24:45 +03:00
Danila Poyarkov 99d2703583 Extract createEditor() to packages/core/src/editor/ folder
Split the 2505-line god store into 13 domain modules sharing an
EditorContext interface:

  editor/types.ts       — EditorState, EditorOptions, Tool, EditorToolDef
  editor/create.ts      — createEditor() assembler
  editor/viewport.ts    — screenToCanvas, applyZoom, pan, zoom*
  editor/selection.ts   — select, clearSelection, marquee, snap, hover
  editor/pages.ts       — switchPage, addPage, deletePage, renamePage
  editor/shapes.ts      — createShape, pen tool, adoptNodesIntoSection
  editor/structure.ts   — group, ungroup, reorder, reparent, z-order
  editor/components.ts  — component/instance/detach/componentSet
  editor/clipboard.ts   — duplicate, copy, paste, delete
  editor/undo.ts        — commitMove/Resize/Rotation, snapshot
  editor/text.ts        — startTextEditing, commitTextEdit
  editor/nodes.ts       — updateNode, setLayoutMode

Move design constants (fills, strokes, zoom) from app to core.
Largest file is structure.ts at 394 lines (was 2505).
App store is now a thin Vue wrapper (747 lines) delegating to
the core Editor — ready for @open-pencil/vue SDK consumption.

All 955 tests pass, 0 lint errors, 1.41% duplication.
2026-03-16 15:14:50 +03:00
Danila Poyarkov f14f0a4259 Fix review issues: undoable lock/visibility toggles, use degToRad 2026-03-16 14:29:26 +03:00
Anton A S edd9a8e398 Entered container border, rotation support, INSTANCE/COMPONENT enterable, tests 2026-03-16 14:26:52 +03:00
Anton A S 6d3e2d20cd Scope checklist: marquee in scope, layer panel sync, delete locked guard, undo/redo validation, tests 2026-03-16 14:26:52 +03:00
Anton A S 1b743bec94 Add selection scope: enter containers with dblclick, exit with Escape or click-outside 2026-03-16 14:26:52 +03:00
Anton A S 19d6e01960 Fix double-click enter: offset coords to group-local space 2026-03-16 14:26:52 +03:00
Anton A S 01235c0d84 Fix group selection: single click selects group, double click enters, locked groups block enter 2026-03-16 14:26:52 +03:00
Anton A S 116acb5cfa Make lock/visibility icons more visible 2026-03-16 14:26:52 +03:00
Anton A S 6596702389 Fix lock: block move/resize/rotate for locked nodes, skip locked in marquee, improve toggle icon visibility 2026-03-16 14:26:52 +03:00
Anton A S bc85d73cc8 Add lock and visibility toggles to layers panel 2026-03-16 14:26:52 +03:00
Danila Poyarkov 8d7dd0a36b Add random.ts helpers, replace all raw crypto.getRandomValues calls
New module packages/core/src/random.ts with randomHex, randomInt,
randomIndex — centralizes all crypto-based randomness.

Replaces:
- Date.now() IDs in VariablesDialog (not unique, not random)
- Hand-rolled hex token in automation/server.ts
- Inline crypto.getRandomValues in clipboard.ts, use-collab.ts

Also adds --color-component Tailwind theme color (#9747ff) and
replaces all 6 hardcoded occurrences across DesignPanel, LayerTree,
NodeContextMenuContent, and menu.ts.
2026-03-16 14:13:36 +03:00
Danila Poyarkov 921d2d35c5 Export IS_BROWSER from core, replace all raw typeof window checks
- Add IS_BROWSER constant to core/constants.ts, rewrite IS_TAURI to use it
- Replace 9 raw 'typeof window' checks across canvaskit, fig-export,
  figma-api, fig-file, fonts, SafariBanner, use-chat
- Promote no-typeof-window-check lint rule from warn to error
- Extract array mutation helpers (updateArrayItem, removeArrayItem,
  toggleArrayVisibility) into useMultiProps — DRY up FillSection and
  StrokeSection (removes 20+ duplicated for-loops)
2026-03-16 14:05:24 +03:00
Danila Poyarkov c06ed32990 Harden linting: add eqeqeq, prefer-const, radix, no-console, no-typeof-window-check
New built-in rules:
- eqeqeq (null-safe) — prevent loose equality coercions
- prefer-const — catch unused let declarations
- radix — require parseInt radix parameter
- no-console — warn on console.log in app code (CLI/MCP exempted)

New custom rule:
- no-typeof-window-check — flag raw typeof window checks, prefer
  IS_BROWSER/IS_TAURI constants (warn-level, constants.ts + fonts.ts
  exempted)

Fix 4 violations caught by new rules.
2026-03-16 13:54:07 +03:00
Danila Poyarkov b3d3c0a00b Use collab injection in MobileHud instead of prop drilling
MobileHud now uses useCollabInjected() directly — same pattern as
CollabPanel — and handles share/join/disconnect internally.

Removes broken event handlers that were never defined in EditorView.
2026-03-16 13:27:17 +03:00
Danila Poyarkov 6a40e4e3e3 Disable automation WebSocket in production web app
Only connect to the local automation bridge in Tauri desktop or
local dev, matching the existing spawnMCPIfNeeded guard.

Fixes #126
2026-03-16 13:27:17 +03:00
Danila Poyarkov 12d3b60e69 Clean up ACP branch: gate debug log, fix capitalizations, harden types
- Gate acpDebugLog behind import.meta.env.DEV (no logging in production)
- Remove saveAcpDebugLog (Desktop hardcode) — clipboard copy is sufficient
- Hide ACP log button unless in DEV mode with entries
- Fix textFromContent: proper type guards instead of unsafe casts
- Fix trailing empty line in server.ts handleBrowserMessage
- Rename Acp → ACP, Mcp → MCP in all identifiers:
  AcpPermissionDialog → ACPPermissionDialog (file + component)
  AcpDebugEntry → ACPDebugEntry
  spawnMcpIfNeeded → spawnMCPIfNeeded
  checkMcpHealth → checkMCPHealth
  McpContent/McpResult/McpTransport → MCPContent/MCPResult/MCPTransport
  createMcpSession → createMCPSession
2026-03-15 18:24:37 +03:00
Anton A S 63f3ef3755 Show agent name in model selector for ACP, fix model dropdown, update docs 2026-03-15 18:13:29 +03:00
Anton A S 55ba13b43b Use full system prompt for ACP agents instead of short context 2026-03-15 18:06:15 +03:00
Anton A S 6fcb29686d Save ACP debug log to Desktop, keep last 5 minutes 2026-03-15 17:47:32 +03:00
Anton A S 83f7c53fee Add ACP debug log with copy button in chat toolbar 2026-03-15 17:45:14 +03:00
Anton A S 206cb3742f Debug: log ACP session updates 2026-03-15 17:42:50 +03:00
Anton A S aaae074223 Fix ACP settings: show stock keys for all providers, always show Clear button 2026-03-15 17:42:01 +03:00
Anton A S a76d20b27b Fix ACP agent commands: claude-agent-acp, remove stale ts-expect-error 2026-03-15 17:06:57 +03:00
Anton A S 583b03eb3b Harden ACP transport, MCP server, and add permission dialog
- Extract mapUpdate to testable module, dynamic import for @tauri-apps/plugin-shell
- Add warnings for unhandled ACP content types and empty tool titles
- MCP server: session limit (max 10), fix null WS comparison, guard JSX preprocessing
- MCP server: read version from package.json instead of hardcoded 0.0.0
- MCP tests: use port 0 (OS-assigned) to prevent collision
- Connection error handling with user-friendly messages, stale session recovery
- Agent crash detection via close handler, destroying flag, buildCrashChunks
- Port collision: detect EADDRINUSE in vite-plugin stderr and log clear error
- Production Tauri: spawn openpencil-mcp via shell plugin, orphan reuse via health check
- Permission confirmation dialog (reka-ui AlertDialog) with queue, 60s auto-reject timeout
- Health check in ProviderSelect hides ACP agents when MCP server unavailable
- Move DESIGN_CONTEXT to app constants, add ACP_PERMISSION_TIMEOUT_MS
- 36 tests across 3 files (acp-transport, acp-permission, mcp-server)
- Update CHANGELOG, README, CONTRIBUTING, AGENTS.md
2026-03-15 16:49:43 +03:00
Danila Poyarkov dfb050538c Spawn MCP server as subprocess instead of SSR module
Replace ssrLoadModule hack with simple child_process.spawn.
Same approach as production (Tauri sidecar) — just run the MCP
server entry point directly. No re-evaluation on HMR, no
WebSocket reconnect storms, no globalThis guards.
2026-03-15 16:42:49 +03:00
Danila Poyarkov 2aa6d7f607 Fix bridge loading: use ssrLoadModule for workspace TS resolution 2026-03-15 16:42:48 +03:00
Danila Poyarkov 1092e6b9f5 Unify MCP server: always proxy to live editor via WebSocket
Remove headless SceneGraph mode — the MCP server now always proxies
tool calls to the browser via WebSocket. One server, one architecture:

  HTTP :7600 — /health, /rpc (CLI), /mcp (MCP Streamable HTTP)
  WS   :7601 — browser connects, executes tool calls against live editor

- Delete http.ts (merged into server.ts)
- Delete headless SceneGraph, open_file, save_file, new_document
- Delete canvaskit-wasm dependency (no headless rendering)
- Add ws dependency for WebSocket server
- Rewrite tests with mock browser over real HTTP+WebSocket
- Simplify bridge.ts to 6 lines (just imports and starts the server)
2026-03-15 16:42:46 +03:00
Danila Poyarkov 5641554ab7 Clean up automation bridge: direct imports, upgrade zod to v4
- Replace ssrLoadModule hacks with direct imports from @open-pencil/core
- Replace isBunRuntime() hack with @hono/node-server (works everywhere)
- Remove ViteServer type parameter from startAutomationBridge
- Upgrade @open-pencil/mcp zod dependency from v3 to v4
- Add @open-pencil/mcp as root devDependency for bridge imports
2026-03-15 16:40:41 +03:00
Danila Poyarkov fea1f4f95d Fix MCP bridge SSR module resolution
ssrLoadModule('@open-pencil/mcp') fails because Vite can't resolve
workspace TypeScript packages. Use relative path to source instead.
2026-03-15 16:40:39 +03:00
Danila Poyarkov 3d098b8a13 Remove shiki shim — handled by forked vue-stream-markdown
The fork inlines shiki stubs at build time, so consumers no longer
need to alias shiki to a shim.
2026-03-15 16:40:38 +03:00
Danila Poyarkov 4f0c11e275 Export shiki stubs from shim to prevent undefined destructuring
The shim must export everything vue-stream-markdown destructures
from import('shiki'): bundledThemesInfo, bundledLanguagesInfo,
createHighlighter. A throwing shim causes the import to succeed
but all named exports to be undefined.
2026-03-15 16:40:37 +03:00
Danila Poyarkov 758ddde8d8 Fix ACP stop button: close stream immediately on abort
The cancel request was sent to the agent but the stream stayed open
until prompt() resolved, leaving the UI in streaming state. Now the
stream closes immediately with a finish chunk, guarded against
double-close from the concurrent prompt resolution.
2026-03-15 16:40:36 +03:00
Danila Poyarkov ace8c26f2e Use forked vue-stream-markdown with shiki optional chaining fix
Replace npm vue-stream-markdown with open-pencil fork that guards
bundledThemesInfo/bundledLanguagesInfo with ?. to prevent TypeError
when shiki is not installed. Revert shiki shim and cdn-options hacks.
2026-03-15 16:40:35 +03:00
Danila Poyarkov 660bb3aab6 Fix shiki shim: export stubs instead of throwing
The throwing shim caused hasShikiModule() to return false, but
import('shiki') still resolved in some code paths where the module
was imported directly without the has-check, causing undefined
destructuring and the 'e.find' TypeError.
2026-03-15 16:40:33 +03:00
Danila Poyarkov 19505665f5 Add ACP transport unit tests for mapUpdate chunk mapping
Tests cover: text deltas (empty skip, start/continue), reasoning
chunks, tool call lifecycle (input/output/error), title fallback,
and unhandled update types.
2026-03-15 16:40:31 +03:00
Danila Poyarkov b71a3523d2 Inject design context into first ACP prompt
Without context, the agent uses its own file/terminal tools instead
of the MCP design tools. Prepend instructions on the first message
telling it to use open-pencil MCP tools to draw on the canvas.
2026-03-15 16:40:28 +03:00
Danila Poyarkov ff78d8d57b Bridge MCP server to live editor via automation WebSocket
ACP agents now get an MCP server at http://127.0.0.1:7600/mcp that
proxies tool calls through the automation WebSocket to the browser,
where they execute against the live editor store. No separate
subprocess, no stale npm package — tools operate on the open canvas.

- Add /mcp endpoint to automation bridge (Streamable HTTP transport)
- Each MCP tool call → sendToBrowser({command:'tool'}) → browser executes
- Strip mcp__open-pencil__ prefix from tool names in chat UI
- Export paramToZod from MCP package for bridge reuse
2026-03-15 16:40:27 +03:00
Danila Poyarkov e79fb5ac5d Connect ACP agents to OpenPencil MCP tools
Pass openpencil-mcp as a stdio MCP server to the ACP session so
agents get all design tools (create shapes, render JSX, export
images, etc.) instead of just plain conversation.

- Add makeFigma option to createServer for external FigmaAPI injection
- Skip file lifecycle tools (open/save/new) when using external FigmaAPI
- Pass MCP server config in ACP newSession mcpServers
- Add bun to Tauri shell scope for MCP server subprocess
2026-03-15 16:40:26 +03:00
Danila Poyarkov f02bd84aec Fix ACP transport: Uint8Array conversion, empty deltas, shiki error
- Convert Tauri shell stdout data (number[]) to Uint8Array for
  ndJsonStream compatibility
- Skip empty text deltas from agent_message_chunk
- Disable shiki CDN in Markdown component (not installed, caused
  'e.find' TypeError on inline code)
- Remove debug console.log statements
2026-03-15 16:40:24 +03:00
Danila Poyarkov 2109bd6605 Fix ACP shell scope config and surface connection errors
- Add scoped command definitions for claude-code-acp, codex-acp, gemini
  in Tauri shell plugin capabilities (required for Command.create)
- Show error banner in ChatPanel when transport initialization fails
  instead of silently swallowing the error
2026-03-15 16:40:23 +03:00
Danila Poyarkov 7adac1b270 Fix lint errors in ACP transport and merge master 2026-03-15 16:40:22 +03:00
Danila Poyarkov 30430433c6 Add ACP client support for using external coding agents
Implement Agent Client Protocol (ACP) integration so users can use their
existing Claude Code, Codex, or Gemini CLI subscriptions directly in
OpenPencil's AI chat instead of managing separate API keys.

- Add ACPAgentDef/ACPAgentID types and ACP_AGENTS registry to core constants
- Create ACPChatTransport implementing Vercel AI SDK ChatTransport over ACP
- Spawn agent as subprocess via tauri-plugin-shell, communicate over stdio
- Map ACP session/update notifications to UIMessageChunk stream
- Update provider selector to show ACP agents alongside API-key providers
- Hide API key fields for ACP providers in setup/settings UI
- Add tauri-plugin-shell dependency and capability permissions
- ACP agents only appear in Tauri desktop builds (subprocess required)
2026-03-15 16:40:18 +03:00
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
Danila Poyarkov 4caf4281fd Fix file open dialog not working on first click in Safari
Replace hand-rolled input element creation with VueUse's useFileDialog,
which creates the input once and reuses it. Creating a new <input>
element and calling .click() immediately was being swallowed by Safari
on the first attempt.
2026-03-14 10:46:51 +03:00
Danila Poyarkov 512819c859 Disable autosave by default 2026-03-14 10:39:35 +03:00
Anton A S 402d791093 Add mobile UI example to system prompt 2026-03-13 14:56:50 +03:00