Skip to content

Feat/new hooks and ccomponents - #32

Merged
AllTerrainDeveloper merged 3 commits into
trunkfrom
feat/new-hooks-and-ccomponents
Apr 27, 2026
Merged

Feat/new hooks and ccomponents#32
AllTerrainDeveloper merged 3 commits into
trunkfrom
feat/new-hooks-and-ccomponents

Conversation

@AllTerrainDeveloper

@AllTerrainDeveloper AllTerrainDeveloper commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

DevTools / cross-plugin instrumentation surface (+ supporting components)

These new hooks and components would allow amazing things as:
https://github.com/user-attachments/assets/89ba2828-9aa5-46bb-b6f4-0cf20c2aa5ac

## Test plugin
query-monitor.zip

Why

Desktop Mode is a plugin platform — every window can come from a different plugin, and the most useful devtools (a SQL inspector, a network logger, a perf profiler) are themselves plugins that need to attach behavior to other plugins' windows. Today there's no documented surface for that. The escape hatches plugin authors reach for — iframe.contentWindow.fetch = …, walking another plugin's DOM, custom REST endpoints with hand-rolled poll loops — fight each other and break across permalink schemes.

This PR adds the supported surface, plus the UI primitives those devtools end up needing.

The work was driven by a real plugin (an external "SQL Inspector") whose author hit four compounding gaps in succession. Each round of debugging surfaced one more gap; each gap is fixed and covered by a regression test below.

What's new

1. wp.desktop.devtools — the cross-plugin instrumentation surface

A new public API on wp.desktop that lets one plugin attach behavior to a window registered by another plugin without reaching into iframe globals.

Method Purpose
addRequestHeader(windowId, name, value) Contribute an HTTP header to every fetch / XHR / sendBeacon from the target window. Multiple devtools can contribute the same header — values are joined per RFC 7230. Returns a disposer.
onRequest(windowId, cb, { observe }) Subscribe to every completed network call. Default payload is the privacy-conscious summary; observe: true adds request + response headers.
reloadWithDebugSession(windowId, sessionId, opts?) Reload an iframe with a session id baked into both the URL and the header registry. Bundles the four-step boilerplate every devtool would otherwise re-derive.
debug.startSession() / publish() / subscribe() Generic per-session pub/sub bus, server- and client-side. Subscriptions auto-poll a REST endpoint and replay events.

Architecture: the shell owns one instrumentation channel per window and brokers contributions from many devtools so they don't fight each other. Reference-counted contributions, deterministic header merging (RFC 7230 comma-join for duplicates), per-window load listener that re-pushes instrumentation on every iframe document load.

2. desktop_mode_debug_publish() — server-side debug bus

PHP companion to wp.desktop.devtools.debug. Plugins running inside an admin / REST / AJAX request hook a capture (e.g. SAVEQUERIES for SQL, pre_http_request for outbound HTTP), then publish to a per-(session, channel) ring buffer. The shell polls and replays events to client-side subscribers.

$sid = desktop_mode_debug_session_for_request();
if ( '' !== $sid ) {
    desktop_mode_debug_publish( $sid, 'query', array(
        'sql' => $sql, 'time' => $duration,
    ) );
}
  • Storage: per-(session, channel) ring buffer in a transient, capped at 500 events (filterable), TTL 1 hour.
  • REST: GET /wp-desktop/v1/debug?sessionId=…&since=…&channels[]=…. Permission: logged-in admin (manage_options); override via desktop_mode_debug_rest_permission.
  • Synchronous desktop_mode_debug_publish action fires on every publish for observability widgets that don't want to round-trip through the poll loop.

3. Window.config.ownerHandle — attribution

Every window registered via desktop_mode_register_window( $args ) (with 'script' => 'my-plugin-handle') carries that handle through to JS-side Window.config.ownerHandle. Lets devtools identify which plugin owns a window without parsing URLs.

4. UI components — <wpd-log>, <wpd-badge>, <wpd-code copy>

Drawn from real needs in the SQL Inspector plugin, but generic enough to belong in the kit:

  • <wpd-log> — virtualized streaming list. Append-only API (push, pushMany), LRU cap (max-rows), tail-stickiness (classic tail -f), per-row renderer. Two modes:

    • Default (fixed-row-height) — fast path. total = entries × rowHeight, no measurement passes. Required when feed rates are high (a chatty admin page can fire 200+ queries per request).
    • auto-row-height — measured per-row heights, cumulative offsets, binary-search viewport finding. One layout pass per visible row but unblocks variable-content rows (header + body, expandable details).
  • <wpd-badge> — colored-dot status pill. Five tones (success, warning, danger, info, neutral) plus no-dot for count chips. Tiny, recurring need that every plugin was reinventing badly.

  • <wpd-code copy> — added a copy boolean attribute to the existing <wpd-code> that surfaces a copy-to-clipboard affordance. Hover-revealed inline, always-visible on block. Fires wpd-copy after a successful clipboard write. Falls back to document.execCommand('copy') under permission-locked iframes.

Bug fixes surfaced during plugin author's six rounds of integration

Every one of these is a regression test in this PR.

Bug 1 — poll loop didn't pass channels (silent empty drains)

