Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ed86d2e
Add starter agents.md file amd ship it to site root
Mar 2, 2026
0352a44
Add further clarifications to agents md
Mar 2, 2026
0104b7c
Tweaks
Mar 2, 2026
06a428f
Merge branch 'trunk' of github.com:Automattic/studio into add/starter…
Mar 2, 2026
4537c8b
Fix failing test
Mar 2, 2026
38441c9
Cleanup
Mar 2, 2026
7072b3f
Add new modal for AGENTS.MD settings
Mar 3, 2026
9d6c7b4
Add preload
Mar 3, 2026
da5d87d
Remove extra tab
Mar 3, 2026
95f3184
Merge branch 'trunk' of github.com:Automattic/studio into fix/add-ui-…
Mar 5, 2026
fac7f77
Remove reinstall button
Mar 5, 2026
a1c10a8
Add customization section
Mar 5, 2026
c3aad27
Add handle focus event listener
Mar 5, 2026
29b78d1
Clean up installation of all files
Mar 6, 2026
daa8b31
Fix type repetition
Mar 6, 2026
52832f0
Remove loading spinner
Mar 6, 2026
d932a17
Merge branch 'trunk' of github.com:Automattic/studio into fix/add-ui-…
Mar 6, 2026
70e4e04
Fix unintended changes
Mar 6, 2026
1ceb92f
Fix linter
Mar 6, 2026
d799eea
Fix linter
Mar 6, 2026
10446d5
Ensure we don't install AGENTS.MD by default
Mar 9, 2026
8555a31
Merge branch 'trunk' of github.com:Automattic/studio into fix/add-ui-…
Mar 9, 2026
f9ef42e
Add UI for managing skills
Mar 9, 2026
ac395c2
Merge branch 'trunk' of github.com:Automattic/studio into add/ui-for-…
Mar 10, 2026
935de7c
Add skills management mechanisms
Mar 10, 2026
1ebb180
Merge branch 'trunk' of github.com:Automattic/studio into add/ui-for-…
Mar 11, 2026
4b37dd0
Clean up empty line
Mar 11, 2026
fe482e4
Clean up cx
Mar 11, 2026
819c5ea
Clean up ipc handlers
Mar 11, 2026
f6779cd
Remove Refresh option
Mar 11, 2026
ac3914c
Clean up remove button
Mar 11, 2026
76e73b6
Adjust the font sizing on loading states
Mar 11, 2026
36dd97f
Clean up source at the bottom
Mar 11, 2026
747f805
Adjust colours
Mar 11, 2026
8deb293
Remove refresh functionality
Mar 11, 2026
d860ca4
Merge branch 'trunk' of github.com:Automattic/studio into add/ui-for-…
Mar 18, 2026
39f8de2
Lint fix
Mar 19, 2026
dea8ee2
Clean up unnecessary files
Mar 19, 2026
539c11c
Add management per skill
Mar 19, 2026
1c3bf11
Classes formatting
Mar 19, 2026
730cac4
Add skill removal
Mar 19, 2026
2833c86
Slightly adjust gap
Mar 19, 2026
3abd9b1
Merge branch 'trunk' of github.com:Automattic/studio into add/ui-for-…
Mar 23, 2026
efcb3b2
Fix labels
Mar 23, 2026
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
174 changes: 122 additions & 52 deletions apps/studio/src/components/ai-settings-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { TabPanel } from '@wordpress/components';
import { Icon, check } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { useCallback, useEffect, useState } from 'react';
import Button from 'src/components/button';
import Modal from 'src/components/modal';
import { cx } from 'src/lib/cx';
import { getIpcApi } from 'src/lib/get-ipc-api';
import {
INSTRUCTION_FILES,
type InstructionFileType,
} from 'src/modules/agent-instructions/constants';
import { type InstructionFileStatus } from 'src/modules/agent-instructions/lib/instructions';
import {
BUNDLED_SKILLS,
type SkillStatus,
} from 'src/modules/agent-instructions/lib/skills-constants';
import { type SkillStatus } from 'src/modules/agent-instructions/lib/skills-constants';

