4runr-os 2.3.9 → 2.4.1

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,5 +1,6 @@
1
1
  use crate::io::IoHandler;
2
2
  use crate::screens::{NavigationState, Screen};
3
+ use crate::storage::Cache;
3
4
  use crate::ui::agent_builder::AgentBuilderState;
4
5
  use crate::ui::run_manager::RunManagerState;
5
6
  use crate::ui::settings::SettingsState;
@@ -95,6 +96,10 @@ pub struct AppState {
95
96
  pub agent_builder: AgentBuilderState,
96
97
  pub run_manager: RunManagerState,
97
98
  pub settings: SettingsState,
99
+
100
+ // Local cache
101
+ pub cache: Option<Cache>,
102
+ pub cache_loaded: bool,
98
103
  }
99
104
 
100
105
  impl Default for AppState {
@@ -148,6 +153,8 @@ impl Default for AppState {
148
153
  agent_builder: AgentBuilderState::default(),
149
154
  run_manager: RunManagerState::default(),
150
155
  settings: SettingsState::default(),
156
+ cache: Cache::new().ok(),
157
+ cache_loaded: false,
151
158
  }
152
159
  }
153
160
  }
@@ -170,14 +177,37 @@ impl App {
170
177
  RunMode::Local => Duration::from_millis(25),
171
178
  };
172
179
 
180
+ let mut state = AppState::default();
181
+ Self::load_cached_data(&mut state);
182
+
173
183
  Self {
174
- state: AppState::default(),
184
+ state,
175
185
  render_scheduler,
176
186
  input_debounce: None,
177
187
  input_debounce_duration,
178
188
  }
179
189
  }
180
190
 
191
+ /// Load cached data on startup
192
+ fn load_cached_data(state: &mut AppState) {
193
+ if let Some(cache) = &state.cache {
194
+ // Load cached agents
195
+ let agents = cache.get_agents();
196
+ if !agents.is_empty() {
197
+ state.capabilities = agents.iter()
198
+ .map(|a| a.name.clone())
199
+ .collect();
200
+ state.cache_loaded = true;
201
+ }
202
+
203
+ // Load cached system status
204
+ if let Some(status) = cache.get_system_status() {
205
+ state.network_status = if status.connected { "Connected (cached)".to_string() } else { "Disconnected (cached)".to_string() };
206
+ state.posture_status = format!("{} (cached)", status.posture);
207
+ }
208
+ }
209
+ }
210
+
181
211
  pub fn request_render(&mut self, reason: &str) -> bool {
182
212
  self.render_scheduler.request_render(reason)
183
213
  }
