WrengleWrengle
Views (custom plugin UI)

Views (custom plugin UI)

Beta

A 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:

toml
[[views]]
id = "plugin.example.my-view"
label = "My View"
entry = "ui/index.html"
accepts_selection = true
selection_label = "Send to My View"
FieldRequiredDescription
idYesUnique 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 ...
labelYesDisplay name shown in the command palette and Actions menu.
entryYesRelative path to the HTML file inside the plugin folder. The file must exist at reload time.
accepts_selectionNoIf true, an item appears in the editor Actions menu to send selected text to this view. Defaults to false.
selection_labelNoLabel 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 src attributes 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.

DirectionMessage typePayloadWhen
view → hostwrengle:readyView finished booting; host delivers init and any queued messages.
host → viewwrengle:init{ theme: "light" | "dark", themeTokens: Record<string, string> }Sent once after the ready handshake.
host → viewwrengle:theme{ theme: "light" | "dark", themeTokens: Record<string, string> }Sent when the user changes the app theme or active theme tokens.
host → viewwrengle: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:

javascript
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:

HelperAvailabilityBehavior
window.wrengle.state.get(key)Builder-created offline feature appsReads plugin-scoped local JSON state.
window.wrengle.state.set(key, value)Builder-created offline feature appsWrites plugin-scoped local JSON state.
window.wrengle.state.delete(key)Builder-created offline feature appsDeletes 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 labelled selection_label appears 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

SymptomWhat to check
View missing from paletteOpen Settings → Plugins and click Reload. Check Diagnostics for manifest errors.
Blank tabVerify the entry path is correct relative to the plugin folder and that the file is under 5 MB.
Selection item missingSet accepts_selection = true in your [[views]] block and reload.
Stale UI while developingReopen the tab — the HTML file is re-read each time the tab opens. No reload required for HTML changes.
docs / plugins/viewsAll documentation