-
Notifications
You must be signed in to change notification settings - Fork 41
example window links
Open a post and two of its comments in three windows and the desktop draws splines terminated by circular dots from each comment window to the post window — the larger dot sits on the window the content belongs to (dots are rotation-invariant, so a tie meeting a border at any angle looks right — arrowheads read wrong on skewed approaches). Two open posts whose contents hyperlink each other get a single spline with large dots on both ends. Focusing a window raises the windows directly tied to it (they surface just below the focused one, without stealing focus): the root pulls up all of its children; a child pulls up its parent, not its siblings. The splines lift along with them so no unrelated window covers them, and every relative is marked with an accent outline plus a soft glow. The user tunes all of this in two places: OpenStation Preferences → Features → Window links (master on/off, bring-related-to-front, highlight-related) and OpenStation Preferences → Windows → Window links (link style; show always — the default — / when focused / off).
Core content relates automatically: post/page/CPT editors announce themselves as roots — plus, as outbound references, the posts their content hyperlinks, the media embedded in their content (wp-image-{id}, which catches inserted-but-never-attached images), their featured image, and their assigned categories/tags (term/{taxonomy}). Comment-edit screens, attached-media screens (both the classic editor and the upload.php?item=N grid detail), arrive pre-rooted at their parent post, and term edit screens (term.php) are roots that assigned posts point at — all resolved server-side by the chromeless bridge, since the URL alone can't answer "which post does comment 45 belong to".
Direction semantics — one deliberate reading, applied everywhere: the edge points at the thing its source belongs to or refers to (relational structure — never "which window opened which"). The engine derives typed, directed edges from the identities; the built-in renderer encodes the direction as dot size (larger dot on the target):
| Edge kind | Derived from | Endpoint dots |
|---|---|---|
child-root |
a ref with root pointing at an open root window |
large dot at the root — comment → post |
child-root |
a links entry with rel: 'child' (the declarer announces something that belongs to IT — a post's embedded/featured media, which only the post knows about) |
large dot at the DECLARER — media → post |
reference |
a plain links entry (rel omitted / 'references') |
large dot at the referenced window — post → term, post A → post B (hyperlink) |
reference + bidirectional: true
|
two windows referencing each other (merged into one edge) | large dots at both ends |
The two child-root rows are the same visible relationship expressed from either side — attached media roots itself at the post; merely-inserted media is declared by the post via rel: 'child'. Both draw media → post, so the direction never flips over an invisible technicality like attachment state.
Three extension surfaces, smallest first.
Any window can carry a content identity. A ref without root IS a root; a ref with root joins that root's group as a child. Namespace your type (vendor/sub-type).
// At open time, on the window config…
wp.os.registerWindow( {
id: 'acme-order-77',
title: 'Order #77',
render: renderOrder,
content: { type: 'acme/order', id: 77, root: { type: 'acme/customer', id: 12 } },
} );
// …or any time later:
wp.os.relations.set( 'acme-customer-12', { type: 'acme/customer', id: 12 } );The moment both windows are open, the tie draws — no further wiring. Query and react:
wp.os.relations.groups(); // → [ { key: 'acme/customer:12', rootWindowIds, children } ]
wp.os.relations.edges(); // → [ { fromWindowId, toWindowId, kind, bidirectional } ]
wp.os.relations.related( windowId ); // → tied window ids (group members + reference endpoints)
document.addEventListener( 'os-window-link-groups-changed', ( e ) => {
console.log( 'relations changed:', e.detail.groups );
} );To tie two windows without a parent/child hierarchy, use links — mutual links render as one bidirectional tie (large dots at both ends):
wp.os.relations.set( windowA, { type: 'post', id: 1, links: [ { type: 'post', id: 2 } ] } );
wp.os.relations.set( windowB, { type: 'post', id: 2, links: [ { type: 'post', id: 1 } ] } );Iframe windows get their identity from the chromeless bridge. Add your screen via the openstation_window_content_identity filter — it runs in real admin context, so you can resolve parents the URL doesn't carry:
add_filter( 'openstation_window_content_identity', function ( $identity, $screen ) {
if ( $screen && 'acme_order_page' === $screen->id && isset( $_GET['order'] ) ) {
$order = acme_get_order( absint( $_GET['order'] ) );
if ( $order ) {
return array(
'type' => 'acme/order',
'id' => $order->id,
'label' => $order->title,
'root' => array( 'type' => 'acme/customer', 'id' => $order->customer_id ),
);
}
}
return $identity;
}, 10, 2 );Return null to suppress detection for a screen. The bridge re-announces on every iframe navigation, so identities never go stale.
The default svg-splines renderer registers through the same public API yours will use. One renderer is active at a time; the user picks it in OpenStation Preferences.
wp.os.registerWindowLinkRenderer( {
id: 'acme/dotted-lines',
label: 'Dotted lines',
description: 'Straight dotted connectors between related windows.',
owner: 'acme-link-renderer', // script handle → live-unregister on deactivation
mount: ( ctx ) => {
const svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
svg.style.width = svg.style.height = '100%';
ctx.container.appendChild( svg );
const draw = ( frame ) => {
svg.replaceChildren();
// frame.edges already encodes direction + bidirectional
// merging — iterate it rather than re-deriving from groups.
for ( const edge of frame.edges ) {
if ( ! edge.from || ! edge.to ) {
continue; // null rect = minimized / split view / other desktop
}
const line = document.createElementNS( svg.namespaceURI, 'line' );
line.setAttribute( 'x1', edge.from.x + edge.from.width / 2 );
line.setAttribute( 'y1', edge.from.y + edge.from.height / 2 );
line.setAttribute( 'x2', edge.to.x + edge.to.width / 2 );
line.setAttribute( 'y2', edge.to.y + edge.to.height / 2 );
line.setAttribute( 'stroke', 'var(--os-window-link-color)' );
line.setAttribute( 'stroke-dasharray', edge.kind === 'reference' ? '4 4' : '' );
// edge.bidirectional / edge.focused are yours to style —
// the built-in uses <marker> endpoint dots and an active class.
svg.appendChild( line );
}
};
const off = ctx.onFrame( draw ); // rAF-coalesced: live drag/resize + membership changes
draw( ctx.getFrame() );
return () => {
off();
svg.remove();
};
},
} );ctx.container is the shell's link layer — absolutely positioned over the desktop, pointer-events: none, stacked behind the windows. You own its children; return a teardown that removes them.
Canvas / PixiJS sketch — pull instead of push: append your <canvas> to ctx.container, run your own ticker, and read ctx.getFrame() per tick.
wp.os.registerWindowLinkRenderer( {
id: 'acme/pixi-links',
label: 'Pixi links',
mount: async ( ctx ) => {
await wp.os.loadModules( [ 'pixijs' ] ); // shared vendor loader — see content-graph
const app = new window.PIXI.Application();
await app.init( { backgroundAlpha: 0, resizeTo: ctx.container } );
ctx.container.appendChild( app.canvas );
const g = new window.PIXI.Graphics();
app.stage.addChild( g );
app.ticker.add( () => {
g.clear();
for ( const edge of ctx.getFrame().edges ) {
/* …stroke your curve from edge.from to edge.to,
endpoint markers per edge.kind / edge.bidirectional… */
}
} );
return () => app.destroy( { removeView: true } );
},
} );add_action( 'admin_enqueue_scripts', function () {
wp_register_script(
'acme-link-renderer',
plugins_url( 'js/link-renderer.js', __FILE__ ),
array( 'openstation' ),
'1.0.0',
true
);
wp_enqueue_script( 'acme-link-renderer' );
} );
openstation_register_window_link_renderer_script( 'acme-link-renderer' );Your renderer appears in the OpenStation Preferences selector the moment the plugin activates — no reload. With owner set (above), deactivation live-unregisters it; if it was the active pick, the shell falls back to svg-splines.
The built-in splines read CSS custom properties — restyle without replacing the renderer: --os-window-link-color, --os-window-link-color-active (focused group), --os-window-link-width, --os-window-link-accent (the related-window outline stamped as os-window--linked), and --os-window-link-glow (the soft halo behind those windows — a literal color-with-alpha, not a color-mix(), so a failed resolve can't invalidate the composed box-shadow).
-
javascript-reference.md—wp.os.relations,registerWindowLinkRenderer, frame shapes, events, JS filters (includingos.window-links.contentto rewrite identities andos.window-links.rendererto force-swap the active renderer). -
hooks-reference.md—openstation_window_content_identity,openstation_register_window_link_renderer_script(). -
bridge-protocol.md— theos-content-identitymessage.
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config