diff --git a/apps/cli/ai/system-prompt.ts b/apps/cli/ai/system-prompt.ts index ef9f708bb9..e35b5128dc 100644 --- a/apps/cli/ai/system-prompt.ts +++ b/apps/cli/ai/system-prompt.ts @@ -112,7 +112,8 @@ function buildLocalIntro( options: { previewSteering: boolean } ): string { const previewSteeringTools = options.previewSteering ? ` - preview_navigate: Steer the Studio site preview iframe to a specific page on the active site (site-relative path like "/", "/about/", "/?p=42"). Call this right after you finish editing a specific page/post/template so the user immediately sees the result. -- preview_reload: Reload the preview iframe at its current URL. Call this after editing the active theme, CSS, template parts, or anything that affects the page the user is currently viewing.` +- preview_reload: Reload the preview iframe at its current URL. Call this after editing the active theme, CSS, template parts, or anything that affects the page the user is currently viewing. +- studio_generate_panel: Generates a custom React panel that renders in the preview pane. This is the way to fulfil any request to view, list, edit, or manage the user's WordPress data — posts, pages, comments, users, settings, custom dashboards, anything. You write a complete TSX module with an exported \`stage\`; Studio bundles and ships it. See "Generate panels" below for the conventions.` : ''; const previewSteeringSection = options.previewSteering @@ -125,7 +126,20 @@ Call \`preview_navigate\` / \`preview_reload\` as a side effect of your editing - After editing the homepage, front page template, or global theme assets (style.css, functions.php, template parts): call \`preview_reload\` (the user is most likely on "/"). - After editing or creating a specific page or post: call \`preview_navigate\` with that page's path (e.g. \`/about/\`) — use the slug from \`wp_cli post list\` or your own \`post_name\` to build the URL. - After editing a single template like \`single-product.php\` or a CPT page: navigate to an example URL that uses that template. -- Do not call these tools on a remote WordPress.com site.` +- Do not call these tools on a remote WordPress.com site. + +## Generate panels + +When the user wants to view or interact with their site's data — list posts, edit settings, see a dashboard, anything generative-UI shaped — call \`studio_generate_panel\`. Don't dump a \`wp_cli post list\` table into chat; render the answer as a real screen in the preview pane. + +Conventions every generated panel must follow: + +1. **Listing entities** (posts, pages, comments, users, CPTs, terms): use \`\` from \`@wordpress/dataviews\`. Drive it with \`useSelect\` + \`getEntityRecords\` from \`@wordpress/core-data\`. Define a \`fields\` array and a \`view\` state; wire \`onChangeView\` so the user can sort, filter, and paginate. +2. **Forms** (settings, single-record edit): use \`\` from \`@wordpress/dataviews\`. Read with \`getEntityRecord\` (e.g. \`("root", "site")\` for site settings, \`("postType", "", id)\` for a single record). Save through \`dispatch( coreStore ).saveEntityRecord(...)\` or \`saveEditedEntityRecord\`. +3. **Dashboards / multi-source views / bespoke layouts**: compose \`@wordpress/components\` (Card, CardBody, Flex, FlexItem, Notice, Button) with the same core-data fetching pattern. +4. **Client↔server communication always goes through \`@wordpress/core-data\`** — \`useSelect\` + \`getEntityRecord(s)\` for reads, \`dispatch\` + \`saveEntityRecord\` for writes. Do NOT use \`apiFetch\` directly unless the data is genuinely outside the entity model. +5. **Need data not in core REST?** Extend the API: write a small mu-plugin at \`/wp-content/mu-plugins/studio-extend.php\` that registers your endpoint via \`register_rest_route()\` (or registers a CPT with \`show_in_rest: true\`). Then read it via \`getEntityRecords\` against the new route. +6. **Imports must come from \`@wordpress/*\` only** (externalized to \`wp.*\` globals at runtime). Use \`@wordpress/element\` for hooks (\`useState\`/\`useEffect\`/\`useMemo\`) and \`@wordpress/i18n\` (\`__()\`) for translatable strings. No lodash, axios, react-query, etc.` : ''; return `${ AGENT_IDENTITY } You manage and modify local WordPress sites using your Studio tools and generate content for these sites. diff --git a/apps/cli/ai/tools/generate-panel.ts b/apps/cli/ai/tools/generate-panel.ts new file mode 100644 index 0000000000..7d79a7c720 --- /dev/null +++ b/apps/cli/ai/tools/generate-panel.ts @@ -0,0 +1,170 @@ +import { tool } from '@anthropic-ai/claude-agent-sdk'; +import { z } from 'zod/v4'; +import { emitEvent } from 'cli/ai/json-events'; +import { runCommand as runStartSiteCommand } from 'cli/commands/site/start'; +import { disconnectFromDaemon } from 'cli/lib/daemon-client'; +import { generateAndDeployScratchPanel } from 'cli/lib/studio-panels-builder'; +import { isServerRunning, sendWpCliCommand } from 'cli/lib/wordpress-server-manager'; +import { errorResult, resolveSite, textResult } from './utils'; + +const PLUGIN_BASENAME = 'studio-panels/studio-panels.php'; +const GUTENBERG_BASENAME = 'gutenberg/gutenberg.php'; +const SCRATCH_DEEP_LINK = '/scratch'; +const PANEL_PAGE_SLUG = 'studio-panels-wp-admin'; + +// Cheap structural checks so the agent gets a fixable error before paying +// for a wp-build round-trip on broken source. +export function validateScratchSource( + source: string +): { ok: true } | { ok: false; errors: string[] } { + const errors: string[] = []; + + if ( ! source || source.trim().length === 0 ) { + return { ok: false, errors: [ 'Source is empty.' ] }; + } + + if ( + ! /\bexport\s+(?:function|const|let|var)\s+stage\b/.test( source ) && + ! /\bexport\s*\{\s*[^}]*\bstage\b[^}]*\}/.test( source ) + ) { + errors.push( + 'Source must export a `stage` value (wp-build route convention). ' + + 'Example: `export const stage = () =>
;`' + ); + } + + const importLines = source.match( /^\s*import\b.*$/gm ) ?? []; + for ( const line of importLines ) { + const fromMatch = line.match( /from\s+['"]([^'"]+)['"]/ ); + if ( ! fromMatch ) continue; + const spec = fromMatch[ 1 ]; + if ( ! spec.startsWith( '@wordpress/' ) ) { + errors.push( + `Disallowed import: \`${ spec }\`. Only \`@wordpress/*\` packages are externalized; ` + + `anything else won't resolve at runtime.` + ); + } + } + + return errors.length === 0 ? { ok: true } : { ok: false, errors }; +} + +async function ensureSiteRunningAndPluginActive( site: { + id: string; + path: string; +} ): Promise< void > { + if ( ! ( await isServerRunning( site.id ) ) ) { + await runStartSiteCommand( site.path, /* skipBrowser */ true, /* skipLogDetails */ true ); + } + try { + await sendWpCliCommand( site.id, [ + 'plugin', + 'install', + 'gutenberg', + '--activate', + '--quiet', + ] ); + } catch { + // Non-fatal — surfaced as an admin notice in the panel page if Gutenberg is truly absent. + } + try { + await sendWpCliCommand( site.id, [ + 'plugin', + 'activate', + PLUGIN_BASENAME, + GUTENBERG_BASENAME, + ] ); + } catch { + // Non-fatal — manual activation still works. + } +} + +export const generatePanelTool = tool( + 'studio_generate_panel', + 'Generates a custom React panel rendered in the Studio site preview pane. This is the way ' + + 'to fulfil any "show me / list / display / edit" request involving the user\'s WordPress ' + + 'data — list of posts/pages/comments/users/CPTs, edit forms, settings screens, ' + + 'dashboards, anything else. Write a complete TSX module exporting a `stage` value ' + + '(wp-build route convention); Studio bundles it via @wordpress/build (~200ms) and renders ' + + 'it inside the studio-panels wp-admin page.\n\n' + + 'CONVENTIONS — follow these so panels stay consistent and idiomatic:\n' + + '1. LISTING entities (posts, pages, comments, users, CPTs, terms): use from ' + + '@wordpress/dataviews. Drive it with `useSelect` + `getEntityRecords` from ' + + '@wordpress/core-data. Define `fields` and a `view` state, wire `onChangeView` so the ' + + 'user can sort/filter/paginate.\n' + + '2. FORMS (settings, single-record edit): use from @wordpress/dataviews. Read ' + + 'the record with `getEntityRecord` and save via `dispatch(coreStore).saveEntityRecord` / ' + + '`saveEditedEntityRecord`. For site settings, the entity is `("root", "site")`.\n' + + '3. DASHBOARDS / multi-source / bespoke layouts: compose @wordpress/components (Card, ' + + 'CardBody, Flex, FlexItem, Notice, Button) with the same core-data fetching pattern.\n' + + '4. CLIENT↔SERVER COMMUNICATION: always go through @wordpress/core-data ' + + '(useSelect + getEntityRecord(s) for reads, dispatch + saveEntityRecord for writes). Do ' + + 'NOT call `apiFetch` directly unless the data is genuinely outside the entity model.\n' + + '5. NEED DATA NOT IN CORE REST? Extend the API: write a small mu-plugin at ' + + '`/wp-content/mu-plugins/studio-extend.php` that registers your endpoint via ' + + '`register_rest_route` (or registers a custom post type with `show_in_rest: true`). ' + + 'Then the panel can read it via `getEntityRecords` against the new route.\n' + + '6. IMPORTS must come from `@wordpress/*` only (externalized to `wp.*` globals). No ' + + 'lodash, axios, react-query, etc. Use `useState/useEffect/useMemo` from ' + + '@wordpress/element and `__()` from @wordpress/i18n for translatable strings.', + { + nameOrPath: z + .string() + .describe( 'The site name or file system path of the local site to render the panel into.' ), + source: z + .string() + .min( 1 ) + .describe( + 'Complete TSX source for the panel. Must export a `stage` value: ' + + '`export const stage = () =>
;`. Imports must come from `@wordpress/*` ' + + 'packages only.' + ), + summary: z + .string() + .optional() + .describe( + 'Optional one-line description of what the panel does, included in the response.' + ), + }, + async ( args ) => { + const validation = validateScratchSource( args.source ); + if ( ! validation.ok ) { + return errorResult( 'Source validation failed:\n - ' + validation.errors.join( '\n - ' ) ); + } + + try { + const site = await resolveSite( args.nameOrPath ); + const result = await generateAndDeployScratchPanel( { + source: args.source, + sitePath: site.path, + } ); + try { + await ensureSiteRunningAndPluginActive( site ); + + const url = `/wp-admin/admin.php?page=${ PANEL_PAGE_SLUG }&p=${ encodeURIComponent( + SCRATCH_DEEP_LINK + ) }`; + const navigatePath = `/studio-auto-login?redirect_to=${ encodeURIComponent( url ) }`; + emitEvent( { + type: 'preview.command', + timestamp: new Date().toISOString(), + kind: 'navigate', + path: navigatePath, + } ); + + const summary = args.summary ? ` — ${ args.summary }` : ''; + return textResult( + `Generated scratch panel${ summary } and deployed to ${ site.name } in ${ result.durationMs }ms (v${ result.bumpedVersion }).` + ); + } finally { + // Daemon bus stays open after `sendWpCliCommand`; close it so + // the agent child can exit and the UI's `winding_down` phase ends. + await disconnectFromDaemon().catch( () => undefined ); + } + } catch ( error ) { + return errorResult( + `Failed to generate panel: ${ error instanceof Error ? error.message : String( error ) }` + ); + } + } +); diff --git a/apps/cli/ai/tools/index.ts b/apps/cli/ai/tools/index.ts index 591eaeca59..60fb9060d7 100644 --- a/apps/cli/ai/tools/index.ts +++ b/apps/cli/ai/tools/index.ts @@ -5,6 +5,7 @@ import { createSiteTool } from './create-site'; import { deletePreviewTool } from './delete-preview'; import { deleteSiteTool } from './delete-site'; import { exportSiteTool } from './export-site'; +import { generatePanelTool } from './generate-panel'; import { importSiteTool } from './import-site'; import { installTaxonomyScriptsTool } from './install-taxonomy-scripts'; import { listConnectedRemoteSitesTool } from './list-connected-remote-sites'; @@ -29,11 +30,17 @@ import { runWpCliTool } from './wp-cli'; import { createWpcomRequestTool } from './wpcom-request'; export { captureCommandOutput } from './utils'; +export { generatePanelTool, validateScratchSource } from './generate-panel'; // Preview-steering tools only belong in the toolset when the Studio desktop UI // is on the other end of the IPC channel — outside of that, navigate/reload -// calls render as noise in the terminal transcript. -const previewSteeringToolDefinitions = [ previewNavigateTool, previewReloadTool ]; +// calls render as noise in the terminal transcript. studio_generate_panel +// also emits `preview.command` events, so it belongs here too. +const previewSteeringToolDefinitions = [ + previewNavigateTool, + previewReloadTool, + generatePanelTool, +]; export const studioToolDefinitions = [ createSiteTool, diff --git a/apps/cli/lib/studio-panels-builder.ts b/apps/cli/lib/studio-panels-builder.ts new file mode 100644 index 0000000000..3f4a79f32a --- /dev/null +++ b/apps/cli/lib/studio-panels-builder.ts @@ -0,0 +1,111 @@ +import { exec } from 'child_process'; +import { existsSync, readFileSync } from 'fs'; +import { writeFile } from 'fs/promises'; +import path from 'path'; +import { promisify } from 'util'; +import { ensureStudioPanelsInstalled } from './studio-panels-installer'; + +const execAsync = promisify( exec ); + +// On-the-fly panel generation: the agent writes TSX into the scratch route's +// source file, this module rebuilds the plugin via wp-build, and the +// installer pushes the rebuilt artifact to the site. Dev-mode only — needs +// the apps/studio-panels/ source tree on disk and writable; a packaged CLI +// build does not include it. + +const SCRATCH_RELATIVE_SOURCE = 'routes/scratch/stage.tsx'; + +interface BuildResult { + bumpedVersion: string; + durationMs: number; +} + +// Walks up from the runtime location until it finds `apps/studio-panels/`'s +// package.json. Returns null if the source tree isn't present. +function findStudioPanelsSourceDir( startDir: string = import.meta.dirname ): string | null { + let current = path.resolve( startDir ); + const root = path.parse( current ).root; + while ( current !== root ) { + const candidate = path.join( current, 'apps', 'studio-panels', 'package.json' ); + if ( existsSync( candidate ) ) { + try { + const pkg = JSON.parse( readFileSync( candidate, 'utf8' ) ) as { name?: string }; + if ( pkg.name === '@studio/panels' ) { + return path.dirname( candidate ); + } + } catch { + // fall through and keep walking + } + } + current = path.dirname( current ); + } + return null; +} + +export class ScratchSourceTreeMissingError extends Error { + constructor() { + super( + 'Could not find apps/studio-panels/ source tree. ' + + 'On-the-fly panel generation requires the monorepo source to be writable. ' + + 'This tool only works in development.' + ); + this.name = 'ScratchSourceTreeMissingError'; + } +} + +// Append `-scratch.` to the base version, incrementing on each regen so +// the installer always sees a newer version. +function bumpScratchVersion( base: string ): string { + const match = base.match( /^(.+?)-scratch\.(\d+)$/ ); + if ( match ) { + return `${ match[ 1 ] }-scratch.${ Number( match[ 2 ] ) + 1 }`; + } + return `${ base }-scratch.1`; +} + +function readVersion( file: string ): string | null { + try { + return readFileSync( file, 'utf8' ).trim() || null; + } catch { + return null; + } +} + +export async function generateAndDeployScratchPanel( { + source, + sitePath, +}: { + source: string; + sitePath: string; +} ): Promise< BuildResult > { + const panelsDir = findStudioPanelsSourceDir(); + if ( ! panelsDir ) { + throw new ScratchSourceTreeMissingError(); + } + + const sourceFile = path.join( panelsDir, SCRATCH_RELATIVE_SOURCE ); + if ( ! existsSync( path.dirname( sourceFile ) ) ) { + throw new Error( `Scratch source directory not found: ${ path.dirname( sourceFile ) }` ); + } + + await writeFile( sourceFile, source, 'utf8' ); + + const versionFile = path.join( panelsDir, 'version.txt' ); + const bumpedVersion = bumpScratchVersion( readVersion( versionFile ) || '0.1.0' ); + + const t0 = Date.now(); + try { + await execAsync( 'npm run build', { cwd: panelsDir, env: process.env } ); + } catch ( err: unknown ) { + const message = err instanceof Error ? err.message : String( err ); + throw new Error( `wp-build failed:\n${ message }` ); + } + + // Overwrite the version.txt that wp-build emitted so the installer sees a + // new release and replaces the on-site copy. + await writeFile( versionFile, bumpedVersion, 'utf8' ); + + await ensureStudioPanelsInstalled( sitePath, panelsDir ); + + return { bumpedVersion, durationMs: Date.now() - t0 }; +} diff --git a/apps/cli/lib/studio-panels-installer.ts b/apps/cli/lib/studio-panels-installer.ts new file mode 100644 index 0000000000..ac930ebc76 --- /dev/null +++ b/apps/cli/lib/studio-panels-installer.ts @@ -0,0 +1,51 @@ +import { cp, mkdir, readFile, rm } from 'fs/promises'; +import path from 'path'; + +const PLUGIN_DIR_NAME = 'studio-panels'; + +// Bundled by `viteStaticCopy` (see vite.config.dev/prod/npm) as a sibling of +// the CLI modules in dist/. +const BUNDLED_PLUGIN_DIR = path.resolve( import.meta.dirname, PLUGIN_DIR_NAME ); + +interface InstallResult { + installed: boolean; + previousVersion: string | null; + currentVersion: string; +} + +async function readVersion( versionFile: string ): Promise< string | null > { + try { + const contents = await readFile( versionFile, 'utf8' ); + return contents.trim() || null; + } catch { + return null; + } +} + +// `sourceDir` defaults to the bundled CLI dist; `studio-panels-builder` passes +// the monorepo source root for the on-the-fly path. +export async function ensureStudioPanelsInstalled( + sitePath: string, + sourceDir: string = BUNDLED_PLUGIN_DIR +): Promise< InstallResult > { + const targetDir = path.join( sitePath, 'wp-content', 'plugins', PLUGIN_DIR_NAME ); + const currentVersion = await readVersion( path.join( sourceDir, 'version.txt' ) ); + + if ( ! currentVersion ) { + throw new Error( + `Studio Panels plugin missing version.txt at ${ sourceDir }. Did the build complete?` + ); + } + + const previousVersion = await readVersion( path.join( targetDir, 'version.txt' ) ); + if ( previousVersion === currentVersion ) { + return { installed: false, previousVersion, currentVersion }; + } + + // Replace wholesale so removed routes from a previous version don't linger. + await rm( targetDir, { recursive: true, force: true } ); + await mkdir( path.dirname( targetDir ), { recursive: true } ); + await cp( sourceDir, targetDir, { recursive: true } ); + + return { installed: true, previousVersion, currentVersion }; +} diff --git a/apps/cli/vite.config.dev.ts b/apps/cli/vite.config.dev.ts index 64853a5e87..11056eab30 100644 --- a/apps/cli/vite.config.dev.ts +++ b/apps/cli/vite.config.dev.ts @@ -3,6 +3,8 @@ import { defineConfig, mergeConfig } from 'vite'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import { baseConfig } from './vite.config.base'; +const studioPanelsRoot = resolve( __dirname, '../studio-panels' ); + export default mergeConfig( baseConfig, defineConfig( { @@ -13,6 +15,18 @@ export default mergeConfig( src: 'ai/plugin', dest: '.', }, + { + src: `${ studioPanelsRoot }/studio-panels.php`, + dest: 'studio-panels', + }, + { + src: `${ studioPanelsRoot }/version.txt`, + dest: 'studio-panels', + }, + { + src: `${ studioPanelsRoot }/build`, + dest: 'studio-panels', + }, ], } ), ], diff --git a/apps/cli/vite.config.npm.ts b/apps/cli/vite.config.npm.ts index 097745de6b..b99c738d89 100644 --- a/apps/cli/vite.config.npm.ts +++ b/apps/cli/vite.config.npm.ts @@ -1,7 +1,10 @@ +import { resolve } from 'path'; import { defineConfig, mergeConfig } from 'vite'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import { baseConfig } from './vite.config.base'; +const studioPanelsRoot = resolve( __dirname, '../studio-panels' ); + export default mergeConfig( baseConfig, defineConfig( { @@ -12,6 +15,18 @@ export default mergeConfig( src: 'ai/plugin', dest: '.', }, + { + src: `${ studioPanelsRoot }/studio-panels.php`, + dest: 'studio-panels', + }, + { + src: `${ studioPanelsRoot }/version.txt`, + dest: 'studio-panels', + }, + { + src: `${ studioPanelsRoot }/build`, + dest: 'studio-panels', + }, ], } ), ], diff --git a/apps/cli/vite.config.prod.ts b/apps/cli/vite.config.prod.ts index f2ce360ab7..86defc395c 100644 --- a/apps/cli/vite.config.prod.ts +++ b/apps/cli/vite.config.prod.ts @@ -7,11 +7,28 @@ import { baseConfig } from './vite.config.base'; const cliNodeModulesPath = resolve( __dirname, 'node_modules' ); const distCliNodeModulesPath = resolve( __dirname, 'dist/cli/node_modules' ); +const studioPanelsRoot = resolve( __dirname, '../studio-panels' ); export default mergeConfig( baseConfig, defineConfig( { plugins: [ + viteStaticCopy( { + targets: [ + { + src: `${ studioPanelsRoot }/studio-panels.php`, + dest: 'studio-panels', + }, + { + src: `${ studioPanelsRoot }/version.txt`, + dest: 'studio-panels', + }, + { + src: `${ studioPanelsRoot }/build`, + dest: 'studio-panels', + }, + ], + } ), ...( existsSync( cliNodeModulesPath ) ? [ viteStaticCopy( { diff --git a/apps/studio-panels/.gitignore b/apps/studio-panels/.gitignore new file mode 100644 index 0000000000..bc6c627f23 --- /dev/null +++ b/apps/studio-panels/.gitignore @@ -0,0 +1,5 @@ +build/ +build-module/ +build-style/ +version.txt +node_modules/ diff --git a/apps/studio-panels/package.json b/apps/studio-panels/package.json new file mode 100644 index 0000000000..557e84e091 --- /dev/null +++ b/apps/studio-panels/package.json @@ -0,0 +1,35 @@ +{ + "name": "@studio/panels", + "version": "0.3.0", + "private": true, + "type": "module", + "description": "WordPress admin panels for Studio agent UI. Uses @wordpress/build's pages routing system; requires Gutenberg (or WP 7.0+) on the host site for the @wordpress/boot, @wordpress/route, and @wordpress/dataviews script modules.", + "license": "GPL-2.0-or-later", + "scripts": { + "build": "wp-build && node scripts/post-build.mjs", + "dev": "wp-build --watch", + "lint": "eslint routes", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "wpPlugin": { + "name": "studio_panels", + "scriptGlobal": false, + "packageNamespace": "studio-panels", + "handlePrefix": "studio-panels", + "pages": [ "studio-panels" ] + }, + "devDependencies": { + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.7", + "@wordpress/build": "^0.13.0", + "@wordpress/components": "32.6.0", + "@wordpress/core-data": "^7.18.0", + "@wordpress/data": "^10.18.0", + "@wordpress/dataviews": "^14.2.0", + "@wordpress/element": "^6.18.0", + "@wordpress/i18n": "^6.18.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "~5.9.3" + } +} diff --git a/apps/studio-panels/routes/scratch/package.json b/apps/studio-panels/routes/scratch/package.json new file mode 100644 index 0000000000..213d22ca6c --- /dev/null +++ b/apps/studio-panels/routes/scratch/package.json @@ -0,0 +1,19 @@ +{ + "name": "@studio-panels/route-scratch", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Studio Panels scratch route — overwritten by studio_generate_panel agent tool.", + "license": "GPL-2.0-or-later", + "route": { + "path": "/scratch", + "page": "studio-panels" + }, + "dependencies": { + "@wordpress/components": "32.6.0", + "@wordpress/core-data": "^7.18.0", + "@wordpress/data": "^10.18.0", + "@wordpress/element": "^6.18.0", + "@wordpress/i18n": "^6.18.0" + } +} diff --git a/apps/studio-panels/routes/scratch/stage.tsx b/apps/studio-panels/routes/scratch/stage.tsx new file mode 100644 index 0000000000..eb719deb31 --- /dev/null +++ b/apps/studio-panels/routes/scratch/stage.tsx @@ -0,0 +1,15 @@ +// Default placeholder. Overwritten by `studio_generate_panel` to mount the +// agent-generated component. The exported name MUST be `stage` — wp-build +// looks for that export to mount the route content. +import { __ } from '@wordpress/i18n'; + +export const stage = () => ( +
+

{ __( 'No scratch panel generated yet' ) }

+

+ { __( + 'This slot is overwritten by the studio_generate_panel agent tool. Ask the agent to generate a custom panel and reload this page.' + ) } +

+
+); diff --git a/apps/studio-panels/scripts/post-build.mjs b/apps/studio-panels/scripts/post-build.mjs new file mode 100644 index 0000000000..baba19c9e5 --- /dev/null +++ b/apps/studio-panels/scripts/post-build.mjs @@ -0,0 +1,57 @@ +// Post-build steps for the studio-panels plugin. +// +// 1. Drop a stub `build/modules/boot/index.min.asset.php` so wp-build's +// generated page-wp-admin.php template stops short-circuiting its enqueue. +// The template assumes you bundle your own copy of `@wordpress/boot` +// (Gutenberg's monorepo case). We don't — Gutenberg provides it. The stub +// just lists boot's classic-script dependencies so the prerequisites +// `