4runr-os 2.4.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,247 @@
1
+ /// Agent List Screen
2
+ /// View and manage AI agents
3
+
4
+ use ratatui::prelude::*;
5
+ use ratatui::widgets::{Block, Borders, Paragraph, Wrap, Table, Row, Cell};
6
+ use crate::app::{AppState, AgentInfo};
7
+
8
+ // === 4RUNR BRAND COLORS ===
9
+ const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
10
+ const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
11
+ const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
12
+ const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
13
+ const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
14
+ const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
15
+ const BG_PANEL: Color = Color::Rgb(18, 18, 25);
16
+
17
+ /// Render agent list screen
18
+ pub fn render(f: &mut Frame, state: &AppState) {
19
+ let area = f.size();
20
+
21
+ if state.agent_list.detail_view.is_some() {
22
+ // Render list view first (as base)
23
+ render_list_view(f, area, state);
24
+ // Then render detail popup on top
25
+ render_detail_popup(f, area, state);
26
+ } else {
27
+ // Render list view only
28
+ render_list_view(f, area, state);
29
+ }
30
+ }
31
+
32
+ fn render_list_view(f: &mut Frame, area: Rect, state: &AppState) {
33
+ use ratatui::layout::{Constraint, Direction, Layout};
34
+
35
+ // Split into header, content, footer
36
+ let chunks = Layout::default()
37
+ .direction(Direction::Vertical)
38
+ .constraints([
39
+ Constraint::Length(3), // Header
40
+ Constraint::Min(0), // Content
41
+ Constraint::Length(3), // Footer
42
+ ])
43
+ .split(area);
44
+
45
+ // === HEADER ===
46
+ let agent_count = state.agent_list.agents.len();
47
+ let header_block = Block::default()
48
+ .title(format!(" 🤖 Agent List ({} agents) ", agent_count))
49
+ .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
50
+ .borders(Borders::ALL)
51
+ .border_style(Style::default().fg(CYBER_CYAN))
52
+ .style(Style::default().bg(BG_PANEL));
53
+
54
+ f.render_widget(header_block, chunks[0]);
55
+
56
+ // === CONTENT: Agent Table ===
57
+ let content_block = Block::default()
58
+ .borders(Borders::ALL)
59
+ .border_style(Style::default().fg(TEXT_MUTED))
60
+ .style(Style::default().bg(BG_PANEL));
61
+
62
+ f.render_widget(content_block.clone(), chunks[1]);
63
+
64
+ let table_area = content_block.inner(chunks[1]);
65
+
66
+ // Create table rows
67
+ let rows: Vec<Row> = state.agent_list.agents.iter().enumerate().map(|(i, agent)| {
68
+ let is_selected = i == state.agent_list.selected_index;
69
+ let style = if is_selected {
70
+ Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)
71
+ } else {
72
+ Style::default().fg(TEXT_PRIMARY)
73
+ };
74
+
75
+ let name = if is_selected {
76
+ format!("▶ {}", agent.name)
77
+ } else {
78
+ format!(" {}", agent.name)
79
+ };
80
+
81
+ let desc = agent.description.as_deref().unwrap_or("No description")
82
+ .chars().take(30).collect::<String>();
83
+ let model = agent.model.chars().take(15).collect::<String>();
84
+ let provider = agent.provider.chars().take(10).collect::<String>();
85
+
86
+ Row::new(vec![
87
+ Cell::from(name).style(style),
88
+ Cell::from(desc).style(style),
89
+ Cell::from(model).style(style),
90
+ Cell::from(provider).style(style),
91
+ ])
92
+ }).collect();
93
+
94
+ let table = Table::new(
95
+ rows,
96
+ [
97
+ Constraint::Percentage(25),
98
+ Constraint::Percentage(35),
99
+ Constraint::Percentage(20),
100
+ Constraint::Percentage(20),
101
+ ]
102
+ )
103
+ .header(Row::new(vec![
104
+ Cell::from("Name").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
105
+ Cell::from("Description").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
106
+ Cell::from("Model").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
107
+ Cell::from("Provider").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
108
+ ]))
109
+ .column_spacing(1)
110
+ .style(Style::default().fg(TEXT_PRIMARY));
111
+
112
+ f.render_widget(table, table_area);
113
+
114
+ // === FOOTER ===
115
+ let footer_text = Line::from(vec![
116
+ Span::styled("↑/↓", Style::default().fg(CYBER_CYAN)),
117
+ Span::styled(" Navigate | ", Style::default().fg(TEXT_DIM)),
118
+ Span::styled("Enter", Style::default().fg(NEON_GREEN)),
119
+ Span::styled(" View Details | ", Style::default().fg(TEXT_DIM)),
120
+ Span::styled("ESC", Style::default().fg(BRAND_PURPLE)),
121
+ Span::styled(" Close", Style::default().fg(TEXT_DIM)),
122
+ ]);
123
+
124
+ f.render_widget(
125
+ Paragraph::new(footer_text)
126
+ .alignment(Alignment::Center)
127
+ .style(Style::default().bg(BG_PANEL)),
128
+ chunks[2]
129
+ );
130
+ }
131
+
132
+ fn render_detail_popup(f: &mut Frame, area: Rect, state: &AppState) {
133
+ let detail_index = state.agent_list.detail_view.unwrap();
134
+ if let Some(agent) = state.agent_list.agents.get(detail_index) {
135
+ render_agent_detail(f, area, agent);
136
+ }
137
+ }
138
+
139
+ fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
140
+ use ratatui::layout::{Constraint, Direction, Layout};
141
+
142
+ // Calculate popup size (70% width, 80% height, centered)
143
+ let popup_width = (area.width * 70 / 100).max(50);
144
+ let popup_height = (area.height * 80 / 100).max(15);
145
+ let popup_x = (area.width.saturating_sub(popup_width)) / 2;
146
+ let popup_y = (area.height.saturating_sub(popup_height)) / 2;
147
+
148
+ let popup_area = Rect {
149
+ x: popup_x,
150
+ y: popup_y,
151
+ width: popup_width,
152
+ height: popup_height,
153
+ };
154
+
155
+ // Render dimmed overlay
156
+ let overlay = Block::default()
157
+ .style(Style::default().bg(Color::Black).fg(Color::Black));
158
+ f.render_widget(overlay, area);
159
+
160
+ // Render detail popup
161
+ let detail_block = Block::default()
162
+ .title(format!(" 📋 Agent Details: {} ", agent.name))
163
+ .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
164
+ .borders(Borders::ALL)
165
+ .border_style(Style::default().fg(CYBER_CYAN))
166
+ .style(Style::default().bg(BG_PANEL));
167
+
168
+ f.render_widget(detail_block.clone(), popup_area);
169
+
170
+ let inner = detail_block.inner(popup_area);
171
+
172
+ // Create detail text
173
+ let mut detail_lines = vec![
174
+ Line::from(vec![
175
+ Span::styled("Name: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
176
+ Span::styled(&agent.name, Style::default().fg(NEON_GREEN)),
177
+ ]),
178
+ Line::from(""),
179
+ ];
180
+
181
+ if let Some(desc) = &agent.description {
182
+ detail_lines.push(Line::from(vec![
183
+ Span::styled("Description: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
184
+ Span::styled(desc, Style::default().fg(TEXT_PRIMARY)),
185
+ ]));
186
+ detail_lines.push(Line::from(""));
187
+ }
188
+
189
+ detail_lines.push(Line::from(vec![
190
+ Span::styled("Model: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
191
+ Span::styled(&agent.model, Style::default().fg(TEXT_PRIMARY)),
192
+ ]));
193
+ detail_lines.push(Line::from(vec![
194
+ Span::styled("Provider: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
195
+ Span::styled(&agent.provider, Style::default().fg(TEXT_PRIMARY)),
196
+ ]));
197
+
198
+ if let Some(temp) = agent.temperature {
199
+ detail_lines.push(Line::from(vec![
200
+ Span::styled("Temperature: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
201
+ Span::styled(format!("{:.2}", temp), Style::default().fg(TEXT_PRIMARY)),
202
+ ]));
203
+ }
204
+
205
+ if let Some(max_tokens) = agent.max_tokens {
206
+ detail_lines.push(Line::from(vec![
207
+ Span::styled("Max Tokens: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
208
+ Span::styled(format!("{}", max_tokens), Style::default().fg(TEXT_PRIMARY)),
209
+ ]));
210
+ }
211
+
212
+ if !agent.tools.is_empty() {
213
+ detail_lines.push(Line::from(""));
214
+ detail_lines.push(Line::from(vec![
215
+ Span::styled("Tools: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
216
+ ]));
217
+ for tool in &agent.tools {
218
+ detail_lines.push(Line::from(vec![
219
+ Span::raw(" • "),
220
+ Span::styled(tool, Style::default().fg(NEON_GREEN)),
221
+ ]));
222
+ }
223
+ }
224
+
225
+ if let Some(prompt) = &agent.system_prompt {
226
+ detail_lines.push(Line::from(""));
227
+ detail_lines.push(Line::from(vec![
228
+ Span::styled("System Prompt: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
229
+ ]));
230
+ // Show first 200 chars of prompt
231
+ let prompt_preview = prompt.chars().take(200).collect::<String>();
232
+ detail_lines.push(Line::from(format!(" {}", prompt_preview)).style(Style::default().fg(TEXT_DIM)));
233
+ }
234
+
235
+ detail_lines.push(Line::from(""));
236
+ detail_lines.push(Line::from(vec![
237
+ Span::styled("Press ", Style::default().fg(TEXT_DIM)),
238
+ Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
239
+ Span::styled(" to close", Style::default().fg(TEXT_DIM)),
240
+ ]));
241
+
242
+ f.render_widget(
243
+ Paragraph::new(detail_lines)
244
+ .wrap(Wrap { trim: false }),
245
+ inner
246
+ );
247
+ }
@@ -0,0 +1,353 @@
1
+ /// Help popup screen
2
+ /// Professional, organized command reference
3
+
4
+ use ratatui::prelude::*;
5
+ use ratatui::widgets::{Block, Borders, Paragraph, Wrap, List, ListItem};
6
+ use crate::app::AppState;
7
+
8
+ // === 4RUNR BRAND COLORS ===
9
+ const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
10
+ const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
11
+ const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
12
+ const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
13
+ const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
14
+ const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
15
+ const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
16
+ const BG_PANEL: Color = Color::Rgb(18, 18, 25);
17
+
18
+ /// Render help popup overlay
19
+ pub fn render_help(f: &mut Frame, area: Rect, _state: &AppState) {
20
+ // Calculate popup size (80% width, 85% height, centered)
21
+ let popup_width = (area.width * 80 / 100).max(60);
22
+ let popup_height = (area.height * 85 / 100).max(20);
23
+ let popup_x = (area.width.saturating_sub(popup_width)) / 2;
24
+ let popup_y = (area.height.saturating_sub(popup_height)) / 2;
25
+
26
+ let popup_area = Rect {
27
+ x: popup_x,
28
+ y: popup_y,
29
+ width: popup_width,
30
+ height: popup_height,
31
+ };
32
+
33
+ // Render background overlay (dimmed)
34
+ let overlay = Block::default()
35
+ .style(Style::default().bg(Color::Black).fg(Color::Black));
36
+ f.render_widget(overlay, area);
37
+
38
+ // Render help popup
39
+ render_help_content(f, popup_area);
40
+ }
41
+
42
+ fn render_help_content(f: &mut Frame, area: Rect) {
43
+ use ratatui::layout::{Constraint, Direction, Layout};
44
+
45
+ // Split into sections
46
+ let chunks = Layout::default()
47
+ .direction(Direction::Vertical)
48
+ .constraints([
49
+ Constraint::Length(3), // Header
50
+ Constraint::Min(0), // Content
51
+ Constraint::Length(1), // Footer
52
+ ])
53
+ .split(area);
54
+
55
+ // === HEADER ===
56
+ let header_block = Block::default()
57
+ .title(" 📖 4Runr AI Agent OS - Command Reference ")
58
+ .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
59
+ .borders(Borders::ALL)
60
+ .border_style(Style::default().fg(CYBER_CYAN))
61
+ .style(Style::default().bg(BG_PANEL));
62
+
63
+ let header_text = Line::from(vec![
64
+ Span::styled("Press ", Style::default().fg(TEXT_DIM)),
65
+ Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
66
+ Span::styled(" to close", Style::default().fg(TEXT_DIM)),
67
+ ]);
68
+
69
+ f.render_widget(header_block, chunks[0]);
70
+ f.render_widget(
71
+ Paragraph::new(header_text).alignment(Alignment::Right),
72
+ Rect {
73
+ x: chunks[0].x + 1,
74
+ y: chunks[0].y + 1,
75
+ width: chunks[0].width.saturating_sub(2),
76
+ height: 1,
77
+ }
78
+ );
79
+
80
+ // === CONTENT ===
81
+ let content_chunks = Layout::default()
82
+ .direction(Direction::Horizontal)
83
+ .constraints([
84
+ Constraint::Percentage(40), // Left: Command list
85
+ Constraint::Percentage(60), // Right: Details
86
+ ])
87
+ .split(chunks[1]);
88
+
89
+ // Left panel: Command categories
90
+ render_command_list(f, content_chunks[0]);
91
+
92
+ // Right panel: Command details
93
+ render_command_details(f, content_chunks[1]);
94
+
95
+ // === FOOTER ===
96
+ let footer_text = Line::from(vec![
97
+ Span::styled("Navigate: ", Style::default().fg(TEXT_DIM)),
98
+ Span::styled("↑/↓", Style::default().fg(CYBER_CYAN)),
99
+ Span::styled(" Select | ", Style::default().fg(TEXT_MUTED)),
100
+ Span::styled("ESC", Style::default().fg(NEON_GREEN)),
101
+ Span::styled(" Close", Style::default().fg(TEXT_DIM)),
102
+ ]);
103
+
104
+ f.render_widget(
105
+ Paragraph::new(footer_text)
106
+ .alignment(Alignment::Center)
107
+ .style(Style::default().bg(BG_PANEL)),
108
+ chunks[2]
109
+ );
110
+ }
111
+
112
+ fn render_command_list(f: &mut Frame, area: Rect) {
113
+ use ratatui::layout::{Constraint, Direction, Layout};
114
+
115
+ let chunks = Layout::default()
116
+ .direction(Direction::Vertical)
117
+ .constraints([
118
+ Constraint::Length(8), // Navigation Commands
119
+ Constraint::Length(7), // Local Commands
120
+ Constraint::Min(0), // WebSocket Commands
121
+ ])
122
+ .split(area);
123
+
124
+ // Navigation Commands section
125
+ let nav_block = Block::default()
126
+ .title(" 🧭 Navigation ")
127
+ .title_style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD))
128
+ .borders(Borders::ALL)
129
+ .border_style(Style::default().fg(CYBER_CYAN))
130
+ .style(Style::default().bg(BG_PANEL));
131
+
132
+ let nav_items = vec![
133
+ ListItem::new(Line::from(vec![
134
+ Span::styled("build", Style::default().fg(NEON_GREEN)),
135
+ ])),
136
+ ListItem::new(Line::from(vec![
137
+ Span::styled("runs", Style::default().fg(NEON_GREEN)),
138
+ ])),
139
+ ListItem::new(Line::from(vec![
140
+ Span::styled("config", Style::default().fg(NEON_GREEN)),
141
+ Span::styled(", ", Style::default().fg(TEXT_DIM)),
142
+ Span::styled("settings", Style::default().fg(NEON_GREEN)),
143
+ ])),
144
+ ListItem::new(Line::from(vec![
145
+ Span::styled("ESC", Style::default().fg(NEON_GREEN)),
146
+ ])),
147
+ ];
148
+
149
+ f.render_widget(nav_block.clone(), chunks[0]);
150
+ f.render_widget(
151
+ List::new(nav_items)
152
+ .style(Style::default().fg(TEXT_PRIMARY)),
153
+ nav_block.inner(chunks[0])
154
+ );
155
+
156
+ // Local Commands section
157
+ let local_block = Block::default()
158
+ .title(" 💻 Local ")
159
+ .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
160
+ .borders(Borders::ALL)
161
+ .border_style(Style::default().fg(BRAND_PURPLE))
162
+ .style(Style::default().bg(BG_PANEL));
163
+
164
+ let local_items = vec![
165
+ ListItem::new(Line::from(vec![
166
+ Span::styled("quit", Style::default().fg(NEON_GREEN)),
167
+ Span::styled(", ", Style::default().fg(TEXT_DIM)),
168
+ Span::styled("exit", Style::default().fg(NEON_GREEN)),
169
+ ])),
170
+ ListItem::new(Line::from(vec![
171
+ Span::styled("clear", Style::default().fg(NEON_GREEN)),
172
+ ])),
173
+ ListItem::new(Line::from(vec![
174
+ Span::styled("help", Style::default().fg(NEON_GREEN)),
175
+ ])),
176
+ ListItem::new(Line::from(vec![
177
+ Span::styled(":perf", Style::default().fg(NEON_GREEN)),
178
+ ])),
179
+ ];
180
+
181
+ f.render_widget(local_block.clone(), chunks[1]);
182
+ f.render_widget(
183
+ List::new(local_items)
184
+ .style(Style::default().fg(TEXT_PRIMARY)),
185
+ local_block.inner(chunks[1])
186
+ );
187
+
188
+ // WebSocket Commands section
189
+ let ws_block = Block::default()
190
+ .title(" 🌐 WebSocket (requires connection) ")
191
+ .title_style(Style::default().fg(AMBER_WARN).add_modifier(Modifier::BOLD))
192
+ .borders(Borders::ALL)
193
+ .border_style(Style::default().fg(AMBER_WARN))
194
+ .style(Style::default().bg(BG_PANEL));
195
+
196
+ let ws_items = vec![
197
+ ListItem::new(Line::from(vec![
198
+ Span::styled("agent.list", Style::default().fg(NEON_GREEN)),
199
+ ])),
200
+ ListItem::new(Line::from(vec![
201
+ Span::styled("agent.get", Style::default().fg(NEON_GREEN)),
202
+ ])),
203
+ ListItem::new(Line::from(vec![
204
+ Span::styled("agent.create", Style::default().fg(NEON_GREEN)),
205
+ ])),
206
+ ListItem::new(Line::from(vec![
207
+ Span::styled("agent.delete", Style::default().fg(NEON_GREEN)),
208
+ ])),
209
+ ListItem::new(Line::from(vec![
210
+ Span::styled("system.status", Style::default().fg(NEON_GREEN)),
211
+ ])),
212
+ ListItem::new(Line::from(vec![
213
+ Span::styled("run.list", Style::default().fg(NEON_GREEN)),
214
+ ])),
215
+ ListItem::new(Line::from(vec![
216
+ Span::styled("tool.list", Style::default().fg(NEON_GREEN)),
217
+ ])),
218
+ ];
219
+
220
+ f.render_widget(ws_block.clone(), chunks[2]);
221
+ f.render_widget(
222
+ List::new(ws_items)
223
+ .style(Style::default().fg(TEXT_PRIMARY)),
224
+ ws_block.inner(chunks[2])
225
+ );
226
+ }
227
+
228
+ fn render_command_details(f: &mut Frame, area: Rect) {
229
+ use ratatui::layout::{Constraint, Direction, Layout};
230
+
231
+ let chunks = Layout::default()
232
+ .direction(Direction::Vertical)
233
+ .constraints([
234
+ Constraint::Percentage(50), // Command descriptions
235
+ Constraint::Percentage(50), // Screen controls
236
+ ])
237
+ .split(area);
238
+
239
+ // Command descriptions
240
+ let desc_block = Block::default()
241
+ .title(" 📋 Command Details ")
242
+ .title_style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD))
243
+ .borders(Borders::ALL)
244
+ .border_style(Style::default().fg(CYBER_CYAN))
245
+ .style(Style::default().bg(BG_PANEL));
246
+
247
+ let desc_text = vec![
248
+ Line::from(vec![
249
+ Span::styled("build", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
250
+ Span::styled(" - Open Agent Builder (6-step wizard)", Style::default().fg(TEXT_PRIMARY)),
251
+ ]),
252
+ Line::from(""),
253
+ Line::from(vec![
254
+ Span::styled("runs", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
255
+ Span::styled(" - Open Run Manager (list, filter, sort)", Style::default().fg(TEXT_PRIMARY)),
256
+ ]),
257
+ Line::from(""),
258
+ Line::from(vec![
259
+ Span::styled("config, settings", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
260
+ Span::styled(" - Open Settings (mode, AI provider)", Style::default().fg(TEXT_PRIMARY)),
261
+ ]),
262
+ Line::from(""),
263
+ Line::from(vec![
264
+ Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
265
+ Span::styled(" - Close overlay/popup or clear input", Style::default().fg(TEXT_PRIMARY)),
266
+ ]),
267
+ Line::from(""),
268
+ Line::from(vec![
269
+ Span::styled("quit, exit", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
270
+ Span::styled(" - Exit application", Style::default().fg(TEXT_PRIMARY)),
271
+ ]),
272
+ Line::from(""),
273
+ Line::from(vec![
274
+ Span::styled("clear", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
275
+ Span::styled(" - Clear operations log", Style::default().fg(TEXT_PRIMARY)),
276
+ ]),
277
+ Line::from(""),
278
+ Line::from(vec![
279
+ Span::styled("help", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
280
+ Span::styled(" - Show this help", Style::default().fg(TEXT_PRIMARY)),
281
+ ]),
282
+ Line::from(""),
283
+ Line::from(vec![
284
+ Span::styled(":perf", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
285
+ Span::styled(" - Show performance stats", Style::default().fg(TEXT_PRIMARY)),
286
+ ]),
287
+ ];
288
+
289
+ f.render_widget(desc_block.clone(), chunks[0]);
290
+ f.render_widget(
291
+ Paragraph::new(desc_text)
292
+ .wrap(Wrap { trim: false }),
293
+ desc_block.inner(chunks[0])
294
+ );
295
+
296
+ // Screen controls
297
+ let controls_block = Block::default()
298
+ .title(" ⌨️ Screen Controls ")
299
+ .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
300
+ .borders(Borders::ALL)
301
+ .border_style(Style::default().fg(BRAND_PURPLE))
302
+ .style(Style::default().bg(BG_PANEL));
303
+
304
+ let controls_text = vec![
305
+ Line::from(vec![
306
+ Span::styled("Agent Builder:", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
307
+ ]),
308
+ Line::from(vec![
309
+ Span::raw(" "),
310
+ Span::styled("Enter", Style::default().fg(NEON_GREEN)),
311
+ Span::styled(" = Next | ", Style::default().fg(TEXT_DIM)),
312
+ Span::styled("Backspace", Style::default().fg(NEON_GREEN)),
313
+ Span::styled(" = Prev | ", Style::default().fg(TEXT_DIM)),
314
+ Span::styled("ESC", Style::default().fg(NEON_GREEN)),
315
+ Span::styled(" = Cancel", Style::default().fg(TEXT_DIM)),
316
+ ]),
317
+ Line::from(""),
318
+ Line::from(vec![
319
+ Span::styled("Run Manager:", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
320
+ ]),
321
+ Line::from(vec![
322
+ Span::raw(" "),
323
+ Span::styled("↑/↓", Style::default().fg(NEON_GREEN)),
324
+ Span::styled(" = Navigate | ", Style::default().fg(TEXT_DIM)),
325
+ Span::styled("F", Style::default().fg(NEON_GREEN)),
326
+ Span::styled(" = Filter | ", Style::default().fg(TEXT_DIM)),
327
+ Span::styled("S", Style::default().fg(NEON_GREEN)),
328
+ Span::styled(" = Sort | ", Style::default().fg(TEXT_DIM)),
329
+ Span::styled("R", Style::default().fg(NEON_GREEN)),
330
+ Span::styled(" = Refresh", Style::default().fg(TEXT_DIM)),
331
+ ]),
332
+ Line::from(""),
333
+ Line::from(vec![
334
+ Span::styled("Settings:", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
335
+ ]),
336
+ Line::from(vec![
337
+ Span::raw(" "),
338
+ Span::styled("↑/↓", Style::default().fg(NEON_GREEN)),
339
+ Span::styled(" = Navigate | ", Style::default().fg(TEXT_DIM)),
340
+ Span::styled("Space", Style::default().fg(NEON_GREEN)),
341
+ Span::styled(" = Toggle | ", Style::default().fg(TEXT_DIM)),
342
+ Span::styled("Enter", Style::default().fg(NEON_GREEN)),
343
+ Span::styled(" = Save", Style::default().fg(TEXT_DIM)),
344
+ ]),
345
+ ];
346
+
347
+ f.render_widget(controls_block.clone(), chunks[1]);
348
+ f.render_widget(
349
+ Paragraph::new(controls_text)
350
+ .wrap(Wrap { trim: false }),
351
+ controls_block.inner(chunks[1])
352
+ );
353
+ }
@@ -120,7 +120,7 @@ fn render_header(f: &mut Frame, area: Rect, state: &AppState) {
120
120
 
121
121
  // Line 1: Brand + version + uptime - Bug 3 fix: Use "4Runr." with dot (matches brand logo)
122
122
  // Use npm package version (2.3.5) - matches package.json
123
- const PACKAGE_VERSION: &str = "2.4.0";
123
+ const PACKAGE_VERSION: &str = "2.4.2";
124
124
  let brand_line = Line::from(vec![
125
125
  Span::styled("4Runr.", Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
126
126
  Span::styled(" AI AGENT OS", Style::default().fg(BRAND_VIOLET)),
@@ -422,7 +422,6 @@ fn mem_color(value: f64) -> Color {
422
422
  else if value > 0.6 { Color::Rgb(255, 191, 0) }
423
423
  else { CYBER_CYAN }
424
424
  }
425
-
426
425
  fn render_center_column(f: &mut Frame, area: Rect, state: &AppState) {
427
426
  let panel_area = Rect {
428
427
  x: area.x + 1,
@@ -468,8 +467,8 @@ fn render_center_column(f: &mut Frame, area: Rect, state: &AppState) {
468
467
 
469
468
  // Show real logs with proper formatting (reversed order - newest first)
470
469
  for log in state.logs.iter().rev().skip(start_idx).take(visible_height) {
471
- // Parse log format: [COMPONENT] message
472
470
  if log.starts_with("[") {
471
+ // Parse log format: [COMPONENT] message
473
472
  if let Some(bracket_end) = log.find(']') {
474
473
  let component = &log[1..bracket_end];
475
474
  let message = log[bracket_end + 1..].trim();
@@ -480,6 +479,7 @@ fn render_center_column(f: &mut Frame, area: Rect, state: &AppState) {
480
479
  "SENTINEL" => NEON_GREEN,
481
480
  "WORKER" => AMBER_WARN,
482
481
  "SYSTEM" => TEXT_DIM,
482
+ "HELP" => CYBER_CYAN,
483
483
  _ => TEXT_DIM,
484
484
  };
485
485
 
@@ -1,5 +1,7 @@
1
1
  pub mod agent_builder;
2
+ pub mod agent_list;
2
3
  pub mod boot;
4
+ pub mod help;
3
5
  pub mod layout;
4
6
  pub mod run_manager;
5
7
  pub mod safe_viewport;