Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
428a5fb
Agentic UI site creation onboarding
shaunandrews Jun 23, 2026
9615da5
Remove admin email onboarding icon
shaunandrews Jun 23, 2026
40c2fbb
Consolidate shared onboarding helpers
shaunandrews Jun 23, 2026
889318f
Merge remote-tracking branch 'origin/trunk' into agentic-ui/site-crea…
shaunandrews Jun 29, 2026
12c975a
Avoid treating development environments as staging
shaunandrews Jun 29, 2026
756ed7c
Fix sync site filtering and environment labels
shaunandrews Jun 29, 2026
e4787e3
Restore trunk sync site filtering and environment behavior
bcotrim Jul 2, 2026
bcbc655
Merge remote-tracking branch 'origin/trunk' into agentic-ui/site-crea…
bcotrim Jul 2, 2026
a82f999
Fix onboarding import IPC call, roll back failed imports, fix backup …
bcotrim Jul 2, 2026
005ca2a
Fix stale-search auto-select race and drop dead already-connected gro…
bcotrim Jul 2, 2026
f3189dc
Let the onboarding dot grid sleep when the pointer idles or leaves th…
bcotrim Jul 2, 2026
ab3215e
Report parse-dropped /me/sites entries to Sentry
bcotrim Jul 2, 2026
ded1da1
Remove WordPress.com connections when a site is deleted
bcotrim Jul 2, 2026
6ef4ab7
Fix grammar in blueprint selector subtitle
bcotrim Jul 2, 2026
0e520bf
Merge remote-tracking branch 'origin/trunk' into agentic-ui/site-crea…
bcotrim Jul 3, 2026
70b9b49
Wire onboarding connector methods through the studio ui local surface
bcotrim Jul 3, 2026
ec0ce25
Fix create site form getting stuck when path generation fails or is s…
bcotrim Jul 3, 2026
e83e2f5
Remove extracted blueprint bundle temp dir after site create on the l…
bcotrim Jul 3, 2026
54731e9
Drop the paged syncable-sites API; the connect picker filters the unp…
bcotrim Jul 3, 2026
3cb595a
Start the pulled site after connect and surface picker load errors
bcotrim Jul 3, 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
46 changes: 44 additions & 2 deletions apps/local/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { DEFAULT_TOKEN_LIFETIME_MS } from '@studio/common/constants';
import { createCliRunner } from '@studio/common/lib/cli-process';
import {
addConnectedWpcomSite,
getAllConnectedWpcomSitesForCurrentUser,
getConnectedWpcomSitesForLocalSite,
removeConnectedWpcomSite,
} from '@studio/common/lib/connected-sites';
Expand All @@ -35,7 +36,7 @@ import {
} from '@studio/common/lib/fs-utils';
import { generateNumberedName, generateSiteName } from '@studio/common/lib/generate-site-name';
import { isErrnoException } from '@studio/common/lib/is-errno-exception';
import { getAuthenticationUrl } from '@studio/common/lib/oauth';
import { getAuthenticationUrl, getSignUpUrl } from '@studio/common/lib/oauth';
import { decodePassword } from '@studio/common/lib/passwords';
import { sanitizeFolderName } from '@studio/common/lib/sanitize-folder-name';
import {
Expand Down Expand Up @@ -363,7 +364,13 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
res.json( { url: null } );
return;
}
res.json( { url: getAuthenticationUrl( 'en', redirectUri ) } );
// `signup=1` routes through WordPress.com account creation first, then
// back into the same authorize redirect.
const url =
req.query.signup === '1'
? getSignUpUrl( 'en', redirectUri )
: getAuthenticationUrl( 'en', redirectUri );
res.json( { url } );
} );

// Log in by storing a token from the redirect callback (or pasted from
Expand Down Expand Up @@ -501,6 +508,22 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
} )
);

// Numbered-name collision search ("My Site", "My Site 2", ...) against both
// existing site names and non-empty site folders — mirrors the desktop's
// findAvailableSitePath.
api.get(
'/site-defaults/available-path',
asyncHandler( async ( req: Request, res: Response ) => {
const sites = await listSites( execute );
const name = await generateNumberedName(
String( req.query.name ?? '' ),
sites.map( ( s ) => s.name ),
sitesRoot
);
res.json( { name, path: path.join( sitesRoot, sanitizeFolderName( name ) ) } );
} )
);

