Skip to content

OS Settings tabs, AI Copilot extensibility, UI component kit expansion#15

Merged
AllTerrainDeveloper merged 4 commits intotrunkfrom
feature/new_hooks_and_components
Apr 24, 2026
Merged

OS Settings tabs, AI Copilot extensibility, UI component kit expansion#15
AllTerrainDeveloper merged 4 commits intotrunkfrom
feature/new_hooks_and_components

Conversation

@AllTerrainDeveloper
Copy link
Copy Markdown
Collaborator

@AllTerrainDeveloper AllTerrainDeveloper commented Apr 24, 2026

Summary

Opens three new extension surfaces for third-party plugins:

  1. OS Settings tabs — plugins can add their own tab to the OS Settings window via wp.desktop.registerSettingsTab() (+ PHP registration APIs for live activation / deactivation).
  2. AI Copilot programmatic accesswp.desktop.ai.ask() posts to the same /ai/search endpoint the built-in overlay uses; opt-in flags let registered slash-commands become AI-invokable tools; server-side plugins can register their own PHP-dispatched AI tools.
  3. UI component kit<wpd-code>, <wpd-steps> / <wpd-step>, plus an opt-in stack attribute on <wpd-section>.

Plus a load of PHP filters / actions around the AI endpoint, a wp.desktop.ready() bootstrap alias to close a long-standing late-load race, and a bundle of documentation + tests.

Nothing in this PR is a breaking change. Every new API is additive. Every new hook is marked Experimental, since 0.17.0 — we reserve the right to adjust signatures before 0.17.0 cuts.


What's new and what can be achieved with the new hooks

Demo.24.April.mov

1. OS Settings tabs (Experimental)

Third-party plugins can now own a tab in the OS Settings window.

JS entry point:

wp.desktop.ready( () => {
    wp.desktop.registerSettingsTab( {
        id:         'my-plugin',
        label:      'My Plugin',
        capability: 'manage_options',   // optional — admin-only when set to this
        order:      50,                  // optional — default 100 (after built-ins)
        owner:      'my-plugin-settings', // optional — live-unregister on deactivation
        render( body, ctx ) {
            // ctx = { isAdmin, getOsSettings(), subscribeOsSettings(cb) }
            body.innerHTML = `<wpd-section heading="My Plugin" stack>…</wpd-section>`;
            const { apiKey } = ctx.getOsSettings().ai;
            ctx.subscribeOsSettings( ( next ) => { /* repaint */ } );
        },
    } );
} );

Built-in tab orders: appearance=10, ai=20, extended=30, help=40. Third-party default 100.

PHP registration (for live refresh):

// Minimum-ceremony opt-in:
wp_desktop_register_settings_tab_script( 'my-plugin-settings' );

// Optional — declare metadata for live-unregistration-on-deactivation:
wp_register_desktop_settings_tab( [
    'id'         => 'my-plugin',
    'label'      => __( 'My Plugin' ),
    'capability' => 'manage_options',
    'order'      => 50,
    'script'     => 'my-plugin-settings',
] );

Third-party tabs now see the same OS Settings state the built-in AI tab sees via ctx.getOsSettings() ({ wallpaper, accent, dockSize, ai: { enabled, provider, apiKey } }) + ctx.subscribeOsSettings(cb) for live updates — removes the localStorage-poke workaround from plugin code.

2. AI Copilot programmatic access (Experimental)

wp.desktop.ai.ask( query, opts? ) — the new client-side entry point

// Simple question-answer — same endpoint the overlay uses.
const res = await wp.desktop.ai.ask( 'where do I manage categories?' );
// res = { answer_type: 'navigation', message, admin_links: [...], request_id }

AskOptions:

Field Notes
signal AbortSignal — propagates to fetch.
resumeTool / startOffset Pagination through exhausted search runs.
tools false | 'aiCallable' | string[] | (slug) => boolean — opt in to command tool-calling (see below).
systemPrompt string | { mode: 'append' | 'replace', text: string }.
followUp boolean — when true, ask the AI to compose a natural-language reply after a command runs (see below).
commandContext Override the CommandContext handed to invoked commands.

