Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d45f096
cli-26: switch agent shortcuts
May 19, 2026
ff7a4df
(no commit message provided)
May 19, 2026
1d4cb4b
fix: Update keybindings for agent navigation
May 19, 2026
d538368
feat: Add /switch-agent command with tab completion
May 19, 2026
a47da74
fix: Resolve circular import in switch_agent command
May 19, 2026
aec45d6
feat: Add SwitchAgentCommand to command registry
May 19, 2026
78c06ac
fix: Resolve circular import in switch_agent command
May 19, 2026
63a21f4
fix: Improve agent switching robustness in TUI
May 19, 2026
4f95e10
feat: Add tag to ConversationService add_message call
May 19, 2026
3883a52
refactor: Improve agent switching logic and completions
May 19, 2026
ded0451
fix: Ensure agent switching updates UI on main thread
May 19, 2026
8c745f3
refactor: Remove unnecessary call_from_thread in TUI
May 19, 2026
a18de88
refactor: Move AgentService import to top of on_input_area_submit
May 19, 2026
1b8b81c
test: add tests for switch_agent command
May 19, 2026
27e916f
feat(agent): Add /switch-agent command and Ctrl+Shift shortcuts
May 19, 2026
9318603
(no commit message provided)
May 19, 2026
a57834d
(no commit message provided)
May 19, 2026
5404d22
cli-26: removed wrong documetnation
May 19, 2026
1dea3f6
feat: Add UUID prefix to duplicate agent names in TUI
May 19, 2026
093999e
feat: Add UUID prefix matching for agent switching
May 19, 2026
bd8b569
feat: Implement UUID prefix for agent switching
May 19, 2026
cb599ce
refactor: Improve agent name resolution in switch-agent command
May 19, 2026
499f563
feat: Improve agent switching display and completions
May 19, 2026
c7b3ab1
refactor: Always include UUID prefix for sub-agent completions
May 19, 2026
68b2340
feat: Always show UUID prefix for sub-agents in UI
May 19, 2026
a00a2b7
cli-26: fixed black
May 19, 2026
2d9b2ce
fix: Remove unused name_counts variable in input_container.py
May 19, 2026
51f73ac
chore: Remove unused import 'Counter' from input_container.py
May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cecli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -199,6 +201,7 @@
"InvokeAgentCommand",
"ReapAgentCommand",
"SpawnAgentCommand",
"SwitchAgentCommand",
"LintCommand",
"ListSessionsCommand",
"ListSkillsCommand",
Expand Down
2 changes: 2 additions & 0 deletions cecli/commands/invoke_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
114 changes: 114 additions & 0 deletions cecli/commands/switch_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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


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 <agent-name>")
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:
# 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 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.startswith(agent_name):
agent_uuid = uuid
break

if agent_uuid is None:
io.tool_error(f"Error: Agent '{agent_name}' not found.")
return 1

if hasattr(io, "output_queue") and io.output_queue:
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 = []

# Determine current foreground agent
foreground_uuid = agent_service.foreground_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():
if uuid != foreground_uuid:
name = sub_agent_info.name
# Always include UUID prefix for sub-agents
names.append(f"{name} ({uuid[:3]})")

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 <agent-name> # 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
73 changes: 73 additions & 0 deletions cecli/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,14 @@ def handle_output_message(self, msg):

footer = self.query_one(MainFooter)
footer.update_mode(msg.get("mode", "code"))
elif msg_type == "switch_agent":
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("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."""
Expand Down Expand Up @@ -678,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():
Expand All @@ -703,6 +713,63 @@ 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 <agent-name>")
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
else:
# 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 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.startswith(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)
Expand Down Expand Up @@ -976,6 +1043,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
Expand Down
21 changes: 15 additions & 6 deletions cecli/tui/widgets/input_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,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()
Expand All @@ -49,7 +49,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:
Expand All @@ -61,13 +61,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)),
}
Expand All @@ -78,6 +79,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),
}
Expand All @@ -101,13 +103,14 @@ 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 = []

for sa in sub_agents:
active = sa.get("active", False)
gen = sa.get("generating", False)
Expand All @@ -118,7 +121,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 != "primary":
display_name = f"{name} ({sa['uuid'][:3]})"

parts.append(f"{icon} {display_name}")
return " ".join(parts)

def update_cost(self, cost_text: str):
Expand Down
1 change: 1 addition & 0 deletions cecli/website/docs/usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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 |
Expand Down
Loading
Loading