The six native list windows are now .os.php apps under apps/ — apps/posts/, apps/pages/, apps/users/, apps/user-edit/, apps/plugins/, apps/comments/ — driven by the App Framework instead of three PHP modules plus three TypeScript bundles. Every window keeps its frozen id (desktop-mode-posts, desktop-mode-pages, desktop-mode-users, desktop-mode-user-edit, desktop-mode-plugins, desktop-mode-comments), its title, size, gate, opt-in setting and URL remap, so dock tiles, session restore, desktop shortcuts and the nativeXEnabled toggles carry over untouched. Same windows, same rows, same styling on a desk and on a phone; new plumbing.
| Surface | Replacement |
|---|---|
Filters openstation_posts_window_args, openstation_pages_window_args, openstation_users_window_args, openstation_user_edit_window_args, openstation_plugins_window_args, openstation_comments_window_args |
openstation_app_manifest — $manifest['width'], ['icon'], ['title'], ['config'] (the former config blob, now the config extra), … for the matching $id; openstation_app_window_args for the registration args. |
Filters openstation_*_window_template_html (all six) |
There is no server template. Each body is a client view; the data-os-posts-*, data-os-users-*, data-os-plugins-* and data-os-comments-* hooks still mark the same elements, and the old root classes (desktop-mode-posts, desktop-mode-users, desktop-mode-plugins, os-comments) are still on the roots for third-party CSS. |
PHP openstation_*_window_render_template() / _register_window() / _default_query_args() callers of openstation_register_window() |
The apps register themselves through the framework. openstation_*_window_default_query_args() still exist (the openstation_*_window_query_args filters are unchanged) and now feed each app's data(). |
Script handles os-posts-window, os-plugins-window, os-comments-window; bundles assets/js/{posts,plugins,comments}-window[.min].js; style handles of the same names; assets/css/{posts,plugins,comments}-window.css |
The shared openstation-app-runtime bundle plus each app's companion client view (assets/js/apps/<name>[.min].js) and stylesheet (apps/<dir>/<name>.css, registered on first open). The list layout Posts, Pages and Users share (.os-app-list__*) is in assets/css/app-runtime.css; apps/user-edit/ points at apps/users/users.css. |
wp.os.getWindowConfig( id ) keys postsUrl, usersUrl, commentsUrl, bulkUrl, replyUrl, createUserUrl, insightsUrlBase, pluginsUrl, queryArgs, … |
The list is the app's data(); the static facts an app still needs ride wp.os.getWindowConfig( id ).extra (ctx.extra in the client view): mode, editPostUrlBase, newPostUrl, defaultOrderby / defaultOrder (what a cleared column sort returns to), frontPageId / postsPageId / pageTemplates (Pages), the capability flags and role / locale / colour-scheme / contact-method maps from one memoised openstation_users_profile_facts() (Users and User Edit share it), caps, ajaxUrl, ajaxNonce, updatesNonce, autoUpdatesEnabled, selfPluginFile, adminUrl (Plugins), canModerate, canEditComments (Comments). |
The shared stores desktop-mode/user-edit/target (setUserEditTarget() / readUserEditTarget() / subscribeUserEditTarget() in src/posts-window/user-edit-target.ts), desktop-mode/comments/post-filter (setCommentsPostFilter()), desktop-mode/plugins-window/tab-target (setPluginsWindowTab()) |
Open-time params. wp.os.openWindow( 'desktop-mode-user-edit', { params: { userId } } ) — or openUserEditWindow( userId, { source, fallback } ) from src/open-targets/user-edit-window.ts, the door WP Explorer and the agents roster use; wp.os.openWindow( 'desktop-mode-comments', { params: { post } } ) (0 clears the scope); wp.os.openWindow( 'desktop-mode-plugins', { params: { tab: 'installed' | 'browse' | 'featured' } } ). The URL remaps pass the same params. Each app reads them on mount; a live singleton retargets through the new reopen lifecycle action; params ride the session, so a reload restores the same person, scope or tab. |
The whole src/posts-window/, src/plugins-window/, src/comments-window/ directories and includes/{posts,pages,users,user-edit,plugins,comments}-window/ |
The apps own their code: apps/<dir>/<name>.os.php + parts/*.php (permissions, REST fields and routes, AJAX handlers — the same global function names), apps/<dir>/<name>.os.ts + parts/*.ts (cell renderers, columns, the canvases, the profile component, the plugin views, the conversation view). The <os-user-profile> element lives in apps/users/parts/ and is shared by the Users and User Edit bundles. |
openstation_comments_window_after_bulk firing only from the REST route |
It also fires from the Comments app's moderate action, since both run openstation_comments_window_moderate(). |
The Users default _fields carrying openstation_assignable_roles, url, description; the openstation_user_stats field feeding the table |
The app computes the page's content counts in two grouped queries (openstation_users_window_stats_for()) and reads the assignable roles off its config extra. Every REST field stays registered; openstation_users_window_query_args adds them back for a consumer that reads them off the rows. |
<os-user-profile> compiled into the Users and User Edit bundles, resolving its config from window.openStationWindowConfig |
One companion bundle, assets/js/apps/user-profile[.min].js (script handle openstation-user-profile, registered by apps/users/parts/profile-script.php and appended to both windows through openstation_app_window_args). The element takes config / fetch / toast properties from the app that mounts it and falls back to the shell when unfed; the Users app mounts it only when the Profile tab is picked. |
Filter openstation_comments_window_reply_editor |
Gone. The composer is a plain <os-textarea>; the filter never selected anything else ('rich' and 'gutenberg' were never implemented). |
The Comments default _fields carrying openstation_replies_count and openstation_can_moderate |
The app computes the page's reply counts in one grouped query and ships the moderation cap in its config extra. Both REST fields stay registered; openstation_comments_window_query_args adds them back for a consumer that reads them off the rows. |
os.plugin.changed broadcasts with source: 'plugins-window' |
source: 'plugins-app', carrying the changed plugins' ids. os.post.changed keeps source: 'posts-window'; a Pages trash now announces page, not post. |
The ?openstation_force_refresh=1 flag on the Plugins window's Refresh (openstation_plugins_window_force_refresh_requested()) |
Gone. Refresh is the app's reload action, which forces the update check through openstation_plugins_window_prime_updates_once( true ); the openstation_plugins_window_refresh_updates filter still receives $force. |
One dm_pwsz_<md5> size transient per plugin |
One dm_pwsz_map transient for the whole install, read once per request and invalidated on upgrader_process_complete / deleted_plugin. |
The Browse request's updated sort and tag filter; banners, contributors, donate_link in the wp.org calls |
Removed — nothing in the window offered them. openstation_plugins_window_browse_args no longer sees a tag in its raw params. |
The dock drop-target id os-plugins-window/dock |
desktop-mode-plugins/dock. |
A plugin delete dispatch reaching Core's controller on a network |
Refused, like the admin-ajax half already was. openstation_plugins_window_caps() is the screen gate — Core's site plugins screen offers no Delete, the files are the network admin's — and a super admin holds delete_plugins, so the action enforces the gate rather than the bare capability. |
- The open. A window paints its frame the moment it opens — the client view's
placeholder(app-framework.md), with the table's skeleton or the rail's ghost rows untilmountlands the page — and the shell prefetches every app bundle and sheet in idle time after boot, so a first open waits on neither the download nor the shell's overlay. With "Prewarm windows on hover" on, a hover over the tile sends the window's firstmountahead of the click (wp.os.prewarmWindow()), and the rows are there when it opens. - The gates:
openstation_{posts,pages,users,plugins,comments}_window_user_can_register/_user_can_use,openstation_user_edit_window_user_can_register,openstation_users_window_assignable_roles. - The query-args filters:
openstation_{posts,pages,users,comments}_window_query_args,openstation_pages_window_template_labels— the same_fields/_embed/post_typereach the same Core REST controllers, in-process, so everyregister_rest_field(), therest_post_querytags-AND filter and theopenstation_*fields land on the rows exactly as before. - Every REST route under
/desktop-mode/v1:/term-counts,/tag-cooccurrence,/users*,/users/{id}/*,/comments/*— seeincludes/rest/README.mdfor the new handler files. The app actions share one PHP function per operation with them. - Every other
openstation_plugins_window_*filter and action (_browse_args,_browse_response,_info_response,_review_parser,_icon_url,_local_icon_candidates,_refresh_updates,_auto_updates_enabled,_installed, the featured-slugs pair), thewp_ajax_openstation_plugins_*handlers,openstation_plugins_window_count_visible_updates()(the dock badge). openstation_comments_window_spam_score,openstation_users_window_user_created,_login_recorded,openstation_user_edit_window_insights,OPENSTATION_LAST_LOGIN_META_KEY. (openstation_comments_ai_is_enabled,_toggledand the AI moderation option were unchanged by this migration but have since been removed — seemigration-comments-ai-scoring.md.)- The JS extension points of the Posts window: the
openstation.postsWindow.columns/statusSegments/bulkActions/toolbarTrailingfilters, theopenstation.postsWindow.opened/dataLoadedactions, theos-posts-window-opened/os-posts-window-data-loadedCustomEvents, thePostsWindowContext/BulkAction/StatusSegmentshapes (now exported fromapps/posts/parts/types.ts), theos.post.changedandos.term.changedbroadcasts, thenativePostsHiddenColumnssetting behind "Show columns". - The
wporg-plugindrag payload and the dock drop target, the plugins update queue and icon fallback chain, the upload dialog, the self-deactivate reload. - The phone layouts: the status picker, the card list, the bottom bulk bar, the folded Categories / Tags editors, the one-pane Comments view.
The six windows were 35,359 lines — 24,489 of TypeScript across three bundles, 7,517 of PHP registration / template / REST glue, 3,353 of CSS (the deleted files at trunk, counted with wc -l) — and each carried its own copy of the same plumbing: a config blob, a REST client for the list, pager DOM, toolbar wiring, a shared store to smuggle a deep link past openById(). As apps they are 25,737 lines: 24,816 under the six apps/<dir>/ directories (every file except *.test.ts; PHP, TypeScript and CSS alike) plus the 921 net lines the framework grew (git diff --numstat against trunk over includes/framework/, src/app-runtime/, app-runtime.css and the shell seams) to absorb that plumbing for every future list too: openstation_app_rest() / openstation_app_rest_page() (in-process Core REST with _fields and _embed, so a data() serves the exact rows a browser fetched), the reopen lifecycle and per-session params (the deep-link contract), App::config( callable ) (per-viewer config resolved when the manifest is built), the own-echo skip on watch() (a window's announce never costs it a second round trip), the list-window furniture (statusControl(), pager(), mountMenuCheckboxes(), createListTableSync()) and the shared .os-app-list__* layout with its phone rules. The test surface grew from 2,247 lines (the deleted vitest and registration suites) to 5,998 (app vitest 2,829, app PHPUnit 2,444, framework 725).