Skip to content

Commit 83c7e1e

Browse files
authored
Merge pull request #519 from szmania/CLI-26-subagent-windows-shortcuts
CLI-26: Sub-agent Windows shortcuts, /switch-agent command, and UUID identifiers
2 parents 7e63a67 + 51f73ac commit 83c7e1e

7 files changed

Lines changed: 312 additions & 6 deletions

File tree

cecli/commands/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
from .save_session import SaveSessionCommand
6666
from .settings import SettingsCommand
6767
from .spawn_agent import SpawnAgentCommand
68+
from .switch_agent import SwitchAgentCommand
6869
from .terminal_setup import TerminalSetupCommand
6970
from .test import TestCommand
7071
from .think_tokens import ThinkTokensCommand
@@ -118,6 +119,7 @@
118119
CommandRegistry.register(InvokeAgentCommand)
119120
CommandRegistry.register(ReapAgentCommand)
120121
CommandRegistry.register(SpawnAgentCommand)
122+
CommandRegistry.register(SwitchAgentCommand)
121123
CommandRegistry.register(IncludeSkillCommand)
122124
CommandRegistry.register(LintCommand)
123125
CommandRegistry.register(ListSessionsCommand)
@@ -199,6 +201,7 @@
199201
"InvokeAgentCommand",
200202
"ReapAgentCommand",
201203
"SpawnAgentCommand",
204+
"SwitchAgentCommand",
202205
"LintCommand",
203206
"ListSessionsCommand",
204207
"ListSkillsCommand",

cecli/commands/invoke_agent.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ async def execute(cls, io, coder, args, **kwargs):
2525
summary = await agent_service.invoke(name, prompt, blocking=True)
2626
if summary:
2727
from cecli.helpers.conversation.service import ConversationService
28+
from cecli.helpers.conversation.tags import MessageTag
2829

2930
ConversationService.get_manager(coder).add_message(
3031
message_dict=dict(role="user", content=summary),
32+
tag=MessageTag.CUR,
3133
)
3234
io.tool_output(f"Sub-agent '{name}' completed:\n{summary}")
3335
else:

cecli/commands/switch_agent.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from typing import List
2+
3+
from cecli.commands.utils.base_command import BaseCommand
4+
from cecli.commands.utils.helpers import format_command_result
5+
from cecli.helpers.agents.service import AgentService
6+
7+
8+
class SwitchAgentCommand(BaseCommand):
9+
NORM_NAME = "switch-agent"
10+
DESCRIPTION = "Switch to a specific agent by name"
11+
12+
@classmethod
13+
async def execute(cls, io, coder, args, **kwargs):
14+
"""Execute the switch-agent command."""
15+
agent_name = args.strip()
16+
if not agent_name:
17+
io.tool_error("Usage: /switch-agent <agent-name>")
18+
return 1
19+
20+
try:
21+
agent_service = AgentService.get_instance(coder)
22+
except Exception as e:
23+
io.tool_error(f"Could not get agent service: {e}")
24+
return 1
25+
26+
agent_uuid = None
27+
28+
if agent_name == "primary":
29+
agent_uuid = str(coder.uuid)
30+
else:
31+
if agent_service and agent_service.sub_agents:
32+
# Try parsing "name (uuid)" format
33+
if agent_name.endswith(")") and " (" in agent_name:
34+
try:
35+
# Extract uuid prefix from "name (prefix)"
36+
uuid_prefix = agent_name.rsplit(" (", 1)[1][:-1]
37+
for uuid, info in agent_service.sub_agents.items():
38+
if uuid.startswith(uuid_prefix):
39+
agent_uuid = uuid
40+
break
41+
except IndexError:
42+
pass # Not the format we expected
43+
44+
# If not found via "name (uuid)", try matching by name directly
45+
if agent_uuid is None:
46+
for uuid, sub_agent_info in agent_service.sub_agents.items():
47+
if sub_agent_info.name == agent_name:
48+
agent_uuid = uuid
49+
break
50+
51+
# If still not found, try matching by uuid prefix directly
52+
if agent_uuid is None:
53+
for uuid, sub_agent_info in agent_service.sub_agents.items():
54+
if uuid.startswith(agent_name):
55+
agent_uuid = uuid
56+
break
57+
58+
if agent_uuid is None:
59+
io.tool_error(f"Error: Agent '{agent_name}' not found.")
60+
return 1
61+
62+
if hasattr(io, "output_queue") and io.output_queue:
63+
io.output_queue.put({"type": "switch_agent", "uuid": agent_uuid})
64+
else:
65+
# Non-TUI mode
66+
if agent_uuid == str(coder.uuid):
67+
agent_service.foreground_uuid = None
68+
else:
69+
agent_service.foreground_uuid = agent_uuid
70+
io.tool_output(f"Switched to agent: {agent_name}")
71+
72+
return format_command_result(io, "switch-agent", f"Switched to agent '{agent_name}'")
73+
74+
@classmethod
75+
def get_completions(cls, io, coder, args) -> List[str]:
76+
"""Get completion options for switch-agent command."""
77+
try:
78+
agent_service = AgentService.get_instance(coder)
79+
names = []
80+
81+
# Determine current foreground agent
82+
foreground_uuid = agent_service.foreground_uuid
83+
84+
# Add "primary" only if not already on primary
85+
if foreground_uuid is not None:
86+
names.append("primary")
87+
88+
# Add sub-agent names, excluding the currently active one
89+
if agent_service and agent_service.sub_agents:
90+
for uuid, sub_agent_info in agent_service.sub_agents.items():
91+
if uuid != foreground_uuid:
92+
name = sub_agent_info.name
93+
# Always include UUID prefix for sub-agents
94+
names.append(f"{name} ({uuid[:3]})")
95+
96+
current_arg = args.strip().lower()
97+
if current_arg:
98+
return [name for name in names if name.lower().startswith(current_arg)]
99+
else:
100+
return names
101+
except Exception:
102+
return ["primary"]
103+
104+
@classmethod
105+
def get_help(cls) -> str:
106+
"""Get help text for the switch-agent command."""
107+
help_text = super().get_help()
108+
help_text += "\nUsage:\n"
109+
help_text += " /switch-agent <agent-name> # Switch to a specific agent\n"
110+
help_text += "\nExamples:\n"
111+
help_text += " /switch-agent primary\n"
112+
help_text += " /switch-agent reviewer\n"
113+
help_text += "\nUse tab for auto-completion of agent names.\n"
114+
return help_text