@@ -336,19 +366,33 @@ impl App {
336
366
  self.state.logs.push_back("[HELP] 4Runr AI Agent OS - Command Reference".into());
337
367
  self.state.logs.push_back("[HELP] ═══════════════════════════════════════".into());
338
368
  self.state.logs.push_back("".into());
369
+
370
+ // Local Commands
339
371
  self.state.logs.push_back("[HELP] Local Commands:".into());
340
372
  self.state.logs.push_back(" quit, exit - Exit application".into());
341
373
  self.state.logs.push_back(" clear - Clear logs".into());
342
374
  self.state.logs.push_back(" help - Show this help".into());
343
375
  self.state.logs.push_back(" :perf - Show performance stats".into());
344
376
  self.state.logs.push_back("".into());
377
+
378
+ // Navigation Commands
345
379
  self.state.logs.push_back("[HELP] Navigation Commands:".into());
346
380
  self.state.logs.push_back(" build - Open Agent Builder (6-step wizard)".into());
347
381
  self.state.logs.push_back(" runs - Open Run Manager (list, filter, sort)".into());
348
382
  self.state.logs.push_back(" config, settings - Open Settings (mode, AI provider)".into());
349
383
  self.state.logs.push_back(" ESC - Close overlay/popup or clear input".into());
350
384
  self.state.logs.push_back("".into());
385
+
386
+ // Screen Controls
387
+ self.state.logs.push_back("[HELP] Screen Controls:".into());
388
+ self.state.logs.push_back(" Agent Builder: Enter=Next, Backspace=Prev, ESC=Cancel".into());
389
+ self.state.logs.push_back(" Run Manager: ↑/↓=Navigate, F=Filter, S=Sort, R=Refresh".into());
390
+ self.state.logs.push_back(" Settings: ↑/↓=Navigate, Space=Toggle, Enter=Save".into());
391
+ self.state.logs.push_back("".into());
392
+
393
+ // WebSocket Commands - Label BEFORE separator
351
394
  self.state.logs.push_back("[HELP] WebSocket Commands (requires connection):".into());
395
+ self.state.logs.push_back("[HELP] ─────────────────────────────────────────".into());
352
396
  self.state.logs.push_back(" agent.list - List all agents".into());
353
397
  self.state.logs.push_back(" agent.get - Get agent details (data: {name})".into());
354
398
  self.state.logs.push_back(" agent.create - Create agent (use Agent Builder)".into());
@@ -357,11 +401,7 @@ impl App {
357
401
  self.state.logs.push_back(" run.list - List runs (requires gateway)".into());
358
402
  self.state.logs.push_back(" tool.list - List available tools".into());
359
403
  self.state.logs.push_back("".into());
360
- self.state.logs.push_back("[HELP] Screen Controls:".into());
361
- self.state.logs.push_back(" Agent Builder: Enter=Next, Backspace=Prev, ESC=Cancel".into());
362
- self.state.logs.push_back(" Run Manager: ↑/↓=Navigate, F=Filter, S=Sort, R=Refresh".into());
363
- self.state.logs.push_back(" Settings: ↑/↓=Navigate, Space=Toggle, Enter=Save".into());
364
- self.state.logs.push_back("".into());
404
+
365
405
  self.state.logs.push_back("[HELP] Press F12 to toggle performance overlay".into());
366
406
  self.state.logs.push_back("[HELP] ═══════════════════════════════════════".into());
367
407
  }
@@ -680,6 +720,29 @@ impl App {
680
720
  self.handle_agent_builder_backspace();
681
721
  self.request_render("agent_builder_backspace");
682
722
  }
723
+ // Left Arrow - go back to previous step
724
+ KeyCode::Left => {
725
+ if self.state.agent_builder.current_step > 1 {
726
+ self.state.agent_builder.prev_step();
727
+ self.add_log("Agent Builder: Moved to previous step".to_string());
728
+ self.request_render("agent_builder_prev_step");
729
+ }
730
+ }
731
+ // Right Arrow - move to next step (same as Enter)
732
+ KeyCode::Right => {
733
+ if self.state.agent_builder.current_step == 6 {
734
+ // Final step - create agent
735
+ self.create_agent_from_builder(ws_client);
736
+ } else {
737
+ // Move to next step
738
+ if self.state.agent_builder.next_step() {
739
+ self.request_render("agent_builder_next");
740
+ } else {
741
+ // Validation failed - errors are in state
742
+ self.request_render("agent_builder_validation");
743
+ }
744
+ }
745
+ }
683
746
  // Tab - move to next field (within step)
684
747
  KeyCode::Tab => {
685
748
  let shift = key.modifiers.contains(KeyModifiers::SHIFT);
@@ -971,31 +1034,35 @@ impl App {
971
1034
  // Filter
972
1035
  KeyCode::Char('f') | KeyCode::Char('F') => {
973
1036
  self.state.run_manager.next_filter();
974
- self.add_log(format!("[RUN] Filter: {}", self.state.run_manager.filter.as_str()));
975
1037
  self.request_render("run_manager_filter");
976
1038
  }
977
1039
 
978
1040
  // Sort
979
1041
  KeyCode::Char('s') | KeyCode::Char('S') => {
980
1042
  self.state.run_manager.next_sort();
981
- self.add_log(format!("[RUN] Sort: {}", self.state.run_manager.sort.as_str()));
982
1043
  self.request_render("run_manager_sort");
983
1044
  }
984
1045
 
985
1046
  // Refresh
986
1047
  KeyCode::Char('r') | KeyCode::Char('R') => {
987
- self.add_log("[RUN] Refreshing run list...".to_string());
988
1048
  // TODO: Send run.list command via WebSocket
989
1049
  self.request_render("run_manager_refresh");
990
1050
  }
991
1051
 
992
- // View details
1052
+ // View details / Close detail view
993
1053
  KeyCode::Enter => {
994
- if let Some(run) = self.state.run_manager.selected_run() {
995
- self.add_log(format!("[RUN] Viewing run: {}", run.name));
996
- // TODO: Open run details overlay
1054
+ if self.state.run_manager.is_detail_view() {
1055
+ // Close detail view
1056
+ self.state.run_manager.close_detail_view();
1057
+ self.add_log("[RUN] Closed detail view".to_string());
997
1058
  } else {
998
- self.add_log("[RUN] No run selected".to_string());
1059
+ // Open detail view
1060
+ if let Some(run) = self.state.run_manager.selected_run() {
1061
+ self.add_log(format!("[RUN] Viewing run: {}", run.name));
1062
+ self.state.run_manager.toggle_detail_view();
1063
+ } else {
1064
+ self.add_log("[RUN] No run selected".to_string());
1065
+ }
999
1066
  }
1000
1067
  self.request_render("run_manager_view");
1001
1068
  }
@@ -1022,9 +1089,17 @@ impl App {
1022
1089
  self.request_render("run_manager_delete");
1023
1090
  }
1024
1091
 
1025
- // Close
1092
+ // Close detail view or Run Manager
1026
1093
  KeyCode::Esc => {
1027
- self.pop_overlay();
1094
+ if self.state.run_manager.is_detail_view() {
1095
+ // Close detail view
1096
+ self.state.run_manager.close_detail_view();
1097
+ self.add_log("[RUN] Closed detail view".to_string());
1098
+ self.request_render("run_manager_close_detail");
1099
+ } else {
1100
+ // Close Run Manager
1101
+ self.pop_overlay();
1102
+ }
1028
1103
  }
1029
1104
 
1030
1105
  _ => {}
@@ -9,6 +9,7 @@ use std::time::{Duration, Instant};
9
9
  mod app;
10
10
  mod io;
11
11
  mod screens;
12
+ mod storage;
12
13
  mod ui;
13
14
  mod websocket;
14
15
 
@@ -99,7 +100,18 @@ fn main() -> Result<()> {
99
100
  }
100
101
  }
101
102
  WsClientMessage::Disconnected => {
102
- app.add_log("WebSocket disconnected".to_string());
103
+ app.add_log("[WEBSOCKET] Disconnected from server".to_string());
104
+ app.state.connected = false;
105
+ }
106
+ WsClientMessage::ConnectionLost => {
107
+ app.add_log("[WEBSOCKET] Connection lost - attempting to reconnect...".to_string());
108
+ app.state.connected = false;
109
+ }
110
+ WsClientMessage::Reconnecting => {
111
+ app.add_log("[WEBSOCKET] Reconnecting...".to_string());
112
+ }
113
+ WsClientMessage::ReconnectFailed(reason) => {
114
+ app.add_log(format!("[WEBSOCKET] Reconnection failed: {}", reason));
103
115
  }
104
116
  WsClientMessage::Response(resp) => {
105
117
  if resp.payload.success {
@@ -139,11 +151,35 @@ fn main() -> Result<()> {
139
151
  })
140
152
  .collect();
141
153
 
142
- app.add_log(format!(
143
- "✓ [{}] Loaded {} agents",
144
- short_id,
145
- agents_array.len()
146
- ));
154
+ // Update cache with agent data
155
+ if let Some(cache) = &mut app.state.cache {
156
+ use crate::storage::cache::AgentData;
157
+ use std::time::{SystemTime, UNIX_EPOCH};
158
+
159
+ let agents: Vec<AgentData> = agents_array.iter()
160
+ .filter_map(|agent| {
161
+ let obj = agent.as_object()?;
162
+ Some(AgentData {
163
+ name: obj.get("name")?.as_str()?.to_string(),
164
+ description: obj.get("description").and_then(|d| d.as_str()).map(|s| s.to_string()),
165
+ model: obj.get("model").and_then(|m| m.as_str()).unwrap_or("unknown").to_string(),
166
+ provider: obj.get("provider").and_then(|p| p.as_str()).unwrap_or("unknown").to_string(),
167
+ created_at: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
168
+ })
169
+ })
170
+ .collect();
171
+
172
+ let _ = cache.update_agents(agents);
173
+ }
174
+
175
+ // Only log if agents were actually loaded
176
+ if agents_array.len() > 0 {
177
+ app.add_log(format!(
178
+ "✓ [{}] Loaded {} agents",
179
+ short_id,
180
+ agents_array.len()
181
+ ));
182
+ }
147
183
  }
148
184
  } else {
149
185
  app.add_log(format!("✓ [{}] Success", short_id));
@@ -156,10 +192,26 @@ fn main() -> Result<()> {
156
192
  }
157
193
  } else {
158
194
  let short_id = &resp.id[resp.id.len().saturating_sub(8)..];
195
+ let error_msg = resp.payload.error.unwrap_or_else(|| "Unknown error".to_string());
196
+
197
+ // Try to extract command name from response for better error messages
198
+ let command_hint = if resp.id.contains("agent") {
199
+ " (agent command)"
200
+ } else if resp.id.contains("system") {
201
+ " (system command)"
202
+ } else if resp.id.contains("run") {
203
+ " (run command)"
204
+ } else if resp.id.contains("tool") {
205
+ " (tool command)"
206
+ } else {
207
+ ""
208
+ };
209
+
159
210
  app.add_log(format!(
160
- "✗ [{}] Error: {}",
211
+ "✗ [{}]{} Error: {}",
161
212
  short_id,
162
- resp.payload.error.unwrap_or_default()
213
+ command_hint,
214
+ error_msg
163
215
  ));
164
216
  }
165
217
  }
@@ -171,7 +223,8 @@ fn main() -> Result<()> {
171
223
  ));
172
224
  }
173
225
  WsClientMessage::Error(err) => {
174
- app.add_log(format!("WebSocket error: {}", err));
226
+ app.add_log(format!("[WEBSOCKET] ✗ Error: {}", err));
227
+ app.state.connected = false;
175
228
  }
176
229
  }
177
230
  }
@@ -0,0 +1,213 @@
1
+ /// Cache implementation for local data storage
2
+ /// Stores agents, runs, and system status in JSON format
3
+
4
+ use serde::{Deserialize, Serialize};
5
+ use std::fs;
6
+ use std::path::PathBuf;
7
+ use std::time::{SystemTime, UNIX_EPOCH};
8
+
9
+ #[derive(Debug, Clone, Serialize, Deserialize)]
10
+ pub struct AgentData {
11
+ pub name: String,
12
+ pub description: Option<String>,
13
+ pub model: String,
14
+ pub provider: String,
15
+ pub created_at: u64,
16
+ }
17
+
18
+ #[derive(Debug, Clone, Serialize, Deserialize)]
19
+ pub struct RunData {
20
+ pub id: String,
21
+ pub name: String,
22
+ pub agent: String,
23
+ pub status: String,
24
+ pub started_at: u64,
25
+ pub duration: Option<u64>,
26
+ pub tokens_used: Option<u64>,
27
+ pub cost: Option<f64>,
28
+ }
29
+
30
+ #[derive(Debug, Clone, Serialize, Deserialize)]
31
+ pub struct SystemStatusData {
32
+ pub connected: bool,
33
+ pub gateway_url: Option<String>,
34
+ pub posture: String,
35
+ pub last_updated: u64,
36
+ }
37
+
38
+ #[derive(Debug, Clone, Serialize, Deserialize, Default)]
39
+ pub struct CacheData {
40
+ pub agents: Vec<AgentData>,
41
+ pub runs: Vec<RunData>,
42
+ pub system_status: Option<SystemStatusData>,
43
+ pub last_updated: u64,
44
+ }
45
+
46
+ #[derive(Debug, Clone)]
47
+ pub struct Cache {
48
+ cache_path: PathBuf,
49
+ data: CacheData,
50
+ }
51
+
52
+ impl Cache {
53
+ /// Create a new cache instance
54
+ pub fn new() -> Result<Self, std::io::Error> {
55
+ let cache_dir = Self::get_cache_dir()?;
56
+ fs::create_dir_all(&cache_dir)?;
57
+
58
+ let cache_path = cache_dir.join("4runr_cache.json");
59
+ let data = Self::load_from_file(&cache_path)?;
60
+
61
+ Ok(Self { cache_path, data })
62
+ }
63
+
64
+ /// Get the cache directory path
65
+ fn get_cache_dir() -> Result<PathBuf, std::io::Error> {
66
+ #[cfg(target_os = "windows")]
67
+ {
68
+ let appdata = std::env::var("APPDATA")
69
+ .unwrap_or_else(|_| String::from("C:\\Users\\Default\\AppData\\Roaming"));
70
+ Ok(PathBuf::from(appdata).join("4runr"))
71
+ }
72
+
73
+ #[cfg(not(target_os = "windows"))]
74
+ {
75
+ let home = std::env::var("HOME")
76
+ .unwrap_or_else(|_| String::from("/tmp"));
77
+ Ok(PathBuf::from(home).join(".4runr"))
78
+ }
79
+ }
80
+
81
+ /// Load cache from file
82
+ fn load_from_file(path: &PathBuf) -> Result<CacheData, std::io::Error> {
83
+ if !path.exists() {
84
+ return Ok(CacheData::default());
85
+ }
86
+
87
+ let contents = fs::read_to_string(path)?;
88
+ let data: CacheData = serde_json::from_str(&contents)
89
+ .unwrap_or_default();
90
+
91
+ Ok(data)
92
+ }
93
+
94
+ /// Save cache to file
95
+ fn save_to_file(&self) -> Result<(), std::io::Error> {
96
+ let json = serde_json::to_string_pretty(&self.data)?;
97
+ fs::write(&self.cache_path, json)?;
98
+ Ok(())
99
+ }
100
+
101
+ /// Get current timestamp
102
+ fn now() -> u64 {
103
+ SystemTime::now()
104
+ .duration_since(UNIX_EPOCH)
105
+ .unwrap()
106
+ .as_secs()
107
+ }
108
+
109
+ // === Agent Operations ===
110
+
111
+ /// Get all cached agents
112
+ pub fn get_agents(&self) -> Vec<AgentData> {
113
+ self.data.agents.clone()
114
+ }
115
+
116
+ /// Update agents cache
117
+ pub fn update_agents(&mut self, agents: Vec<AgentData>) -> Result<(), std::io::Error> {
118
+ self.data.agents = agents;
119
+ self.data.last_updated = Self::now();
120
+ self.save_to_file()
121
+ }
122
+
123
+ /// Add a single agent
124
+ pub fn add_agent(&mut self, agent: AgentData) -> Result<(), std::io::Error> {
125
+ self.data.agents.push(agent);
126
+ self.data.last_updated = Self::now();
127
+ self.save_to_file()
128
+ }
129
+
130
+ /// Remove an agent by name
131
+ pub fn remove_agent(&mut self, name: &str) -> Result<(), std::io::Error> {
132
+ self.data.agents.retain(|a| a.name != name);
133
+ self.data.last_updated = Self::now();
134
+ self.save_to_file()
135
+ }
136
+
137
+ // === Run Operations ===
138
+
139
+ /// Get all cached runs
140
+ pub fn get_runs(&self) -> Vec<RunData> {
141
+ self.data.runs.clone()
142
+ }
143
+
144
+ /// Update runs cache
145
+ pub fn update_runs(&mut self, runs: Vec<RunData>) -> Result<(), std::io::Error> {
146
+ self.data.runs = runs;
147
+ self.data.last_updated = Self::now();
148
+ self.save_to_file()
149
+ }
150
+
151
+ /// Add a single run
152
+ pub fn add_run(&mut self, run: RunData) -> Result<(), std::io::Error> {
153
+ self.data.runs.push(run);
154
+ self.data.last_updated = Self::now();
155
+ self.save_to_file()
156
+ }
157
+
158
+ /// Update a run by ID
159
+ pub fn update_run(&mut self, id: &str, run: RunData) -> Result<(), std::io::Error> {
160
+ if let Some(existing) = self.data.runs.iter_mut().find(|r| r.id == id) {
161
+ *existing = run;
162
+ self.data.last_updated = Self::now();
163
+ self.save_to_file()?;
164
+ }
165
+ Ok(())
166
+ }
167
+
168
+ // === System Status Operations ===
169
+
170
+ /// Get cached system status
171
+ pub fn get_system_status(&self) -> Option<SystemStatusData> {
172
+ self.data.system_status.clone()
173
+ }
174
+
175
+ /// Update system status cache
176
+ pub fn update_system_status(&mut self, status: SystemStatusData) -> Result<(), std::io::Error> {
177
+ self.data.system_status = Some(status);
178
+ self.data.last_updated = Self::now();
179
+ self.save_to_file()
180
+ }
181
+
182
+ // === Cache Management ===
183
+
184
+ /// Get cache age in seconds
185
+ pub fn get_age(&self) -> u64 {
186
+ Self::now().saturating_sub(self.data.last_updated)
187
+ }
188
+
189
+ /// Check if cache is stale (older than threshold)
190
+ pub fn is_stale(&self, threshold_secs: u64) -> bool {
191
+ self.get_age() > threshold_secs
192
+ }
193
+
194
+ /// Clear all cached data
195
+ pub fn clear(&mut self) -> Result<(), std::io::Error> {
196
+ self.data = CacheData::default();
197
+ self.save_to_file()
198
+ }
199
+
200
+ /// Get cache data reference
201
+ pub fn data(&self) -> &CacheData {
202
+ &self.data
203
+ }
204
+ }
205
+
206
+ impl Default for Cache {
207
+ fn default() -> Self {
208
+ Self::new().unwrap_or_else(|_| Self {
209
+ cache_path: PathBuf::from("4runr_cache.json"),
210
+ data: CacheData::default(),
211
+ })
212
+ }
213
+ }
@@ -0,0 +1,6 @@
1
+ /// Local data storage and caching module
2
+ /// Provides persistent storage for agents, runs, and system status
3
+
4
+ pub mod cache;
5
+
6
+ pub use cache::{Cache, CacheData};