codetether_agent/tui/
protocol_registry_view.rs1use 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}