cecli/tui/app.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,14 @@ def handle_output_message(self, msg):
554554

555555
footer = self.query_one(MainFooter)
556556
footer.update_mode(msg.get("mode", "code"))
557+
elif msg_type == "switch_agent":
558+
target_uuid = msg["uuid"]
559+
# Ensure the target container exists before switching
560+
primary_uuid = str(self.worker.coder.uuid)
561+
if target_uuid != primary_uuid and target_uuid not in self._sub_agent_containers:
562+
self.show_error("Agent container not found. Cannot switch.")
563+
else:
564+
self._switch_to_container(target_uuid)
557565

558566
def add_output(self, text, task_id=None):
559567
"""Add output to the output container."""
@@ -678,6 +686,8 @@ def on_input_area_text_changed(self, message: InputArea.TextChanged):
678686

679687
def on_input_area_submit(self, message: InputArea.Submit):
680688
"""Handle input submission."""
689+
from cecli.helpers.agents.service import AgentService
690+
681691
user_input = message.value
682692

683693
if not user_input.strip():
@@ -703,6 +713,63 @@ def on_input_area_submit(self, message: InputArea.Submit):
703713
self._open_editor_suspended(initial_content)
704714
return
705715

716+
# Intercept /switch-agent command to handle immediately without LLM processing
717+
if stripped.startswith("/switch-agent"):
718+
parts = stripped.split(maxsplit=1)
719+
agent_name = parts[1].strip() if len(parts) > 1 else ""
720+
721+
input_area = self.query_one("#input", InputArea)
722+
input_area.value = ""
723+
724+
if not agent_name:
725+
self.show_error("Usage: /switch-agent <agent-name>")
726+
return
727+
728+
# Resolve agent name to UUID
729+
agent_service = AgentService.get_instance(self.worker.coder)
730+
primary_uuid = str(self.worker.coder.uuid)
731+
732+
target_uuid = None
733+
if agent_name == "primary":
734+
target_uuid = primary_uuid
735+
else:
736+
# Try parsing "name (uuid)" format
737+
if agent_name.endswith(")") and " (" in agent_name:
738+
try:
739+
# Extract uuid prefix from "name (prefix)"
740+
uuid_prefix = agent_name.rsplit(" (", 1)[1][:-1]
741+
for uuid, info in agent_service.sub_agents.items():
742+
if uuid.startswith(uuid_prefix):
743+
target_uuid = uuid
744+
break
745+
except IndexError:
746+
pass # Not the format we expected
747+
748+
# If not found via "name (uuid)", try matching by name directly
749+
if target_uuid is None:
750+
for uuid, info in agent_service.sub_agents.items():
751+
if info.name == agent_name:
752+
target_uuid = uuid
753+
break
754+
755+
# If still not found, try matching by uuid prefix directly
756+
if target_uuid is None:
757+
for uuid, info in agent_service.sub_agents.items():
758+
if uuid.startswith(agent_name):
759+
target_uuid = uuid
760+
break
761+
762+
if target_uuid is None:
763+
self.show_error(f"Agent '{agent_name}' not found.")
764+
return
765+
766+
if target_uuid != primary_uuid and target_uuid not in self._sub_agent_containers:
767+
self.show_error(f"Agent container for '{agent_name}' not found.")
768+
return
769+
770+
self._switch_to_container(target_uuid)
771+
return
772+
706773
# Save to history before clearing
707774
input_area = self.query_one("#input", InputArea)
708775
input_area.save_to_history(user_input)
@@ -976,6 +1043,12 @@ def _switch_to_container(self, uuid: str) -> None:
9761043
agent_service = AgentService.get_instance(self.worker.coder)
9771044
primary_uuid = str(self.worker.coder.uuid)
9781045

