diff --git a/api/agents.py.dox.md b/api/agents.py.dox.md index e7f6895ac..567e075fc 100644 --- a/api/agents.py.dox.md +++ b/api/agents.py.dox.md @@ -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 diff --git a/helpers/integration_commands.py.dox.md b/helpers/integration_commands.py.dox.md index ec3266fb0..86795ec28 100644 --- a/helpers/integration_commands.py.dox.md +++ b/helpers/integration_commands.py.dox.md @@ -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 ` 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 diff --git a/helpers/settings.py b/helpers/settings.py index de7b50bb0..9eabff9ee 100644 --- a/helpers/settings.py +++ b/helpers/settings.py @@ -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"] ): diff --git a/helpers/settings.py.dox.md b/helpers/settings.py.dox.md index 413f267f9..aaf5b3812 100644 --- a/helpers/settings.py.dox.md +++ b/helpers/settings.py.dox.md @@ -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 diff --git a/helpers/subagents.py b/helpers/subagents.py index db4de677a..dbb9575f4 100644 --- a/helpers/subagents.py +++ b/helpers/subagents.py @@ -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]: diff --git a/helpers/subagents.py.dox.md b/helpers/subagents.py.dox.md index 03830b28e..4ae967cb8 100644 --- a/helpers/subagents.py.dox.md +++ b/helpers/subagents.py.dox.md @@ -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. diff --git a/plugins/_a0_connector/AGENTS.md b/plugins/_a0_connector/AGENTS.md index 462af9153..26404965d 100644 --- a/plugins/_a0_connector/AGENTS.md +++ b/plugins/_a0_connector/AGENTS.md @@ -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 diff --git a/plugins/_model_config/AGENTS.md b/plugins/_model_config/AGENTS.md index 6c2abc172..a50814fd2 100644 --- a/plugins/_model_config/AGENTS.md +++ b/plugins/_model_config/AGENTS.md @@ -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 diff --git a/plugins/_model_config/webui/switcher-mixin.js b/plugins/_model_config/webui/switcher-mixin.js index d4df9fa42..9ec17dcf8 100644 --- a/plugins/_model_config/webui/switcher-mixin.js +++ b/plugins/_model_config/webui/switcher-mixin.js @@ -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; diff --git a/plugins/_telegram_integration/AGENTS.md b/plugins/_telegram_integration/AGENTS.md index 3b36cb663..256ac1ee6 100644 --- a/plugins/_telegram_integration/AGENTS.md +++ b/plugins/_telegram_integration/AGENTS.md @@ -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 diff --git a/tests/test_agent_editor.py b/tests/test_agent_editor.py index 1cf70bf24..fd0025127 100644 --- a/tests/test_agent_editor.py +++ b/tests/test_agent_editor.py @@ -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"), ( diff --git a/tests/test_subagent_metadata_merge.py b/tests/test_subagent_metadata_merge.py index db82950d3..459653d3f 100644 --- a/tests/test_subagent_metadata_merge.py +++ b/tests/test_subagent_metadata_merge.py @@ -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"}]