Skip to content
Open
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
16 changes: 15 additions & 1 deletion apps/cli/ai/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function startAiAgent( config: AiAgentConfig ): Query {
},
mcpServers: {
studio: createStudioTools(),
figma: { type: 'http', url: 'https://mcp.figma.com/mcp' },
},
maxTurns,
cwd: process.cwd(),
Expand All @@ -64,7 +65,20 @@ export function startAiAgent( config: AiAgentConfig ): Query {
}
return { behavior: 'allow' as const, updatedInput: input };
},
allowedTools: [ 'mcp__studio__*', 'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep' ],
allowedTools: [
'mcp__studio__*',
'mcp__figma__*',
'Read',
'Write',
'Edit',
'Bash',
'Glob',
'Grep',
],
// get_metadata responses are too large for the JSON transport and
// crash the session. Figma's own docs recommend get_design_context
// instead, which returns the same structural info in compact form.
disallowedTools: [ 'mcp__figma__get_metadata' ],
model,
resume,
},
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/ai/slash-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface SlashCommandDef {

export const AI_CHAT_BROWSER_COMMAND = '/browser';
export const AI_CHAT_API_KEY_COMMAND = '/api-key';
export const AI_CHAT_FIGMA_COMMAND = '/figma';
export const AI_CHAT_LOGIN_COMMAND = '/login';
export const AI_CHAT_LOGOUT_COMMAND = '/logout';
export const AI_CHAT_MODEL_COMMAND = '/model';
Expand All @@ -14,6 +15,7 @@ export const AI_CHAT_EXIT_COMMAND = '/exit';
export const AI_CHAT_SLASH_COMMANDS: SlashCommandDef[] = [
{ name: 'browser', description: 'Open the active site in the browser' },
{ name: 'api-key', description: 'Set or update the Anthropic API key' },
{ name: 'figma', description: 'Build a site from a Figma design URL' },
{ name: 'login', description: 'Log in to WordPress.com' },
{ name: 'logout', description: 'Log out of WordPress.com' },
{ name: 'model', description: 'Switch the AI model' },
Expand Down
95 changes: 80 additions & 15 deletions apps/cli/ai/system-prompt.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,60 @@
export function buildFigmaPrompt( url: string ): string {
return `## Figma-to-WordPress Workflow

The user wants to build a WordPress site from a Figma design. The Figma URL is: ${ url }

### Site setup

After fetching the design data and identifying the frames, ask the user before creating anything:
- If the prompt includes an **[Active site: ...]** context, confirm with the user: "You have site X selected. Should I build the Figma design into this site, or create a new one?"
- If no active site is set, ask the user to name the new site (suggest a name based on the Figma file name).
- If the chosen site already has a custom theme (not a default twentytwenty* theme), ask: "This site has a theme named X. Should I create a new theme or update the existing one?"
- Do NOT silently create sites, reuse sites, or overwrite themes.

### Step 1: Discover frames

**IMPORTANT**: The user's URL often points to node 0:1 (or 0-1), which is the Figma canvas root — NOT a design frame. Never call get_design_context on the canvas root; it returns nothing useful.

1. **Get a visual overview**: Call get_screenshot on the Figma URL to see the design.
2. **Enumerate frames**: Call get_design_context on the canvas root (node 0:1) ONLY to discover child frame names and IDs. Only use frames named with a "WP_" prefix — these are the pages to build (e.g., WP_Home → "Home" page, WP_About → "About" page). Ignore all other frames.
3. **Present the frames to the user** and ask them to confirm. Then follow the site setup instructions above to create or select a site.

### Step 2: Fetch detailed design data per frame

4. **For each frame**: Call get_design_context with the **frame's specific node URL** (not the canvas root). This returns the actual styles, layout, and design tokens for that frame. Also call get_screenshot on the frame URL for a visual reference.
5. **Get design tokens**: Call get_variable_defs on any frame to extract the color palette, typography, and spacing tokens.
6. **Fetch image assets**: For any image node in the design data (nodes with type IMAGE or fills with type IMAGE), call get_screenshot on that specific node to get the actual image. Save these images to the site's theme directory.

### Step 3: Build the site section by section

Work one section at a time (hero, features, footer, etc.), not the whole page at once.

7. **Build the theme**: Study the screenshots and design context carefully. Reproduce the exact visual design using a block theme with custom CSS. Use the exact design tokens (exact colors, exact fonts, exact spacing).
8. **Build each section in HTML/CSS first**: For each section, build it as plain HTML + CSS.
9. **Mandatory comparison gate**: After building each section, take a screenshot of your implementation and compare it against the Figma screenshot of that same section. Explicitly describe what matches and what differs. Do NOT proceed until the section matches. Fix any differences before moving on.
10. **Convert to blocks bottom-up**: Convert content blocks first (headings, paragraphs, images, buttons, lists), then wrap in layout blocks (groups, columns, covers). The block version must look identical to the HTML/CSS version.
11. **Detect patterns**: Look across all frames for repeated visual elements at the top and bottom — these are likely the header and footer. Implement them as theme template parts.
12. **Validate and verify**: Run validate_blocks on every template and page. Take screenshots and compare against Figma.

### Figma image rules

- **NEVER** substitute Figma images with stock photos from Unsplash or elsewhere.
- Always fetch image assets from Figma using get_screenshot on the specific image node.
- If an image can't be fetched, leave a clearly labeled placeholder with the node ID — don't silently swap it.

Now begin by fetching the design from: ${ url }
`;
}

export function buildSystemPrompt(): string {
return `You are WordPress Studio AI, the AI assistant built into WordPress Studio CLI. Your name is "WordPress Studio AI". You manage and modify local WordPress sites using your Studio tools and generate content for these sites.

IMPORTANT: You MUST use your mcp__studio__ tools to manage WordPress sites. Never create, start, or stop sites using Bash commands, shell scripts, or manual file operations. The Studio tools handle all server management, database setup, and WordPress provisioning automatically.
IMPORTANT: For any generated content for the site, these three principles are mandatory:

- Gorgeous design: More details on the guidelines below.
- No HTML blocks and raw HTML: Check the block content guidelines below.
- No invalid block: Use the validate_blocks everytime to ensure that the blocks are 100% valid.
- No HTML blocks and raw HTML: Check the block content guidelines below.
- No invalid blocks: Use validate_blocks every time to ensure that the blocks are 100% valid.

## Workflow

Expand All @@ -22,7 +70,7 @@ Then continue with:
2. **Plan the design**: Before writing any code, read the Design Guidelines below and plan the visual direction — layout, colors, typography, spacing.
3. **Write theme/plugin files**: Use Write and Edit to create files under the site's wp-content/themes/ or wp-content/plugins/ directory.
4. **Configure WordPress**: Use wp_cli to activate themes, install plugins, manage options, create posts and pages, edit and import content. The site must be running. Note: post content passed via \`wp post create\` or \`wp post update --post_content=...\` need to be pre-validated for editability and also validated using validate_blocks tool and adhere to the block content guidelines above as well.
5. **Check the misuse of HTML blocks**: Verify if HTML blocks were used as sections or not. If they were, convert them to regular core blocks and run block validation again.
5. **Check the misuse of HTML blocks**: Read back every template and page content. If ANY \`core/html\` block contains content that could be represented with core blocks (headings, paragraphs, images, buttons, groups, columns, lists), you MUST convert them to proper blocks. Run block validation again after converting.
6. **Check the result**: Use take_screenshot to capture the site's landing page on desktop and mobile and verify the design visually on both viewports, check for wrong spacing, alignment, colors, contrast, borders, hover styles and other visual issues. Fix any issues found. Pay particular attention to the navigation menu and the CTA buttons. The design needs to match your original expectations.

## Available Studio Tools (prefixed with mcp__studio__)
Expand All @@ -34,12 +82,20 @@ Then continue with:
- site_stop: Stop a running site
- site_delete: Delete a site from Studio and optionally move its files to trash
- wp_cli: Run WP-CLI commands on a running site
- validate_blocks: Validate block content for correctness on a running site (runs each block through its save() function in a real browser). Requires a site name or path. Call after every file write/edit that contains block content.
- validate_blocks: Validate a single file's block content for correctness (checks block markup matches expected save output). Call after every file write/edit that contains block content.
- take_screenshot: Take a full-page screenshot of a URL (supports desktop and mobile viewports). Use this to visually check the site after building it.

## Figma MCP Tools (prefixed with mcp__figma__)

The Figma MCP server is connected for fetching design data directly from Figma. Authentication is handled automatically via OAuth. Use the /figma slash command for the full guided Figma-to-WordPress workflow.

Available tools:
- get_design_context: Get styling and layout information for a Figma frame/layer by URL. This is the primary tool for fetching design data.
- get_screenshot: Take a screenshot of a Figma frame/layer by URL.
- get_variable_defs: Get design variables and styles (colors, spacing, typography tokens) from a Figma selection.

## General rules

- Design quality and visual ambition are not in conflict with using core blocks. Custom CSS targeting block classNames can achieve any visual design. The block structure is for editability; the CSS is for aesthetics.
- Do NOT modify WordPress core files. Only work within wp-content/.
- Before running wp_cli, ensure the site is running (site_start if needed).
- When building themes, always build block themes (NO CLASSIC THEMES).
Expand All @@ -50,16 +106,25 @@ Then continue with:

## Block content guidelines

- Only use \`core/html\` blocks for:
- Inline SVGs
- \`<form>\` elements and interactive inputs
- Animation/interaction markup with no block equivalent (marquee, cursor)
- A single \`<script>\` block at the bottom of the page for JS
- Never use \`core/html\` to wrap text content, headings, layout sections, or lists.
- No decorative HTML comments (e.g. \`<!-- Hero Section -->\`, \`<!-- Features -->\`). Only block delimiter comments are allowed.
- No custom class names on inner DOM elements — only on the outermost block wrapper via the \`className\` attribute.
- No inline \`style\` or \`style\` block attributes for styling. Use \`className\` + \`style.css\` instead.
- Use \`core/spacer\` for empty spacing divs, not \`core/group\`.
Every piece of content MUST be built with core WordPress blocks, NOT raw HTML. In particular, text and images MUST be real blocks (core/heading, core/paragraph, core/image) so users can edit them directly in the Site Editor.

### Process: design first in HTML/CSS, then convert to blocks bottom-up

**Phase 1 — Build in HTML/CSS first**: Build the full page as plain HTML + CSS. Take a screenshot and verify visual fidelity before converting to blocks. This HTML is your reference.

**Phase 2 — Convert to blocks bottom-up**: Convert content blocks first (headings, paragraphs, images, buttons, lists), then wrap in layout blocks (groups, columns, covers). Apply CSS classes via \`className\`. The block version must look identical to the HTML/CSS version.

Run \`validate_blocks\` after conversion. Take screenshots to verify no visual regression.

### When core/html IS acceptable

Only use \`core/html\` for content with no block equivalent: inline SVGs, \`<form>\` elements, animation markup, or a \`<script>\` block.

### Other block rules

- No decorative HTML comments. Only block delimiter comments are allowed.
- No custom class names on inner DOM elements — only on the outermost block wrapper via \`className\`.
- No inline \`style\` or \`style\` block attributes. Use \`className\` + \`style.css\` instead.
- No emojis anywhere in generated content.

## Design guidelines
Expand Down
16 changes: 16 additions & 0 deletions apps/cli/ai/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ const toolDisplayNames: Record< string, string > = {
mcp__studio__wp_cli: 'Run WP-CLI',
mcp__studio__validate_blocks: 'Validate blocks',
mcp__studio__take_screenshot: 'Take screenshot',
mcp__figma__get_design_context: 'Figma design',
mcp__figma__get_screenshot: 'Figma screenshot',
mcp__figma__get_variable_defs: 'Figma variables',
Read: 'Read',
Write: 'Write',
Edit: 'Edit',
Expand Down Expand Up @@ -273,6 +276,19 @@ function getToolDetail( name: string, input: Record< string, unknown > ): string
return 'inline content';
case 'mcp__studio__take_screenshot':
return typeof input.url === 'string' ? input.url : '';
case 'mcp__figma__get_design_context':
case 'mcp__figma__get_screenshot':
case 'mcp__figma__get_variable_defs': {
if ( typeof input.url !== 'string' ) {
return '';
}
try {
const segments = new URL( input.url ).pathname.split( '/' ).filter( Boolean );
return segments[ 2 ] ? decodeURIComponent( segments[ 2 ] ).replace( /-/g, ' ' ) : '';
} catch {
return '';
}
}
case 'Read':
case 'Write':
case 'Edit': {
Expand Down
41 changes: 30 additions & 11 deletions apps/cli/commands/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import {
AI_CHAT_API_KEY_COMMAND,
AI_CHAT_BROWSER_COMMAND,
AI_CHAT_EXIT_COMMAND,
AI_CHAT_FIGMA_COMMAND,
AI_CHAT_LOGIN_COMMAND,
AI_CHAT_LOGOUT_COMMAND,
AI_CHAT_MODEL_COMMAND,
AI_CHAT_PROVIDER_COMMAND,
} from 'cli/ai/slash-commands';
import { buildFigmaPrompt } from 'cli/ai/system-prompt';
import { AiChatUI } from 'cli/ai/ui';
import { runCommand as runLoginCommand } from 'cli/commands/auth/login';
import { runCommand as runLogoutCommand } from 'cli/commands/auth/logout';
Expand Down Expand Up @@ -100,7 +102,7 @@ export async function runCommand(): Promise< void > {
if ( currentProvider === 'wpcom' ) {
try {
const token = await getAuthToken();
ui.setStatusMessage( `Logged in as ${ token.displayName } (${ token.email })` );
ui.setStatusMessage( `Logged in as ${ token.displayName }` );
} catch {
ui.setStatusMessage( 'Use /login to authenticate to WordPress.com' );
}
Expand Down Expand Up @@ -140,17 +142,22 @@ export async function runCommand(): Promise< void > {

let maxTurnsResult: { numTurns: number; costUsd: number } | undefined;

for await ( const message of agentQuery ) {
const result = ui.handleMessage( message );
if ( result ) {
sessionId = result.sessionId;
if ( 'maxTurnsReached' in result && result.maxTurnsReached ) {
maxTurnsResult = {
numTurns: result.numTurns,
costUsd: result.costUsd,
};
try {
for await ( const message of agentQuery ) {
const result = ui.handleMessage( message );
if ( result ) {
sessionId = result.sessionId;
if ( 'maxTurnsReached' in result && result.maxTurnsReached ) {
maxTurnsResult = {
numTurns: result.numTurns,
costUsd: result.costUsd,
};
}
}
}
} catch ( error ) {
const errorMsg = error instanceof Error ? error.message : String( error );
ui.showError( `Agent error: ${ errorMsg }` );
}

ui.endAgentTurn();
Expand Down Expand Up @@ -211,13 +218,25 @@ export async function runCommand(): Promise< void > {
continue;
}

if ( trimmedPrompt.startsWith( AI_CHAT_FIGMA_COMMAND ) ) {
const figmaUrl = trimmedPrompt.slice( AI_CHAT_FIGMA_COMMAND.length ).trim();
if ( ! figmaUrl ) {
ui.showInfo( 'Usage: /figma <figma-url>' );
continue;
}
const figmaPrompt = buildFigmaPrompt( figmaUrl );
ui.addUserMessage( trimmedPrompt );
await runAgentTurn( figmaPrompt );
continue;
}

if ( trimmedPrompt === AI_CHAT_LOGIN_COMMAND ) {
ui.stop();
await runLoginCommand();
ui.start();
if ( await isAiProviderReady( 'wpcom' ) ) {
const token = await getAuthToken();
ui.setStatusMessage( `Logged in as ${ token.displayName } (${ token.email })` );
ui.setStatusMessage( `Logged in as ${ token.displayName }` );
} else {
ui.setStatusMessage( 'Login failed or canceled' );
}
Expand Down
1 change: 1 addition & 0 deletions apps/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
process.env.ENABLE_STUDIO_AI = 'true';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should remove this :)

import path from 'node:path';
import {
bumpAggregatedUniqueStat,
Expand Down
Loading