Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/figma/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "figma",
"version": "2.0.12",
"version": "2.0.13",
"description": "Figma workflows for design implementation, Code Connect templates, and design system rule generation.",
"author": {
"name": "Figma",
Expand Down
3 changes: 3 additions & 0 deletions plugins/figma/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ It currently includes these skills:

- `figma-implement-design`
- `figma-code-connect`
- `figma-copy-paste`
- `figma-create-design-system-rules`
- `figma-create-new-file`
- `figma-generate-design`
Expand All @@ -18,6 +19,7 @@ It currently includes these skills:
- translating Figma frames and components into production-ready UI code
- inspecting design context and screenshots through the connected Figma tools
- creating parserless Code Connect template files for published Figma components
- copying exact, editable screens, components, and layers between Figma files
- generating project-specific design system rules for Figma-to-code workflows
- creating or updating full screens and design system libraries in Figma
- creating new Figma or FigJam files when needed for a workflow
Expand Down Expand Up @@ -64,6 +66,7 @@ The current skill set is focused on these workflows:

- implementing designs from Figma with high visual fidelity
- creating parserless Code Connect templates for published Figma components
- transferring existing Figma content between files without recreating or flattening it
- generating durable project rules for future Figma-to-code work
- creating or updating Figma files, screens, and design system libraries

Expand Down
22 changes: 22 additions & 0 deletions plugins/figma/skills/figma-copy-paste/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: figma-copy-paste
description: Copy exact, editable Figma screens, frames, components, or layers from one file to another with Figma's native clipboard, then verify the pasted result. Use when the user asks to copy, paste, transfer, or duplicate existing Figma content between files without recreating or flattening it.
---

# Figma Copy Paste

Use Figma's native clipboard. Use Figma MCP to identify and verify content; never reconstruct an exact-copy request.

Computer Use is required to send the native OS-level Copy and Paste events that make exact cross-file Figma transfer reliable on macOS and Windows.

## Workflow

1. Identify the smallest complete source node that satisfies the request. For a vague request or oversized source canvas, follow [references/selection.md](references/selection.md). Prefer a full screen over an inner layer or surrounding multi-screen canvas. When the user supplies an exact node ID and metadata confirms it is already the requested complete node, do not substitute an ancestor. Record its file key, node ID, name, type, dimensions, metadata counts, variables, and screenshot. Name similar frames excluded from the copy.
2. Keep the source read-only. Never cut, detach, flatten, move, or republish it. For view-only sources, use MCP read tools rather than `use_figma`.
3. Inspect the destination page and record its existing top-level nodes and local assets.
4. Follow [references/clipboard.md](references/clipboard.md) to select the exact node, validate the native Figma clipboard payload, paste, and handle the watchdog and variable prompt.
5. Do not copy every main component set preemptively. Preserve linked instances; localize only components the user later chooses to edit independently.
6. Follow [references/verification.md](references/verification.md) to compare structure, bindings, and isolated renders.
7. Report the source and destination node IDs, clipboard path, variable behavior, fidelity classification, discrepancies, and optional follow-up work.

Ask only when two materially different complete source nodes remain equally plausible. Never substitute an image, generated nodes, or a flattened export for a failed native transfer.
4 changes: 4 additions & 0 deletions plugins/figma/skills/figma-copy-paste/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Figma Copy Paste"
short_description: "Copy exact Figma layers across files"
default_prompt: "Use $figma-copy-paste to copy an exact frame between Figma files and verify fidelity."
40 changes: 40 additions & 0 deletions plugins/figma/skills/figma-copy-paste/references/clipboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Native Clipboard Transfer

## Copy

If the Computer plugin reports that the device is locked, stop before Copy, verify the destination is unchanged, and ask the user to unlock it. Never bypass the OS lock.

Before Copy and Paste, confirm Computer can act on the Figma window. If it cannot, reactivate the exact Figma app or browser tab with Computer and retry once; then stop without mutating the destination if the window remains unavailable.

For a FigJam source (`/board/`), read [figjam.md](figjam.md) before selecting the node.

For a Design source opened in Dev Mode (`m=dev` or Inspect), switch to Design mode before selecting or copying. Dev Mode may serialize properties or a different focused node even when the pasteboard contains `figmeta`.

1. Open the exact node deep link and use Computer to select the exact Layers-panel row, not canvas text or Dev Mode inspector content. Browser accessibility/DOM may be used when available, but is optional.
2. Re-check that the selected URL still contains the expected node ID, and that the properties panel shows the expected layer name and dimensions; names such as `Window` are not unique.
3. Focus Figma and invoke Copy through Computer's real app keypress. Browser-scoped `press` events do not count as OS-level Copy.
4. On macOS, validate the host pasteboard:

```bash
scripts/inspect_figma_clipboard.py \
--expect-file-key FILE_KEY \
--expect-node-id NODE_ID \
--wait-seconds 5 \
--require-match
```

Run with host/escalated access if a sandboxed call sees an empty pasteboard. Browser-scoped clipboard inspectors are not authoritative.
On Windows, where this host validator is unavailable, record clipboard validation as unavailable and continue only after confirming the exact source selection and capturing the destination baseline. Treat any unexpected destination delta as failure and reset immediately.
5. If validation returns visible child text instead of `figmeta`, treat it as a focus failure. Click the exact Layers row again before retrying. Do not repeat Copy with unchanged focus.
6. If validation still fails, invoke Figma's top Edit-menu `Copy`, then poll again. Use the selected layer row's context-menu `Copy` as the final fallback and poll once more.
7. If all three attempts fail for a nested node in a view-only source, read [view-only-nested.md](view-only-nested.md). Otherwise report a safe copy failure. Do not recreate or request edit access unless the user asks.

## Paste

1. Focus an empty destination canvas area and invoke OS-level Paste.
2. Do not use canvas `Paste here`; it has stalled in automation.
3. Wait up to 20 seconds for `Pasting…` to resolve, a variable prompt to appear, or a new node to materialize.
4. If the paste stalls, cancel or undo, verify the destination returned to its baseline, and retry the complete copy sequence once.
5. If Figma reports unpublished variables, choose `Copy variables into this file`. Otherwise preserve remote library bindings.

Track: `selected`, `copied`, `clipboard-validated`, `paste-started`, `variable-prompt`, `pasted`, `verified`, `reset`. Return the last state and timings on failure.
10 changes: 10 additions & 0 deletions plugins/figma/skills/figma-copy-paste/references/figjam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# FigJam Source

Load the Figma FigJam skill and inspect the exact node with `get_figjam`. FigJam has one implicit page and no Design-style Layers panel.

1. Open the exact `/board/` node deep link.
2. Select the node on the FigJam canvas through its accessible canvas object or a coordinate grounded in a fresh screenshot.
3. Confirm the URL node ID and visible selection bounds before OS-level Copy.
4. Use the normal host pasteboard validation and paste watchdog.

When pasting into a Design file, node types may convert. For image-filled FigJam rectangles, require matching dimensions, image content, crop, corner geometry, and visual metrics rather than the same editor-specific node type. For stickies, shapes, connectors, or widgets, report any conversion explicitly and fail if editability or visible relationships are lost.
9 changes: 9 additions & 0 deletions plugins/figma/skills/figma-copy-paste/references/selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Vague Source Selection

Use this only when the user describes a screen or component instead of supplying its exact node, or when the supplied node is an oversized multi-screen canvas.

1. Turn the request into two or three visible cues: distinctive text, selected navigation state, and required region or control.
2. Keep the source read-only. Use Figma's Find through Chrome to locate the rarest visible text cue, then inspect its ancestors in the Layers panel. Do not copy the search result itself unless it is the requested complete object.
3. Promote to the smallest ancestor with a complete visual boundary and plausible screen or component dimensions. Reject page canvases, multi-screen strips, clipped fragments, and frames missing any required cue.
4. Capture isolated screenshots and metadata for at most three plausible candidates. Choose the only candidate that satisfies every cue; record why the others fail.
5. If two candidates remain materially plausible, ask the user. If none match, stop without copying.
27 changes: 27 additions & 0 deletions plugins/figma/skills/figma-copy-paste/references/verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Fidelity Verification

Capture source and destination metadata, resolved variables, and isolated screenshots before slow analysis.

## Structure

- Normalize node IDs and the pasted root's page position.
- Compare root name, type, dimensions, direct children, metadata-tree type counts, visible names and geometry, instance count/status, variables, and library access.
- Do not expand remote instance interiors into a second count tree.

## Visuals

- Render source and destination in isolation at identical dimensions.
- Compare decoded pixels, not PNG file hashes.
- Also compare white-composited renders to separate alpha-container differences from visible differences.
- Use `scripts/compare_pngs.py` for deterministic metrics.
- If raw render sizes differ, inspect its `transparent_bounds_normalized` metrics. Use them only when the structural dimensions match and the removed rows or columns are fully transparent; report both raw bounds and normalized scores.

Classify:

- `exact`: decoded pixels and normalized structure match exactly.
- `native-equivalent`: white-composited similarity is at least 0.999, visible structure/counts/variables match, and differences are limited to hidden metadata or negligible float normalization.
- `failed`: any visible, layout, instance, variable, or library mismatch, or similarity below 0.999.

Report every normalization even for `native-equivalent` so behavior can be tracked.

If teardown is requested, save the evidence, restore and verify the destination immediately, then run offline pixel analysis. This protects cleanup from later timeouts.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# View-only Nested Layer Fallback

Use only when a requested nested layer cannot produce native `figmeta`, but a containing top-level frame can.

1. Record the target's hierarchy path, sibling indexes, name, type, dimensions, metadata counts, variables, and screenshot.
2. Select the nearest complete top-level containing frame and transfer it natively using the normal clipboard procedure.
3. In the editable destination, locate the requested descendant by the recorded hierarchy fingerprint.
4. Clone that descendant within the destination file and append the clone to the page. This is a native same-file clone, not reconstruction.
5. Remove the temporary containing frame.
6. Verify the retained clone directly against the original nested source node.

Abort and undo if the descendant fingerprint is not unique or the temporary top-level frame cannot be transferred natively.
103 changes: 103 additions & 0 deletions plugins/figma/skills/figma-copy-paste/scripts/compare_pngs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Compare two decoded PNG renders and emit stable JSON metrics."""

from __future__ import annotations

import argparse
import json
from pathlib import Path

import numpy as np
from PIL import Image, ImageChops, ImageStat


def global_ssim(left: Image.Image, right: Image.Image) -> float:
"""Return a deterministic whole-image SSIM-like score in the range [-1, 1]."""
left_array = np.asarray(left, dtype=np.float64)
right_array = np.asarray(right, dtype=np.float64)
scores: list[float] = []
c1 = (0.01 * 255) ** 2
c2 = (0.03 * 255) ** 2
for channel in range(left_array.shape[2]):
x = left_array[:, :, channel]
y = right_array[:, :, channel]
mean_x = float(x.mean())
mean_y = float(y.mean())
variance_x = float(x.var())
variance_y = float(y.var())
covariance = float(((x - mean_x) * (y - mean_y)).mean())
numerator = (2 * mean_x * mean_y + c1) * (2 * covariance + c2)
denominator = (mean_x**2 + mean_y**2 + c1) * (
variance_x + variance_y + c2
)
scores.append(numerator / denominator if denominator else 1.0)
return float(sum(scores) / len(scores))


def metrics(left: Image.Image, right: Image.Image) -> dict[str, object]:
if left.size != right.size:
return {"same_size": False, "left_size": left.size, "right_size": right.size}

diff = ImageChops.difference(left, right)
stat = ImageStat.Stat(diff)
pixels = (
diff.get_flattened_data()
if hasattr(diff, "get_flattened_data")
else diff.getdata()
)
changed = sum(1 for pixel in pixels if any(pixel))
total = left.width * left.height
return {
"same_size": True,
"decoded_exact": changed == 0,
"changed_pixels": changed,
"changed_pixel_ratio": changed / total if total else 0,
"global_ssim": global_ssim(left, right),
"mean_absolute_error_by_channel": stat.mean,
"rms_by_channel": stat.rms,
"max_delta_by_channel": [maximum for _, maximum in stat.extrema],
}


def white_composite(image: Image.Image) -> Image.Image:
background = Image.new("RGBA", image.size, (255, 255, 255, 255))
return Image.alpha_composite(background, image).convert("RGB")


def crop_to_visible_bounds(image: Image.Image) -> tuple[Image.Image, tuple[int, ...] | None]:
"""Crop fully transparent outer padding without changing visible pixels."""
bounds = image.getchannel("A").getbbox()
if bounds is None:
return Image.new("RGBA", (1, 1), (0, 0, 0, 0)), None
return image.crop(bounds), bounds


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("left", type=Path)
parser.add_argument("right", type=Path)
args = parser.parse_args()

left = Image.open(args.left).convert("RGBA")
right = Image.open(args.right).convert("RGBA")
left_visible, left_bounds = crop_to_visible_bounds(left)
right_visible, right_bounds = crop_to_visible_bounds(right)
result = {
"left": str(args.left),
"right": str(args.right),
"rgba": metrics(left, right),
"white_composited": metrics(white_composite(left), white_composite(right)),
"transparent_bounds_normalized": {
"left_bounds": left_bounds,
"right_bounds": right_bounds,
"rgba": metrics(left_visible, right_visible),
"white_composited": metrics(
white_composite(left_visible), white_composite(right_visible)
),
},
}
print(json.dumps(result, indent=2))


if __name__ == "__main__":
main()
Loading