Views (custom plugin UI)
BetaA view lets a local plugin ship its own user interface. Wrengle renders the view as a tab in the editor area — it can sit in a split next to your notes — inside a sandboxed iframe.
Views are part of the local developer preview. The bridge protocol below may change between releases.
Declare a view
Add a [[views]] block to your plugin.toml:
[[views]]
id = "plugin.example.my-view"
label = "My View"
entry = "ui/index.html"
accepts_selection = true
selection_label = "Send to My View"| Field | Required | Description |
|---|---|---|
id | Yes | Unique within the manifest across actions and views. Must start with the plugin id followed by a dot. Lowercase ASCII letters, digits, dots, and hyphens only; no ... |
label | Yes | Display name shown in the command palette and Actions menu. |
entry | Yes | Relative path to the HTML file inside the plugin folder. The file must exist at reload time. |
accepts_selection | No | If true, an item appears in the editor Actions menu to send selected text to this view. Defaults to false. |
selection_label | No | Label for the Actions menu item. Defaults to Send selection to <label>. Only meaningful when accepts_selection = true. |
A plugin can be view-only: no [runtime], no plugin.wasm, and no build step
are required. The manifest only needs at least one action or one view to be
valid.
One self-contained HTML file
The entry file must be a single self-contained HTML file. Wrengle reads it
fresh each time the view tab is opened, so during development you can edit the
file and reopen the tab to see changes — no plugin reload needed.
Constraints:
- All CSS and JavaScript must be inlined in the single file. No external
stylesheets, no
srcattributes pointing to other files. - If you use a build tool, configure it to output a single bundled file.
- The file must be no larger than 5 MB.
The bridge protocol
Wrengle and your view exchange messages over window.postMessage. This is the
only communication channel; the iframe has no access to Wrengle's internal APIs.
| Direction | Message type | Payload | When |
|---|---|---|---|
| view → host | wrengle:ready | — | View finished booting; host delivers init and any queued messages. |
| host → view | wrengle:init | { theme: "light" | "dark", themeTokens: Record<string, string> } | Sent once after the ready handshake. |
| host → view | wrengle:theme | { theme: "light" | "dark", themeTokens: Record<string, string> } | Sent when the user changes the app theme or active theme tokens. |
| host → view | wrengle:selection | { text: string } | Sent when the user invokes the view's selection action. |
Wrengle injects the active theme variables into the view before first paint and
sends the same values as themeTokens whenever the active theme changes.
themeTokens keys are CSS custom property names with the leading --, such as
--background, --foreground, --border, --primary, and
--primary-foreground. Use those variables and the rest of the Wrengle theme
tokens instead of maintaining a separate plugin color palette.
Add this block inside a script element in your HTML to implement the bridge:
function applyWrengleTheme(message) {
if (message.theme) {
document.documentElement.dataset.theme = message.theme;
}
if (!message.themeTokens || typeof message.themeTokens !== "object") return;
for (const [name, value] of Object.entries(message.themeTokens)) {
if (/^--[A-Za-z0-9_-]+$/.test(name) && typeof value === "string" && value.length > 0) {
document.documentElement.style.setProperty(name, value);
}
}
}
window.addEventListener("message", (event) => {
if (event.source !== window.parent) return;
const msg = event.data;
if (!msg || typeof msg !== "object") return;
if (msg.type === "wrengle:init" || msg.type === "wrengle:theme") {
applyWrengleTheme(msg);
} else if (msg.type === "wrengle:selection") {
// msg.text is the text the user selected in the editor
}
});
parent.postMessage({ type: "wrengle:ready" }, "*");The wrengle:ready message tells the host your view is ready to receive
messages. Send it as soon as your initialization is complete.
Builder-created offline state helpers
Plugin Builder currently creates static custom views and offline feature apps.
Builder-created offline feature app drafts declare expected state helper usage,
and generated code is checked against that draft contract before Apply.
Installed app state is plugin-scoped local JSON state managed by Wrengle. Manual
local plugin views cannot use these generated window.wrengle helpers yet.
Plugin Builder injects these helpers into offline feature apps so generated code does not need direct parent-window access:
| Helper | Availability | Behavior |
|---|---|---|
window.wrengle.state.get(key) | Builder-created offline feature apps | Reads plugin-scoped local JSON state. |
window.wrengle.state.set(key, value) | Builder-created offline feature apps | Writes plugin-scoped local JSON state. |
window.wrengle.state.delete(key) | Builder-created offline feature apps | Deletes plugin-scoped local JSON state. |
Builder drafts declare expected state keys and generated code is checked
against those keys before Apply. Installed app calls are scoped to the plugin's
local JSON state and limited to the declared keys. Builder-created offline apps
do not receive state.list or state.clear. State reset is a Wrengle-owned
action from Plugin Builder, not an injected view helper. Resetting state removes
the app's local saved data only; it does not remove plugin files, prompt
history, or conversations.
Builder-created offline apps cannot read or write Wrengle notes, projects, blocks, meetings, workflows, files, or settings. They cannot use dashboard snapshots, external network access, secrets, shell commands, Tauri IPC, generated WASM actions, or app-mediated write proposals.
Workspace-aware generated plugins remain future work. Prompts that need
dashboard.snapshot, readNote, readBlocks, writeNote, writeBlocks,
recent notes, project data, note writes, block writes, external APIs,
credentials, or secrets are blocked instead of being turned into fixture-backed
previews. If write proposals return in a future Builder release, nothing changes
until you approve the proposal in Wrengle.
Where views appear
- Command palette: an "Open <label>" entry appears for every loaded view, with the plugin name shown as attribution. Use ⌘K (macOS) or Ctrl+K to open the palette.
- Editor Actions menu: when
accepts_selection = true, an item labelledselection_labelappears in the floating toolbar's Actions menu when text is selected in a note.
The palette command always opens a new tab, so you can run several instances of the same view side by side. The selection action instead reuses the most recently used open tab of that view — it focuses that tab and sends the text there, opening a new tab only when none is open.
For a runnable reference, follow the
JSON Compare tutorial, which loads
examples/plugins/json-compare with no build step.
Sandbox and trust
The iframe is created with sandbox="allow-scripts" and no allow-same-origin.
This means:
- Except for declared Builder-created offline state helpers, the view has no access to the app, your vault, or Wrengle's APIs.
- The view cannot read cookies, localStorage, or sessionStorage from the app origin.
- The view cannot call the host app's IPC or access the parent DOM.
Unlike WASM actions, manual view JavaScript can make outbound network requests:
the sandbox attribute does not block fetch or XMLHttpRequest. Direct view
JavaScript network access is not controlled by httpHosts. Builder-created
offline apps are checked against network access and remain offline. The
httpHosts capability gates only Wrengle host-bridge HTTP calls made by WASM
actions.
Load only local plugin code you trust.
Troubleshooting
| Symptom | What to check |
|---|---|
| View missing from palette | Open Settings → Plugins and click Reload. Check Diagnostics for manifest errors. |
| Blank tab | Verify the entry path is correct relative to the plugin folder and that the file is under 5 MB. |
| Selection item missing | Set accepts_selection = true in your [[views]] block and reload. |
| Stale UI while developing | Reopen the tab — the HTML file is re-read each time the tab opens. No reload required for HTML changes. |