4runr-os 2.6.4 → 2.6.6

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.
@@ -1,341 +1,293 @@
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 detail popup
23
- render_detail_popup(f, area, state);
24
- } else {
25
- // Render list view
26
- render_list_view(f, area, state);
27
- }
28
- }
29
-
30
- fn render_list_view(f: &mut Frame, area: Rect, state: &AppState) {
31
- use ratatui::layout::{Constraint, Direction, Layout};
32
-
33
- // Split into header, content, footer
34
- let chunks = Layout::default()
35
- .direction(Direction::Vertical)
36
- .constraints([
37
- Constraint::Length(3), // Header
38
- Constraint::Min(0), // Content
39
- Constraint::Length(3), // Footer
40
- ])
41
- .split(area);
42
-
43
- // === HEADER ===
44
- let agent_count = state.agent_list.agents.len();
45
- let header_block = Block::default()
46
- .title(format!(" 🤖 Agent List ", ))
47
- .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
48
- .borders(Borders::ALL)
49
- .border_style(Style::default().fg(CYBER_CYAN))
50
- .style(Style::default().bg(BG_PANEL));
51
-
52
- let header_inner = header_block.inner(chunks[0]);
53
- f.render_widget(header_block, chunks[0]);
54
-
55
- // Add count on the right side
56
- let count_text = Line::from(vec![
57
- Span::styled(format!("{} total", agent_count), Style::default().fg(TEXT_DIM)),
58
- ]);
59
-
60
- f.render_widget(
61
- Paragraph::new(count_text).alignment(Alignment::Right),
62
- header_inner
63
- );
64
-
65
- // === CONTENT: Agent Table ===
66
- let content_block = Block::default()
67
- .borders(Borders::ALL)
68
- .border_style(Style::default().fg(TEXT_MUTED))
69
- .style(Style::default().bg(BG_PANEL));
70
-
71
- let table_area = content_block.inner(chunks[1]);
72
- f.render_widget(content_block, chunks[1]);
73
-
74
- if state.agent_list.agents.is_empty() {
75
- // Show "No agents" message
76
- f.render_widget(
77
- Paragraph::new("No agents available.\n\nUse Agent Builder to create new agents.")
78
- .style(Style::default().fg(TEXT_DIM))
79
- .alignment(Alignment::Center),
80
- table_area
81
- );
82
- } else {
83
- // Create table rows
84
- let rows: Vec<Row> = state.agent_list.agents.iter().enumerate().map(|(i, agent)| {
85
- let is_selected = i == state.agent_list.selected_index;
86
- let style = if is_selected {
87
- Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)
88
- } else {
89
- Style::default().fg(TEXT_PRIMARY)
90
- };
91
-
92
- let name = if is_selected {
93
- format!("▶ {}", agent.name)
94
- } else {
95
- format!(" {}", agent.name)
96
- };
97
-
98
- let desc = agent.description.as_deref().unwrap_or("No description");
99
- let desc_display = if desc.is_empty() || desc == "No description" {
100
- "—".to_string()
101
- } else {
102
- desc.chars().take(35).collect::<String>()
103
- };
104
-
105
- let model_display = if agent.model.is_empty() || agent.model == "unknown" {
106
- "".to_string()
107
- } else {
108
- agent.model.chars().take(15).collect::<String>()
109
- };
110
-
111
- let provider_display = if agent.provider.is_empty() || agent.provider == "unknown" {
112
- "—".to_string()
113
- } else {
114
- agent.provider.chars().take(12).collect::<String>()
115
- };
116
-
117
- Row::new(vec![
118
- Cell::from(name).style(style),
119
- Cell::from(desc_display).style(style),
120
- Cell::from(model_display).style(style),
121
- Cell::from(provider_display).style(style),
122
- ])
123
- }).collect();
124
-
125
- let table = Table::new(rows, [
126
- Constraint::Percentage(25),
127
- Constraint::Percentage(35),
128
- Constraint::Percentage(20),
129
- Constraint::Percentage(20),
130
- ])
131
- .header(Row::new(vec![
132
- Cell::from("NAME").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
133
- Cell::from("DESCRIPTION").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
134
- Cell::from("MODEL").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
135
- Cell::from("PROVIDER").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
136
- ]))
137
- .column_spacing(1)
138
- .style(Style::default().fg(TEXT_PRIMARY));
139
-
140
- f.render_widget(table, table_area);
141
- }
142
-
143
- // === FOOTER ===
144
- let footer_text = Line::from(vec![
145
- Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
146
- Span::styled(" Navigate", Style::default().fg(TEXT_PRIMARY)),
147
- Span::styled(" │ ", Style::default().fg(TEXT_MUTED)),
148
- Span::styled("Enter", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
149
- Span::styled(" View Details", Style::default().fg(TEXT_PRIMARY)),
150
- Span::styled(" │ ", Style::default().fg(TEXT_MUTED)),
151
- Span::styled("ESC", Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
152
- Span::styled(" Close", Style::default().fg(TEXT_PRIMARY)),
153
- ]);
154
-
155
- f.render_widget(
156
- Paragraph::new(footer_text)
157
- .alignment(Alignment::Center)
158
- .style(Style::default().bg(BG_PANEL))
159
- .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(TEXT_MUTED))),
160
- chunks[2]
161
- );
162
- }
163
-
164
- fn render_detail_popup(f: &mut Frame, area: Rect, state: &AppState) {
165
- // Render full-screen dimmed overlay first
166
- f.render_widget(
167
- Block::default().style(Style::default().bg(Color::Black)),
168
- area
169
- );
170
-
171
- // Then render detail popup on top
172
- let detail_index = state.agent_list.detail_view.unwrap();
173
- if let Some(agent) = state.agent_list.agents.get(detail_index) {
174
- render_agent_detail(f, area, agent);
175
- }
176
- }
177
-
178
- fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
179
- use ratatui::layout::{Constraint, Direction, Layout};
180
-
181
- // Calculate popup size (65% width, 75% height, centered)
182
- let popup_width = (area.width * 65 / 100).max(55);
183
- let popup_height = (area.height * 75 / 100).max(18);
184
- let popup_x = (area.width.saturating_sub(popup_width)) / 2;
185
- let popup_y = (area.height.saturating_sub(popup_height)) / 2;
186
-
187
- let popup_area = Rect {
188
- x: popup_x,
189
- y: popup_y,
190
- width: popup_width,
191
- height: popup_height,
192
- };
193
-
194
- // Split popup into header, content, footer
195
- let chunks = Layout::default()
196
- .direction(Direction::Vertical)
197
- .constraints([
198
- Constraint::Length(3), // Header
199
- Constraint::Min(0), // Content
200
- Constraint::Length(1), // Footer
201
- ])
202
- .split(popup_area);
203
-
204
- // === HEADER ===
205
- let header_block = Block::default()
206
- .title(format!(" 📋 Agent: {} ", agent.name))
207
- .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
208
- .borders(Borders::ALL)
209
- .border_style(Style::default().fg(CYBER_CYAN))
210
- .style(Style::default().bg(BG_PANEL));
211
-
212
- let header_inner = header_block.inner(chunks[0]);
213
- f.render_widget(header_block, chunks[0]);
214
-
215
- // Add ESC instruction
216
- let esc_text = Line::from(vec![
217
- Span::styled("[ESC]", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
218
- ]);
219
- f.render_widget(
220
- Paragraph::new(esc_text).alignment(Alignment::Right),
221
- header_inner
222
- );
223
-
224
- // === CONTENT ===
225
- let content_block = Block::default()
226
- .borders(Borders::ALL)
227
- .border_style(Style::default().fg(TEXT_MUTED))
228
- .style(Style::default().bg(BG_PANEL));
229
-
230
- let content_inner = content_block.inner(chunks[1]);
231
- f.render_widget(content_block, chunks[1]);
232
-
233
- // Create detail text with cleaner layout
234
- let mut detail_lines: Vec<Line> = vec![
235
- Line::from(""),
236
- ];
237
-
238
- // Basic Info
239
- add_field(&mut detail_lines, "Name", agent.name.clone());
240
-
241
- if let Some(desc) = &agent.description {
242
- if !desc.is_empty() {
243
- add_field(&mut detail_lines, "Description", desc.clone());
244
- }
245
- }
246
-
247
- let model_display = if agent.model.is_empty() || agent.model == "unknown" { "—".to_string() } else { agent.model.clone() };
248
- add_field(&mut detail_lines, "Model", model_display);
249
-
250
- let provider_display = if agent.provider.is_empty() || agent.provider == "unknown" { "—".to_string() } else { agent.provider.clone() };
251
- add_field(&mut detail_lines, "Provider", provider_display);
252
-
253
- if let Some(temp) = agent.temperature {
254
- let temp_str = format!("{:.2}", temp);
255
- add_field(&mut detail_lines, "Temperature", temp_str);
256
- }
257
-
258
- if let Some(max_tokens) = agent.max_tokens {
259
- let tokens_str = format!("{}", max_tokens);
260
- add_field(&mut detail_lines, "Max Tokens", tokens_str);
261
- }
262
-
263
- if !agent.tools.is_empty() {
264
- detail_lines.push(Line::from(""));
265
- detail_lines.push(Line::from(vec![
266
- Span::styled(" Tools: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
267
- ]));
268
- for tool in &agent.tools {
269
- detail_lines.push(Line::from(vec![
270
- Span::raw(" • "),
271
- Span::styled(tool, Style::default().fg(NEON_GREEN)),
272
- ]));
273
- }
274
- }
275
-
276
- if let Some(prompt) = &agent.system_prompt {
277
- if !prompt.is_empty() {
278
- detail_lines.push(Line::from(""));
279
- detail_lines.push(Line::from(vec![
280
- Span::styled(" ────────────────────────────────────────", Style::default().fg(TEXT_MUTED)),
281
- ]));
282
- detail_lines.push(Line::from(""));
283
- detail_lines.push(Line::from(vec![
284
- Span::styled(" System Prompt:", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
285
- ]));
286
- detail_lines.push(Line::from(""));
287
- // Wrap system prompt text (simple manual wrapping)
288
- let words: Vec<&str> = prompt.split_whitespace().collect();
289
- let max_width = (popup_width as usize).saturating_sub(8);
290
- let mut current_line = String::from(" ");
291
-
292
- for word in words {
293
- if current_line.len() + word.len() + 1 > max_width {
294
- detail_lines.push(Line::from(vec![
295
- Span::styled(current_line.trim_end().to_string(), Style::default().fg(TEXT_DIM)),
296
- ]));
297
- current_line = format!(" {}", word);
298
- } else {
299
- if current_line.len() > 2 {
300
- current_line.push(' ');
301
- }
302
- current_line.push_str(word);
303
- }
304
- }
305
- if current_line.trim().len() > 0 {
306
- detail_lines.push(Line::from(vec![
307
- Span::styled(current_line.trim_end().to_string(), Style::default().fg(TEXT_DIM)),
308
- ]));
309
- }
310
- }
311
- }
312
-
313
- // Render content
314
- f.render_widget(
315
- Paragraph::new(detail_lines)
316
- .wrap(Wrap { trim: false }),
317
- content_inner
318
- );
319
-
320
- // === FOOTER ===
321
- let footer_text = Line::from(vec![
322
- Span::styled("Press ", Style::default().fg(TEXT_DIM)),
323
- Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
324
- Span::styled(" to close", Style::default().fg(TEXT_DIM)),
325
- ]);
326
-
327
- f.render_widget(
328
- Paragraph::new(footer_text)
329
- .alignment(Alignment::Center)
330
- .style(Style::default().bg(BG_PANEL)),
331
- chunks[2]
332
- );
333
- }
334
-
335
- /// Helper function to add a field line
336
- fn add_field(lines: &mut Vec<Line>, label: &str, value: String) {
337
- lines.push(Line::from(vec![
338
- Span::styled(format!(" {:<15}", format!("{}:", label)), Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
339
- Span::styled(value, Style::default().fg(TEXT_PRIMARY)),
340
- ]));
341
- }
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 detail popup
23
+ render_detail_popup(f, area, state);
24
+ } else {
25
+ // Render list view
26
+ render_list_view(f, area, state);
27
+ }
28
+ }
29
+
30
+ fn render_list_view(f: &mut Frame, area: Rect, state: &AppState) {
31
+ use ratatui::layout::{Constraint, Direction, Layout};
32
+
33
+ // Split into header, content, footer
34
+ let chunks = Layout::default()
35
+ .direction(Direction::Vertical)
36
+ .constraints([
37
+ Constraint::Length(3), // Header
38
+ Constraint::Min(0), // Content
39
+ Constraint::Length(3), // Footer
40
+ ])
41
+ .split(area);
42
+
43
+ // === HEADER ===
44
+ let agent_count = state.agent_list.agents.len();
45
+ let header_block = Block::default()
46
+ .title(format!(" 🤖 Agent List ({} agents) ", agent_count))
47
+ .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
48
+ .borders(Borders::ALL)
49
+ .border_style(Style::default().fg(CYBER_CYAN))
50
+ .style(Style::default().bg(BG_PANEL));
51
+
52
+ f.render_widget(header_block, chunks[0]);
53
+
54
+ // === CONTENT: Agent Table ===
55
+ let content_block = Block::default()
56
+ .borders(Borders::ALL)
57
+ .border_style(Style::default().fg(TEXT_MUTED))
58
+ .style(Style::default().bg(BG_PANEL));
59
+
60
+ let table_area = content_block.inner(chunks[1]);
61
+ f.render_widget(content_block, chunks[1]);
62
+
63
+ if state.agent_list.agents.is_empty() {
64
+ // Show "No agents" message
65
+ f.render_widget(
66
+ Paragraph::new("No agents available.\n\nUse Agent Builder to create new agents.")
67
+ .style(Style::default().fg(TEXT_DIM))
68
+ .alignment(Alignment::Center),
69
+ table_area
70
+ );
71
+ } else {
72
+ // Create table rows
73
+ let rows: Vec<Row> = state.agent_list.agents.iter().enumerate().map(|(i, agent)| {
74
+ let is_selected = i == state.agent_list.selected_index;
75
+ let style = if is_selected {
76
+ Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)
77
+ } else {
78
+ Style::default().fg(TEXT_PRIMARY)
79
+ };
80
+
81
+ let name = if is_selected {
82
+ format!("▶ {}", agent.name)
83
+ } else {
84
+ format!(" {}", agent.name)
85
+ };
86
+
87
+ let desc = agent.description.as_deref().unwrap_or("No description").chars().take(30).collect::<String>();
88
+ let model = agent.model.chars().take(15).collect::<String>();
89
+ let provider = agent.provider.chars().take(10).collect::<String>();
90
+
91
+ Row::new(vec![
92
+ Cell::from(name).style(style),
93
+ Cell::from(desc).style(style),
94
+ Cell::from(model).style(style),
95
+ Cell::from(provider).style(style),
96
+ ])
97
+ }).collect();
98
+
99
+ let table = Table::new(rows, [
100
+ Constraint::Percentage(25),
101
+ Constraint::Percentage(35),
102
+ Constraint::Percentage(20),
103
+ Constraint::Percentage(20),
104
+ ])
105
+ .header(Row::new(vec![
106
+ Cell::from("Name").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
107
+ Cell::from("Description").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
108
+ Cell::from("Model").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
109
+ Cell::from("Provider").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
110
+ ]))
111
+ .column_spacing(1)
112
+ .style(Style::default().fg(TEXT_PRIMARY));
113
+
114
+ f.render_widget(table, table_area);
115
+ }
116
+
117
+ // === FOOTER ===
118
+ let footer_text = Line::from(vec![
119
+ Span::styled("↑/↓", Style::default().fg(CYBER_CYAN)),
120
+ Span::styled(" Navigate | ", Style::default().fg(TEXT_DIM)),
121
+ Span::styled("Enter", Style::default().fg(NEON_GREEN)),
122
+ Span::styled(" View Details | ", Style::default().fg(TEXT_DIM)),
123
+ Span::styled("ESC", Style::default().fg(BRAND_PURPLE)),
124
+ Span::styled(" Close", Style::default().fg(TEXT_DIM)),
125
+ ]);
126
+
127
+ f.render_widget(
128
+ Paragraph::new(footer_text)
129
+ .alignment(Alignment::Center)
130
+ .style(Style::default().bg(BG_PANEL))
131
+ .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(TEXT_MUTED))),
132
+ chunks[2]
133
+ );
134
+ }
135
+
136
+ fn render_detail_popup(f: &mut Frame, area: Rect, state: &AppState) {
137
+ // First render a full-screen dim overlay
138
+ let dim_overlay = Block::default()
139
+ .style(Style::default().bg(Color::Black));
140
+ f.render_widget(dim_overlay, area);
141
+
142
+ // Then render detail popup on top
143
+ let detail_index = state.agent_list.detail_view.unwrap();
144
+ if let Some(agent) = state.agent_list.agents.get(detail_index) {
145
+ render_agent_detail(f, area, agent);
146
+ }
147
+ }
148
+
149
+ fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
150
+ use ratatui::layout::{Constraint, Direction, Layout};
151
+
152
+ // Calculate popup size (75% width, 85% height, centered)
153
+ let popup_width = (area.width * 75 / 100).max(60);
154
+ let popup_height = (area.height * 85 / 100).max(20);
155
+ let popup_x = (area.width.saturating_sub(popup_width)) / 2;
156
+ let popup_y = (area.height.saturating_sub(popup_height)) / 2;
157
+
158
+ let popup_area = Rect {
159
+ x: popup_x,
160
+ y: popup_y,
161
+ width: popup_width,
162
+ height: popup_height,
163
+ };
164
+
165
+ // Render detail popup block
166
+ let detail_block = Block::default()
167
+ .title(format!(" 📋 Agent Details: {} ", agent.name))
168
+ .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
169
+ .borders(Borders::ALL)
170
+ .border_style(Style::default().fg(CYBER_CYAN))
171
+ .style(Style::default().bg(BG_PANEL));
172
+
173
+ let inner = detail_block.inner(popup_area);
174
+ f.render_widget(detail_block, popup_area);
175
+
176
+ // Split inner area for better layout
177
+ let content_chunks = Layout::default()
178
+ .direction(Direction::Vertical)
179
+ .constraints([
180
+ Constraint::Min(0), // Main content
181
+ Constraint::Length(2), // Footer
182
+ ])
183
+ .split(inner);
184
+
185
+ // Create detail text with better spacing
186
+ let mut detail_lines = vec![
187
+ Line::from(""), // Top padding
188
+ Line::from(vec![
189
+ Span::styled("Name: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
190
+ Span::styled(&agent.name, Style::default().fg(NEON_GREEN)),
191
+ ]),
192
+ Line::from(""),
193
+ ];
194
+
195
+ if let Some(desc) = &agent.description {
196
+ detail_lines.push(Line::from(vec![
197
+ Span::styled("Description: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
198
+ Span::styled(desc, Style::default().fg(TEXT_PRIMARY)),
199
+ ]));
200
+ detail_lines.push(Line::from(""));
201
+ }
202
+
203
+ detail_lines.push(Line::from(vec![
204
+ Span::styled("Model: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
205
+ Span::styled(&agent.model, Style::default().fg(TEXT_PRIMARY)),
206
+ ]));
207
+ detail_lines.push(Line::from(""));
208
+
209
+ detail_lines.push(Line::from(vec![
210
+ Span::styled("Provider: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
211
+ Span::styled(&agent.provider, Style::default().fg(TEXT_PRIMARY)),
212
+ ]));
213
+ detail_lines.push(Line::from(""));
214
+
215
+ if let Some(temp) = agent.temperature {
216
+ detail_lines.push(Line::from(vec![
217
+ Span::styled("Temperature: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
218
+ Span::styled(format!("{:.2}", temp), Style::default().fg(TEXT_PRIMARY)),
219
+ ]));
220
+ detail_lines.push(Line::from(""));
221
+ }
222
+
223
+ if let Some(max_tokens) = agent.max_tokens {
224
+ detail_lines.push(Line::from(vec![
225
+ Span::styled("Max Tokens: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
226
+ Span::styled(format!("{}", max_tokens), Style::default().fg(TEXT_PRIMARY)),
227
+ ]));
228
+ detail_lines.push(Line::from(""));
229
+ }
230
+
231
+ if !agent.tools.is_empty() {
232
+ detail_lines.push(Line::from(""));
233
+ detail_lines.push(Line::from(vec![
234
+ Span::styled("Tools:", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
235
+ ]));
236
+ for tool in &agent.tools {
237
+ detail_lines.push(Line::from(vec![
238
+ Span::raw(" • "),
239
+ Span::styled(tool, Style::default().fg(NEON_GREEN)),
240
+ ]));
241
+ }
242
+ detail_lines.push(Line::from(""));
243
+ }
244
+
245
+ if let Some(prompt) = &agent.system_prompt {
246
+ detail_lines.push(Line::from(""));
247
+ detail_lines.push(Line::from(vec![
248
+ Span::styled("System Prompt:", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
249
+ ]));
250
+ detail_lines.push(Line::from(""));
251
+ // Show first 300 chars of prompt
252
+ let prompt_preview = prompt.chars().take(300).collect::<String>();
253
+ // Simple word wrapping
254
+ let wrap_width = (popup_width.saturating_sub(6)) as usize;
255
+ let words: Vec<&str> = prompt_preview.split_whitespace().collect();
256
+ let mut current_line = String::from(" ");
257
+
258
+ for word in words {
259
+ if current_line.len() + word.len() + 1 > wrap_width {
260
+ detail_lines.push(Line::from(current_line.clone()).style(Style::default().fg(TEXT_DIM)));
261
+ current_line = format!(" {}", word);
262
+ } else {
263
+ if current_line.len() > 2 {
264
+ current_line.push(' ');
265
+ }
266
+ current_line.push_str(word);
267
+ }
268
+ }
269
+ if current_line.len() > 2 {
270
+ detail_lines.push(Line::from(current_line).style(Style::default().fg(TEXT_DIM)));
271
+ }
272
+ }
273
+
274
+ // Render main content
275
+ f.render_widget(
276
+ Paragraph::new(detail_lines)
277
+ .wrap(Wrap { trim: false }),
278
+ content_chunks[0]
279
+ );
280
+
281
+ // Render footer
282
+ let footer_text = Line::from(vec![
283
+ Span::styled("Press ", Style::default().fg(TEXT_DIM)),
284
+ Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
285
+ Span::styled(" to close", Style::default().fg(TEXT_DIM)),
286
+ ]);
287
+
288
+ f.render_widget(
289
+ Paragraph::new(footer_text)
290
+ .alignment(Alignment::Center),
291
+ content_chunks[1]
292
+ );
293
+ }
@@ -1,211 +1,284 @@
1
- /// Help popup screen - Professional card-based layout
2
- /// Clean, readable command reference
3
-
4
- use ratatui::prelude::*;
5
- use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
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 (85% width, 90% height, centered)
21
- let popup_width = (area.width * 85 / 100).max(70);
22
- let popup_height = (area.height * 90 / 100).max(25);
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 dimmed overlay
34
- f.render_widget(
35
- Block::default().style(Style::default().bg(Color::Black)),
36
- area
37
- );
38
-
39
- // Render help popup
40
- render_help_content(f, popup_area);
41
- }
42
-
43
- fn render_help_content(f: &mut Frame, area: Rect) {
44
- use ratatui::layout::{Constraint, Direction, Layout};
45
-
46
- // Split into header, content, footer
47
- let chunks = Layout::default()
48
- .direction(Direction::Vertical)
49
- .constraints([
50
- Constraint::Length(3), // Header
51
- Constraint::Min(0), // Content
52
- Constraint::Length(1), // Footer
53
- ])
54
- .split(area);
55
-
56
- // === HEADER ===
57
- let header_block = Block::default()
58
- .title(" 📖 4Runr AI Agent OS - Command Reference ")
59
- .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
60
- .borders(Borders::ALL)
61
- .border_style(Style::default().fg(CYBER_CYAN))
62
- .style(Style::default().bg(BG_PANEL));
63
-
64
- let header_inner = header_block.inner(chunks[0]);
65
- f.render_widget(header_block, chunks[0]);
66
-
67
- let header_text = Line::from(vec![
68
- Span::styled("Press ", Style::default().fg(TEXT_DIM)),
69
- Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
70
- Span::styled(" to close", Style::default().fg(TEXT_DIM)),
71
- ]);
72
-
73
- f.render_widget(
74
- Paragraph::new(header_text).alignment(Alignment::Right),
75
- header_inner
76
- );
77
-
78
- // === CONTENT ===
79
- let content_block = Block::default()
80
- .borders(Borders::ALL)
81
- .border_style(Style::default().fg(TEXT_MUTED))
82
- .style(Style::default().bg(BG_PANEL));
83
-
84
- let content_inner = content_block.inner(chunks[1]);
85
- f.render_widget(content_block, chunks[1]);
86
-
87
- // Build help content with card-based sections
88
- let mut lines: Vec<Line> = Vec::new();
89
-
90
- // Navigation Commands Section
91
- lines.push(Line::from(vec![
92
- Span::styled(" ", Style::default()),
93
- ]));
94
- lines.push(Line::from(vec![
95
- Span::styled(" 🧭 NAVIGATION COMMANDS ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
96
- ]));
97
- lines.push(Line::from(vec![
98
- Span::styled(" ", Style::default()),
99
- ]));
100
-
101
- add_command(&mut lines, "build", "Open Agent Builder (6-step wizard)");
102
- add_command(&mut lines, "runs", "Open Run Manager (filter & sort)");
103
- add_command(&mut lines, "config, settings", "Open Settings (mode & provider)");
104
- add_command(&mut lines, "ESC", "Close overlay or clear input");
105
-
106
- lines.push(Line::from(vec![Span::styled(" ", Style::default())]));
107
-
108
- // Local Commands Section
109
- lines.push(Line::from(vec![
110
- Span::styled(" 💻 LOCAL COMMANDS ", Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
111
- ]));
112
- lines.push(Line::from(vec![
113
- Span::styled(" ", Style::default()),
114
- ]));
115
-
116
- add_command(&mut lines, "quit, exit", "Exit application");
117
- add_command(&mut lines, "clear", "Clear operations log");
118
- add_command(&mut lines, "help", "Show this help");
119
- add_command(&mut lines, ":perf", "Show performance stats");
120
-
121
- lines.push(Line::from(vec![Span::styled(" ", Style::default())]));
122
-
123
- // WebSocket Commands Section
124
- lines.push(Line::from(vec![
125
- Span::styled(" 🌐 WEBSOCKET COMMANDS ", Style::default().fg(AMBER_WARN).add_modifier(Modifier::BOLD)),
126
- Span::styled(" (requires connection)", Style::default().fg(TEXT_DIM)),
127
- ]));
128
- lines.push(Line::from(vec![
129
- Span::styled(" ", Style::default()),
130
- ]));
131
-
132
- add_command(&mut lines, "agent.list", "List all agents");
133
- add_command(&mut lines, "agent.get", "Get agent details");
134
- add_command(&mut lines, "agent.create", "Create new agent");
135
- add_command(&mut lines, "agent.delete", "Delete agent");
136
- add_command(&mut lines, "system.status", "Get system status");
137
- add_command(&mut lines, "run.list", "List all runs");
138
- add_command(&mut lines, "tool.list", "List available tools");
139
-
140
- lines.push(Line::from(vec![Span::styled(" ", Style::default())]));
141
-
142
- // Keyboard Shortcuts Section
143
- lines.push(Line::from(vec![
144
- Span::styled(" ⌨️ KEYBOARD SHORTCUTS ", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
145
- ]));
146
- lines.push(Line::from(vec![
147
- Span::styled(" ", Style::default()),
148
- ]));
149
-
150
- lines.push(Line::from(vec![
151
- Span::styled(" Agent Builder: ", Style::default().fg(CYBER_CYAN)),
152
- Span::styled("Enter", Style::default().fg(NEON_GREEN)),
153
- Span::styled("=Next ", Style::default().fg(TEXT_PRIMARY)),
154
- Span::styled("Backspace", Style::default().fg(NEON_GREEN)),
155
- Span::styled("=Prev ", Style::default().fg(TEXT_PRIMARY)),
156
- Span::styled("ESC", Style::default().fg(NEON_GREEN)),
157
- Span::styled("=Cancel", Style::default().fg(TEXT_PRIMARY)),
158
- ]));
159
-
160
- lines.push(Line::from(vec![
161
- Span::styled(" Run Manager: ", Style::default().fg(CYBER_CYAN)),
162
- Span::styled("↑/↓", Style::default().fg(NEON_GREEN)),
163
- Span::styled("=Navigate ", Style::default().fg(TEXT_PRIMARY)),
164
- Span::styled("F", Style::default().fg(NEON_GREEN)),
165
- Span::styled("=Filter ", Style::default().fg(TEXT_PRIMARY)),
166
- Span::styled("S", Style::default().fg(NEON_GREEN)),
167
- Span::styled("=Sort ", Style::default().fg(TEXT_PRIMARY)),
168
- Span::styled("R", Style::default().fg(NEON_GREEN)),
169
- Span::styled("=Refresh", Style::default().fg(TEXT_PRIMARY)),
170
- ]));
171
-
172
- lines.push(Line::from(vec![
173
- Span::styled(" Settings: ", Style::default().fg(CYBER_CYAN)),
174
- Span::styled("↑/↓", Style::default().fg(NEON_GREEN)),
175
- Span::styled("=Navigate ", Style::default().fg(TEXT_PRIMARY)),
176
- Span::styled("Space", Style::default().fg(NEON_GREEN)),
177
- Span::styled("=Toggle ", Style::default().fg(TEXT_PRIMARY)),
178
- Span::styled("Enter", Style::default().fg(NEON_GREEN)),
179
- Span::styled("=Save", Style::default().fg(TEXT_PRIMARY)),
180
- ]));
181
-
182
- // Render content
183
- f.render_widget(
184
- Paragraph::new(lines)
185
- .wrap(Wrap { trim: false }),
186
- content_inner
187
- );
188
-
189
- // === FOOTER ===
190
- let footer_text = Line::from(vec![
191
- Span::styled("Press ", Style::default().fg(TEXT_DIM)),
192
- Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
193
- Span::styled(" to close", Style::default().fg(TEXT_DIM)),
194
- ]);
195
-
196
- f.render_widget(
197
- Paragraph::new(footer_text)
198
- .alignment(Alignment::Center)
199
- .style(Style::default().bg(BG_PANEL)),
200
- chunks[2]
201
- );
202
- }
203
-
204
- /// Helper function to add a command line
205
- fn add_command(lines: &mut Vec<Line>, command: &str, description: &str) {
206
- lines.push(Line::from(vec![
207
- Span::styled(" ", Style::default()),
208
- Span::styled(format!("{:<20}", command), Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
209
- Span::styled(description, Style::default().fg(TEXT_PRIMARY)),
210
- ]));
211
- }
1
+ /// Help popup screen
2
+ /// Professional, organized command reference - REDESIGNED
3
+
4
+ use ratatui::prelude::*;
5
+ use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
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 BG_PANEL: Color = Color::Rgb(18, 18, 25);
16
+
17
+ /// Render help popup overlay
18
+ pub fn render_help(f: &mut Frame, area: Rect, _state: &AppState) {
19
+ // Render background overlay (dimmed)
20
+ let overlay = Block::default()
21
+ .style(Style::default().bg(Color::Black));
22
+ f.render_widget(overlay, area);
23
+
24
+ // Calculate popup size (85% width, 90% height, centered)
25
+ let popup_width = (area.width * 85 / 100).max(70);
26
+ let popup_height = (area.height * 90 / 100).max(25);
27
+ let popup_x = (area.width.saturating_sub(popup_width)) / 2;
28
+ let popup_y = (area.height.saturating_sub(popup_height)) / 2;
29
+
30
+ let popup_area = Rect {
31
+ x: popup_x,
32
+ y: popup_y,
33
+ width: popup_width,
34
+ height: popup_height,
35
+ };
36
+
37
+ // Render help popup
38
+ render_help_content(f, popup_area);
39
+ }
40
+
41
+ fn render_help_content(f: &mut Frame, area: Rect) {
42
+ use ratatui::layout::{Constraint, Direction, Layout};
43
+
44
+ // Split into header, content, footer
45
+ let chunks = Layout::default()
46
+ .direction(Direction::Vertical)
47
+ .constraints([
48
+ Constraint::Length(3), // Header
49
+ Constraint::Min(0), // Content
50
+ Constraint::Length(3), // Footer
51
+ ])
52
+ .split(area);
53
+
54
+ // === HEADER ===
55
+ let header_block = Block::default()
56
+ .title(" 📖 4Runr AI Agent OS - Command Reference ")
57
+ .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
58
+ .borders(Borders::ALL)
59
+ .border_style(Style::default().fg(CYBER_CYAN))
60
+ .style(Style::default().bg(BG_PANEL));
61
+
62
+ f.render_widget(header_block, chunks[0]);
63
+
64
+ // === CONTENT ===
65
+ let content_block = Block::default()
66
+ .borders(Borders::ALL)
67
+ .border_style(Style::default().fg(CYBER_CYAN))
68
+ .style(Style::default().bg(BG_PANEL));
69
+
70
+ let inner = content_block.inner(chunks[1]);
71
+ f.render_widget(content_block, chunks[1]);
72
+
73
+ // Build help text - single column, vertical layout
74
+ let mut help_lines = vec![
75
+ Line::from(""),
76
+ Line::from(vec![
77
+ Span::styled("═══════════════════════════════════════════════════════════════", Style::default().fg(CYBER_CYAN)),
78
+ ]),
79
+ Line::from(vec![
80
+ Span::styled(" 🧭 NAVIGATION COMMANDS", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
81
+ ]),
82
+ Line::from(vec![
83
+ Span::styled("═══════════════════════════════════════════════════════════════", Style::default().fg(CYBER_CYAN)),
84
+ ]),
85
+ Line::from(""),
86
+ Line::from(vec![
87
+ Span::styled(" build", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
88
+ ]),
89
+ Line::from(vec![
90
+ Span::styled(" Open Agent Builder (6-step wizard)", Style::default().fg(TEXT_PRIMARY)),
91
+ ]),
92
+ Line::from(""),
93
+ Line::from(vec![
94
+ Span::styled(" runs", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
95
+ ]),
96
+ Line::from(vec![
97
+ Span::styled(" Open Run Manager (list, filter, sort)", Style::default().fg(TEXT_PRIMARY)),
98
+ ]),
99
+ Line::from(""),
100
+ Line::from(vec![
101
+ Span::styled(" config, settings", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
102
+ ]),
103
+ Line::from(vec![
104
+ Span::styled(" Open Settings (mode, AI provider)", Style::default().fg(TEXT_PRIMARY)),
105
+ ]),
106
+ Line::from(""),
107
+ Line::from(""),
108
+ Line::from(vec![
109
+ Span::styled("═══════════════════════════════════════════════════════════════", Style::default().fg(BRAND_PURPLE)),
110
+ ]),
111
+ Line::from(vec![
112
+ Span::styled(" 💻 LOCAL COMMANDS", Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
113
+ ]),
114
+ Line::from(vec![
115
+ Span::styled("═══════════════════════════════════════════════════════════════", Style::default().fg(BRAND_PURPLE)),
116
+ ]),
117
+ Line::from(""),
118
+ Line::from(vec![
119
+ Span::styled(" quit, exit", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
120
+ ]),
121
+ Line::from(vec![
122
+ Span::styled(" Exit application", Style::default().fg(TEXT_PRIMARY)),
123
+ ]),
124
+ Line::from(""),
125
+ Line::from(vec![
126
+ Span::styled(" clear", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
127
+ ]),
128
+ Line::from(vec![
129
+ Span::styled(" Clear operations log", Style::default().fg(TEXT_PRIMARY)),
130
+ ]),
131
+ Line::from(""),
132
+ Line::from(vec![
133
+ Span::styled(" help", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
134
+ ]),
135
+ Line::from(vec![
136
+ Span::styled(" Show this help", Style::default().fg(TEXT_PRIMARY)),
137
+ ]),
138
+ Line::from(""),
139
+ Line::from(vec![
140
+ Span::styled(" :perf", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
141
+ ]),
142
+ Line::from(vec![
143
+ Span::styled(" Show performance stats", Style::default().fg(TEXT_PRIMARY)),
144
+ ]),
145
+ Line::from(""),
146
+ Line::from(""),
147
+ Line::from(vec![
148
+ Span::styled("═══════════════════════════════════════════════════════════════", Style::default().fg(AMBER_WARN)),
149
+ ]),
150
+ Line::from(vec![
151
+ Span::styled(" 🌐 WEBSOCKET COMMANDS (requires connection)", Style::default().fg(AMBER_WARN).add_modifier(Modifier::BOLD)),
152
+ ]),
153
+ Line::from(vec![
154
+ Span::styled("═══════════════════════════════════════════════════════════════", Style::default().fg(AMBER_WARN)),
155
+ ]),
156
+ Line::from(""),
157
+ Line::from(vec![
158
+ Span::styled(" agent.list", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
159
+ ]),
160
+ Line::from(vec![
161
+ Span::styled(" List all agents", Style::default().fg(TEXT_PRIMARY)),
162
+ ]),
163
+ Line::from(""),
164
+ Line::from(vec![
165
+ Span::styled(" agent.get", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
166
+ ]),
167
+ Line::from(vec![
168
+ Span::styled(" Get agent details (data: {name})", Style::default().fg(TEXT_PRIMARY)),
169
+ ]),
170
+ Line::from(""),
171
+ Line::from(vec![
172
+ Span::styled(" agent.create", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
173
+ ]),
174
+ Line::from(vec![
175
+ Span::styled(" Create agent (use Agent Builder)", Style::default().fg(TEXT_PRIMARY)),
176
+ ]),
177
+ Line::from(""),
178
+ Line::from(vec![
179
+ Span::styled(" agent.delete", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
180
+ ]),
181
+ Line::from(vec![
182
+ Span::styled(" Delete agent (data: {name})", Style::default().fg(TEXT_PRIMARY)),
183
+ ]),
184
+ Line::from(""),
185
+ Line::from(vec![
186
+ Span::styled(" system.status", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
187
+ ]),
188
+ Line::from(vec![
189
+ Span::styled(" Get system status", Style::default().fg(TEXT_PRIMARY)),
190
+ ]),
191
+ Line::from(""),
192
+ Line::from(vec![
193
+ Span::styled(" run.list", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
194
+ ]),
195
+ Line::from(vec![
196
+ Span::styled(" List runs (requires gateway)", Style::default().fg(TEXT_PRIMARY)),
197
+ ]),
198
+ Line::from(""),
199
+ Line::from(vec![
200
+ Span::styled(" tool.list", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
201
+ ]),
202
+ Line::from(vec![
203
+ Span::styled(" List available tools", Style::default().fg(TEXT_PRIMARY)),
204
+ ]),
205
+ Line::from(""),
206
+ Line::from(""),
207
+ Line::from(vec![
208
+ Span::styled("═══════════════════════════════════════════════════════════════", Style::default().fg(CYBER_CYAN)),
209
+ ]),
210
+ Line::from(vec![
211
+ Span::styled(" ⌨️ SCREEN CONTROLS", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
212
+ ]),
213
+ Line::from(vec![
214
+ Span::styled("═══════════════════════════════════════════════════════════════", Style::default().fg(CYBER_CYAN)),
215
+ ]),
216
+ Line::from(""),
217
+ Line::from(vec![
218
+ Span::styled(" Agent Builder:", Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
219
+ ]),
220
+ Line::from(vec![
221
+ Span::styled(" Enter", Style::default().fg(NEON_GREEN)),
222
+ Span::styled(" = Next step | ", Style::default().fg(TEXT_DIM)),
223
+ Span::styled("Backspace", Style::default().fg(NEON_GREEN)),
224
+ Span::styled(" = Previous | ", Style::default().fg(TEXT_DIM)),
225
+ Span::styled("ESC", Style::default().fg(NEON_GREEN)),
226
+ Span::styled(" = Cancel", Style::default().fg(TEXT_DIM)),
227
+ ]),
228
+ Line::from(""),
229
+ Line::from(vec![
230
+ Span::styled(" Run Manager:", Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
231
+ ]),
232
+ Line::from(vec![
233
+ Span::styled(" ↑/↓", Style::default().fg(NEON_GREEN)),
234
+ Span::styled(" = Navigate | ", Style::default().fg(TEXT_DIM)),
235
+ Span::styled("F", Style::default().fg(NEON_GREEN)),
236
+ Span::styled(" = Filter | ", Style::default().fg(TEXT_DIM)),
237
+ Span::styled("S", Style::default().fg(NEON_GREEN)),
238
+ Span::styled(" = Sort | ", Style::default().fg(TEXT_DIM)),
239
+ Span::styled("R", Style::default().fg(NEON_GREEN)),
240
+ Span::styled(" = Refresh", Style::default().fg(TEXT_DIM)),
241
+ ]),
242
+ Line::from(""),
243
+ Line::from(vec![
244
+ Span::styled(" Settings:", Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
245
+ ]),
246
+ Line::from(vec![
247
+ Span::styled(" ↑/↓", Style::default().fg(NEON_GREEN)),
248
+ Span::styled(" = Navigate | ", Style::default().fg(TEXT_DIM)),
249
+ Span::styled("Space", Style::default().fg(NEON_GREEN)),
250
+ Span::styled(" = Toggle | ", Style::default().fg(TEXT_DIM)),
251
+ Span::styled("Enter", Style::default().fg(NEON_GREEN)),
252
+ Span::styled(" = Save", Style::default().fg(TEXT_DIM)),
253
+ ]),
254
+ Line::from(""),
255
+ ];
256
+
257
+ f.render_widget(
258
+ Paragraph::new(help_lines)
259
+ .style(Style::default().bg(BG_PANEL))
260
+ .wrap(Wrap { trim: false }),
261
+ inner
262
+ );
263
+
264
+ // === FOOTER ===
265
+ let footer_text = Line::from(vec![
266
+ Span::styled("Press ", Style::default().fg(TEXT_DIM)),
267
+ Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
268
+ Span::styled(" to close", Style::default().fg(TEXT_DIM)),
269
+ ]);
270
+
271
+ let footer_block = Block::default()
272
+ .borders(Borders::ALL)
273
+ .border_style(Style::default().fg(CYBER_CYAN))
274
+ .style(Style::default().bg(BG_PANEL));
275
+
276
+ let footer_inner = footer_block.inner(chunks[2]);
277
+ f.render_widget(footer_block, chunks[2]);
278
+ f.render_widget(
279
+ Paragraph::new(footer_text)
280
+ .style(Style::default().bg(BG_PANEL))
281
+ .alignment(Alignment::Center),
282
+ footer_inner
283
+ );
284
+ }
@@ -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.6.4";
123
+ const PACKAGE_VERSION: &str = "2.6.6";
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)),
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.6.4",
3
+ "version": "2.6.6",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.6.4: Fixed lifetime errors by removing 'static constraint from Line vectors, allowing Rust to infer appropriate lifetimes. Complete UI polish with redesigned help popup, fixed agent detail popup overlay, and enhanced agent list. Professional, clean, readable interface. Built with Rust + Ratatui. ⚠️ Pre-MVP / Development Phase",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.6.6: CRITICAL FIX - Help popup transparency fixed (solid background). Completely redesigned help popup (clean, readable, single-column layout). Fixed agent detail popup rendering bugs (proper dimming, no overlap). Enhanced visual hierarchy with better spacing. Built with Rust + Ratatui. ⚠️ Pre-MVP / Development Phase",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",