From 09835c7e14ae134923b9a742e33ceee2211fe770 Mon Sep 17 00:00:00 2001 From: Mika Andrianarijaona Date: Fri, 6 Feb 2026 11:40:53 +0100 Subject: [PATCH 01/10] Add storage support for custom site directory --- apps/studio/src/storage/storage-types.ts | 1 + apps/studio/src/storage/user-data.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/studio/src/storage/storage-types.ts b/apps/studio/src/storage/storage-types.ts index c7d53e2299..44af6a3ad8 100644 --- a/apps/studio/src/storage/storage-types.ts +++ b/apps/studio/src/storage/storage-types.ts @@ -30,6 +30,7 @@ export interface UserData { preferredEditor?: SupportedEditor; betaFeatures?: BetaFeatures; stopSitesOnQuit?: boolean; + defaultSiteDirectory?: string; } export interface PersistedUserData extends Omit< UserData, 'sites' > { diff --git a/apps/studio/src/storage/user-data.ts b/apps/studio/src/storage/user-data.ts index 6ca0ca6985..b1fbbf690d 100644 --- a/apps/studio/src/storage/user-data.ts +++ b/apps/studio/src/storage/user-data.ts @@ -160,7 +160,8 @@ type UserDataSafeKeys = | 'lastSeenVersion' | 'preferredTerminal' | 'preferredEditor' - | 'betaFeatures'; + | 'betaFeatures' + | 'defaultSiteDirectory'; type PartialUserDataWithSafeKeysToUpdate = Partial< Pick< UserData, UserDataSafeKeys > >; From 2a35b84c7ccc24dfe53b7f5f5d349823396e565a Mon Sep 17 00:00:00 2001 From: Mika Andrianarijaona Date: Fri, 6 Feb 2026 11:47:04 +0100 Subject: [PATCH 02/10] Add IPC helpers for default site directory --- apps/studio/src/ipc-handlers.ts | 5 ++++- .../src/modules/user-settings/lib/ipc-handlers.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index 9e2267e161..ed1238f65f 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -91,7 +91,7 @@ import { supportedEditorConfig, SupportedEditor } from 'src/modules/user-setting import { getUserEditor, 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 { getSiteThumbnailPath } from 'src/storage/paths'; import { loadUserData, lockAppdata, @@ -99,6 +99,7 @@ import { unlockAppdata, updateAppdata, } from 'src/storage/user-data'; +import { resolveDefaultSiteDirectory } from 'src/storage/default-site-directory'; import { Blueprint } from 'src/stores/wpcom-api'; import type { RawDirectoryEntry } from 'src/modules/sync/types'; import type { WpCliResult } from 'src/site-server'; @@ -140,6 +141,8 @@ export { saveUserLocale, saveUserTerminal, showUserSettings, + getDefaultSiteDirectory, + saveDefaultSiteDirectory, } from 'src/modules/user-settings/lib/ipc-handlers'; export async function getAgentInstructionsStatus( diff --git a/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts b/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts index 392bc18239..7b843d83cf 100644 --- a/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts +++ b/apps/studio/src/modules/user-settings/lib/ipc-handlers.ts @@ -7,6 +7,7 @@ import { SUPPORTED_EDITORS, SupportedEditor } from 'src/modules/user-settings/li import { SupportedTerminal } from 'src/modules/user-settings/lib/terminal'; import { UserSettingsTabName } from 'src/modules/user-settings/user-settings-types'; import { loadUserData, updateAppdata } from 'src/storage/user-data'; +import { ensureWritableDirectory } from 'src/storage/default-site-directory'; export function getInstalledAppsAndTerminals(): InstalledApps { return { @@ -49,6 +50,17 @@ export async function saveUserEditor( event: IpcMainInvokeEvent, editor: Support await updateAppdata( { preferredEditor: editor } ); } +export async function getDefaultSiteDirectory(): Promise< string | undefined > { + const userData = await loadUserData(); + return userData.defaultSiteDirectory; +} + +export async function saveDefaultSiteDirectory( event: IpcMainInvokeEvent, directory: string ) { + await ensureWritableDirectory( directory ); + await sendIpcEventToRenderer( 'user-preference-changed' ); + await updateAppdata( { defaultSiteDirectory: directory } ); +} + export async function getUserLocale() { return getUserLocaleWithFallback(); } From 9a33722eecb1997b1ceaf0cd5f61b5a7a520a9ca Mon Sep 17 00:00:00 2001 From: Mika Andrianarijaona Date: Fri, 6 Feb 2026 11:48:37 +0100 Subject: [PATCH 03/10] Use default site preference in dialog helpers --- apps/studio/src/ipc-handlers.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index ed1238f65f..32519e2cac 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -614,9 +614,11 @@ export async function showSaveAsDialog( event: IpcMainInvokeEvent, options: Save throw new Error( `No window found for sender of showSaveAsDialog message: ${ event.frameId }` ); } + const defaultSiteDirectory = await resolveDefaultSiteDirectory(); const defaultPath = - options.defaultPath === nodePath.basename( options.defaultPath ?? '' ) - ? nodePath.join( DEFAULT_SITE_PATH, options.defaultPath ) + typeof options.defaultPath === 'string' && + options.defaultPath === nodePath.basename( options.defaultPath ) + ? nodePath.join( defaultSiteDirectory, options.defaultPath ) : options.defaultPath; const { canceled, filePath } = await dialog.showSaveDialog( parentWindow, { defaultPath, @@ -651,9 +653,10 @@ export async function showOpenFolderDialog( }; } + const defaultSiteDirectory = await resolveDefaultSiteDirectory(); const { canceled, filePaths } = await dialog.showOpenDialog( parentWindow, { title, - defaultPath: defaultDialogPath !== '' ? defaultDialogPath : DEFAULT_SITE_PATH, + defaultPath: defaultDialogPath !== '' ? defaultDialogPath : defaultSiteDirectory, properties: [ 'openDirectory', 'createDirectory', // allow user to create new directories; macOS only @@ -895,7 +898,8 @@ export async function generateProposedSitePath( _event: IpcMainInvokeEvent, siteName: string ): Promise< FolderDialogResponse > { - const path = nodePath.join( DEFAULT_SITE_PATH, sanitizeFolderName( siteName ) ); + const defaultSiteDirectory = await resolveDefaultSiteDirectory(); + const path = nodePath.join( defaultSiteDirectory, sanitizeFolderName( siteName ) ); try { return { From 89f4ce1d7a4bc27b0abf38dec63805565bf5937e Mon Sep 17 00:00:00 2001 From: Mika Andrianarijaona Date: Fri, 6 Feb 2026 14:03:13 +0100 Subject: [PATCH 04/10] wip: create new component --- apps/studio/src/ipc-handlers.ts | 2 +- .../components/default-directory-picker.tsx | 39 +++++++++++++ .../components/preferences-tab.tsx | 58 ++++++++++++++++++- .../modules/user-settings/lib/ipc-handlers.ts | 10 ++-- apps/studio/src/preload.ts | 3 + .../src/storage/default-site-directory.ts | 42 ++++++++++++++ 6 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 apps/studio/src/modules/user-settings/components/default-directory-picker.tsx create mode 100644 apps/studio/src/storage/default-site-directory.ts diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index 32519e2cac..ef187a02de 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -91,6 +91,7 @@ import { supportedEditorConfig, SupportedEditor } from 'src/modules/user-setting import { getUserEditor, 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 { resolveDefaultSiteDirectory } from 'src/storage/default-site-directory'; import { getSiteThumbnailPath } from 'src/storage/paths'; import { loadUserData, @@ -99,7 +100,6 @@ import { unlockAppdata, updateAppdata, } from 'src/storage/user-data'; -import { resolveDefaultSiteDirectory } from 'src/storage/default-site-directory'; import { Blueprint } from 'src/stores/wpcom-api'; import type { RawDirectoryEntry } from 'src/modules/sync/types'; import type { WpCliResult } from 'src/site-server'; diff --git a/apps/studio/src/modules/user-settings/components/default-directory-picker.tsx b/apps/studio/src/modules/user-settings/components/default-directory-picker.tsx new file mode 100644 index 0000000000..f6960cd13e --- /dev/null +++ b/apps/studio/src/modules/user-settings/components/default-directory-picker.tsx @@ -0,0 +1,39 @@ +import { useI18n } from '@wordpress/react-i18n'; +import Button from 'src/components/button'; +import { SettingsFormField } from './settings-form-field'; + +interface DefaultDirectoryPickerProps { + directory?: string; + isLoading: boolean; + isSelecting: boolean; + onPick: () => void; +} + +export const DefaultDirectoryPicker = ( { + directory, + isLoading, + isSelecting, + onPick, +}: DefaultDirectoryPickerProps ) => { + const { __ } = useI18n(); + + return ( + +
+

+ { isLoading ? __( 'Loading...' ) : directory ?? '' } +

+
+ +
+
+
+ ); +}; diff --git a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx index 9ff501bbfd..c328d11be6 100644 --- a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx @@ -1,10 +1,11 @@ import { SupportedLocale } from '@studio/common/lib/locale'; import { useI18n } from '@wordpress/react-i18n'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import Button from 'src/components/button'; import { useFeatureFlags } from 'src/hooks/use-feature-flags'; import { isWindowsStore } from 'src/lib/app-globals'; import { McpSettings } from 'src/modules/mcp/components/mcp-settings'; +import { getIpcApi } from 'src/lib/get-ipc-api'; import { EditorPicker } from 'src/modules/user-settings/components/editor-picker'; import { LanguagePicker } from 'src/modules/user-settings/components/language-picker'; import { StudioCliToggle } from 'src/modules/user-settings/components/studio-cli-toggle'; @@ -21,6 +22,7 @@ import { useGetStudioCliIsInstalledQuery, useSaveStudioCliIsInstalledMutation, } from 'src/stores/installed-apps-api'; +import { DefaultDirectoryPicker } from './default-directory-picker'; export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { const { __ } = useI18n(); @@ -40,6 +42,10 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { const [ dirtyEditor, setDirtyEditor ] = useState< SupportedEditor | null >(); const [ dirtyTerminal, setDirtyTerminal ] = useState< SupportedTerminal >(); const [ dirtyIsCliInstalled, setDirtyIsCliInstalled ] = useState< boolean >(); + const [ storedDefaultSiteDirectory, setStoredDefaultSiteDirectory ] = useState< string >(); + const [ defaultSiteDirectory, setDefaultSiteDirectory ] = useState< string >(); + const [ isLoadingDefaultSiteDirectory, setIsLoadingDefaultSiteDirectory ] = useState( true ); + const [ isSelectingDefaultDirectory, setIsSelectingDefaultDirectory ] = useState( false ); const savePreferences = async () => { if ( dirtyLocale ) { @@ -54,6 +60,13 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { if ( dirtyIsCliInstalled !== undefined ) { await saveCliIsInstalled( dirtyIsCliInstalled ); } + const isDefaultDirectoryDirty = + storedDefaultSiteDirectory !== undefined && + defaultSiteDirectory !== undefined && + storedDefaultSiteDirectory !== defaultSiteDirectory; + if ( isDefaultDirectoryDirty && defaultSiteDirectory ) { + await getIpcApi().saveDefaultSiteDirectory( defaultSiteDirectory ); + } onClose(); }; @@ -67,8 +80,45 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { [ dirtyEditor, editor ], [ dirtyTerminal, terminal ], [ dirtyIsCliInstalled, isCliInstalled ], + [ defaultSiteDirectory, storedDefaultSiteDirectory ], ].some( ( [ a, b ] ) => a !== undefined && a !== b ); + useEffect( () => { + let isMounted = true; + void ( async () => { + try { + const directory = await getIpcApi().getDefaultSiteDirectory(); + if ( ! isMounted ) { + return; + } + setStoredDefaultSiteDirectory( directory ); + setDefaultSiteDirectory( directory ); + } finally { + if ( isMounted ) { + setIsLoadingDefaultSiteDirectory( false ); + } + } + } )(); + return () => { + isMounted = false; + }; + }, [] ); + + const handleChangeDefaultDirectory = async () => { + setIsSelectingDefaultDirectory( true ); + try { + const response = await getIpcApi().showOpenFolderDialog( + __( 'Select default site directory' ), + defaultSiteDirectory ?? '' + ); + if ( response?.path ) { + setDefaultSiteDirectory( response.path ); + } + } finally { + setIsSelectingDefaultDirectory( false ); + } + }; + return ( <> @@ -84,6 +134,12 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { { enableAgentSuite && } ) } +
+ + +
+ ); +} diff --git a/apps/studio/src/components/site-form-error.tsx b/apps/studio/src/components/site-form-error.tsx new file mode 100644 index 0000000000..96380c527f --- /dev/null +++ b/apps/studio/src/components/site-form-error.tsx @@ -0,0 +1,35 @@ +import { Icon } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { cautionFilled, tip } from '@wordpress/icons'; +import { cx } from 'src/lib/cx'; + +export interface SiteFormErrorProps { + error?: string; + tipMessage?: string; + className?: string; +} + +export const SiteFormError = ( { error, tipMessage = '', className = '' }: SiteFormErrorProps ) => { + return ( + ( error || tipMessage ) && ( + + ) + ); +}; diff --git a/apps/studio/src/modules/add-site/components/create-site-form.tsx b/apps/studio/src/modules/add-site/components/create-site-form.tsx index a72e60e534..34dbe8191c 100644 --- a/apps/studio/src/modules/add-site/components/create-site-form.tsx +++ b/apps/studio/src/modules/add-site/components/create-site-form.tsx @@ -16,9 +16,10 @@ import { tip, cautionFilled, chevronRight, chevronDown, chevronLeft } from '@wor import { useI18n } from '@wordpress/react-i18n'; import { FormEvent, useState, useEffect, useCallback, useMemo, useRef, RefObject } from 'react'; import Button from 'src/components/button'; -import FolderIcon from 'src/components/folder-icon'; +import { FormPathInputComponent } from 'src/components/form-path-input'; import { LearnMoreLink, LearnHowLink } from 'src/components/learn-more'; import PasswordControl from 'src/components/password-control'; +import { SiteFormError } from 'src/components/site-form-error'; import TextControlComponent from 'src/components/text-control'; import { WPVersionSelector } from 'src/components/wp-version-selector'; import { cx } from 'src/lib/cx'; @@ -58,104 +59,6 @@ interface CreateSiteFormProps { formRef?: RefObject< HTMLFormElement >; } -interface FormPathInputComponentProps { - value: string; - onClick: () => void; - error?: string; - doesPathContainWordPress: boolean; - id?: string; -} - -interface SiteFormErrorProps { - error?: string; - tipMessage?: string; - className?: string; -} - -const SiteFormError = ( { error, tipMessage = '', className = '' }: SiteFormErrorProps ) => { - return ( - ( error || tipMessage ) && ( - - ) - ); -}; - -function FormPathInputComponent( { - value, - onClick, - error, - doesPathContainWordPress, - id, -}: FormPathInputComponentProps ) { - const { __ } = useI18n(); - return ( -
- - - -
- ); -} - export const CreateSiteForm = ( { defaultValues = {}, onSelectPath, diff --git a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx index c328d11be6..8d117dfde6 100644 --- a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx @@ -4,8 +4,8 @@ import { useEffect, useState } from 'react'; import Button from 'src/components/button'; import { useFeatureFlags } from 'src/hooks/use-feature-flags'; import { isWindowsStore } from 'src/lib/app-globals'; -import { McpSettings } from 'src/modules/mcp/components/mcp-settings'; import { getIpcApi } from 'src/lib/get-ipc-api'; +import { McpSettings } from 'src/modules/mcp/components/mcp-settings'; import { EditorPicker } from 'src/modules/user-settings/components/editor-picker'; import { LanguagePicker } from 'src/modules/user-settings/components/language-picker'; import { StudioCliToggle } from 'src/modules/user-settings/components/studio-cli-toggle'; From 834325c9c8eec339ebfe21ac78921e9e33697eba Mon Sep 17 00:00:00 2001 From: Mika Andrianarijaona Date: Wed, 18 Mar 2026 10:10:55 +0100 Subject: [PATCH 09/10] Replace DefaultDirectoryPicker with FormPathInputComponent in PreferencesTab --- .../components/preferences-tab.tsx | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx index 8d117dfde6..75b9e7f55e 100644 --- a/apps/studio/src/modules/user-settings/components/preferences-tab.tsx +++ b/apps/studio/src/modules/user-settings/components/preferences-tab.tsx @@ -2,6 +2,7 @@ import { SupportedLocale } from '@studio/common/lib/locale'; import { useI18n } from '@wordpress/react-i18n'; import { useEffect, useState } from 'react'; import Button from 'src/components/button'; +import { FormPathInputComponent } from 'src/components/form-path-input'; import { useFeatureFlags } from 'src/hooks/use-feature-flags'; import { isWindowsStore } from 'src/lib/app-globals'; import { getIpcApi } from 'src/lib/get-ipc-api'; @@ -22,7 +23,7 @@ import { useGetStudioCliIsInstalledQuery, useSaveStudioCliIsInstalledMutation, } from 'src/stores/installed-apps-api'; -import { DefaultDirectoryPicker } from './default-directory-picker'; +import { SettingsFormField } from './settings-form-field'; export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { const { __ } = useI18n(); @@ -45,7 +46,6 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { const [ storedDefaultSiteDirectory, setStoredDefaultSiteDirectory ] = useState< string >(); const [ defaultSiteDirectory, setDefaultSiteDirectory ] = useState< string >(); const [ isLoadingDefaultSiteDirectory, setIsLoadingDefaultSiteDirectory ] = useState( true ); - const [ isSelectingDefaultDirectory, setIsSelectingDefaultDirectory ] = useState( false ); const savePreferences = async () => { if ( dirtyLocale ) { @@ -105,17 +105,12 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { }, [] ); const handleChangeDefaultDirectory = async () => { - setIsSelectingDefaultDirectory( true ); - try { - const response = await getIpcApi().showOpenFolderDialog( - __( 'Select default site directory' ), - defaultSiteDirectory ?? '' - ); - if ( response?.path ) { - setDefaultSiteDirectory( response.path ); - } - } finally { - setIsSelectingDefaultDirectory( false ); + const response = await getIpcApi().showOpenFolderDialog( + __( 'Select default site directory' ), + defaultSiteDirectory ?? '' + ); + if ( response?.path ) { + setDefaultSiteDirectory( response.path ); } }; @@ -134,12 +129,13 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => { { enableAgentSuite && } ) } - + + +
-
- - - ); -};