PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | includes/ai-copilot/settings.php +175 -79 0.9.21.1.10 View file →
@@ -1,13 +1,17 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — AI Copilot settings helpers.
3 + * OpenStation — AI Copilot settings + capability helpers.
4 4 *
5 - * Thin wrappers around `desktop_mode_get_os_settings()` scoped to the AI block.
6 - * All other copilot modules call these instead of reading user meta
7 - * directly so the key path is one place to change.
5 + * The Copilot no longer stores credentials of its own. WordPress 7.0 owns
6 + * provider credentials (Settings → Connectors) and model routing
7 + * (`wp_ai_client_prompt()`, which injects the configured key automatically).
8 + * These helpers therefore only carry the per-user "AI assistant" toggle
9 + * (`ai.enabled`) and expose the Core capability signals the shell uses to
10 + * decide whether to surface the assistant at all. Provider + model selection
11 + * is delegated entirely to the Core AI Client — nothing is persisted here.
8 12 *
9 - * @package WPDesktopMode
13 + * @package OpenStation
10 14 */
11 15
12 16 defined( 'ABSPATH' ) || exit;
13 17
@@ -13,116 +17,208 @@
13 17
14 18 /**
15 19 * Returns the AI settings block for a given user.
16 20 *
17 - * @since 0.14.0
21 + * `enabled` defaults to `false`: the assistant is opt-in, turned on from
22 + * OS Settings → Features (and only enable-able once a provider is configured).
23 + * Provider + model selection is left entirely to the Core AI Client.
18 24 *
19 25 * @param int $user_id
20 - * @return array{ enabled: bool, provider: string, apiKey: string }
26 + * @return array{ enabled: bool }
21 27 */
22 -function desktop_mode_ai_get_settings( $user_id ) {
23 - $os = desktop_mode_get_os_settings( (int) $user_id );
24 - $defaults = array(
25 - 'enabled' => false,
26 - 'provider' => 'openai',
27 - 'apiKey' => '',
28 - 'apiKeys' => array(),
28 +function openstation_ai_get_settings( $user_id ) {
29 + $os = openstation_get_os_settings( (int) $user_id );
30 + $ai = isset( $os['ai'] ) && is_array( $os['ai'] ) ? $os['ai'] : array();
31 + return array(
32 + 'enabled' => isset( $ai['enabled'] ) ? (bool) $ai['enabled'] : false,
29 33 );
30 - $ai = isset( $os['ai'] ) && is_array( $os['ai'] ) ? $os['ai'] : array();
31 - return array_merge( $defaults, $ai );
32 34 }
33 35
34 36 /**
35 - * Whether AI processing is active — checks platform settings first,
36 - * then falls back to the user's personal settings.
37 + * Whether the Core AI primitives the Copilot depends on are present.
37 38 *
38 - * Priority:
39 - * 1. Platform-wide settings (wp_options) — enabled by any admin.
40 - * 2. Per-user settings (user meta) — personal override.
39 + * The assistant is built on the AI Client (generation), the Connectors API
40 + * (credentials) and the Abilities API (tools, adopted in a follow-up). When
41 + * any of these is missing — e.g. WordPress < 7.0, or AI disabled site-wide
42 + * via `wp_supports_ai()` — the shell hides the assistant entirely.
41 43 *
42 - * @since 0.14.0
44 + * @return bool
45 + */
46 +function openstation_ai_is_available() {
47 + return function_exists( 'wp_ai_client_prompt' )
48 + && function_exists( 'wp_get_connectors' )
49 + && function_exists( 'wp_register_ability' )
50 + && function_exists( 'wp_supports_ai' )
51 + && wp_supports_ai();
52 +}
53 +
54 +/**
55 + * Whether a text-generation provider is configured and usable.
43 56 *
44 - * @param int $user_id
57 + * The baseline capability gate: a no-network, deterministic
58 + * `is_supported_for_text_generation()` probe against the AI Client registry
59 + * (which Core populates from the configured Connectors). This is what plain
60 + * text-generation features — the on-demand comment analysis ability, which
61 + * only needs structured text output — gate on. The agentic assistant needs more; see
62 + * {@see openstation_ai_assistant_provider_configured()}.
63 + *
64 + * Credentials are supplied by Core from the configured Connector; no API request
65 + * is made.
66 + *
45 67 * @return bool
46 68 */
47 -function desktop_mode_ai_is_enabled( $user_id ) {
48 - $user_id = (int) $user_id;
49 - $platform = desktop_mode_ai_get_platform_settings();
50 - $active = function_exists( 'desktop_mode_ai_get_active_provider_id' )
51 - ? desktop_mode_ai_get_active_provider_id( $user_id )
52 - : 'openai';
53 -
54 - // 1. Platform key — works for any user, including anonymous contexts.
55 - // Resolve via the per-provider map first, then fall back to the
56 - // legacy single `apiKey` field (which is treated as the openai key).
57 - if ( ! empty( $platform['enabled'] ) && '' !== desktop_mode_ai_resolve_key_for_provider( $platform, $active ) ) {
58 - return true;
69 +function openstation_ai_provider_configured() {
70 + if ( ! openstation_ai_is_available() ) {
71 + return false;
59 72 }
73 + return (bool) wp_ai_client_prompt( 'test' )->is_supported_for_text_generation();
74 +}
60 75
61 - // 2. Per-user override.
62 - $ai = desktop_mode_ai_get_settings( $user_id );
63 - if ( empty( $ai['enabled'] ) ) {
76 +/**
77 + * Whether a provider that can actually run the agentic assistant is configured.
78 + *
79 + * Follows the AI Client feature-detection guidance: rather than probing a bare
80 + * builder, we configure it the way the assistant actually calls the client —
81 + * with a function declaration — before the (no-network, deterministic)
82 + * `is_supported_for_text_generation()` check. `ModelRequirements::fromPromptData()`
83 + * turns the attached declaration into a `functionDeclarations` requirement, so
84 + * the check passes only when an available model supports text generation *and*
85 + * function calling — the two capabilities the tool loop depends on.
86 + *
87 + * Falls back to the plain text-generation gate when the SDK's FunctionDeclaration
88 + * class isn't present (e.g. older WordPress).
89 + *
90 + * @return bool
91 + */
92 +function openstation_ai_assistant_provider_configured() {
93 + if ( ! openstation_ai_is_available() ) {
64 94 return false;
65 95 }
66 - if ( '' === desktop_mode_ai_resolve_key_for_provider( $ai, $active ) ) {
67 - return false;
96 +
97 + $probe = openstation_ai_capability_probe_declaration();
98 + if ( ! $probe ) {
99 + return openstation_ai_provider_configured();
68 100 }
69 101
70 - return true;
102 + return (bool) wp_ai_client_prompt( 'test' )
103 + ->using_function_declarations( $probe )
104 + ->is_supported_for_text_generation();
71 105 }
72 106
73 107 /**
74 - * Resolve which key — from a settings block — applies to a given provider.
108 + * Builds a throwaway function declaration used only for capability detection.
75 109 *
76 - * Order: explicit `apiKeys[provider]` → legacy `apiKey` (only when the
77 - * provider is `openai`, since that's what the legacy field meant) → ''.
110 + * Mirrors the shape of the Copilot's real tool declarations so the AI Client's
111 + * support check additionally requires function-calling capability. Returns null
112 + * when the SDK class isn't available, letting the caller fall back to a plain
113 + * text-generation check.
78 114 *
79 - * @since 0.18.0
80 - *
81 - * @param array $settings `apiKeys` and `apiKey` carrying settings block.
82 - * @param string $provider_id Provider id to resolve for.
83 - * @return string Trimmed API key, or '' if none.
115 + * @return object|null A `FunctionDeclaration`, or null.
84 116 */
85 -function desktop_mode_ai_resolve_key_for_provider( array $settings, $provider_id ) {
86 - $provider_id = (string) $provider_id;
87 - $keys = isset( $settings['apiKeys'] ) && is_array( $settings['apiKeys'] ) ? $settings['apiKeys'] : array();
88 -
89 - if ( isset( $keys[ $provider_id ] ) && is_string( $keys[ $provider_id ] ) && '' !== $keys[ $provider_id ] ) {
90 - return (string) $keys[ $provider_id ];
117 +function openstation_ai_capability_probe_declaration() {
118 + $class = '\WordPress\AiClient\Tools\DTO\FunctionDeclaration';
119 + if ( ! class_exists( $class ) ) {
120 + return null;
91 121 }
92 -
93 - if ( 'openai' === $provider_id && isset( $settings['apiKey'] ) && is_string( $settings['apiKey'] ) && '' !== $settings['apiKey'] ) {
94 - return (string) $settings['apiKey'];
122 + try {
123 + return new $class(
124 + 'capability_probe',
125 + 'Feature-detection probe; never invoked.',
126 + null
127 + );
128 + } catch ( \Throwable $e ) {
129 + return null;
95 130 }
131 +}
96 132
97 - return '';
133 +/**
134 + * Whether the AI assistant is active for a given user.
135 + *
136 + * This is purely the per-user toggle (default off, opt-in). Availability of the Core
137 + * APIs and whether a provider key is set are separate, orthogonal checks
138 + * ({@see openstation_ai_is_available()} / {@see openstation_ai_provider_configured()})
139 + * so callers can distinguish "user turned it off" from "not set up yet".
140 + *
141 + * @param int $user_id
142 + * @return bool
143 + */
144 +function openstation_ai_is_enabled( $user_id ) {
145 + $ai = openstation_ai_get_settings( (int) $user_id );
146 + return ! empty( $ai['enabled'] );
98 147 }
99 148
100 149 /**
101 - * Returns the API key to use for a given user.
150 + * Builds the `aiAssistant` shell-config payload for a user.
102 151 *
103 - * Per-user key takes precedence (personal override); falls back to the
104 - * platform-wide key so anonymous contexts (cron, WP-CLI, anonymous
105 - * comments) always have a key available when the admin has configured one.
152 + * The client uses this to decide whether to surface the Cmd+K assistant and
153 + * its admin-bar icon at all (`available`) and the user's own on/off toggle
154 + * (`enabled`). Two capability gates are reported separately:
106 155 *
107 - * @since 0.14.0
156 + * - `assistantProviderConfigured` — a provider that supports text generation
157 + * *and* function calling (what the agentic assistant needs). Gates the Cmd+K
158 + * assistant, its admin-bar icon, and the "AI assistant" toggle in Features.
159 + * - `providerConfigured` — the baseline text-generation gate, for the calls
160 + * that need text output but not function calling (the Drafts writing
161 + * assistant, the on-demand comment analysis ability).
108 162 *
109 - * @param int $user_id
110 - * @return string API key, or empty string if none configured.
163 + * Provider + model selection is delegated to the Core AI Client, so there is no
164 + * per-user preference to carry here.
165 + *
166 + * @param int|null $user_id Defaults to the current user.
167 + * @return array{ available: bool, providerConfigured: bool, assistantProviderConfigured: bool, enabled: bool, connectorsUrl: string }
111 168 */
112 -function desktop_mode_ai_get_api_key( $user_id ) {
113 - $user_id = (int) $user_id;
114 - $active = function_exists( 'desktop_mode_ai_get_active_provider_id' )
115 - ? desktop_mode_ai_get_active_provider_id( $user_id )
116 - : 'openai';
169 +function openstation_ai_assistant_config( $user_id = null ) {
170 + $user_id = null === $user_id ? get_current_user_id() : (int) $user_id;
117 171
118 - // Per-user override takes precedence.
119 - $ai = desktop_mode_ai_get_settings( $user_id );
120 - $key = desktop_mode_ai_resolve_key_for_provider( $ai, $active );
121 - if ( '' !== $key ) {
122 - return $key;
172 + $connectors_url = admin_url( 'options-connectors.php' );
173 +
174 + if ( ! openstation_ai_is_available() ) {
175 + return array(
176 + 'available' => false,
177 + 'providerConfigured' => false,
178 + 'assistantProviderConfigured' => false,
179 + 'enabled' => false,
180 + 'connectorsUrl' => $connectors_url,
181 + );
123 182 }
124 183
125 - // Fall back to platform key.
126 - $platform = desktop_mode_ai_get_platform_settings();
127 - return desktop_mode_ai_resolve_key_for_provider( $platform, $active );
184 + $ai = openstation_ai_get_settings( $user_id );
185 +
186 + return array(
187 + 'available' => true,
188 + 'providerConfigured' => openstation_ai_provider_configured(),
189 + 'assistantProviderConfigured' => openstation_ai_assistant_provider_configured(),
190 + 'enabled' => (bool) $ai['enabled'],
191 + 'connectorsUrl' => $connectors_url,
192 + );
193 +}
194 +
195 +/**
196 + * REST: GET `desktop-mode/v1/ai/status`.
197 + *
198 + * Returns the current {@see openstation_ai_assistant_config()} so the shell
199 + * can re-check provider availability without a page reload — e.g. after the
200 + * user configures an AI provider in Settings → Connectors.
201 + */
202 +function openstation_register_ai_status_rest_route() {
203 + register_rest_route(
204 + 'desktop-mode/v1',
205 + '/ai/status',
206 + array(
207 + 'methods' => WP_REST_Server::READABLE,
208 + 'callback' => 'openstation_rest_ai_status',
209 + 'permission_callback' => static function () {
210 + return is_user_logged_in() && current_user_can( 'read' );
211 + },
212 + )
213 + );
214 +}
215 +add_action( 'rest_api_init', 'openstation_register_ai_status_rest_route' );
216 +
217 +/**
218 + * REST handler for the AI status probe.
219 + *
220 + * @return WP_REST_Response
221 + */
222 +function openstation_rest_ai_status() {
223 + return rest_ensure_response( openstation_ai_assistant_config() );
128 224 }