Add WebUI slots for plugin controls
Expose extension slots beside the model/profile strip and at the end of Interface visibility controls. Let plugin stores register responsive visibility defaults without coupling shared WebUI components to a specific affordance.
Alessandro committed
Aug 23, 2026 at 03:28 UTC
fe86e99a3446a21ade684c4b7c7066c9ae22e5af
6 files changed
+37
-2
plugins/_model_config/AGENTS.md
+2
@@ -28,6 +28,8 @@
28
- `modelConfig.createPresetEditor()` owns local preset drafts, row actions, and stable UI-only row keys so deletion or renaming cannot rebind nested model fields.
29
- The preset editor maps each model provider's API-key field to the shared API-key store; saving the editor persists dirty keys separately and never writes secrets into preset YAML.
30
- The compact chat selector label combines the effective preset with only the leaf name of its main model; utility and provider text stay out of the closed selector.
31
+- The compact selector strip exposes `model-context-strip-end` after the agent
32
+ profile selector so adjacent bundled controls can stay plugin-owned.
33
- The adjacent agent-profile selector reads the always-enabled Agent Editor list
34
endpoint directly so the active profile shows its effective title and avatar,
35
and omits profiles disabled in the chat's current scope plus the exact
plugins/_model_config/extensions/webui/chat-input-progress-start/model-switcher.html
+7
@@ -177,6 +177,8 @@
177
</div>
178
</div>
179
</template>
180
+
181
+ <x-extension id="model-context-strip-end"></x-extension>
182
</div>
183
</div>
184
</template>
@@ -196,6 +198,11 @@
198
min-width: 0;
199
flex-wrap: wrap;
200
}
201
+ .model-context-strip > x-extension,
202
+ .model-context-strip > x-extension > x-component,
203
+ .model-context-strip > x-extension > x-component > div[x-data] {
204
+ display: contents;
205
+ }
206
.agent-profile-avatar {
207
width: 1.35rem;
208
height: 1.35rem;
tests/test_ui_control_visibility.py
+3
@@ -25,6 +25,9 @@ def test_ui_controls_have_independent_mobile_and_desktop_visibility() -> None:
25
assert control in preferences
26
assert control in settings_store
27
28
+ assert "registerUiControlVisibility" in preferences
29
+ assert 'id="interface-controls-end"' in interface
30
+
31
assert "section-interface" in settings_store
32
assert 'settings/agent/interface.html' in settings
33
assert "smartphone" in interface
webui/components/settings/AGENTS.md
+3
@@ -18,6 +18,9 @@
18
- Do not store secrets in localStorage, URLs, or console output.
19
- Preserve Store Gating and modal footer conventions in settings components.
20
- Interface control visibility is edited as a Save/Cancel draft, persisted with instance settings, and applied through the shared frontend preference store after Settings saves successfully.
21
+- Bundled controls contributed by plugins add their Interface row through
22
+ `interface-controls-end` and register visibility defaults with the shared
23
+ preference store.
24
- MCP manager tool toggles write `disabled_tools` into the draft JSON and require Apply before changing the running MCP tool set.
25
- Confirmed MCP server removals apply immediately and refresh server status; other MCP manager draft edits still require Apply.
26
- MCP manager local command forms accept shell-style command and argument lines; quote argument values that intentionally contain spaces.
webui/components/settings/agent/interface.html
+7
@@ -51,6 +51,7 @@
51
</div>
52
</div>
53
</template>
54
+ <x-extension id="interface-controls-end"></x-extension>
55
</div>
56
</div>
57
</template>
@@ -80,6 +81,12 @@
81
border-bottom: 0;
82
}
83
84
+ .ui-visibility-list > x-extension,
85
+ .ui-visibility-list > x-extension > x-component,
86
+ .ui-visibility-list > x-extension > x-component > div[x-data] {
87
+ display: contents;
88
+ }
89
+
90
.ui-visibility-control {
91
display: flex;
92
align-items: center;
webui/components/sidebar/bottom/preferences/preferences-store.js
+15
-2
@@ -2,12 +2,12 @@ import { createStore } from "/js/AlpineStore.js";
2
import { ttsService } from "/js/tts-service.js";
3
import { applyModeSteps } from "/components/messages/process-group/process-group-dom.js";
4
5
-const UI_VISIBILITY_DEFAULTS = Object.freeze({
5
+const UI_VISIBILITY_DEFAULTS = {
6
projectSelector: { mobile: true, desktop: true },
7
time: { mobile: false, desktop: true },
8
connectionStatus: { mobile: true, desktop: true },
9
rightCanvasRail: { mobile: true, desktop: true },
10
-});
10
+};
11
12
function normalizeUiVisibility(value = {}) {
13
return Object.fromEntries(
@@ -93,6 +93,19 @@ const model = {
93
_uiVisibility: normalizeUiVisibility(globalThis.runtimeInfo?.uiControlVisibility),
94
_isMobileViewport: false,
95
96
+ registerUiControlVisibility(control, defaults = {}) {
97
+ const id = String(control || "").trim();
98
+ if (!id) return;
99
+ UI_VISIBILITY_DEFAULTS[id] = {
100
+ mobile: defaults.mobile !== false,
101
+ desktop: defaults.desktop !== false,
102
+ };
103
+ this._uiVisibility = normalizeUiVisibility({
104
+ ...(globalThis.runtimeInfo?.uiControlVisibility || {}),
105
+ ...(this._uiVisibility || {}),
106
+ });
107
+ },
108
+
109
uiVisibilitySnapshot() {
110
return normalizeUiVisibility(this._uiVisibility);
111
},