Skip to content

Studio Panels: agent-generated wp-admin UI in the preview pane#3345

Draft
youknowriad wants to merge 21 commits intotrunkfrom
claude/quizzical-leavitt-82c37b
Draft

Studio Panels: agent-generated wp-admin UI in the preview pane#3345
youknowriad wants to merge 21 commits intotrunkfrom
claude/quizzical-leavitt-82c37b

Conversation

@youknowriad
Copy link
Copy Markdown
Contributor

Related issues

  • Related to: agent-driven UI generation in Studio's site preview

How AI was used in this PR

Built end-to-end with Claude Code: scaffolding the @studio/panels workspace, the studio_generate_panel agent tool, the wp-build round-trip pipeline, and the SitePreview Site↔Panel toggle. Reviewers should pay particular attention to:

  • The wp-build asset stub in apps/studio-panels/scripts/post-build.mjs — it works around a wp-build enqueue gate but is fragile to changes in @wordpress/build.
  • HIDE_ADMIN_BAR_CSS / HIDE_ADMIN_BAR_SCRIPT in apps/ui/src/components/site-preview/index.tsx — these mutate WP-rendered DOM inside the webview to remove the admin bar gap; the top: 46px rule on .boot-layout__stage was the surprising bit.
  • apps/cli/lib/studio-panels-builder.ts — runs npm run build against the source tree, so it's dev-only by design.

Proposed Changes

⚠️ Proof of Concept — this is a directional PR, not a merge-ready change. Per CLAUDE.md guidance for vibe-coded features, opening as draft for the team to review architecture and tradeoffs before any production hardening.

