Skip to content

Commit 51ee41d

Browse files
authored
Merge pull request #539 from szmania/cli-39-agent-specific-status-messages-2
fix: Add UUID disambiguation for duplicate agent names
2 parents 2124b1a + ea5e87b commit 51ee41d

8 files changed

Lines changed: 324 additions & 42 deletions

File tree

cecli/coders/base_coder.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,7 +1597,7 @@ async def output_task(self, preproc):
15971597
self.io.output_task = asyncio.create_task(self.generate(user_message, preproc))
15981598

15991599
# Start spinner for output task
1600-
self.io.start_spinner("Processing...")
1600+
self.io.start_spinner("Processing...", coder_uuid=getattr(self, "uuid", None))
16011601
await self.io.recreate_input()
16021602

16031603
# Monitor output task
@@ -2376,7 +2376,7 @@ async def format_in_executor():
23762376
if not self.tui:
23772377
spinner_text += f" • ${self.format_cost(self.total_cost)} session"
23782378

2379-
self.io.start_spinner(spinner_text)
2379+
self.io.start_spinner(spinner_text, coder_uuid=getattr(self, "uuid", None))
23802380
if self.stream:
23812381
self.mdstream = True
23822382
else:
@@ -2463,9 +2463,7 @@ async def format_in_executor():
24632463
self.mdstream = None
24642464

24652465
# Ensure any waiting spinner is stopped
2466-
self.io.start_spinner("Processing Answer...")
2467-
2468-
self.partial_response_content = self.get_multi_response_content_in_progress(True)
2466+
self.io.start_spinner("Processing Answer...", coder_uuid=getattr(self, "uuid", None))
24692467
self.remove_reasoning_content()
24702468
self.multi_response_content = ""
24712469

cecli/io.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ def _spinner_supports_unicode(self) -> bool:
553553
except Exception:
554554
return False
555555

556-
def start_spinner(self, text, update_last_text=True):
556+
def start_spinner(self, text, update_last_text=True, **kwargs):
557557
"""Start the spinner."""
558558
self.stop_spinner()
559559

cecli/tui/app.py

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -502,9 +502,10 @@ def check_output_queue(self):
502502
pass
503503

504504
def handle_output_message(self, msg):
505-
"""Route output messages to appropriate handlers."""
506505
msg_type = msg["type"]
507506

507+
# Resolve agent_name from coder_uuid for agent-specific status messages
508+
agent_name = self._resolve_agent_name(msg.get("coder_uuid"))
508509
if msg_type == "output":
509510
container = self._get_output_container(msg)
510511
container.add_output(msg["text"], msg.get("task_id"))
@@ -532,15 +533,15 @@ def handle_output_message(self, msg):
532533
container = self._get_output_container(msg)
533534
container.start_task(msg["task_id"], msg["title"], msg.get("task_type"))
534535
elif msg_type == "confirmation":
535-
self.show_confirmation(msg)
536+
self.show_confirmation(msg, agent_name=agent_name)
536537
elif msg_type == "spinner":
537-
self.update_spinner(msg)
538+
self.update_spinner(msg, agent_name=agent_name)
538539
elif msg_type == "ready_for_input":
539540
self.enable_input(msg)
540541
footer = self.query_one(MainFooter)
541542
footer.stop_spinner()
542543
elif msg_type == "error":
543-
self.show_error(msg["message"])
544+
self.show_error(msg["message"], agent_name=agent_name)
544545
elif msg_type == "cost_update":
545546
footer = self.query_one(MainFooter)
546547
footer.update_cost(msg.get("cost", 0))
@@ -563,6 +564,53 @@ def handle_output_message(self, msg):
563564
else:
564565
self._switch_to_container(target_uuid)
565566

567+
def _resolve_agent_name(self, coder_uuid: str | None) -> str | None:
568+
"""Resolve an agent display name from a coder_uuid.
569+
570+
Returns the sub-agent's name if the coder_uuid belongs to a known
571+
sub-agent. For the primary agent, returns "primary" if sub-agents
572+
exist, otherwise None.
573+
574+
If multiple sub-agents share the same name, disambiguates by
575+
appending the first 3 characters of the UUID in parentheses.
576+
"""
577+
if not coder_uuid:
578+
return None
579+
try:
580+
if not self.worker or not self.worker.coder:
581+
return None # Cannot resolve without a coder
582+
from cecli.helpers.agents.service import AgentService
583+
584+
agent_service = AgentService.get_instance(self.worker.coder)
585+
if not agent_service:
586+
return None
587+
primary_uuid = str(agent_service.coder.uuid)
588+
if coder_uuid == primary_uuid:
589+
if agent_service.sub_agents:
590+
return "primary"
591+
return None # Primary agent gets no prefix
592+
if not agent_service.sub_agents:
593+
return None
594+
for info in agent_service.sub_agents.values():
595+
if not info or not info.coder:
596+
continue
597+
if str(info.coder.uuid) == coder_uuid:
598+
# Check for duplicate names among sub-agents
599+
name_count = sum(
600+
1
601+
for i in agent_service.sub_agents.values()
602+
if i and hasattr(i, "name") and i.name == info.name
603+
)
604+
if name_count > 1:
605+
# Disambiguate with first 3 UUID characters
606+
short_uuid = str(info.coder.uuid)[:3]
607+
return f"{info.name} ({short_uuid})"
608+
return info.name
609+
except (AttributeError, ImportError, KeyError):
610+
# Agent service not available or coder not yet initialized
611+
pass
612+
return None
613+
566614
def add_output(self, text, task_id=None):
567615
"""Add output to the output container."""
568616
output_container = self.query_one("#output", OutputContainer)
@@ -601,7 +649,7 @@ def start_task(self, task_id, title, task_type="general"):
601649
output_container = self.query_one("#output", OutputContainer)
602650
output_container.start_task(task_id, title, task_type)
603651

