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.
@@ -29,6 +29,35 @@ pub enum AppMode {
29
29
  Main,
30
30
  }
31
31
 
32
+ #[derive(Debug, Clone)]
33
+ pub struct AgentListState {
34
+ pub agents: Vec<AgentInfo>,
35
+ pub selected_index: usize,
36
+ pub detail_view: Option<usize>, // None = list view, Some(index) = detail popup
37
+ }
38
+
39
+ impl Default for AgentListState {
40
+ fn default() -> Self {
41
+ Self {
42
+ agents: Vec::new(),
43
+ selected_index: 0,
44
+ detail_view: None,
45
+ }
46
+ }
47
+ }
48
+
49
+ #[derive(Debug, Clone)]
50
+ pub struct AgentInfo {
51
+ pub name: String,
52
+ pub description: Option<String>,
53
+ pub model: String,
54
+ pub provider: String,
55
+ pub system_prompt: Option<String>,
56
+ pub temperature: Option<f32>,
57
+ pub max_tokens: Option<u32>,
58
+ pub tools: Vec<String>,
59
+ }
60
+
32
61
  #[derive(Debug, Clone)]
33
62
  pub struct AppState {
34
63
  // Navigation state (NEW - replaces simple mode)
@@ -96,6 +125,7 @@ pub struct AppState {
96
125
  pub agent_builder: AgentBuilderState,
97
126
  pub run_manager: RunManagerState,
98
127
  pub settings: SettingsState,
128
+ pub agent_list: AgentListState,
99
129
 
100
130
  // Local cache
101
131
  pub cache: Option<Cache>,
@@ -153,6 +183,7 @@ impl Default for AppState {
153
183
  agent_builder: AgentBuilderState::default(),
154
184
  run_manager: RunManagerState::default(),
155
185
  settings: SettingsState::default(),
186
+ agent_list: AgentListState::default(),
156
187
  cache: Cache::new().ok(),
157
188
  cache_loaded: false,
158
189
  }
@@ -339,6 +370,11 @@ impl App {
339
370
  return self.handle_settings_input(key);
340
371
  }
341
372
 
373
+ // === AGENT LIST INPUT HANDLING ===
374
+ if self.state.navigation.current_screen() == &Screen::AgentList {
375
+ return self.handle_agent_list_input(key);
376
+ }
377
+
342
378
  // === MAIN INPUT HANDLING ===
343
379
  match key.code {
344
380
  // Typing - ALL characters go to command input (with debounce)
@@ -362,48 +398,9 @@ impl App {
362
398
  "quit" | "exit" => return Ok(true),
363
399
  "clear" => self.state.logs.clear(),
364
400
  "help" => {
365
- self.state.logs.push_back("[HELP] ═══════════════════════════════════════".into());
366
- self.state.logs.push_back("[HELP] 4Runr AI Agent OS - Command Reference".into());
367
- self.state.logs.push_back("[HELP] ═══════════════════════════════════════".into());
368
- self.state.logs.push_back("".into());
369
-
370
- // Local Commands
371
- self.state.logs.push_back("[HELP] Local Commands:".into());
372
- self.state.logs.push_back(" quit, exit - Exit application".into());
373
- self.state.logs.push_back(" clear - Clear logs".into());
374
- self.state.logs.push_back(" help - Show this help".into());
375
- self.state.logs.push_back(" :perf - Show performance stats".into());
376
- self.state.logs.push_back("".into());
377
-
378
- // Navigation Commands
379
- self.state.logs.push_back("[HELP] Navigation Commands:".into());
380
- self.state.logs.push_back(" build - Open Agent Builder (6-step wizard)".into());
381
- self.state.logs.push_back(" runs - Open Run Manager (list, filter, sort)".into());
382
- self.state.logs.push_back(" config, settings - Open Settings (mode, AI provider)".into());
383
- self.state.logs.push_back(" ESC - Close overlay/popup or clear input".into());
384
- self.state.logs.push_back("".into());
385
-
386
- // WebSocket Commands - Label BEFORE commands with clear separator
387
- self.state.logs.push_back("[HELP] ─────────────────────────────────────────".into());
388
- self.state.logs.push_back("[HELP] WebSocket Commands (requires connection):".into());
389
- self.state.logs.push_back("[HELP] ─────────────────────────────────────────".into());
390
- self.state.logs.push_back(" agent.list - List all agents".into());
391
- self.state.logs.push_back(" agent.get - Get agent details (data: {name})".into());
392
- self.state.logs.push_back(" agent.create - Create agent (use Agent Builder)".into());
393
- self.state.logs.push_back(" agent.delete - Delete agent (data: {name})".into());
394
- self.state.logs.push_back(" system.status - Get system status".into());
395
- self.state.logs.push_back(" run.list - List runs (requires gateway)".into());
396
- self.state.logs.push_back(" tool.list - List available tools".into());
397
- self.state.logs.push_back("".into());
398
-
399
- // Screen Controls
400
- self.state.logs.push_back("[HELP] Screen Controls:".into());
401
- self.state.logs.push_back(" Agent Builder: Enter=Next, Backspace=Prev, ESC=Cancel".into());
402
- self.state.logs.push_back(" Run Manager: ↑/↓=Navigate, F=Filter, S=Sort, R=Refresh".into());
403
- self.state.logs.push_back(" Settings: ↑/↓=Navigate, Space=Toggle, Enter=Save".into());
404
- self.state.logs.push_back("".into());
405
- self.state.logs.push_back("[HELP] Press F12 to toggle performance overlay".into());
406
- self.state.logs.push_back("[HELP] ═══════════════════════════════════════".into());
401
+ // Open help popup instead of logging to operations log
402
+ self.push_popup(Screen::Help);
403
+ self.request_render("help_command");
407
404
  }
408
405
  ":perf" => {
409
406
  // Perf self-check command
@@ -591,6 +588,10 @@ impl App {
591
588
  use crate::ui::settings;
592
589
  settings::render(f, &self.state);
593
590
  }
591
+ Screen::AgentList => {
592
+ use crate::ui::agent_list;
593
+ agent_list::render(f, &self.state);
594
+ }
594
595
  Screen::Confirmation { message, action } => {
595
596
  // TODO: Implement confirmation popup
596
597
  // For now, just render the base screen
@@ -606,10 +607,13 @@ impl App {
606
607
  let _ = message; // Suppress warning
607
608
  }
608
609
  Screen::Help => {
609
- // TODO: Implement help popup
610
- // For now, just render the base screen
610
+ // Render base screen first (dimmed)
611
611
  use crate::ui::layout;
612
612
  layout::render(f, &self.state);
613
+
614
+ // Render help popup overlay
615
+ use crate::ui::help;
616
+ help::render_help(f, f.size(), &self.state);
613
617
  }
614
618
  }
615
619
  }
@@ -1034,20 +1038,17 @@ impl App {
1034
1038
  // Filter
1035
1039
  KeyCode::Char('f') | KeyCode::Char('F') => {
1036
1040
  self.state.run_manager.next_filter();
1037
- self.add_log(format!("[RUN] Filter: {}", self.state.run_manager.filter.as_str()));
1038
1041
  self.request_render("run_manager_filter");
1039
1042
  }
1040
1043
 
1041
1044
  // Sort
1042
1045
  KeyCode::Char('s') | KeyCode::Char('S') => {
1043
1046
  self.state.run_manager.next_sort();
1044
- self.add_log(format!("[RUN] Sort: {}", self.state.run_manager.sort.as_str()));
1045
1047
  self.request_render("run_manager_sort");
1046
1048
  }
1047
1049
 
1048
1050
  // Refresh
1049
1051
  KeyCode::Char('r') | KeyCode::Char('R') => {
1050
- self.add_log("[RUN] Refreshing run list...".to_string());
1051
1052
  // TODO: Send run.list command via WebSocket
1052
1053
  self.request_render("run_manager_refresh");
1053
1054
  }
@@ -1111,6 +1112,69 @@ impl App {
1111
1112
  Ok(false)
1112
1113
  }
1113
1114
 
1115
+ // ============================================================
1116
+ // AGENT LIST INPUT HANDLING (Step 4)
1117
+ // ============================================================
1118
+
1119
+ /// Handle input when Agent List screen is active
1120
+ fn handle_agent_list_input(&mut self, key: KeyEvent) -> anyhow::Result<bool> {
1121
+ use crossterm::event::KeyModifiers;
1122
+
1123
+ // Ctrl+C/Q to exit
1124
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
1125
+ match key.code {
1126
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
1127
+ _ => return Ok(false),
1128
+ }
1129
+ }
1130
+
1131
+ match key.code {
1132
+ // Navigation
1133
+ KeyCode::Up => {
1134
+ if self.state.agent_list.selected_index > 0 {
1135
+ self.state.agent_list.selected_index -= 1;
1136
+ }
1137
+ self.request_render("agent_list_up");
1138
+ }
1139
+ KeyCode::Down => {
1140
+ let max = self.state.agent_list.agents.len().saturating_sub(1);
1141
+ if self.state.agent_list.selected_index < max {
1142
+ self.state.agent_list.selected_index += 1;
1143
+ }
1144
+ self.request_render("agent_list_down");
1145
+ }
1146
+
1147
+ // View details / Close detail view
1148
+ KeyCode::Enter => {
1149
+ if self.state.agent_list.detail_view.is_some() {
1150
+ // Close detail view
1151
+ self.state.agent_list.detail_view = None;
1152
+ } else {
1153
+ // Open detail view for selected agent
1154
+ self.state.agent_list.detail_view = Some(self.state.agent_list.selected_index);
1155
+ }
1156
+ self.request_render("agent_list_toggle_detail");
1157
+ }
1158
+
1159
+ // Close
1160
+ KeyCode::Esc => {
1161
+ if self.state.agent_list.detail_view.is_some() {
1162
+ // Close detail popup first
1163
+ self.state.agent_list.detail_view = None;
1164
+ self.request_render("agent_list_close_detail");
1165
+ } else {
1166
+ // Close agent list
1167
+ self.pop_overlay();
1168
+ self.request_render("agent_list_close");
1169
+ }
1170
+ }
1171
+
1172
+ _ => {}
1173
+ }
1174
+
1175
+ Ok(false)
1176
+ }
1177
+
1114
1178
  // ============================================================
1115
1179
  // SETTINGS INPUT HANDLING (Step 4.8)
1116
1180
  // ============================================================
@@ -151,12 +151,35 @@ fn main() -> Result<()> {
151
151
  })
152
152
  .collect();
153
153
 
154
+ // Parse agents into AgentInfo structs for Agent List viewer
155
+ use crate::app::AgentInfo;
156
+ let agents: Vec<AgentInfo> = agents_array.iter()
157
+ .filter_map(|agent| {
158
+ let obj = agent.as_object()?;
159
+ Some(AgentInfo {
160
+ name: obj.get("name")?.as_str()?.to_string(),
161
+ description: obj.get("description").and_then(|d| d.as_str()).map(|s| s.to_string()),
162
+ model: obj.get("model").and_then(|m| m.as_str()).unwrap_or("unknown").to_string(),
163
+ provider: obj.get("provider").and_then(|p| p.as_str()).unwrap_or("unknown").to_string(),
164
+ system_prompt: obj.get("systemPrompt").and_then(|sp| sp.as_str()).map(|s| s.to_string()),
165
+ temperature: obj.get("temperature").and_then(|t| t.as_f64()).map(|f| f as f32),
166
+ max_tokens: obj.get("maxTokens").and_then(|mt| mt.as_u64()).map(|u| u as u32),
167
+ tools: obj.get("tools")
168
+ .and_then(|t| t.as_array())
169
+ .map(|arr| arr.iter()
170
+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
171
+ .collect())
172
+ .unwrap_or_default(),
173
+ })
174
+ })
175
+ .collect();
176
+
154
177
  // Update cache with agent data
155
178
  if let Some(cache) = &mut app.state.cache {
156
179
  use crate::storage::cache::AgentData;
157
180
  use std::time::{SystemTime, UNIX_EPOCH};
158
181
 
159
- let agents: Vec<AgentData> = agents_array.iter()
182
+ let cache_agents: Vec<AgentData> = agents_array.iter()
160
183
  .filter_map(|agent| {
161
184
  let obj = agent.as_object()?;
162
185
  Some(AgentData {
@@ -169,14 +192,26 @@ fn main() -> Result<()> {
169
192
  })
170
193
  .collect();
171
194
 
172
- let _ = cache.update_agents(agents);
195
+ let _ = cache.update_agents(cache_agents);
173
196
  }
174
197
 
175
- app.add_log(format!(
176
- "✓ [{}] Loaded {} agents",
177
- short_id,
178
- agents_array.len()
179
- ));
198
+ // Update agent list state
199
+ app.state.agent_list.agents = agents;
200
+ app.state.agent_list.selected_index = 0;
201
+ app.state.agent_list.detail_view = None;
202
+
203
+ // Open AgentList overlay
204
+ app.push_overlay(Screen::AgentList);
205
+ app.request_render("agent_list_opened");
206
+
207
+ // Only log if agents were actually loaded
208
+ if agents_array.len() > 0 {
209
+ app.add_log(format!(
210
+ "✓ [{}] Loaded {} agents",
211
+ short_id,
212
+ agents_array.len()
213
+ ));
214
+ }
180
215
  }
181
216
  } else {
182
217
  app.add_log(format!("✓ [{}] Success", short_id));
@@ -19,6 +19,7 @@ pub enum Screen {
19
19
  AgentBuilder,
20
20
  RunManager,
21
21
  Settings,
22
+ AgentList,
22
23
 
23
24
  // Popup screens (small overlays)
24
25
  Confirmation { message: String, action: String },
@@ -31,7 +32,7 @@ impl Screen {
31
32
  pub fn is_overlay(&self) -> bool {
32
33
  matches!(
33
34
  self,
34
- Screen::AgentBuilder | Screen::RunManager | Screen::Settings
35
+ Screen::AgentBuilder | Screen::RunManager | Screen::Settings | Screen::AgentList
35
36
  )
36
37
  }
37
38
 
@@ -56,6 +57,7 @@ impl Screen {
56
57
  Screen::AgentBuilder => "Agent Builder",
57
58
  Screen::RunManager => "Run Manager",
58
59
  Screen::Settings => "Settings",
60
+ Screen::AgentList => "Agent List",
59
61
  Screen::Confirmation { .. } => "Confirmation",
60
62
  Screen::Alert { .. } => "Alert",
61
63
  Screen::Help => "Help",