4runr-os 2.10.39 → 2.10.41

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.
Files changed (51) hide show
  1. package/apps/gateway/dist/apps/gateway/src/index.js +14 -4
  2. package/apps/gateway/dist/apps/gateway/src/index.js.map +1 -1
  3. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts +18 -0
  4. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts.map +1 -0
  5. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js +117 -0
  6. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js.map +1 -0
  7. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts +2 -0
  8. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts.map +1 -0
  9. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js +54 -0
  10. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js.map +1 -0
  11. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts +15 -0
  12. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts.map +1 -0
  13. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js +164 -0
  14. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js.map +1 -0
  15. package/apps/gateway/package-lock.json +204 -353
  16. package/apps/gateway/src/index.ts +27 -8
  17. package/apps/gateway/src/metrics/monitoring-detail.ts +162 -0
  18. package/apps/gateway/src/middleware/log-capture.ts +70 -0
  19. package/apps/gateway/src/routes/monitoring.ts +298 -0
  20. package/dist/gateway-client.d.ts +2 -0
  21. package/dist/gateway-client.d.ts.map +1 -1
  22. package/dist/gateway-client.js +22 -0
  23. package/dist/gateway-client.js.map +1 -1
  24. package/dist/tui-handlers.js +498 -0
  25. package/dist/tui-handlers.js.map +1 -1
  26. package/mk3-tui/src/app/render_scheduler.rs +111 -112
  27. package/mk3-tui/src/app.rs +1078 -295
  28. package/mk3-tui/src/debug_log.rs +131 -124
  29. package/mk3-tui/src/io/mod.rs +63 -66
  30. package/mk3-tui/src/io/protocol.rs +14 -15
  31. package/mk3-tui/src/io/stdio.rs +31 -32
  32. package/mk3-tui/src/io/ws.rs +25 -32
  33. package/mk3-tui/src/main.rs +774 -212
  34. package/mk3-tui/src/monitoring/mod.rs +428 -0
  35. package/mk3-tui/src/screens/mod.rs +53 -39
  36. package/mk3-tui/src/storage/cache.rs +221 -224
  37. package/mk3-tui/src/storage/mod.rs +5 -6
  38. package/mk3-tui/src/ui/agent_builder.rs +1148 -922
  39. package/mk3-tui/src/ui/agent_list.rs +344 -295
  40. package/mk3-tui/src/ui/boot.rs +145 -148
  41. package/mk3-tui/src/ui/connection_portal.rs +121 -98
  42. package/mk3-tui/src/ui/help.rs +340 -284
  43. package/mk3-tui/src/ui/layout.rs +966 -803
  44. package/mk3-tui/src/ui/mod.rs +1 -1
  45. package/mk3-tui/src/ui/portal_monitoring.rs +1027 -147
  46. package/mk3-tui/src/ui/run_manager.rs +784 -764
  47. package/mk3-tui/src/ui/safe_viewport.rs +236 -235
  48. package/mk3-tui/src/ui/settings.rs +414 -362
  49. package/mk3-tui/src/ui/setup_portal.rs +158 -101
  50. package/mk3-tui/src/websocket.rs +315 -308
  51. package/package.json +2 -2