Commands as AI tools

DesktopCommand now has an opt-in aiCallable: true flag. When set, ask() callers with tools: 'aiCallable' harvest the command into the OpenAI tool list. If the model picks it, the server returns { answer_type: 'tool_call', tool: { slug, args } } and the shell invokes run() locally.

wp.desktop.registerCommand( {
    slug:       'turn_lights',
    label:      'Turn lights',
    description:'Toggle smart lights.',
    hint:       'ON or OFF',
    aiCallable: true,            // ← required for the AI to see it
    run: ( args ) => `Lights ${ args.toUpperCase() }.`,
} );

const res = await wp.desktop.ai.ask( 'hey turn on the lights', { tools: 'aiCallable' } );
// res.answer_type === 'tool_call'
// res.toolCall === { slug: 'turn_lights', args: 'ON', result: 'Lights ON.' }

Opt-in is deliberate. AI tool-calling is a paraphrasing channel — auto-exposing every registered command (including destructive ones) would turn a typo into a catastrophe.

followUp: true — agentic mode (natural-language replies)

Default (one-shot) mode sets res.message to whatever the plugin's run() returned — typically a short status string. Opt into agentic mode to get an AI-composed reply in the voice of the system prompt:

const res = await wp.desktop.ai.ask( 'turn on the office light', {
    tools:    'aiCallable',
    followUp: true,
} );

// Before (one-shot):
// res.message === 'Light is ON.'

// After (followUp):
// res.message === 'Done — your office light is on now. Anything else?'
// res.toolCall.result === 'Light is ON.'   // raw return preserved
  • Cost: one extra OpenAI round-trip per command invocation.
  • Graceful: if the follow-up leg fails (network / API), ask() does not throw — res.message falls back to the one-shot string.
  • AbortSignal works across both legs.

Use it for voice / chat / assistant surfaces; skip for one-tap "execute" UI buttons.

wp_register_desktop_ai_tool() — PHP-dispatched AI tools

For integrations whose logic is inherently server-side (WooCommerce lookups, site-health checks, WP-CLI wrappers):

wp_register_desktop_ai_tool( [
    'name'             => 'list_recent_orders',
    'description'      => 'Return the N most recent WooCommerce orders.',
    'parameters'       => [
        'type'       => 'object',
        'properties' => [
            'limit'  => [ 'type' => 'integer' ],
            'status' => [ 'type' => 'string', 'enum' => [ 'processing', 'completed' ] ],
        ],
        'required'   => [ 'limit' ],
    ],
    'handler'          => 'my_plugin_list_orders',  // function( array $args, int $user_id ): array|WP_Error
    'capability'       => 'manage_woocommerce',
    'progress_message' => 'Checking recent orders…',
] );

capability is enforced before the tool is visible to the model — unauthorised users never see it exists. Thrown exceptions and WP_Error returns are caught into a structured { error, tool, message } payload so the agent can continue with other tools rather than hard-failing.

New PHP filters / actions

All Experimental, since 0.17.0. Full documentation in docs/hooks-reference.md.

Filters (transform the request / response):

  • wp_desktop_ai_request — rewrite the whole request bundle before the agent loop starts.
  • wp_desktop_ai_system_prompt_appendix — stacking append to the built-in instructions (the 95% extension point).
  • wp_desktop_ai_system_prompt — final transform pass.
  • wp_desktop_ai_system_prompt_replace_capability — cap required for mode: 'replace'. Default manage_options.
  • wp_desktop_ai_tools — transform the whole tool list.
  • wp_desktop_ai_command_tools — transform only the command-derived subset.
  • wp_desktop_ai_command_allowed — per-slug gate, return false to drop.
  • wp_desktop_ai_tool_result — mutate a tool's result before it goes back to the model.
  • wp_desktop_ai_answer — final transform hook on every success path.
  • wp_desktop_ai_followup_outcome_max_chars — bound the outcome payload size (default 4000).