The JS poll URL omitted the active subscription channels entirely. The server-side drain has no channel set to walk without an explicit param OR a desktop_mode_debug_channels filter contributor, so it returned { events: [] } on every poll. Plugins following the docs verbatim saw zero events with no error. Fix: stamp every active subscription channel as channels[]=… on every poll. The server already supported it.

Bug 2 — URL composition broke under ugly permalinks

${restUrl}wp-desktop/v1/debug?sessionId=… works on pretty-permalink installs (/wp-json/) but produces <site>/?rest_route=/wp-desktop/v1/debug?sessionId=… — two ? separators, malformed — under ugly permalinks. WordPress routed the request to the homepage, returned HTML, JSON.parse blew up. Fix: compose the URL via WHATWG URL + searchParams.set/append; the parser folds the second batch of params into the existing query.

Bug 3 — <wpd-log> clipped variable-content rows silently

Default fixed-row-height mode is by design (it's what makes the virtualizer tractable), but it wasn't documented and didn't have an escape hatch. Plugin authors discovered it by shipping: dev with one-line entries looked fine, production with multi-line entries got the bottom half eaten. Fix: prominent JSDoc + in-product Help-tab warning, plus an opt-in auto-row-height mode for variable-content rows.

Bug 4 — addRequestHeader instrumentation didn't survive manual iframe reloads

The shell relies on a wp-desktop-ready postMessage to fire IFRAME_READY and re-push instrumentation. That signal isn't actually emitted by either iframe-side bridge today, so the re-push path was dead code. Plugins that rewrote iframe.src directly (to add a debug-session token visible to the document load) saw their headers silently dropped on the new document. Fix: install a per-window load listener that re-pushes instrumentation deterministically, plus the reloadWithDebugSession() primitive that bundles the four-step boilerplate so the next plugin author doesn't have to wire any of it up.

Test coverage

  • 490/490 vitest tests passing (15 new dedicated to the devtools surface, 7 to <wpd-log>, 3 to <wpd-badge>, plus regression tests for each of the four bugs above).
  • tsc clean (tsc --noEmit).
  • eslint clean (src/**/*.ts).
  • Production builds clean — both desktop[.min].js and iframe-bridge[.min].js.

Files

Added

  • src/devtools/index.ts — public API module
  • includes/devtools.php — server-side debug bus + REST route
  • src/ui/components/wpd-badge/ — badge component + styles + tests
  • src/ui/components/wpd-log/ — virtualized log component + styles + tests
  • tests/vitest/devtools.test.ts — 16 tests covering the API surface and every bug regression
  • docs/examples/devtools-instrumentation.md — full SQL Inspector walkthrough

Modified

  • src/types.tsownerHandle on WindowConfig + NativeWindowServerEntry
  • src/desktop.ts + src/public-api.ts — mount wp.desktop.devtools on the public API; export new types
  • src/native-windows.ts — propagate ownerHandle through both server-sync open path and createRegisterWindow
  • src/window/iframe-bridge.ts — extend IFRAME_NETWORK_COMPLETED payload with optional requestHeaders / responseHeaders
  • src/ui/components/index.ts — register <wpd-log>, <wpd-badge>
  • src/ui/components/wpd-code/copy attribute + clipboard handler + styles
  • includes/render.php — chromeless inline bridge: instrumentation message listener, header merging into fetch / XHR / sendBeacon, observe-mode header capture
  • includes/helpers.php — emit ownerHandle on the native-window payload
  • desktop-mode.php — require the new devtools include
  • docs/hooks-reference.md + docs/javascript-reference.md + docs/examples/README.md — full doc coverage

Deployment notes

  • All new APIs are tagged Experimental (@since 0.6.0). Backwards-compatibility guarantees apply once promoted to Stable in a future release.
  • No breaking changes to existing surfaces. The IFRAME_NETWORK_COMPLETED action gained two optional payload fields — pre-existing subscribers see no behavioral change.
  • The chromeless inline bridge gained ~120 lines of instrumentation glue. Best-effort — wrapped in try/catch so a malfunction in the instrumentation doesn't tank the rest of the bridge.
  • Built assets (assets/js/desktop[.min].js, assets/js/iframe-bridge[.min].js) are gitignored — run npm run build before tagging a release.
Open WordPress Playground Preview

…ebugging

- Added DevTools API to allow third-party plugins to contribute HTTP headers, observe network requests, and publish debug events.
- Implemented header contributions via `addRequestHeader` method, allowing multiple plugins to contribute to the same header.
- Introduced request observation with `onRequest` method to subscribe to completed network calls, including optional request/response headers.
- Established a generic debug bus for session-based event publishing and subscription, facilitating real-time debugging data streaming.
- Enhanced existing window configuration to include `ownerHandle` for better plugin attribution.
- Updated public API and types to reflect new DevTools functionalities.
- Implemented server-side support for debug event publishing and REST endpoint for fetching debug events.
@AllTerrainDeveloper AllTerrainDeveloper self-assigned this Apr 27, 2026
@AllTerrainDeveloper
AllTerrainDeveloper merged commit a12faf7 into trunk Apr 27, 2026
8 checks passed
@AllTerrainDeveloper
AllTerrainDeveloper deleted the feat/new-hooks-and-ccomponents branch April 27, 2026 17:21
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.

1 participant