PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.6
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
desktop-mode / apps / my-wordpress / parts / agents.php

agents.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.6, at apps/my-wordpress/parts/agents.php

322 lines 11.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * My WordPress — the Agents section: payload and actions.
4 *
5 * Part of the `my-wordpress` app: required by `my-wordpress.os.php`,
6 * same namespace, plain `.php` on purpose — only `*.os.php` files are
7 * app entries to the framework loader. WP Explorer's Agents surface
8 * on the app: the section config computed by the same helpers, the
9 * cast through the same REST shaper, the catalogues settled with the
10 * data, and the four mutations (draft / create / update / delete)
11 * behind the same capability gates as the `/desktop-mode/v1/agents`
12 * routes they mirror.
13 *
14 * @package OpenStation
15 */
16
17 namespace OpenStation\Apps\MyWordPress;
18
19 use OpenStation\App\Os;
20 use OpenStation\App\State;
21
22 // Direct access, unless a standalone host is booting on bare PHP.
23 if ( ! defined( 'ABSPATH' ) ) {
24 defined( 'OPENSTATION_STANDALONE' ) || exit;
25 }
26
27 /**
28 * Whether the Agents framework is on. False when the module is absent.
29 *
30 * @return bool
31 */
32 function agents_enabled() {
33 return function_exists( 'openstation_agents_enabled' ) && openstation_agents_enabled();
34 }
35
36 /**
37 * Whether the acting user may create / edit / delete agents.
38 *
39 * @return bool
40 */
41 function agents_can_manage() {
42 return function_exists( 'openstation_agents_user_can_manage' ) && openstation_agents_user_can_manage();
43 }
44
45 /**
46 * The canonical shape of every agent on the site — the same rows the
47 * `/desktop-mode/v1/agents` list route serves, through the same shaper.
48 *
49 * @return array<int,array<string,mixed>>
50 */
51 function agents_list() {
52 if ( ! function_exists( 'openstation_agent_get_agents' ) || ! function_exists( 'openstation_agents_rest_shape_user' ) ) {
53 return array();
54 }
55 $out = array();
56 foreach ( openstation_agent_get_agents() as $user ) {
57 $shape = openstation_agents_rest_shape_user( $user );
58 if ( $shape ) {
59 // For "Open profile"'s iframe fallback — computed here so
60 // the client never assembles an admin URL by hand.
61 $shape['profileUrl'] = esc_url_raw( admin_url( 'user-edit.php?user_id=' . (int) $shape['id'] ) );
62 $out[] = $shape;
63 }
64 }
65 return $out;
66 }
67
68 /**
69 * Every role's translated label, keyed by slug — what the cast badges
70 * read. Shipped for READERS too: a card resolving its role label out
71 * of a manage-only catalogue would degrade to the raw slug in English
72 * for everyone else, which is exactly what the preview cast's
73 * `roleLabel` exists to avoid.
74 *
75 * @return array<string,string>
76 */
77 function agents_role_labels() {
78 $labels = array();
79 foreach ( wp_roles()->get_names() as $slug => $name ) {
80 $labels[ $slug ] = translate_user_role( $name );
81 }
82 return $labels;
83 }
84
85 /**
86 * The full Agents payload the client view paints: WP Explorer's
87 * section config (same keys, computed by the same helpers), the cast,
88 * and — because a server view recomputes data on every interaction —
89 * the catalogues the original fetched lazily over REST, settled here
90 * once per render instead.
91 *
92 * @param Os $os Host handle.
93 * @param State $state State.
94 * @return array<string,mixed>
95 */
96 function agents_payload( Os $os, State $state ) {
97 $enabled = agents_enabled();
98 $can_manage = agents_can_manage();
99 $ai_available = function_exists( 'openstation_ai_is_available' ) && openstation_ai_is_available();
100 // The live half of WP Explorer's `/ai/status` probe, answered
101 // in-process: a text-generation provider is configured and usable.
102 $ai_ready = $ai_available && (
103 ( function_exists( 'openstation_ai_assistant_provider_configured' ) && openstation_ai_assistant_provider_configured() )
104 || ( function_exists( 'openstation_ai_provider_configured' ) && openstation_ai_provider_configured() )
105 );
106
107 $payload = array(
108 'enabled' => $enabled,
109 'canEnable' => current_user_can( 'manage_options' ),
110 'canManage' => $can_manage,
111 'canInvoke' => function_exists( 'openstation_agents_user_can_invoke' ) && openstation_agents_user_can_invoke(),
112 'aiAvailable' => $ai_available,
113 'aiReady' => $ai_ready,
114 'connectorsUrl' => esc_url_raw( admin_url( 'options-connectors.php' ) ),
115 'runWindowId' => 'desktop-mode-agent-run',
116 // For the drop targets' dispatch engine (`agents-dispatch.ts`),
117 // which posts to `/agents/:id/invoke` exactly as WP Explorer's.
118 'restRoot' => esc_url_raw( rest_url() ),
119 'restNonce' => wp_create_nonce( 'wp_rest' ),
120 'list' => $enabled ? agents_list() : array(),
121 'roleLabels' => agents_role_labels(),
122 'abilities' => $enabled && function_exists( 'openstation_agents_abilities_catalogue' )
123 ? array_values( openstation_agents_abilities_catalogue() )
124 : array(),
125 'triggerKinds' => $enabled && function_exists( 'openstation_agent_trigger_kinds' )
126 ? array_values( openstation_agent_trigger_kinds() )
127 : array(),
128 'hooks' => $enabled && function_exists( 'openstation_agent_hooks_catalogue' )
129 ? array_values( openstation_agent_hooks_catalogue() )
130 : array(),
131 // Assignable roles stay manage-only, like the `/agents/roles`
132 // route: this is the PICKER's list, not the badges'.
133 'roles' => $enabled && $can_manage && function_exists( 'openstation_agent_allowed_roles' )
134 ? array_values(
135 array_map(
136 static function ( $slug ) {
137 $names = wp_roles()->get_names();
138 return array(
139 'slug' => $slug,
140 'label' => isset( $names[ $slug ] ) ? translate_user_role( $names[ $slug ] ) : $slug,
141 );
142 },
143 array_values( openstation_agent_allowed_roles() )
144 )
145 )
146 : null,
147 );
148
149 // The cast this site WOULD be seeded with — only while off, which
150 // is the only state that draws it.
151 if ( ! $enabled && function_exists( 'openstation_agents_preview_cast' ) ) {
152 $payload['preview'] = openstation_agents_preview_cast();
153 }
154
155 return $payload;
156 }
157
158 /**
159 * The wizard's draft, read out of the state slot as a clean array.
160 *
161 * @param State $state State.
162 * @return array<string,mixed>
163 */
164 function agents_cast_of( State $state ) {
165 $cast = $state->get( 'cast' );
166 return is_array( $cast ) ? $cast : array();
167 }
168
169 /**
170 * `agent-draft`: one AI generate call with a strict answer schema,
171 * filtered against the site's catalogues on the server. Creates
172 * nothing; a failure keeps the user on Describe with the reason under
173 * the brief.
174 *
175 * @param State $state State.
176 * @return void
177 */
178 function agent_draft_action( State $state ) {
179 if ( ! agents_enabled() || ! agents_can_manage() || ! function_exists( 'openstation_agent_draft' ) ) {
180 return;
181 }
182 $cast = agents_cast_of( $state );
183 // The client raised `drafting` when it dispatched; every way
184 // out of this action lowers it in the state it returns.
185 $cast['drafting'] = false;
186 $state->set( 'cast', $cast );
187 $brief = trim( (string) ( $cast['brief'] ?? '' ) );
188 if ( '' === $brief ) {
189 $state->set( 'briefError', __( 'Describe the agent first. A sentence is enough.', 'desktop-mode' ) );
190 return;
191 }
192 $draft = openstation_agent_draft( $brief, get_current_user_id() );
193 if ( is_wp_error( $draft ) ) {
194 $state->set( 'briefError', $draft->get_error_message() );
195 return;
196 }
197 // Fold the draft into the cast: an empty answer for a field
198 // keeps whatever was there. The role and the abilities come
199 // back already filtered against the catalogues ('' / dropped
200 // when the model's pick is not one the site allows).
201 foreach ( array( 'name', 'description', 'instructions' ) as $field ) {
202 if ( '' !== trim( (string) ( $draft[ $field ] ?? '' ) ) ) {
203 $cast[ $field ] = trim( (string) $draft[ $field ] );
204 }
205 }
206 if ( '' !== trim( (string) ( $draft['vibes'] ?? '' ) ) ) {
207 $cast['vibes'] = mb_substr( trim( (string) $draft['vibes'] ), 0, 120 );
208 }
209 if ( '' !== (string) ( $draft['role'] ?? '' ) ) {
210 $cast['role'] = (string) $draft['role'];
211 }
212 if ( isset( $draft['abilities'] ) && is_array( $draft['abilities'] ) ) {
213 $cast['abilities'] = array_values( array_map( 'strval', $draft['abilities'] ) );
214 }
215 // Filled in, Meet is a review.
216 $state->set( 'cast', $cast )->set( 'wstep', 1 )->set( 'briefError', '' );
217 }
218
219 /**
220 * `agent-create`: one request, the whole definition — abilities and
221 * triggers included, so an agent is never briefly on the site in a
222 * half-configured state.
223 *
224 * @param State $state State.
225 * @param Os $os Host handle.
226 * @return void
227 */
228 function agent_create_action( State $state, Os $os ) {
229 if ( ! agents_enabled() || ! agents_can_manage() || ! function_exists( 'openstation_agent_create' ) ) {
230 return;
231 }
232 $cast = agents_cast_of( $state );
233 if ( '' === trim( (string) ( $cast['name'] ?? '' ) ) ) {
234 $state->set( 'agentNotice', __( 'Agent name is required.', 'desktop-mode' ) )->set( 'wstep', 1 );
235 return;
236 }
237 $user = openstation_agent_create(
238 array(
239 'name' => trim( (string) ( $cast['name'] ?? '' ) ),
240 'role' => (string) ( $cast['role'] ?? '' ),
241 'description' => trim( (string) ( $cast['description'] ?? '' ) ),
242 'instructions' => (string) ( $cast['instructions'] ?? '' ),
243 'abilities' => (array) ( $cast['abilities'] ?? array() ),
244 'triggers' => (array) ( $cast['triggers'] ?? array() ),
245 'vibes' => trim( (string) ( $cast['vibes'] ?? '' ) ),
246 'face' => $cast['face'] ?? null,
247 'faceSeed' => (int) ( $cast['faceSeed'] ?? 0 ),
248 )
249 );
250 if ( is_wp_error( $user ) ) {
251 $state->set( 'agentNotice', $user->get_error_message() );
252 return;
253 }
254 $state->set( 'casting', false )->set( 'wstep', 0 )->reset( 'cast' )
255 ->set( 'item', (int) $user->ID )->set( 'pane', 'define' )
256 ->set( 'agentNotice', '' )->set( 'briefError', '' );
257 $os->announce( 'user', 'created', (int) $user->ID );
258 }
259
260 /**
261 * `agent-update`: any subset of the definition fields, like the
262 * PATCH route.
263 *
264 * @param State $state State.
265 * @param Os $os Host handle.
266 * @param array<string,mixed> $args Trigger args.
267 * @return void
268 */
269 function agent_update_action( State $state, Os $os, array $args ) {
270 if ( ! agents_enabled() || ! agents_can_manage() || ! function_exists( 'openstation_agent_update' ) ) {
271 return;
272 }
273 $id = (int) ( $args['id'] ?? 0 );
274 $fields = array();
275 foreach ( array( 'name', 'role', 'description', 'instructions', 'abilities', 'triggers', 'vibes', 'face', 'faceSeed' ) as $field ) {
276 if ( array_key_exists( $field, $args ) ) {
277 $fields[ $field ] = $args[ $field ];
278 }
279 }
280 $updated = openstation_agent_update( $id, $fields );
281 if ( is_wp_error( $updated ) ) {
282 $state->set( 'agentNotice', $updated->get_error_message() );
283 return;
284 }
285 // The face backfill is a courtesy write and stays silent;
286 // everything else confirms in the words the original used.
287 if ( array_key_exists( 'abilities', $fields ) ) {
288 $state->set( 'agentNotice', __( 'Abilities saved.', 'desktop-mode' ) );
289 } elseif ( array_key_exists( 'triggers', $fields ) ) {
290 $state->set( 'agentNotice', __( 'Triggers saved.', 'desktop-mode' ) );
291 } elseif ( ! array_key_exists( 'face', $fields ) ) {
292 $state->set( 'agentNotice', __( 'Agent saved.', 'desktop-mode' ) );
293 }
294 $os->announce( 'user', 'updated', $id );
295 }
296
297 /**
298 * `agent-delete`: delete the agent user (agents only — a plain user
299 * is refused by the store).
300 *
301 * @param State $state State.
302 * @param Os $os Host handle.
303 * @param array<string,mixed> $args Trigger args.
304 * @return void
305 */
306 function agent_delete_action( State $state, Os $os, array $args ) {
307 if ( ! agents_enabled() || ! agents_can_manage() || ! function_exists( 'openstation_agent_delete' ) ) {
308 return;
309 }
310 $id = (int) ( $args['id'] ?? 0 );
311 $result = openstation_agent_delete( $id );
312 if ( is_wp_error( $result ) ) {
313 $state->set( 'agentNotice', $result->get_error_message() );
314 return;
315 }
316 if ( $id === (int) $state->get( 'item' ) ) {
317 $state->set( 'item', 0 );
318 }
319 $state->set( 'agentNotice', '' );
320 $os->announce( 'user', 'deleted', $id );
321 }
322