@@ -96,7 +96,8 @@ class FuzzForgeApp(App[None]):
9696
9797 /* Modal screens */
9898 AgentSetupScreen, AgentUnlinkScreen,
99- HubManagerScreen, LinkHubScreen, CloneHubScreen {
99+ HubManagerScreen, LinkHubScreen, CloneHubScreen,
100+ BuildImageScreen {
100101 align: center middle;
101102 }
102103
@@ -130,6 +131,30 @@ class FuzzForgeApp(App[None]):
130131 overflow-y: auto;
131132 }
132133
134+ #build-dialog {
135+ width: 100;
136+ height: 80%;
137+ border: thick #4699fc;
138+ background: $surface;
139+ padding: 2 3;
140+ }
141+
142+ #build-log {
143+ height: 1fr;
144+ border: round $panel;
145+ margin: 1 0;
146+ }
147+
148+ #build-subtitle {
149+ color: $text-muted;
150+ margin-bottom: 1;
151+ }
152+
153+ #build-status {
154+ height: 1;
155+ margin-top: 1;
156+ }
157+
133158 .dialog-title {
134159 text-style: bold;
135160 text-align: center;
@@ -168,6 +193,7 @@ class FuzzForgeApp(App[None]):
168193 Binding ("q" , "quit" , "Quit" ),
169194 Binding ("h" , "manage_hubs" , "Hub Manager" ),
170195 Binding ("r" , "refresh" , "Refresh" ),
196+ Binding ("enter" , "select_row" , "Select" , show = False ),
171197 ]
172198
173199 def compose (self ) -> ComposeResult :
@@ -194,7 +220,9 @@ def compose(self) -> ComposeResult:
194220 def on_mount (self ) -> None :
195221 """Populate tables on startup."""
196222 self ._agent_rows : list [_AgentRow ] = []
197- self .query_one ("#hub-panel" ).border_title = "Hub Servers"
223+ # hub row data: (server_name, image, hub_name) | None for group headers
224+ self ._hub_rows : list [tuple [str , str , str ] | None ] = []
225+ self .query_one ("#hub-panel" ).border_title = "Hub Servers [dim](Enter to build)[/dim]"
198226 self .query_one ("#agents-panel" ).border_title = "AI Agents"
199227 self ._refresh_agents ()
200228 self ._refresh_hub ()
@@ -220,6 +248,7 @@ def _refresh_agents(self) -> None:
220248
221249 def _refresh_hub (self ) -> None :
222250 """Refresh the hub servers table, grouped by source hub."""
251+ self ._hub_rows = []
223252 table = self .query_one ("#hub-table" , DataTable )
224253 table .clear (columns = True )
225254 table .add_columns ("Server" , "Image" , "Hub" , "Status" )
@@ -275,6 +304,7 @@ def _refresh_hub(self) -> None:
275304 style = "bold" ,
276305 )
277306 table .add_row (header , "" , "" , "" )
307+ self ._hub_rows .append (None ) # group header — not selectable
278308
279309 # Tool rows
280310 for server , is_ready , status_text in statuses :
@@ -287,21 +317,25 @@ def _refresh_hub(self) -> None:
287317 elif is_ready :
288318 status_cell = Text ("✓ Ready" , style = "green" )
289319 else :
290- status_cell = Text (f"✗ { status_text } " , style = "red" )
320+ status_cell = Text (f"✗ { status_text } " , style = "red dim " )
291321
292322 table .add_row (
293323 f" { name } " ,
294324 Text (image , style = "dim" ),
295325 hub_name ,
296326 status_cell ,
297327 )
328+ self ._hub_rows .append ((name , image , hub_name ))
298329
299330 def on_data_table_row_selected (self , event : DataTable .RowSelected ) -> None :
300- """Handle row selection on the agents table."""
301- if event .data_table .id != "agents-table" :
302- return
303-
304- idx = event .cursor_row
331+ """Handle row selection on agents and hub tables."""
332+ if event .data_table .id == "agents-table" :
333+ self ._handle_agent_row (event .cursor_row )
334+ elif event .data_table .id == "hub-table" :
335+ self ._handle_hub_row (event .cursor_row )
336+
337+ def _handle_agent_row (self , idx : int ) -> None :
338+ """Open agent setup/unlink for the selected agent row."""
305339 if idx < 0 or idx >= len (self ._agent_rows ):
306340 return
307341
@@ -322,6 +356,32 @@ def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
322356 callback = self ._on_agent_changed ,
323357 )
324358
359+ def _handle_hub_row (self , idx : int ) -> None :
360+ """Open the build dialog for the selected hub tool row."""
361+ if idx < 0 or idx >= len (self ._hub_rows ):
362+ return
363+ row_data = self ._hub_rows [idx ]
364+ if row_data is None :
365+ return # group header row — ignore
366+
367+ server_name , image , hub_name = row_data
368+ if hub_name == "manual" :
369+ self .notify ("Manual servers must be built outside FuzzForge" )
370+ return
371+
372+ from fuzzforge_cli .tui .screens .build_image import BuildImageScreen
373+
374+ self .push_screen (
375+ BuildImageScreen (server_name , image , hub_name ),
376+ callback = self ._on_image_built ,
377+ )
378+
379+ def _on_image_built (self , success : bool ) -> None :
380+ """Refresh hub status after a build attempt."""
381+ self ._refresh_hub ()
382+ if success :
383+ self .notify ("Image built successfully" , severity = "information" )
384+
325385 def on_button_pressed (self , event : Button .Pressed ) -> None :
326386 """Handle button presses."""
327387 if event .button .id == "btn-hub-manager" :
0 commit comments