Skip to content
2 changes: 1 addition & 1 deletion apps/cli/ai/skills/visual-design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Build working code that is:
Focus on:

- **Typography**: Choose fonts that suit the concept. Avoid defaulting to generic choices like Arial, Inter, Roboto, or system fonts unless restraint is clearly part of the brief. Pair display and body typography intentionally.
- **Color and theme**: Commit to a palette and define it once in the theme's `theme.json` (`settings.color.palette`), then drive every section, block, and CSS rule from those palette colors by slug. Use dominant colors and sharp accents deliberately instead of timid, evenly distributed colors. When redesigning or adding sections to a site that already has an active theme, inherit its existing palette rather than inventing new custom colors. Treat the palette as the single source of truth — do not scatter hardcoded hex values across block markup or CSS; introduce a custom color only when the concept genuinely needs one the palette lacks, and add it to the palette first. See the `block-content` skill for how to reference palette colors.
- **Color and theme**: Commit to a palette and define it once in the theme's `theme.json` (`settings.color.palette`), then drive every section, block, and CSS rule from those palette colors by slug. Use dominant colors and sharp accents deliberately instead of timid, evenly distributed colors. When redesigning or adding sections to a site that already has an active theme, inherit its existing palette rather than inventing new custom colors. When that active theme is an installed third-party theme, put palette overrides and all other design changes in a child theme (scaffold_theme with parentTheme), never in the installed theme's own files. Treat the palette as the single source of truth — do not scatter hardcoded hex values across block markup or CSS; introduce a custom color only when the concept genuinely needs one the palette lacks, and add it to the palette first. See the `block-content` skill for how to reference palette colors.
- **Motion**: Use animation and transitions when they serve the concept. Prefer CSS where possible. A few well-orchestrated moments are better than scattered effects.
- **Spatial composition**: Use asymmetry, overlap, diagonal flow, grid-breaking elements, generous negative space, or controlled density when appropriate to the concept.
- **Backgrounds and visual details**: Add atmosphere and depth with contextual textures, patterns, shadows, borders, transparency, or custom visual treatments when they reinforce the direction.
Expand Down
5 changes: 3 additions & 2 deletions apps/cli/ai/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Then continue with:

