@@ -24,8 +24,21 @@ pub struct BBSTuiState {
2424 pub scroll_offset : usize ,
2525 pub status_message : String ,
2626 pub connected : bool , // Track if we've initialized the connection
27- pub input_active : bool , // NEW: Track if input mode is active
28- pub selected_board_index : Option < usize > , // NEW: Track selected board for visual feedback
27+ pub input_active : bool , // Track if input mode is active
28+ pub selected_board_index : Option < usize > , // Track selected board for visual feedback
29+ // Agent integration
30+ pub agents : Vec < User > , // Known AI agents
31+ pub agent_status : AgentStatus , // Current agent listening status
32+ /// Cache of user_id -> User for displaying post authors
33+ pub user_cache : std:: collections:: HashMap < i32 , User > ,
34+ }
35+
36+ /// Agent listening status for the BBS
37+ #[ derive( Clone , Debug , Default ) ]
38+ pub struct AgentStatus {
39+ pub osvm_agent_online : bool ,
40+ pub last_agent_activity : Option < String > ,
41+ pub agents_listening : usize ,
2942}
3043
3144impl BBSTuiState {
@@ -40,9 +53,73 @@ impl BBSTuiState {
4053 scroll_offset : 0 ,
4154 status_message : "Connecting to BBS..." . to_string ( ) ,
4255 connected : false ,
43- input_active : false , // NEW
44- selected_board_index : Some ( 0 ) , // NEW: Default to first board
56+ input_active : false ,
57+ selected_board_index : Some ( 0 ) ,
58+ agents : Vec :: new ( ) ,
59+ agent_status : AgentStatus :: default ( ) ,
60+ user_cache : std:: collections:: HashMap :: new ( ) ,
61+ }
62+ }
63+
64+ /// Check if a user is an AI agent based on naming conventions
65+ pub fn is_agent ( user : & User ) -> bool {
66+ let short_upper = user. short_name . to_uppercase ( ) ;
67+ let long_lower = user. long_name . to_lowercase ( ) ;
68+
69+ // Agent detection heuristics:
70+ // 1. Short name patterns: OSVM, AI, BOT, AGT
71+ // 2. Long name contains: agent, bot, assistant, ai
72+ // 3. Node ID patterns: !aaaa (reserved for agents)
73+ short_upper == "OSVM" ||
74+ short_upper == "AI" ||
75+ short_upper == "BOT" ||
76+ short_upper == "AGT" ||
77+ short_upper == "TUI" || // TUI user is system
78+ long_lower. contains ( "agent" ) ||
79+ long_lower. contains ( "bot" ) ||
80+ long_lower. contains ( "assistant" ) ||
81+ user. node_id . starts_with ( "!aaaa" ) ||
82+ user. node_id . starts_with ( "!tui" )
83+ }
84+
85+ /// Load agents and update status
86+ pub fn refresh_agents ( & mut self ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
87+ if let Some ( ref mut conn) = * self . conn . lock ( ) . unwrap ( ) {
88+ // Get all users and filter for agents
89+ let all_users = db:: users:: list_all ( conn) ?;
90+ self . agents = all_users. iter ( )
91+ . filter ( |u| Self :: is_agent ( u) )
92+ . cloned ( )
93+ . collect ( ) ;
94+
95+ // Update agent status
96+ self . agent_status . agents_listening = self . agents . len ( ) ;
97+ self . agent_status . osvm_agent_online = self . agents . iter ( )
98+ . any ( |a| a. short_name . to_uppercase ( ) == "OSVM" ) ;
99+
100+ // Find most recent agent activity
101+ if let Some ( most_recent) = self . agents . iter ( )
102+ . filter_map ( |a| a. last_acted_at_us )
103+ . max ( )
104+ {
105+ self . agent_status . last_agent_activity = Some (
106+ crate :: utils:: bbs:: models:: User :: last_acted_at (
107+ & self . agents . iter ( ) . find ( |a| a. last_acted_at_us == Some ( most_recent) ) . unwrap ( )
108+ )
109+ ) ;
110+ }
111+
112+ // Cache all users for post author lookup
113+ for user in all_users {
114+ self . user_cache . insert ( user. id , user) ;
115+ }
45116 }
117+ Ok ( ( ) )
118+ }
119+
120+ /// Get user by ID from cache
121+ pub fn get_user ( & self , user_id : i32 ) -> Option < & User > {
122+ self . user_cache . get ( & user_id)
46123 }
47124
48125 /// Initialize BBS connection
@@ -56,8 +133,16 @@ impl BBSTuiState {
56133
57134 * self . conn . lock ( ) . unwrap ( ) = Some ( conn) ;
58135 self . connected = true ;
59- self . status_message = "Connected to OSVM BBS" . to_string ( ) ;
60136 self . refresh_boards ( ) ?;
137+ self . refresh_agents ( ) ?; // Load agent info
138+
139+ // Update status with agent info
140+ let agent_hint = if self . agent_status . agents_listening > 0 {
141+ format ! ( " | 🤖 {} agents listening" , self . agent_status. agents_listening)
142+ } else {
143+ String :: new ( )
144+ } ;
145+ self . status_message = format ! ( "Connected to OSVM BBS{}" , agent_hint) ;
61146
62147 Ok ( ( ) )
63148 }
@@ -137,13 +222,22 @@ pub fn render_bbs_tab(f: &mut Frame, area: Rect, state: &mut BBSTuiState) {
137222 ] )
138223 . split ( area) ;
139224
140- // Header with better info
225+ // Header with agent status
141226 let board_name = state. current_board . and_then ( |id| {
142227 state. boards . iter ( ) . find ( |b| b. id == id) . map ( |b| b. name . as_str ( ) )
143228 } ) . unwrap_or ( "No board selected" ) ;
144229
145- let header_text = format ! ( "📡 OSVM BBS - Meshtastic | Board: {} | {} posts" ,
146- board_name, state. posts. len( ) ) ;
230+ // Agent status indicator
231+ let agent_indicator = if state. agent_status . agents_listening > 0 {
232+ format ! ( " | 🤖 {} agent{} listening" ,
233+ state. agent_status. agents_listening,
234+ if state. agent_status. agents_listening == 1 { "" } else { "s" } )
235+ } else {
236+ " | 🔇 No agents" . to_string ( )
237+ } ;
238+
239+ let header_text = format ! ( "📡 OSVM BBS | Board: {} | {} posts{}" ,
240+ board_name, state. posts. len( ) , agent_indicator) ;
147241
148242 let header = Paragraph :: new ( header_text)
149243 . style ( Style :: default ( ) . fg ( Color :: Cyan ) . add_modifier ( Modifier :: BOLD ) )
@@ -188,23 +282,57 @@ pub fn render_bbs_tab(f: &mut Frame, area: Rect, state: &mut BBSTuiState) {
188282 . border_style ( Style :: default ( ) . fg ( Color :: Cyan ) ) ) ;
189283 f. render_widget ( board_list, main_chunks[ 0 ] ) ;
190284
191- // Posts area with MUCH better formatting
285+ // Posts area with agent badges
192286 let post_lines: Vec < Line > = if state. posts . is_empty ( ) {
193287 vec ! [
194288 Line :: from( "" ) ,
195289 Line :: from( Span :: styled( " No posts yet in this board." , Style :: default ( ) . fg( Color :: DarkGray ) ) ) ,
196290 Line :: from( "" ) ,
197291 Line :: from( Span :: styled( " Press 'i' to write a new post!" , Style :: default ( ) . fg( Color :: Yellow ) ) ) ,
292+ Line :: from( "" ) ,
293+ if state. agent_status. agents_listening > 0 {
294+ Line :: from( Span :: styled( " 💡 Tip: Use @agent to get AI assistance!" , Style :: default ( ) . fg( Color :: Magenta ) ) )
295+ } else {
296+ Line :: from( Span :: styled( " 📝 Register an agent with: osvm bbs agent register <name>" , Style :: default ( ) . fg( Color :: DarkGray ) ) )
297+ } ,
198298 ]
199299 } else {
200300 state. posts . iter ( ) . enumerate ( ) . flat_map ( |( i, p) | {
301+ // Check if this post is from an agent
302+ let ( author_name, is_agent) = if let Some ( user) = state. get_user ( p. user_id ) {
303+ ( user. short_name . clone ( ) , BBSTuiState :: is_agent ( user) )
304+ } else {
305+ ( format ! ( "#{}" , p. user_id) , false )
306+ } ;
307+
308+ // Build author display with agent badge
309+ let author_spans = if is_agent {
310+ vec ! [
311+ Span :: styled( "🤖 " , Style :: default ( ) . fg( Color :: Magenta ) ) ,
312+ Span :: styled( author_name, Style :: default ( ) . fg( Color :: Magenta ) . add_modifier( Modifier :: BOLD ) ) ,
313+ ]
314+ } else {
315+ vec ! [
316+ Span :: styled( author_name, Style :: default ( ) . fg( Color :: Green ) ) ,
317+ ]
318+ } ;
319+
320+ let mut header_spans = vec ! [
321+ Span :: styled( format!( "#{} " , i + 1 ) , Style :: default ( ) . fg( Color :: Cyan ) . add_modifier( Modifier :: BOLD ) ) ,
322+ ] ;
323+ header_spans. extend ( author_spans) ;
324+ header_spans. push ( Span :: styled ( format ! ( " • {}" , p. created_at( ) ) , Style :: default ( ) . fg ( Color :: DarkGray ) ) ) ;
325+
326+ // Highlight agent posts with different body style
327+ let body_style = if is_agent {
328+ Style :: default ( ) . fg ( Color :: White )
329+ } else {
330+ Style :: default ( )
331+ } ;
332+
201333 vec ! [
202- Line :: from( vec![
203- Span :: styled( format!( "#{} " , i + 1 ) , Style :: default ( ) . fg( Color :: Cyan ) . add_modifier( Modifier :: BOLD ) ) ,
204- Span :: styled( format!( "user#{}" , p. user_id) , Style :: default ( ) . fg( Color :: Green ) ) ,
205- Span :: styled( format!( " • {}" , p. created_at( ) ) , Style :: default ( ) . fg( Color :: DarkGray ) ) ,
206- ] ) ,
207- Line :: from( Span :: raw( format!( " {}" , p. body) ) ) ,
334+ Line :: from( header_spans) ,
335+ Line :: from( Span :: styled( format!( " {}" , p. body) , body_style) ) ,
208336 Line :: from( "" ) , // Spacing
209337 ]
210338 } ) . collect ( )
@@ -219,12 +347,18 @@ pub fn render_bbs_tab(f: &mut Frame, area: Rect, state: &mut BBSTuiState) {
219347 . scroll ( ( state. scroll_offset as u16 , 0 ) ) ;
220348 f. render_widget ( posts_widget, main_chunks[ 1 ] ) ;
221349
222- // ACTUAL INPUT BOX (like Chat tab!)
350+ // Input box with agent hints
351+ let input_placeholder = if state. agent_status . agents_listening > 0 {
352+ "Press 'i' to write... (use @agent for AI help)"
353+ } else {
354+ "Press 'i' to write a message..."
355+ } ;
356+
223357 let input_text = if state. input_active {
224358 format ! ( "{}█" , state. input_buffer) // Show cursor
225359 } else {
226360 if state. input_buffer . is_empty ( ) {
227- "Press 'i' to write a message..." . to_string ( )
361+ input_placeholder . to_string ( )
228362 } else {
229363 state. input_buffer . clone ( )
230364 }
@@ -242,19 +376,34 @@ pub fn render_bbs_tab(f: &mut Frame, area: Rect, state: &mut BBSTuiState) {
242376 Style :: default ( ) . fg ( Color :: DarkGray )
243377 } ;
244378
379+ // Input title shows agent availability
380+ let input_title = if state. agent_status . agents_listening > 0 {
381+ " 📝 Message (🤖 agents available) "
382+ } else {
383+ " 📝 Message Input "
384+ } ;
385+
245386 let input_widget = Paragraph :: new ( input_text)
246387 . style ( input_style)
247388 . block ( Block :: default ( )
248389 . borders ( Borders :: ALL )
249- . title ( " 📝 Message Input " )
390+ . title ( input_title )
250391 . border_style ( border_style) ) ;
251392 f. render_widget ( input_widget, chunks[ 2 ] ) ;
252393
253- // Status bar with USEFUL info
394+ // Status bar - show agent-aware help
254395 let status_text = if state. input_active {
255- "Press Enter to send • Esc to cancel • Backspace to delete"
396+ if state. agent_status . agents_listening > 0 {
397+ "Enter=Send • Esc=Cancel • @agent for AI help • Backspace=Delete"
398+ } else {
399+ "Press Enter to send • Esc to cancel • Backspace to delete"
400+ }
256401 } else {
257- "i=Input • j/k=Scroll • 1-9=Board • r=Refresh • ?=Help"
402+ if state. agent_status . agents_listening > 0 {
403+ "i=Input • j/k=Scroll • 1-9=Board • r=Refresh • 🤖 Agents listening!"
404+ } else {
405+ "i=Input • j/k=Scroll • 1-9=Board • r=Refresh • q=Quit"
406+ }
258407 } ;
259408
260409 let status = Paragraph :: new ( status_text)
@@ -412,6 +561,57 @@ mod tests {
412561 assert ! ( !state. connected) ;
413562 assert ! ( !state. input_active) ;
414563 assert_eq ! ( state. selected_board_index, Some ( 0 ) ) ;
564+ // New agent-related fields
565+ assert ! ( state. agents. is_empty( ) ) ;
566+ assert_eq ! ( state. agent_status. agents_listening, 0 ) ;
567+ assert ! ( !state. agent_status. osvm_agent_online) ;
568+ assert ! ( state. user_cache. is_empty( ) ) ;
569+ }
570+
571+ #[ test]
572+ fn test_is_agent_detection ( ) {
573+ // Test agent detection heuristics
574+ let osvm_agent = User {
575+ id : 1 ,
576+ node_id : "!aaaabbbb" . to_string ( ) ,
577+ short_name : "OSVM" . to_string ( ) ,
578+ long_name : "OSVM Research Agent" . to_string ( ) ,
579+ jackass : false ,
580+ in_board : None ,
581+ created_at_us : 0 ,
582+ last_seen_at_us : 0 ,
583+ last_acted_at_us : None ,
584+ bio : None ,
585+ } ;
586+ assert ! ( BBSTuiState :: is_agent( & osvm_agent) , "OSVM should be detected as agent" ) ;
587+
588+ let bot_user = User {
589+ id : 2 ,
590+ node_id : "!12345678" . to_string ( ) ,
591+ short_name : "BOT" . to_string ( ) ,
592+ long_name : "Some Bot" . to_string ( ) ,
593+ jackass : false ,
594+ in_board : None ,
595+ created_at_us : 0 ,
596+ last_seen_at_us : 0 ,
597+ last_acted_at_us : None ,
598+ bio : None ,
599+ } ;
600+ assert ! ( BBSTuiState :: is_agent( & bot_user) , "BOT should be detected as agent" ) ;
601+
602+ let regular_user = User {
603+ id : 3 ,
604+ node_id : "!deadbeef" . to_string ( ) ,
605+ short_name : "USER" . to_string ( ) ,
606+ long_name : "Regular Human" . to_string ( ) ,
607+ jackass : false ,
608+ in_board : None ,
609+ created_at_us : 0 ,
610+ last_seen_at_us : 0 ,
611+ last_acted_at_us : None ,
612+ bio : None ,
613+ } ;
614+ assert ! ( !BBSTuiState :: is_agent( & regular_user) , "Regular user should NOT be detected as agent" ) ;
415615 }
416616
417617 // =========================================================================
0 commit comments