Actions (observability):

  • wp_desktop_ai_search_started — first anchor of the trio.
  • wp_desktop_ai_tool_called — fires for every tool invocation.
  • wp_desktop_ai_search_completed — fires on every success path.
  • wp_desktop_ai_search_error — catches WP_Error paths + tool-handler throws.
  • wp_desktop_ai_tool_registered — fires after wp_register_desktop_ai_tool() succeeds.

Every hook carries a shared request_id UUID for trace correlation.

3. UI component kit additions (Experimental)

<wpd-section stack> — opt-in gap between slotted children

<!-- Before: no gap, cramped by default -->
<wpd-section heading="Settings">
    <wpd-text-field label="Name"></wpd-text-field>
    <wpd-button>Save</wpd-button>
</wpd-section>

<!-- After: add `stack` -->
<wpd-section heading="Settings" stack>
    <wpd-text-field label="Name"></wpd-text-field>
    <wpd-button>Save</wpd-button>
</wpd-section>

Gap is --wpd-section-gap (default 12px). Backwards compatible — existing sections (no attribute) render exactly as before.

<wpd-code> — inline / block code badge

Renders via a real <code> element with no global keypress listeners — safe for URLs, flag names, or anything else that would steal keystrokes if stamped out as <wpd-key>.

Open <wpd-code>chrome://flags</wpd-code> and enable
<wpd-code>experimental-web-platform-features</wpd-code>.

<wpd-code block>wp_register_desktop_settings_tab( [...] );</wpd-code>

<wpd-steps> + <wpd-step> — auto-numbered onboarding

Numbers come from a CSS counter — inserting or removing a step renumbers the rest automatically. done on a step renders ✓ instead of the number.

<wpd-steps>
    <wpd-step title="Install">Click Install in the plugin directory.</wpd-step>
    <wpd-step title="Activate">Click Activate on the Plugins page.</wpd-step>
    <wpd-step title="Connect your API key" done>Already set up.</wpd-step>
</wpd-steps>

4. wp.desktop.ready( fn ) — bootstrap alias

Short alias of whenReady mirroring jQuery( fn ). Runs the callback synchronously (microtask) if wp-desktop.init has already fired, otherwise queues via addAction. Safe for scripts injected mid-session by any server-sync module (widgets, wallpapers, commands, settings tabs) — closes a class of late-load races where addAction('wp-desktop.init', …) silently misses the already-fired event.

wp.desktop.ready( () => {
    wp.desktop.registerSettingsTab( { ... } );
} );

Every doc and recipe example that previously used wp.hooks.addAction('wp-desktop.init', …) has been migrated to wp.desktop.ready() with an explicit "Why not addAction directly?" callout in the JS reference.


Implementation notes

Security posture

  • aiCallable is opt-in per command — destructive commands don't auto-expose to natural-language invocation.
  • Command tool execution is client-side only — the server returns a slug+args intent; the model can't reach through to server code via this path.
  • PHP-registered AI tools are capability-gated before they're visible to the model — unauthorised users don't even see they exist.
  • systemPrompt: { mode: 'replace' } requires manage_options by default (filterable); non-admin requests silently downgrade to append.
  • Follow-up outcome bounded to 4000 chars by default (filterable) so a buggy plugin returning a 5 MB blob can't balloon token usage.
  • Tool-handler exceptions caught + scrubbed — raw exception messages never surface to callers or the model.
  • Follow-up truncation uses mb_substr (with byte-level fallback) so multibyte payloads don't produce invalid JSON.

Live refresh

OS Settings tabs follow the same server-sync pattern as commands / widgets / wallpapers: serverSettingsTabScripts + serverSettingsTabs in the shell payload, src/settings/server-sync.ts injects scripts on activation + unregisters tabs on deactivation via either the JS owner tag or the PHP-declared id↔handle snapshot.

Extracted helpers

  • wpdm_ai_compose_instructions() — unifies the three-layer system-prompt composition across primary + follow-up legs.
  • src/ai/ask.ts split into postToSearch, dispatchToolCall, composeFollowUp — top-level ask() is now ~70 lines.

Files changed

