From 7e460a0ea97800a9e2e8d640132630c2fd405cef Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Fri, 20 Mar 2026 13:13:53 +0100 Subject: [PATCH 01/10] Add skills as first pass --- apps/studio/src/ipc-handlers.ts | 35 ++++ .../user-settings/components/skills-tab.tsx | 165 ++++++++++++++++++ .../components/user-settings.tsx | 31 ++-- .../user-settings/user-settings-types.ts | 2 +- apps/studio/src/preload.ts | 4 + 5 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 apps/studio/src/modules/user-settings/components/skills-tab.tsx diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index 2cf2b5d1bf..ea39afbb9f 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -19,6 +19,7 @@ import nodePath from 'path'; import * as Sentry from '@sentry/electron/main'; import { installAiInstructionsToSite, + installSkillToSite, updateManagedInstructionFiles, } from '@studio/common/lib/agent-skills'; import { validateBlueprintData } from '@studio/common/lib/blueprint-validation'; @@ -81,6 +82,7 @@ import { type InstructionFileStatus, } from 'src/modules/agent-instructions/lib/instructions'; import { + BUNDLED_SKILLS, getSkillsStatus, installAllSkills, type SkillStatus, @@ -195,6 +197,39 @@ export async function installWordPressSkills( await installAllSkills( server.details.path, overwrite ); } +export async function getWordPressSkillsStatusAllSites( + _event: IpcMainInvokeEvent +): Promise< SkillStatus[] > { + const { sites } = await loadUserData(); + if ( ! sites.length ) { + return BUNDLED_SKILLS.map( ( skill ) => ( { ...skill, installed: false } ) ); + } + const allSiteStatuses = await Promise.all( sites.map( ( site ) => getSkillsStatus( site.path ) ) ); + return BUNDLED_SKILLS.map( ( skill ) => ( { + ...skill, + installed: allSiteStatuses.every( + ( siteStatuses ) => siteStatuses.find( ( s ) => s.id === skill.id )?.installed ?? false + ), + } ) ); +} + +export async function installWordPressSkillsToAllSites( + _event: IpcMainInvokeEvent, + options?: { skillId?: string; overwrite?: boolean } +): Promise< void > { + const { sites } = await loadUserData(); + const overwrite = options?.overwrite ?? false; + const bundledPath = getAiInstructionsPath(); + const tasks = sites.flatMap( ( site ) => + options?.skillId + ? [ installSkillToSite( site.path, bundledPath, options.skillId, overwrite ) ] + : BUNDLED_SKILLS.map( ( skill ) => + installSkillToSite( site.path, bundledPath, skill.id, overwrite ) + ) + ); + await Promise.allSettled( tasks ); +} + const DEBUG_LOG_MAX_LINES = 50; const PM2_HOME = nodePath.join( os.homedir(), '.studio', 'pm2' ); const DEFAULT_ENCODED_PASSWORD = encodePassword( 'password' ); diff --git a/apps/studio/src/modules/user-settings/components/skills-tab.tsx b/apps/studio/src/modules/user-settings/components/skills-tab.tsx new file mode 100644 index 0000000000..d3087e342b --- /dev/null +++ b/apps/studio/src/modules/user-settings/components/skills-tab.tsx @@ -0,0 +1,165 @@ +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 { getIpcApi } from 'src/lib/get-ipc-api'; +import { type SkillStatus } from 'src/modules/agent-instructions/lib/skills-constants'; + +export function SkillsTab() { + const { __ } = useI18n(); + const [ statuses, setStatuses ] = useState< SkillStatus[] >( [] ); + const [ error, setError ] = useState< string | null >( null ); + const [ installingSkillId, setInstallingSkillId ] = useState< string | null >( null ); + const [ installingAll, setInstallingAll ] = useState( false ); + + const refreshStatus = useCallback( async () => { + try { + const result = await getIpcApi().getWordPressSkillsStatusAllSites(); + setStatuses( result as SkillStatus[] ); + setError( null ); + } catch ( err ) { + const errorMessage = err instanceof Error ? err.message : String( err ); + setError( errorMessage ); + } + }, [] ); + + useEffect( () => { + void refreshStatus(); + const handleFocus = () => void refreshStatus(); + window.addEventListener( 'focus', handleFocus ); + return () => window.removeEventListener( 'focus', handleFocus ); + }, [ refreshStatus ] ); + + const handleInstallSkill = useCallback( + async ( skillId: string ) => { + setInstallingSkillId( skillId ); + setError( null ); + try { + await getIpcApi().installWordPressSkillsToAllSites( { skillId } ); + await refreshStatus(); + } catch ( err ) { + const errorMessage = err instanceof Error ? err.message : String( err ); + setError( errorMessage ); + } finally { + setInstallingSkillId( null ); + } + }, + [ refreshStatus ] + ); + + const wordPressSkills = statuses.filter( ( s ) => s.id !== 'studio-cli' ); + const installedSkills = wordPressSkills.filter( ( s ) => s.installed ); + const availableSkills = wordPressSkills.filter( ( s ) => ! s.installed ); + + const handleInstallAll = useCallback( async () => { + setInstallingAll( true ); + setError( null ); + try { + for ( const skill of availableSkills ) { + await getIpcApi().installWordPressSkillsToAllSites( { skillId: skill.id } ); + } + await refreshStatus(); + } catch ( err ) { + const errorMessage = err instanceof Error ? err.message : String( err ); + setError( errorMessage ); + } finally { + setInstallingAll( false ); + } + }, [ availableSkills, refreshStatus ] ); + const isAnyInstalling = installingSkillId !== null || installingAll; + + return ( +
+

+ { __( 'WordPress development skills for AI agents, installed across all your local sites.' ) } +

+ + { error && ( +
+ { error } +
+ ) } + + { installedSkills.length > 0 && ( +
+ + { __( 'Installed' ) } + +
+ { installedSkills.map( ( skill ) => ( +
+
+
+ + { skill.displayName } + + + + { __( 'Installed' ) } + +
+
{ skill.description }
+
+
+ ) ) } +
+
+ ) } + + { availableSkills.length > 0 && ( +
+
+ + { __( 'Available' ) } + + +
+
+ { availableSkills.map( ( skill ) => { + const isInstallingThis = installingSkillId === skill.id; + return ( +
+
+
+ { skill.displayName } +
+
{ skill.description }
+
+
+ +
+
+ ); + } ) } +
+
+ ) } + + { statuses.length === 0 && ! error && ( +
+ { __( 'Loading skills...' ) } +
+ ) } +
+ ); +} diff --git a/apps/studio/src/modules/user-settings/components/user-settings.tsx b/apps/studio/src/modules/user-settings/components/user-settings.tsx index 7a6cf6108c..4504d60398 100644 --- a/apps/studio/src/modules/user-settings/components/user-settings.tsx +++ b/apps/studio/src/modules/user-settings/components/user-settings.tsx @@ -10,6 +10,7 @@ import { getIpcApi } from 'src/lib/get-ipc-api'; import { AccountTab } from 'src/modules/user-settings/components/account-tab'; import { NonAuthenticatedAccountTab } from 'src/modules/user-settings/components/non-authenticated-account-tab'; import { PreferencesTab } from 'src/modules/user-settings/components/preferences-tab'; +import { SkillsTab } from 'src/modules/user-settings/components/skills-tab'; import { UsageTab } from 'src/modules/user-settings/components/usage-tab'; import { UserSettingsTab } from 'src/modules/user-settings/user-settings-types'; import { useRootSelector } from 'src/stores'; @@ -90,6 +91,11 @@ export default function UserSettings() { } ); } + tabs.push( { + name: 'skills', + title: __( 'Skills' ), + } ); + return ( <> { needsToOpenUserSettings && ( @@ -117,18 +123,19 @@ export default function UserSettings() { ) : ( ) ) } - { name === 'preferences' && } - { name === 'usage' && isAuthenticated && ( - - ) } - + { name === 'preferences' && } + { name === 'usage' && isAuthenticated && ( + + ) } + { name === 'skills' && } + ) } diff --git a/apps/studio/src/modules/user-settings/user-settings-types.ts b/apps/studio/src/modules/user-settings/user-settings-types.ts index cf253236d1..417d561126 100644 --- a/apps/studio/src/modules/user-settings/user-settings-types.ts +++ b/apps/studio/src/modules/user-settings/user-settings-types.ts @@ -1,4 +1,4 @@ -export type UserSettingsTabName = 'account' | 'preferences' | 'usage'; +export type UserSettingsTabName = 'account' | 'preferences' | 'usage' | 'skills'; export type UserSettingsTab = { name: UserSettingsTabName; title: string; diff --git a/apps/studio/src/preload.ts b/apps/studio/src/preload.ts index c63853e0e5..794f6b9f35 100644 --- a/apps/studio/src/preload.ts +++ b/apps/studio/src/preload.ts @@ -164,6 +164,10 @@ const api: IpcApi = { getWordPressSkillsStatus: ( siteId ) => ipcRendererInvoke( 'getWordPressSkillsStatus', siteId ), installWordPressSkills: ( siteId, options ) => ipcRendererInvoke( 'installWordPressSkills', siteId, options ), + getWordPressSkillsStatusAllSites: () => + ipcRendererInvoke( 'getWordPressSkillsStatusAllSites' ), + installWordPressSkillsToAllSites: ( options ) => + ipcRendererInvoke( 'installWordPressSkillsToAllSites', options ), }; contextBridge.exposeInMainWorld( 'ipcApi', api ); From dc59cc53445b555a8776454ba1fffc01cbd36f6d Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Fri, 20 Mar 2026 13:41:58 +0100 Subject: [PATCH 02/10] Fix linter --- apps/studio/src/ipc-handlers.ts | 4 ++- .../user-settings/components/skills-tab.tsx | 16 +++++------- .../components/user-settings.tsx | 26 +++++++++---------- apps/studio/src/preload.ts | 3 +-- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index ea39afbb9f..561cd45df2 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -204,7 +204,9 @@ export async function getWordPressSkillsStatusAllSites( if ( ! sites.length ) { return BUNDLED_SKILLS.map( ( skill ) => ( { ...skill, installed: false } ) ); } - const allSiteStatuses = await Promise.all( sites.map( ( site ) => getSkillsStatus( site.path ) ) ); + const allSiteStatuses = await Promise.all( + sites.map( ( site ) => getSkillsStatus( site.path ) ) + ); return BUNDLED_SKILLS.map( ( skill ) => ( { ...skill, installed: allSiteStatuses.every( diff --git a/apps/studio/src/modules/user-settings/components/skills-tab.tsx b/apps/studio/src/modules/user-settings/components/skills-tab.tsx index d3087e342b..b99e84a1a4 100644 --- a/apps/studio/src/modules/user-settings/components/skills-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/skills-tab.tsx @@ -71,7 +71,9 @@ export function SkillsTab() { return (

- { __( 'WordPress development skills for AI agents, installed across all your local sites.' ) } + { __( + 'WordPress development skills for AI agents, installed across all your local sites.' + ) }

{ error && ( @@ -93,9 +95,7 @@ export function SkillsTab() { >
- - { skill.displayName } - + { skill.displayName } { __( 'Installed' ) } @@ -133,9 +133,7 @@ export function SkillsTab() { className="flex items-center justify-between px-3 py-2.5 border-b border-frame-border last:border-b-0" >
-
- { skill.displayName } -
+
{ skill.displayName }
{ skill.description }
@@ -156,9 +154,7 @@ export function SkillsTab() { ) } { statuses.length === 0 && ! error && ( -
- { __( 'Loading skills...' ) } -
+
{ __( 'Loading skills...' ) }
) }
); diff --git a/apps/studio/src/modules/user-settings/components/user-settings.tsx b/apps/studio/src/modules/user-settings/components/user-settings.tsx index 4504d60398..0451d540ba 100644 --- a/apps/studio/src/modules/user-settings/components/user-settings.tsx +++ b/apps/studio/src/modules/user-settings/components/user-settings.tsx @@ -123,19 +123,19 @@ export default function UserSettings() { ) : ( ) ) } - { name === 'preferences' && } - { name === 'usage' && isAuthenticated && ( - - ) } - { name === 'skills' && } -
+ { name === 'preferences' && } + { name === 'usage' && isAuthenticated && ( + + ) } + { name === 'skills' && } +
) } diff --git a/apps/studio/src/preload.ts b/apps/studio/src/preload.ts index 794f6b9f35..567fc2eeb8 100644 --- a/apps/studio/src/preload.ts +++ b/apps/studio/src/preload.ts @@ -164,8 +164,7 @@ const api: IpcApi = { getWordPressSkillsStatus: ( siteId ) => ipcRendererInvoke( 'getWordPressSkillsStatus', siteId ), installWordPressSkills: ( siteId, options ) => ipcRendererInvoke( 'installWordPressSkills', siteId, options ), - getWordPressSkillsStatusAllSites: () => - ipcRendererInvoke( 'getWordPressSkillsStatusAllSites' ), + getWordPressSkillsStatusAllSites: () => ipcRendererInvoke( 'getWordPressSkillsStatusAllSites' ), installWordPressSkillsToAllSites: ( options ) => ipcRendererInvoke( 'installWordPressSkillsToAllSites', options ), }; From 5a966843aff74d7196016438de86df44836e4673 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Fri, 20 Mar 2026 13:55:45 +0100 Subject: [PATCH 03/10] Add removal option --- apps/studio/src/ipc-handlers.ts | 10 +++++ .../user-settings/components/skills-tab.tsx | 37 ++++++++++++++++++- apps/studio/src/preload.ts | 2 + tools/common/lib/agent-skills.ts | 7 ++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index 561cd45df2..251082d653 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -20,6 +20,7 @@ import * as Sentry from '@sentry/electron/main'; import { installAiInstructionsToSite, installSkillToSite, + removeSkillFromSite, updateManagedInstructionFiles, } from '@studio/common/lib/agent-skills'; import { validateBlueprintData } from '@studio/common/lib/blueprint-validation'; @@ -232,6 +233,15 @@ export async function installWordPressSkillsToAllSites( await Promise.allSettled( tasks ); } +export async function removeWordPressSkillFromAllSites( + _event: IpcMainInvokeEvent, + skillId: string +): Promise< void > { + const { sites } = await loadUserData(); + const tasks = sites.map( ( site ) => removeSkillFromSite( site.path, skillId ) ); + await Promise.allSettled( tasks ); +} + const DEBUG_LOG_MAX_LINES = 50; const PM2_HOME = nodePath.join( os.homedir(), '.studio', 'pm2' ); const DEFAULT_ENCODED_PASSWORD = encodePassword( 'password' ); diff --git a/apps/studio/src/modules/user-settings/components/skills-tab.tsx b/apps/studio/src/modules/user-settings/components/skills-tab.tsx index b99e84a1a4..8473ed7082 100644 --- a/apps/studio/src/modules/user-settings/components/skills-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/skills-tab.tsx @@ -1,4 +1,5 @@ -import { Icon, check } from '@wordpress/icons'; +import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; +import { Icon, check, moreVertical } from '@wordpress/icons'; import { useI18n } from '@wordpress/react-i18n'; import { useCallback, useEffect, useState } from 'react'; import Button from 'src/components/button'; @@ -30,6 +31,20 @@ export function SkillsTab() { return () => window.removeEventListener( 'focus', handleFocus ); }, [ refreshStatus ] ); + const handleRemoveSkill = useCallback( + async ( skillId: string ) => { + setError( null ); + try { + await getIpcApi().removeWordPressSkillFromAllSites( skillId ); + await refreshStatus(); + } catch ( err ) { + const errorMessage = err instanceof Error ? err.message : String( err ); + setError( errorMessage ); + } + }, + [ refreshStatus ] + ); + const handleInstallSkill = useCallback( async ( skillId: string ) => { setInstallingSkillId( skillId ); @@ -103,6 +118,26 @@ export function SkillsTab() {
{ skill.description }
+ + { ( { onClose }: { onClose: () => void } ) => ( + + { + void handleRemoveSkill( skill.id ); + onClose(); + } } + > + { __( 'Remove' ) } + + + ) } + ) ) } diff --git a/apps/studio/src/preload.ts b/apps/studio/src/preload.ts index 567fc2eeb8..dd8c1ab24f 100644 --- a/apps/studio/src/preload.ts +++ b/apps/studio/src/preload.ts @@ -167,6 +167,8 @@ const api: IpcApi = { getWordPressSkillsStatusAllSites: () => ipcRendererInvoke( 'getWordPressSkillsStatusAllSites' ), installWordPressSkillsToAllSites: ( options ) => ipcRendererInvoke( 'installWordPressSkillsToAllSites', options ), + removeWordPressSkillFromAllSites: ( skillId ) => + ipcRendererInvoke( 'removeWordPressSkillFromAllSites', skillId ), }; contextBridge.exposeInMainWorld( 'ipcApi', api ); diff --git a/tools/common/lib/agent-skills.ts b/tools/common/lib/agent-skills.ts index 2f47dd9d7a..796d87b396 100644 --- a/tools/common/lib/agent-skills.ts +++ b/tools/common/lib/agent-skills.ts @@ -83,6 +83,13 @@ async function installInstructionFile( await fs.copyFile( path.join( bundledPath, fileName ), dest ); } +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 } ); +} + export async function installSkillToSite( sitePath: string, bundledPath: string, From 2a563fe1a8572872f889babdf7aced2f5982eaa7 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Fri, 20 Mar 2026 15:28:32 +0100 Subject: [PATCH 04/10] Add header to avaible --- .../user-settings/components/skills-tab.tsx | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/apps/studio/src/modules/user-settings/components/skills-tab.tsx b/apps/studio/src/modules/user-settings/components/skills-tab.tsx index 8473ed7082..1975940edc 100644 --- a/apps/studio/src/modules/user-settings/components/skills-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/skills-tab.tsx @@ -145,46 +145,44 @@ export function SkillsTab() { ) } { availableSkills.length > 0 && ( -
-
+
+
{ __( 'Available' ) }
-
- { availableSkills.map( ( skill ) => { - const isInstallingThis = installingSkillId === skill.id; - return ( -
-
-
{ skill.displayName }
-
{ skill.description }
-
-
- -
+ { availableSkills.map( ( skill ) => { + const isInstallingThis = installingSkillId === skill.id; + return ( +
+
+
{ skill.displayName }
+
{ skill.description }
- ); - } ) } -
+
+ +
+
+ ); + } ) }
) } From f9199051acc4acc6e632a3c6127b399bd3a378d7 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Fri, 20 Mar 2026 15:56:33 +0100 Subject: [PATCH 05/10] Misc style fixes --- .../user-settings/components/skills-tab.tsx | 109 +++++++++--------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/apps/studio/src/modules/user-settings/components/skills-tab.tsx b/apps/studio/src/modules/user-settings/components/skills-tab.tsx index 1975940edc..c69bf7dab1 100644 --- a/apps/studio/src/modules/user-settings/components/skills-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/skills-tab.tsx @@ -81,14 +81,13 @@ export function SkillsTab() { setInstallingAll( false ); } }, [ availableSkills, refreshStatus ] ); + const isAnyInstalling = installingSkillId !== null || installingAll; return (
-

- { __( - 'WordPress development skills for AI agents, installed across all your local sites.' - ) } +

+ { __( 'Agents can decide to use skills to help them accomplish specialized tasks.' ) }

{ error && ( @@ -98,66 +97,68 @@ export function SkillsTab() { ) } { installedSkills.length > 0 && ( -
- - { __( 'Installed' ) } - -
- { installedSkills.map( ( skill ) => ( -
-
-
- { skill.displayName } - - - { __( 'Installed' ) } - -
-
{ skill.description }
+
+
+ + { __( 'Installed' ) } + +
+ { installedSkills.map( ( skill ) => ( +
+
+
+ { skill.displayName } + + + { __( 'Installed' ) } +
- - { ( { onClose }: { onClose: () => void } ) => ( - - { - void handleRemoveSkill( skill.id ); - onClose(); - } } - > - { __( 'Remove' ) } - - - ) } - +
{ skill.description }
- ) ) } -
+ + { ( { onClose }: { onClose: () => void } ) => ( + + { + void handleRemoveSkill( skill.id ); + onClose(); + } } + > + { __( 'Remove' ) } + + + ) } + +
+ ) ) }
) } { availableSkills.length > 0 && (
- + { __( 'Available' ) } - +
+ +
{ availableSkills.map( ( skill ) => { const isInstallingThis = installingSkillId === skill.id; From 41fbcf667e8c4878585e43ab53f3d48ca50f12a2 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Fri, 20 Mar 2026 16:09:26 +0100 Subject: [PATCH 06/10] Styling --- .../user-settings/components/skills-tab.tsx | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/studio/src/modules/user-settings/components/skills-tab.tsx b/apps/studio/src/modules/user-settings/components/skills-tab.tsx index c69bf7dab1..1dc1dc21b7 100644 --- a/apps/studio/src/modules/user-settings/components/skills-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/skills-tab.tsx @@ -110,13 +110,13 @@ export function SkillsTab() { >
- { skill.displayName } + { skill.displayName } { __( 'Installed' ) }
-
{ skill.description }
+
{ skill.description }
{ __( 'Available' ) } -
- -
+
{ availableSkills.map( ( skill ) => { const isInstallingThis = installingSkillId === skill.id; @@ -168,8 +166,8 @@ export function SkillsTab() { className="flex items-center justify-between px-3 py-2.5 border-b border-frame-border last:border-b-0" >
-
{ skill.displayName }
-
{ skill.description }
+
{ skill.displayName }
+
{ skill.description }
From 16301959ca33f33cc8fff30c264f7dc5ec194568 Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 23 Mar 2026 15:24:54 +0100 Subject: [PATCH 08/10] Fix badge in the dark mode --- .../src/modules/user-settings/components/skills-tab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/modules/user-settings/components/skills-tab.tsx b/apps/studio/src/modules/user-settings/components/skills-tab.tsx index 44bdebf007..69f9f4f814 100644 --- a/apps/studio/src/modules/user-settings/components/skills-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/skills-tab.tsx @@ -111,8 +111,8 @@ export function SkillsTab() {
{ skill.displayName } - - + + { __( 'Installed' ) }
From 93c003ab5787a393ae092a3569c93f3a7c698cda Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 23 Mar 2026 15:26:50 +0100 Subject: [PATCH 09/10] Wrap available skills in use memo --- .../user-settings/components/skills-tab.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/studio/src/modules/user-settings/components/skills-tab.tsx b/apps/studio/src/modules/user-settings/components/skills-tab.tsx index 69f9f4f814..3e1115b4f6 100644 --- a/apps/studio/src/modules/user-settings/components/skills-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/skills-tab.tsx @@ -1,7 +1,7 @@ import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; import { Icon, check, moreVertical } from '@wordpress/icons'; import { useI18n } from '@wordpress/react-i18n'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import Button from 'src/components/button'; import { getIpcApi } from 'src/lib/get-ipc-api'; import { type SkillStatus } from 'src/modules/agent-instructions/lib/skills-constants'; @@ -62,9 +62,18 @@ export function SkillsTab() { [ refreshStatus ] ); - const wordPressSkills = statuses.filter( ( s ) => s.id !== 'studio-cli' ); - const installedSkills = wordPressSkills.filter( ( s ) => s.installed ); - const availableSkills = wordPressSkills.filter( ( s ) => ! s.installed ); + const wordPressSkills = useMemo( + () => statuses.filter( ( s ) => s.id !== 'studio-cli' ), + [ statuses ] + ); + const installedSkills = useMemo( + () => wordPressSkills.filter( ( s ) => s.installed ), + [ wordPressSkills ] + ); + const availableSkills = useMemo( + () => wordPressSkills.filter( ( s ) => ! s.installed ), + [ wordPressSkills ] + ); const handleInstallAll = useCallback( async () => { setInstallingAll( true ); From 366ff12b3dac7164c5574e50f19ef248c154021c Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Mon, 23 Mar 2026 15:54:05 +0100 Subject: [PATCH 10/10] Fix IPC handlers issue --- apps/studio/src/ipc-handlers.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index bce9d9300a..b6d8cf9f7a 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -231,7 +231,12 @@ export async function installWordPressSkillsToAllSites( installSkillToSite( site.path, bundledPath, skill.id, overwrite ) ) ); - await Promise.allSettled( tasks ); + const results = await Promise.allSettled( tasks ); + results.forEach( ( result ) => { + if ( result.status === 'rejected' ) { + console.error( '[skills] Failed to install skill:', result.reason ); + } + } ); } export async function removeWordPressSkillFromAllSites( @@ -240,7 +245,12 @@ export async function removeWordPressSkillFromAllSites( ): Promise< void > { const { sites } = await loadUserData(); const tasks = sites.map( ( site ) => removeSkillFromSite( site.path, skillId ) ); - await Promise.allSettled( tasks ); + const results = await Promise.allSettled( tasks ); + results.forEach( ( result ) => { + if ( result.status === 'rejected' ) { + console.error( '[skills] Failed to remove skill:', result.reason ); + } + } ); } const DEBUG_LOG_MAX_LINES = 50;