api.post( '/paths/compare', ( req: Request, res: Response ) => {
const { path1, path2 } = req.body as { path1?: string; path2?: string };
res.json( { equal: !! path1 && !! path2 && arePathsEqual( path1, path2 ) } );
Expand All @@ -519,6 +542,7 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
adminUsername?: string;
adminPassword?: string;
adminEmail?: string;
skipStart?: boolean;
// Optional Blueprint to apply on creation: `blueprint` is the parsed
// blueprint JSON; `filePath` (set for uploaded ZIP bundles) lets the
// CLI resolve relative assets.
Expand Down Expand Up @@ -546,6 +570,7 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
adminUsername: body.adminUsername,
adminPassword: body.adminPassword,
adminEmail: body.adminEmail,
noStart: body.skipStart,
blueprint: body.blueprint?.blueprint,
originalBlueprintPath: body.blueprint?.filePath,
} );
Expand All @@ -559,6 +584,14 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
} );
} finally {
cleanup();
// The client intentionally skips success-path cleanup of an uploaded
// ZIP bundle's extract dir (see blueprint-selector) — the desktop
// removes it once create settles, so mirror that here.
if ( body.blueprint?.filePath ) {
await cleanupBlueprintTempDir(
path.dirname( path.resolve( body.blueprint.filePath ) )
).catch( () => undefined );
}
}

const created = ( await listSites( execute ) ).find( ( s ) => s.id === siteId );
Expand Down Expand Up @@ -966,6 +999,15 @@ export async function startLocalServer( options: LocalServerOptions ): Promise<
} )
);

// All connections for the current user, across local sites — the browser
// analog of the desktop's empty-id getConnectedWpcomSites contract.
api.get(
'/wpcom/connected-sites',
asyncHandler( async ( _req: Request, res: Response ) => {
res.json( await getAllConnectedWpcomSitesForCurrentUser() );
} )
);

