diff --git a/pyproject.toml b/pyproject.toml index e48023ef2..cdb88b664 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "codegen-api-client", "typer>=0.12.5", "rich>=13.7.1", + "textual>=0.91.0", "hatch-vcs>=0.4.0", "hatchling>=1.25.0", # CLI and git functionality dependencies diff --git a/src/codegen/cli/cli.py b/src/codegen/cli/cli.py index 0ce5f873d..b1ca50c70 100644 --- a/src/codegen/cli/cli.py +++ b/src/codegen/cli/cli.py @@ -4,6 +4,7 @@ from codegen import __version__ # Import config command (still a Typer app) +from codegen.cli.commands.agent.main import agent from codegen.cli.commands.agents.main import agents_app # Import the actual command functions @@ -16,6 +17,7 @@ from codegen.cli.commands.profile.main import profile from codegen.cli.commands.style_debug.main import style_debug from codegen.cli.commands.tools.main import tools +from codegen.cli.commands.tui.main import tui from codegen.cli.commands.update.main import update install(show_locals=True) @@ -32,6 +34,7 @@ def version_callback(value: bool): main = typer.Typer(name="codegen", help="Codegen - the Operating System for Code Agents.", rich_markup_mode="rich") # Add individual commands to the main app +main.command("agent", help="Create a new agent run with a prompt.")(agent) main.command("claude", help="Run Claude Code with OpenTelemetry monitoring and logging.")(claude) main.command("init", help="Initialize or update the Codegen folder.")(init) main.command("login", help="Store authentication token.")(login) @@ -39,6 +42,7 @@ def version_callback(value: bool): main.command("profile", help="Display information about the currently authenticated user.")(profile) main.command("style-debug", help="Debug command to visualize CLI styling (spinners, etc).")(style_debug) main.command("tools", help="List available tools from the Codegen API.")(tools) +main.command("tui", help="Launch the interactive TUI interface.")(tui) main.command("update", help="Update Codegen to the latest or specified version")(update) # Add Typer apps as sub-applications @@ -47,10 +51,14 @@ def version_callback(value: bool): main.add_typer(integrations_app, name="integrations") -@main.callback() -def main_callback(version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True, help="Show version and exit")): +@main.callback(invoke_without_command=True) +def main_callback(ctx: typer.Context, version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True, help="Show version and exit")): """Codegen - the Operating System for Code Agents""" - pass + if ctx.invoked_subcommand is None: + # No subcommand provided, launch TUI + from codegen.cli.tui.app import run_tui + + run_tui() if __name__ == "__main__": diff --git a/src/codegen/cli/commands/agent/__init__.py b/src/codegen/cli/commands/agent/__init__.py new file mode 100644 index 000000000..3e51d8df8 --- /dev/null +++ b/src/codegen/cli/commands/agent/__init__.py @@ -0,0 +1 @@ +"""Agent command module.""" diff --git a/src/codegen/cli/commands/agent/main.py b/src/codegen/cli/commands/agent/main.py new file mode 100644 index 000000000..58a208cf1 --- /dev/null +++ b/src/codegen/cli/commands/agent/main.py @@ -0,0 +1,159 @@ +"""Agent command for creating remote agent runs.""" + +import requests +import typer +from rich import box +from rich.console import Console +from rich.panel import Panel + +from codegen.cli.api.endpoints import API_ENDPOINT +from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.rich.spinners import create_spinner +from codegen.cli.utils.org import resolve_org_id + +console = Console() + +# Create the agent app +agent_app = typer.Typer(help="Create and manage individual agent runs") + + +@agent_app.command() +def create( + prompt: str = typer.Option(..., "--prompt", "-p", help="The prompt to send to the agent"), + org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)"), + model: str | None = typer.Option(None, help="Model to use for this agent run (optional)"), + repo_id: int | None = typer.Option(None, help="Repository ID to use for this agent run (optional)"), +): + """Create a new agent run with the given prompt.""" + # Get the current token + token = get_current_token() + if not token: + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) + + try: + # Resolve org id + resolved_org_id = resolve_org_id(org_id) + if resolved_org_id is None: + console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.") + raise typer.Exit(1) + + # Prepare the request payload + payload = { + "prompt": prompt, + } + + if model: + payload["model"] = model + if repo_id: + payload["repo_id"] = repo_id + + # Make API request to create agent run with spinner + spinner = create_spinner("Creating agent run...") + spinner.start() + + try: + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/agent/run" + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + agent_run_data = response.json() + finally: + spinner.stop() + + # Extract agent run information + run_id = agent_run_data.get("id", "Unknown") + status = agent_run_data.get("status", "Unknown") + web_url = agent_run_data.get("web_url", "") + created_at = agent_run_data.get("created_at", "") + + # Format created date + if created_at: + try: + from datetime import datetime + + dt = datetime.fromisoformat(created_at.replace("Z", "+00:00")) + created_display = dt.strftime("%B %d, %Y at %H:%M") + except (ValueError, TypeError): + created_display = created_at + else: + created_display = "Unknown" + + # Status with emoji + status_display = status + if status == "COMPLETE": + status_display = "✅ Complete" + elif status == "RUNNING": + status_display = "🏃 Running" + elif status == "FAILED": + status_display = "❌ Failed" + elif status == "STOPPED": + status_display = "âšī¸ Stopped" + elif status == "PENDING": + status_display = "âŗ Pending" + + # Create result display + result_info = [] + result_info.append(f"[cyan]Agent Run ID:[/cyan] {run_id}") + result_info.append(f"[cyan]Status:[/cyan] {status_display}") + result_info.append(f"[cyan]Created:[/cyan] {created_display}") + if web_url: + result_info.append(f"[cyan]Web URL:[/cyan] {web_url}") + + result_text = "\n".join(result_info) + + console.print( + Panel( + result_text, + title="🤖 [bold]Agent Run Created[/bold]", + border_style="green", + box=box.ROUNDED, + padding=(1, 2), + ) + ) + + # Show next steps + console.print("\n[dim]💡 Track progress with:[/dim] [cyan]codegen agents[/cyan]") + if web_url: + console.print(f"[dim]🌐 View in browser:[/dim] [link]{web_url}[/link]") + + except requests.RequestException as e: + console.print(f"[red]Error creating agent run:[/red] {e}", style="bold red") + if hasattr(e, "response") and e.response is not None: + try: + error_detail = e.response.json().get("detail", "Unknown error") + console.print(f"[red]Details:[/red] {error_detail}") + except (ValueError, KeyError): + pass + raise typer.Exit(1) + except Exception as e: + console.print(f"[red]Unexpected error:[/red] {e}", style="bold red") + raise typer.Exit(1) + + +# Default callback for the agent app +@agent_app.callback(invoke_without_command=True) +def agent_callback(ctx: typer.Context): + """Create and manage individual agent runs.""" + if ctx.invoked_subcommand is None: + # If no subcommand is provided, show help + print(ctx.get_help()) + raise typer.Exit() + + +# For backward compatibility, also allow `codegen agent --prompt "..."` +def agent( + prompt: str = typer.Option(None, "--prompt", "-p", help="The prompt to send to the agent"), + org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)"), + model: str | None = typer.Option(None, help="Model to use for this agent run (optional)"), + repo_id: int | None = typer.Option(None, help="Repository ID to use for this agent run (optional)"), +): + """Create a new agent run with the given prompt.""" + if prompt: + # If prompt is provided, create the agent run + create(prompt=prompt, org_id=org_id, model=model, repo_id=repo_id) + else: + # If no prompt, show help + console.print("[red]Error:[/red] --prompt is required") + console.print("Usage: [cyan]codegen agent --prompt 'Your prompt here'[/cyan]") + raise typer.Exit(1) diff --git a/src/codegen/cli/commands/agents/main.py b/src/codegen/cli/commands/agents/main.py index dcbe74da9..bf1381bc6 100644 --- a/src/codegen/cli/commands/agents/main.py +++ b/src/codegen/cli/commands/agents/main.py @@ -33,13 +33,28 @@ def list_agents(org_id: int | None = typer.Option(None, help="Organization ID (d raise typer.Exit(1) # Make API request to list agent runs with spinner - spinner = create_spinner("Fetching agent runs...") + spinner = create_spinner("Fetching your recent API agent runs...") spinner.start() try: headers = {"Authorization": f"Bearer {token}"} + # Filter to only API source type and current user's agent runs + params = { + "source_type": "API", + # We'll get the user_id from the /users/me endpoint + } + + # First get the current user ID + user_response = requests.get(f"{API_ENDPOINT.rstrip('/')}/v1/users/me", headers=headers) + user_response.raise_for_status() + user_data = user_response.json() + user_id = user_data.get("id") + + if user_id: + params["user_id"] = user_id + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/agent/runs" - response = requests.get(url, headers=headers) + response = requests.get(url, headers=headers, params=params) response.raise_for_status() response_data = response.json() finally: @@ -52,21 +67,20 @@ def list_agents(org_id: int | None = typer.Option(None, help="Organization ID (d page_size = response_data.get("page_size", 10) if not agent_runs: - console.print("[yellow]No agent runs found.[/yellow]") + console.print("[yellow]No API agent runs found for your user.[/yellow]") return # Create a table to display agent runs table = Table( - title=f"Agent Runs (Page {page}, Total: {total})", + title=f"Your Recent API Agent Runs (Page {page}, Total: {total})", border_style="blue", show_header=True, title_justify="center", ) - table.add_column("ID", style="cyan", no_wrap=True) - table.add_column("Status", style="white", justify="center") - table.add_column("Source", style="magenta") table.add_column("Created", style="dim") - table.add_column("Result", style="green") + table.add_column("Status", style="white", justify="center") + table.add_column("Summary", style="green") + table.add_column("Link", style="blue") # Add agent runs to table for agent_run in agent_runs: @@ -74,20 +88,37 @@ def list_agents(org_id: int | None = typer.Option(None, help="Organization ID (d status = agent_run.get("status", "Unknown") source_type = agent_run.get("source_type", "Unknown") created_at = agent_run.get("created_at", "Unknown") - result = agent_run.get("result", "") - # Status with emoji - status_display = status + # Extract summary from task_timeline_json, similar to frontend + timeline = agent_run.get("task_timeline_json") + summary = None + if timeline and isinstance(timeline, dict) and "summary" in timeline: + if isinstance(timeline["summary"], str): + summary = timeline["summary"] + + # Fall back to goal_prompt if no summary + if not summary: + summary = agent_run.get("goal_prompt", "") + + # Status with colored circles if status == "COMPLETE": - status_display = "✅ Complete" + status_display = "[green]●[/green] Complete" + elif status == "ACTIVE": + status_display = "[dim]●[/dim] Active" elif status == "RUNNING": - status_display = "🏃 Running" + status_display = "[dim]●[/dim] Running" + elif status == "CANCELLED": + status_display = "[yellow]●[/yellow] Cancelled" + elif status == "ERROR": + status_display = "[red]●[/red] Error" elif status == "FAILED": - status_display = "❌ Failed" + status_display = "[red]●[/red] Failed" elif status == "STOPPED": - status_display = "âšī¸ Stopped" + status_display = "[yellow]●[/yellow] Stopped" elif status == "PENDING": - status_display = "âŗ Pending" + status_display = "[dim]●[/dim] Pending" + else: + status_display = "[dim]●[/dim] " + status # Format created date (just show date and time, not full timestamp) if created_at and created_at != "Unknown": @@ -102,13 +133,20 @@ def list_agents(org_id: int | None = typer.Option(None, help="Organization ID (d else: created_display = created_at - # Truncate result if too long - result_display = result[:50] + "..." if result and len(result) > 50 else result or "No result" + # Truncate summary if too long + summary_display = summary[:50] + "..." if summary and len(summary) > 50 else summary or "No summary" + + # Create web link for the agent run + web_url = agent_run.get("web_url") + if not web_url: + # Construct URL if not provided + web_url = f"https://codegen.com/traces/{run_id}" + link_display = web_url - table.add_row(run_id, status_display, source_type, created_display, result_display) + table.add_row(created_display, status_display, summary_display, link_display) console.print(table) - console.print(f"\n[green]Showing {len(agent_runs)} of {total} agent runs[/green]") + console.print(f"\n[green]Showing {len(agent_runs)} of {total} API agent runs[/green]") except requests.RequestException as e: console.print(f"[red]Error fetching agent runs:[/red] {e}", style="bold red") diff --git a/src/codegen/cli/commands/profile/main.py b/src/codegen/cli/commands/profile/main.py index dbe7334b4..d615c04f5 100644 --- a/src/codegen/cli/commands/profile/main.py +++ b/src/codegen/cli/commands/profile/main.py @@ -1,29 +1,110 @@ -import rich +"""Profile command for the Codegen CLI.""" + +import requests +import typer from rich import box +from rich.console import Console from rich.panel import Panel -from codegen.cli.auth.decorators import requires_auth -from codegen.cli.auth.session import CodegenSession +from codegen.cli.api.endpoints import API_ENDPOINT +from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.rich.spinners import create_spinner +from codegen.cli.utils.org import resolve_org_id + +console = Console() -# from codegen.cli.workspace.decorators import requires_init # Removed to simplify CLI +def profile(): + """Display information about the currently authenticated user.""" + # Get the current token + token = get_current_token() + if not token: + console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.") + raise typer.Exit(1) -def requires_init(func): - """Simple stub decorator that does nothing.""" - return func + try: + # Make API request to get current user info with spinner + spinner = create_spinner("Fetching user profile and organization info...") + spinner.start() + try: + headers = {"Authorization": f"Bearer {token}"} + url = f"{API_ENDPOINT.rstrip('/')}/v1/users/me" + response = requests.get(url, headers=headers) + response.raise_for_status() + user_data = response.json() + finally: + spinner.stop() -@requires_auth -@requires_init -def profile(session: CodegenSession): - """Display information about the currently authenticated user.""" - repo_config = session.config.repository - rich.print( - Panel( - f"[cyan]Name:[/cyan] {repo_config.user_name}\n[cyan]Email:[/cyan] {repo_config.user_email}\n[cyan]Repo:[/cyan] {repo_config.name}", - title="🔑 [bold]Current Profile[/bold]", - border_style="cyan", - box=box.ROUNDED, - padding=(1, 2), + # Extract user information + user_id = user_data.get("id", "Unknown") + full_name = user_data.get("full_name", "") + email = user_data.get("email", "") + github_username = user_data.get("github_username", "") + github_user_id = user_data.get("github_user_id", "") + avatar_url = user_data.get("avatar_url", "") + role = user_data.get("role", "") + + # Get organization information + org_id = resolve_org_id() + org_name = None + + if org_id: + try: + # Fetch organizations to get the name + orgs_url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations" + orgs_response = requests.get(orgs_url, headers=headers) + orgs_response.raise_for_status() + orgs_data = orgs_response.json() + + # Find the matching organization + organizations = orgs_data.get("items", []) + for org in organizations: + if org.get("id") == org_id: + org_name = org.get("name", f"Organization {org_id}") + break + + if not org_name: + org_name = f"Organization {org_id}" # Fallback if not found + + except Exception: + # If we can't fetch org name, fall back to showing ID + org_name = f"Organization {org_id}" + + # Create profile display + profile_info = [] + if user_id != "Unknown": + profile_info.append(f"[cyan]User ID:[/cyan] {user_id}") + if full_name: + profile_info.append(f"[cyan]Name:[/cyan] {full_name}") + if email: + profile_info.append(f"[cyan]Email:[/cyan] {email}") + if github_username: + profile_info.append(f"[cyan]GitHub:[/cyan] {github_username}") + if org_name: + profile_info.append(f"[cyan]Organization:[/cyan] {org_name}") + elif org_id: + profile_info.append(f"[cyan]Organization:[/cyan] Organization {org_id}") + else: + profile_info.append("[cyan]Organization:[/cyan] [yellow]Not configured[/yellow] (set CODEGEN_ORG_ID or REPOSITORY_ORG_ID)") + if role: + profile_info.append(f"[cyan]Role:[/cyan] {role}") + + profile_text = "\n".join(profile_info) if profile_info else "No profile information available" + + console.print( + Panel( + profile_text, + title="👤 [bold]User Profile[/bold]", + border_style="cyan", + box=box.ROUNDED, + padding=(1, 2), + ) ) - ) + + except requests.RequestException as e: + console.print(f"[red]Error fetching profile:[/red] {e}", style="bold red") + raise typer.Exit(1) + except Exception as e: + console.print(f"[red]Unexpected error:[/red] {e}", style="bold red") + raise typer.Exit(1) diff --git a/src/codegen/cli/commands/tui/__init__.py b/src/codegen/cli/commands/tui/__init__.py new file mode 100644 index 000000000..f6a335d72 --- /dev/null +++ b/src/codegen/cli/commands/tui/__init__.py @@ -0,0 +1 @@ +"""TUI command module.""" diff --git a/src/codegen/cli/commands/tui/main.py b/src/codegen/cli/commands/tui/main.py new file mode 100644 index 000000000..174d10634 --- /dev/null +++ b/src/codegen/cli/commands/tui/main.py @@ -0,0 +1,12 @@ +"""TUI command for the Codegen CLI.""" + +from codegen.cli.tui.app import run_tui + + +def tui(): + """Launch the Codegen TUI interface.""" + run_tui() + + +if __name__ == "__main__": + tui() diff --git a/src/codegen/cli/tui/__init__.py b/src/codegen/cli/tui/__init__.py new file mode 100644 index 000000000..70f88a592 --- /dev/null +++ b/src/codegen/cli/tui/__init__.py @@ -0,0 +1 @@ +"""TUI (Terminal User Interface) module for Codegen CLI.""" diff --git a/src/codegen/cli/tui/app.py b/src/codegen/cli/tui/app.py new file mode 100644 index 000000000..07375bbc5 --- /dev/null +++ b/src/codegen/cli/tui/app.py @@ -0,0 +1,188 @@ +"""Main TUI application for Codegen CLI.""" + +import asyncio +import webbrowser + +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Container, Vertical +from textual.widgets import DataTable, Footer, Header, Static + +from codegen.cli.auth.token_manager import get_current_token +from codegen.cli.utils.org import resolve_org_id + + +class CodegenTUI(App): + """Simple Codegen TUI for browsing agent runs.""" + + CSS_PATH = "codegen_tui.tcss" + TITLE = "Recent Agent Runs - Codegen CLI" + BINDINGS = [ + Binding("escape,ctrl+c", "quit", "Quit", priority=True), + Binding("enter", "open_url", "Open", show=True), + Binding("r", "refresh", "Refresh", show=True), + ] + + def __init__(self): + super().__init__() + self.token = get_current_token() + self.is_authenticated = bool(self.token) + if self.is_authenticated: + self.org_id = resolve_org_id() + self.agent_runs = [] + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Header() + if not self.is_authenticated: + yield Container(Static("âš ī¸ Not authenticated. Please run 'codegen login' first.", classes="warning-message"), id="auth-warning") + else: + with Vertical(): + yield Static("🤖 Your Recent API Agent Runs", classes="title") + yield Static("Use ↑↓ to navigate, Enter to open, R to refresh, Esc to quit", classes="help") + table = DataTable(id="agents-table", cursor_type="row") + table.add_columns("Created", "Status", "Summary") + yield table + yield Footer() + + def on_mount(self) -> None: + """Called when app starts.""" + if self.is_authenticated and self.org_id: + task = asyncio.create_task(self._load_agents_data()) + # Store reference to prevent garbage collection + self._load_task = task + + async def _load_agents_data(self) -> None: + """Load agents data into the table.""" + table = self.query_one("#agents-table", DataTable) + table.clear() + + if not self.token or not self.org_id: + return + + try: + import requests + + from codegen.cli.api.endpoints import API_ENDPOINT + + headers = {"Authorization": f"Bearer {self.token}"} + + # First get the current user ID + user_response = requests.get(f"{API_ENDPOINT.rstrip('/')}/v1/users/me", headers=headers) + user_response.raise_for_status() + user_data = user_response.json() + user_id = user_data.get("id") + + # Filter to only API source type and current user's agent runs + params = { + "source_type": "API", + "limit": 20, # Show recent 20 + } + + if user_id: + params["user_id"] = user_id + + # Fetch agent runs + url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{self.org_id}/agent/runs" + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + response_data = response.json() + + agent_runs = response_data.get("items", []) + self.agent_runs = agent_runs # Store for URL opening + + for agent_run in agent_runs: + run_id = str(agent_run.get("id", "Unknown")) + status = agent_run.get("status", "Unknown") + created_at = agent_run.get("created_at", "Unknown") + + # Extract summary from task_timeline_json, similar to frontend + timeline = agent_run.get("task_timeline_json") + summary = None + if timeline and isinstance(timeline, dict) and "summary" in timeline: + if isinstance(timeline["summary"], str): + summary = timeline["summary"] + + # Fall back to goal_prompt if no summary + if not summary: + summary = agent_run.get("goal_prompt", "") + + # Status with colored circles + if status == "COMPLETE": + status_display = "● Complete" + elif status == "ACTIVE": + status_display = "● Active" + elif status == "RUNNING": + status_display = "● Running" + elif status == "CANCELLED": + status_display = "● Cancelled" + elif status == "ERROR": + status_display = "● Error" + elif status == "FAILED": + status_display = "● Failed" + elif status == "STOPPED": + status_display = "● Stopped" + elif status == "PENDING": + status_display = "● Pending" + else: + status_display = "● " + status + + # Format created date + if created_at and created_at != "Unknown": + try: + from datetime import datetime + + dt = datetime.fromisoformat(created_at.replace("Z", "+00:00")) + created_display = dt.strftime("%m/%d %H:%M") + except (ValueError, TypeError): + created_display = created_at[:16] if len(created_at) > 16 else created_at + else: + created_display = created_at + + # Truncate summary if too long + summary_display = summary[:60] + "..." if summary and len(summary) > 60 else summary or "No summary" + + table.add_row(created_display, status_display, summary_display, key=run_id) + + except Exception as e: + # If API call fails, show error in table + table.add_row("Error", f"Failed to load: {e}", "") + + def action_open_url(self) -> None: + """Open the selected agent run URL.""" + table = self.query_one("#agents-table", DataTable) + if table.cursor_row is not None and table.cursor_row < len(self.agent_runs): + agent_run = self.agent_runs[table.cursor_row] + run_id = agent_run.get("id") + web_url = agent_run.get("web_url") + + if not web_url: + # Construct URL if not provided + web_url = f"https://codegen.com/traces/{run_id}" + + # Try to open URL + try: + webbrowser.open(web_url) + self.notify(f"🌐 Opened {web_url}") + except Exception as e: + self.notify(f"❌ Failed to open URL: {e}", severity="error") + + def action_refresh(self) -> None: + """Refresh agent runs data.""" + if self.is_authenticated and self.org_id: + self.notify("🔄 Refreshing...", timeout=1) + task = asyncio.create_task(self._load_agents_data()) + # Store reference to prevent garbage collection + self._refresh_task = task + else: + self.notify("❌ Not authenticated or no org ID", severity="error") + + def action_quit(self) -> None: + """Quit the application.""" + self.exit() + + +def run_tui(): + """Run the Codegen TUI.""" + app = CodegenTUI() + app.run() diff --git a/src/codegen/cli/tui/codegen_tui.tcss b/src/codegen/cli/tui/codegen_tui.tcss new file mode 100644 index 000000000..3d8c92795 --- /dev/null +++ b/src/codegen/cli/tui/codegen_tui.tcss @@ -0,0 +1,74 @@ +/* Codegen TUI Styles */ + +Screen { + background: $background; +} + +Header { + dock: top; + height: 3; + background: $primary; + color: $text; +} + +Footer { + dock: bottom; + height: 3; + background: $primary; + color: $text; +} + +.title { + text-style: bold; + margin: 1; + color: $primary; + text-align: center; +} + +.help { + margin-bottom: 1; + color: $text-muted; + text-align: center; +} + +.warning-message { + text-align: center; + margin: 2; + padding: 2; + background: $warning; + color: $text; + text-style: bold; +} + +DataTable { + height: 1fr; + margin-top: 1; +} + +DataTable > .datatable--header { + text-style: bold; + background: $primary; + color: $text; +} + +DataTable > .datatable--odd-row { + background: $surface; +} + +DataTable > .datatable--even-row { + background: $background; +} + +DataTable > .datatable--cursor { + background: $accent; + color: $text; +} + +#auth-warning { + height: 100%; + align: center middle; +} + +Vertical { + height: 100%; +} diff --git a/uv.lock b/uv.lock index 9fe7a7fc6..a8d76c2ef 100644 --- a/uv.lock +++ b/uv.lock @@ -439,6 +439,7 @@ dependencies = [ { name = "requests" }, { name = "rich" }, { name = "sentry-sdk" }, + { name = "textual" }, { name = "typer" }, { name = "unidiff" }, ] @@ -500,6 +501,7 @@ requires-dist = [ { name = "requests", specifier = ">=2.32.3" }, { name = "rich", specifier = ">=13.7.1" }, { name = "sentry-sdk", specifier = "==2.29.1" }, + { name = "textual", specifier = ">=0.91.0" }, { name = "typer", specifier = ">=0.12.5" }, { name = "unidiff", specifier = ">=0.7.5" }, ] @@ -1481,6 +1483,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/09/2032e7d15c544a0e3cd831c51d77a8ca57f7555b2e1b2922142eddb02a84/jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4", size = 59700, upload-time = "2024-07-16T17:02:01.115Z" }, ] +[[package]] +name = "linkify-it-py" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -1519,6 +1533,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] +plugins = [ + { name = "mdit-py-plugins" }, +] + [[package]] name = "markupsafe" version = "3.0.2" @@ -1589,6 +1611,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232, upload-time = "2025-06-12T08:20:28.551Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -2181,11 +2215,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] @@ -2809,6 +2843,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, ] +[[package]] +name = "textual" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify", "plugins"] }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/ce/f0f938d33d9bebbf8629e0020be00c560ddfa90a23ebe727c2e5aa3f30cf/textual-5.3.0.tar.gz", hash = "sha256:1b6128b339adef2e298cc23ab4777180443240ece5c232f29b22960efd658d4d", size = 1557651, upload-time = "2025-08-07T12:36:50.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2f/f7c8a533bee50fbf5bb37ffc1621e7b2cdd8c9a6301fc51faa35fa50b09d/textual-5.3.0-py3-none-any.whl", hash = "sha256:02a6abc065514c4e21f94e79aaecea1f78a28a85d11d7bfc64abf3392d399890", size = 702671, upload-time = "2025-08-07T12:36:48.272Z" }, +] + [[package]] name = "tinycss2" version = "1.4.0" @@ -2976,6 +3026,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, ] +[[package]] +name = "uc-micro-py" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, +] + [[package]] name = "unidiff" version = "0.7.5"