Adds a generative-UI capability to Studio: the agent can generate custom wp-admin pages on demand from prompts like "Show me a list of posts".

  • @studio/panels workspace package (apps/studio-panels/): a wp-admin plugin built with @wordpress/build's pages routing system. Ships a scratch route that the agent overwrites.
  • studio_generate_panel agent tool (apps/cli/ai/tools/generate-panel.ts): writes TSX into the scratch route, runs wp-build (~200ms), bumps a -scratch.N version suffix, and pushes the rebuilt artifact into the site. The tool description encodes 6 conventions (DataViews for lists, DataForm for forms, @wordpress/core-data for I/O, mu-plugin extension for custom REST, @wordpress/* imports only).
  • Site↔Panel toggle in the preview pane (apps/ui/src/components/site-preview/): two <webview>s kept mounted with CSS visibility: hidden so toggling preserves each side's in-page state. Driven by a SessionUIProvider reducer with site / panel slots (apps/ui/src/hooks/use-session-ui.tsx).
  • WP admin bar hidden in both site and panel previews via injected CSS + JS that beats the cascade and survives @wordpress/boot's async layout mounts.
  • Auto-bootstrap: the panel tool starts the site if needed, installs Gutenberg via wp plugin install gutenberg --activate, activates the panels plugin, navigates the preview through /studio-auto-login, and disconnects the daemon bus so the agent child can exit.

Dev-mode only: requires the apps/studio-panels/ source tree on disk and writable. A packaged CLI build does not include it.

Testing Instructions

  1. Pull the branch and run npm install (lockfile changes for the new workspace).
  2. npm run cli:build && npm start — boot the desktop app.
  3. Open a chat session for any local site, ensure the preview pane is visible.
  4. Ask the agent: "Show me a list of posts" (or pages, comments, users…).
  5. Expect: agent calls studio_generate_panel, the preview navigates to the panel, the Site/Panel toggle in the preview header becomes enabled, and the rendered panel uses <DataViews> against getEntityRecords.
  6. Toggle Site↔Panel — both sides should retain their state (scroll, filters).
  7. Verify the WP admin bar is hidden in both Site (when logged-in front-end) and Panel modes, with no top gap on .boot-layout__stage.
  8. Ask follow-up: "Add a settings form for the site title" — expect a <DataForm> against ('root', 'site').

Pre-merge Checklist

  • npm run typecheck passes
  • npm test apps/cli/ai/tests/tools.test.ts passes (21/21)
  • npx eslint clean on changed files
  • npm run cli:build succeeds
  • Architecture review (see PoC note above)
  • Decide whether this should ship in a packaged build or stay dev-only
  • Have you checked for TypeScript, React or other console errors?

🤖 Generated with Claude Code

youknowriad and others added 21 commits May 4, 2026 14:42
Initial wp-build-based plugin shell that will host agent-driven wp-admin
panels. Wires the new workspace into root scripts and lint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
list/ renders a parameterized DataView for any post type, fed by
core-data's getEntityRecords with full URL-driven filter/page/search.
scratch/ is the slot studio_generate_panel overwrites with on-the-fly
TSX. Both follow wp-build's routes/ convention so the generated page
auto-discovers them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- studio_show_panel and studio_generate_panel agent tools (one file each
  under apps/cli/ai/tools/), wired into the preview-steering set so they
  only register when Studio desktop is on the other end of the IPC
- studio-panels-installer copies the bundled plugin into a site by version
  comparison; studio-panels-builder writes agent TSX into the scratch route
  and runs wp-build for on-the-fly generation
- Both tools auto-start the site, install Gutenberg if missing, activate
  the panels plugin, navigate the preview through /studio-auto-login, and
  disconnect the daemon bus so the agent child can exit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
viteStaticCopy now copies apps/studio-panels/{studio-panels.php, version.txt,
build/} into dist/cli/studio-panels/ for all three CLI build configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… panels

- Track sitePath and panelPath independently; agent navigation events
  route to one or the other based on URL pattern (isPanelPath helper).
- Header gains a small Site/Panel segmented toggle. Panel button is
  disabled until the agent has actually generated a panel.
- WebviewSurface gains an enableInspector prop (default true). On panel
  pages it's false, so the annotate button never appears — those pages
  are agent-rendered UI, not site content to comment on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…st stage

The wp-build route loader pattern requires useLoaderData() to read the data
in the stage, but @wordpress/route 0.5.0 (the shipping npm version) doesn't
export that hook. Drop the loader and read URL search params directly with
useSearch() — same effect, fewer indirection layers, works with the route
package version Gutenberg currently ships.

Bumps panels to v0.2.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The model needs to see these alongside preview_navigate / preview_reload
to choose them for "show me posts" requests. Without the listing it falls
back to wp_cli post list (whose own description includes "user list" as
an example, which the model generalises).

Two neutral one-line entries, no defensive language.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trunk landed two changes that touched the same areas:
- apps/cli/ai/tools.ts split into apps/cli/ai/tools/*.ts (one tool per
  file). My show-panel/generate-panel files slot in cleanly. Dropped my
  redundant helpers.ts in favour of trunk's shared utils.ts. Registered
  the panel tools in the previewSteeringToolDefinitions array.
- apps/ui SessionUIProvider/useSessionPreviewUI/useSessionCommands
  reducer that hoisted preview state out of SitePreview. Reworked my
  Site/Panel toggle and panel-path detection to fit:
    - isPanelPath now lives in use-session-ui (single source of truth)
    - PreviewUIState gains mode + sitePath/panelPath
    - preview/navigate routes paths into the right slot via isPanelPath
    - SitePreview accepts mode/setMode/hasPanel as props (passed by
      session-view from useSessionPreviewUI)
    - enableInspector on WebviewSurface derives from current path

Also adopted trunk's `npm run lint --workspaces --if-present` pattern;
@studio/panels gets its own `lint` script for routes/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SitePreview is only ever rendered inside SessionUIProvider, so the
controlled-component motivation for prop-drilling path/reloadNonce/mode/
setMode/hasPanel doesn't apply. Reading from useSessionPreviewUI()
directly drops six props from the call site and matches the convention
useConnector / useSession / useSites already use elsewhere in the file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…el toggle

Previously the single SitePreview webview re-navigated on every toggle —
including running through /studio-auto-login again — which lost in-page
state (scroll position, DataView filters) and felt sluggish.

Restructure SessionUI's preview state into two slots (`site` and `panel`,
each with its own path + reloadNonce). SitePreview renders one webview
per slot, kept mounted side-by-side; CSS visibility:hidden swaps which
one is shown. The agent's preview/navigate action only bumps the slot it
targets — the other side keeps its previous URL and DOM untouched, so
toggling back is instant and the in-page state is intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Studio preview pane has its own chrome (window controls, mode toggle,
external-link button); the WP admin bar at the top of every wp-admin and
logged-in front-end page is visual noise. Inject a small stylesheet via
the webview's insertCSS() on each dom-ready so it covers both the site
slot and the panel slot, and survives navigation within either.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WP reserves admin-bar height via !important rules in two media queries
(46px at <=782px and a separate <=600px block) on top of the desktop
32px rule. The single top-level `html` rule could be outranked by the
later mobile rule depending on cascade order, leaving a visible gap on
narrow widths. Override the same selectors inside both media queries so
the preview is edge-to-edge regardless of width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wp-admin chrome (`#wpwrap`, `#wpcontent`, `#wpbody`, `#wpbody-content`)
adds its own top padding on top of the html/body reservation to leave
room for the admin bar. Zeroing only the body left a wrapper-level gap
visible on the panel surface. Cover those wrapper IDs at every breakpoint
the same way as the body selectors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CSS injection wasn't winning the cascade against WP's admin-bar height
reservation in some configurations (mobile media queries, theme rules
with their own !important, body inline styles). Switch to running a
small JS snippet via executeJavaScript on every dom-ready: it sets
inline `style.setProperty(..., 'important')` on html/body and the four
wp-admin wrapper IDs, which beats any external rule unconditionally,
and a MutationObserver re-applies the fix if WP re-renders the bar
later (e.g. via heartbeat).

Also wire Cmd/Ctrl+Shift+I to call `webview.openDevTools()` so the
inner page is finally inspectable from inside Studio's preview pane —
otherwise debugging anything in the iframe is essentially impossible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous CSS / JS reset only touched a known list of wp-admin wrapper
IDs. Some surfaces (themes, custom admin pages) reserve admin-bar
height on a different element that we don't enumerate, so a gap
remained even with the bar hidden.

After the known-id pass, walk the leftmost-deepest path from `body` for
up to 12 levels and zero margin-top / padding-top / border-top-width on
any element whose computed value contributes top space. Logs each
zeroed element to the inner page's console so the source can be
inspected via Cmd/Ctrl+Shift+I when debugging.

Also broadens the MutationObserver to catch style/class attribute
changes in case WP toggles admin-bar classes after first paint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…slots

`.boot-layout--single-page .boot-layout__stage` and `.boot-layout__inspector`
are the layout primitives @wordpress/boot wraps the studio-panels routes
in. They reserve their own admin-bar height via a class-based rule that
the previous getElementById pass and the leftmost-walker missed (the
walker bails after a few levels and these wrappers sit deeper inside).

Add them as explicit selectors so the panel surface lands flush with
the preview pane header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
getToolDisplayName fell back to the raw mcp__studio__… ID for ~13
tools (push/pull/import/export, taxonomist, need-for-speed, rank-me-up,
list_connected_remote_sites, open/wait_for_annotations, wpcom_request,
plus the new studio_show_panel and studio_generate_panel) and a handful
of Claude Code preset tools (AskUserQuestion, WebFetch, WebSearch,
NotebookEdit), so the chat UI showed cryptic raw names. Add localized
labels for all of them, plus getToolDetail clauses so the panel tools
surface their `kind · postType` / `summary` and wpcom_request shows
`METHOD path`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The JS-based fix only zeroes elements that exist when it runs at
dom-ready. @wordpress/boot mounts `.boot-layout--single-page
.boot-layout__stage` (and `__inspector`) asynchronously after that, so
the very first paint of the panel page still showed a top gap before
the MutationObserver caught up.

Two complementary fixes:
- Inject a high-specificity stylesheet via insertCSS so the rule is
  in the cascade before boot's elements arrive — covers them as they
  render, no observer round-trip needed.
- Re-run the JS fix on requestAnimationFrame + 100ms + 500ms timeouts
  to backstop the observer for boot's typical mount timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_panel

The parameterized list panel was instant but capped at "title/date/
status/id/type/author" columns and one `kind: list` shape. Generative
TSX writes a fresh panel per request — slower (~3-6s vs <1s) but
unconstrained: any column set, any layout, mixed sources, edit forms,
dashboards. The trade-off is intentional now that on-the-fly works
reliably.

Removed:
- apps/cli/ai/tools/show-panel.ts (and its export from tools/index.ts)
- apps/studio-panels/routes/list/ (its DataView is what
  generate-panel emits anyway)
- mcp__studio__studio_show_panel entries in tools/common/ai/tools.ts

Added — guidance for the agent so generated panels stay idiomatic:
- studio_generate_panel description carries six numbered conventions
  (DataViews for lists, DataForm for forms, components for dashboards,
  core-data for I/O, mu-plugin extension for custom REST, @wordpress/*
  imports only).
- A "Generate panels" section in the local-mode system prompt mirrors
  the same conventions so they're in context for the whole session.

Bumps panels to 0.3.0 so the installer re-copies on next tool call,
clearing the dead routes/list/ from existing sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… header

The remaining gap turned out to be `top: 46px` on
`.boot-layout--single-page .boot-layout__stage|__inspector` (the rule
@wordpress/boot uses to leave room for the mobile admin bar at 46px).
killTopSpace only zeroed margin/padding/border so positioning offsets
slipped through. Add killTopOffset that detects elements with
non-zero computed `top` and absolute/fixed/relative positioning, and
zeroes both `top` and resets `height: 100%`. Also extend the static
CSS injection to set top/height on the same selectors so it covers
elements that arrive after the JS runs.

Add a "Inspect" icon button to the preview header (uses @wordpress/icons
`code` icon) which calls openDevTools() on the active webview surface.
The earlier Cmd+Shift+I shortcut on the host window only fired when the
host had focus; clicking into the iframe trapped the keystroke. The
button is always reachable. WebviewSurface gains a forwardRef +
useImperativeHandle exposing openDevTools so SitePreview can call it on
whichever slot is currently visible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…comments

- apps/ui/site-preview: remove the Inspect button + WebviewSurfaceHandle
  (forwardRef/useImperativeHandle gone), drop the body-walker, console
  diagnostics, rAF/setTimeout retries, and host keydown handler. Slim down
  HIDE_ADMIN_BAR_CSS/SCRIPT to the minimal set that beats the cascade.
- apps/cli/lib/studio-panels-installer: drop unused getBundledPluginDir /
  getInstalledPluginDir / pluginDir field, inline the pathExists helper,
  collapse JSDoc to a one-liner.
- apps/cli/lib/studio-panels-builder: shorter comments, simpler readVersion,
  drop the redundant `searched` constructor arg.
- apps/cli/ai/tools/generate-panel: collapse validateScratchSource JSDoc
  to a one-line WHY comment.
- apps/ui/use-session-ui: trim comment cruft to the load-bearing WHY lines.
- apps/studio-panels/routes/scratch: revert to the placeholder so the file
  typechecks; the agent overwrites it on demand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant