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.
This commit is contained in:
Alessandro 2026-08-11 14:22:14 +02:00
parent 2a2890d892
commit 9dab4f2460
12 changed files with 71 additions and 14 deletions

View file

@ -20,6 +20,8 @@
- Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change.
- `Agents` is an `ApiHandler`.
- `Agents` defines `process(...)`.
- The response uses the shared `subagents.get_all_agents_list()` presentation
catalog; profile visibility rules belong to that helper.
- Imported dependency areas include: `helpers`, `helpers.api`.
## Key Concepts

View file

@ -20,7 +20,9 @@
- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
- Observed side-effect areas: filesystem writes, model calls, plugin state, settings/state persistence.
- Imported dependency areas include: `__future__`, `helpers`, `helpers.persist_chat`, `helpers.state_monitor_integration`, `plugins._model_config.helpers`, `re`, `typing`.
- `/agent` switches the top-level chat profile and preserves existing subordinate agent profiles.
- `/agent` switches the top-level chat profile and preserves existing
subordinate agent profiles. Choices come from the shared presentation
catalog, though status may report an existing chat using the utility profile.
- `/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.
## Key Concepts

View file

@ -263,6 +263,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
for key, item in sorted(
subagents.get_available_agents_dict(None).items()
)
if key != "default"
],
knowledge_subdirs=[{"value": subdir, "label": subdir}
for subdir in files.get_subdirectories("knowledge", exclude="default")],
@ -288,7 +289,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
}
current_profile = current.get("agent_profile")
if current_profile and not any(
if current_profile and current_profile != "default" and not any(
option["value"] == current_profile
for option in additional["agent_subdirs"]
):

View file

@ -69,7 +69,8 @@
- `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.
- The Global default-profile selector lists only globally available profiles.
A currently configured unavailable profile remains visible with an explicit
unavailable label so settings can round-trip it truthfully.
unavailable label so settings can round-trip it truthfully, except that the
exact `default` utility profile is never offered as a selectable option.
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
## Work Guidance

View file

@ -300,11 +300,11 @@ def get_all_agents_list() -> list[dict[str, str]]:
else:
merged[name] = item
result: list[dict[str, str]] = []
for key in sorted(merged.keys()):
item = merged[key]
result.append({"key": key, "label": item.title or key})
return result
return [
{"key": key, "label": item.title or key}
for key, item in sorted(merged.items())
if key != "default"
]
def get_default_promp_file_names() -> list[str]:

View file

@ -51,6 +51,8 @@
title fallbacks do not become authored overrides.
- Available-profile resolution includes definitions from the selected project,
then applies that project's sparse `agents.json` availability overrides.
- `get_all_agents_list()` is the shared presentation catalog and omits the exact
`default` utility profile. Runtime discovery and loading remain unchanged.
- Bundled directories require an authored profile definition; the `_example`
reference directory is never selectable. Every real profile, including
`Default`, follows the same Global and project availability rules.

View file

@ -56,6 +56,8 @@
- 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.
- The protected v1 `agent_editor` route delegates to the bundled Agent Editor
API and must not define another profile schema or write profile files itself.
- The protected v1 `agents_list` response uses the shared agent presentation
catalog rather than applying connector-specific visibility rules.
- 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.
## Work Guidance

View file

@ -28,7 +28,11 @@
- `modelConfig.createPresetEditor()` owns local preset drafts, row actions, and stable UI-only row keys so deletion or renaming cannot rebind nested model fields.
- 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.
- 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.
- 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.
- 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 plus the exact
`default` utility profile. A chat already using `default` may still show that
current status without adding a selectable or editable row.
- Reload the agent-profile selector catalog when a chat changes project or
active profile so project-only profiles never linger in the visible choices.
- When forced agent-profile catalog loads overlap, only the newest request may

View file

