From 7f5a388e2be5badcb35fad4ca1e73e8f62a59bb8 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 9 Mar 2026 15:22:14 +0300 Subject: [PATCH] Code review: svg-export/ folder, restore comments, cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .pi/context.md | 110 ++++++++++++++++++ packages/core/src/kiwi/fig-import.ts | 6 +- packages/core/src/kiwi/instance-overrides.ts | 7 ++ packages/core/src/kiwi/kiwi-convert.ts | 4 +- packages/core/src/render/renderer.ts | 4 +- .../defs.ts} | 12 +- .../{svg-export.ts => svg-export/index.ts} | 16 +-- .../paths.ts} | 2 +- packages/core/src/tools/vector.ts | 2 +- src/automation/bridge.ts | 4 +- src/automation/server.ts | 4 +- src/components/ChatPanel.vue | 4 +- src/components/EditorCanvas.vue | 5 +- src/components/FillPicker.vue | 4 +- src/components/FontPicker.vue | 3 +- src/components/LayerTree.vue | 5 +- src/components/MobileHud.vue | 4 +- src/components/ScrubInput.vue | 12 +- src/components/chat/ChatMessage.vue | 31 +++-- src/components/chat/ProviderSetup.vue | 5 +- src/components/properties/LayoutSection.vue | 17 +-- src/composables/use-canvas.ts | 2 +- src/composables/use-chat.ts | 2 - src/composables/use-inline-rename.ts | 2 +- src/composables/use-menu.ts | 2 +- 25 files changed, 194 insertions(+), 75 deletions(-) create mode 100644 .pi/context.md rename packages/core/src/{svg-export-defs.ts => svg-export/defs.ts} (95%) rename packages/core/src/{svg-export.ts => svg-export/index.ts} (97%) rename packages/core/src/{svg-export-paths.ts => svg-export/paths.ts} (99%) diff --git a/.pi/context.md b/.pi/context.md new file mode 100644 index 000000000..9077390c2 --- /dev/null +++ b/.pi/context.md @@ -0,0 +1,110 @@ +# Code Context + +## Files Retrieved +1. `packages/core/src/tools/analyze.ts` (lines 1-391) - Full file: analyze tools, diff tools, eval tool +2. `packages/core/src/tools/vector.ts` (lines 1-264) - Full file: boolean ops, path ops, viewport, SVG/image export +3. `packages/core/src/tools/ai-adapter.ts` (lines 1-96) - Full file: Vercel AI SDK adapter +4. `packages/core/src/tools/read.ts` (lines 1-183) - Full file: query/read tools +5. `packages/core/src/tools/registry.ts` (lines 1-100) - Full file: ALL_TOOLS array + +## analyze.ts — Tool Definitions + +| Line | Tool Name | +|------|-----------| +| 128 | `analyze_colors` | +| 197 | `analyze_typography` | +| 251 | `analyze_spacing` | +| 295 | `analyze_clusters` | +| 356 | `diff_create` (under "Diff tools" section) | +| 398 | `diff_show` | +| 449 | `eval` | + +**No `describe` tool exists** in analyze.ts or anywhere in the tools directory. + +## read.ts — Tool Names + +| Tool Name | Description | +|-----------|-------------| +| `get_selection` | Currently selected nodes | +| `get_page_tree` | Node tree of current page | +| `get_node` | Detailed properties by ID | +| `find_nodes` | Find by name/type | +| `get_components` | List components | +| `list_pages` | List all pages | +| `switch_page` | Switch page by name/ID | +| `get_current_page` | Current page name/ID | +| `page_bounds` | Bounding box of all page objects | +| `select_nodes` | Select nodes by ID | +| `list_fonts` | Fonts used on current page | + +**Neither `get_jsx` nor `diff_jsx` exist** in read.ts or any other tool file. + +## vector.ts — exportImage Tool (line ~225) + +- **Default format:** `'PNG'` +- **Default scale:** `1` (min: 0.1, max: 4) +- **Base64 conversion:** Dual-path: + - Node.js: `Buffer.from(data).toString('base64')` + - Browser: `btoa(String.fromCharCode(...data))` +- Returns: `{ mimeType, base64, byteLength }` +- Falls back to error if `figma.exportImage` is unavailable + +## ai-adapter.ts — Full Contents + +- **`AIAdapterOptions`** interface: `getFigma()`, `onBeforeExecute?`, `onAfterExecute?`, `onFlashNodes?` +- **`toolsToAI()`** function: Converts `ToolDef[]` → Vercel AI SDK `tool()` objects + - Accepts deps: `{ v: valibot, valibotSchema, tool }` (lazy imports) + - Builds valibot schemas from `ParamDef` type declarations + - Wraps each `execute` with before/after hooks and error catching + - If tool `mutates` and returns node IDs, calls `onFlashNodes` for visual feedback +- **`paramToValibot()`**: Maps param types → valibot schemas + - `string` → `v.string()` or `v.picklist()` if enum + - `number` → `v.number()` with optional `minValue`/`maxValue` pipes + - `boolean` → `v.boolean()` + - `color` → `v.string()` with description + - `string[]` → `v.array(v.string())` with `minLength(1)` + - Non-required params wrapped in `v.optional()` +- **`extractNodeIds()`**: Extracts IDs from result objects (handles `id`, `selection[]`, `results[]`) + +## registry.ts — ALL_TOOLS Array (84 tools total) + +**Read (11):** get_selection, get_page_tree, get_node, find_nodes, get_components, list_pages, switch_page, get_current_page, page_bounds, select_nodes, list_fonts + +**Create (7):** createShape, render, createComponent, createInstance, createPage, createVector, createSlice + +**Modify (20):** setFill, setStroke, setEffects, updateNode, setLayout, setConstraints, setRotation, setOpacity, setRadius, setMinMax, setText, setFont, setFontRange, setTextResize, setVisible, setBlend, setLocked, setStrokeAlign, setTextProperties, setLayoutChild + +**Structure (17):** deleteNode, cloneNode, renameNode, reparentNode, groupNodes, ungroupNode, flattenNodes, nodeToComponent, nodeBounds, nodeMove, nodeResize, nodeAncestors, nodeChildren, nodeTree, nodeBindings, nodeReplaceWith, arrangeNodes + +**Variables (11):** listVariables, listCollections, getVariable, findVariables, createVariable, setVariable, deleteVariable, bindVariable, getCollection, createCollection, deleteCollection + +**Vector & Export (14):** booleanUnion, booleanSubtract, booleanIntersect, booleanExclude, pathGet, pathSet, pathScale, pathFlip, pathMove, viewportGet, viewportSet, viewportZoomToFit, exportSvg, exportImage + +**Analyze & Diff (6):** analyzeColors, analyzeTypography, analyzeSpacing, analyzeClusters, diffCreate, diffShow + +**Eval (1):** evalCode + +## Architecture + +``` +ToolDef (schema.ts) -- type-safe tool definition with params + execute + ├── read.ts -- query tools (selection, find, pages, fonts) + ├── create.ts -- shape/component/page creation + ├── modify.ts -- property setters + ├── structure.ts -- tree operations + ├── variables.ts -- variable/collection CRUD + ├── vector.ts -- boolean ops, paths, viewport, export + └── analyze.ts -- analysis, diff, eval + │ +registry.ts -- assembles ALL_TOOLS array + │ +ai-adapter.ts -- toolsToAI() → Vercel AI SDK tool() objects + (valibot schemas, before/after hooks, flash nodes) +``` + +Each `defineTool()` creates a `ToolDef` with typed params. The `registry.ts` collects all into `ALL_TOOLS[]`. The `ai-adapter.ts` converts them to Vercel AI SDK format with valibot validation schemas, injecting `FigmaAPI` at execution time via `getFigma()`. + +## Start Here + +- **`packages/core/src/tools/registry.ts`** — the central manifest of all 84 tools, organized by domain. Shows the full tool surface and imports from all domain files. +- **`packages/core/src/tools/ai-adapter.ts`** — the bridge between tool definitions and the AI chat system. Key to understanding how tools are exposed to LLMs. diff --git a/packages/core/src/kiwi/fig-import.ts b/packages/core/src/kiwi/fig-import.ts index 4b73e698a..295a119c6 100644 --- a/packages/core/src/kiwi/fig-import.ts +++ b/packages/core/src/kiwi/fig-import.ts @@ -161,8 +161,6 @@ function importPages( created: Set, createSceneNode: (ncId: string, graphParentId: string) => void ): void { - const getChildren = (ncId: string): string[] => childrenMap.get(ncId) ?? [] - let docId: string | null = null for (const [id, nc] of changeMap) { if (nc.type === 'DOCUMENT' || id === '0:0') { @@ -172,14 +170,14 @@ function importPages( } if (docId) { - for (const canvasId of getChildren(docId)) { + for (const canvasId of childrenMap.get(docId) ?? []) { const canvasNc = changeMap.get(canvasId) if (!canvasNc) continue if (canvasNc.type === 'CANVAS') { const page = graph.addPage(canvasNc.name ?? 'Page') if (canvasNc.internalOnly) page.internalOnly = true created.add(canvasId) - for (const childId of getChildren(canvasId)) { + for (const childId of childrenMap.get(canvasId) ?? []) { createSceneNode(childId, page.id) } } else { diff --git a/packages/core/src/kiwi/instance-overrides.ts b/packages/core/src/kiwi/instance-overrides.ts index 1596dac7a..a2b4b5605 100644 --- a/packages/core/src/kiwi/instance-overrides.ts +++ b/packages/core/src/kiwi/instance-overrides.ts @@ -270,6 +270,7 @@ export function populateAndApplyOverrides( ) { for (const node of graph.getAllNodes()) { if (node.type !== 'INSTANCE') continue + // Apply assignments from the instance's own kiwi data first const ownFigmaId = nodeIdToGuid.get(node.id) if (ownFigmaId) { const ownAssignments = assignmentSources.get(ownFigmaId) @@ -278,6 +279,8 @@ export function populateAndApplyOverrides( } } + // Also apply from cloned instance sources — after population, cloned + // instances have componentId pointing to the original kiwi node if (!node.componentId) continue const sourceFigmaId = nodeIdToGuid.get(node.componentId) if (!sourceFigmaId) continue @@ -459,6 +462,9 @@ export function populateAndApplyOverrides( return { dsdModified, dsdSizeSet } } + // Propagate DSD changes through clone chains — each clone should match + // its source for size/position/geometry. Nodes whose size was explicitly + // set by DSD keep their own values; others inherit from their source. function propagateDsdChanges(dsdModified: Set, dsdSizeSet: Set) { if (dsdModified.size === 0) return @@ -643,6 +649,7 @@ export function populateAndApplyOverrides( const node = graph.getNode(cloneId) if (!node) continue + // Don't overwrite nodes directly targeted by symbolOverrides if (seeds.has(cloneId)) { syncQueue.push(cloneId) continue diff --git a/packages/core/src/kiwi/kiwi-convert.ts b/packages/core/src/kiwi/kiwi-convert.ts index b342be0b0..1a3cbe681 100644 --- a/packages/core/src/kiwi/kiwi-convert.ts +++ b/packages/core/src/kiwi/kiwi-convert.ts @@ -557,8 +557,6 @@ export function nodeChangeToProps( let nodeType = mapNodeType(nc.type) if (nodeType === 'FRAME' && isComponentSet(nc)) nodeType = 'COMPONENT_SET' - const dashPattern = nc.dashPattern ?? [] - return { nodeType, name: nc.name ?? nodeType, @@ -568,7 +566,7 @@ export function nodeChangeToProps( locked: nc.locked ?? false, blendMode: (nc.blendMode as Fill['blendMode']) ?? 'PASS_THROUGH', fills: convertFills(nc.fillPaints), - strokes: convertStrokes(nc.strokePaints, nc.strokeWeight, nc.strokeAlign, nc.strokeCap, nc.strokeJoin, dashPattern), + strokes: convertStrokes(nc.strokePaints, nc.strokeWeight, nc.strokeAlign, nc.strokeCap, nc.strokeJoin, nc.dashPattern ?? []), effects: convertEffects(nc.effects), ...convertCornerProps(nc), ...convertTextProps(nc), diff --git a/packages/core/src/render/renderer.ts b/packages/core/src/render/renderer.ts index ef1877063..f7cbdf767 100644 --- a/packages/core/src/render/renderer.ts +++ b/packages/core/src/render/renderer.ts @@ -274,7 +274,7 @@ function applyTextOverrides(props: Record, o: Partial, o: Partial): void { +function applyShapeAndEffectOverrides(props: Record, o: Partial): void { if (props.points !== undefined) o.pointCount = props.points as number if (props.innerRadius !== undefined) o.starInnerRadius = props.innerRadius as number if (props.pointCount !== undefined) o.pointCount = props.pointCount as number @@ -321,7 +321,7 @@ function propsToOverrides(props: Record, isText: boolean): Part applyVisualOverrides(props, o) applyLayoutOverrides(props, o, w, h) if (isText) applyTextOverrides(props, o) - applyEffectOverrides(props, o) + applyShapeAndEffectOverrides(props, o) return o } diff --git a/packages/core/src/svg-export-defs.ts b/packages/core/src/svg-export/defs.ts similarity index 95% rename from packages/core/src/svg-export-defs.ts rename to packages/core/src/svg-export/defs.ts index 0047f07df..72007965a 100644 --- a/packages/core/src/svg-export-defs.ts +++ b/packages/core/src/svg-export/defs.ts @@ -1,11 +1,11 @@ -import { colorToHex, colorToHex8 } from './color' -import { round } from './svg-export-paths' +import { colorToHex, colorToHex8 } from '../color' +import { round } from './paths' -import { svg } from './svg-node' +import { svg } from '../svg-node' -import type { SVGNode } from './svg-node' -import type { SceneGraph, SceneNode, Fill, Effect } from './scene-graph' -import type { Color } from './types' +import type { SVGNode } from '../svg-node' +import type { SceneGraph, SceneNode, Fill, Effect } from '../scene-graph' +import type { Color } from '../types' export interface SVGExportContext { defs: SVGNode[] diff --git a/packages/core/src/svg-export.ts b/packages/core/src/svg-export/index.ts similarity index 97% rename from packages/core/src/svg-export.ts rename to packages/core/src/svg-export/index.ts index 111433fa0..c6c490044 100644 --- a/packages/core/src/svg-export.ts +++ b/packages/core/src/svg-export/index.ts @@ -1,4 +1,4 @@ -import { computeContentBounds } from './render-image' +import { computeContentBounds } from '../render-image' import { round, geometryBlobToSVGPath, @@ -7,7 +7,7 @@ import { hasRadius, roundedRectPath, arcPath -} from './svg-export-paths' +} from './paths' import { nextDefId, formatColor, @@ -16,15 +16,15 @@ import { SVG_STROKE_CAP, SVG_STROKE_JOIN, SVG_BLEND_MODE -} from './svg-export-defs' +} from './defs' -export { geometryBlobToSVGPath, vectorNetworkToSVGPaths } from './svg-export-paths' +export { geometryBlobToSVGPath, vectorNetworkToSVGPaths } from './paths' -import { svg, renderSVGNode } from './svg-node' +import { svg, renderSVGNode } from '../svg-node' -import type { SVGNode } from './svg-node' -import type { SceneGraph, SceneNode, Fill, Stroke, CharacterStyleOverride } from './scene-graph' -import type { SVGExportContext } from './svg-export-defs' +import type { SVGNode } from '../svg-node' +import type { SceneGraph, SceneNode, Fill, Stroke, CharacterStyleOverride } from '../scene-graph' +import type { SVGExportContext } from './defs' // --- Node rendering --- diff --git a/packages/core/src/svg-export-paths.ts b/packages/core/src/svg-export/paths.ts similarity index 99% rename from packages/core/src/svg-export-paths.ts rename to packages/core/src/svg-export/paths.ts index c218524a0..d6685795b 100644 --- a/packages/core/src/svg-export-paths.ts +++ b/packages/core/src/svg-export/paths.ts @@ -1,4 +1,4 @@ -import type { SceneNode, VectorNetwork, VectorSegment, VectorVertex } from './scene-graph' +import type { SceneNode, VectorNetwork, VectorSegment, VectorVertex } from '../scene-graph' const CMD_CLOSE = 0 const CMD_MOVE_TO = 1 diff --git a/packages/core/src/tools/vector.ts b/packages/core/src/tools/vector.ts index 4688164a6..3da662662 100644 --- a/packages/core/src/tools/vector.ts +++ b/packages/core/src/tools/vector.ts @@ -237,7 +237,7 @@ export const exportSvg = defineTool({ } }, execute: async (figma, args) => { - const { renderNodesToSVG } = await import('../svg-export.js') + const { renderNodesToSVG } = await import('../svg-export/index.js') const pageId = figma.currentPageId const ids = args.ids && args.ids.length > 0 diff --git a/src/automation/bridge.ts b/src/automation/bridge.ts index d9ee2301a..74299c58a 100644 --- a/src/automation/bridge.ts +++ b/src/automation/bridge.ts @@ -101,7 +101,9 @@ export function startAutomationBridge(server: ViteServer) { authToken = null ws.on('message', (raw) => { - handleBrowserMessage(typeof raw === 'string' ? raw : Buffer.from(raw as Buffer).toString('utf-8')) + handleBrowserMessage( + typeof raw === 'string' ? raw : Buffer.from(raw as Buffer).toString('utf-8') + ) }) ws.on('close', () => { diff --git a/src/automation/server.ts b/src/automation/server.ts index a584280a1..2474a9de3 100644 --- a/src/automation/server.ts +++ b/src/automation/server.ts @@ -122,7 +122,9 @@ export function connectAutomation(getStore: () => EditorStore) { return { ok: true, result: { jsx } } } - const commandHandlers: Partial Promise>> = { + const commandHandlers: Partial< + Record Promise> + > = { eval: handleEval, tool: handleTool, export: handleExport, diff --git a/src/components/ChatPanel.vue b/src/components/ChatPanel.vue index eead37d45..e8c84de8f 100644 --- a/src/components/ChatPanel.vue +++ b/src/components/ChatPanel.vue @@ -31,7 +31,9 @@ function handleSubmit(text: string) { const c = ensureChat() if (c) chat.value = markRaw(c) } - chat.value?.sendMessage({ text }).catch(() => { /* user-facing error handled by UI */ }) + chat.value?.sendMessage({ text }).catch(() => { + /* user-facing error handled by UI */ + }) } function handleStop() { diff --git a/src/components/EditorCanvas.vue b/src/components/EditorCanvas.vue index e763c1c6a..22734f09d 100644 --- a/src/components/EditorCanvas.vue +++ b/src/components/EditorCanvas.vue @@ -12,7 +12,10 @@ const store = useEditorStore() const collab = useCollabInjected() const canvasRef = ref(null) -const { hitTestSectionTitle, hitTestComponentLabel, hitTestFrameTitle } = useCanvas(canvasRef, store) +const { hitTestSectionTitle, hitTestComponentLabel, hitTestFrameTitle } = useCanvas( + canvasRef, + store +) const { cursorOverride } = useCanvasInput( canvasRef, store, diff --git a/src/components/FillPicker.vue b/src/components/FillPicker.vue index 945c1486f..f88f25a19 100644 --- a/src/components/FillPicker.vue +++ b/src/components/FillPicker.vue @@ -87,9 +87,7 @@ function onColorUpdate(color: Color) { function setCategory(cat: FillCategory) { if (cat === fillCategory.value) return if (cat === 'SOLID') { - const color = fill.gradientStops?.length - ? { ...fill.gradientStops[0].color } - : fill.color + const color = fill.gradientStops?.length ? { ...fill.gradientStops[0].color } : fill.color emit('update', { ...fill, type: 'SOLID', color }) } else if (cat === 'GRADIENT') { const type: GradientSubtype = 'GRADIENT_LINEAR' diff --git a/src/components/FontPicker.vue b/src/components/FontPicker.vue index 1d294089b..acd852d07 100644 --- a/src/components/FontPicker.vue +++ b/src/components/FontPicker.vue @@ -1,7 +1,6 @@