11"""Main TUI application for Codegen CLI."""
22
33import asyncio
4- from datetime import UTC , datetime
4+ import webbrowser
55
66from textual .app import App , ComposeResult
77from 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
1211from codegen .cli .auth .token_manager import get_current_token
1312from codegen .cli .utils .org import resolve_org_id
1413
1514
1615class 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