Hide the utility agent from profile selectors

Centralize omission of the internal default profile in the shared presentation catalog and keep status reporting intact for existing chats. Align Settings, model-switcher, Connector, Telegram, and selector contracts with focused regressions.

Alessandro committed Aug 11, 2026 at 14:22 UTC 9dab4f2460cf4e1ead2188430c3ed8fc4aed478c
12 files changed +71 -14
api/agents.py.dox.md
+2
@@ -20,6 +20,8 @@
20 - Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
21 - `Agents` is an `ApiHandler`.
22 - `Agents` defines `process(...)`.
23 +- The response uses the shared `subagents.get_all_agents_list()` presentation
24 + catalog; profile visibility rules belong to that helper.
25 - Imported dependency areas include: `helpers`, `helpers.api`.
26
27 ## Key Concepts
helpers/integration_commands.py.dox.md
+3 -1
@@ -20,7 +20,9 @@
20 - Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
21 - Observed side-effect areas: filesystem writes, model calls, plugin state, settings/state persistence.
22 - Imported dependency areas include: `__future__`, `helpers`, `helpers.persist_chat`, `helpers.state_monitor_integration`, `plugins._model_config.helpers`, `re`, `typing`.
23 -- `/agent` switches the top-level chat profile and preserves existing subordinate agent profiles.
23 +- `/agent` switches the top-level chat profile and preserves existing
24 + subordinate agent profiles. Choices come from the shared presentation
25 + catalog, though status may report an existing chat using the utility profile.
26 - `/model <preset>` stores a per-chat global preset reference, `/model inherit` clears it, and status always reports the effective scoped-or-chat preset. `Default` is a real selectable preset, not an alias for clearing the chat selection.
27
28 ## Key Concepts
helpers/settings.py
+2 -1
@@ -263,6 +263,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
263 for key, item in sorted(
264 subagents.get_available_agents_dict(None).items()
265 )
266 + if key != "default"
267 ],
268 knowledge_subdirs=[{"value": subdir, "label": subdir}
269 for subdir in files.get_subdirectories("knowledge", exclude="default")],
@@ -288,7 +289,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
289 }
290
291 current_profile = current.get("agent_profile")
291 - if current_profile and not any(
292 + if current_profile and current_profile != "default" and not any(
293 option["value"] == current_profile
294 for option in additional["agent_subdirs"]
295 ):
helpers/settings.py.dox.md
+2 -1
@@ -69,7 +69,8 @@
69 - `ui_control_visibility` stores validated mobile and desktop visibility flags for the project selector, clock, connection status, and right canvas rail; missing or malformed values fall back per device.
70 - The Global default-profile selector lists only globally available profiles.
71 A currently configured unavailable profile remains visible with an explicit
72 - unavailable label so settings can round-trip it truthfully.
72 + unavailable label so settings can round-trip it truthfully, except that the
73 + exact `default` utility profile is never offered as a selectable option.
74 - Keep request/response, tool, or helper semantics documented here at the same time as source changes.
75
76 ## Work Guidance
helpers/subagents.py
+5 -5
@@ -300,11 +300,11 @@ def get_all_agents_list() -> list[dict[str, str]]:
300 else:
301 merged[name] = item
302
303 - result: list[dict[str, str]] = []
304 - for key in sorted(merged.keys()):
305 - item = merged[key]
306 - result.append({"key": key, "label": item.title or key})
307 - return result
303 + return [
304 + {"key": key, "label": item.title or key}
305 + for key, item in sorted(merged.items())
306 + if key != "default"
307 + ]
308
309
310 def get_default_promp_file_names() -> list[str]:
helpers/subagents.py.dox.md
+2
@@ -51,6 +51,8 @@
51 title fallbacks do not become authored overrides.
52 - Available-profile resolution includes definitions from the selected project,
53 then applies that project's sparse `agents.json` availability overrides.
54 +- `get_all_agents_list()` is the shared presentation catalog and omits the exact
55 + `default` utility profile. Runtime discovery and loading remain unchanged.
56 - Bundled directories require an authored profile definition; the `_example`
57 reference directory is never selectable. Every real profile, including
58 `Default`, follows the same Global and project availability rules.
plugins/_a0_connector/AGENTS.md
+2
@@ -56,6 +56,8 @@
56 - Model preset definitions exposed through v1 are global; project arguments select scope but never create project-owned definitions. Model switcher state reports the effective main, utility, and embedding models and preserves embedding-change notifications.
57 - The protected v1 `agent_editor` route delegates to the bundled Agent Editor
58 API and must not define another profile schema or write profile files itself.
59 +- The protected v1 `agents_list` response uses the shared agent presentation
60 + catalog rather than applying connector-specific visibility rules.
61 - Computer Use receipts describe transport success unless the connector returns explicit effect evidence. Linux target-bound typing requires a verified active/focused `window_id`; window activation uses focus, never a press action on an application or window node. Do not retry an identical failed Computer Use call.
62
63 ## Work Guidance
plugins/_model_config/AGENTS.md
+5 -1
@@ -28,7 +28,11 @@
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 adjacent agent-profile selector reads the always-enabled Agent Editor list endpoint directly so the active profile shows its effective title and avatar, and omits profiles disabled in the chat's current scope.
31 +- The adjacent agent-profile selector reads the always-enabled Agent Editor list
32 + endpoint directly so the active profile shows its effective title and avatar,
33 + and omits profiles disabled in the chat's current scope plus the exact
34 + `default` utility profile. A chat already using `default` may still show that
35 + current status without adding a selectable or editable row.
36 - Reload the agent-profile selector catalog when a chat changes project or
37 active profile so project-only profiles never linger in the visible choices.
38 - When forced agent-profile catalog loads overlap, only the newest request may
plugins/_model_config/webui/switcher-mixin.js
+2 -2
@@ -51,7 +51,7 @@ export const switcherMethods = {
51 });
52 if (requestSeq !== this.agentProfilesLoadSeq) return this.agentProfiles;
53 this.agentProfiles = (data.profiles || [])
54 - .filter(profile => profile.id && profile.id !== "_example" && profile.enabled !== false)
54 + .filter(profile => profile.id && !["_example", "default"].includes(profile.id) && profile.enabled !== false)
55 .map(profile => ({
56 key: profile.id,
57 label: profile.title || profile.id,
@@ -127,7 +127,7 @@ export const switcherMethods = {
127
128 getAgentProfileList(activeKey = "", activeLabel = "") {
129 const profiles = [...(this.agentProfiles || [])];
130 - if (activeKey && !profiles.some(profile => profile.key === activeKey)) {
130 + if (activeKey && activeKey !== "default" && !profiles.some(profile => profile.key === activeKey)) {
131 profiles.unshift({ key: activeKey, label: activeLabel || activeKey });
132 }
133 return profiles;
plugins/_telegram_integration/AGENTS.md
+4 -1
@@ -16,7 +16,10 @@
16 - Treat bot tokens, chat IDs, attachments, and user data as sensitive.
17 - Keep allowed-user, group-mode, project, model, and `/send` controls enforced.
18 - Install Telegram dependencies into the framework runtime only when required.
19 -- Agent profile picker actions change the top-level chat profile and must preserve existing subordinate agent profiles.
19 +- Agent profile picker actions change the top-level chat profile and must
20 + preserve existing subordinate agent profiles. Picker rows and direct matches
21 + use the shared presentation catalog while current status may still report an
22 + existing chat that uses the utility profile.
23 - Model picker status shows the effective preset; clearing a chat override returns to its scoped preset rather than assuming `Default`.
24
25 ## Work Guidance
tests/test_agent_editor.py
+30 -2
@@ -1,5 +1,6 @@
1 from __future__ import annotations
2
3 +import asyncio
4 from io import BytesIO
5 import json
6 from pathlib import Path
@@ -1082,7 +1083,10 @@ def test_settings_default_profile_catalog_uses_global_availability(
1083 lambda _project: {
1084 "default": settings.subagents.SubAgentListItem(
1085 name="default", title="Default"
1085 - )
1086 + ),
1087 + "agent0": settings.subagents.SubAgentListItem(
1088 + name="agent0", title="Agent 0"
1089 + ),
1090 },
1091 )
1092 configured = settings.get_default_settings().copy()
@@ -1091,7 +1095,7 @@ def test_settings_default_profile_catalog_uses_global_availability(
1095 options = settings.convert_out(configured)["additional"]["agent_subdirs"]
1096
1097 assert options == [
1094 - {"value": "default", "label": "Default"},
1098 + {"value": "agent0", "label": "Agent 0"},
1099 {
1100 "value": "disabled-profile",
1101 "label": "disabled-profile (unavailable)",
@@ -1099,6 +1103,30 @@ def test_settings_default_profile_catalog_uses_global_availability(
1103 ]
1104
1105
1106 +def test_shared_profile_catalog_drives_generic_and_web_selectors(
1107 + monkeypatch: pytest.MonkeyPatch,
1108 +) -> None:
1109 + from api import agents
1110 + from helpers import integration_commands, subagents
1111 +
1112 + profiles = [{"key": "agent0", "label": "Agent 0"}]
1113 + monkeypatch.setattr(subagents, "get_all_agents_list", lambda: profiles)
1114 +
1115 + handler = agents.Agents.__new__(agents.Agents)
1116 + response = asyncio.run(handler.process({"action": "list"}, None))
1117 + assert [item["key"] for item in response["data"]] == ["agent0"]
1118 +
1119 + context = SimpleNamespace(
1120 + agent0=SimpleNamespace(config=SimpleNamespace(profile="default")),
1121 + is_running=lambda: False,
1122 + )
1123 + status = integration_commands._handle_agent(context, "")
1124 + assert "Current agent: default" in status
1125 + assert "Agent 0 (agent0)" in status
1126 + assert "Default (default)" not in status
1127 + assert "was not found" in integration_commands._handle_agent(context, "default")
1128 +
1129 +
1130 @pytest.mark.parametrize(
1131 ("relative_path", "patch", "label"),
1132 (
tests/test_subagent_metadata_merge.py
+12
@@ -132,3 +132,15 @@ def test_available_agents_include_project_profiles_and_project_overrides(
132
133 assert requested == ["demo"]
134 assert list(available) == ["project-only"]
135 +
136 +
137 +def test_all_agents_list_omits_default_utility_profile(
138 + tmp_path: Path, monkeypatch
139 +) -> None:
140 + root = tmp_path / "agents"
141 + _write_profile(root, "default", "title: Default\n")
142 + _write_profile(root, "agent0", "title: Agent 0\n")
143 + monkeypatch.setattr(subagents, "get_agents_roots", lambda: [str(root)])
144 +
145 + assert "default" in subagents._get_agents_list_from_dir(str(root), "default")
146 + assert subagents.get_all_agents_list() == [{"key": "agent0", "label": "Agent 0"}]