Skip to content

Commit 0c918a6

Browse files
Kasper Jungeclaude
authored andcommitted
refactor: extract ConsoleEmitter event handlers into focused methods
Replace the monolithic emit() method (72 lines, 8+ branches in an if/elif chain) with a dispatch table mapping EventType to private handler methods. Each handler is now independently readable and testable. Also modernize type annotations: Optional[T] → T | None to match the convention used in the rest of the codebase, and remove the now-unused typing.Optional import. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 657e682 commit 0c918a6

1 file changed

Lines changed: 83 additions & 77 deletions

File tree

src/ralphify/cli.py

Lines changed: 83 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import tomllib
1010
import uuid
1111
from pathlib import Path
12-
from typing import Optional
12+
from collections.abc import Callable
1313

1414
import typer
1515
from rich.console import Console
@@ -290,90 +290,96 @@ def __init__(self, console: Console) -> None:
290290
self._rprint = console.print
291291

292292
def emit(self, event: Event) -> None:
293-
d = event.data
294-
t = event.type
295-
296-
if t == EventType.RUN_STARTED:
297-
if d.get("timeout"):
298-
self._rprint(f"[dim]Timeout: {_format_duration(d['timeout'])} per iteration[/dim]")
299-
if d.get("checks"):
300-
self._rprint(f"[dim]Checks: {d['checks']} enabled[/dim]")
301-
if d.get("contexts"):
302-
self._rprint(f"[dim]Contexts: {d['contexts']} enabled[/dim]")
303-
if d.get("instructions"):
304-
self._rprint(f"[dim]Instructions: {d['instructions']} enabled[/dim]")
305-
306-
elif t == EventType.ITERATION_STARTED:
307-
self._rprint(f"\n[bold blue]── Iteration {d['iteration']} ──[/bold blue]")
308-
309-
elif t in (EventType.ITERATION_COMPLETED, EventType.ITERATION_FAILED, EventType.ITERATION_TIMED_OUT):
310-
iteration = d["iteration"]
311-
returncode = d.get("returncode")
312-
detail = d["detail"]
313-
log_file = d.get("log_file")
314-
315-
if returncode is None:
316-
color, icon = "yellow", "\u23f1"
317-
elif returncode == 0:
318-
color, icon = "green", "\u2713"
293+
handler = self._handlers.get(event.type)
294+
if handler:
295+
handler(self, event.data)
296+
297+
def _on_run_started(self, d: dict) -> None:
298+
if d.get("timeout"):
299+
self._rprint(f"[dim]Timeout: {_format_duration(d['timeout'])} per iteration[/dim]")
300+
if d.get("checks"):
301+
self._rprint(f"[dim]Checks: {d['checks']} enabled[/dim]")
302+
if d.get("contexts"):
303+
self._rprint(f"[dim]Contexts: {d['contexts']} enabled[/dim]")
304+
if d.get("instructions"):
305+
self._rprint(f"[dim]Instructions: {d['instructions']} enabled[/dim]")
306+
307+
def _on_iteration_started(self, d: dict) -> None:
308+
self._rprint(f"\n[bold blue]── Iteration {d['iteration']} ──[/bold blue]")
309+
310+
def _on_iteration_ended(self, d: dict) -> None:
311+
returncode = d.get("returncode")
312+
if returncode is None:
313+
color, icon = "yellow", "\u23f1"
314+
elif returncode == 0:
315+
color, icon = "green", "\u2713"
316+
else:
317+
color, icon = "red", "\u2717"
318+
319+
status_msg = f"[{color}]{icon} Iteration {d['iteration']} {d['detail']}"
320+
if d.get("log_file"):
321+
status_msg += f" \u2192 {d['log_file']}"
322+
status_msg += f"[/{color}]"
323+
self._rprint(status_msg)
324+
325+
def _on_checks_completed(self, d: dict) -> None:
326+
parts = []
327+
if d["passed"]:
328+
parts.append(f"{d['passed']} passed")
329+
if d["failed"]:
330+
parts.append(f"{d['failed']} failed")
331+
self._rprint(f" [bold]Checks:[/bold] {', '.join(parts)}")
332+
for r in d["results"]:
333+
if r["passed"]:
334+
self._rprint(f" [green]\u2713[/green] {r['name']}")
335+
elif r["timed_out"]:
336+
self._rprint(f" [yellow]\u23f1[/yellow] {r['name']} (timed out)")
319337
else:
320-
color, icon = "red", "\u2717"
321-
322-
status_msg = f"[{color}]{icon} Iteration {iteration} {detail}"
323-
if log_file:
324-
status_msg += f" \u2192 {log_file}"
325-
status_msg += f"[/{color}]"
326-
self._rprint(status_msg)
327-
328-
elif t == EventType.CHECKS_COMPLETED:
329-
passed = d["passed"]
330-
failed = d["failed"]
331-
parts = []
332-
if passed:
333-
parts.append(f"{passed} passed")
338+
self._rprint(f" [red]\u2717[/red] {r['name']} (exit {r['exit_code']})")
339+
340+
def _on_log_message(self, d: dict) -> None:
341+
msg = d.get("message", "")
342+
if "Stopping" in msg:
343+
self._rprint(f"[red]{msg}[/red]")
344+
elif "Waiting" in msg:
345+
self._rprint(f"[dim]{msg}[/dim]")
346+
347+
def _on_run_stopped(self, d: dict) -> None:
348+
if d.get("reason") == "completed":
349+
total = d.get("total", 0)
350+
completed = d.get("completed", 0)
351+
failed = d.get("failed", 0)
352+
timed_out_count = d.get("timed_out", 0)
353+
summary = f"\n[green]Done: {total} iteration(s) \u2014 {completed} succeeded"
334354
if failed:
335-
parts.append(f"{failed} failed")
336-
self._rprint(f" [bold]Checks:[/bold] {', '.join(parts)}")
337-
for r in d["results"]:
338-
if r["passed"]:
339-
self._rprint(f" [green]\u2713[/green] {r['name']}")
340-
elif r["timed_out"]:
341-
self._rprint(f" [yellow]\u23f1[/yellow] {r['name']} (timed out)")
342-
else:
343-
self._rprint(f" [red]\u2717[/red] {r['name']} (exit {r['exit_code']})")
344-
345-
elif t == EventType.LOG_MESSAGE:
346-
msg = d.get("message", "")
347-
if "Stopping" in msg:
348-
self._rprint(f"[red]{msg}[/red]")
349-
elif "Waiting" in msg:
350-
self._rprint(f"[dim]{msg}[/dim]")
351-
352-
elif t == EventType.RUN_STOPPED:
353-
if d.get("reason") == "completed":
354-
total = d.get("total", 0)
355-
completed = d.get("completed", 0)
356-
failed = d.get("failed", 0)
357-
timed_out_count = d.get("timed_out", 0)
358-
summary = f"\n[green]Done: {total} iteration(s) \u2014 {completed} succeeded"
359-
if failed:
360-
summary += f", {failed} failed"
361-
if timed_out_count:
362-
summary += f" ({timed_out_count} timed out)"
363-
summary += "[/green]"
364-
self._rprint(summary)
355+
summary += f", {failed} failed"
356+
if timed_out_count:
357+
summary += f" ({timed_out_count} timed out)"
358+
summary += "[/green]"
359+
self._rprint(summary)
360+
361+
_handlers: dict[EventType, "Callable[[ConsoleEmitter, dict], None]"] = {
362+
EventType.RUN_STARTED: _on_run_started,
363+
EventType.ITERATION_STARTED: _on_iteration_started,
364+
EventType.ITERATION_COMPLETED: _on_iteration_ended,
365+
EventType.ITERATION_FAILED: _on_iteration_ended,
366+
EventType.ITERATION_TIMED_OUT: _on_iteration_ended,
367+
EventType.CHECKS_COMPLETED: _on_checks_completed,
368+
EventType.LOG_MESSAGE: _on_log_message,
369+
EventType.RUN_STOPPED: _on_run_stopped,
370+
}
365371

