Skip to content

Commit 678abf4

Browse files
authored
textual v0 + CLI cleanups (#1195)
1 parent 91fcbce commit 678abf4

File tree

12 files changed

+669
-46
lines changed

12 files changed

+669
-46
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ dependencies = [
99
"codegen-api-client",
1010
"typer>=0.12.5",
1111
"rich>=13.7.1",
12+
"textual>=0.91.0",
1213
"hatch-vcs>=0.4.0",
1314
"hatchling>=1.25.0",
1415
# CLI and git functionality dependencies

src/codegen/cli/cli.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from codegen import __version__
55

66
# Import config command (still a Typer app)
7+
from codegen.cli.commands.agent.main import agent
78
from codegen.cli.commands.agents.main import agents_app
89

910
# Import the actual command functions
@@ -16,6 +17,7 @@
1617
from codegen.cli.commands.profile.main import profile
1718
from codegen.cli.commands.style_debug.main import style_debug
1819
from codegen.cli.commands.tools.main import tools
20+
from codegen.cli.commands.tui.main import tui
1921
from codegen.cli.commands.update.main import update
2022

2123
install(show_locals=True)
@@ -32,13 +34,15 @@ def version_callback(value: bool):
3234
main = typer.Typer(name="codegen", help="Codegen - the Operating System for Code Agents.", rich_markup_mode="rich")
3335

3436
# Add individual commands to the main app
37+
main.command("agent", help="Create a new agent run with a prompt.")(agent)
3538
main.command("claude", help="Run Claude Code with OpenTelemetry monitoring and logging.")(claude)
3639
main.command("init", help="Initialize or update the Codegen folder.")(init)
3740
main.command("login", help="Store authentication token.")(login)
3841
main.command("logout", help="Clear stored authentication token.")(logout)
3942
main.command("profile", help="Display information about the currently authenticated user.")(profile)
4043
main.command("style-debug", help="Debug command to visualize CLI styling (spinners, etc).")(style_debug)
4144
main.command("tools", help="List available tools from the Codegen API.")(tools)
45+
main.command("tui", help="Launch the interactive TUI interface.")(tui)
4246
main.command("update", help="Update Codegen to the latest or specified version")(update)
4347

4448
# Add Typer apps as sub-applications
@@ -47,10 +51,14 @@ def version_callback(value: bool):
4751
main.add_typer(integrations_app, name="integrations")
4852

4953

50-
@main.callback()
51-
def main_callback(version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True, help="Show version and exit")):
54+
@main.callback(invoke_without_command=True)
55+
def main_callback(ctx: typer.Context, version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True, help="Show version and exit")):
5256
"""Codegen - the Operating System for Code Agents"""
53-
pass
57+
if ctx.invoked_subcommand is None:
58+
# No subcommand provided, launch TUI
59+
from codegen.cli.tui.app import run_tui
60+
61+
run_tui()
5462

5563

