diff --git a/lib/experimental/connectors/default-connectors.php b/lib/experimental/connectors/default-connectors.php
index 0480bb766c323c..fe3c805fc32b37 100644
--- a/lib/experimental/connectors/default-connectors.php
+++ b/lib/experimental/connectors/default-connectors.php
@@ -427,8 +427,6 @@ function _gutenberg_get_connector_script_module_data( array $data ): array {
return $data;
}
- $registry = \WordPress\AiClient\AiClient::defaultRegistry();
-
// Build a slug-to-file map for plugin installation status.
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
@@ -448,11 +446,6 @@ function _gutenberg_get_connector_script_module_data( array $data ): array {
$auth_out['settingName'] = $auth['setting_name'] ?? '';
$auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null;
$auth_out['keySource'] = _gutenberg_get_api_key_source( $connector_id, $auth['setting_name'] ?? '' );
- try {
- $auth_out['isConnected'] = $registry->hasProvider( $connector_id ) && $registry->isProviderConfigured( $connector_id );
- } catch ( Exception $e ) {
- $auth_out['isConnected'] = false;
- }
}
$connector_out = array(
@@ -485,3 +478,59 @@ function _gutenberg_get_connector_script_module_data( array $data ): array {
}
remove_filter( 'script_module_data_options-connectors-wp-admin', '_wp_connectors_get_connector_script_module_data' );
add_filter( 'script_module_data_options-connectors-wp-admin', '_gutenberg_get_connector_script_module_data' );
+
+/**
+ * Streams connector connection-status checks to the browser after the page
+ * has been flushed, so the initial render is not blocked by slow HTTP
+ * validation requests.
+ *
+ * Each resolved status is delivered as an inline ',
+ wp_json_encode( $connector_id ),
+ $is_connected ? 'true' : 'false',
+ wp_json_encode( $connector_id ),
+ $is_connected ? 'true' : 'false'
+ );
+
+ // Flush each result so the browser processes it immediately.
+ flush();
+ }
+}
+add_action( 'admin_footer-settings_page_options-connectors-wp-admin', '_gutenberg_stream_connector_statuses' );
diff --git a/routes/connectors-home/ai-plugin-callout.tsx b/routes/connectors-home/ai-plugin-callout.tsx
index e008cd5ec4ed1f..62e0c8326e60d2 100644
--- a/routes/connectors-home/ai-plugin-callout.tsx
+++ b/routes/connectors-home/ai-plugin-callout.tsx
@@ -34,13 +34,13 @@ export function AiPluginCallout() {
const [ isBusy, setIsBusy ] = useState( false );
const [ justActivated, setJustActivated ] = useState( false );
- // Server-side initial state — true if any provider was already connected at page load.
+ // Server-side initial state — true if any provider has a configured key at page load.
const initialHasConnectedProvider = useRef(
connectorDataValues.some(
( c ) =>
c.type === 'ai_provider' &&
c.authentication.method === 'api_key' &&
- c.authentication.isConnected
+ c.authentication.keySource !== 'none'
)
).current;
diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx
index d12be5228ca6f7..8668576706f7fd 100644
--- a/routes/connectors-home/default-connectors.tsx
+++ b/routes/connectors-home/default-connectors.tsx
@@ -29,7 +29,6 @@ type ConnectorAuthentication =
settingName: string;
credentialsUrl: string | null;
keySource?: ApiKeySource;
- isConnected?: boolean;
}
| { method: 'none' };
@@ -102,6 +101,7 @@ const ConnectedBadge = () => (
const UnavailableActionBadge = () => { __( 'Not available' ) };
interface ApiKeyConnectorConfig {
+ connectorId: string;
pluginSlug?: string;
settingName: string;
helpUrl?: string;
@@ -109,12 +109,12 @@ interface ApiKeyConnectorConfig {
isInstalled?: boolean;
isActivated?: boolean;
keySource?: ApiKeySource;
- initialIsConnected?: boolean;
}
function ApiKeyConnector( {
label,
description,
+ connectorId,
pluginSlug,
settingName,
helpUrl,
@@ -122,7 +122,6 @@ function ApiKeyConnector( {
isInstalled,
isActivated,
keySource: initialKeySource,
- initialIsConnected,
}: ConnectorRenderProps & ApiKeyConnectorConfig ) {
let helpLabel: string | undefined;
try {
@@ -141,6 +140,7 @@ function ApiKeyConnector( {
setIsExpanded,
isBusy,
isConnected,
+ isCheckingConnection,
currentApiKey,
keySource,
handleButtonClick,
@@ -148,12 +148,12 @@ function ApiKeyConnector( {
saveApiKey,
removeApiKey,
} = useConnectorPlugin( {
+ connectorId,
pluginSlug,
settingName,
isInstalled,
isActivated,
keySource: initialKeySource,
- initialIsConnected,
} );
const isExternallyConfigured =
keySource === 'env' || keySource === 'constant';
@@ -187,7 +187,11 @@ function ApiKeyConnector( {
: 'compact'
}
onClick={ handleButtonClick }
- disabled={ pluginStatus === 'checking' || isBusy }
+ disabled={
+ pluginStatus === 'checking' ||
+ isCheckingConnection ||
+ isBusy
+ }
isBusy={ isBusy }
aria-expanded={ isExpanded }
>
@@ -247,6 +251,7 @@ export function registerDefaultConnectors() {
render: ( props ) => (
),
} );
diff --git a/routes/connectors-home/use-connector-plugin.ts b/routes/connectors-home/use-connector-plugin.ts
index 9287f4abc5234c..34d2d4f51ad708 100644
--- a/routes/connectors-home/use-connector-plugin.ts
+++ b/routes/connectors-home/use-connector-plugin.ts
@@ -3,7 +3,7 @@
*/
import { store as coreStore } from '@wordpress/core-data';
import { useSelect, useDispatch } from '@wordpress/data';
-import { useState } from '@wordpress/element';
+import { useState, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import type { __experimentalApiKeySource as ApiKeySource } from '@wordpress/connectors';
@@ -11,12 +11,12 @@ import type { __experimentalApiKeySource as ApiKeySource } from '@wordpress/conn
export type PluginStatus = 'checking' | 'not-installed' | 'inactive' | 'active';
interface UseConnectorPluginOptions {
+ connectorId: string;
pluginSlug?: string;
settingName: string;
isInstalled?: boolean;
isActivated?: boolean;
keySource?: ApiKeySource;
- initialIsConnected?: boolean;
}
interface UseConnectorPluginReturn {
@@ -27,6 +27,7 @@ interface UseConnectorPluginReturn {
setIsExpanded: ( expanded: boolean ) => void;
isBusy: boolean;
isConnected: boolean;
+ isCheckingConnection: boolean;
currentApiKey: string;
keySource: ApiKeySource;
handleButtonClick: () => void;
@@ -36,17 +37,53 @@ interface UseConnectorPluginReturn {
}
export function useConnectorPlugin( {
+ connectorId,
pluginSlug,
settingName,
isInstalled,
isActivated,
keySource = 'none',
- initialIsConnected = false,
}: UseConnectorPluginOptions ): UseConnectorPluginReturn {
const [ isExpanded, setIsExpanded ] = useState( false );
const [ isBusy, setIsBusy ] = useState( false );
- const [ connectedState, setConnectedState ] =
- useState( initialIsConnected );
+ const [ connectedState, setConnectedState ] = useState< boolean | null >(
+ () => {
+ // Check if streaming already delivered the result before React mounted.
+ const streamed = (
+ window as unknown as {
+ __connectorStatuses?: Record< string, boolean >;
+ }
+ ).__connectorStatuses?.[ connectorId ];
+ return streamed !== undefined ? streamed : null;
+ }
+ );
+
+ // Listen for streamed connector-status events from the server.
+ useEffect( () => {
+ if ( connectedState !== null ) {
+ return;
+ }
+ const handler = ( e: Event ) => {
+ const detail = ( e as CustomEvent ).detail;
+ if ( detail.id === connectorId ) {
+ setConnectedState( detail.connected );
+ }
+ };
+ document.addEventListener( 'connector-status', handler );
+
+ // Check again in case the event fired between initial state and effect registration.
+ const streamed = (
+ window as unknown as {
+ __connectorStatuses?: Record< string, boolean >;
+ }
+ ).__connectorStatuses?.[ connectorId ];
+ if ( streamed !== undefined ) {
+ setConnectedState( streamed );
+ }
+
+ return () =>
+ document.removeEventListener( 'connector-status', handler );
+ }, [ connectorId, connectedState ] );
// Local override for immediate UI feedback after install/activate.
const [ pluginStatusOverride, setPluginStatusOverride ] =
useState< PluginStatus | null >( null );
@@ -144,8 +181,11 @@ export function useConnectorPlugin( {
// Use canManagePlugins (from plugin entity resolution) for activation capability.
const canActivatePlugins = canManagePlugins;
+ const isCheckingConnection =
+ pluginStatus === 'active' && connectedState === null;
+
const isConnected =
- ( pluginStatus === 'active' && connectedState ) ||
+ ( pluginStatus === 'active' && connectedState === true ) ||
// After install/activate, if settings re-fetch reveals an existing key,
// update connected state (mirrors what the server would report on page load).
( pluginStatusOverride === 'active' && !! currentApiKey );
@@ -226,6 +266,9 @@ export function useConnectorPlugin( {
if ( isConnected ) {
return __( 'Edit' );
}
+ if ( isCheckingConnection ) {
+ return __( 'Checking…' );
+ }
switch ( pluginStatus ) {
case 'checking':
return __( 'Checking…' );
@@ -298,6 +341,7 @@ export function useConnectorPlugin( {
setIsExpanded,
isBusy,
isConnected,
+ isCheckingConnection,
currentApiKey,
keySource,
handleButtonClick,