-
Notifications
You must be signed in to change notification settings - Fork 41
example mio form editing
Status: Experimental. See Window-scoped MIO for the complete contract.
This example assumes an application-owned forms adapter. It is not a global MIO tool or a WordPress Ability. The adapter uses authenticated app actions or wp.os.fetch, and its server checks the acting user's capability, document ownership, expected revision and idempotency key. forms.operationStatus is a read-only endpoint. The application already owns the draft being edited.
import type { MioAbility, MioOperationOutcome } from 'openstation';
const saveForm: MioAbility = {
name: 'save_form_edit',
effect: 'write',
description: 'Validate and save an edit to the current form. Preserve all other fields.',
parameters: {
type: 'object',
properties: {
editId: { type: 'string' },
baseRevision: { type: 'string' },
patch: { type: 'string', description: 'App-defined field patch, not a replacement form.' },
},
required: ['editId', 'baseRevision', 'patch'],
additionalProperties: false,
},
allowed: () => forms.canEdit(),
validate: (args) => {
const keys = ['editId', 'baseRevision', 'patch'];
if (Object.keys(args).length !== keys.length ||
!keys.every(key => typeof args[key] === 'string')) {
return {
ok: false,
retryable: true,
errors: [{
code: 'edit_envelope', path: '$',
message: 'Provide only editId, baseRevision and patch as strings.',
suggestion: 'Read the current form for its edit ID and revision.',
}],
};
}
return true;
},
run: async (args, signal, operation): Promise<MioOperationOutcome> => {
// The adapter must not retry a timeout automatically. The server binds
// the key to the payload and revision, then records a durable receipt.
const result = await forms.validateAndSave({
editId: args.editId,
baseRevision: args.baseRevision,
patch: args.patch,
turnId: operation.turnId,
idempotencyKey: operation.idempotencyKey,
signal,
});
if (result.kind === 'validation-error') {
// Only return this when the server guarantees NO write occurred.
// It consumes the same turn budget even if editId changes.
return {
effect: 'none', status: 'rejected', retryable: true,
errors: result.errors,
};
}
return {
effect: 'write', status: 'confirmed', receipt: result.receipt,
data: { editId: result.editId, revision: result.revision },
};
},
history: ({ result }) => ({ result }),
};
const lease = wp.os.mio.registerWindow(ctx.windowId, {
host: ctx.root,
title: 'Form editor',
revision: () => forms.currentRevision(),
prompt: () => `Help edit this form. Current revision: ${forms.currentRevision()}.`,
documents: [{
id: 'fields/date.md', title: 'Date field', version: 'schema-r2',
topics: ['validation', 'forms'], componentIds: ['os-date-picker'],
markdown: dateFieldHelp,
}],
abilities: () => [
{
name: 'read_current_form', effect: 'read',
description: 'Read the complete current draft, edit ID and revision.',
parameters: { type: 'object', properties: {}, additionalProperties: false },
validate: args => Object.keys(args).length === 0,
run: (_args, signal) => forms.readCurrent(signal),
},
saveForm,
],
onTurnBegin: turn => forms.showTurnProgress(turn.turnId),
onTurnAbort: turn => forms.markTurnStopped(turn.turnId),
onTurnEnd: summary => forms.showTurnSummary(summary),
onOperation: operation => forms.observeOperation(operation),
operationStatus: (operation, signal) =>
forms.operationStatus(operation.idempotencyKey, signal),
compactHistory: history => {
// Preserve the latest COMPLETE read; remove only superseded copies.
// Older save receipts and precise argument errors remain as evidence.
const lastRead = history.outcomes.findLastIndex(
entry => (entry as { name?: string }).name === 'read_current_form'
);
return {
...history,
outcomes: history.outcomes.filter(
(entry, index) => (entry as { name?: string }).name !== 'read_current_form' || index === lastRead
),
};
},
});The name assertions above describe entries produced by this app; validate external history before using the same pattern. Keep full documents intact when they are still needed. If even one document plus context does not fit, offer revisioned resource reads and field/diff operations; do not slice the definition. history can replace large successful tool results with an immutable {editId, documentHash, byteLength} reference when the app supplies a corresponding read operation. compactHistory must preserve correction feedback and authoritative receipts.
The shell gives one user message three validation failures, sixteen calls and eight model rounds. Starting a fresh edit resource does not reset those counters. Each actual invocation receives a distinct call/idempotency ID; a duplicate write cannot be repeated just by reordering JSON keys. The server must still reject stale revisions and bind its idempotency key to the same payload. Client-side deduplication is not a replacement for server concurrency control.
If the user closes the editor while a request is in flight, retain only the operation metadata your app needs for reconciliation. A retained lease can call await lease.inspectOperation(callId) after disposal; this invokes only forms.operationStatus, never validateAndSave. The application can also query its endpoint after reload using its own stored IDs. Confirmed receipts survive late network errors. An unknown outcome means “inspect status,” not “submit the same draft again.”
Extend the adapter above with responseActions. The following is consumer code for an application-owned Forms API; OpenStation does not ship the external Forms plugin. Remember the saved form ID inside the successful save callback, keyed by the authoritative receipt. Keep this map bounded (for example the latest 64 receipts) and clear it when disposing the editor. If an older receipt has been evicted, omit its action. Do not persist preview URLs or use a model-supplied form ID.
const savedFormsByReceipt = new Map<string, number>();
// Inside saveForm.run, after validateAndSave returns an authoritative success:
savedFormsByReceipt.set(result.receipt, result.formId);
while (savedFormsByReceipt.size > 64) {
savedFormsByReceipt.delete(savedFormsByReceipt.keys().next().value!);
}
// Then return the confirmed MioOperationOutcome shown above.
// Add this property to the window registration:
responseActions: ({summary, operations}) => {
if (summary.status !== 'completed' || summary.unknownWrites > 0) return [];
const saved = [...operations].reverse().find(operation =>
operation.ability === 'save_form_edit' &&
operation.status === 'confirmed' &&
operation.receipt && savedFormsByReceipt.has(operation.receipt)
);
if (!saved?.receipt) return [];
const formId = savedFormsByReceipt.get(saved.receipt)!;
return [{
id: 'preview-saved-form', label: 'Preview',
ariaLabel: 'Preview the saved form', icon: 'dashicons-visibility',
emphasis: 'primary', effect: 'navigate',
allowed: () => ctx.root.isConnected && forms.canEdit(),
run: async ({signal}) => {
// Authenticated read; extend this app wrapper to accept AbortSignal.
// The server must reject a deleted form or revoked permission.
const form = await forms.getForm(formId, signal);
signal.throwIfAborted();
// App-owned native opener, keyed by form.id to reuse its window.
openPreviewWindow(form.id, form.title, form.previewUrl);
},
}];
},Use the native openPreviewWindow directly. A convenience helper that first saves dirty content would make this read/navigation button perform an undeclared write. Preview addresses the latest saved definition of the bound form, even after switching the editor to another form. It does not reconstruct the historical revision at the time of the chat message. Refresh its nonced URL through authenticated WordPress on each click; do not bake it into the conversation.
Duplicate clicks share one pending invocation. Once it finishes, a later click can focus the same preview again. Closing chat aborts the read; reopening the same live conversation restores eligible actions. Disposing the editor removes its callbacks. A local fetch/opening failure appears alongside the button, leaves the saved reply intact, and never invokes the provider or automatically retries.
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
- The App Framework — a window in one PHP file
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Data model — where OpenStation keeps its data
- 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
- Window-scoped MIO
- Mio
- Mobile — the phone layer
- Multisite
- Native Windows & Framework Interop
- OpenStation Network
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
- Workspaces
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration — Code Blue becomes an App Framework app
- Migration: window, wallpaper and widget bundles load on demand
- Migration — Posts, Pages, Users, User Edit, Plugins and Comments become App Framework apps
- Migration — the navigation model
- Migration — OpenStation Preferences becomes an App Framework app
- Performance settings move to Extended options
- Presence storage migration
- Migration — the Recycle Bin becomes an App Framework app
- Migration — the shell boots from its own screen
- Migration — Station Home becomes an App Framework app
- Migration: a native window's tabs move to the window chrome
- Migration — WP Explorer becomes the my-wordpress app
- Migration — WordPress package globals are no longer ambient
More
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
- Repairable form edits with MIO
- Register a window companion
- Pin your app to the phone tab bar, and react to the mode
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- WP Explorer — add a column to the list view
- 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
- Ship a window as an .os.php app
- 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
- Revisions in their own window — extend or redirect "View revisions"
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Observe stored-file cleanup failures
- 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
- Place something where the user can reach it — wp.os.workArea
- Ship a workspace template