5664
if __name__ == "__main__":
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Agent command module."""
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""Agent command for creating remote agent runs."""
2+
3+
import requests
4+
import typer
5+
from rich import box
6+
from rich.console import Console
7+
from rich.panel import Panel
8+
9+
from codegen.cli.api.endpoints import API_ENDPOINT
10+
from codegen.cli.auth.token_manager import get_current_token
11+
from codegen.cli.rich.spinners import create_spinner
12+
from codegen.cli.utils.org import resolve_org_id
13+
14+
console = Console()
15+
16+
# Create the agent app
17+
agent_app = typer.Typer(help="Create and manage individual agent runs")
18+
19+
20+
@agent_app.command()
21+
def create(
22+
prompt: str = typer.Option(..., "--prompt", "-p", help="The prompt to send to the agent"),
23+
org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)"),
24+
model: str | None = typer.Option(None, help="Model to use for this agent run (optional)"),
25+
repo_id: int | None = typer.Option(None, help="Repository ID to use for this agent run (optional)"),
26+
):
27+
"""Create a new agent run with the given prompt."""
28+
# Get the current token
29+
token = get_current_token()
30+
if not token:
31+
console.print("[red]Error:[/red] Not authenticated. Please run 'codegen login' first.")
32+
raise typer.Exit(1)
33+
34+
try:
35+
# Resolve org id
36+
resolved_org_id = resolve_org_id(org_id)
37+
if resolved_org_id is None:
38+
console.print("[red]Error:[/red] Organization ID not provided. Pass --org-id, set CODEGEN_ORG_ID, or REPOSITORY_ORG_ID.")
39+
raise typer.Exit(1)
40+
41+
# Prepare the request payload
42+
payload = {
43+
"prompt": prompt,
44+
}
45+
46+
if model:
47+
payload["model"] = model
48+
if repo_id:
49+
payload["repo_id"] = repo_id
50+
51+
# Make API request to create agent run with spinner
52+
spinner = create_spinner("Creating agent run...")
53+
spinner.start()
54+
55+
try:
56+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
57+
url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/agent/run"
58+
response = requests.post(url, headers=headers, json=payload)
59+
response.raise_for_status()
60+
agent_run_data = response.json()
61+
finally:
62+
spinner.stop()
63+
64+
# Extract agent run information
65+
run_id = agent_run_data.get("id", "Unknown")
66+
status = agent_run_data.get("status", "Unknown")
67+
web_url = agent_run_data.get("web_url", "")
68+
created_at = agent_run_data.get("created_at", "")
69+
70+
# Format created date
71+
if created_at:
72+
try:
73+
from datetime import datetime
74+
75+
dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
76+
created_display = dt.strftime("%B %d, %Y at %H:%M")
77+
except (ValueError, TypeError):
78+
created_display = created_at
79+
else:
80+
created_display = "Unknown"
81+
82+
# Status with emoji
83+
status_display = status
84+
if status == "COMPLETE":
85+
status_display = "✅ Complete"
86+
elif status == "RUNNING":
87+
status_display = "🏃 Running"
88+
elif status == "FAILED":
89+
status_display = "❌ Failed"
90+
elif status == "STOPPED":
91+
status_display = "⏹️ Stopped"
92+
elif status == "PENDING":
93+
status_display = "⏳ Pending"
94+
95+
# Create result display
96+
result_info = []
97+
result_info.append(f"[cyan]Agent Run ID:[/cyan] {run_id}")
98+
result_info.append(f"[cyan]Status:[/cyan] {status_display}")
99+
result_info.append(f"[cyan]Created:[/cyan] {created_display}")
100+
if web_url:
101+
result_info.append(f"[cyan]Web URL:[/cyan] {web_url}")
102+
103+
result_text = "\n".join(result_info)
104+
105+
console.print(
106+
Panel(
107+
result_text,
108+
title="🤖 [bold]Agent Run Created[/bold]",
109+
border_style="green",
110+
box=box.ROUNDED,
111+
padding=(1, 2),
112+
)
113+
)
114+
115+
# Show next steps
116+
console.print("\n[dim]💡 Track progress with:[/dim] [cyan]codegen agents[/cyan]")
117+
if web_url:
118+
console.print(f"[dim]🌐 View in browser:[/dim] [link]{web_url}[/link]")
119+
120+
except requests.RequestException as e:
121+
console.print(f"[red]Error creating agent run:[/red] {e}", style="bold red")
122+
if hasattr(e, "response") and e.response is not None:
123+
try:
124+
error_detail = e.response.json().get("detail", "Unknown error")
125+
console.print(f"[red]Details:[/red] {error_detail}")
126+
except (ValueError, KeyError):
127+
pass
128+
raise typer.Exit(1)
129+
except Exception as e:
130+
console.print(f"[red]Unexpected error:[/red] {e}", style="bold red")
131+
raise typer.Exit(1)
132+
133+
134+
# Default callback for the agent app
135+
@agent_app.callback(invoke_without_command=True)
136+
def agent_callback(ctx: typer.Context):
137+
"""Create and manage individual agent runs."""
138+
if ctx.invoked_subcommand is None:
139+
# If no subcommand is provided, show help
140+
print(ctx.get_help())
141+
raise typer.Exit()
142+
143+
144+
# For backward compatibility, also allow `codegen agent --prompt "..."`
145+
def agent(
146+
prompt: str = typer.Option(None, "--prompt", "-p", help="The prompt to send to the agent"),
147+
org_id: int | None = typer.Option(None, help="Organization ID (defaults to CODEGEN_ORG_ID/REPOSITORY_ORG_ID or auto-detect)"),
148+
model: str | None = typer.Option(None, help="Model to use for this agent run (optional)"),
149+
repo_id: int | None = typer.Option(None, help="Repository ID to use for this agent run (optional)"),
150+
):
151+
"""Create a new agent run with the given prompt."""
152+
if prompt:
153+
# If prompt is provided, create the agent run
154+
create(prompt=prompt, org_id=org_id, model=model, repo_id=repo_id)
155+
else:
156+
# If no prompt, show help
157+
console.print("[red]Error:[/red] --prompt is required")
158+
console.print("Usage: [cyan]codegen agent --prompt 'Your prompt here'[/cyan]")
159+
raise typer.Exit(1)