1046+
# Check if the target container exists
1047+
if uuid != primary_uuid and uuid not in self._sub_agent_containers:
1048+
# Sub-agent container not found, fall back to primary
1049+
self.show_error(f"Agent container for UUID {uuid} not found. Switching to primary.")
1050+
uuid = primary_uuid
1051+
9791052
if uuid == primary_uuid:
9801053
# Switch to primary agent
9811054
agent_service.foreground_uuid = None

cecli/tui/widgets/input_container.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def update_mode(self, mode: str):
4040
sub_agents = self._get_sub_agents()
4141
if sub_agents:
4242
pills_text = self._format_sub_agent_pills(sub_agents, self.show_squares)
43-
self.border_title = f"{mode}: {pills_text}"
43+
self.border_title = f"agent: {pills_text}"
4444
else:
4545
self.border_title = mode
4646
self.refresh()
@@ -49,7 +49,7 @@ def _get_sub_agents(self) -> list:
4949
"""Query AgentService via self.app to build sub-agent pill data.
5050
5151
Returns:
52-
List of dicts with ``name``, ``active``, and ``generating`` keys,
52+
List of dicts with ``name``, ``uuid``, ``active``, and ``generating`` keys,
5353
or empty list.
5454
"""
5555
try:
@@ -61,13 +61,14 @@ def _get_sub_agents(self) -> list:
6161
agent_service = AgentService.get_instance(coder)
6262

6363
sub_agents = []
64-
primary_uuid = agent_service.coder.uuid
64+
primary_uuid = str(agent_service.coder.uuid)
6565
active_uuid = agent_service.foreground_uuid or primary_uuid
6666

6767
# Primary is never "generating" in the sub-agent sense
6868
sub_agents.append(
6969
{
7070
"name": "primary",
71+
"uuid": primary_uuid,
7172
"active": active_uuid == primary_uuid,
7273
"generating": is_active(getattr(coder.io, "output_task", None)),
7374
}
@@ -78,6 +79,7 @@ def _get_sub_agents(self) -> list:
7879
sub_agents.append(
7980
{
8081
"name": info.name,
82+
"uuid": coder_uuid,
8183
"active": coder_uuid == active_uuid,
8284
"generating": is_active(info.generate_task),
8385
}
@@ -101,13 +103,14 @@ def _format_sub_agent_pills(sub_agents: list, show_squares: bool = False) -> str
101103
- ◆/■ (generating, active) — alternates for animation
102104
103105
Args:
104-
sub_agents: List of dicts with ``name``, ``active``, and ``generating`` keys.
106+
sub_agents: List of dicts with ``name``, ``uuid``, ``active``, and ``generating`` keys.
105107
show_squares: If True, use square icons (□/■) instead of diamonds (◇/◆) for generating agents.
106108
107109
Returns:
108-
A string like ``"◍ primary ◆ reviewer"``.
110+
A string like ``"◍ primary ◆ reviewer (a6b)"``.
109111
"""
110112
parts = []
113+
111114
for sa in sub_agents:
112115
active = sa.get("active", False)
113116
gen = sa.get("generating", False)
@@ -118,7 +121,13 @@ def _format_sub_agent_pills(sub_agents: list, show_squares: bool = False) -> str
118121
icon = "◆" if active else "◇"
119122
else:
120123
icon = "●" if active else "○"
121-
parts.append(f"{icon} {sa['name']}")
124+
125+
name = sa["name"]
126+
display_name = name
127+
if name != "primary":
128+
display_name = f"{name} ({sa['uuid'][:3]})"
129+
130+
parts.append(f"{icon} {display_name}")
122131
return " ".join(parts)
123132

124133
def update_cost(self, cost_text: str):

cecli/website/docs/usage/commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ cog.out(get_help_md())
5959
| **/run** | Run a shell command and optionally add the output to the chat (alias: !) |
6060
| **/save** | Save commands to a file that can reconstruct the current chat session's files |
6161
| **/settings** | Print out the current settings |
62+
| **/switch-agent** | Switch to a specific agent by name |
6263
| **/test** | Run a shell command and add the output to the chat on non-zero exit code |
6364
| **/think-tokens** | Set the thinking token budget, eg: 8096, 8k, 10.5k, 0.5M, or 0 to disable. |
6465
| **/tokens** | Report on the number of tokens used by the current chat context |

0 commit comments

Comments
 (0)