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"}]