Skip to main content

codetether_agent/tui/
protocol_registry_view.rs

1use ratatui::Frame;
2use ratatui::layout::Rect;
3use ratatui::style::{Color, Modifier, Style};
4use ratatui::text::{Line, Span};
5use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
6
7use crate::a2a::types::AgentCard;
8use crate::tui::app::state::App;
9
10pub fn render_protocol_registry(f: &mut Frame, app: &mut App, area: Rect) {
11    let cards = collect_agent_cards(app);
12    let chunks = ratatui::layout::Layout::default()
13        .direction(ratatui::layout::Direction::Horizontal)
14        .constraints([
15            ratatui::layout::Constraint::Percentage(40),
16            ratatui::layout::Constraint::Percentage(60),
17        ])
18        .split(area);
19    f.render_widget(list_widget(&cards), chunks[0]);
20    let block = Block::default()
21        .borders(Borders::ALL)
22        .title("Agent Details");
23    let inner = block.inner(chunks[1]);
24    f.render_widget(block, chunks[1]);
25    let body = detail_widget(app, &cards).unwrap_or_else(empty_widget);
26    f.render_widget(body, inner);
27}
28
29fn collect_agent_cards(app: &App) -> Vec<AgentCard> {
30    let mut names = app
31        .state
32        .worker_bridge_registered_agents
33        .iter()
34        .cloned()
35        .collect::<Vec<_>>();
36    names.sort();
37    names
38        .into_iter()
39        .map(|name| agent_card(app, name))
40        .collect()
41}
42
43fn agent_card(app: &App, name: String) -> AgentCard {
44    AgentCard {
45        name,
46        description: String::new(),
47        url: app
48            .state
49            .worker_id
50            .clone()
51            .unwrap_or_else(|| "unknown".into()),
52        version: "0.3.0".into(),
53        protocol_version: "0.3.0".into(),
54        preferred_transport: None,
55        additional_interfaces: vec![],
56        capabilities: Default::default(),
57        skills: vec![],
58        default_input_modes: vec![],
59        default_output_modes: vec![],
60        provider: None,
61        icon_url: None,
62        documentation_url: None,
63        security_schemes: Default::default(),
64        security: vec![],
65        supports_authenticated_extended_card: false,
66        signatures: vec![],
67    }
68}
69
70fn list_widget(cards: &[AgentCard]) -> List<'static> {
71    let items = cards.iter().map(list_item).collect::<Vec<_>>();
72    List::new(items).block(
73        Block::default()
74            .borders(Borders::ALL)
75            .title(list_title(cards)),
76    )
77}
78
79fn list_title(cards: &[AgentCard]) -> String {
80    format!("Agent Cards ({})", cards.len())
81}
82
83fn list_item(card: &AgentCard) -> ListItem<'static> {
84    let dot = if card.capabilities.streaming {
85        "● "
86    } else {
87        "○ "
88    };
89    let color = if card.capabilities.streaming {
90        Color::Green
91    } else {
92        Color::Yellow
93    };
94    let line = Line::from(vec![
95        Span::styled(dot, Style::default().fg(color)),
96        Span::styled(
97            card.name.clone(),
98            Style::default().add_modifier(Modifier::BOLD),
99        ),
100        Span::raw("  "),
101        Span::styled(card.url.clone(), Style::default().fg(Color::DarkGray)),
102    ]);
103    ListItem::new(line)
104}
105
106fn detail_widget(app: &App, cards: &[AgentCard]) -> Option<Paragraph<'static>> {
107    let idx = app
108        .state
109        .protocol_selected
110        .min(cards.len().saturating_sub(1));
111    let card = cards.get(idx)?;
112    Some(
113        Paragraph::new(detail_lines(card))
114            .wrap(Wrap { trim: false })
115            .scroll((app.state.protocol_scroll as u16, 0)),
116    )
117}
118
119fn detail_lines(card: &AgentCard) -> Vec<Line<'static>> {
120    vec![
121        field("Name", card.name.clone()),
122        Line::from(""),
123        field("URL", card.url.clone()),
124        Line::from(""),
125        field("Version", card.version.clone()),
126        field("Protocol", card.protocol_version.clone()),
127        Line::from(""),
128        Line::from(Span::styled(
129            "─ Capabilities ─",
130            Style::default().fg(Color::Cyan),
131        )),
132        field("Streaming", yes_no(card.capabilities.streaming)),
133        field("Push Notif", yes_no(card.capabilities.push_notifications)),
134        field(
135            "Transitions",
136            yes_no(card.capabilities.state_transition_history),
137        ),
138    ]
139}
140
141fn field(label: &str, value: String) -> Line<'static> {
142    Line::from(vec![
143        Span::styled(
144            label.to_string(),
145            Style::default().add_modifier(Modifier::BOLD),
146        ),
147        Span::raw("  "),
148        Span::raw(value),
149    ])
150}
151
152fn yes_no(value: bool) -> String {
153    if value { "yes" } else { "no" }.to_string()
154}
155
156fn empty_widget<'a>() -> Paragraph<'a> {
157    Paragraph::new("No agent cards registered.\nConnect to an A2A server to see cards here.")
158        .wrap(Wrap { trim: false })
159}