diff --git a/apps/studio/src/components/site-icon.tsx b/apps/studio/src/components/site-icon.tsx
new file mode 100644
index 0000000000..67fd77f360
--- /dev/null
+++ b/apps/studio/src/components/site-icon.tsx
@@ -0,0 +1,66 @@
+import { WordPressLogoCircle } from 'src/components/wordpress-logo-circle';
+import { useThemeDetails } from 'src/hooks/use-theme-details';
+import { cx } from 'src/lib/cx';
+
+const SITE_ICON_COLORS = [
+ '#5B8A72', // sage
+ '#7B6B8A', // lavender
+ '#8A7B5B', // khaki
+ '#5B7B8A', // steel
+ '#8A5B6B', // mauve
+ '#6B8A5B', // moss
+ '#5B6B8A', // slate
+ '#8A6B5B', // clay
+];
+
+/**
+ * Simple hash of a string to a number, used for deterministic
+ * color assignment for sites without an explicit iconColorIndex.
+ */
+function hashStringToIndex( str: string ): number {
+ let hash = 0;
+ for ( let i = 0; i < str.length; i++ ) {
+ hash = ( hash * 31 + str.charCodeAt( i ) ) | 0;
+ }
+ return Math.abs( hash );
+}
+
+export function SiteIcon( {
+ siteId,
+ iconColorIndex,
+ size = 16,
+ className,
+}: {
+ siteId: string;
+ iconColorIndex?: number;
+ size?: number;
+ className?: string;
+} ) {
+ const { siteIcons } = useThemeDetails();
+ const iconData = siteIcons[ siteId ];
+
+ if ( iconData ) {
+ return (
+

+ );
+ }
+
+ const colorIndex =
+ typeof iconColorIndex === 'number' ? iconColorIndex : hashStringToIndex( siteId );
+ const bgColor = SITE_ICON_COLORS[ colorIndex % SITE_ICON_COLORS.length ];
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/studio/src/components/site-menu.tsx b/apps/studio/src/components/site-menu.tsx
index 7c3c1b44bb..a0d0746bd9 100644
--- a/apps/studio/src/components/site-menu.tsx
+++ b/apps/studio/src/components/site-menu.tsx
@@ -4,13 +4,14 @@ import { Spinner } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { useEffect, useState } from 'react';
import { XDebugIcon } from 'src/components/icons/xdebug-icon';
+import { SiteIcon } from 'src/components/site-icon';
import { Tooltip } from 'src/components/tooltip';
import { useSyncSites } from 'src/hooks/sync-sites';
import { useContentTabs } from 'src/hooks/use-content-tabs';
import { useDeleteSite } from 'src/hooks/use-delete-site';
import { useImportExport } from 'src/hooks/use-import-export';
import { useSiteDetails } from 'src/hooks/use-site-details';
-import { isMac, isWindows } from 'src/lib/app-globals';
+import { isWindows } from 'src/lib/app-globals';
import { cx } from 'src/lib/cx';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { supportedEditorConfig } from 'src/modules/user-settings/lib/editor';
@@ -208,8 +209,7 @@ function SiteItem( {
return (
{ showSpinner ? (
@@ -386,11 +387,11 @@ export default function SiteMenu( { className }: SiteMenuProps ) {
scrollbarGutter: 'stable',
} }
className={ cx(
- 'w-full overflow-y-auto overflow-x-hidden flex flex-col gap-0.5 pb-4',
+ 'w-full overflow-y-auto overflow-x-hidden flex flex-col gap-1 px-2 pb-4',
className
) }
>
-
+
{ sites.map( ( site, index ) => (
false ),
};
-vi.mock( 'src/hooks/use-site-details', () => ( {
- useSiteDetails: () => ( { ...siteDetailsMocked } ),
+vi.mock( 'src/hooks/use-site-details', async ( importOriginal ) => {
+ const actual = await importOriginal< typeof import('src/hooks/use-site-details') >();
+ return {
+ ...actual,
+ useSiteDetails: () => ( { ...siteDetailsMocked } ),
+ };
+} );
+
+vi.mock( 'src/hooks/use-theme-details', () => ( {
+ useThemeDetails: () => ( { siteIcons: {} } ),
+ ThemeDetailsContext: { Provider: ( { children }: { children: React.ReactNode } ) => children },
} ) );
const renderWithProvider = ( children: React.ReactElement ) => {
diff --git a/apps/studio/src/constants.ts b/apps/studio/src/constants.ts
index 999a29b7e4..188f0105e1 100644
--- a/apps/studio/src/constants.ts
+++ b/apps/studio/src/constants.ts
@@ -1,7 +1,7 @@
import { HOUR_MS } from '@studio/common/constants';
export const DEFAULT_WIDTH = 900;
export const MAIN_MIN_HEIGHT = 600;
-export const SIDEBAR_WIDTH = 208;
+export const SIDEBAR_WIDTH = 248;
export const MAIN_MIN_WIDTH = DEFAULT_WIDTH - SIDEBAR_WIDTH + 20;
export const APP_CHROME_SPACING = 10;
export const MIN_WIDTH_CLASS_TO_MEASURE = 'app-measure-tabs-width';
diff --git a/apps/studio/src/hooks/use-site-details.tsx b/apps/studio/src/hooks/use-site-details.tsx
index e9e805f5db..c2b1fbc090 100644
--- a/apps/studio/src/hooks/use-site-details.tsx
+++ b/apps/studio/src/hooks/use-site-details.tsx
@@ -328,6 +328,7 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
running: false,
isAddingSite: true,
phpVersion: '',
+ iconColorIndex: prevData.length % 8,
},
] )
);
@@ -524,6 +525,7 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
running: false,
isAddingSite: true,
phpVersion: sourceSite.phpVersion,
+ iconColorIndex: prevData.length % 8,
},
] )
);
diff --git a/apps/studio/src/hooks/use-theme-details.tsx b/apps/studio/src/hooks/use-theme-details.tsx
index 7ea95ecaea..a56f4cd211 100644
--- a/apps/studio/src/hooks/use-theme-details.tsx
+++ b/apps/studio/src/hooks/use-theme-details.tsx
@@ -6,15 +6,18 @@ import { getIpcApi } from 'src/lib/get-ipc-api';
type ThemeDetailsType = SiteDetails[ 'themeDetails' ] | undefined;
type ThumbnailType = string | undefined;
+type SiteIconType = string | undefined;
interface ThemeDetailsContextType {
loadingThemeDetails: Record< string, boolean >;
loadingThumbnails: Record< string, boolean >;
themeDetails: Record< string, ThemeDetailsType >;
thumbnails: Record< string, ThumbnailType >;
+ siteIcons: Record< string, SiteIconType >;
initialLoading: boolean;
selectedThemeDetails: ThemeDetailsType;
selectedThumbnail: ThumbnailType;
+ selectedSiteIcon: SiteIconType;
selectedLoadingThemeDetails: boolean;
selectedLoadingThumbnails: boolean;
}
@@ -24,9 +27,11 @@ export const ThemeDetailsContext = createContext< ThemeDetailsContextType >( {
loadingThumbnails: {},
themeDetails: {},
thumbnails: {},
+ siteIcons: {},
initialLoading: false,
selectedThemeDetails: undefined,
selectedThumbnail: undefined,
+ selectedSiteIcon: undefined,
selectedLoadingThemeDetails: false,
selectedLoadingThumbnails: false,
} );
@@ -44,6 +49,7 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c
{}
);
const [ loadingThumbnails, setLoadingThumbnails ] = useState< Record< string, boolean > >( {} );
+ const [ siteIcons, setSiteIcons ] = useState< Record< string, SiteIconType > >( {} );
useIpcListener( 'theme-details-loading', ( _evt, { id } ) => {
setLoadingThemeDetails( ( loadingThemeDetails ) => {
@@ -81,31 +87,48 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c
} );
} );
+ useIpcListener( 'site-icon-loaded', ( _evt, { id, imageData } ) => {
+ setSiteIcons( ( siteIcons ) => {
+ return { ...siteIcons, [ id ]: imageData ?? undefined };
+ } );
+ } );
+
useWindowListener( 'focus', async () => {
- // When the window is focused, we need to kick off a request to refetch the theme details, if server is running.
- if ( ! selectedSite?.id || selectedSite.running === false ) {
- return;
- }
- await getIpcApi().loadThemeDetails( selectedSite.id, false );
+ // When the window is focused, refetch theme details for all running sites.
+ const runningSites = sites.filter( ( site ) => site.running );
+ await Promise.all(
+ runningSites.map( ( site ) => getIpcApi().loadThemeDetails( site.id, false ) )
+ );
} );
+ useEffect( () => {
+ // When the selected site changes, refresh its theme details (including icon).
+ if ( selectedSite?.id && selectedSite.running ) {
+ void getIpcApi().loadThemeDetails( selectedSite.id, false );
+ }
+ }, [ selectedSite?.id ] ); // eslint-disable-line react-hooks/exhaustive-deps
+
useEffect( () => {
let isCurrent = true;
// Initial load. Prefetch all the thumbnails for the sites.
const run = async () => {
const newThemeDetails = { ...themeDetails };
const newThumbnailData = { ...thumbnails };
+ const newSiteIcons = { ...siteIcons };
for ( const site of sites ) {
if ( site.themeDetails ) {
newThemeDetails[ site.id ] = { ...site.themeDetails };
const thumbnailData = await getIpcApi().getThumbnailData( site.id );
newThumbnailData[ site.id ] = thumbnailData ?? undefined;
}
+ const iconData = await getIpcApi().getSiteIconData( site.id );
+ newSiteIcons[ site.id ] = iconData ?? undefined;
}
if ( isCurrent ) {
setInitialLoad( true );
setThemeDetails( newThemeDetails );
setThumbnails( newThumbnailData );
+ setSiteIcons( newSiteIcons );
}
};
if ( sites.length > 0 && ! loadingSites && ! initialLoad && isCurrent ) {
@@ -114,17 +137,19 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c
return () => {
isCurrent = false;
};
- }, [ initialLoad, loadingSites, sites, themeDetails, thumbnails ] );
+ }, [ initialLoad, loadingSites, sites, themeDetails, thumbnails, siteIcons ] );
const contextValue = useMemo( () => {
return {
thumbnails,
themeDetails,
+ siteIcons,
loadingThemeDetails,
loadingThumbnails,
initialLoading: ! initialLoad,
selectedThemeDetails: themeDetails[ selectedSite?.id ?? '' ],
selectedThumbnail: thumbnails[ selectedSite?.id ?? '' ],
+ selectedSiteIcon: siteIcons[ selectedSite?.id ?? '' ],
selectedLoadingThemeDetails: loadingThemeDetails[ selectedSite?.id ?? '' ],
selectedLoadingThumbnails: loadingThumbnails[ selectedSite?.id ?? '' ],
};
@@ -133,6 +158,7 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c
loadingThemeDetails,
loadingThumbnails,
selectedSite?.id,
+ siteIcons,
themeDetails,
thumbnails,
] );
diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts
index ea040162dc..77d2a4c67a 100644
--- a/apps/studio/src/ipc-handlers.ts
+++ b/apps/studio/src/ipc-handlers.ts
@@ -72,7 +72,7 @@ import { supportedEditorConfig, SupportedEditor } from 'src/modules/user-setting
import { getUserTerminal } from 'src/modules/user-settings/lib/ipc-handlers';
import { winFindEditorPath } from 'src/modules/user-settings/lib/win-editor-path';
import { SiteServer, stopAllServers as triggerStopAllServers } from 'src/site-server';
-import { DEFAULT_SITE_PATH, getSiteThumbnailPath } from 'src/storage/paths';
+import { DEFAULT_SITE_PATH, getSiteIconPath, getSiteThumbnailPath } from 'src/storage/paths';
import {
loadUserData,
lockAppdata,
@@ -264,6 +264,10 @@ export async function createSite(
const siteId = providedSiteId || crypto.randomUUID();
+ // Determine the icon color index for this site (round-robin from palette of 8 colors)
+ const existingSites = ( await loadUserData() ).sites;
+ const iconColorIndex = existingSites.length % 8;
+
const metric = getBlueprintMetric( blueprint?.slug );
bumpStat( StatsGroup.STUDIO_SITE_CREATE, metric );
@@ -280,7 +284,7 @@ export async function createSite(
blueprint: blueprint?.blueprint,
noStart,
},
- { wpVersion, blueprint: blueprint?.blueprint }
+ { wpVersion, blueprint: blueprint?.blueprint, iconColorIndex }
);
// If the site is running after creation, fetch theme details and update thumbnail
@@ -608,6 +612,18 @@ export async function copySite(
);
}
+ const sourceIconPath = getSiteIconPath( sourceSiteId );
+ const newIconPath = getSiteIconPath( newSiteId );
+ if ( fs.existsSync( sourceIconPath ) ) {
+ await fs.promises.copyFile( sourceIconPath, newIconPath );
+ const iconData = await getImageData( newIconPath );
+ sendIpcEventToRendererWithWindow(
+ BrowserWindow.fromWebContents( event.sender ),
+ 'site-icon-loaded',
+ { id: newSiteId, imageData: iconData }
+ );
+ }
+
const port = await portFinder.getOpenPort();
const newSiteDetails: StoppedSiteDetails = {
@@ -624,6 +640,7 @@ export async function copySite(
try {
await lockAppdata();
const userData = await loadUserData();
+ newSiteDetails.iconColorIndex = userData.sites.length % 8;
userData.sites.push( newSiteDetails );
await saveUserData( userData );
} finally {
@@ -870,6 +887,19 @@ export async function loadThemeDetails(
console.error( `Failed to update thumbnail for server ${ id }:`, error );
}
+ try {
+ sendIpcEventToRendererWithWindow( parentWindow, 'site-icon-loading', { id } );
+ await server.updateCachedSiteIcon();
+ const iconData = await getImageData( getSiteIconPath( id ) );
+ sendIpcEventToRendererWithWindow( parentWindow, 'site-icon-loaded', {
+ id,
+ imageData: iconData,
+ } );
+ } catch ( error ) {
+ sendIpcEventToRendererWithWindow( parentWindow, 'site-icon-load-error', { id } );
+ console.error( `Failed to update site icon for server ${ id }:`, error );
+ }
+
return themeDetails;
}
@@ -920,6 +950,11 @@ export function getThumbnailData( _event: IpcMainInvokeEvent, id: string ) {
return getImageData( path );
}
+export function getSiteIconData( _event: IpcMainInvokeEvent, id: string ) {
+ const path = getSiteIconPath( id );
+ return getImageData( path );
+}
+
function promiseExec( command: string, options: ExecOptions = {} ): Promise< void > {
return new Promise( ( resolve, reject ) => {
exec( command, options, ( error ) => {
diff --git a/apps/studio/src/ipc-types.d.ts b/apps/studio/src/ipc-types.d.ts
index 00353d0da0..74a71a3e51 100644
--- a/apps/studio/src/ipc-types.d.ts
+++ b/apps/studio/src/ipc-types.d.ts
@@ -34,6 +34,7 @@ interface StoppedSiteDetails {
enableDebugLog?: boolean;
enableDebugDisplay?: boolean;
sortOrder?: number;
+ iconColorIndex?: number;
}
interface StartedSiteDetails extends StoppedSiteDetails {
diff --git a/apps/studio/src/ipc-utils.ts b/apps/studio/src/ipc-utils.ts
index 25cbc40425..d1260a3efe 100644
--- a/apps/studio/src/ipc-utils.ts
+++ b/apps/studio/src/ipc-utils.ts
@@ -50,6 +50,9 @@ export interface IpcEvents {
'thumbnail-loading': [ { id: string } ];
'thumbnail-loaded': [ { id: string; imageData: string | null } ];
'thumbnail-load-error': [ { id: string } ];
+ 'site-icon-loading': [ { id: string } ];
+ 'site-icon-loaded': [ { id: string; imageData: string | null } ];
+ 'site-icon-load-error': [ { id: string } ];
'user-settings': [ { tabName?: string } ];
'window-fullscreen-change': [ boolean ];
'user-preference-changed': [ void ];
diff --git a/apps/studio/src/preload.ts b/apps/studio/src/preload.ts
index 9e30c143bb..5b29781c9c 100644
--- a/apps/studio/src/preload.ts
+++ b/apps/studio/src/preload.ts
@@ -1,7 +1,7 @@
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
-import '@sentry/electron/preload';
+// import '@sentry/electron/preload'; // Crashes with Electron 39 — Sentry bug
import { IpcRendererEvent, contextBridge, ipcRenderer, webUtils } from 'electron';
import { IpcEvents } from 'src/ipc-utils';
@@ -91,6 +91,7 @@ const api: IpcApi = {
loadThemeDetails: ( id, emitThemeDetailsLoadingEvent = true ) =>
ipcRendererInvoke( 'loadThemeDetails', id, emitThemeDetailsLoadingEvent ),
getThumbnailData: ( id ) => ipcRendererInvoke( 'getThumbnailData', id ),
+ getSiteIconData: ( id ) => ipcRendererInvoke( 'getSiteIconData', id ),
getInstalledAppsAndTerminals: () => ipcRendererInvoke( 'getInstalledAppsAndTerminals' ),
importSite: ( { id, backupFile } ) => ipcRendererInvoke( 'importSite', { id, backupFile } ),
executeWPCLiInline: ( options ) => ipcRendererInvoke( 'executeWPCLiInline', options ),
diff --git a/apps/studio/src/site-server.ts b/apps/studio/src/site-server.ts
index 51953d085c..e756530dcf 100644
--- a/apps/studio/src/site-server.ts
+++ b/apps/studio/src/site-server.ts
@@ -1,4 +1,7 @@
+import { nativeImage } from 'electron';
import fs from 'fs';
+import http from 'node:http';
+import https from 'node:https';
import nodePath from 'path';
import * as Sentry from '@sentry/electron/main';
import { SQLITE_FILENAME } from '@studio/common/constants';
@@ -14,7 +17,7 @@ import { CliServerProcess } from 'src/modules/cli/lib/cli-server-process';
import { createSiteViaCli, type CreateSiteOptions } from 'src/modules/cli/lib/cli-site-creator';
import { executeCliCommand } from 'src/modules/cli/lib/execute-command';
import { createScreenshotWindow } from 'src/screenshot-window';
-import { getSiteThumbnailPath } from 'src/storage/paths';
+import { getSiteIconPath, getSiteThumbnailPath } from 'src/storage/paths';
import { loadUserData, lockAppdata, saveUserData, unlockAppdata } from 'src/storage/user-data';
import type { BlueprintV1Declaration } from '@wp-playground/blueprints';
@@ -77,10 +80,41 @@ function getAbsoluteUrl( details: SiteDetails ): string {
return `http://localhost:${ details.port }`;
}
+function fetchImageBuffer( url: string ): Promise< Buffer > {
+ return new Promise( ( resolve, reject ) => {
+ const protocol = url.startsWith( 'https' ) ? https : http;
+ const request = protocol.get( url, { timeout: 5000, rejectUnauthorized: false }, ( res ) => {
+ if (
+ res.statusCode &&
+ res.statusCode >= 300 &&
+ res.statusCode < 400 &&
+ res.headers.location
+ ) {
+ fetchImageBuffer( res.headers.location ).then( resolve, reject );
+ return;
+ }
+ if ( ! res.statusCode || res.statusCode >= 400 ) {
+ reject( new Error( `HTTP ${ res.statusCode } fetching ${ url }` ) );
+ return;
+ }
+ const chunks: Buffer[] = [];
+ res.on( 'data', ( chunk: Buffer ) => chunks.push( chunk ) );
+ res.on( 'end', () => resolve( Buffer.concat( chunks ) ) );
+ res.on( 'error', reject );
+ } );
+ request.on( 'timeout', () => {
+ request.destroy();
+ reject( new Error( `Timeout fetching ${ url }` ) );
+ } );
+ request.on( 'error', reject );
+ } );
+}
+
// We use SiteDetails for storing it in appdata-v1.json, so this meta was introduced for extra data which is not stored locally
type SiteServerMeta = {
wpVersion?: string;
blueprint?: BlueprintV1Declaration;
+ iconColorIndex?: number;
};
export class SiteServer {
@@ -141,18 +175,34 @@ export class SiteServer {
port: 0,
phpVersion: options.phpVersion || '',
running: false,
+ iconColorIndex: meta.iconColorIndex,
};
const server = SiteServer.register( placeholderDetails, meta );
server.hasOngoingOperation = true;
try {
const result = await createSiteViaCli( { ...options, siteId } );
- const userData = await loadUserData();
- const siteData = userData.sites.find( ( s ) => s.id === result.id );
- if ( ! siteData ) {
- throw new Error( `Site with ID ${ result.id } not found in appdata after CLI creation` );
+
+ // Persist iconColorIndex to appdata immediately so file-watcher
+ // reloads never see the site without it (prevents color flash).
+ let userData: Awaited< ReturnType< typeof loadUserData > >;
+ try {
+ await lockAppdata();
+ userData = await loadUserData();
+ const siteData = userData.sites.find( ( s ) => s.id === result.id );
+ if ( ! siteData ) {
+ throw new Error( `Site with ID ${ result.id } not found in appdata after CLI creation` );
+ }
+ if ( meta.iconColorIndex !== undefined ) {
+ siteData.iconColorIndex = meta.iconColorIndex;
+ await saveUserData( userData );
+ }
+ } finally {
+ await unlockAppdata();
}
+ const siteData = userData.sites.find( ( s ) => s.id === result.id )!;
+
let siteDetails: SiteDetails;
if ( result.running ) {
const url = siteData.customDomain
@@ -191,6 +241,11 @@ export class SiteServer {
await fs.promises.unlink( thumbnailPath );
}
+ const iconPath = getSiteIconPath( this.details.id );
+ if ( fs.existsSync( iconPath ) ) {
+ await fs.promises.unlink( iconPath );
+ }
+
await this.server.delete( deleteFiles );
deletedServers.push( this.details.id );
servers.delete( this.details.id );
@@ -300,6 +355,44 @@ export class SiteServer {
.finally( () => window.destroy() );
}
+ async updateCachedSiteIcon() {
+ if ( ! this.details.running ) {
+ return;
+ }
+
+ let iconUrl = '';
+
+ // Use get_site_icon_url() PHP function to resolve the actual icon URL.
+ // The 'site_icon' WP option only stores an attachment ID, not a URL.
+ try {
+ const { stdout, exitCode } = await this.executeWpCliCommand(
+ [ 'eval', 'echo get_site_icon_url( 512 );' ],
+ { skipPluginsAndThemes: true }
+ );
+ if ( exitCode === 0 && stdout.trim() ) {
+ iconUrl = stdout.trim();
+ }
+ } catch {
+ // WP-CLI failed, no icon available
+ }
+
+ if ( ! iconUrl ) {
+ return;
+ }
+
+ const buffer = await fetchImageBuffer( iconUrl );
+ const image = nativeImage.createFromBuffer( buffer );
+ if ( image.isEmpty() ) {
+ throw new Error( 'Could not parse image from buffer' );
+ }
+
+ const resized = image.resize( { width: 64, height: 64 } );
+ const outPath = getSiteIconPath( this.details.id );
+ const outDir = nodePath.dirname( outPath );
+ await fs.promises.mkdir( outDir, { recursive: true } );
+ await fs.promises.writeFile( outPath, resized.toPNG() );
+ }
+
async executeWpCliCommand(
args: string | string[],
{
diff --git a/apps/studio/src/storage/paths.ts b/apps/studio/src/storage/paths.ts
index 65d180fbf6..1ff0164e31 100644
--- a/apps/studio/src/storage/paths.ts
+++ b/apps/studio/src/storage/paths.ts
@@ -42,6 +42,10 @@ export function getSiteThumbnailPath( siteId: string ): string {
return path.join( getAppDataPath(), getAppName(), 'thumbnails', `${ siteId }.png` );
}
+export function getSiteIconPath( siteId: string ): string {
+ return path.join( getAppDataPath(), getAppName(), 'site-icons', `${ siteId }.png` );
+}
+
export function getResourcesPath(): string {
if ( ! app ) {
throw new Error( 'Electron app not available in child process' );
diff --git a/apps/studio/src/storage/user-data.ts b/apps/studio/src/storage/user-data.ts
index d34cf32e76..d512acdd06 100644
--- a/apps/studio/src/storage/user-data.ts
+++ b/apps/studio/src/storage/user-data.ts
@@ -200,6 +200,7 @@ function toDiskFormat( { sites, ...rest }: UserData ): PersistedUserData {
enableDebugLog,
enableDebugDisplay,
sortOrder,
+ iconColorIndex,
} ) => {
// No object spreading allowed. TypeScript's structural typing is too permissive and
// will permit us to persist properties that aren't in the type definition.
@@ -220,6 +221,7 @@ function toDiskFormat( { sites, ...rest }: UserData ): PersistedUserData {
enableDebugLog,
enableDebugDisplay,
sortOrder,
+ iconColorIndex,
themeDetails: {
name: themeDetails?.name || '',
path: themeDetails?.path || '',
diff --git a/apps/studio/src/tests/ipc-handlers.test.ts b/apps/studio/src/tests/ipc-handlers.test.ts
index 315e22cf82..db181e63a0 100644
--- a/apps/studio/src/tests/ipc-handlers.test.ts
+++ b/apps/studio/src/tests/ipc-handlers.test.ts
@@ -70,14 +70,17 @@ const mockSiteDetails: StoppedSiteDetails = {
enableHttps: undefined,
};
-vi.mocked( SiteServer.create ).mockResolvedValue( {
- server: {
- start: vi.fn(),
- details: mockSiteDetails,
- updateSiteDetails: vi.fn(),
- updateCachedThumbnail: vi.fn().mockResolvedValue( undefined ),
- } as unknown as SiteServer,
- details: mockSiteDetails,
+vi.mocked( SiteServer.create ).mockImplementation( async ( _options, meta ) => {
+ const details = { ...mockSiteDetails, iconColorIndex: meta?.iconColorIndex };
+ return {
+ server: {
+ start: vi.fn(),
+ details,
+ updateSiteDetails: vi.fn(),
+ updateCachedThumbnail: vi.fn().mockResolvedValue( undefined ),
+ } as unknown as SiteServer,
+ details,
+ };
} );
vi.mocked( SiteServer.register, { partial: true } ).mockImplementation( ( details ) => ( {
@@ -123,6 +126,7 @@ describe( 'createSite', () => {
customDomain: undefined,
enableHttps: undefined,
isWpAutoUpdating: false,
+ iconColorIndex: 0,
} );
expect( SiteServer.create ).toHaveBeenCalledWith(