Skip to content

Commit fdba80f

Browse files
0xrinegadeclaude
andcommitted
feat(bbs): Add agent activity panel and Meshtastic integration
Agent Activity Panel (sidebar): - Split sidebar into Boards (top) and Agents (bottom) - Show Meshtastic connection status: "📻 Mesh: Offline/Connected" - Display online agent count with status indicators - Show each agent with 🤖 badge, ● (active) or ○ (idle) - Compact layout showing up to 4 agents with "+N more" overflow - Registration hint when no agents present Meshtastic Integration: - Add --mesh/-m flag to 'osvm bbs tui' command - Optional address parameter (default: localhost:4403) - Non-blocking connection attempt - TUI works without mesh - Status reflected in agent panel UI Layout Changes: - Sidebar now 50/50 split between boards and agents - Agent panel border turns magenta when agents online - Meshtastic status always visible at top of agent panel Usage: osvm bbs tui # Without mesh osvm bbs tui --mesh # Connect to localhost:4403 osvm bbs tui -m 192.168.1.100 # Connect to specific IP 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent b153cf3 commit fdba80f

File tree

3 files changed

+120
-5
lines changed

3 files changed

+120
-5
lines changed

src/clparse/bbs.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,15 @@ pub fn build_bbs_command() -> Command {
315315
.help("Initial board to open")
316316
.default_value("GENERAL")
317317
.index(1),
318+
)
319+
.arg(
320+
Arg::new("mesh")
321+
.long("mesh")
322+
.short('m')
323+
.value_name("ADDRESS")
324+
.help("Connect to Meshtastic radio (e.g., 192.168.1.100:4403)")
325+
.num_args(0..=1)
326+
.default_missing_value("localhost:4403"),
318327
),
319328
)
320329
// Stats and info

src/commands/bbs_handler.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,7 @@ async fn handle_tui(matches: &ArgMatches) -> Result<()> {
758758
use std::time::Duration;
759759

760760
let initial_board = matches.get_one::<String>("board").unwrap();
761+
let mesh_address = matches.get_one::<String>("mesh");
761762

762763
// Initialize BBS state
763764
let mut bbs_state = crate::utils::bbs::tui_widgets::BBSTuiState::new();
@@ -767,6 +768,12 @@ async fn handle_tui(matches: &ArgMatches) -> Result<()> {
767768
return Err(anyhow!("Failed to connect to BBS database: {}\nRun 'osvm bbs init' first.", e));
768769
}
769770

771+
// Try to connect to Meshtastic if --mesh flag provided
772+
if let Some(addr) = mesh_address {
773+
println!("📻 Connecting to Meshtastic @ {}...", addr);
774+
bbs_state.try_connect_meshtastic(Some(addr));
775+
}
776+
770777
// Find and select the initial board
771778
if let Some(idx) = bbs_state.boards.iter().position(|b| b.name.eq_ignore_ascii_case(initial_board)) {
772779
let board_id = bbs_state.boards[idx].id;

src/utils/bbs/tui_widgets.rs

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4446
impl 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

Comments
 (0)