feat: plugins config APIs

This commit is contained in:
peachismomo
2026-07-12 16:55:04 +08:00
parent a00fac9b72
commit ce21a09cb1
8 changed files with 224 additions and 10 deletions

View File

@@ -151,16 +151,27 @@
<div id="configEditor" class="config-editor" hidden>
<textarea id="configText" class="config-textarea thin-scroll" spellcheck="false"
autocomplete="off" autocapitalize="off" aria-label="Capability configuration (JSON)"></textarea>
<div class="config-editor-footer">
<span id="configValidation" class="config-validation" role="status" aria-live="polite"></span>
<button id="configSaveBtn" class="ButtonStyleConfirm ButtonTypeChoice" type="button">Save</button>
</div>
</div>
<!-- Custom capability UI. Sandboxed without allow-same-origin, so the plugin's HTML
runs in an opaque origin: it cannot touch this page, and the only host surface
it gets is the window.orca getConfig/saveConfig bridge injected into srcdoc. -->
<iframe id="configCustom" class="config-custom" title="Plugin configuration"
sandbox="allow-scripts" referrerpolicy="no-referrer" hidden></iframe>
<!-- Host chrome for both editors, not just the JSON one: a capability with a custom
UI needs Restore just as much, and keeping it here leaves it out of the plugin's
HTML and off the JS bridge. Save and the validation message belong to the JSON
editor alone (a custom UI saves through its own controls), so they are hidden
when a custom UI is showing. -->
<div id="configFooter" class="config-view-footer" hidden>
<span id="configValidation" class="config-validation" role="status" aria-live="polite"></span>
<div class="config-actions">
<button id="configRestoreBtn" class="ButtonStyleRegular ButtonTypeChoice" type="button"
title="Discard the settings saved for this capability and restore the plugin's defaults">
Restore defaults
</button>
<button id="configSaveBtn" class="ButtonStyleConfirm ButtonTypeChoice" type="button">Save</button>
</div>
</div>
</div>
</div>
</section>

View File

@@ -70,7 +70,15 @@ function OnInit() {
document.getElementById("configSidebar")?.addEventListener("click", OnConfigSidebarClick);
document.getElementById("configSaveBtn")?.addEventListener("click", SaveCapabilityConfig);
document.getElementById("configText")?.addEventListener("input", ValidateConfigText);
document.getElementById("configRestoreBtn")?.addEventListener("click", RestoreCapabilityConfig);
const configText = document.getElementById("configText");
// why: common.js installs a document-level onkeydown that cancels the default action of every key
// (returnValue=false) to block webview shortcuts; on the way up it also swallows typing. Stop
// the editor's keydowns from bubbling to it so the textarea stays editable, leaving the global
// guard intact. Same treatment as the search field (see plugin-search.js).
configText?.addEventListener("keydown", (event) => event.stopPropagation());
configText?.addEventListener("input", ValidateConfigText);
// The custom capability UI is sandboxed into an opaque origin, so it reaches us only through
// postMessage. Match on the frame's own contentWindow rather than the origin (which is "null"
// for a sandboxed frame) and ignore anything else on the channel.
@@ -939,12 +947,14 @@ function RequestCapabilityConfig() {
}
// Empties both editors, so nothing from the previously selected capability can linger while the
// next one is still in flight.
// next one is still in flight. The footer goes with them: until a config has actually loaded there
// is nothing to save or restore.
function ClearCapabilityConfigView() {
const editor = document.getElementById("configEditor");
const custom = document.getElementById("configCustom");
const text = document.getElementById("configText");
const error = document.getElementById("configError");
const footer = document.getElementById("configFooter");
if (editor)
editor.hidden = true;
@@ -958,6 +968,8 @@ function ClearCapabilityConfigView() {
error.hidden = true;
error.textContent = "";
}
if (footer)
footer.hidden = true;
SetConfigValidation("");
}
@@ -986,6 +998,10 @@ function ApplyCapabilityConfig(payload) {
const config = (payload && typeof payload.config === "object" && payload.config !== null) ? payload.config : {};
const html = String(payload?.custom_html || "");
// Restore is host chrome and applies to either editor; Save and the validation message belong to
// the JSON editor, since a custom UI saves through its own controls via the bridge.
ShowConfigFooter(!html);
if (html) {
// A capability with its own UI: hand it the config through the bridge, never the raw file.
if (custom) {
@@ -1010,6 +1026,21 @@ function ApplyCapabilityConfig(payload) {
SetConfigValidation("");
}
// Reveals the footer for the loaded capability. `withEditorControls` is false for a custom UI,
// leaving Restore on its own.
function ShowConfigFooter(withEditorControls) {
const footer = document.getElementById("configFooter");
const save = document.getElementById("configSaveBtn");
const validation = document.getElementById("configValidation");
if (footer)
footer.hidden = false;
if (save)
save.hidden = !withEditorControls;
if (validation)
validation.hidden = !withEditorControls;
}
function SetConfigValidation(message) {
const node = document.getElementById("configValidation");
const save = document.getElementById("configSaveBtn");
@@ -1054,6 +1085,21 @@ function SaveCapabilityConfig() {
});
}
// Asks the native side to write the capability's default config over whatever is stored. The
// defaults come from the capability's get_default_config(), never from this page — the host does not
// know what a given plugin considers default. The native side confirms before discarding anything,
// and replies with the same "saved" payload, so both editors reload from what was persisted.
function RestoreCapabilityConfig() {
if (!selectedPluginId || !selectedCapabilityName)
return;
SendMessage("restore_capability_config", {
plugin_key: selectedPluginId,
capability_name: selectedCapabilityName,
capability_type: selectedCapabilityType
});
}
function ApplyCapabilityConfigSaved(payload) {
if (!IsCurrentCapability(payload))
return;

View File

@@ -1182,9 +1182,9 @@ body {
min-height: 0;
padding: 8px;
box-sizing: border-box;
/* common.css applies `user-select: none` to *, which in WebKit also stops a textarea taking a
caret — the editor would be focusable but impossible to type into. Editable surfaces have to
opt back in (same as include/xterm/xterm.css does for the terminal). */
/* common.css applies `user-select: none` to *, so without this the user could type into the
editor but not select, drag or copy what they had typed. (What blocks typing is the global
onkeydown guard in common.js — see the keydown handler in index.js.) */
-webkit-user-select: text;
user-select: text;
border: 1px solid var(--border);
@@ -1204,13 +1204,29 @@ body {
border-color: var(--main-color);
}
.config-editor-footer {
.config-view-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.config-view-footer[hidden] {
display: none;
}
/* Restore sits immediately left of Save, both pinned right; the validation message takes the
remaining space on the left. */
.config-actions {
display: flex;
align-items: center;
gap: 8px;
}
.config-actions > button[hidden] {
display: none;
}
.config-validation {
color: var(--muted);
font-size: 11px;