604-
def show_confirmation(self, msg):
652+
def show_confirmation(self, msg, agent_name: str | None = None):
605653
"""Show inline confirmation bar."""
606654
# Disable input while confirm bar is active
607655
input_area = self.query_one("#input", InputArea)
@@ -623,6 +671,7 @@ def show_confirmation(self, msg):
623671
allow_never=allow_never,
624672
default=options.get("default", "y"),
625673
explicit_yes_required=options.get("explicit_yes_required", False),
674+
agent_name=agent_name,
626675
)
627676

628677
def enable_input(self, msg, coder=None):
@@ -657,24 +706,25 @@ def enable_input(self, msg, coder=None):
657706

658707
input_area.focus()
659708

660-
def update_spinner(self, msg):
709+
def update_spinner(self, msg, agent_name: str | None = None):
661710
"""Update spinner in footer."""
662711
footer = self.query_one(MainFooter)
663712
action = msg.get("action", "start")
664713

665714
if action == "start":
666-
footer.start_spinner(msg.get("text", ""))
715+
footer.start_spinner(msg.get("text", ""), agent_name=agent_name or "")
667716
elif action == "update":
668717
footer.spinner_text = msg.get("text", "")
669718
elif action == "update_suffix":
670719
footer.spinner_suffix = msg.get("text", "")
671720
elif action == "stop":
672721
footer.stop_spinner()
673722

674-
def show_error(self, message):
675-
"""Show error notification."""
676-
status_bar = self.query_one("#status-bar", StatusBar)
677-
status_bar.show_notification(f"Error: {message}", severity="error", timeout=10)
723+
def show_error(self, message, agent_name: str | None = None):
724+
"""Show an error message in the status bar."""
725+
self.status_bar.show_notification(
726+
message, severity="error", timeout=5, agent_name=agent_name
727+
)
678728

679729
def on_resize(self) -> None:
680730
file_list = self.query_one("#file-list", FileList)
@@ -781,15 +831,21 @@ def on_input_area_submit(self, message: InputArea.Submit):
781831

782832
# Update footer to show processing
783833
footer = self.query_one(MainFooter)
784-
footer.start_spinner("Processing...")
785834

786835
coder = self.worker.coder
787-
788-
if coder:
789-
coder.io.start_spinner("Processing...")
790-
791836
# Determine which coder is in the foreground for input routing
792837
foreground_coder = AgentService.get_instance(coder).foreground_coder
838+
coder_uuid = (
839+
str(foreground_coder.uuid)
840+
if foreground_coder and hasattr(foreground_coder, "uuid")
841+
else None
842+
)
843+
agent_name = self._resolve_agent_name(coder_uuid)
844+
845+
footer.start_spinner("Processing...", agent_name=agent_name or "")
846+
847+
if coder:
848+
coder.io.start_spinner("Processing...", coder_uuid=coder_uuid)
793849

794850
if coder and is_active(getattr(coder.io, "output_task", None)):
795851
from cecli.helpers.conversation import ConversationService, MessageTag

cecli/tui/io.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -328,13 +328,15 @@ def _reroute_output(self, text, msg_type, **kwargs):
328328

329329
return False
330330

331-
def start_spinner(self, text, update_last_text=True):
331+
def start_spinner(self, text, update_last_text=True, **kwargs):
332332
"""Override start_spinner to send spinner state to TUI.
333333
334334
Args:
335335
text: Spinner text
336336
update_last_text: Whether to update last_spinner_text
337+
coder_uuid: Optional uuid string to include in the message
337338
"""
339+
coder_uuid = kwargs.get("coder_uuid", None)
338340
# Call parent to maintain state
339341
super().start_spinner(text, update_last_text)
340342

@@ -344,23 +346,27 @@ def start_spinner(self, text, update_last_text=True):
344346
"type": "spinner",
345347
"action": "start",
346348
"text": text,
349+
"coder_uuid": coder_uuid,
347350
}
348351
)
349352