api.get(
'/sites/:id/connected-sites',
asyncHandler( async ( req: Request, res: Response ) => {
Expand Down
17 changes: 16 additions & 1 deletion apps/studio/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ import {
} from '@studio/common/lib/blueprint-bundle';
import { validateBlueprintData } from '@studio/common/lib/blueprint-validation';
import { parseCliError, errorMessageContains } from '@studio/common/lib/cli-error';
import { getConnectedWpcomSitesForLocalSite } from '@studio/common/lib/connected-sites';
import {
getConnectedWpcomSitesForLocalSite,
removeConnectedWpcomSite,
} from '@studio/common/lib/connected-sites';
import { createDeployIgnoreFilter } from '@studio/common/lib/deploy-ignore';
import {
calculateDirectorySizeForArchive,
Expand Down Expand Up @@ -1116,6 +1119,18 @@ export async function deleteSite( event: IpcMainInvokeEvent, id: string, deleteF
throw new Error( 'Site not found.' );
}
await server.delete( deleteFiles );
// Remove the deleted site's WordPress.com connections so the remote sites
// don't stay marked as connected to a local site that no longer exists.
// Owning this here covers every caller (default UI, apps/ui, rollbacks).
try {
const connectedSites = await getConnectedWpcomSitesForLocalSite( id );
for ( const site of connectedSites ) {
await removeConnectedWpcomSite( id, site.id );
}
} catch ( error ) {
// Cleanup must not fail the deletion itself.
Sentry.captureException( error );
}
}

export async function copySite(
Expand Down
8 changes: 1 addition & 7 deletions apps/studio/src/lib/oauth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { CLIENT_ID } from '@studio/common/constants';
import { SupportedLocale } from '@studio/common/lib/locale';
import { getAuthenticationUrl } from '@studio/common/lib/oauth';
import { readAuthToken, type StoredAuthToken } from '@studio/common/lib/shared-config';

export function getSignUpUrl( locale: SupportedLocale ) {
const oauth2Redirect = encodeURIComponent( getAuthenticationUrl( locale ) );
return `https://wordpress.com/start/wpcc/oauth2-user?oauth2_client_id=${ CLIENT_ID }&oauth2_redirect=${ oauth2Redirect }&locale=${ locale }`;
}
export { getSignUpUrl } from '@studio/common/lib/oauth';

export async function getAuthenticationToken(): Promise< StoredAuthToken | null > {
return readAuthToken();
Expand Down
46 changes: 5 additions & 41 deletions apps/studio/src/modules/add-site/components/new-site-options.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
curateBlueprintsForDisplay,
FEATURED_BLUEPRINT_SLUGS,
} from '@studio/common/lib/blueprint-curation';
import {
__experimentalVStack as VStack,
__experimentalHeading as Heading,
Expand Down Expand Up @@ -171,46 +175,6 @@ function BlueprintCard( {
);
}

const BLUEPRINT_DISPLAY_NAMES: Record< string, string > = {
'Quick Start': 'WordPress.com',
Development: 'WordPress Dev',
Commerce: 'WooCommerce',
};

function getBlueprintExcerptOverrides( __: ( text: string ) => string ): Record< string, string > {
return {
'Quick Start': __(
'A WordPress.com-like environment with Business plan plugins and themes pre-installed.'
),
Commerce: __(
'Create your next online store with WooCommerce and its companion plugins pre-installed.'
),
Development: __( 'A streamlined environment for building and testing themes or plugins.' ),
};
}

const BLUEPRINT_ORDER: Record< string, number > = {
'Quick Start': 1,
Commerce: 2,
Development: 3,
};

const FEATURED_BLUEPRINT_SLUGS = new Set( [ 'woo-shop', 'development', 'quick-start' ] );

function renameBlueprintsForDisplay(
blueprints: Blueprint[],
__: ( text: string ) => string
): Blueprint[] {
const excerptOverrides = getBlueprintExcerptOverrides( __ );
return [ ...blueprints ]
.sort( ( a, b ) => ( BLUEPRINT_ORDER[ a.title ] ?? 99 ) - ( BLUEPRINT_ORDER[ b.title ] ?? 99 ) )
.map( ( item ) => ( {
...item,
excerpt: excerptOverrides[ item.title ] || item.excerpt,
title: BLUEPRINT_DISPLAY_NAMES[ item.title ] || item.title,
} ) );
}

export function NewSiteOptions( {
blueprints,
isLoadingBlueprints,
Expand Down Expand Up @@ -286,7 +250,7 @@ export function NewSiteOptions( {
{ __( 'Could not load templates.' ) }
</div>
) : (
renameBlueprintsForDisplay( featuredBlueprints, __ ).map( ( item ) => (
curateBlueprintsForDisplay( featuredBlueprints, __ ).map( ( item ) => (
<BlueprintCard
key={ item.slug }
blueprint={ item }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getMshotUrl } from '@studio/common/lib/sync/mshots';
import { Icon, SearchControl as SearchControlWp, Spinner } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { useI18n } from '@wordpress/react-i18n';
Expand Down Expand Up @@ -418,10 +419,6 @@ function ListSites( {
);
}

function getMshotUrl( siteUrl: string ): string {
return `https://s0.wp.com/mshots/v1/${ encodeURIComponent( siteUrl ) }?w=600&h=400`;
}

function SiteThumbnail( {
site,
isDisabled,
Expand Down
28 changes: 5 additions & 23 deletions apps/studio/src/modules/sync/lib/environment-utils.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
import { SyncSite } from '@studio/common/types/sync';
import { __ } from '@wordpress/i18n';
import { z } from 'zod';

const EnvironmentSchema = z.enum( [ 'production', 'staging', 'development' ] );
export type EnvironmentType = z.infer< typeof EnvironmentSchema >;

export const getSiteEnvironment = ( site: SyncSite ): EnvironmentType => {
if ( site.isPressable ) {
const parsed = EnvironmentSchema.safeParse( site.environmentType );
return parsed.success ? parsed.data : 'production';
}
return site.isStaging ? 'staging' : 'production';
};

export const getEnvironmentLabel = ( type: EnvironmentType ): string => {
const labels: Record< EnvironmentType, string > = {
production: __( 'Production' ),
staging: __( 'Staging' ),
development: __( 'Development' ),
};
return labels[ type ] || type.charAt( 0 ).toUpperCase() + type.slice( 1 );
};
export {
getEnvironmentLabel,
getSiteEnvironment,
type EnvironmentType,
} from '@studio/common/lib/sync/environment-utils';
1 change: 1 addition & 0 deletions apps/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@tanstack/react-query": "^5.75.5",
"@tanstack/react-query-persist-client": "^5.96.2",
"@tanstack/react-router": "^1.120.14",
"@wordpress/a11y": "^4.47.0",
"@wordpress/api-fetch": "^7.47.0",
"@wordpress/components": "^34.0.0",
"@wordpress/core-data": "^7.47.0",
Expand Down
Loading
Loading