"""First-run welcome banner and agent availability helper."""
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from rich.console import Group, RenderableType
from rich.panel import Panel
from rich.text import Text
from ralph.agents.availability import HasListAgents, check_agent_availability
from ralph.banner import SupportsPrint, show_banner
from ralph.display.context import DisplayContext
from ralph.onboarding import getting_started_pointer_sentence, welcome_panel_next_steps
if TYPE_CHECKING:
from ralph.config.bootstrap import BootstrapResult
from ralph.display.context import DisplayContext
_KNOWN_AGENT_INSTALL_URLS: dict[str, str] = {
"claude": "https://docs.claude.com/claude-code",
"opencode": "https://opencode.ai",
"agy": "https://github.com/google-antigravity/antigravity-cli",
}
def _build_agent_availability_content(
agent_registry: HasListAgents | None,
) -> list[RenderableType]:
"""Build agent availability content or generic PATH message."""
content: list[RenderableType] = []
if agent_registry is not None:
try:
availability = check_agent_availability(agent_registry)
avail_lines: list[Text] = []
for registry_name, status in availability:
agent = agent_registry.get(registry_name)
label = (
(agent.display_name or registry_name) if agent is not None else registry_name
)
if status == "available":
t = Text(f" • {label}: ")
t.append("on PATH", style="theme.status.success")
avail_lines.append(t)
elif status == "missing_on_path":
install_url = _KNOWN_AGENT_INSTALL_URLS.get(registry_name.lower())
t = Text(f" • {label}: ")
t.append("⚠ missing (not on PATH)", style="theme.status.warning")
if install_url:
t.append(f" install: {install_url}", style="theme.text.muted")
avail_lines.append(t)
else: # no_cmd
t = Text(f" • {label}: ")
t.append("⚠ missing (not on PATH)", style="theme.status.warning")
avail_lines.append(t)
if avail_lines:
content.append(Text("Detected agents:", style="theme.banner.title"))
content.extend(avail_lines)
return content
except Exception:
pass
content.append(Text("Ensure your AI agents are on PATH (e.g., `claude`, `opencode`, `agy`)"))
return content
def _build_regenerate_summary(results: list[BootstrapResult]) -> Text | None:
"""Build summary text for regenerate operation showing backup info."""
regenerated = [r for r in results if r.action == "regenerated"]
if not regenerated:
return None
backup_count = sum(1 for r in regenerated if r.backup is not None)
text = Text(f"Regenerated {len(regenerated)} config file(s)")
if backup_count > 0:
text.append(" (")
text.append(
f"{backup_count} backup(s) saved with .bak suffix",
style="theme.status.warning",
)
text.append(")")
return text
def _partition_config_files(results: list[BootstrapResult]) -> tuple[list[str], list[str]]:
"""Split created config file names into global and local display groups."""
global_files: list[str] = []
local_files: list[str] = []
for result in results:
if result.action == "skipped":
continue
path_str = str(result.path)
filename = result.path.name
if ".agent" in path_str or path_str.startswith("."):
local_files.append(filename)
else:
global_files.append(filename)
return global_files, local_files
def _append_file_section(content: list[RenderableType], heading: str, files: list[str]) -> None:
"""Append a headed bullet list of config files when present."""
if not files:
return
content.append(Text(heading, style="theme.banner.title"))
content.extend(Text(f" • {name}") for name in files)
def _build_next_steps_text() -> Text:
"""Build the welcome panel next-steps block."""
next_steps = Text("Next steps:\n", style="theme.banner.title")
lines = welcome_panel_next_steps()
for index, line in enumerate(lines, start=1):
next_steps.append(f" {index}. {line}")
if index < len(lines):
next_steps.append("\n")
return next_steps
[docs]
def emit_first_run_welcome(
console: object,
results: list[BootstrapResult],
*,
agent_registry: HasListAgents | None = None,
is_regenerate: bool = False,
display_context: DisplayContext,
) -> None:
"""Print a structured first-run welcome panel.
Args:
console: A rich.console.Console-like object with a .print() method.
results: Bootstrap results from a bootstrap operation.
agent_registry: Optional agent registry for availability checking.
is_regenerate: Whether this is a regenerate (--regenerate-config) operation.
display_context: Display context for adaptive layout (required).
"""
if all(r.action == "skipped" for r in results):
return
has_new_or_regenerated = any(r.action in {"created", "regenerated"} for r in results)
if not has_new_or_regenerated:
return
rich_console = cast("SupportsPrint", console)
show_banner(display_context=display_context, console=rich_console)
content: list[RenderableType] = []
intro = Text("Ralph Workflow orchestrates AI coding agents through a ")
intro.append("planning → development loop", style="theme.phase.planning")
intro.append(" driven by your PROMPT.md.")
content.append(intro)
docs_line1 = Text(getting_started_pointer_sentence(), style="theme.text.muted")
content.append(docs_line1)
docs_line2 = Text("Offline docs: ", style="theme.text.muted")
docs_line2.append("python -m pydoc ralph", style="theme.cat.meta")
docs_line2.append(" · run ", style="theme.text.muted")
docs_line2.append("make serve-docs", style="theme.cat.meta")
docs_line2.append(
" from ralph-workflow/ for the full HTML reference.",
style="theme.text.muted",
)
content.append(docs_line2)
content.append(Text()) # blank line
if is_regenerate:
summary = _build_regenerate_summary(results)
if summary:
content.append(summary)
content.append(Text()) # blank line
if not is_regenerate:
content.extend(_build_agent_availability_content(agent_registry))
global_files, local_files = _partition_config_files(results)
_append_file_section(content, "Global config files:", global_files)
_append_file_section(content, "Local config files:", local_files)
content.append(_build_next_steps_text())
panel = Panel(
Group(*content),
title="Ralph Workflow first-run setup",
border_style="theme.banner.border",
padding=(1, 2),
)
rich_console.print(panel)