@@ -1,295 +1,344 @@
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("D/Del", Style::default().fg(Color::Rgb(255, 100, 100))),
124
- Span::styled(" Delete | ", Style::default().fg(TEXT_DIM)),
125
- Span::styled("ESC", Style::default().fg(BRAND_PURPLE)),
126
- Span::styled(" Close", Style::default().fg(TEXT_DIM)),
127
- ]);
128
-
129
- f.render_widget(
130
- Paragraph::new(footer_text)
131
- .alignment(Alignment::Center)
132
- .style(Style::default().bg(BG_PANEL))
133
- .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(TEXT_MUTED))),
134
- chunks[2]
135
- );
136
- }
137
-
138
- fn render_detail_popup(f: &mut Frame, area: Rect, state: &AppState) {
139
- // First render a full-screen dim overlay
140
- let dim_overlay = Block::default()
141
- .style(Style::default().bg(Color::Black));
142
- f.render_widget(dim_overlay, area);
143
-
144
- // Then render detail popup on top
145
- let detail_index = state.agent_list.detail_view.unwrap();
146
- if let Some(agent) = state.agent_list.agents.get(detail_index) {
147
- render_agent_detail(f, area, agent);
148
- }
149
- }
150
-
151
- fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
152
- use ratatui::layout::{Constraint, Direction, Layout};
153
-
154
- // Calculate popup size (75% width, 85% height, centered)
155
- let popup_width = (area.width * 75 / 100).max(60);
156
- let popup_height = (area.height * 85 / 100).max(20);
157
- let popup_x = (area.width.saturating_sub(popup_width)) / 2;
158
- let popup_y = (area.height.saturating_sub(popup_height)) / 2;
159
-
160
- let popup_area = Rect {
161
- x: popup_x,
162
- y: popup_y,
163
- width: popup_width,
164
- height: popup_height,
165
- };
166
-
167
- // Render detail popup block
168
- let detail_block = Block::default()
169
- .title(format!(" 📋 Agent Details: {} ", agent.name))
170
- .title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
171
- .borders(Borders::ALL)
172
- .border_style(Style::default().fg(CYBER_CYAN))
173
- .style(Style::default().bg(BG_PANEL));
174
-
175
- let inner = detail_block.inner(popup_area);
176
- f.render_widget(detail_block, popup_area);
177
-
178
- // Split inner area for better layout
179
- let content_chunks = Layout::default()
180
- .direction(Direction::Vertical)
181
- .constraints([
182
- Constraint::Min(0), // Main content
183
- Constraint::Length(2), // Footer
184
- ])
185
- .split(inner);
186
-
187
- // Create detail text with better spacing
188
- let mut detail_lines = vec![
189
- Line::from(""), // Top padding
190
- Line::from(vec![
191
- Span::styled("Name: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
192
- Span::styled(&agent.name, Style::default().fg(NEON_GREEN)),
193
- ]),
194
- Line::from(""),
195
- ];
196
-
197
- if let Some(desc) = &agent.description {
198
- detail_lines.push(Line::from(vec![
199
- Span::styled("Description: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
200
- Span::styled(desc, Style::default().fg(TEXT_PRIMARY)),
201
- ]));
202
- detail_lines.push(Line::from(""));
203
- }
204
-
205
- detail_lines.push(Line::from(vec![
206
- Span::styled("Model: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
207
- Span::styled(&agent.model, Style::default().fg(TEXT_PRIMARY)),
208
- ]));
209
- detail_lines.push(Line::from(""));
210
-
211
- detail_lines.push(Line::from(vec![
212
- Span::styled("Provider: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
213
- Span::styled(&agent.provider, Style::default().fg(TEXT_PRIMARY)),
214
- ]));
215
- detail_lines.push(Line::from(""));
216
-
217
- if let Some(temp) = agent.temperature {
218
- detail_lines.push(Line::from(vec![
219
- Span::styled("Temperature: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
220
- Span::styled(format!("{:.2}", temp), Style::default().fg(TEXT_PRIMARY)),
221
- ]));
222
- detail_lines.push(Line::from(""));
223
- }
224
-
225
- if let Some(max_tokens) = agent.max_tokens {
226
- detail_lines.push(Line::from(vec![
227
- Span::styled("Max Tokens: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
228
- Span::styled(format!("{}", max_tokens), Style::default().fg(TEXT_PRIMARY)),
229
- ]));
230
- detail_lines.push(Line::from(""));
231
- }
232
-
233
- if !agent.tools.is_empty() {
234
- detail_lines.push(Line::from(""));
235
- detail_lines.push(Line::from(vec![
236
- Span::styled("Tools:", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
237
- ]));
238
- for tool in &agent.tools {
239
- detail_lines.push(Line::from(vec![
240
- Span::raw(""),
241
- Span::styled(tool, Style::default().fg(NEON_GREEN)),
242
- ]));
243
- }
244
- detail_lines.push(Line::from(""));
245
- }
246
-
247
- if let Some(prompt) = &agent.system_prompt {
248
- detail_lines.push(Line::from(""));
249
- detail_lines.push(Line::from(vec![
250
- Span::styled("System Prompt:", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
251
- ]));
252
- detail_lines.push(Line::from(""));
253
- // Show first 300 chars of prompt
254
- let prompt_preview = prompt.chars().take(300).collect::<String>();
255
- // Simple word wrapping
256
- let wrap_width = (popup_width.saturating_sub(6)) as usize;
257
- let words: Vec<&str> = prompt_preview.split_whitespace().collect();
258
- let mut current_line = String::from(" ");
259
-
260
- for word in words {
261
- if current_line.len() + word.len() + 1 > wrap_width {
262
- detail_lines.push(Line::from(current_line.clone()).style(Style::default().fg(TEXT_DIM)));
263
- current_line = format!(" {}", word);
264
- } else {
265
- if current_line.len() > 2 {
266
- current_line.push(' ');
267
- }
268
- current_line.push_str(word);
269
- }
270
- }
271
- if current_line.len() > 2 {
272
- detail_lines.push(Line::from(current_line).style(Style::default().fg(TEXT_DIM)));
273
- }
274
- }
275
-
276
- // Render main content
277
- f.render_widget(
278
- Paragraph::new(detail_lines)
279
- .wrap(Wrap { trim: false }),
280
- content_chunks[0]
281
- );
282
-
283
- // Render footer
284
- let footer_text = Line::from(vec![
285
- Span::styled("Press ", Style::default().fg(TEXT_DIM)),
286
- Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
287
- Span::styled(" to close", Style::default().fg(TEXT_DIM)),
288
- ]);
289
-
290
- f.render_widget(
291
- Paragraph::new(footer_text)
292
- .alignment(Alignment::Center),
293
- content_chunks[1]
294
- );
295
- }
1
+ use crate::app::{AgentInfo, AppState};
2
+ /// Agent List Screen
3
+ /// View and manage AI agents
4
+ use ratatui::prelude::*;
5
+ use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table, Wrap};
6
+
7
+ // === 4RUNR BRAND COLORS ===
8
+ const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
9
+ const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
10
+ const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
11
+ const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
12
+ const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
13
+ const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
14
+ const BG_PANEL: Color = Color::Rgb(18, 18, 25);
15
+
16
+ /// Render agent list screen
17
+ pub fn render(f: &mut Frame, state: &AppState) {
18
+ let area = f.size();
19
+
20
+ if state.agent_list.detail_view.is_some() {
21
+ // Render detail popup
22
+ render_detail_popup(f, area, state);
23
+ } else {
24
+ // Render list view
25
+ render_list_view(f, area, state);
26
+ }
27
+ }
28
+
29
+ fn render_list_view(f: &mut Frame, area: Rect, state: &AppState) {
30
+ use ratatui::layout::{Constraint, Direction, Layout};
31
+
32
+ // Split into header, content, footer
33
+ let chunks = Layout::default()
34
+ .direction(Direction::Vertical)
35
+ .constraints([
36
+ Constraint::Length(3), // Header
37
+ Constraint::Min(0), // Content
38
+ Constraint::Length(3), // Footer
39
+ ])
40
+ .split(area);
41
+
42
+ // === HEADER ===
43
+ let agent_count = state.agent_list.agents.len();
44
+ let header_block = Block::default()
45
+ .title(format!(" 🤖 Agent List ({} agents) ", agent_count))
46
+ .title_style(
47
+ Style::default()
48
+ .fg(BRAND_PURPLE)
49
+ .add_modifier(Modifier::BOLD),
50
+ )
51
+ .borders(Borders::ALL)
52
+ .border_style(Style::default().fg(CYBER_CYAN))
53
+ .style(Style::default().bg(BG_PANEL));
54
+
55
+ f.render_widget(header_block, chunks[0]);
56
+
57
+ // === CONTENT: Agent Table ===
58
+ let content_block = Block::default()
59
+ .borders(Borders::ALL)
60
+ .border_style(Style::default().fg(TEXT_MUTED))
61
+ .style(Style::default().bg(BG_PANEL));
62
+
63
+ let table_area = content_block.inner(chunks[1]);
64
+ f.render_widget(content_block, chunks[1]);
65
+
66
+ if state.agent_list.agents.is_empty() {
67
+ // Show "No agents" message
68
+ f.render_widget(
69
+ Paragraph::new("No agents available.\n\nUse Agent Builder to create new agents.")
70
+ .style(Style::default().fg(TEXT_DIM))
71
+ .alignment(Alignment::Center),
72
+ table_area,
73
+ );
74
+ } else {
75
+ // Create table rows
76
+ let rows: Vec<Row> = state
77
+ .agent_list
78
+ .agents
79
+ .iter()
80
+ .enumerate()
81
+ .map(|(i, agent)| {
82
+ let is_selected = i == state.agent_list.selected_index;
83
+ let style = if is_selected {
84
+ Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)
85
+ } else {
86
+ Style::default().fg(TEXT_PRIMARY)
87
+ };
88
+
89
+ let name = if is_selected {
90
+ format!("▶ {}", agent.name)
91
+ } else {
92
+ format!(" {}", agent.name)
93
+ };
94
+
95
+ let desc = agent
96
+ .description
97
+ .as_deref()
98
+ .unwrap_or("No description")
99
+ .chars()
100
+ .take(30)
101
+ .collect::<String>();
102
+ let model = agent.model.chars().take(15).collect::<String>();
103
+ let provider = agent.provider.chars().take(10).collect::<String>();
104
+
105
+ Row::new(vec![
106
+ Cell::from(name).style(style),
107
+ Cell::from(desc).style(style),
108
+ Cell::from(model).style(style),
109
+ Cell::from(provider).style(style),
110
+ ])
111
+ })
112
+ .collect();
113
+
114
+ let table = Table::new(
115
+ rows,
116
+ [
117
+ Constraint::Percentage(25),
118
+ Constraint::Percentage(35),
119
+ Constraint::Percentage(20),
120
+ Constraint::Percentage(20),
121
+ ],
122
+ )
123
+ .header(Row::new(vec![
124
+ Cell::from("Name").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
125
+ Cell::from("Description")
126
+ .style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
127
+ Cell::from("Model").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
128
+ Cell::from("Provider")
129
+ .style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
130
+ ]))
131
+ .column_spacing(1)
132
+ .style(Style::default().fg(TEXT_PRIMARY));
133
+
134
+ f.render_widget(table, table_area);
135
+ }
136
+
137
+ // === FOOTER ===
138
+ let footer_text = Line::from(vec![
139
+ Span::styled("↑/↓", Style::default().fg(CYBER_CYAN)),
140
+ Span::styled(" Navigate | ", Style::default().fg(TEXT_DIM)),
141
+ Span::styled("Enter", Style::default().fg(NEON_GREEN)),
142
+ Span::styled(" View Details | ", Style::default().fg(TEXT_DIM)),
143
+ Span::styled("D/Del", Style::default().fg(Color::Rgb(255, 100, 100))),
144
+ Span::styled(" Delete | ", Style::default().fg(TEXT_DIM)),
145
+ Span::styled("ESC", Style::default().fg(BRAND_PURPLE)),
146
+ Span::styled(" Close", Style::default().fg(TEXT_DIM)),
147
+ ]);
148
+
149
+ f.render_widget(
150
+ Paragraph::new(footer_text)
151
+ .alignment(Alignment::Center)
152
+ .style(Style::default().bg(BG_PANEL))
153
+ .block(
154
+ Block::default()
155
+ .borders(Borders::ALL)
156
+ .border_style(Style::default().fg(TEXT_MUTED)),
157
+ ),
158
+ chunks[2],
159
+ );
160
+ }
161
+
162
+ fn render_detail_popup(f: &mut Frame, area: Rect, state: &AppState) {
163
+ // First render a full-screen dim overlay
164
+ let dim_overlay = Block::default().style(Style::default().bg(Color::Black));
165
+ f.render_widget(dim_overlay, area);
166
+
167
+ // Then render detail popup on top
168
+ let detail_index = state.agent_list.detail_view.unwrap();
169
+ if let Some(agent) = state.agent_list.agents.get(detail_index) {
170
+ render_agent_detail(f, area, agent);
171
+ }
172
+ }
173
+
174
+ fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
175
+ use ratatui::layout::{Constraint, Direction, Layout};
176
+
177
+ // Calculate popup size (75% width, 85% height, centered)
178
+ let popup_width = (area.width * 75 / 100).max(60);
179
+ let popup_height = (area.height * 85 / 100).max(20);
180
+ let popup_x = (area.width.saturating_sub(popup_width)) / 2;
181
+ let popup_y = (area.height.saturating_sub(popup_height)) / 2;
182
+
183
+ let popup_area = Rect {
184
+ x: popup_x,
185
+ y: popup_y,
186
+ width: popup_width,
187
+ height: popup_height,
188
+ };
189
+
190
+ // Render detail popup block
191
+ let detail_block = Block::default()
192
+ .title(format!(" 📋 Agent Details: {} ", agent.name))
193
+ .title_style(
194
+ Style::default()
195
+ .fg(BRAND_PURPLE)
196
+ .add_modifier(Modifier::BOLD),
197
+ )
198
+ .borders(Borders::ALL)
199
+ .border_style(Style::default().fg(CYBER_CYAN))
200
+ .style(Style::default().bg(BG_PANEL));
201
+
202
+ let inner = detail_block.inner(popup_area);
203
+ f.render_widget(detail_block, popup_area);
204
+
205
+ // Split inner area for better layout
206
+ let content_chunks = Layout::default()
207
+ .direction(Direction::Vertical)
208
+ .constraints([
209
+ Constraint::Min(0), // Main content
210
+ Constraint::Length(2), // Footer
211
+ ])
212
+ .split(inner);
213
+
214
+ // Create detail text with better spacing
215
+ let mut detail_lines = vec![
216
+ Line::from(""), // Top padding
217
+ Line::from(vec![
218
+ Span::styled(
219
+ "Name: ",
220
+ Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD),
221
+ ),
222
+ Span::styled(&agent.name, Style::default().fg(NEON_GREEN)),
223
+ ]),
224
+ Line::from(""),
225
+ ];
226
+
227
+ if let Some(desc) = &agent.description {
228
+ detail_lines.push(Line::from(vec![
229
+ Span::styled(
230
+ "Description: ",
231
+ Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD),
232
+ ),
233
+ Span::styled(desc, Style::default().fg(TEXT_PRIMARY)),
234
+ ]));
235
+ detail_lines.push(Line::from(""));
236
+ }
237
+
238
+ detail_lines.push(Line::from(vec![
239
+ Span::styled(
240
+ "Model: ",
241
+ Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD),
242
+ ),
243
+ Span::styled(&agent.model, Style::default().fg(TEXT_PRIMARY)),
244
+ ]));
245
+ detail_lines.push(Line::from(""));
246
+
247
+ detail_lines.push(Line::from(vec![
248
+ Span::styled(
249
+ "Provider: ",
250
+ Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD),
251
+ ),
252
+ Span::styled(&agent.provider, Style::default().fg(TEXT_PRIMARY)),
253
+ ]));
254
+ detail_lines.push(Line::from(""));
255
+
256
+ if let Some(temp) = agent.temperature {
257
+ detail_lines.push(Line::from(vec![
258
+ Span::styled(
259
+ "Temperature: ",
260
+ Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD),
261
+ ),
262
+ Span::styled(format!("{:.2}", temp), Style::default().fg(TEXT_PRIMARY)),
263
+ ]));
264
+ detail_lines.push(Line::from(""));
265
+ }
266
+
267
+ if let Some(max_tokens) = agent.max_tokens {
268
+ detail_lines.push(Line::from(vec![
269
+ Span::styled(
270
+ "Max Tokens: ",
271
+ Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD),
272
+ ),
273
+ Span::styled(format!("{}", max_tokens), Style::default().fg(TEXT_PRIMARY)),
274
+ ]));
275
+ detail_lines.push(Line::from(""));
276
+ }
277
+
278
+ if !agent.tools.is_empty() {
279
+ detail_lines.push(Line::from(""));
280
+ detail_lines.push(Line::from(vec![Span::styled(
281
+ "Tools:",
282
+ Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD),
283
+ )]));
284
+ for tool in &agent.tools {
285
+ detail_lines.push(Line::from(vec![
286
+ Span::raw(""),
287
+ Span::styled(tool, Style::default().fg(NEON_GREEN)),
288
+ ]));
289
+ }
290
+ detail_lines.push(Line::from(""));
291
+ }
292
+
293
+ if let Some(prompt) = &agent.system_prompt {
294
+ detail_lines.push(Line::from(""));
295
+ detail_lines.push(Line::from(vec![Span::styled(
296
+ "System Prompt:",
297
+ Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD),
298
+ )]));
299
+ detail_lines.push(Line::from(""));
300
+ // Show first 300 chars of prompt
301
+ let prompt_preview = prompt.chars().take(300).collect::<String>();
302
+ // Simple word wrapping
303
+ let wrap_width = (popup_width.saturating_sub(6)) as usize;
304
+ let words: Vec<&str> = prompt_preview.split_whitespace().collect();
305
+ let mut current_line = String::from(" ");
306
+
307
+ for word in words {
308
+ if current_line.len() + word.len() + 1 > wrap_width {
309
+ detail_lines
310
+ .push(Line::from(current_line.clone()).style(Style::default().fg(TEXT_DIM)));
311
+ current_line = format!(" {}", word);
312
+ } else {
313
+ if current_line.len() > 2 {
314
+ current_line.push(' ');
315
+ }
316
+ current_line.push_str(word);
317
+ }
318
+ }
319
+ if current_line.len() > 2 {
320
+ detail_lines.push(Line::from(current_line).style(Style::default().fg(TEXT_DIM)));
321
+ }
322
+ }
323
+
324
+ // Render main content
325
+ f.render_widget(
326
+ Paragraph::new(detail_lines).wrap(Wrap { trim: false }),
327
+ content_chunks[0],
328
+ );
329
+
330
+ // Render footer
331
+ let footer_text = Line::from(vec![
332
+ Span::styled("Press ", Style::default().fg(TEXT_DIM)),
333
+ Span::styled(
334
+ "ESC",
335
+ Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD),
336
+ ),
337
+ Span::styled(" to close", Style::default().fg(TEXT_DIM)),
338
+ ]);
339
+
340
+ f.render_widget(
341
+ Paragraph::new(footer_text).alignment(Alignment::Center),
342
+ content_chunks[1],
343
+ );
344
+ }