UI contributions
BetaUI contributions let trusted local plugin manifests place host-rendered entries in Wrengle surfaces without injecting plugin React code into the app shell. Wrengle validates the manifest on Settings -> Plugins -> Reload or app reload, then exposes accepted actions, views, and UI contributions through one atomic snapshot.
The current manifest schema uses surface-specific TOML tables. Runtime snapshots
normalize those accepted rows into UiContribution entries with surface,
group, target, and disabled fields. Do not rely on runtime registration or
dynamic unregistration from plugin code in this phase.
Surfaces
| Surface | Manifest table | Notes |
|---|---|---|
| Command palette | [[palette]] | References a reusable [[commands]] entry or declares label plus target directly. |
| Slash menu | [[slash_menu]] | Adds editor slash-menu rows with plugin attribution. Custom block schemas are not supported. |
| Context menu | [[context_menus]] | Supports surface = "fileTree" and surface = "editorSelection". |
| Keybinding | [[keybindings]] | References [[commands]]; default_chords must pass host shortcut validation and conflict checks. |
| Status bar | [[status_bar]] | Static plugin-attributed button; no live polling or background status computation. |
| Settings page | [[settings_pages]] | Appears under Settings -> Plugins and renders a same-plugin sandboxed view. |
| Docked panel | [[panels]] | Renders a same-plugin sandboxed view in rightDock or utilityDock. |
Reusable [[commands]] are not visible by themselves. A commands-only plugin is
invalid; expose a command through [[palette]] or [[keybindings]].
Targets
Targets must resolve at reload. Same-plugin action and view references cannot cross into another plugin.
| Target type | Example | Behavior |
|---|---|---|
hostCommand | { type = "hostCommand", command = "newNote", args = {} } | Runs one explicit host command from the allowlist below. |
runPluginAction | { type = "runPluginAction", action = "plugin.example.action", request = "selection" } | Opens the normal plugin action confirmation and grant flow. |
openPluginView | { type = "openPluginView", view = "plugin.example.view" } | Opens a declared plugin view as a tab. |
sendSelectionToPluginView | { type = "sendSelectionToPluginView", view = "plugin.example.view" } | Sends selected editor text to a declared view. |
openPluginSettingsPage | { type = "openPluginSettingsPage", settingsPage = "plugin.example.settings" } | Opens a declared plugin settings page. |
openPluginPanel | { type = "openPluginPanel", panel = "plugin.example.panel" } | Opens a declared plugin panel. |
Action request modes are empty, selection, and surfaceContext.
selection requires selected editor text when the target runs.
surfaceContext is for bounded host context such as a file-tree row.
Host command allowlist
Wrengle does not expose a generic runCommand(name, args) dispatcher. UI
contributions can call only these host commands:
| Command | Args |
|---|---|
openSearch | Optional { query = "text" }. Context-menu placeholders expand from bounded surface context when available: file-tree rows expose {path}, {name}, and {kind}; editor-selection rows expose {selectedText}, {notePath}, and {blockId}. |
newNote | No args. |
toggleTerminal | No args. |
focusEditor | No args. |
openSettings | Optional { page = "plugins" } for built-in pages: general, plugins, integrations, appearance, or privacy. Plugin settings pages use openPluginSettingsPage. |
showNotification | Required { message = "...", level = "info" | "warning" | "error" }. Notifications are visibly attributed to the plugin. |
Host-command args are schema-checked on reload and again after placeholder expansion. Absolute filesystem paths are rejected or redacted; file-tree context uses vault-relative paths.
Manifest examples
Palette and keybinding entries commonly share a reusable command:
[[commands]]
id = "plugin.quick-notes.new-note"
label = "New quick note"
description = "Create a note from anywhere."
target = { type = "hostCommand", command = "newNote", args = {} }
[[palette]]
id = "plugin.quick-notes.new-note.palette"
command = "plugin.quick-notes.new-note"
group = "Plugins"
[[keybindings]]
id = "plugin.quick-notes.new-note.keybinding"
command = "plugin.quick-notes.new-note"
default_chords = ["Ctrl+Shift+KeyN"]
when = "workspaceOpen"Context menus receive bounded surface context:
[[context_menus]]
id = "plugin.quick-notes.search-file"
surface = "fileTree"
label = "Search related"
description = "Search for notes related to this file."
target = { type = "hostCommand", command = "openSearch", args = { query = "{path}" } }
[[context_menus]]
id = "plugin.quick-notes.summarize-selection"
surface = "editorSelection"
label = "Summarize selection"
target = { type = "runPluginAction", action = "plugin.quick-notes.summarize", request = "selection" }Settings pages and panels use declared plugin views as their body:
[[views]]
id = "plugin.quick-notes.settings-view"
label = "Quick Notes Settings"
entry = "ui/settings.html"
[[views]]
id = "plugin.quick-notes.panel-view"
label = "Quick Notes"
entry = "ui/panel.html"
[[settings_pages]]
id = "plugin.quick-notes.settings"
label = "Quick Notes"
view = "plugin.quick-notes.settings-view"
[[panels]]
id = "plugin.quick-notes.panel"
label = "Quick Notes"
view = "plugin.quick-notes.panel-view"
placement = "rightDock"
[[status_bar]]
id = "plugin.quick-notes.status"
label = "Quick Notes"
target = { type = "openPluginPanel", panel = "plugin.quick-notes.panel" }Reload, policy, and audit
Accepted contributions become visible only after plugin reload or app reload. Wrengle swaps the contribution snapshot atomically, so actions, views, diagnostics, and UI rows come from the same generation. Invalid rows are skipped with reload diagnostics.
Policy can disable all UI from one plugin installation, one surface, one host command, or one contribution. Settings currently exposes contribution-level disable and re-enable controls; broader policy scopes are kept as an explicit host API for maintainer-reviewed tooling. Disabled contributions are kept out of host UI surfaces. Attempts to invoke stale, missing, disabled, or invalid targets are denied and audited with path, secret, and selected-text redaction.
Current limits
- No marketplace, remote install, publishing flow, or public API stability guarantee exists for UI contributions.
- Runtime dynamic contribution APIs are not supported.
- Plugins cannot inject React components, arbitrary HTML, or CSS into host chrome. Custom UI belongs in sandboxed plugin views.
- UI contributions cannot add editor block schemas, terminal shell access, direct filesystem access, direct settings writes, or silent note/block writes.
- Plugin keybindings do not set native menu accelerators and fail closed when inputs, modals, settings, terminals, editors, or plugin iframes have focus.