interface AiSettingsModalProps {
isOpen: boolean;
Expand Down Expand Up @@ -159,7 +158,8 @@ function WordPressSkillsPanel( { siteId }: { siteId: string } ) {
const { __ } = useI18n();
const [ statuses, setStatuses ] = useState< SkillStatus[] >( [] );
const [ error, setError ] = useState< string | null >( null );
const [ installing, setInstalling ] = useState( false );
const [ installingSkillId, setInstallingSkillId ] = useState< string | null >( null );
const [ removingSkillId, setRemovingSkillId ] = useState< string | null >( null );

const refreshStatus = useCallback( async () => {
try {
Expand All @@ -179,25 +179,55 @@ function WordPressSkillsPanel( { siteId }: { siteId: string } ) {
return () => window.removeEventListener( 'focus', handleFocus );
}, [ refreshStatus ] );

const handleInstall = useCallback(
async ( overwrite: boolean = false ) => {
setInstalling( true );
const handleInstallSkill = useCallback(
async ( skillId: string ) => {
setInstallingSkillId( skillId );
setError( null );
try {
await getIpcApi().installWordPressSkills( siteId, { overwrite } );
await getIpcApi().installWordPressSkillById( siteId, skillId );
await refreshStatus();
} catch ( err ) {
const errorMessage = err instanceof Error ? err.message : String( err );
setError( errorMessage );
} finally {
setInstalling( false );
setInstallingSkillId( null );
}
},
[ siteId, refreshStatus ]
);

const handleRemoveSkill = useCallback(
async ( skillId: string ) => {
setRemovingSkillId( skillId );
setError( null );
try {
await getIpcApi().removeWordPressSkillById( siteId, skillId );
await refreshStatus();
} catch ( err ) {
const errorMessage = err instanceof Error ? err.message : String( err );
setError( errorMessage );
} finally {
setRemovingSkillId( null );
}
},
[ siteId, refreshStatus ]
);

const handleInstallAll = useCallback( async () => {
setInstallingSkillId( 'all' );
setError( null );
try {
await getIpcApi().installWordPressSkills( siteId );
await refreshStatus();
} catch ( err ) {
const errorMessage = err instanceof Error ? err.message : String( err );
setError( errorMessage );
} finally {
setInstallingSkillId( null );
}
}, [ siteId, refreshStatus ] );

const allInstalled = statuses.length > 0 && statuses.every( ( s ) => s.installed );
const installedCount = statuses.filter( ( s ) => s.installed ).length;

return (
<div className="flex flex-col gap-4">
Expand All @@ -208,6 +238,16 @@ function WordPressSkillsPanel( { siteId }: { siteId: string } ) {
{ __( 'WordPress development skills for AI agents' ) }
</p>
</div>
{ ! allInstalled && (
<Button
variant="link"
onClick={ handleInstallAll }
disabled={ installingSkillId !== null || removingSkillId !== null }
className="text-sm"
>
{ installingSkillId === 'all' ? __( 'Installing...' ) : __( 'Install All' ) }
</Button>
) }
</div>

{ error && (
Expand All @@ -217,43 +257,64 @@ function WordPressSkillsPanel( { siteId }: { siteId: string } ) {
) }

<div className="border border-frame-border rounded-md overflow-hidden">
<div className="flex items-center justify-between px-3 py-2.5">
<div className="flex-1 min-w-0 pr-3">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-frame-text">
{ __( 'WordPress Skills' ) }
</span>
{ allInstalled && (
<span className="inline-flex items-center gap-1 text-[11px] text-[#1a6928] bg-[#ceead6] dark:text-[#6ee7a0] dark:bg-[#1a3a24] px-2 py-0.5 rounded-full">
<Icon icon={ check } size={ 12 } />
{ __( 'Installed' ) }
</span>
) }
{ ! allInstalled && installedCount > 0 && (
<span className="inline-flex items-center gap-1 text-[11px] text-frame-text-secondary bg-frame-surface px-2 py-0.5 rounded-full">
{ `${ installedCount }/${ BUNDLED_SKILLS.length }` }
</span>
) }
</div>
<div className="text-xs text-frame-text-secondary">
{ __( 'Plugins, blocks, themes, REST API, and WP-CLI skills' ) }
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Button
variant="secondary"
onClick={ () => handleInstall( allInstalled ) }
disabled={ installing }
className="text-xs py-1 px-2"
{ statuses.map( ( skill ) => {
const isInstalling = installingSkillId === skill.id;
return (
<div
key={ skill.id }
className="flex items-center justify-between px-3 py-2.5 border-b border-frame-border last:border-b-0"
>
{ installing
? __( 'Installing...' )
: allInstalled
? __( 'Reinstall' )
: __( 'Install' ) }
</Button>
</div>
</div>
<div className="flex-1 min-w-0 pr-3">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-frame-text truncate">
{ skill.displayName }
</span>
{ skill.installed && (
<span className="inline-flex items-center gap-1 text-[11px] text-green-900 bg-green-50 dark:!text-green-300 dark:bg-green-950 px-2 py-0.5 rounded-full flex-shrink-0">
<Icon className="dark:fill-green-300" icon={ check } size={ 12 } />
{ __( 'Installed' ) }
</span>
) }
</div>
<div className="text-xs text-frame-text-secondary truncate">
{ skill.description }
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{ skill.installed ? (
<>
<Button
variant="link"
onClick={ () =>
getIpcApi().openFileInIDE( `.agents/skills/${ skill.id }/SKILL.md`, siteId )
}
className="text-xs"
>
{ __( 'Open' ) }
</Button>
<Button
variant="link"
onClick={ () => handleRemoveSkill( skill.id ) }
disabled={ removingSkillId !== null }
className="text-xs !text-a8c-red-50"
>
{ removingSkillId === skill.id ? __( 'Removing...' ) : __( 'Remove' ) }
</Button>
</>
) : (
<Button
variant="secondary"
onClick={ () => handleInstallSkill( skill.id ) }
disabled={ installingSkillId !== null }
className="text-xs py-1 px-2"
>
{ isInstalling ? __( 'Installing...' ) : __( 'Install' ) }
</Button>
) }
</div>
</div>
);
} ) }
</div>
</div>
);
Expand All @@ -266,18 +327,27 @@ export function AiSettingsModal( { isOpen, onClose, siteId }: AiSettingsModalPro
return null;
}

const tabs = [
{ name: 'skills', title: __( 'Skills' ) },
{ name: 'instructions', title: __( 'Instructions' ) },
];

return (
<Modal
title={ __( 'AI settings' ) }
isDismissible
onRequestClose={ onClose }
size="medium"
className="min-h-[350px] app-no-drag-region"
className={ cx( 'min-h-[350px] app-no-drag-region', '[&_[role="document"]]:px-0' ) }
>
<div className="px-2 pb-4 flex gap-6 flex-col">
<AgentInstructionsPanel siteId={ siteId } />
<WordPressSkillsPanel siteId={ siteId } />
</div>
<TabPanel className="w-full" tabs={ tabs } orientation="horizontal">
{ ( { name } ) => (
<div className="mt-6 px-8 pb-4 flex flex-col gap-4">
{ name === 'skills' && <WordPressSkillsPanel siteId={ siteId } /> }
{ name === 'instructions' && <AgentInstructionsPanel siteId={ siteId } /> }
</div>
) }
</TabPanel>
</Modal>
);
}
28 changes: 28 additions & 0 deletions apps/studio/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ import {
import {
getSkillsStatus,
installAllSkills,
installSkillById,
removeSkillById,
type SkillStatus,
} from 'src/modules/agent-instructions/lib/skills';
import { editSiteViaCli, EditSiteOptions } from 'src/modules/cli/lib/cli-site-editor';
Expand Down Expand Up @@ -196,6 +198,32 @@ export async function installWordPressSkills(
await installAllSkills( server.details.path, overwrite );
}

export async function installWordPressSkillById(
_event: IpcMainInvokeEvent,
siteId: string,
skillId: string,
options?: { overwrite?: boolean }
): Promise< void > {
const server = SiteServer.get( siteId );
if ( ! server ) {
throw new Error( `Site not found: ${ siteId }` );
}
const overwrite = options?.overwrite ?? false;
await installSkillById( server.details.path, skillId, overwrite );
}

export async function removeWordPressSkillById(
_event: IpcMainInvokeEvent,
siteId: string,
skillId: string
): Promise< void > {
const server = SiteServer.get( siteId );
if ( ! server ) {
throw new Error( `Site not found: ${ siteId }` );
}
await removeSkillById( server.details.path, skillId );
}

const DEBUG_LOG_MAX_LINES = 50;
const PM2_HOME = nodePath.join( os.homedir(), '.studio', 'pm2' );
const DEFAULT_ENCODED_PASSWORD = encodePassword( 'password' );
Expand Down
17 changes: 16 additions & 1 deletion apps/studio/src/modules/agent-instructions/lib/skills.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import nodePath from 'path';
import { installSkillToSite } from '@studio/common/lib/agent-skills';
import {
installSkillToSite,
removeSkillFromSite,
} from '@studio/common/lib/agent-skills';
import { pathExists } from '@studio/common/lib/fs-utils';
import { getAiInstructionsPath } from 'src/lib/server-files-paths';
import { BUNDLED_SKILLS, type SkillStatus } from './skills-constants';
Expand Down Expand Up @@ -31,3 +34,15 @@ export async function installAllSkills(
}
}
}

export async function installSkillById(
sitePath: string,
skillId: string,
overwrite: boolean = false
): Promise< void > {
await installSkillToSite( sitePath, getAiInstructionsPath(), skillId, overwrite );
}

export async function removeSkillById( sitePath: string, skillId: string ): Promise< void > {
await removeSkillFromSite( sitePath, skillId );
}
4 changes: 4 additions & 0 deletions apps/studio/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ const api: IpcApi = {
getWordPressSkillsStatus: ( siteId ) => ipcRendererInvoke( 'getWordPressSkillsStatus', siteId ),
installWordPressSkills: ( siteId, options ) =>
ipcRendererInvoke( 'installWordPressSkills', siteId, options ),
installWordPressSkillById: ( siteId, skillId, options ) =>
ipcRendererInvoke( 'installWordPressSkillById', siteId, skillId, options ),
removeWordPressSkillById: ( siteId, skillId ) =>
ipcRendererInvoke( 'removeWordPressSkillById', siteId, skillId ),
};

contextBridge.exposeInMainWorld( 'ipcApi', api );
Expand Down
7 changes: 7 additions & 0 deletions tools/common/lib/agent-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import { isErrnoException } from './is-errno-exception';
*/
const MANAGED_INSTRUCTION_FILES = [ 'STUDIO.md', 'CLAUDE.md' ];

export async function removeSkillFromSite( sitePath: string, skillId: string ): Promise< void > {
const agentsSkillPath = path.join( sitePath, '.agents', 'skills', skillId );
const claudeSkillPath = path.join( sitePath, '.claude', 'skills', skillId );
await fs.rm( agentsSkillPath, { recursive: true, force: true } );
await fs.rm( claudeSkillPath, { recursive: true, force: true } );
}

/**
* Install all bundled AI instructions and skills from a source directory into a site.
*
Expand Down
Loading