src/codegen/cli/commands/agents/main.py

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,28 @@ def list_agents(org_id: int | None = typer.Option(None, help="Organization ID (d
3333
raise typer.Exit(1)
3434

3535
# Make API request to list agent runs with spinner
36-
spinner = create_spinner("Fetching agent runs...")
36+
spinner = create_spinner("Fetching your recent API agent runs...")
3737
spinner.start()
3838

3939
try:
4040
headers = {"Authorization": f"Bearer {token}"}
41+
# Filter to only API source type and current user's agent runs
42+
params = {
43+
"source_type": "API",
44+
# We'll get the user_id from the /users/me endpoint
45+
}
46+
47+
# First get the current user ID
48+
user_response = requests.get(f"{API_ENDPOINT.rstrip('/')}/v1/users/me", headers=headers)
49+
user_response.raise_for_status()
50+
user_data = user_response.json()
51+
user_id = user_data.get("id")
52+
53+
if user_id:
54+
params["user_id"] = user_id
55+
4156
url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{resolved_org_id}/agent/runs"
42-
response = requests.get(url, headers=headers)
57+
response = requests.get(url, headers=headers, params=params)
4358
response.raise_for_status()
4459
response_data = response.json()
4560
finally:
@@ -52,42 +67,58 @@ def list_agents(org_id: int | None = typer.Option(None, help="Organization ID (d
5267
page_size = response_data.get("page_size", 10)
5368

5469
if not agent_runs:
55-
console.print("[yellow]No agent runs found.[/yellow]")
70+
console.print("[yellow]No API agent runs found for your user.[/yellow]")
5671
return
5772

5873
# Create a table to display agent runs
5974
table = Table(
60-
title=f"Agent Runs (Page {page}, Total: {total})",
75+
title=f"Your Recent API Agent Runs (Page {page}, Total: {total})",
6176
border_style="blue",
6277
show_header=True,
6378
title_justify="center",
6479
)
65-
table.add_column("ID", style="cyan", no_wrap=True)
66-
table.add_column("Status", style="white", justify="center")
67-
table.add_column("Source", style="magenta")
6880
table.add_column("Created", style="dim")
69-
table.add_column("Result", style="green")
81+
table.add_column("Status", style="white", justify="center")
82+
table.add_column("Summary", style="green")
83+
table.add_column("Link", style="blue")
7084

7185
# Add agent runs to table
7286
for agent_run in agent_runs:
7387
run_id = str(agent_run.get("id", "Unknown"))
7488
status = agent_run.get("status", "Unknown")
7589
source_type = agent_run.get("source_type", "Unknown")
7690
created_at = agent_run.get("created_at", "Unknown")
77-
result = agent_run.get("result", "")
7891

79-
# Status with emoji
80-
status_display = status
92+
# Extract summary from task_timeline_json, similar to frontend
93+
timeline = agent_run.get("task_timeline_json")
94+
summary = None
95+
if timeline and isinstance(timeline, dict) and "summary" in timeline:
96+
if isinstance(timeline["summary"], str):
97+
summary = timeline["summary"]
98+
99+
# Fall back to goal_prompt if no summary
100+
if not summary:
101+
summary = agent_run.get("goal_prompt", "")
102+
103+
# Status with colored circles
81104
if status == "COMPLETE":
82-
status_display = "✅ Complete"
105+
status_display = "[green]●[/green] Complete"
106+
elif status == "ACTIVE":
107+
status_display = "[dim]●[/dim] Active"
83108
elif status == "RUNNING":
84-
status_display = "🏃 Running"
109+
status_display = "[dim]●[/dim] Running"
110+
elif status == "CANCELLED":
111+
status_display = "[yellow]●[/yellow] Cancelled"
112+
elif status == "ERROR":
113+
status_display = "[red]●[/red] Error"
85114
elif status == "FAILED":
86-
status_display = " Failed"
115+
status_display = "[red]●[/red] Failed"
87116
elif status == "STOPPED":
88-
status_display = "⏹️ Stopped"
117+
status_display = "[yellow]●[/yellow] Stopped"
89118
elif status == "PENDING":
90-
status_display = "⏳ Pending"
119+
status_display = "[dim]●[/dim] Pending"
120+
else:
121+
status_display = "[dim]●[/dim] " + status
91122

92123
# Format created date (just show date and time, not full timestamp)
93124
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
102133
else:
103134
created_display = created_at
104135

105-
# Truncate result if too long
106-
result_display = result[:50] + "..." if result and len(result) > 50 else result or "No result"
136+
# Truncate summary if too long
137+
summary_display = summary[:50] + "..." if summary and len(summary) > 50 else summary or "No summary"
138+
139+
# Create web link for the agent run
140+
web_url = agent_run.get("web_url")
141+
if not web_url:
142+
# Construct URL if not provided
143+
web_url = f"https://codegen.com/traces/{run_id}"
144+
link_display = web_url
107145

108-
table.add_row(run_id, status_display, source_type, created_display, result_display)
146+
table.add_row(created_display, status_display, summary_display, link_display)
109147

110148
console.print(table)
111-
console.print(f"\n[green]Showing {len(agent_runs)} of {total} agent runs[/green]")
149+
console.print(f"\n[green]Showing {len(agent_runs)} of {total} API agent runs[/green]")
112150

113151
except requests.RequestException as e:
114152
console.print(f"[red]Error fetching agent runs:[/red] {e}", style="bold red")

0 commit comments

Comments
 (0)