-
{ __( 'Agentic features' ) }
-
+
+
+
+
{ __( 'Agentic features' ) }
+
{ __(
'Chat with an agent that builds and edits your sites. Turning this off hides chat — your existing conversations are kept.'
) }
-
+
savePreferences.mutate( { agenticFeaturesEnabled: ! enabled } ) }
/>
-
+
+ { signedOut ? (
+
{ __( 'You must log in for agentic features.' ) }
+ ) : null }
);
}
export function AiPanel() {
const connector = useConnector();
+ // Offline is the one whole-tab state worth a banner here; the signed-out
+ // sign-in pitch and the usage meters (AI credits, preview sites) live in the
+ // account sidebar.
+ const { reason } = usePreviewAgenticFeatures();
+
return (
-
-
- { connector.capabilities.agentInstructions &&
}
+
+
+
+ { reason === 'offline' ?
: null }
+
+ { connector.capabilities.agentInstructions &&
}
+
+
);
}
diff --git a/apps/ui/src/components/settings-view/index.test.tsx b/apps/ui/src/components/settings-view/index.test.tsx
index ff9b99b416..d029fcba9d 100644
--- a/apps/ui/src/components/settings-view/index.test.tsx
+++ b/apps/ui/src/components/settings-view/index.test.tsx
@@ -82,10 +82,6 @@ vi.mock( './ai-panel', () => ( {
AiPanel: () =>
,
} ) );
-vi.mock( './skills-panel', () => ( {
- SkillsPanel: () => null,
-} ) );
-
vi.mock( './studio-cli-section', () => ( {
StudioCliSection: () => null,
} ) );
@@ -101,7 +97,7 @@ vi.mock( '@/hooks/use-color-scheme', () => ( {
} ) );
vi.mock( './mcp-panel', () => ( {
- McpPanel: () =>
,
+ McpSection: () =>
,
} ) );
vi.mock( '@/data/core/query-client', () => ( {
@@ -117,16 +113,6 @@ vi.mock( '@/data/queries/use-user-preferences', () => ( {
useUserPreferences: vi.fn(),
} ) );
-vi.mock( '@/data/queries/use-wapuu-score', () => ( {
- useWapuuScore: () => ( { data: null } ),
-} ) );
-
-// The mocked Tabs render every panel unconditionally; the usage panel has its
-// own test file.
-vi.mock( './usage-panel', () => ( {
- UsagePanel: () => null,
-} ) );
-
vi.mock( '@/hooks/use-traffic-light-space', () => ( {
useTrafficLightSpace: () => false,
} ) );
@@ -225,30 +211,13 @@ describe( 'SettingsView', () => {
it( 'renders the AI tab with its panel', () => {
render(
);
- expect( screen.getByRole( 'button', { name: 'AI' } ) ).toBeInTheDocument();
- expect( screen.getByTestId( 'ai-panel' ) ).toBeInTheDocument();
- } );
-
- it( 'offers the AI tab on every host, since chat can be toggled anywhere', () => {
- useConnectorMock.mockReturnValue( {
- selectDefaultSiteDirectory,
- capabilities: { agentInstructions: false, switchToClassicUi: false },
- } as never );
-
- render(
);
-
- expect( screen.getByRole( 'button', { name: 'AI' } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'button', { name: 'Agent' } ) ).toBeInTheDocument();
expect( screen.getByTestId( 'ai-panel' ) ).toBeInTheDocument();
} );
- it( 'offers Switch to classic in the Settings tab when the host ships the classic UI', () => {
- render(
);
-
- expect( screen.getByRole( 'heading', { name: 'Studio experience' } ) ).toBeInTheDocument();
- expect( screen.getByRole( 'button', { name: 'Switch to classic' } ) ).toBeInTheDocument();
- } );
-
- it( 'hides Switch to classic when there is no classic UI to switch to', () => {
+ it( 'always shows the AI tab, even when the host has no AI settings to offer', () => {
+ // The AI tab now also holds Usage, which is shown for every host, so the
+ // tab is always present — the AI-specific sections gate themselves inside.
useConnectorMock.mockReturnValue( {
selectDefaultSiteDirectory,
capabilities: { agentInstructions: false, switchToClassicUi: false },
@@ -256,20 +225,22 @@ describe( 'SettingsView', () => {
render(
);
- expect( screen.queryByRole( 'button', { name: 'Switch to classic' } ) ).not.toBeInTheDocument();
+ expect( screen.getByRole( 'button', { name: 'Agent' } ) ).toBeInTheDocument();
+ expect( screen.getByTestId( 'ai-panel' ) ).toBeInTheDocument();
} );
- it( 'recognizes the keyboard tab id', () => {
- expect( isSettingsTab( 'keyboard' ) ).toBe( true );
+ it( 'recognizes valid settings tab ids', () => {
+ expect( isSettingsTab( 'preferences' ) ).toBe( true );
+ expect( isSettingsTab( 'ai' ) ).toBe( true );
+ expect( isSettingsTab( 'keyboard' ) ).toBe( false );
expect( isSettingsTab( 'unknown' ) ).toBe( false );
} );
- it( 'renders keyboard shortcut sections', () => {
- render(
);
+ it( 'renders keyboard shortcut sections inside Settings', () => {
+ render(
);
- expect( screen.getByRole( 'button', { name: 'Keyboard' } ) ).toBeInTheDocument();
expect( screen.getByRole( 'heading', { name: 'Composer' } ) ).toBeInTheDocument();
- expect( screen.getByRole( 'heading', { name: 'Site preview' } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'heading', { name: 'Preview' } ) ).toBeInTheDocument();
expect( screen.getByText( 'New chat' ) ).toBeInTheDocument();
expect( screen.getByText( 'Send message' ) ).toBeInTheDocument();
expect( screen.getByLabelText( 'Control + Comma' ) ).toBeInTheDocument();
diff --git a/apps/ui/src/components/settings-view/index.tsx b/apps/ui/src/components/settings-view/index.tsx
index c3737d3947..5ed678719c 100644
--- a/apps/ui/src/components/settings-view/index.tsx
+++ b/apps/ui/src/components/settings-view/index.tsx
@@ -1,7 +1,6 @@
import { supportedLocaleNames } from '@studio/common/lib/locale';
import { SUPPORTED_EDITORS, supportedEditorConfig } from '@studio/common/lib/user-settings/editor';
import { SUPPORTED_TERMINALS, terminalConfig } from '@studio/common/lib/user-settings/terminal';
-import { CheckboxControl } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { close, file, Icon } from '@wordpress/icons';
import { Button, IconButton, SelectControl } from '@wordpress/ui';
@@ -17,13 +16,15 @@ import { useTrafficLightSpace } from '@/hooks/use-traffic-light-space';
import { AccountSection } from './account-section';
import { AiPanel } from './ai-panel';
import { KeyboardPanel } from './keyboard-panel';
-import { McpPanel } from './mcp-panel';
+import { McpSection } from './mcp-panel';
import { UNSET, toPreferencesFormData, toPreferencesPatch } from './preferences';
-import { SkillsPanel } from './skills-panel';
+import {
+ SettingsPreviewPanel,
+ SettingsPreviewProvider,
+ SHOW_PREVIEW_CONTROLS,
+} from './settings-preview';
import { StudioCliSection } from './studio-cli-section';
import styles from './style.module.css';
-import { UsagePanel } from './usage-panel';
-import { WapuuScore } from './wapuu-score';
import type { PreferencesFormData } from './preferences';
import type {
ColorScheme,
@@ -35,7 +36,7 @@ import type {
} from '@/data/core';
import type { ReactNode } from 'react';
-const SETTINGS_TABS = [ 'preferences', 'ai', 'usage', 'keyboard', 'skills', 'mcp' ] as const;
+const SETTINGS_TABS = [ 'preferences', 'ai' ] as const;
type TabId = ( typeof SETTINGS_TABS )[ number ];
@@ -86,6 +87,13 @@ const QUIT_SITES_BEHAVIOR_ELEMENTS: {
{ value: 'stop', label: __( 'Stop sites' ) },
];
+type AnalyticsChoice = 'share' | 'off';
+
+const ANALYTICS_ELEMENTS: { value: AnalyticsChoice; label: string }[] = [
+ { value: 'share', label: __( 'Share anonymous data' ) },
+ { value: 'off', label: __( 'Don’t share' ) },
+];
+
const LOCALE_ELEMENTS: { value: SupportedLocale; label: string }[] = Object.entries(
supportedLocaleNames
).map( ( [ value, label ] ) => ( { value: value as SupportedLocale, label } ) );
@@ -105,11 +113,7 @@ function SettingsHeader() {
{ __( 'Settings' ) }
- { __( 'AI' ) }
- { __( 'Usage' ) }
- { __( 'Keyboard' ) }
- { __( 'Skills' ) }
- { __( 'MCP' ) }
+ { __( 'Agent' ) }
{ onClose ? (
@@ -138,13 +142,13 @@ function PreferenceRow( {
children: ReactNode;
} ) {
return (
-
-
-
{ title }
- { description ?
{ description }
: null }
+
+
+ { title }
+ { description ? { description } : null }
-
{ children }
-
+
{ children }
+
);
}
@@ -240,26 +244,35 @@ function DefaultSiteDirectoryField( { value, onSelect }: { value: string; onSele
);
}
+// Leaving the agentic UI entirely is a separate, heavier action than the AI
+// tab's chat toggle: it reloads the window into the classic Studio interface.
+// Only hosts that ship the classic renderer can switch (see capabilities).
function StudioExperienceSection() {
const connector = useConnector();
if ( ! connector.capabilities.switchToClassicUi ) {
return null;
}
return (
-
-
- void connector.disableAgenticUi() }
- >
- { __( 'Switch to classic' ) }
-
-
+
+
+
+
{ __( 'Studio Beta' ) }
+
+ { __( 'You’re using the new Studio with AI chat and a built-in site preview.' ) }
+
+
+
+ void connector.disableAgenticUi() }
+ >
+ { __( 'Switch to classic' ) }
+
+
+
);
}
@@ -280,65 +293,75 @@ function PreferencesPanel( {
onChange: ( update: Partial< PreferencesFormData > ) => void;
} ) {
return (
-
-
- { __( 'General' ) }
- { saveError ? (
-
- { __( 'An error occurred while saving settings. Please try again.' ) }
-
- ) : null }
-
-
- onChange( { locale } ) }
- />
-
-
-
- label={ __( 'Preferred editor' ) }
- value={ data.editor }
- options={ editorElements( installedApps ) }
- onChange={ ( editor ) => onChange( { editor } ) }
- />
-
-
-
- label={ __( 'Preferred terminal' ) }
- value={ data.terminal }
- options={ terminalElements( installedApps ) }
- onChange={ ( terminal ) => onChange( { terminal } ) }
- />
-
-
-
-
- label={ __( 'When quitting with running sites' ) }
- className={ styles.selectControlWide }
- value={ data.quitSitesBehavior }
- options={ QUIT_SITES_BEHAVIOR_ELEMENTS }
- onChange={ ( quitSitesBehavior ) => onChange( { quitSitesBehavior } ) }
- />
-
-
- onChange( { analyticsEnabled } ) }
- />
-
-
+
-
-
-
+
+
+
+
+
{ __( 'General' ) }
+
+
+ { saveError ? (
+
+ { __( 'An error occurred while saving settings. Please try again.' ) }
+
+ ) : null }
+
+
+
+ onChange( { locale } ) }
+ />
+
+
+
+ label={ __( 'Preferred editor' ) }
+ value={ data.editor }
+ options={ editorElements( installedApps ) }
+ onChange={ ( editor ) => onChange( { editor } ) }
+ />
+
+
+
+ label={ __( 'Preferred terminal' ) }
+ value={ data.terminal }
+ options={ terminalElements( installedApps ) }
+ onChange={ ( terminal ) => onChange( { terminal } ) }
+ />
+
+
+
+
+ label={ __( 'When quitting with running sites' ) }
+ className={ styles.selectControlWide }
+ value={ data.quitSitesBehavior }
+ options={ QUIT_SITES_BEHAVIOR_ELEMENTS }
+ onChange={ ( quitSitesBehavior ) => onChange( { quitSitesBehavior } ) }
+ />
+
+
+
+ label={ __( 'Help improve Studio by sharing anonymous usage data' ) }
+ className={ styles.selectControlAuto }
+ value={ data.analyticsEnabled ? 'share' : 'off' }
+ options={ ANALYTICS_ELEMENTS }
+ onChange={ ( choice ) => onChange( { analyticsEnabled: choice === 'share' } ) }
+ />
+
+
+
+
+
+
+
+
);
}
@@ -416,47 +439,38 @@ export function SettingsView( {
};
return (
-
-
{
- if ( tabId && isSettingsTab( tabId ) ) {
- onTabChange( tabId );
- }
- } }
- >
-
+
+
+
{
+ if ( tabId && isSettingsTab( tabId ) ) {
+ onTabChange( tabId );
+ }
+ } }
+ >
+
-
-
-
- void handleSelectDefaultDirectory() }
- onChange={ handleChange }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ void handleSelectDefaultDirectory() }
+ onChange={ handleChange }
+ />
+
+
+
+
+
-
-
-
+
+
+ { SHOW_PREVIEW_CONTROLS ? : null }
+
);
}
diff --git a/apps/ui/src/components/settings-view/keyboard-panel.tsx b/apps/ui/src/components/settings-view/keyboard-panel.tsx
index 5a6946d631..9ce465ba2a 100644
--- a/apps/ui/src/components/settings-view/keyboard-panel.tsx
+++ b/apps/ui/src/components/settings-view/keyboard-panel.tsx
@@ -33,7 +33,7 @@ function getShortcutSections( isApple: boolean ): ShortcutSection[] {
const navModifierKey = isApple ? '⌘' : 'Alt';
return [
{
- title: __( 'General' ),
+ title: __( 'Global' ),
shortcuts: [ { label: __( 'Open settings' ), keys: [ modifierKey, ',' ] } ],
},
{
@@ -46,9 +46,9 @@ function getShortcutSections( isApple: boolean ): ShortcutSection[] {
],
},
{
- title: __( 'Site preview' ),
+ title: __( 'Preview' ),
shortcuts: [
- { label: __( 'Toggle site preview' ), keys: [ modifierKey, 'Shift', 'B' ] },
+ { label: __( 'Toggle preview' ), keys: [ modifierKey, 'Shift', 'B' ] },
{ label: __( 'Reload preview' ), keys: [ modifierKey, 'R' ] },
{ label: __( 'Go back in preview' ), keys: [ navModifierKey, '←' ] },
{ label: __( 'Go forward in preview' ), keys: [ navModifierKey, '→' ] },
@@ -72,22 +72,39 @@ function ShortcutKeys( { keys }: { keys: string[] } ) {
);
}
-export function KeyboardPanel() {
+function ShortcutGroup( { section }: { section: ShortcutSection } ) {
return (
-
- { getShortcutSections( isAppleOS() ).map( ( section ) => (
-
- { section.title }
-
- { section.shortcuts.map( ( shortcut ) => (
-
- { shortcut.label }
-
-
- ) ) }
-
-
- ) ) }
+
+
{ section.title }
+
+ { section.shortcuts.map( ( shortcut ) => (
+
+
+ { shortcut.label }
+
+
+
+ ) ) }
+
);
}
+
+export function KeyboardPanel() {
+ const [ globalSection, ...columnSections ] = getShortcutSections( isAppleOS() );
+ return (
+
+
+
+
{ __( 'Keyboard' ) }
+
+
+
+
+ { columnSections.map( ( section ) => (
+
+ ) ) }
+
+
+ );
+}
diff --git a/apps/ui/src/components/settings-view/mcp-panel.test.tsx b/apps/ui/src/components/settings-view/mcp-panel.test.tsx
index f8549b4bc4..d59b966277 100644
--- a/apps/ui/src/components/settings-view/mcp-panel.test.tsx
+++ b/apps/ui/src/components/settings-view/mcp-panel.test.tsx
@@ -3,7 +3,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { Tooltip } from '@wordpress/ui';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useConnector } from '@/data/core';
-import { McpPanel } from './mcp-panel';
+import { McpSection } from './mcp-panel';
import type { ReactNode } from 'react';
vi.mock( '@/components/learn-more', () => ( {
@@ -20,7 +20,7 @@ function Providers( { children }: { children: ReactNode } ) {
return
{ children } ;
}
-describe( 'McpPanel', () => {
+describe( 'McpSection', () => {
const copyText = vi.fn();
beforeEach( () => {
@@ -30,7 +30,7 @@ describe( 'McpPanel', () => {
} );
it( 'copies the MCP configuration and shows copied feedback once the copy resolves', async () => {
- render(
, { wrapper: Providers } );
+ render(
, { wrapper: Providers } );
expect( screen.getByRole( 'heading', { name: 'MCP' } ) ).toBeInTheDocument();
expect( screen.getByText( /MCP lets other AI tools talk to Studio/ ) ).toBeInTheDocument();
@@ -48,7 +48,7 @@ describe( 'McpPanel', () => {
copyText.mockRejectedValueOnce( error );
try {
- render(
, { wrapper: Providers } );
+ render(
, { wrapper: Providers } );
fireEvent.click( screen.getByRole( 'button', { name: 'Copy MCP configuration' } ) );
diff --git a/apps/ui/src/components/settings-view/mcp-panel.tsx b/apps/ui/src/components/settings-view/mcp-panel.tsx
index aaebedca54..8694eecef2 100644
--- a/apps/ui/src/components/settings-view/mcp-panel.tsx
+++ b/apps/ui/src/components/settings-view/mcp-panel.tsx
@@ -4,28 +4,30 @@ import { CopyButton } from '@/components/copy-button';
import { LearnMoreLink } from '@/components/learn-more';
import styles from './style.module.css';
-export function McpPanel() {
+export function McpSection() {
const configJson = getMcpServerConfigJson();
return (
-
-
- { __( 'MCP' ) }
-
- { __(
- 'MCP lets other AI tools talk to Studio. Use it when you want an assistant outside Studio to create, configure, or inspect your local WordPress sites through the same site controls.'
- ) }{ ' ' }
-
-
-
-
{ configJson }
-
+
+
+
+
{ __( 'MCP' ) }
+
+ { __(
+ 'MCP lets other AI tools talk to Studio. Use it when you want an assistant outside Studio to create, configure, or inspect your local WordPress sites through the same site controls.'
+ ) }{ ' ' }
+
+
-
-
+
+
+
);
}
diff --git a/apps/ui/src/components/settings-view/settings-preview.tsx b/apps/ui/src/components/settings-view/settings-preview.tsx
new file mode 100644
index 0000000000..e4773f3d7e
--- /dev/null
+++ b/apps/ui/src/components/settings-view/settings-preview.tsx
@@ -0,0 +1,278 @@
+/**
+ * TEMP — design-only scenario control.
+ *
+ * A floating panel (dev builds only) that forces the settings UI signed-in or
+ * signed-out, so we can eyeball how the account sidebar and agent tab adapt
+ * without actually logging out.
+ *
+ * To remove: delete this file, drop `
` +
+ * `SettingsPreviewProvider` from index.tsx, and swap the `usePreview*` hooks
+ * back to `useAuthUser` / `useAgenticFeatures` in account-section.tsx and
+ * usage-panel.tsx.
+ */
+import { createContext, useContext, useMemo, useState } from 'react';
+import { useAgenticFeatures } from '@/data/queries/use-agentic-features';
+import { useAuthUser } from '@/data/queries/use-auth-user';
+import type { AuthUser } from '@/data/core';
+import type { AgenticFeatures } from '@/data/queries/use-agentic-features';
+import type { CSSProperties, ReactNode } from 'react';
+
+export const SHOW_PREVIEW_CONTROLS = import.meta.env.DEV && import.meta.env.MODE !== 'test';
+
+type AuthScenario = 'default' | 'in' | 'out';
+
+interface PreviewOverrides {
+ auth: AuthScenario;
+}
+
+const DEFAULT_OVERRIDES: PreviewOverrides = {
+ auth: 'default',
+};
+
+const PREVIEW_USER: AuthUser = {
+ id: 424242,
+ displayName: 'Preview User',
+ email: 'preview@example.com',
+};
+
+interface PreviewContextValue {
+ overrides: PreviewOverrides;
+ setOverride: ( key: keyof PreviewOverrides, value: string ) => void;
+ reset: () => void;
+ active: boolean;
+}
+
+const PreviewContext = createContext< PreviewContextValue >( {
+ overrides: DEFAULT_OVERRIDES,
+ setOverride: () => {},
+ reset: () => {},
+ active: false,
+} );
+
+export function SettingsPreviewProvider( { children }: { children: ReactNode } ) {
+ const [ overrides, setOverrides ] = useState< PreviewOverrides >( DEFAULT_OVERRIDES );
+
+ const value = useMemo< PreviewContextValue >(
+ () => ( {
+ overrides,
+ setOverride: ( key, next ) =>
+ setOverrides( ( prev ) => ( { ...prev, [ key ]: next } ) as PreviewOverrides ),
+ reset: () => setOverrides( DEFAULT_OVERRIDES ),
+ active: Object.values( overrides ).some( ( scenario ) => scenario !== 'default' ),
+ } ),
+ [ overrides ]
+ );
+
+ return
{ children } ;
+}
+
+function usePreviewOverrides() {
+ return useContext( PreviewContext );
+}
+
+// --- Override-aware wrappers around the real data hooks ---------------------
+
+export function usePreviewAuthUser(): { data: AuthUser | null | undefined; isLoading: boolean } {
+ const { overrides } = usePreviewOverrides();
+ const { data, isLoading } = useAuthUser();
+
+ if ( overrides.auth === 'in' ) {
+ return { data: data ?? PREVIEW_USER, isLoading: false };
+ }
+ if ( overrides.auth === 'out' ) {
+ return { data: null, isLoading: false };
+ }
+ return { data, isLoading };
+}
+
+export function usePreviewAgenticFeatures(): AgenticFeatures & { isReady: boolean } {
+ const { overrides } = usePreviewOverrides();
+ const real = useAgenticFeatures();
+
+ if ( overrides.auth === 'out' ) {
+ return { ...real, enabled: false, chatEnabled: false, reason: 'signed-out', isReady: true };
+ }
+ // Forcing signed-in only clears a signed-out reason — offline still wins.
+ if ( overrides.auth === 'in' && real.reason === 'signed-out' ) {
+ return { ...real, enabled: true, reason: null, isReady: true };
+ }
+ return real;
+}
+
+// --- Floating control panel -------------------------------------------------
+
+const SCENARIO_GROUPS: {
+ key: keyof PreviewOverrides;
+ label: string;
+ options: { value: string; label: string }[];
+}[] = [
+ {
+ key: 'auth',
+ label: 'Account',
+ options: [
+ { value: 'default', label: 'Default' },
+ { value: 'in', label: 'Signed in' },
+ { value: 'out', label: 'Signed out' },
+ ],
+ },
+];
+
+const panelStyle: CSSProperties = {
+ position: 'fixed',
+ insetBlockEnd: '16px',
+ insetInlineEnd: '16px',
+ zIndex: 9999,
+ inlineSize: '244px',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '12px',
+ padding: '14px',
+ borderRadius: '10px',
+ border: '1px solid var(--wpds-color-stroke-surface-neutral)',
+ background: 'var(--wpds-color-bg-surface-neutral)',
+ color: 'var(--wpds-color-fg-content-neutral)',
+ boxShadow: '0 8px 28px rgba(0, 0, 0, 0.24)',
+ fontFamily: 'var(--wpds-typography-font-family-body)',
+ fontSize: 'var(--wpds-typography-font-size-sm)',
+};
+
+const headerStyle: CSSProperties = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ gap: '8px',
+};
+
+const titleStyle: CSSProperties = {
+ fontWeight: 600,
+ fontSize: 'var(--wpds-typography-font-size-md)',
+};
+
+const hintStyle: CSSProperties = {
+ margin: 0,
+ color: 'var(--wpds-color-fg-content-neutral-weak)',
+ fontSize: 'var(--wpds-typography-font-size-xs)',
+ lineHeight: 'var(--wpds-typography-line-height-sm)',
+};
+
+const rowStyle: CSSProperties = {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '5px',
+};
+
+const rowLabelStyle: CSSProperties = {
+ color: 'var(--wpds-color-fg-content-neutral-weak)',
+ fontSize: 'var(--wpds-typography-font-size-xs)',
+ textTransform: 'uppercase',
+ letterSpacing: '0.04em',
+};
+
+const segStyle: CSSProperties = {
+ display: 'grid',
+ gridTemplateColumns: 'repeat(3, 1fr)',
+ gap: '3px',
+ padding: '3px',
+ borderRadius: 'var(--wpds-border-radius-sm)',
+ background: 'var(--wpds-color-bg-interactive-neutral-weak)',
+};
+
+function segButtonStyle( selected: boolean ): CSSProperties {
+ return {
+ appearance: 'none',
+ border: 0,
+ borderRadius: 'var(--wpds-border-radius-sm)',
+ padding: '4px 6px',
+ fontSize: 'var(--wpds-typography-font-size-xs)',
+ fontWeight: selected ? 600 : 400,
+ lineHeight: 1.4,
+ cursor: 'pointer',
+ background: selected ? 'var(--wpds-color-fg-content-neutral)' : 'transparent',
+ color: selected
+ ? 'var(--wpds-color-bg-surface-neutral)'
+ : 'var(--wpds-color-fg-content-neutral-weak)',
+ };
+}
+
+const iconButtonStyle: CSSProperties = {
+ appearance: 'none',
+ border: 0,
+ background: 'transparent',
+ color: 'var(--wpds-color-fg-content-neutral-weak)',
+ cursor: 'pointer',
+ fontSize: 'var(--wpds-typography-font-size-md)',
+ lineHeight: 1,
+ padding: '2px 4px',
+};
+
+const collapsedButtonStyle: CSSProperties = {
+ ...panelStyle,
+ inlineSize: 'auto',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: '6px',
+ padding: '8px 12px',
+ cursor: 'pointer',
+ fontWeight: 600,
+};
+
+export function SettingsPreviewPanel() {
+ const { overrides, setOverride, reset, active } = usePreviewOverrides();
+ const [ open, setOpen ] = useState( true );
+
+ if ( ! open ) {
+ return (
+
setOpen( true ) }>
+ Scenarios{ active ? ' •' : '' }
+
+ );
+ }
+
+ return (
+
+
+
Preview scenarios
+
+
+ Reset
+
+ setOpen( false ) }
+ >
+ ×
+
+
+
+ Design-only. Not shown in production builds.
+ { SCENARIO_GROUPS.map( ( group ) => (
+
+
{ group.label }
+
+ { group.options.map( ( option ) => {
+ const selected = overrides[ group.key ] === option.value;
+ return (
+ setOverride( group.key, option.value ) }
+ >
+ { option.label }
+
+ );
+ } ) }
+
+
+ ) ) }
+
+ );
+}
diff --git a/apps/ui/src/components/settings-view/skills-panel.tsx b/apps/ui/src/components/settings-view/skills-panel.tsx
index eb069d25aa..0da34ea6b7 100644
--- a/apps/ui/src/components/settings-view/skills-panel.tsx
+++ b/apps/ui/src/components/settings-view/skills-panel.tsx
@@ -28,17 +28,19 @@ function SkillRow( {
onToggle: () => void;
} ) {
return (
-
-
-
{ skill.displayName }
-
{ skill.description }
+
+
+ { skill.displayName }
+ { skill.description }
+
+
+
-
);
}
@@ -68,54 +70,54 @@ export function SkillsPanel() {
};
return (
-
-
-
-
-
{ __( 'Skills' ) }
- { availableSkills.length > 0 ? (
-
- installAllSkills.mutate( availableSkills.map( ( skill ) => skill.id ) )
- }
- >
- { __( 'Install all' ) }
-
- ) : null }
-
-
+
+
+
+
{ __( 'Skills' ) }
+
{ __(
'Skills are reusable instructions that teach agents how to complete specialized WordPress tasks. Enable the ones you want Studio to add to sites so agents have the right context before they start working.'
) }{ ' ' }
- { visibleError ?
{ visibleError }
: null }
- { isLoading ?
{ __( 'Loading skills…' ) }
: null }
- { ! isLoading && skillList.length === 0 ? (
-
{ __( 'No skills are available.' ) }
- ) : null }
- { skillList.length > 0 ? (
-
- { skillList.map( ( skill ) => (
- handleToggle( skill ) }
- />
- ) ) }
-
+ { availableSkills.length > 0 ? (
+
+
+ installAllSkills.mutate( availableSkills.map( ( skill ) => skill.id ) )
+ }
+ >
+ { __( 'Install all' ) }
+
+
) : null }
-
-
+
+ { visibleError ? { visibleError }
: null }
+ { isLoading ? { __( 'Loading skills…' ) }
: null }
+ { ! isLoading && skillList.length === 0 ? (
+ { __( 'No skills are available.' ) }
+ ) : null }
+ { skillList.length > 0 ? (
+
+ { skillList.map( ( skill ) => (
+ handleToggle( skill ) }
+ />
+ ) ) }
+
+ ) : null }
+
);
}
diff --git a/apps/ui/src/components/settings-view/studio-cli-section.tsx b/apps/ui/src/components/settings-view/studio-cli-section.tsx
index 31cdefaad7..13529d2d45 100644
--- a/apps/ui/src/components/settings-view/studio-cli-section.tsx
+++ b/apps/ui/src/components/settings-view/studio-cli-section.tsx
@@ -47,42 +47,44 @@ export function StudioCliSection() {
);
return (
-
-
-
- { __( 'Studio CLI' ) }
-
- { externallyManaged ? (
- // The default open delay reads as unresponsive on a disabled
- // control, so this tooltip gets its own faster provider.
-
-
- { toggle } }
- />
- }
- >
- { __(
- 'This studio command was installed with the standalone CLI installer, so Studio can’t manage it. Run studio uninstall in a terminal to remove it.'
- ) }
-
-
-
- ) : (
- toggle
- ) }
+
+
+
+
{ __( 'Studio CLI' ) }
+
+ { __( 'Use the studio command in any terminal to manage sites and run WP-CLI.' ) }{ ' ' }
+
+
+
+
+ { externallyManaged ? (
+ // The default open delay reads as unresponsive on a disabled
+ // control, so this tooltip gets its own faster provider.
+
+
+ { toggle } }
+ />
+ }
+ >
+ { __(
+ 'This studio command was installed with the standalone CLI installer, so Studio can’t manage it. Run studio uninstall in a terminal to remove it.'
+ ) }
+
+
+
+ ) : (
+ toggle
+ ) }
+
{ savePreferences.isError ? (
{ __( 'An error occurred while updating the Studio CLI. Please try again.' ) }
) : null }
-
- { __( 'Use the studio command in any terminal to manage sites and run WP-CLI.' ) }{ ' ' }
-
-
);
}
diff --git a/apps/ui/src/components/settings-view/studio-code-panel.test.tsx b/apps/ui/src/components/settings-view/studio-code-panel.test.tsx
index 4a519b47d4..56e3902125 100644
--- a/apps/ui/src/components/settings-view/studio-code-panel.test.tsx
+++ b/apps/ui/src/components/settings-view/studio-code-panel.test.tsx
@@ -5,20 +5,22 @@ import {
useAgentInstructions,
useSaveAgentInstructions,
} from '@/data/queries/use-agent-instructions';
+import { useAgenticFeatures } from '@/data/queries/use-agentic-features';
import { StudioCodePanel } from './studio-code-panel';
-vi.mock( '@wordpress/dataviews', () => ( {
- DataForm: ( {
- data,
- onChange,
- }: {
- data: { content: string };
- onChange: ( update: { content: string } ) => void;
+vi.mock( '@wordpress/components', () => ( {
+ FormToggle: ( props: {
+ checked: boolean;
+ disabled?: boolean;
+ 'aria-label'?: string;
+ onChange: () => void;
} ) => (
-