4runr-os 2.3.8 → 2.4.0

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,27 @@ 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
+ // WebSocket Commands - Label BEFORE commands with clear separator
387
+ self.state.logs.push_back("[HELP] ─────────────────────────────────────────".into());
351
388
  self.state.logs.push_back("[HELP] WebSocket Commands (requires connection):".into());
389
+ self.state.logs.push_back("[HELP] ─────────────────────────────────────────".into());
352
390
  self.state.logs.push_back(" agent.list - List all agents".into());
353
391
  self.state.logs.push_back(" agent.get - Get agent details (data: {name})".into());
354
392
  self.state.logs.push_back(" agent.create - Create agent (use Agent Builder)".into());
@@ -357,6 +395,8 @@ impl App {
357
395
  self.state.logs.push_back(" run.list - List runs (requires gateway)".into());
358
396
  self.state.logs.push_back(" tool.list - List available tools".into());
359
397
  self.state.logs.push_back("".into());
398
+
399
+ // Screen Controls
360
400
  self.state.logs.push_back("[HELP] Screen Controls:".into());
361
401
  self.state.logs.push_back(" Agent Builder: Enter=Next, Backspace=Prev, ESC=Cancel".into());
362
402
  self.state.logs.push_back(" Run Manager: ↑/↓=Navigate, F=Filter, S=Sort, R=Refresh".into());
@@ -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);
@@ -989,13 +1052,20 @@ impl App {
989
1052
  self.request_render("run_manager_refresh");
990
1053
  }
991
1054
 
992
- // View details
1055
+ // View details / Close detail view
993
1056
  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
1057
+ if self.state.run_manager.is_detail_view() {
1058
+ // Close detail view
1059
+ self.state.run_manager.close_detail_view();
1060
+ self.add_log("[RUN] Closed detail view".to_string());
997
1061
  } else {
998
- self.add_log("[RUN] No run selected".to_string());
1062
+ // Open detail view
1063
+ if let Some(run) = self.state.run_manager.selected_run() {
1064
+ self.add_log(format!("[RUN] Viewing run: {}", run.name));
1065
+ self.state.run_manager.toggle_detail_view();
1066
+ } else {
1067
+ self.add_log("[RUN] No run selected".to_string());
1068
+ }
999
1069
  }
1000
1070
  self.request_render("run_manager_view");
1001
1071
  }
@@ -1022,9 +1092,17 @@ impl App {
1022
1092
  self.request_render("run_manager_delete");
1023
1093
  }
1024
1094
 
1025
- // Close
1095
+ // Close detail view or Run Manager
1026
1096
  KeyCode::Esc => {
1027
- self.pop_overlay();
1097
+ if self.state.run_manager.is_detail_view() {
1098
+ // Close detail view
1099
+ self.state.run_manager.close_detail_view();
1100
+ self.add_log("[RUN] Closed detail view".to_string());
1101
+ self.request_render("run_manager_close_detail");
1102
+ } else {
1103
+ // Close Run Manager
1104
+ self.pop_overlay();
1105
+ }
1028
1106
  }
1029
1107
 
1030
1108
  _ => {}
@@ -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,6 +151,27 @@ fn main() -> Result<()> {
139
151
  })
140
152
  .collect();
141
153
 
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
+
142
175
  app.add_log(format!(
143
176
  "✓ [{}] Loaded {} agents",
144
177
  short_id,
@@ -156,10 +189,26 @@ fn main() -> Result<()> {
156
189
  }
157
190
  } else {
158
191
  let short_id = &resp.id[resp.id.len().saturating_sub(8)..];
192
+ let error_msg = resp.payload.error.unwrap_or_else(|| "Unknown error".to_string());
193
+
194
+ // Try to extract command name from response for better error messages
195
+ let command_hint = if resp.id.contains("agent") {
196
+ " (agent command)"
197
+ } else if resp.id.contains("system") {
198
+ " (system command)"
199
+ } else if resp.id.contains("run") {
200
+ " (run command)"
201
+ } else if resp.id.contains("tool") {
202
+ " (tool command)"
203
+ } else {
204
+ ""
205
+ };
206
+
159
207
  app.add_log(format!(
160
- "✗ [{}] Error: {}",
208
+ "✗ [{}]{} Error: {}",
161
209
  short_id,
162
- resp.payload.error.unwrap_or_default()
210
+ command_hint,
211
+ error_msg
163
212
  ));
164
213
  }
165
214
  }
@@ -171,7 +220,8 @@ fn main() -> Result<()> {
171
220
  ));
172
221
  }
173
222
  WsClientMessage::Error(err) => {
174
- app.add_log(format!("WebSocket error: {}", err));
223
+ app.add_log(format!("[WEBSOCKET] ✗ Error: {}", err));
224
+ app.state.connected = false;
175
225
  }
176
226
  }
177
227
  }
@@ -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};