Skip to content

Commit 2e42a08

Browse files
committed
.
1 parent 7bfa43c commit 2e42a08

File tree

3 files changed

+122
-280
lines changed

3 files changed

+122
-280
lines changed

src/codegen/cli/tui/app.py

Lines changed: 50 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -1,223 +1,56 @@
11
"""Main TUI application for Codegen CLI."""
22

33
import asyncio
4-
from datetime import UTC, datetime
4+
import webbrowser
55

66
from textual.app import App, ComposeResult
77
from textual.binding import Binding
8-
from textual.containers import Container, Horizontal, Vertical
9-
from textual.reactive import reactive
10-
from textual.widgets import DataTable, Footer, Header, Static, TabbedContent, TabPane
8+
from textual.containers import Container, Vertical
9+
from textual.widgets import DataTable, Footer, Header, Static
1110

1211
from codegen.cli.auth.token_manager import get_current_token
1312
from codegen.cli.utils.org import resolve_org_id
1413

1514

1615
class CodegenTUI(App):
17-
"""Main Codegen TUI Application."""
16+
"""Simple Codegen TUI for browsing agent runs."""
1817

19-
TITLE = "Codegen CLI"
2018
CSS_PATH = "codegen_tui.tcss"
21-
19+
TITLE = "Recent Agent Runs - Codegen CLI"
2220
BINDINGS = [
23-
Binding("q", "quit", "Quit"),
24-
Binding("r", "refresh", "Refresh"),
25-
Binding("ctrl+c", "quit", "Quit"),
21+
Binding("escape,ctrl+c", "quit", "Quit", priority=True),
22+
Binding("enter", "open_url", "Open", show=True),
23+
Binding("r", "refresh", "Refresh", show=True),
2624
]
2725

28-
# Reactive attributes
29-
org_id: reactive[int | None] = reactive(None)
30-
is_authenticated: reactive[bool] = reactive(False)
31-
3226
def __init__(self):
3327
super().__init__()
3428
self.token = get_current_token()
3529
self.is_authenticated = bool(self.token)
3630
if self.is_authenticated:
3731
self.org_id = resolve_org_id()
32+
self.agent_runs = []
3833

3934
def compose(self) -> ComposeResult:
40-
"""Compose the main UI."""
35+
"""Create child widgets for the app."""
4136
yield Header()
42-
4337
if not self.is_authenticated:
4438
yield Container(Static("⚠️ Not authenticated. Please run 'codegen login' first.", classes="warning-message"), id="auth-warning")
4539
else:
46-
with TabbedContent(initial="dashboard"):
47-
with TabPane("Dashboard", id="dashboard"):
48-
yield from self._compose_dashboard()
49-
50-
with TabPane("Agents", id="agents"):
51-
yield from self._compose_agents()
52-
53-
with TabPane("Integrations", id="integrations"):
54-
yield from self._compose_integrations()
55-
56-
with TabPane("Tools", id="tools"):
57-
yield from self._compose_tools()
58-
40+
with Vertical():
41+
yield Static("🤖 Your Recent API Agent Runs", classes="title")
42+
yield Static("Use ↑↓ to navigate, Enter to open, R to refresh, Esc to quit", classes="help")
43+
table = DataTable(id="agents-table", cursor_type="row")
44+
table.add_columns("Created", "Status", "Summary")
45+
yield table
5946
yield Footer()
6047