366372

367373
@app.command()
368374
def run(
369-
prompt_name: Optional[str] = typer.Argument(None, help="Name of a prompt in .ralph/prompts/."),
370-
n: Optional[int] = typer.Option(None, "-n", help="Max number of iterations. Infinite if not set."),
371-
prompt_text: Optional[str] = typer.Option(None, "-p", "--prompt", help="Ad-hoc prompt text. Overrides the prompt file."),
372-
prompt_file: Optional[str] = typer.Option(None, "--prompt-file", "-f", help="Path to prompt file. Overrides ralph.toml."),
375+
prompt_name: str | None = typer.Argument(None, help="Name of a prompt in .ralph/prompts/."),
376+
n: int | None = typer.Option(None, "-n", help="Max number of iterations. Infinite if not set."),
377+
prompt_text: str | None = typer.Option(None, "-p", "--prompt", help="Ad-hoc prompt text. Overrides the prompt file."),
378+
prompt_file: str | None = typer.Option(None, "--prompt-file", "-f", help="Path to prompt file. Overrides ralph.toml."),
373379
stop_on_error: bool = typer.Option(False, "--stop-on-error", "-s", help="Stop if the agent exits with non-zero."),
374380
delay: float = typer.Option(0, "--delay", "-d", help="Seconds to wait between iterations."),
375-
log_dir: Optional[str] = typer.Option(None, "--log-dir", "-l", help="Save iteration output to log files in this directory."),
376-
timeout: Optional[float] = typer.Option(None, "--timeout", "-t", help="Max seconds per iteration. Kill agent if exceeded."),
381+
log_dir: str | None = typer.Option(None, "--log-dir", "-l", help="Save iteration output to log files in this directory."),
382+
timeout: float | None = typer.Option(None, "--timeout", "-t", help="Max seconds per iteration. Kill agent if exceeded."),
377383
) -> None:
378384
"""Run the autonomous coding loop.
379385

0 commit comments

Comments
 (0)