From d45f096af63dac82ffbc4d9cca4731e8d483bf45 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 17:33:46 -0700 Subject: [PATCH 01/28] cli-26: switch agent shortcuts --- cecli/tui/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 375e03b3cc1..28f4511f33a 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -246,9 +246,9 @@ def _get_config(self): "input_end": "ctrl+end", "output_up": "shift+pageup", "output_down": "shift+pagedown", - "next_agent": "alt+ctrl+right", - "prev_agent": "alt+ctrl+left", - "main_agent": "alt+ctrl+up", + "next_agent": "shift+right", + "prev_agent": "shift+left", + "main_agent": "shift+up", "editor": "ctrl+o", "history": "ctrl+r", "focus": "ctrl+f", From ff7a4dff4c52bf352c6fae78f77b180e101cf61a Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 18:55:11 -0700 Subject: [PATCH 02/28] (no commit message provided) Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/commands/main_agent.py | 13 +++++++++++++ cecli/commands/next_agent.py | 13 +++++++++++++ cecli/commands/prev_agent.py | 13 +++++++++++++ cecli/website/docs/usage/commands.md | 3 +++ 4 files changed, 42 insertions(+) create mode 100644 cecli/commands/main_agent.py create mode 100644 cecli/commands/next_agent.py create mode 100644 cecli/commands/prev_agent.py diff --git a/cecli/commands/main_agent.py b/cecli/commands/main_agent.py new file mode 100644 index 00000000000..df5aa133eb9 --- /dev/null +++ b/cecli/commands/main_agent.py @@ -0,0 +1,13 @@ +from cecli.commands.utils.base_command import BaseCommand + + +class MainAgentCommand(BaseCommand): + NORM_NAME = "main-agent" + DESCRIPTION = "Switch to the main/primary agent." + + @classmethod + async def execute(cls, io, coder, args, **kwargs): + if not coder.tui or not coder.tui(): + io.tool_error("This command is only available in TUI mode.") + return + coder.tui().action_switch_to_primary() diff --git a/cecli/commands/next_agent.py b/cecli/commands/next_agent.py new file mode 100644 index 00000000000..77cf31e7e72 --- /dev/null +++ b/cecli/commands/next_agent.py @@ -0,0 +1,13 @@ +from cecli.commands.utils.base_command import BaseCommand + + +class NextAgentCommand(BaseCommand): + NORM_NAME = "next-agent" + DESCRIPTION = "Switch to the next agent (primary or sub-agent)." + + @classmethod + async def execute(cls, io, coder, args, **kwargs): + if not coder.tui or not coder.tui(): + io.tool_error("This command is only available in TUI mode.") + return + coder.tui().action_switch_next_agent() diff --git a/cecli/commands/prev_agent.py b/cecli/commands/prev_agent.py new file mode 100644 index 00000000000..813a93d4aa4 --- /dev/null +++ b/cecli/commands/prev_agent.py @@ -0,0 +1,13 @@ +from cecli.commands.utils.base_command import BaseCommand + + +class PrevAgentCommand(BaseCommand): + NORM_NAME = "prev-agent" + DESCRIPTION = "Switch to the previous agent (primary or sub-agent)." + + @classmethod + async def execute(cls, io, coder, args, **kwargs): + if not coder.tui or not coder.tui(): + io.tool_error("This command is only available in TUI mode.") + return + coder.tui().action_switch_prev_agent() diff --git a/cecli/website/docs/usage/commands.md b/cecli/website/docs/usage/commands.md index 2cf365f15b1..98faa4e85fe 100644 --- a/cecli/website/docs/usage/commands.md +++ b/cecli/website/docs/usage/commands.md @@ -44,12 +44,15 @@ cog.out(get_help_md()) | **/load** | Load and execute commands from a file | | **/load-mcp** | Load a MCP server by name | | **/ls** | List all known files and indicate which are included in the chat session | +| **/main-agent** | Switch to the main/primary agent. | | **/map** | Print out the current repository map | | **/map-refresh** | Force a refresh of the repository map | | **/model** | Switch the Main Model to a new LLM | | **/models** | Search the list of available models | +| **/next-agent** | Switch to the next agent (primary or sub-agent). | | **/multiline-mode** | Toggle multiline mode (swaps behavior of Enter and Meta+Enter) | | **/paste** | Paste image/text from the clipboard into the chat. Optionally provide a name for the image. | +| **/prev-agent** | Switch to the previous agent (primary or sub-agent). | | **/quit** | Exit the application | | **/read-only** | Add files to the chat that are for reference only, or turn added files to read-only | | **/reasoning-effort** | Set the reasoning effort level (values: number or low/medium/high depending on model) | From 1d4cb4bfe0bd0e436e21ade8ba8aeedd0a5d5c35 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 19:37:30 -0700 Subject: [PATCH 03/28] fix: Update keybindings for agent navigation --- cecli/tui/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 28f4511f33a..375e03b3cc1 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -246,9 +246,9 @@ def _get_config(self): "input_end": "ctrl+end", "output_up": "shift+pageup", "output_down": "shift+pagedown", - "next_agent": "shift+right", - "prev_agent": "shift+left", - "main_agent": "shift+up", + "next_agent": "alt+ctrl+right", + "prev_agent": "alt+ctrl+left", + "main_agent": "alt+ctrl+up", "editor": "ctrl+o", "history": "ctrl+r", "focus": "ctrl+f", From d538368bb82cd097e3c888463db112d10069eb8d Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 19:37:34 -0700 Subject: [PATCH 04/28] feat: Add /switch-agent command with tab completion Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/commands/switch_agent.py | 82 ++++++++++++++++++++++++++++ cecli/tui/app.py | 2 + cecli/website/docs/usage/commands.md | 1 + 3 files changed, 85 insertions(+) create mode 100644 cecli/commands/switch_agent.py diff --git a/cecli/commands/switch_agent.py b/cecli/commands/switch_agent.py new file mode 100644 index 00000000000..5d81746f7c7 --- /dev/null +++ b/cecli/commands/switch_agent.py @@ -0,0 +1,82 @@ +from typing import List + +from cecli.commands.utils.base_command import BaseCommand +from cecli.commands.utils.helpers import format_command_result +from cecli.helpers.agents.service import AgentService +from cecli.tui.io import TextualInputOutput + + +class SwitchAgentCommand(BaseCommand): + NORM_NAME = "switch-agent" + DESCRIPTION = "Switch to a specific agent by name" + + @classmethod + async def execute(cls, io, coder, args, **kwargs): + """Execute the switch-agent command.""" + agent_name = args.strip() + if not agent_name: + io.tool_error("Usage: /switch-agent ") + return 1 + + try: + agent_service = AgentService.get_instance(coder) + except Exception as e: + io.tool_error(f"Could not get agent service: {e}") + return 1 + + agent_uuid = None + + if agent_name == "primary": + agent_uuid = str(coder.uuid) + else: + if agent_service and agent_service.sub_agents: + for uuid, sub_agent_info in agent_service.sub_agents.items(): + if sub_agent_info.name == agent_name: + agent_uuid = uuid + break + + if agent_uuid is None: + io.tool_error(f"Error: Agent '{agent_name}' not found.") + return 1 + + if isinstance(io, TextualInputOutput): + io.output_queue.put({"type": "switch_agent", "uuid": agent_uuid}) + else: + # Non-TUI mode + if agent_uuid == str(coder.uuid): + agent_service.foreground_uuid = None + else: + agent_service.foreground_uuid = agent_uuid + io.tool_output(f"Switched to agent: {agent_name}") + + return format_command_result(io, "switch-agent", f"Switched to agent '{agent_name}'") + + @classmethod + def get_completions(cls, io, coder, args) -> List[str]: + """Get completion options for switch-agent command.""" + try: + agent_service = AgentService.get_instance(coder) + names = ["primary"] + if agent_service and agent_service.sub_agents: + for sub_agent_info in agent_service.sub_agents.values(): + names.append(sub_agent_info.name) + + current_arg = args.strip().lower() + if current_arg: + return [name for name in names if name.lower().startswith(current_arg)] + else: + return names + except Exception: + return ["primary"] + + @classmethod + def get_help(cls) -> str: + """Get help text for the switch-agent command.""" + help_text = super().get_help() + help_text += "\nUsage:\n" + help_text += " /switch-agent # Switch to a specific agent\n" + help_text += "\nExamples:\n" + help_text += " /switch-agent primary\n" + help_text += " /switch-agent reviewer\n" + help_text += "\nUse tab for auto-completion of agent names.\n" + return help_text diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 375e03b3cc1..16a0569d128 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -554,6 +554,8 @@ def handle_output_message(self, msg): footer = self.query_one(MainFooter) footer.update_mode(msg.get("mode", "code")) + elif msg_type == "switch_agent": + self._switch_to_container(msg["uuid"]) def add_output(self, text, task_id=None): """Add output to the output container.""" diff --git a/cecli/website/docs/usage/commands.md b/cecli/website/docs/usage/commands.md index 98faa4e85fe..cb86e3c590d 100644 --- a/cecli/website/docs/usage/commands.md +++ b/cecli/website/docs/usage/commands.md @@ -62,6 +62,7 @@ cog.out(get_help_md()) | **/run** | Run a shell command and optionally add the output to the chat (alias: !) | | **/save** | Save commands to a file that can reconstruct the current chat session's files | | **/settings** | Print out the current settings | +| **/switch-agent** | Switch to a specific agent by name | | **/test** | Run a shell command and add the output to the chat on non-zero exit code | | **/think-tokens** | Set the thinking token budget, eg: 8096, 8k, 10.5k, 0.5M, or 0 to disable. | | **/tokens** | Report on the number of tokens used by the current chat context | From a47da7480d96f92047cac692fc3d7143dc0663f4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 20:11:30 -0700 Subject: [PATCH 05/28] fix: Resolve circular import in switch_agent command Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/commands/switch_agent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cecli/commands/switch_agent.py b/cecli/commands/switch_agent.py index 5d81746f7c7..0f6587f68f9 100644 --- a/cecli/commands/switch_agent.py +++ b/cecli/commands/switch_agent.py @@ -3,7 +3,6 @@ from cecli.commands.utils.base_command import BaseCommand from cecli.commands.utils.helpers import format_command_result from cecli.helpers.agents.service import AgentService -from cecli.tui.io import TextualInputOutput class SwitchAgentCommand(BaseCommand): @@ -13,6 +12,8 @@ class SwitchAgentCommand(BaseCommand): @classmethod async def execute(cls, io, coder, args, **kwargs): """Execute the switch-agent command.""" + from cecli.tui.io import TextualInputOutput + agent_name = args.strip() if not agent_name: io.tool_error("Usage: /switch-agent ") From aec45d6239940e5d68cea0550b2dba28318b0a5c Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 20:19:48 -0700 Subject: [PATCH 06/28] feat: Add SwitchAgentCommand to command registry --- cecli/commands/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cecli/commands/__init__.py b/cecli/commands/__init__.py index 4e77ec64099..db5aac58604 100644 --- a/cecli/commands/__init__.py +++ b/cecli/commands/__init__.py @@ -65,6 +65,7 @@ from .save_session import SaveSessionCommand from .settings import SettingsCommand from .spawn_agent import SpawnAgentCommand +from .switch_agent import SwitchAgentCommand from .terminal_setup import TerminalSetupCommand from .test import TestCommand from .think_tokens import ThinkTokensCommand @@ -118,6 +119,7 @@ CommandRegistry.register(InvokeAgentCommand) CommandRegistry.register(ReapAgentCommand) CommandRegistry.register(SpawnAgentCommand) +CommandRegistry.register(SwitchAgentCommand) CommandRegistry.register(IncludeSkillCommand) CommandRegistry.register(LintCommand) CommandRegistry.register(ListSessionsCommand) @@ -199,6 +201,7 @@ "InvokeAgentCommand", "ReapAgentCommand", "SpawnAgentCommand", + "SwitchAgentCommand", "LintCommand", "ListSessionsCommand", "ListSkillsCommand", From 78c06acbba0c5181c7f7c0112d5978e21729e5ff Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 20:19:59 -0700 Subject: [PATCH 07/28] fix: Resolve circular import in switch_agent command Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/commands/switch_agent.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cecli/commands/switch_agent.py b/cecli/commands/switch_agent.py index 0f6587f68f9..98b68ef43e4 100644 --- a/cecli/commands/switch_agent.py +++ b/cecli/commands/switch_agent.py @@ -12,8 +12,6 @@ class SwitchAgentCommand(BaseCommand): @classmethod async def execute(cls, io, coder, args, **kwargs): """Execute the switch-agent command.""" - from cecli.tui.io import TextualInputOutput - agent_name = args.strip() if not agent_name: io.tool_error("Usage: /switch-agent ") @@ -40,7 +38,7 @@ async def execute(cls, io, coder, args, **kwargs): io.tool_error(f"Error: Agent '{agent_name}' not found.") return 1 - if isinstance(io, TextualInputOutput): + if hasattr(io, "output_queue") and io.output_queue: io.output_queue.put({"type": "switch_agent", "uuid": agent_uuid}) else: # Non-TUI mode From 63a21f4e6a8247694f3a67497c2cc9430bb8cae4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 20:34:46 -0700 Subject: [PATCH 08/28] fix: Improve agent switching robustness in TUI Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/tui/app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 16a0569d128..cfb29102ac5 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -978,6 +978,12 @@ def _switch_to_container(self, uuid: str) -> None: agent_service = AgentService.get_instance(self.worker.coder) primary_uuid = str(self.worker.coder.uuid) + # Check if the target container exists + if uuid != primary_uuid and uuid not in self._sub_agent_containers: + # Sub-agent container not found, fall back to primary + self.show_error(f"Agent container for UUID {uuid} not found. Switching to primary.") + uuid = primary_uuid + if uuid == primary_uuid: # Switch to primary agent agent_service.foreground_uuid = None From 4f95e10392d17b7666755a2aaa4f92cb7a82751f Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 20:42:49 -0700 Subject: [PATCH 09/28] feat: Add tag to ConversationService add_message call Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/commands/invoke_agent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cecli/commands/invoke_agent.py b/cecli/commands/invoke_agent.py index 6b14c8caf91..d1211cf31d7 100644 --- a/cecli/commands/invoke_agent.py +++ b/cecli/commands/invoke_agent.py @@ -25,9 +25,11 @@ async def execute(cls, io, coder, args, **kwargs): summary = await agent_service.invoke(name, prompt, blocking=True) if summary: from cecli.helpers.conversation.service import ConversationService + from cecli.helpers.conversation.tags import MessageTag ConversationService.get_manager(coder).add_message( message_dict=dict(role="user", content=summary), + tag=MessageTag.CUR, ) io.tool_output(f"Sub-agent '{name}' completed:\n{summary}") else: From 3883a5287975087ca464aeef996d7594b5c9ead4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 20:47:23 -0700 Subject: [PATCH 10/28] refactor: Improve agent switching logic and completions Co-authored-by: cecli (openai/nvidia_nim/deepseek-ai/deepseek-v4-pro) --- cecli/commands/switch_agent.py | 17 ++++++++++++++--- cecli/tui/app.py | 8 +++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/cecli/commands/switch_agent.py b/cecli/commands/switch_agent.py index 98b68ef43e4..9a5d0ec7dcb 100644 --- a/cecli/commands/switch_agent.py +++ b/cecli/commands/switch_agent.py @@ -55,10 +55,21 @@ def get_completions(cls, io, coder, args) -> List[str]: """Get completion options for switch-agent command.""" try: agent_service = AgentService.get_instance(coder) - names = ["primary"] + names = [] + + # Determine current foreground agent + foreground_uuid = agent_service.foreground_uuid + primary_uuid = str(coder.uuid) + + # Add "primary" only if not already on primary + if foreground_uuid is not None: + names.append("primary") + + # Add sub-agent names, excluding the currently active one if agent_service and agent_service.sub_agents: - for sub_agent_info in agent_service.sub_agents.values(): - names.append(sub_agent_info.name) + for uuid, sub_agent_info in agent_service.sub_agents.items(): + if uuid != foreground_uuid: + names.append(sub_agent_info.name) current_arg = args.strip().lower() if current_arg: diff --git a/cecli/tui/app.py b/cecli/tui/app.py index cfb29102ac5..b7dd7cc279b 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -555,7 +555,13 @@ def handle_output_message(self, msg): footer = self.query_one(MainFooter) footer.update_mode(msg.get("mode", "code")) elif msg_type == "switch_agent": - self._switch_to_container(msg["uuid"]) + target_uuid = msg["uuid"] + # Ensure the target container exists before switching + primary_uuid = str(self.worker.coder.uuid) + if target_uuid != primary_uuid and target_uuid not in self._sub_agent_containers: + self.show_error(f"Agent container not found. Cannot switch.") + else: + self._switch_to_container(target_uuid) def add_output(self, text, task_id=None): """Add output to the output container.""" From ded0451338861ca92b5fbe4788a90b609dc4792b Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 20:52:36 -0700 Subject: [PATCH 11/28] fix: Ensure agent switching updates UI on main thread Co-authored-by: cecli (openai/nvidia_nim/deepseek-ai/deepseek-v4-pro) --- cecli/tui/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cecli/tui/app.py b/cecli/tui/app.py index b7dd7cc279b..5c8909dd42d 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -561,7 +561,8 @@ def handle_output_message(self, msg): if target_uuid != primary_uuid and target_uuid not in self._sub_agent_containers: self.show_error(f"Agent container not found. Cannot switch.") else: - self._switch_to_container(target_uuid) + # Use call_from_thread to ensure UI updates happen on the main thread + self.call_from_thread(self._switch_to_container, target_uuid) def add_output(self, text, task_id=None): """Add output to the output container.""" From 8c745f3cd45361db0118c5b893312a6f59fba19d Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 20:59:50 -0700 Subject: [PATCH 12/28] refactor: Remove unnecessary call_from_thread in TUI Co-authored-by: cecli (openai/nvidia_nim/deepseek-ai/deepseek-v4-pro) --- cecli/tui/app.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 5c8909dd42d..5895a60f7e1 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -561,8 +561,7 @@ def handle_output_message(self, msg): if target_uuid != primary_uuid and target_uuid not in self._sub_agent_containers: self.show_error(f"Agent container not found. Cannot switch.") else: - # Use call_from_thread to ensure UI updates happen on the main thread - self.call_from_thread(self._switch_to_container, target_uuid) + self._switch_to_container(target_uuid) def add_output(self, text, task_id=None): """Add output to the output container.""" @@ -712,6 +711,43 @@ def on_input_area_submit(self, message: InputArea.Submit): self._open_editor_suspended(initial_content) return + # Intercept /switch-agent command to handle immediately without LLM processing + if stripped.startswith("/switch-agent"): + parts = stripped.split(maxsplit=1) + agent_name = parts[1].strip() if len(parts) > 1 else "" + + input_area = self.query_one("#input", InputArea) + input_area.value = "" + + if not agent_name: + self.show_error("Usage: /switch-agent ") + return + + # Resolve agent name to UUID + from cecli.helpers.agents.service import AgentService + agent_service = AgentService.get_instance(self.worker.coder) + primary_uuid = str(self.worker.coder.uuid) + + target_uuid = None + if agent_name == "primary": + target_uuid = primary_uuid + else: + for uuid, info in agent_service.sub_agents.items(): + if info.name == agent_name: + target_uuid = uuid + break + + if target_uuid is None: + self.show_error(f"Agent '{agent_name}' not found.") + return + + if target_uuid != primary_uuid and target_uuid not in self._sub_agent_containers: + self.show_error(f"Agent container for '{agent_name}' not found.") + return + + self._switch_to_container(target_uuid) + return + # Save to history before clearing input_area = self.query_one("#input", InputArea) input_area.save_to_history(user_input) From a18de88354c87fa37921aef1c5e3406206199c0c Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 21:02:40 -0700 Subject: [PATCH 13/28] refactor: Move AgentService import to top of on_input_area_submit Co-authored-by: cecli (openai/nvidia_nim/deepseek-ai/deepseek-v4-pro) --- cecli/tui/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 5895a60f7e1..b6ca8c27f50 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -686,6 +686,8 @@ def on_input_area_text_changed(self, message: InputArea.TextChanged): def on_input_area_submit(self, message: InputArea.Submit): """Handle input submission.""" + from cecli.helpers.agents.service import AgentService + user_input = message.value if not user_input.strip(): @@ -724,7 +726,6 @@ def on_input_area_submit(self, message: InputArea.Submit): return # Resolve agent name to UUID - from cecli.helpers.agents.service import AgentService agent_service = AgentService.get_instance(self.worker.coder) primary_uuid = str(self.worker.coder.uuid) From 1b8b81ca35204c8a9510a0ccaf43f00132601231 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 21:57:13 -0700 Subject: [PATCH 14/28] test: add tests for switch_agent command Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- tests/commands/test_switch_agent.py | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/commands/test_switch_agent.py diff --git a/tests/commands/test_switch_agent.py b/tests/commands/test_switch_agent.py new file mode 100644 index 00000000000..a67cd9fcf7d --- /dev/null +++ b/tests/commands/test_switch_agent.py @@ -0,0 +1,88 @@ +import asyncio +from unittest.mock import MagicMock, patch + +import pytest + +from cecli.commands.switch_agent import SwitchAgentCommand + + +@pytest.fixture +def mock_coder(): + coder = MagicMock() + coder.uuid = "primary-uuid" + return coder + + +@pytest.fixture +def mock_io(): + io = MagicMock() + io.output_queue = MagicMock() + return io + + +@pytest.fixture +def mock_agent_service(mock_coder): + with patch("cecli.commands.switch_agent.AgentService") as MockAgentService: + agent_service_instance = MockAgentService.get_instance.return_value + agent_service_instance.sub_agents = { + "sub-uuid-1": MagicMock(name="reviewer"), + } + agent_service_instance.foreground_uuid = None + yield agent_service_instance + + +class TestSwitchAgentCommand: + @pytest.mark.asyncio + async def test_execute_switch_to_sub_agent_tui( + self, mock_coder, mock_io, mock_agent_service + ): + """Test switching to a sub-agent in TUI mode.""" + mock_io.output_queue.put = MagicMock() + + with patch("cecli.commands.switch_agent.hasattr", return_value=True): + await SwitchAgentCommand.execute(mock_io, mock_coder, "reviewer") + + mock_io.output_queue.put.assert_called_once_with( + {"type": "switch_agent", "uuid": "sub-uuid-1"} + ) + + @pytest.mark.asyncio + async def test_execute_switch_to_primary_tui( + self, mock_coder, mock_io, mock_agent_service + ): + """Test switching back to the primary agent in TUI mode.""" + mock_agent_service.foreground_uuid = "sub-uuid-1" + mock_io.output_queue.put = MagicMock() + + with patch("cecli.commands.switch_agent.hasattr", return_value=True): + await SwitchAgentCommand.execute(mock_io, mock_coder, "primary") + + mock_io.output_queue.put.assert_called_once_with( + {"type": "switch_agent", "uuid": "primary-uuid"} + ) + + @pytest.mark.asyncio + async def test_execute_agent_not_found(self, mock_coder, mock_io, mock_agent_service): + """Test error handling when agent is not found.""" + await SwitchAgentCommand.execute(mock_io, mock_coder, "non-existent-agent") + mock_io.tool_error.assert_called_once_with("Error: Agent 'non-existent-agent' not found.") + + def test_get_completions_on_primary(self, mock_coder, mock_io, mock_agent_service): + """Test completions when the primary agent is active.""" + mock_agent_service.foreground_uuid = None + completions = SwitchAgentCommand.get_completions(mock_io, mock_coder, "") + assert "reviewer" in completions + assert "primary" not in completions + + def test_get_completions_on_sub_agent(self, mock_coder, mock_io, mock_agent_service): + """Test completions when a sub-agent is active.""" + mock_agent_service.foreground_uuid = "sub-uuid-1" + completions = SwitchAgentCommand.get_completions(mock_io, mock_coder, "") + assert "primary" in completions + assert "reviewer" not in completions + + def test_get_completions_with_partial_arg(self, mock_coder, mock_io, mock_agent_service): + """Test completions with a partial argument.""" + mock_agent_service.foreground_uuid = None + completions = SwitchAgentCommand.get_completions(mock_io, mock_coder, "rev") + assert completions == ["reviewer"] From 27e916ff9a9d1a2a29773c5950f14193befbd1d2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 22:41:08 -0700 Subject: [PATCH 15/28] feat(agent): Add /switch-agent command and Ctrl+Shift shortcuts --- cecli/commands/main_agent.py | 13 ------------- cecli/commands/next_agent.py | 13 ------------- cecli/commands/prev_agent.py | 13 ------------- cecli/commands/switch_agent.py | 6 +++--- cecli/tui/app.py | 14 +++++++------- 5 files changed, 10 insertions(+), 49 deletions(-) delete mode 100644 cecli/commands/main_agent.py delete mode 100644 cecli/commands/next_agent.py delete mode 100644 cecli/commands/prev_agent.py diff --git a/cecli/commands/main_agent.py b/cecli/commands/main_agent.py deleted file mode 100644 index df5aa133eb9..00000000000 --- a/cecli/commands/main_agent.py +++ /dev/null @@ -1,13 +0,0 @@ -from cecli.commands.utils.base_command import BaseCommand - - -class MainAgentCommand(BaseCommand): - NORM_NAME = "main-agent" - DESCRIPTION = "Switch to the main/primary agent." - - @classmethod - async def execute(cls, io, coder, args, **kwargs): - if not coder.tui or not coder.tui(): - io.tool_error("This command is only available in TUI mode.") - return - coder.tui().action_switch_to_primary() diff --git a/cecli/commands/next_agent.py b/cecli/commands/next_agent.py deleted file mode 100644 index 77cf31e7e72..00000000000 --- a/cecli/commands/next_agent.py +++ /dev/null @@ -1,13 +0,0 @@ -from cecli.commands.utils.base_command import BaseCommand - - -class NextAgentCommand(BaseCommand): - NORM_NAME = "next-agent" - DESCRIPTION = "Switch to the next agent (primary or sub-agent)." - - @classmethod - async def execute(cls, io, coder, args, **kwargs): - if not coder.tui or not coder.tui(): - io.tool_error("This command is only available in TUI mode.") - return - coder.tui().action_switch_next_agent() diff --git a/cecli/commands/prev_agent.py b/cecli/commands/prev_agent.py deleted file mode 100644 index 813a93d4aa4..00000000000 --- a/cecli/commands/prev_agent.py +++ /dev/null @@ -1,13 +0,0 @@ -from cecli.commands.utils.base_command import BaseCommand - - -class PrevAgentCommand(BaseCommand): - NORM_NAME = "prev-agent" - DESCRIPTION = "Switch to the previous agent (primary or sub-agent)." - - @classmethod - async def execute(cls, io, coder, args, **kwargs): - if not coder.tui or not coder.tui(): - io.tool_error("This command is only available in TUI mode.") - return - coder.tui().action_switch_prev_agent() diff --git a/cecli/commands/switch_agent.py b/cecli/commands/switch_agent.py index 9a5d0ec7dcb..bf3ab4bd336 100644 --- a/cecli/commands/switch_agent.py +++ b/cecli/commands/switch_agent.py @@ -56,15 +56,15 @@ def get_completions(cls, io, coder, args) -> List[str]: try: agent_service = AgentService.get_instance(coder) names = [] - + # Determine current foreground agent foreground_uuid = agent_service.foreground_uuid primary_uuid = str(coder.uuid) - + # Add "primary" only if not already on primary if foreground_uuid is not None: names.append("primary") - + # Add sub-agent names, excluding the currently active one if agent_service and agent_service.sub_agents: for uuid, sub_agent_info in agent_service.sub_agents.items(): diff --git a/cecli/tui/app.py b/cecli/tui/app.py index b6ca8c27f50..418187871eb 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -717,18 +717,18 @@ def on_input_area_submit(self, message: InputArea.Submit): if stripped.startswith("/switch-agent"): parts = stripped.split(maxsplit=1) agent_name = parts[1].strip() if len(parts) > 1 else "" - + input_area = self.query_one("#input", InputArea) input_area.value = "" - + if not agent_name: self.show_error("Usage: /switch-agent ") return - + # Resolve agent name to UUID agent_service = AgentService.get_instance(self.worker.coder) primary_uuid = str(self.worker.coder.uuid) - + target_uuid = None if agent_name == "primary": target_uuid = primary_uuid @@ -737,15 +737,15 @@ def on_input_area_submit(self, message: InputArea.Submit): if info.name == agent_name: target_uuid = uuid break - + if target_uuid is None: self.show_error(f"Agent '{agent_name}' not found.") return - + if target_uuid != primary_uuid and target_uuid not in self._sub_agent_containers: self.show_error(f"Agent container for '{agent_name}' not found.") return - + self._switch_to_container(target_uuid) return From 9318603f22724265ee17939d95eb8c732fb0e8e4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 22:51:30 -0700 Subject: [PATCH 16/28] (no commit message provided) --- tests/commands/test_switch_agent.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/commands/test_switch_agent.py b/tests/commands/test_switch_agent.py index a67cd9fcf7d..77f066bc647 100644 --- a/tests/commands/test_switch_agent.py +++ b/tests/commands/test_switch_agent.py @@ -33,9 +33,7 @@ def mock_agent_service(mock_coder): class TestSwitchAgentCommand: @pytest.mark.asyncio - async def test_execute_switch_to_sub_agent_tui( - self, mock_coder, mock_io, mock_agent_service - ): + async def test_execute_switch_to_sub_agent_tui(self, mock_coder, mock_io, mock_agent_service): """Test switching to a sub-agent in TUI mode.""" mock_io.output_queue.put = MagicMock() @@ -47,9 +45,7 @@ async def test_execute_switch_to_sub_agent_tui( ) @pytest.mark.asyncio - async def test_execute_switch_to_primary_tui( - self, mock_coder, mock_io, mock_agent_service - ): + async def test_execute_switch_to_primary_tui(self, mock_coder, mock_io, mock_agent_service): """Test switching back to the primary agent in TUI mode.""" mock_agent_service.foreground_uuid = "sub-uuid-1" mock_io.output_queue.put = MagicMock() From a57834d2bf54c2daafd866d6e1a1f1151325e650 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 22:51:31 -0700 Subject: [PATCH 17/28] (no commit message provided) Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/commands/switch_agent.py | 1 - cecli/tui/app.py | 2 +- tests/commands/test_switch_agent.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cecli/commands/switch_agent.py b/cecli/commands/switch_agent.py index bf3ab4bd336..8d2ab8df155 100644 --- a/cecli/commands/switch_agent.py +++ b/cecli/commands/switch_agent.py @@ -59,7 +59,6 @@ def get_completions(cls, io, coder, args) -> List[str]: # Determine current foreground agent foreground_uuid = agent_service.foreground_uuid - primary_uuid = str(coder.uuid) # Add "primary" only if not already on primary if foreground_uuid is not None: diff --git a/cecli/tui/app.py b/cecli/tui/app.py index 418187871eb..bd2e9d48ee2 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -559,7 +559,7 @@ def handle_output_message(self, msg): # Ensure the target container exists before switching primary_uuid = str(self.worker.coder.uuid) if target_uuid != primary_uuid and target_uuid not in self._sub_agent_containers: - self.show_error(f"Agent container not found. Cannot switch.") + self.show_error("Agent container not found. Cannot switch.") else: self._switch_to_container(target_uuid) diff --git a/tests/commands/test_switch_agent.py b/tests/commands/test_switch_agent.py index 77f066bc647..77bdf0fc97b 100644 --- a/tests/commands/test_switch_agent.py +++ b/tests/commands/test_switch_agent.py @@ -1,4 +1,3 @@ -import asyncio from unittest.mock import MagicMock, patch import pytest From 5404d220b51169e90a2f3c2e938cda0f1938aa51 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 22:54:40 -0700 Subject: [PATCH 18/28] cli-26: removed wrong documetnation --- cecli/website/docs/usage/commands.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/cecli/website/docs/usage/commands.md b/cecli/website/docs/usage/commands.md index cb86e3c590d..10d06994b65 100644 --- a/cecli/website/docs/usage/commands.md +++ b/cecli/website/docs/usage/commands.md @@ -44,15 +44,12 @@ cog.out(get_help_md()) | **/load** | Load and execute commands from a file | | **/load-mcp** | Load a MCP server by name | | **/ls** | List all known files and indicate which are included in the chat session | -| **/main-agent** | Switch to the main/primary agent. | | **/map** | Print out the current repository map | | **/map-refresh** | Force a refresh of the repository map | | **/model** | Switch the Main Model to a new LLM | | **/models** | Search the list of available models | -| **/next-agent** | Switch to the next agent (primary or sub-agent). | | **/multiline-mode** | Toggle multiline mode (swaps behavior of Enter and Meta+Enter) | | **/paste** | Paste image/text from the clipboard into the chat. Optionally provide a name for the image. | -| **/prev-agent** | Switch to the previous agent (primary or sub-agent). | | **/quit** | Exit the application | | **/read-only** | Add files to the chat that are for reference only, or turn added files to read-only | | **/reasoning-effort** | Set the reasoning effort level (values: number or low/medium/high depending on model) | From 1dea3f63eab84a1bb9c816719e667299a8cdd152 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 11:07:43 -0700 Subject: [PATCH 19/28] feat: Add UUID prefix to duplicate agent names in TUI Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/tui/widgets/input_container.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/cecli/tui/widgets/input_container.py b/cecli/tui/widgets/input_container.py index 850404b3f1b..78e7c874ea0 100644 --- a/cecli/tui/widgets/input_container.py +++ b/cecli/tui/widgets/input_container.py @@ -1,3 +1,5 @@ +from collections import Counter + from textual.containers import Vertical from textual.reactive import reactive @@ -49,7 +51,7 @@ def _get_sub_agents(self) -> list: """Query AgentService via self.app to build sub-agent pill data. Returns: - List of dicts with ``name``, ``active``, and ``generating`` keys, + List of dicts with ``name``, ``uuid``, ``active``, and ``generating`` keys, or empty list. """ try: @@ -61,13 +63,14 @@ def _get_sub_agents(self) -> list: agent_service = AgentService.get_instance(coder) sub_agents = [] - primary_uuid = agent_service.coder.uuid + primary_uuid = str(agent_service.coder.uuid) active_uuid = agent_service.foreground_uuid or primary_uuid # Primary is never "generating" in the sub-agent sense sub_agents.append( { "name": "primary", + "uuid": primary_uuid, "active": active_uuid == primary_uuid, "generating": is_active(getattr(coder.io, "output_task", None)), } @@ -78,6 +81,7 @@ def _get_sub_agents(self) -> list: sub_agents.append( { "name": info.name, + "uuid": coder_uuid, "active": coder_uuid == active_uuid, "generating": is_active(info.generate_task), } @@ -101,13 +105,15 @@ def _format_sub_agent_pills(sub_agents: list, show_squares: bool = False) -> str - ◆/■ (generating, active) — alternates for animation Args: - sub_agents: List of dicts with ``name``, ``active``, and ``generating`` keys. + sub_agents: List of dicts with ``name``, ``uuid``, ``active``, and ``generating`` keys. show_squares: If True, use square icons (□/■) instead of diamonds (◇/◆) for generating agents. Returns: - A string like ``"◍ primary ◆ reviewer"``. + A string like ``"◍ primary ◆ reviewer (a6b)"``. """ parts = [] + name_counts = Counter(sa["name"] for sa in sub_agents) + for sa in sub_agents: active = sa.get("active", False) gen = sa.get("generating", False) @@ -118,7 +124,13 @@ def _format_sub_agent_pills(sub_agents: list, show_squares: bool = False) -> str icon = "◆" if active else "◇" else: icon = "●" if active else "○" - parts.append(f"{icon} {sa['name']}") + + name = sa["name"] + display_name = name + if name_counts[name] > 1 and name != "primary": + display_name = f"{name} ({sa['uuid'][:3]})" + + parts.append(f"{icon} {display_name}") return " ".join(parts) def update_cost(self, cost_text: str): From 093999e72ce9a2e152c773b03186af9ec746b94d Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 12:13:25 -0700 Subject: [PATCH 20/28] feat: Add UUID prefix matching for agent switching Co-authored-by: cecli (openai/nvidia_nim/deepseek-ai/deepseek-v4-pro) --- cecli/commands/switch_agent.py | 20 +++++++++++++++++++- cecli/tui/app.py | 7 +++++++ tests/commands/test_switch_agent.py | 24 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/cecli/commands/switch_agent.py b/cecli/commands/switch_agent.py index 8d2ab8df155..cf1236bbcc8 100644 --- a/cecli/commands/switch_agent.py +++ b/cecli/commands/switch_agent.py @@ -34,6 +34,13 @@ async def execute(cls, io, coder, args, **kwargs): agent_uuid = uuid break + # If not found by name, try matching first 3 chars of UUID + if agent_uuid is None: + for uuid, sub_agent_info in agent_service.sub_agents.items(): + if uuid[:3] == agent_name: + agent_uuid = uuid + break + if agent_uuid is None: io.tool_error(f"Error: Agent '{agent_name}' not found.") return 1 @@ -54,6 +61,8 @@ async def execute(cls, io, coder, args, **kwargs): def get_completions(cls, io, coder, args) -> List[str]: """Get completion options for switch-agent command.""" try: + from collections import Counter + agent_service = AgentService.get_instance(coder) names = [] @@ -66,9 +75,18 @@ def get_completions(cls, io, coder, args) -> List[str]: # Add sub-agent names, excluding the currently active one if agent_service and agent_service.sub_agents: + # Count name occurrences to detect duplicates + name_counts = Counter( + info.name for info in agent_service.sub_agents.values() + ) for uuid, sub_agent_info in agent_service.sub_agents.items(): if uuid != foreground_uuid: - names.append(sub_agent_info.name) + name = sub_agent_info.name + if name_counts[name] > 1: + # Include UUID prefix for duplicate names + names.append(f"{name} ({uuid[:3]})") + else: + names.append(name) current_arg = args.strip().lower() if current_arg: diff --git a/cecli/tui/app.py b/cecli/tui/app.py index bd2e9d48ee2..c8a98306581 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -738,6 +738,13 @@ def on_input_area_submit(self, message: InputArea.Submit): target_uuid = uuid break + # If not found by name, try matching first 3 chars of UUID + if target_uuid is None: + for uuid, info in agent_service.sub_agents.items(): + if uuid[:3] == agent_name: + target_uuid = uuid + break + if target_uuid is None: self.show_error(f"Agent '{agent_name}' not found.") return diff --git a/tests/commands/test_switch_agent.py b/tests/commands/test_switch_agent.py index 77bdf0fc97b..3af28001360 100644 --- a/tests/commands/test_switch_agent.py +++ b/tests/commands/test_switch_agent.py @@ -62,6 +62,20 @@ async def test_execute_agent_not_found(self, mock_coder, mock_io, mock_agent_ser await SwitchAgentCommand.execute(mock_io, mock_coder, "non-existent-agent") mock_io.tool_error.assert_called_once_with("Error: Agent 'non-existent-agent' not found.") + @pytest.mark.asyncio + async def test_execute_switch_by_uuid_prefix_tui( + self, mock_coder, mock_io, mock_agent_service + ): + """Test switching to a sub-agent by first 3 UUID chars in TUI mode.""" + mock_io.output_queue.put = MagicMock() + + with patch("cecli.commands.switch_agent.hasattr", return_value=True): + await SwitchAgentCommand.execute(mock_io, mock_coder, "sub") + + mock_io.output_queue.put.assert_called_once_with( + {"type": "switch_agent", "uuid": "sub-uuid-1"} + ) + def test_get_completions_on_primary(self, mock_coder, mock_io, mock_agent_service): """Test completions when the primary agent is active.""" mock_agent_service.foreground_uuid = None @@ -81,3 +95,13 @@ def test_get_completions_with_partial_arg(self, mock_coder, mock_io, mock_agent_ mock_agent_service.foreground_uuid = None completions = SwitchAgentCommand.get_completions(mock_io, mock_coder, "rev") assert completions == ["reviewer"] + + def test_get_completions_with_duplicate_names(self, mock_coder, mock_io, mock_agent_service): + """Test completions include UUID prefixes when there are duplicate names.""" + # Add a second sub-agent with the same name + mock_agent_service.sub_agents["sub-uuid-2"] = MagicMock(name="reviewer") + mock_agent_service.foreground_uuid = None + completions = SwitchAgentCommand.get_completions(mock_io, mock_coder, "") + assert "reviewer (sub)" in completions + assert "reviewer (sub)" in completions # second one also has prefix + assert len([c for c in completions if c.startswith("reviewer")]) == 2 From bd8b569ad1d65cd95f6a7d6dd6928c6d930b5680 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 13:14:13 -0700 Subject: [PATCH 21/28] feat: Implement UUID prefix for agent switching Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/tui/widgets/input_container.py | 2 +- tests/commands/test_switch_agent.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/cecli/tui/widgets/input_container.py b/cecli/tui/widgets/input_container.py index 78e7c874ea0..cec9c6aaac5 100644 --- a/cecli/tui/widgets/input_container.py +++ b/cecli/tui/widgets/input_container.py @@ -42,7 +42,7 @@ def update_mode(self, mode: str): sub_agents = self._get_sub_agents() if sub_agents: pills_text = self._format_sub_agent_pills(sub_agents, self.show_squares) - self.border_title = f"{mode}: {pills_text}" + self.border_title = f"agent: {pills_text}" else: self.border_title = mode self.refresh() diff --git a/tests/commands/test_switch_agent.py b/tests/commands/test_switch_agent.py index 3af28001360..ea220ed19e4 100644 --- a/tests/commands/test_switch_agent.py +++ b/tests/commands/test_switch_agent.py @@ -96,6 +96,30 @@ def test_get_completions_with_partial_arg(self, mock_coder, mock_io, mock_agent_ completions = SwitchAgentCommand.get_completions(mock_io, mock_coder, "rev") assert completions == ["reviewer"] + @pytest.mark.asyncio + async def test_execute_switch_by_uuid_prefix_tui( + self, mock_coder, mock_io, mock_agent_service + ): + """Test switching to a sub-agent by first 3 UUID chars in TUI mode.""" + mock_io.output_queue.put = MagicMock() + + with patch("cecli.commands.switch_agent.hasattr", return_value=True): + await SwitchAgentCommand.execute(mock_io, mock_coder, "sub") + + mock_io.output_queue.put.assert_called_once_with( + {"type": "switch_agent", "uuid": "sub-uuid-1"} + ) + + def test_get_completions_with_duplicate_names(self, mock_coder, mock_io, mock_agent_service): + """Test completions include UUID prefixes when there are duplicate names.""" + # Add a second sub-agent with the same name + mock_agent_service.sub_agents["sub-uuid-2"] = MagicMock(name="reviewer") + mock_agent_service.foreground_uuid = None + completions = SwitchAgentCommand.get_completions(mock_io, mock_coder, "") + assert "reviewer (sub)" in completions + assert "reviewer (sub)" in completions # second one also has prefix + assert len([c for c in completions if c.startswith("reviewer")]) == 2 + def test_get_completions_with_duplicate_names(self, mock_coder, mock_io, mock_agent_service): """Test completions include UUID prefixes when there are duplicate names.""" # Add a second sub-agent with the same name From cb599ce9cf14b17ce710eda29a1a29d875e4c03a Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 13:25:42 -0700 Subject: [PATCH 22/28] refactor: Improve agent name resolution in switch-agent command Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/commands/switch_agent.py | 26 ++++++++++++++++++++------ cecli/tui/app.py | 26 ++++++++++++++++++++------ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/cecli/commands/switch_agent.py b/cecli/commands/switch_agent.py index cf1236bbcc8..d09e260960c 100644 --- a/cecli/commands/switch_agent.py +++ b/cecli/commands/switch_agent.py @@ -29,15 +29,29 @@ async def execute(cls, io, coder, args, **kwargs): agent_uuid = str(coder.uuid) else: if agent_service and agent_service.sub_agents: - for uuid, sub_agent_info in agent_service.sub_agents.items(): - if sub_agent_info.name == agent_name: - agent_uuid = uuid - break + # Try parsing "name (uuid)" format + if agent_name.endswith(")") and " (" in agent_name: + try: + # Extract uuid prefix from "name (prefix)" + uuid_prefix = agent_name.rsplit(" (", 1)[1][:-1] + for uuid, info in agent_service.sub_agents.items(): + if uuid.startswith(uuid_prefix): + agent_uuid = uuid + break + except IndexError: + pass # Not the format we expected + + # If not found via "name (uuid)", try matching by name directly + if agent_uuid is None: + for uuid, sub_agent_info in agent_service.sub_agents.items(): + if sub_agent_info.name == agent_name: + agent_uuid = uuid + break - # If not found by name, try matching first 3 chars of UUID + # If still not found, try matching by uuid prefix directly if agent_uuid is None: for uuid, sub_agent_info in agent_service.sub_agents.items(): - if uuid[:3] == agent_name: + if uuid.startswith(agent_name): agent_uuid = uuid break diff --git a/cecli/tui/app.py b/cecli/tui/app.py index c8a98306581..d3cd0eb736b 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -733,15 +733,29 @@ def on_input_area_submit(self, message: InputArea.Submit): if agent_name == "primary": target_uuid = primary_uuid else: - for uuid, info in agent_service.sub_agents.items(): - if info.name == agent_name: - target_uuid = uuid - break + # Try parsing "name (uuid)" format + if agent_name.endswith(")") and " (" in agent_name: + try: + # Extract uuid prefix from "name (prefix)" + uuid_prefix = agent_name.rsplit(" (", 1)[1][:-1] + for uuid, info in agent_service.sub_agents.items(): + if uuid.startswith(uuid_prefix): + target_uuid = uuid + break + except IndexError: + pass # Not the format we expected + + # If not found via "name (uuid)", try matching by name directly + if target_uuid is None: + for uuid, info in agent_service.sub_agents.items(): + if info.name == agent_name: + target_uuid = uuid + break - # If not found by name, try matching first 3 chars of UUID + # If still not found, try matching by uuid prefix directly if target_uuid is None: for uuid, info in agent_service.sub_agents.items(): - if uuid[:3] == agent_name: + if uuid.startswith(agent_name): target_uuid = uuid break From 499f56394cf0eb6aa5ed93145d6fc4bea68fc6da Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 15:46:42 -0700 Subject: [PATCH 23/28] feat: Improve agent switching display and completions Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- tests/commands/test_switch_agent.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/tests/commands/test_switch_agent.py b/tests/commands/test_switch_agent.py index ea220ed19e4..ef161512fcb 100644 --- a/tests/commands/test_switch_agent.py +++ b/tests/commands/test_switch_agent.py @@ -96,30 +96,6 @@ def test_get_completions_with_partial_arg(self, mock_coder, mock_io, mock_agent_ completions = SwitchAgentCommand.get_completions(mock_io, mock_coder, "rev") assert completions == ["reviewer"] - @pytest.mark.asyncio - async def test_execute_switch_by_uuid_prefix_tui( - self, mock_coder, mock_io, mock_agent_service - ): - """Test switching to a sub-agent by first 3 UUID chars in TUI mode.""" - mock_io.output_queue.put = MagicMock() - - with patch("cecli.commands.switch_agent.hasattr", return_value=True): - await SwitchAgentCommand.execute(mock_io, mock_coder, "sub") - - mock_io.output_queue.put.assert_called_once_with( - {"type": "switch_agent", "uuid": "sub-uuid-1"} - ) - - def test_get_completions_with_duplicate_names(self, mock_coder, mock_io, mock_agent_service): - """Test completions include UUID prefixes when there are duplicate names.""" - # Add a second sub-agent with the same name - mock_agent_service.sub_agents["sub-uuid-2"] = MagicMock(name="reviewer") - mock_agent_service.foreground_uuid = None - completions = SwitchAgentCommand.get_completions(mock_io, mock_coder, "") - assert "reviewer (sub)" in completions - assert "reviewer (sub)" in completions # second one also has prefix - assert len([c for c in completions if c.startswith("reviewer")]) == 2 - def test_get_completions_with_duplicate_names(self, mock_coder, mock_io, mock_agent_service): """Test completions include UUID prefixes when there are duplicate names.""" # Add a second sub-agent with the same name @@ -127,5 +103,4 @@ def test_get_completions_with_duplicate_names(self, mock_coder, mock_io, mock_ag mock_agent_service.foreground_uuid = None completions = SwitchAgentCommand.get_completions(mock_io, mock_coder, "") assert "reviewer (sub)" in completions - assert "reviewer (sub)" in completions # second one also has prefix assert len([c for c in completions if c.startswith("reviewer")]) == 2 From c7b3ab17496af7e0da074233035c56bfc33208f4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 15:56:41 -0700 Subject: [PATCH 24/28] refactor: Always include UUID prefix for sub-agent completions Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/commands/switch_agent.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/cecli/commands/switch_agent.py b/cecli/commands/switch_agent.py index d09e260960c..4840f1581d4 100644 --- a/cecli/commands/switch_agent.py +++ b/cecli/commands/switch_agent.py @@ -75,8 +75,6 @@ async def execute(cls, io, coder, args, **kwargs): def get_completions(cls, io, coder, args) -> List[str]: """Get completion options for switch-agent command.""" try: - from collections import Counter - agent_service = AgentService.get_instance(coder) names = [] @@ -89,18 +87,11 @@ def get_completions(cls, io, coder, args) -> List[str]: # Add sub-agent names, excluding the currently active one if agent_service and agent_service.sub_agents: - # Count name occurrences to detect duplicates - name_counts = Counter( - info.name for info in agent_service.sub_agents.values() - ) for uuid, sub_agent_info in agent_service.sub_agents.items(): if uuid != foreground_uuid: name = sub_agent_info.name - if name_counts[name] > 1: - # Include UUID prefix for duplicate names - names.append(f"{name} ({uuid[:3]})") - else: - names.append(name) + # Always include UUID prefix for sub-agents + names.append(f"{name} ({uuid[:3]})") current_arg = args.strip().lower() if current_arg: From 68b2340610245b2f2f4b2ac47401514e3abd01a2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 15:58:25 -0700 Subject: [PATCH 25/28] feat: Always show UUID prefix for sub-agents in UI Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/tui/widgets/input_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cecli/tui/widgets/input_container.py b/cecli/tui/widgets/input_container.py index cec9c6aaac5..937fdc55bc0 100644 --- a/cecli/tui/widgets/input_container.py +++ b/cecli/tui/widgets/input_container.py @@ -127,7 +127,7 @@ def _format_sub_agent_pills(sub_agents: list, show_squares: bool = False) -> str name = sa["name"] display_name = name - if name_counts[name] > 1 and name != "primary": + if name != "primary": display_name = f"{name} ({sa['uuid'][:3]})" parts.append(f"{icon} {display_name}") From a00a2b751f3090013455eceaf84d03c05d0f2bee Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 16:21:37 -0700 Subject: [PATCH 26/28] cli-26: fixed black --- tests/commands/test_switch_agent.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/commands/test_switch_agent.py b/tests/commands/test_switch_agent.py index ef161512fcb..ae08db69f7d 100644 --- a/tests/commands/test_switch_agent.py +++ b/tests/commands/test_switch_agent.py @@ -63,9 +63,7 @@ async def test_execute_agent_not_found(self, mock_coder, mock_io, mock_agent_ser mock_io.tool_error.assert_called_once_with("Error: Agent 'non-existent-agent' not found.") @pytest.mark.asyncio - async def test_execute_switch_by_uuid_prefix_tui( - self, mock_coder, mock_io, mock_agent_service - ): + async def test_execute_switch_by_uuid_prefix_tui(self, mock_coder, mock_io, mock_agent_service): """Test switching to a sub-agent by first 3 UUID chars in TUI mode.""" mock_io.output_queue.put = MagicMock() From 2d9b2ceaf3469a81f2444a7c8a304aef1821540e Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 16:29:28 -0700 Subject: [PATCH 27/28] fix: Remove unused name_counts variable in input_container.py Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/tui/widgets/input_container.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cecli/tui/widgets/input_container.py b/cecli/tui/widgets/input_container.py index 937fdc55bc0..aedbe4253dd 100644 --- a/cecli/tui/widgets/input_container.py +++ b/cecli/tui/widgets/input_container.py @@ -112,7 +112,6 @@ def _format_sub_agent_pills(sub_agents: list, show_squares: bool = False) -> str A string like ``"◍ primary ◆ reviewer (a6b)"``. """ parts = [] - name_counts = Counter(sa["name"] for sa in sub_agents) for sa in sub_agents: active = sa.get("active", False) From 51f73acd9f13e9b7e128b8ea66bf790157bbdfd5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 16:48:51 -0700 Subject: [PATCH 28/28] chore: Remove unused import 'Counter' from input_container.py Co-authored-by: cecli (openai/gemini_cli_local/gemini-2.5-pro) --- cecli/tui/widgets/input_container.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cecli/tui/widgets/input_container.py b/cecli/tui/widgets/input_container.py index aedbe4253dd..d4b7a8fa3f7 100644 --- a/cecli/tui/widgets/input_container.py +++ b/cecli/tui/widgets/input_container.py @@ -1,5 +1,3 @@ -from collections import Counter - from textual.containers import Vertical from textual.reactive import reactive