1. **Get site details**: Use site_info to get the site path, URL, and credentials.
2. **Plan the design**: Before writing any code, review the site spec (from the \`site-spec\` skill) and load the \`visual-design\` skill to plan the visual direction: layout, colors, typography, and spacing.
3. **Write theme/plugin files**: For a brand new theme, call \`scaffold_theme\` first — it drops an unopinionated block-theme baseline (style.css with only the theme header, theme.json with appearanceTools only, functions.php with frontend + editor style enqueue, default templates and parts, empty assets/fonts and patterns dirs) and activates it by default. Then use Write and Edit to fill the scaffold (one part/template/file per turn). For plugins or for editing an existing theme, use Write and Edit directly under the site's wp-content/themes/ or wp-content/plugins/ directory.
3. **Write theme/plugin files**: For a brand new theme, call \`scaffold_theme\` first — it drops an unopinionated block-theme baseline (style.css with only the theme header, theme.json with appearanceTools only, functions.php with frontend + editor style enqueue, default templates and parts, empty assets/fonts and patterns dirs) and activates it by default. To customize an installed third-party theme, call \`scaffold_theme\` with \`parentTheme\` set to the installed theme's slug — it creates and activates a child theme that inherits the parent's look; put every customization in the child. Then use Write and Edit to fill the scaffold (one part/template/file per turn). For plugins, or for themes Studio Code created on this site (blank scaffolds and child themes), use Write and Edit directly under the site's wp-content/themes/ or wp-content/plugins/ directory.
4. **Provision the site**: Use wp_cli to activate the theme, install and activate any plugins the design needs, and set options. Do this before validating — the live editor only recognizes the active theme and registered plugin blocks. The site must be running.
5. **Validate block content**: Any block content you generate MUST pass validate_blocks before it reaches the site — before \`wp post create/update\` and before \`wp_cli eval\` that imports a scratch file such as \`<site>/tmp/page-<slug>.html\`. Call validate_blocks with \`filePath\` for file content, or pass inline content. It runs a static core/html policy check first: if that reports invalid core/html blocks, editor validation is skipped — rewrite those as editable core or plugin blocks and call again. Once the policy passes it validates in the live editor. If an auto-fix was applied, the file already holds the fixed content; do not replace markup or re-validate unless you change the markup. Use the diff only to update CSS selectors for class/nesting changes. For inline content, use the returned fixed content exactly. Never apply unvalidated block content — a build that skips validate_blocks is incomplete.
6. **Apply content**: Once it passes validation, create/update/import the posts and pages with the validated content. ${ postContentGuidance }
Expand Down Expand Up @@ -188,7 +188,7 @@ For long CSS or page-content files (>~200 lines), load the \`block-content\` ski
- preview_delete: Delete a preview site by hostname
- wp_cli: Run WP-CLI commands on a running site
- refresh_browser: Reload the in-app site preview so the user sees your latest changes. Reloads in place; never stop/start the site to refresh the preview.
- scaffold_theme: Scaffold a minimal block theme (style.css, theme.json, functions.php with frontend + editor enqueue, default templates and parts, empty assets/fonts and patterns dirs) into a site and activate it. Use as the first step when starting a new custom theme; the agent fills design-specific content afterwards. Block themes only.
- scaffold_theme: Scaffold a minimal block theme (style.css, theme.json, functions.php with frontend + editor enqueue, default templates and parts, empty assets/fonts and patterns dirs) into a site and activate it. Use as the first step when starting a new custom theme; the agent fills design-specific content afterwards. Pass parentTheme with an installed theme's slug to scaffold a child theme instead of editing that theme's files. Block themes only.
- validate_blocks: Validate block content in two stages and return a combined report. First a static core/html policy check; if it finds invalid core/html blocks it returns only those (rewrite them as editable core or plugin blocks and call again) and skips the editor. Once it passes, validates in the running site's real block editor: with filePath, applies safe editor fixes directly to the file and returns a CSS-review diff; with inline content, returns exact fixed block content plus the diff. Requires a site name or path. Call after every file write/edit that contains block content.
- take_screenshot: Take a full-page screenshot of a URL (supports desktop, mobile, or \`viewport: "all"\` for both). Use this to visually check the site after building it.
- inspect_design: Inspect the rendered DOM and computed styles of a page by CSS selector to root-cause visual issues. Pair with take_screenshot when verifying or polishing a design.
Expand All @@ -205,6 +205,7 @@ ${ studioPresentToolBullet }${ automaticArtifactSection }

- 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/.
- Do NOT edit the files of installed third-party themes (default themes like twentytwentyfive, marketplace/community themes such as Ollie, anything installed via \`wp theme install\` or already present on the site) — a theme update silently wipes such edits. Default to a child theme: call \`scaffold_theme\` with \`parentTheme\` set to the installed theme's slug, then make every customization (style.css, theme.json, templates, parts, patterns) in the child theme. Themes Studio Code created — their style.css Description says "scaffolded by Studio Code" — are safe to edit directly. If the user explicitly asks you to edit an installed theme's files directly, comply, but first warn once that a theme update will overwrite the changes.
- Before running wp_cli, ensure the site is running (site_start if needed).
- After a change that alters what the site renders (content, options/settings, theme, plugins, activation), call refresh_browser so the in-app preview shows the result. Never stop/start the site (site_stop/site_start) just to refresh the preview.
- When building themes, always build block themes (NO CLASSIC THEMES).
Expand Down
129 changes: 129 additions & 0 deletions apps/cli/ai/tests/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,8 @@ describe( 'Studio AI MCP tools', () => {
const styleCss = await readFile( path.join( themeDir, 'style.css' ), 'utf8' );
expect( styleCss ).toContain( 'Theme Name: Acme Studio' );
expect( styleCss ).toContain( 'Text Domain: acme-studio' );
expect( styleCss ).toContain( 'scaffolded by Studio Code' );
expect( styleCss ).not.toContain( 'Template:' );
expect( styleCss ).toContain( '.wp-site-blocks > * + * {' );
expect( styleCss ).toContain( 'margin-block-start: 0;' );

Expand Down Expand Up @@ -1645,5 +1647,132 @@ describe( 'Studio AI MCP tools', () => {
);
expect( getTextContent( result ) ).toContain( 'Error: stylesheet missing.' );
} );

describe( 'child themes (parentTheme)', () => {
async function installParentTheme( slug: string, styleCss?: string ) {
const parentDir = path.join( tempSiteRoot, 'wp-content', 'themes', slug );
await mkdir( parentDir, { recursive: true } );
await writeFile(
path.join( parentDir, 'style.css' ),
styleCss ?? `/*\nTheme Name: ${ slug }\n*/\n`,
'utf8'
);
}

it( 'scaffolds a minimal child theme when parentTheme is given', async () => {
await installParentTheme( 'ollie' );

const result = await getTool( 'scaffold_theme' ).rawHandler( {
nameOrPath: scaffoldSite.name,
name: 'Ollie Child',
parentTheme: 'ollie',
} as never );

const themeDir = path.join( tempSiteRoot, 'wp-content', 'themes', 'ollie-child' );

const styleCss = await readFile( path.join( themeDir, 'style.css' ), 'utf8' );
expect( styleCss ).toContain( 'Theme Name: Ollie Child' );
expect( styleCss ).toContain( 'Template: ollie' );
expect( styleCss ).toContain( 'scaffolded by Studio Code' );
expect( styleCss ).not.toContain( '.wp-site-blocks' );

const themeJson = JSON.parse(
await readFile( path.join( themeDir, 'theme.json' ), 'utf8' )
) as Record< string, unknown >;
expect( themeJson.version ).toBe( 3 );
expect( themeJson.settings ).toBeUndefined();

const functionsPhp = await readFile( path.join( themeDir, 'functions.php' ), 'utf8' );
expect( functionsPhp ).toContain(
"'ollie-parent-style',\n\t\tget_template_directory_uri() . '/style.css'"
);
expect( functionsPhp ).toContain(
"'ollie-child-style',\n\t\tget_stylesheet_directory_uri() . '/style.css',\n\t\tarray( 'ollie-parent-style' )"
);
expect( functionsPhp ).not.toContain( 'get_parent_theme_file_uri' );

await expect( stat( path.join( themeDir, 'templates' ) ) ).rejects.toThrow();
await expect( stat( path.join( themeDir, 'parts' ) ) ).rejects.toThrow();

expect( getTextContent( result ) ).toContain(
"Child theme 'Ollie Child' of 'ollie' scaffolded at wp-content/themes/ollie-child/."
);
expect( getTextContent( result ) ).toContain( "inherit from 'ollie'" );
} );

it( 'activates the child theme by default when the site is running', async () => {
await installParentTheme( 'ollie' );
vi.mocked( isServerRunning ).mockResolvedValue( {
name: scaffoldSite.id,
pmId: 1,
status: 'online',
pid: 1234,
runtime: SITE_RUNTIME_PLAYGROUND,
} );
vi.mocked( runWpCliCommandWithMessaging ).mockResolvedValue(
mockWpCliResponse( { stdout: "Success: Switched to 'Ollie Child' theme." } ) as never
);

await getTool( 'scaffold_theme' ).rawHandler( {
nameOrPath: scaffoldSite.name,
name: 'Ollie Child',
parentTheme: 'ollie',
} as never );

expect( runWpCliCommandWithMessaging ).toHaveBeenCalledWith( scaffoldSite, [
'theme',
'activate',
'ollie-child',
] );
} );

it( 'fails when the parent theme is not installed', async () => {
await expect(
getTool( 'scaffold_theme' ).rawHandler( {
nameOrPath: scaffoldSite.name,
name: 'Ollie Child',
parentTheme: 'ollie',
} as never )
).rejects.toThrow( /Parent theme 'ollie' is not installed/ );
} );

it( 'fails when the parent is itself a child theme', async () => {
await installParentTheme(
'ollie-child',
'/*\nTheme Name: Ollie Child\nTemplate: ollie\n*/\n'
);

await expect(
getTool( 'scaffold_theme' ).rawHandler( {
nameOrPath: scaffoldSite.name,
name: 'Grandchild',
parentTheme: 'ollie-child',
} as never )
).rejects.toThrow( /grandchild themes.*parentTheme: 'ollie'/ );
} );

it( 'fails when parentTheme equals the child slug', async () => {
await installParentTheme( 'ollie' );

await expect(
getTool( 'scaffold_theme' ).rawHandler( {
nameOrPath: scaffoldSite.name,
name: 'Ollie',
slug: 'ollie',
parentTheme: 'ollie',
} as never )
).rejects.toThrow( /must be different from the child theme slug/ );
} );

it( 'rejects invalid parent theme slugs', async () => {
await expect(
getTool( 'scaffold_theme' ).rawHandler( {
nameOrPath: scaffoldSite.name,
name: 'Ollie Child',
parentTheme: 'Not Valid!',
} as never )
).rejects.toThrow( /Parent theme slug must contain only/ );
} );
} );
} );
} );
Loading