61-
def _compose_dashboard(self) -> ComposeResult:
62-
"""Compose the dashboard tab."""
63-
with Vertical():
64-
yield Static("📊 Dashboard", classes="tab-title")
65-
with Horizontal():
66-
yield Static("🏢 Organization:", classes="info-label")
67-
yield Static(str(self.org_id) if self.org_id else "Unknown", classes="info-value")
68-
with Horizontal():
69-
yield Static("🕐 Last Updated:", classes="info-label")
70-
yield Static(datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC"), classes="info-value")
71-
yield Static("🤖 Recent API Agent Runs", classes="tab-title")
72-
dashboard_table = DataTable(id="dashboard-agents-table")
73-
dashboard_table.add_columns("Created", "Status", "Summary", "Link")
74-
yield dashboard_table
75-
76-
def _compose_agents(self) -> ComposeResult:
77-
"""Compose the agents tab."""
78-
with Vertical():
79-
yield Static("🤖 Your Recent API Agent Runs", classes="tab-title")
80-
table = DataTable(id="agents-table")
81-
table.add_columns("Created", "Status", "Summary", "Link")
82-
yield table
83-
84-
def _compose_integrations(self) -> ComposeResult:
85-
"""Compose the integrations tab."""
86-
with Vertical():
87-
yield Static("🔌 Integrations", classes="tab-title")
88-
table = DataTable(id="integrations-table")
89-
table.add_columns("Integration", "Status", "Type", "Details")
90-
yield table
91-
92-
def _compose_tools(self) -> ComposeResult:
93-
"""Compose the tools tab."""
94-
with Vertical():
95-
yield Static("🛠️ Tools", classes="tab-title")
96-
table = DataTable(id="tools-table")
97-
table.add_columns("Tool Name", "Description", "Category")
98-
yield table
99-
100-
async def on_mount(self) -> None:
101-
"""Initialize the app on mount."""
102-
if self.is_authenticated:
103-
if self.org_id is None:
104-
self.notify("⚠️ No organization ID found. Set CODEGEN_ORG_ID or REPOSITORY_ORG_ID.", severity="warning")
105-
else:
106-
# Load initial data
107-
await self._load_all_data()
108-
109-
async def _load_all_data(self) -> None:
110-
"""Load data for all tabs."""
111-
if not self.is_authenticated or not self.org_id:
112-
return
113-
114-
try:
115-
# Load data in parallel
116-
await asyncio.gather(self._load_dashboard_data(), self._load_agents_data(), self._load_integrations_data(), self._load_tools_data(), return_exceptions=True)
117-
except Exception as e:
118-
self.notify(f"Error loading data: {e}", severity="error")
119-
120-
async def _load_dashboard_data(self) -> None:
121-
"""Load dashboard data (recent agent runs summary)."""
122-
table = self.query_one("#dashboard-agents-table", DataTable)
123-
table.clear()
124-
125-
if not self.token or not self.org_id:
126-
return
127-
128-
try:
129-
import requests
130-
131-
from codegen.cli.api.endpoints import API_ENDPOINT
132-
133-
headers = {"Authorization": f"Bearer {self.token}"}
134-
135-
# First get the current user ID
136-
user_response = requests.get(f"{API_ENDPOINT.rstrip('/')}/v1/users/me", headers=headers)
137-
user_response.raise_for_status()
138-
user_data = user_response.json()
139-
user_id = user_data.get("id")
140-
141-
# Filter to only API source type and current user's agent runs, limit to 5 for dashboard
142-
params = {
143-
"source_type": "API",
144-
"limit": 5, # Show only top 5 recent
145-
}
146-
147-
if user_id:
148-
params["user_id"] = user_id
149-
150-
# Fetch agent runs
151-
url = f"{API_ENDPOINT.rstrip('/')}/v1/organizations/{self.org_id}/agent/runs"
152-
response = requests.get(url, headers=headers, params=params)
153-
response.raise_for_status()
154-
response_data = response.json()
155-
156-
agent_runs = response_data.get("items", [])
157-
158-
for agent_run in agent_runs:
159-
run_id = str(agent_run.get("id", "Unknown"))
160-
status = agent_run.get("status", "Unknown")
161-
created_at = agent_run.get("created_at", "Unknown")
162-
163-
# Extract summary from task_timeline_json, similar to frontend
164-
timeline = agent_run.get("task_timeline_json")
165-
summary = None
166-
if timeline and isinstance(timeline, dict) and "summary" in timeline:
167-
if isinstance(timeline["summary"], str):
168-
summary = timeline["summary"]
169-
170-
# Fall back to goal_prompt if no summary
171-
if not summary:
172-
summary = agent_run.get("goal_prompt", "")
173-
174-
# Status with colored circles
175-
if status == "COMPLETE":
176-
status_display = "● Complete"
177-
elif status == "ACTIVE":
178-
status_display = "● Active"
179-
elif status == "RUNNING":
180-
status_display = "● Running"
181-
elif status == "CANCELLED":
182-
status_display = "● Cancelled"
183-
elif status == "ERROR":
184-
status_display = "● Error"
185-
elif status == "FAILED":
186-
status_display = "● Failed"
187-
elif status == "STOPPED":
188-
status_display = "● Stopped"
189-
elif status == "PENDING":
190-
status_display = "● Pending"
191-
else:
192-
status_display = "● " + status
193-
194-
# Format created date
195-
if created_at and created_at != "Unknown":
196-
try:
197-
from datetime import datetime
198-
199-
dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
200-
created_display = dt.strftime("%m/%d %H:%M")
201-
except (ValueError, TypeError):
202-
created_display = created_at[:16] if len(created_at) > 16 else created_at
203-
else:
204-
created_display = created_at
205-
206-
# Truncate summary if too long for dashboard
207-
summary_display = summary[:40] + "..." if summary and len(summary) > 40 else summary or "No summary"
208-
209-
# Create web link for the agent run
210-
web_url = agent_run.get("web_url")
211-
if not web_url:
212-
# Construct URL if not provided
213-
web_url = f"https://codegen.com/traces/{run_id}"
214-
link_display = web_url
215-
216-
table.add_row(created_display, status_display, summary_display, link_display)
217-
218-
except Exception as e:
219-
# If API call fails, show error in table
220-
table.add_row("Error", f"Failed to load: {e}", "", "")
48+
def on_mount(self) -> None:
49+
"""Called when app starts."""
50+
if self.is_authenticated and self.org_id:
51+
task = asyncio.create_task(self._load_agents_data())
52+
# Store reference to prevent garbage collection
53+
self._load_task = task
22154

22255
async def _load_agents_data(self) -> None:
22356
"""Load agents data into the table."""
@@ -243,7 +76,7 @@ async def _load_agents_data(self) -> None:
24376
# Filter to only API source type and current user's agent runs
24477
params = {
24578
"source_type": "API",
246-
"limit": 10, # Show recent 10
79+
"limit": 20, # Show recent 20
24780
}
24881

24982
if user_id:
@@ -256,11 +89,11 @@ async def _load_agents_data(self) -> None:
25689
response_data = response.json()
25790

25891
agent_runs = response_data.get("items", [])
92+
self.agent_runs = agent_runs # Store for URL opening
25993

26094
for agent_run in agent_runs:
26195
run_id = str(agent_run.get("id", "Unknown"))
26296
status = agent_run.get("status", "Unknown")
263-
source_type = agent_run.get("source_type", "Unknown")
26497
created_at = agent_run.get("created_at", "Unknown")
26598

26699
# Extract summary from task_timeline_json, similar to frontend
@@ -307,48 +140,38 @@ async def _load_agents_data(self) -> None:
307140
created_display = created_at
308141

309142
# Truncate summary if too long
310-
summary_display = summary[:30] + "..." if summary and len(summary) > 30 else summary or "No summary"
143+
summary_display = summary[:60] + "..." if summary and len(summary) > 60 else summary or "No summary"
311144

312-
# Create web link for the agent run
313-
web_url = agent_run.get("web_url")
314-
if not web_url:
315-
# Construct URL if not provided
316-
web_url = f"https://codegen.com/traces/{run_id}"
317-
link_display = web_url
318-
319-
table.add_row(created_display, status_display, summary_display, link_display)
145+
table.add_row(created_display, status_display, summary_display, key=run_id)
320146

321147
except Exception as e:
322148
# If API call fails, show error in table
323-
table.add_row("Error", f"Failed to load: {e}", "", "")
324-
325-
async def _load_integrations_data(self) -> None:
326-
"""Load integrations data into the table."""
327-
table = self.query_one("#integrations-table", DataTable)
328-
table.clear()
149+
table.add_row("Error", f"Failed to load: {e}", "")
329150

330-
# TODO: Implement actual API call
331-
# For now, add placeholder data
332-
table.add_row("GitHub", "✅ Active", "App Install", "Install ID: 12345")
333-
table.add_row("Slack", "✅ Active", "User Token", "Token ID: 67890")
334-
table.add_row("Linear", "❌ Inactive", "User Token", "No token")
335-
336-
async def _load_tools_data(self) -> None:
337-
"""Load tools data into the table."""
338-
table = self.query_one("#tools-table", DataTable)
339-
table.clear()
340-
341-
# TODO: Implement actual API call
342-
# For now, add placeholder data
343-
table.add_row("github_create_pr", "Create a pull request on GitHub", "GitHub")
344-
table.add_row("run_command", "Execute a shell command", "System")
345-
table.add_row("file_write", "Write content to a file", "File System")
151+
def action_open_url(self) -> None:
152+
"""Open the selected agent run URL."""
153+
table = self.query_one("#agents-table", DataTable)
154+
if table.cursor_row is not None and table.cursor_row < len(self.agent_runs):
155+
agent_run = self.agent_runs[table.cursor_row]
156+
run_id = agent_run.get("id")
157+
web_url = agent_run.get("web_url")
158+
159+
if not web_url:
160+
# Construct URL if not provided
161+
web_url = f"https://codegen.com/traces/{run_id}"
162+
163+
# Try to open URL
164+
try:
165+
webbrowser.open(web_url)
166+
self.notify(f"🌐 Opened {web_url}")
167+
except Exception as e:
168+
self.notify(f"❌ Failed to open URL: {e}", severity="error")
346169

347170
def action_refresh(self) -> None:
348-
"""Refresh all data."""
171+
"""Refresh agent runs data."""
349172
if self.is_authenticated and self.org_id:
350-
self.notify("🔄 Refreshing data...", timeout=1)
351-
task = asyncio.create_task(self._load_all_data())
173+
self.notify("🔄 Refreshing...", timeout=1)
174+
task = asyncio.create_task(self._load_agents_data())
352175
# Store reference to prevent garbage collection
353176
self._refresh_task = task
354177
else:
@@ -359,11 +182,7 @@ def action_quit(self) -> None:
359182
self.exit()
360183

361184

362-
def run_tui() -> None:
363-
"""Run the Codegen TUI application."""
185+
def run_tui():
186+
"""Run the Codegen TUI."""
364187
app = CodegenTUI()
365188
app.run()
366-
367-
368-
if __name__ == "__main__":
369-
run_tui()

0 commit comments

Comments
 (0)