UI actions
UI actions add a review or control step before one or more process runs.
Every process launch uses the plugin folder as its working directory, closes stdin, selects the declared runtime, and receives a sanitized environment with Oruva's secrets and backend tokens removed. Success is exit code 0. A launch is refused unless the plugin is enabled, approved, and unchanged since approval: Oruva re-hashes the folder before running, so a plugin whose files changed shows Re-approval required instead of running.
Add the optional field to an action:
{
"id": "review-and-apply",
"label": "Review and apply",
"ui": "ui.html",
"accepts": ["image"],
"args": ["--asset-path", "{asset.path}"]
}
When the user chooses it, Oruva opens a resizable modal titled with the action label and plugin identity, showing the plugin icon when there is one. Its iframe loads ui.html with exactly sandbox="allow-scripts": scripts may run, but the page gets no Node access and no same-origin privilege. The frame is offline: direct fetch, remote images, navigation, and popups are cancelled, and the bridge messages below are the only way out. Permission approval gates the action exactly as it does a headless action.
The iframe does not automatically launch the entry. It first receives context, then decides when to request one or more process runs. Those UI runs use bridge arguments, not the manifest args; the manifest arguments remain important for the headless fallback.
Selection behavior: a UI action receives the full current selection after filtering it to the action's accepted media types, in selection order. If the right-clicked asset was not in the selection, it receives just that asset. The host accepts up to the first 512 valid, unique selected files in one modal session. This batch behavior is exclusive to UI actions; headless actions keep the single clicked-asset behavior above.
If ui is absent, the action stays headless. If it is present but the file is missing or mistyped, the plugin remains discoverable, Oruva logs a warning, and the action falls back to its headless args. An unsafe ui path that escapes the plugin folder is a manifest validation error.
Ready and context handshake
The iframe should install its message listener, then post readiness:
window.parent.postMessage({type: 'oruva-plugin:ready'}, '*');
Oruva replies once:
{
type: 'oruva-plugin:context',
theme: 'dark',
assets: [{id: '42', name: 'chair.jpg', path: 'C:\\Library\\chair.jpg'}]
}
theme is the active theme id (current examples include dark, fable, fable-dark, and blueprint). Each asset has a validated real local path. The modal waits indefinitely for oruva-plugin:ready; it cannot send context before the handshake.
Message protocol
All messages are plain JSON objects sent with window.postMessage.
| Direction | Message | Meaning |
|---|---|---|
| iframe → app | {type:'oruva-plugin:ready'} | Announces that the iframe listener is ready. |
| app → iframe | {type:'oruva-plugin:context', theme, assets:[{id,name,path}]} | Sends theme and selected assets once. |
| iframe → app | {type:'oruva-plugin:run', runId, args} | Starts the entry with bridge-generated CLI pairs. |
| iframe → app | {type:'oruva-plugin:cancel', runId} | Kills that run's process tree. |
| app → iframe | {type:'oruva-plugin:event', runId, payload} | Relays one parsed NDJSON stdout line. |
| app → iframe | {type:'oruva-plugin:run-done', runId, exitCode} | Reports process exit; host-side rejection uses -1. |
| iframe → app | {type:'oruva-plugin:read-file', id, path} | Requests an approved local image as a data URL. |
| app → iframe | {type:'oruva-plugin:read-file-result', id, dataUrl} or {type:'oruva-plugin:read-file-result', id, error} | Returns the local image or error. |
| iframe → app | {type:'oruva-plugin:fetch-image', id, url} | Requests an HTTPS raster image from a declared origin as a data URL. |
| app → iframe | {type:'oruva-plugin:fetch-image-result', id, dataUrl} or {type:'oruva-plugin:fetch-image-result', id, error} | Returns the remote image or error. |
| iframe → app | {type:'oruva-plugin:open-url', id, url} | Asks Oruva to open a declared HTTPS site in the default browser. |
| app → iframe | {type:'oruva-plugin:open-url-result', id, ok:true} or {type:'oruva-plugin:open-url-result', id, error} | Reports whether the site was opened. |
| iframe → app | {type:'oruva-plugin:close'} | Asks Oruva to close the modal. |
Choose a non-empty string runId; Oruva echoes it on every event and the final run-done. The host accepts at most 256 characters and no NUL. Only one plugin process may run per modal at a time. Oruva rejects a second concurrent request, so queue multi-asset or multi-phase work in your iframe. Closing the modal with its X, Escape, or oruva-plugin:close kills every running process tree.
For run, args must be a flat object. Each entry becomes --<key> <string-value> after validation. Values may be strings, numbers, or booleans, are stringified, and may be at most 4096 characters with no NUL. asset-path must exactly match one of the context paths.
Current v1 limitation: the allowed keys are a fixed, app-enforced set: mode, asset-path, slug, matched-index, indices, and verify. A plugin that needs another option must encode it into one of those values or wait for a future manifest-declared whitelist. Do not send arbitrary switch names; they are rejected before launch.
read-file accepts an absolute path only when it is a context asset or was previously announced in this modal session by a file_written event. The host also recognizes search_result.gallery[].local_preview, but search_result is a plugin-specific convention, not a generic platform event. Local reads support PNG, JPEG, WebP, GIF, BMP, TIFF, and AVIF images up to 32 MiB.
fetch-image requires an HTTPS URL without embedded credentials whose origin is one of the plugin's declared network.destinations. The first request to an origin raises the Deny / Allow once / Always allow prompt described under Permissions; a denied or undeclared origin returns an error result. The request travels through the broker (8 MiB response cap, three re-validated redirects) and must come back as raster image/* content — SVG is refused. Use this bridge instead of trying to grant the sandboxed iframe broader privileges.
open-url requires an HTTPS URL whose origin is one of the plugin's declared browser.origins (which in turn requires the process.launch permission with default_browser). It uses the same consent prompt with its own per-site decision and returns ok:true once the browser has been asked to open it.
Streaming NDJSON events
A UI-mode process has a 600-second timeout. stdout is consumed line by line; print one JSON object per line and flush after every event. Each line is capped at 64 KiB. A non-object or unparseable line is still relayed as {event:'log', line}. stderr goes to Oruva's log and diagnostics rather than the iframe. The app relays arbitrary object payloads; except where noted below, event names and fields are conventions shared by your entry and your UI.
These are the recommended generic events:
| Event | Recommended payload and use |
|---|---|
status | {event:'status', message} for short human-readable phase notes. |
progress | {event:'progress', asset_path, file, index, count, received, total}. Use byte counts; total may be null. |
file_written | {event:'file_written', path, kind} where kind is replace or extra. This name, path, and kind are app-interpreted for read access and reconciliation; use absolute paths. |
result | {event:'result', asset_path, status, message, files_written, backup_path} as the last event in a run. files_written is useful to your UI, but does not replace individual file_written events. |
error | {event:'error', message} for a failure the iframe should display. Exit non-zero too when the run failed. |
For useful progress, emit at a stable byte interval and at each file's completion. The 3DSky example uses 256 KiB intervals; that cadence is a plugin choice, not an app-enforced rule.
A real UI plugin: 3DSky Hi-Res Match
The bundled
oruva.sky-matchplugin is the canonical search/act split. Itssearchmode takes one contextasset-path, contacts the five 3DSky origins its manifest declares, streamsstatusobjects, and ends with its own plugin-specificsearch_result; it does not modify the asset.ui.htmlshows the local and remote previews throughread-fileandfetch-image, lets the user review the gallery, and then startsapply. Apply downloads the chosen replacement and extras, streams byte-levelprogress, emitsfile_writtenfor each saved file, and finishes withresult. It is a compact model for separating a safe discovery phase from an explicit write phase without freezing the modal — and its manifest is the complete schema-2 example in the Manifest reference.