350353
self.output_queue.put(
351354
{
352355
"type": "spinner",
356+
"coder_uuid": coder_uuid,
353357
"action": "update_suffix",
354358
"text": "",
355359
}
356360
)
357361

358-
def update_spinner(self, text):
362+
def update_spinner(self, text, **kwargs):
359363
"""Override update_spinner to send updates to TUI.
360364
361365
Args:
362366
text: New spinner text
367+
coder_uuid: Optional uuid string to include in the message
363368
"""
369+
coder_uuid = kwargs.get("coder_uuid", None)
364370
# Call parent
365371
super().update_spinner(text)
366372

@@ -370,15 +376,18 @@ def update_spinner(self, text):
370376
"type": "spinner",
371377
"action": "update",
372378
"text": text,
379+
"coder_uuid": coder_uuid,
373380
}
374381
)
375382

376-
def update_spinner_suffix(self, text=None):
383+
def update_spinner_suffix(self, text=None, **kwargs):
377384
"""Override update_spinner_suffix to send updates to TUI.
378385
379386
Args:
380387
text: New spinner suffix text
388+
coder_uuid: Optional uuid string to include in the message
381389
"""
390+
coder_uuid = kwargs.get("coder_uuid", None)
382391
# Call parent
383392
super().update_spinner_suffix(text)
384393

@@ -388,21 +397,18 @@ def update_spinner_suffix(self, text=None):
388397
"type": "spinner",
389398
"action": "update_suffix",
390399
"text": text,
400+
"coder_uuid": coder_uuid,
391401
}
392402
)
393403

394-
def stop_spinner(self):
404+
def stop_spinner(self, **kwargs):
395405
"""Override stop_spinner to send stop state to TUI."""
406+
coder_uuid = kwargs.get("coder_uuid", None)
396407
# Call parent
397408
super().stop_spinner()
398409

399410
# Send to TUI
400-
self.output_queue.put(
401-
{
402-
"type": "spinner",
403-
"action": "stop",
404-
}
405-
)
411+
self.output_queue.put({"type": "spinner", "action": "stop", "coder_uuid": coder_uuid})
406412

407413
def interrupt_input(self):
408414
self.interrupted = True
@@ -518,6 +524,7 @@ async def confirm_ask(
518524
allow_never=False,
519525
allow_tweak=False,
520526
acknowledge=False,
527+
coder_uuid=None,
521528
):
522529
"""Override confirm_ask to show modal instead of inline prompt.
523530
@@ -594,6 +601,7 @@ async def confirm_ask(
594601
"acknowledge": acknowledge,
595602
"valid_responses": valid_responses,
596603
},
604+
"coder_uuid": coder_uuid,
597605
}
598606
)
599607

cecli/tui/widgets/footer.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class MainFooter(Static):
1010

1111
# Left side info
1212
coder_mode = reactive("code")
13+
agent_name = reactive("")
1314
model_name = reactive("")
1415

1516
# Right side info
@@ -46,6 +47,7 @@ def __init__(
4647
self.project_name = project_name
4748
self.git_branch = git_branch
4849
self.coder_mode = coder_mode
50+
self.agent_name = ""
4951
self._spinner_interval = None
5052

5153
def on_mount(self):
@@ -77,7 +79,7 @@ def _get_display_model(self) -> str:
7779
else:
7880
name = coder.get_active_model().name
7981
except Exception:
80-
name = self.app.worker.coder.get_active_model().name
82+
name = self.model_name
8183

8284
# Strip common prefixes like "openrouter/x-ai/"
8385
if len(name) > 40:
@@ -98,9 +100,10 @@ def render(self) -> Text:
98100
if self.spinner_visible:
99101
spinner_char = self._spinner_chars[self._spinner_frame]
100102
left.append(f"{spinner_char} ")
103+
if self.agent_name:
104+
left.append(f"({self.agent_name}) ")
101105
if self.spinner_text:
102106
left.append(self.spinner_text)
103-
104107
# When a sub-agent is generating, show its model alongside the spinner
105108
# if self._has_running_sub_agent():
106109
# model_display = self._get_display_model()
@@ -176,9 +179,10 @@ def update_mode(self, mode: str):
176179
self.coder_mode = mode
177180
self.refresh()
178181

179-
def start_spinner(self, text: str = ""):
182+
def start_spinner(self, text: str = "", agent_name: str = ""):
180183
"""Show spinner with optional text."""
181184
self.spinner_text = text
185+
self.agent_name = agent_name
182186
self.spinner_visible = True
183187
self.refresh()
184188

@@ -204,6 +208,7 @@ def stop_spinner(self):
204208

205209
self.spinner_visible = False
206210
self.spinner_text = ""
211+
self.agent_name = ""
207212
self.refresh()
208213

209214
def _has_running_sub_agent(self) -> bool:

0 commit comments

Comments
 (0)