@ -51,7 +51,7 @@ export const switcherMethods = {
});
if (requestSeq !== this.agentProfilesLoadSeq) return this.agentProfiles;
this.agentProfiles = (data.profiles || [])
.filter(profile => profile.id && profile.id !== "_example" && profile.enabled !== false)
.filter(profile => profile.id && !["_example", "default"].includes(profile.id) && profile.enabled !== false)
.map(profile => ({
key: profile.id,
label: profile.title || profile.id,
@ -127,7 +127,7 @@ export const switcherMethods = {
getAgentProfileList(activeKey = "", activeLabel = "") {
const profiles = [...(this.agentProfiles || [])];
if (activeKey && !profiles.some(profile => profile.key === activeKey)) {
if (activeKey && activeKey !== "default" && !profiles.some(profile => profile.key === activeKey)) {
profiles.unshift({ key: activeKey, label: activeLabel || activeKey });
}
return profiles;

View file

@ -16,7 +16,10 @@
- Treat bot tokens, chat IDs, attachments, and user data as sensitive.
- Keep allowed-user, group-mode, project, model, and `/send` controls enforced.
- Install Telegram dependencies into the framework runtime only when required.
- Agent profile picker actions change the top-level chat profile and must preserve existing subordinate agent profiles.
- Agent profile picker actions change the top-level chat profile and must
preserve existing subordinate agent profiles. Picker rows and direct matches
use the shared presentation catalog while current status may still report an
existing chat that uses the utility profile.
- Model picker status shows the effective preset; clearing a chat override returns to its scoped preset rather than assuming `Default`.
## Work Guidance

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import asyncio
from io import BytesIO
import json
from pathlib import Path
@ -1082,7 +1083,10 @@ def test_settings_default_profile_catalog_uses_global_availability(
lambda _project: {
"default": settings.subagents.SubAgentListItem(
name="default", title="Default"
)
),
"agent0": settings.subagents.SubAgentListItem(
name="agent0", title="Agent 0"
),
},
)
configured = settings.get_default_settings().copy()
@ -1091,7 +1095,7 @@ def test_settings_default_profile_catalog_uses_global_availability(
options = settings.convert_out(configured)["additional"]["agent_subdirs"]
assert options == [
{"value": "default", "label": "Default"},
{"value": "agent0", "label": "Agent 0"},
{
"value": "disabled-profile",
"label": "disabled-profile (unavailable)",
@ -1099,6 +1103,30 @@ def test_settings_default_profile_catalog_uses_global_availability(
]
def test_shared_profile_catalog_drives_generic_and_web_selectors(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from api import agents
from helpers import integration_commands, subagents
profiles = [{"key": "agent0", "label": "Agent 0"}]
monkeypatch.setattr(subagents, "get_all_agents_list", lambda: profiles)
handler = agents.Agents.__new__(agents.Agents)
response = asyncio.run(handler.process({"action": "list"}, None))
assert [item["key"] for item in response["data"]] == ["agent0"]
context = SimpleNamespace(
agent0=SimpleNamespace(config=SimpleNamespace(profile="default")),
is_running=lambda: False,
)
status = integration_commands._handle_agent(context, "")
assert "Current agent: default" in status
assert "Agent 0 (agent0)" in status
assert "Default (default)" not in status
assert "was not found" in integration_commands._handle_agent(context, "default")
@pytest.mark.parametrize(
("relative_path", "patch", "label"),
(

View file

@ -132,3 +132,15 @@ def test_available_agents_include_project_profiles_and_project_overrides(
assert requested == ["demo"]
assert list(available) == ["project-only"]
def test_all_agents_list_omits_default_utility_profile(
tmp_path: Path, monkeypatch
) -> None:
root = tmp_path / "agents"
_write_profile(root, "default", "title: Default\n")
_write_profile(root, "agent0", "title: Agent 0\n")
monkeypatch.setattr(subagents, "get_agents_roots", lambda: [str(root)])
assert "default" in subagents._get_agents_list_from_dir(str(root), "default")
assert subagents.get_all_agents_list() == [{"key": "agent0", "label": "Agent 0"}]