PHP:

  • includes/ai-copilot/tools-registry.php (new) — wp_register_desktop_ai_tool() + registry.
  • includes/ai-copilot/search.php — extensibility surface, command tools, follow-up leg, prompt composition helper.
  • includes/ai-copilot/bootstrap.php — load the new registry file.
  • includes/settings-tabs.php (new) — wp_desktop_register_settings_tab_script() + wp_register_desktop_settings_tab().
  • wp-desktop-mode.php — load settings-tabs include.
  • includes/render.php, includes/helpers.php — payload wiring.

TypeScript:

  • src/ai/ask.ts (new)
  • src/settings/registry.ts (new), src/settings/server-sync.ts (new)
  • src/settings/index.ts — tabs interleaving + ctx.getOsSettings / subscribeOsSettings.
  • src/ai-assistant.tsask late-binding via attachAsk.
  • src/commands.tsaiCallable flag, listAiCallableCommands(), notify JSDoc rewrite.
  • src/types.ts — new server-entry types.
  • src/ui/components/wpd-section/*stack attribute.
  • src/ui/components/wpd-code/* (new)
  • src/ui/components/wpd-steps/* (new)
  • src/desktop.ts — public API surface + ready alias + sync wiring.
  • src/public-api.ts, src/ui/components/index.ts — exports.

Tests (402 passing, +18 new):

  • tests/phpunit/tests/aiToolsRegistry.php — 9 cases (registry validation, capability gating, WP_Error / exception handling).
  • tests/phpunit/tests/aiSearchExtensibility.php — 10 cases (every layer of wpdm_ai_compose_instructions).
  • tests/phpunit/tests/settingsTabs.php — 10 cases (registration, payload, action fires).
  • tests/vitest/ai-ask.test.ts — 18 cases (ask wrapper, command harvest, tool_call dispatch, follow-up leg happy / degrade / abort, system-prompt plumbing, empty-query guard).
  • src/ui/components/wpd-code/wpd-code.test.ts, src/ui/components/wpd-steps/wpd-steps.test.ts — new component smoke tests.

Docs:

  • docs/hooks-reference.md — AI extensibility section, wp_register_desktop_ai_tool(), settings-tab registrations.
  • docs/javascript-reference.mdwp.desktop.ai.ask, registerSettingsTab, ready() alias, <wpd-code> + <wpd-steps> usage, bootstrap "why not addAction" callout.
  • docs/examples/ai-ask.md (new) — 6-section recipe (simple ask, commands-as-tools, allowlists, system prompt, PHP tool registration, observability, cancellation).
  • docs/examples/README.md — index.
  • Recipes updated across register-command.md, register-wallpaper.md to use wp.desktop.ready().
  • CLAUDE.md — live-refresh payload shape updated; doc tree annotated.

Bundle impact

  • desktop.min.js: 285 KB → 297 KB (gzip 78 KB → 81 KB). ~+3 KB gz for the AI extensibility + settings-tab registry + two new components.

Status labels

Everything new is marked Experimental, since 0.17.0. We'll flip the labels to Stable in the release PR once we're confident the shapes stand. No plugin should depend on an Experimental signature across a major version.


Test plan

OPTIONALLY DOWNLOAD THE PLUGIN SHOWCASED IN THE DEMO:
alcazaba-voice.zip

  • Activate a plugin that registers a settings tab via wp_register_desktop_settings_tab(); verify it appears in the OS Settings window without F5.
  • Deactivate the same plugin; verify the tab disappears from an already-open OS Settings window.
  • Register a slash-command with aiCallable: true; call wp.desktop.ai.ask( '…', { tools: 'aiCallable' } ); verify the command fires.
  • Same, with followUp: true; verify res.message is conversational (not the raw run() return).
  • Register a PHP tool with a capability; verify it's invisible to non-admins.
  • Intentionally throw inside a PHP tool handler; verify wp_desktop_ai_search_error fires and the agent can still answer.
  • Verify <wpd-section stack> adds a gap in a plugin settings tab; verify existing built-in sections render unchanged.
  • <wpd-code>c</wpd-code> inside a page doesn't steal c keystrokes.
  • <wpd-steps> with steps added/removed renumbers automatically.
  • wp.desktop.ready(fn) called from a server-sync-injected script (i.e. after wp-desktop.init has fired) invokes fn synchronously via microtask.
  • Abort an in-flight ask() via AbortController.abort(); verify AbortError rejects and no ghost state lingers.

🤖 Generated with Claude Code

Open WordPress Playground Preview

AllTerrainDeveloper and others added 3 commits April 24, 2026 13:55
- Implemented a new registry for third-party OS Settings tabs in `src/settings/registry.ts`, allowing plugins to register additional settings tabs.
- Created a server sync module in `src/settings/server-sync.ts` to handle live updates of settings tabs from the server.
- Introduced a new inline code component `<wpd-code>` with styles and tests for rendering code snippets safely without intercepting keypresses.
- Developed an ordered steps component `<wpd-steps>` and its child `<wpd-step>` for structured onboarding flows, complete with styles and tests.
- Added PHPUnit tests for the new settings tab registration functions to ensure proper functionality and error handling.

Co-authored-by: Copilot <copilot@github.com>
…ted tests

- Add `wp.desktop.ai.ask()` function for programmatic access to AI Copilot.
- Define interfaces for AskOptions, AskResult, and AskToolCall to structure API responses.
- Implement command invocation logic for tool calls, including error handling for unregistered commands.
- Create unit tests for `wp.desktop.ai.ask()` covering various scenarios including normal responses, tool calls, and error handling.
- Introduce PHP unit tests for `wp_register_desktop_ai_tool()` to validate tool registration and invocation.
- Ensure proper handling of system prompts and follow-up responses in the AI interaction flow.
- Made fallbackContext a required property in AskDeps interface to ensure proper command context handling.
- Introduced liftMessage and serialiseOutcome utility functions for better message handling and serialization of command results.
- Refactored ask function to utilize postToSearch for network requests, improving error handling and reducing code duplication.
- Enhanced dispatchToolCall to handle command execution and error structuring uniformly.
- Added tests to cover new functionality and ensure robust error handling for empty queries with non-default options.

feat(commands): update notify method documentation

- Updated documentation for the notify method in CommandContext to clarify its current no-op status and future plans for implementation.

fix(ui): mark WpdCode and WpdSteps components as experimental

- Changed status of WpdCode and WpdSteps components from stable to experimental to reflect their current development stage.

test(ai): add unit tests for aiSearch extensibility

- Created a new test suite for verifying the extensibility of the /ai/search endpoint, ensuring filters and actions function as expected.
Copy link
Copy Markdown
Collaborator

@epeicher epeicher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @AllTerrainDeveloper! I have done the following tests:

  • Register a slash-command with aiCallable: true; call wp.desktop.ai.ask( '…', { tools: 'aiCallable' } ); verify the command fires. ✅
Image
  • Same, with followUp: true; verify res.message is conversational (not the raw run() return). ✅
First Follow up
Image Image
  • Register a PHP tool with a capability; verify it's invisible to non-admins
Admin Non-admins
Image Image
  • Intentionally throw inside a PHP tool handler; verify wp_desktop_ai_search_error fires and the agent can still answer.
Image
  • Verify adds a gap in a plugin settings tab; verify existing built-in sections render unchanged. ✅
Image
  • <wpd-code>c</wpd-code> inside a page doesn't steal c keystrokes. ✅
Image
  • <wpd-steps> with steps added/removed renumbers automatically. ✅
Image
  • wp.desktop.ready(fn) called from a server-sync-injected script (i.e. after wp-desktop.init has fired) invokes fn synchronously via microtask. ✅
Image

And they all have passed as expected. LGTM!

@AllTerrainDeveloper
Copy link
Copy Markdown
Collaborator Author

Wow, great testing!

@AllTerrainDeveloper AllTerrainDeveloper merged commit 9484987 into trunk Apr 24, 2026
54 of 55 checks passed
@AllTerrainDeveloper AllTerrainDeveloper deleted the feature/new_hooks_and_components branch April 24, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants