@@ -39,6 +39,8 @@ pub struct AgentStatus {
3939 pub osvm_agent_online : bool ,
4040 pub last_agent_activity : Option < String > ,
4141 pub agents_listening : usize ,
42+ pub meshtastic_connected : bool ,
43+ pub meshtastic_node_id : Option < String > ,
4244}
4345
4446impl BBSTuiState {
@@ -122,6 +124,29 @@ impl BBSTuiState {
122124 self . user_cache . get ( & user_id)
123125 }
124126
127+ /// Try to connect to Meshtastic radio (optional)
128+ /// This doesn't fail if Meshtastic is unavailable - just sets status
129+ pub fn try_connect_meshtastic ( & mut self , address : Option < & str > ) {
130+ use crate :: utils:: bbs:: meshtastic:: { MeshtasticRadio , ConnectionState } ;
131+
132+ let addr = address. unwrap_or ( "localhost:4403" ) ;
133+
134+ if let Some ( mut radio) = MeshtasticRadio :: from_address ( addr) {
135+ match radio. connect ( ) {
136+ Ok ( ( ) ) => {
137+ self . agent_status . meshtastic_connected = true ;
138+ self . agent_status . meshtastic_node_id = Some ( format ! ( "!{:08x}" , radio. our_node_id( ) ) ) ;
139+ self . status_message = format ! ( "📻 Meshtastic connected @ {}" , addr) ;
140+ }
141+ Err ( e) => {
142+ self . agent_status . meshtastic_connected = false ;
143+ // Don't show error - mesh is optional
144+ log:: debug!( "Meshtastic connection failed: {}" , e) ;
145+ }
146+ }
147+ }
148+ }
149+
125150 /// Initialize BBS connection
126151 pub fn connect ( & mut self ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
127152 if self . connected {
@@ -249,11 +274,20 @@ pub fn render_bbs_tab(f: &mut Frame, area: Rect, state: &mut BBSTuiState) {
249274 let main_chunks = Layout :: default ( )
250275 . direction ( Direction :: Horizontal )
251276 . constraints ( [
252- Constraint :: Percentage ( 25 ) , // Board list (smaller )
253- Constraint :: Percentage ( 75 ) , // Posts (bigger)
277+ Constraint :: Percentage ( 25 ) , // Sidebar (boards + agents )
278+ Constraint :: Percentage ( 75 ) , // Posts
254279 ] )
255280 . split ( chunks[ 1 ] ) ;
256281
282+ // Split sidebar into boards (top) and agent activity (bottom)
283+ let sidebar_chunks = Layout :: default ( )
284+ . direction ( Direction :: Vertical )
285+ . constraints ( [
286+ Constraint :: Percentage ( 50 ) , // Boards
287+ Constraint :: Percentage ( 50 ) , // Agent activity
288+ ] )
289+ . split ( main_chunks[ 0 ] ) ;
290+
257291 // Board list with CLEAR selection indicators
258292 let boards: Vec < ListItem > = state
259293 . boards
@@ -269,7 +303,7 @@ pub fn render_bbs_tab(f: &mut Frame, area: Rect, state: &mut BBSTuiState) {
269303 } ;
270304
271305 ListItem :: new ( Line :: from ( vec ! [
272- Span :: styled( format!( "{}{} " , prefix, idx + 1 ) , style) , // Show 1-based index
306+ Span :: styled( format!( "{}{} " , prefix, idx + 1 ) , style) ,
273307 Span :: styled( & board. name, style) ,
274308 ] ) )
275309 } )
@@ -278,9 +312,74 @@ pub fn render_bbs_tab(f: &mut Frame, area: Rect, state: &mut BBSTuiState) {
278312 let board_list = List :: new ( boards)
279313 . block ( Block :: default ( )
280314 . borders ( Borders :: ALL )
281- . title ( " Boards (1-9 to select ) " )
315+ . title ( " Boards (1-9) " )
282316 . border_style ( Style :: default ( ) . fg ( Color :: Cyan ) ) ) ;
283- f. render_widget ( board_list, main_chunks[ 0 ] ) ;
317+ f. render_widget ( board_list, sidebar_chunks[ 0 ] ) ;
318+
319+ // Agent Activity Panel
320+ let mut agent_lines: Vec < Line > = Vec :: new ( ) ;
321+
322+ // Meshtastic status at top
323+ let mesh_status = if state. agent_status . meshtastic_connected {
324+ Line :: from ( vec ! [
325+ Span :: styled( " 📻 " , Style :: default ( ) . fg( Color :: Green ) ) ,
326+ Span :: styled( "Mesh: " , Style :: default ( ) . fg( Color :: Green ) ) ,
327+ Span :: styled(
328+ state. agent_status. meshtastic_node_id. as_deref( ) . unwrap_or( "Connected" ) ,
329+ Style :: default ( ) . fg( Color :: Green ) . add_modifier( Modifier :: BOLD )
330+ ) ,
331+ ] )
332+ } else {
333+ Line :: from ( vec ! [
334+ Span :: styled( " 📻 " , Style :: default ( ) . fg( Color :: DarkGray ) ) ,
335+ Span :: styled( "Mesh: Offline" , Style :: default ( ) . fg( Color :: DarkGray ) ) ,
336+ ] )
337+ } ;
338+ agent_lines. push ( mesh_status) ;
339+ agent_lines. push ( Line :: from ( "" ) ) ;
340+
341+ // Agent list
342+ if state. agents . is_empty ( ) {
343+ agent_lines. push ( Line :: from ( Span :: styled ( " No agents" , Style :: default ( ) . fg ( Color :: DarkGray ) ) ) ) ;
344+ agent_lines. push ( Line :: from ( "" ) ) ;
345+ agent_lines. push ( Line :: from ( Span :: styled ( " osvm bbs agent" , Style :: default ( ) . fg ( Color :: Yellow ) ) ) ) ;
346+ agent_lines. push ( Line :: from ( Span :: styled ( " register <name>" , Style :: default ( ) . fg ( Color :: Yellow ) ) ) ) ;
347+ } else {
348+ agent_lines. push ( Line :: from ( Span :: styled (
349+ format ! ( " {} agent{}" , state. agents. len( ) , if state. agents. len( ) == 1 { "" } else { "s" } ) ,
350+ Style :: default ( ) . fg ( Color :: Green )
351+ ) ) ) ;
352+
353+ // Show each agent (compact)
354+ for agent in state. agents . iter ( ) . take ( 4 ) {
355+ let status_icon = if agent. last_acted_at_us . is_some ( ) { "●" } else { "○" } ;
356+ let status_color = if agent. last_acted_at_us . is_some ( ) { Color :: Green } else { Color :: DarkGray } ;
357+
358+ agent_lines. push ( Line :: from ( vec ! [
359+ Span :: styled( format!( " {} " , status_icon) , Style :: default ( ) . fg( status_color) ) ,
360+ Span :: styled( "🤖 " , Style :: default ( ) . fg( Color :: Magenta ) ) ,
361+ Span :: styled( & agent. short_name, Style :: default ( ) . fg( Color :: Magenta ) . add_modifier( Modifier :: BOLD ) ) ,
362+ ] ) ) ;
363+ }
364+
365+ if state. agents . len ( ) > 4 {
366+ agent_lines. push ( Line :: from ( Span :: styled (
367+ format ! ( " +{} more" , state. agents. len( ) - 4 ) ,
368+ Style :: default ( ) . fg ( Color :: DarkGray )
369+ ) ) ) ;
370+ }
371+ } ;
372+
373+ let agent_panel = Paragraph :: new ( agent_lines)
374+ . block ( Block :: default ( )
375+ . borders ( Borders :: ALL )
376+ . title ( " 🤖 Agents " )
377+ . border_style ( if state. agent_status . agents_listening > 0 {
378+ Style :: default ( ) . fg ( Color :: Magenta )
379+ } else {
380+ Style :: default ( ) . fg ( Color :: DarkGray )
381+ } ) ) ;
382+ f. render_widget ( agent_panel, sidebar_chunks[ 1 ] ) ;
284383
285384 // Posts area with agent badges
286385 let post_lines: Vec < Line > = if state. posts . is_empty ( ) {
0 commit comments