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.
@@ -5,6 +5,16 @@ use ratatui::prelude::*;
5
5
  use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
6
6
  use crate::app::AppState;
7
7
 
8
+ // === 4RUNR BRAND COLORS (matching layout.rs) ===
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
+
8
18
  /// Settings state
9
19
  #[derive(Debug, Clone)]
10
20
  pub struct SettingsState {
@@ -170,19 +180,20 @@ pub fn render(f: &mut Frame, state: &AppState) {
170
180
 
171
181
  fn render_header(f: &mut Frame, area: Rect, settings: &SettingsState) {
172
182
  let title = if settings.has_changes {
173
- " Settings (Unsaved Changes) "
183
+ " ⚙️ Settings (Unsaved Changes) "
174
184
  } else {
175
- " Settings "
185
+ " ⚙️ Settings "
176
186
  };
177
187
 
178
188
  let block = Block::default()
179
189
  .title(title)
180
190
  .borders(Borders::ALL)
181
191
  .border_style(Style::default().fg(if settings.has_changes {
182
- Color::Yellow
192
+ AMBER_WARN
183
193
  } else {
184
- Color::Cyan
185
- }));
194
+ BRAND_PURPLE
195
+ }))
196
+ .style(Style::default().bg(BG_PANEL));
186
197
 
187
198
  f.render_widget(block, area);
188
199
  }
@@ -190,7 +201,8 @@ fn render_header(f: &mut Frame, area: Rect, settings: &SettingsState) {
190
201
  fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState) {
191
202
  let block = Block::default()
192
203
  .borders(Borders::ALL)
193
- .border_style(Style::default().fg(Color::Cyan));
204
+ .border_style(Style::default().fg(CYBER_CYAN))
205
+ .style(Style::default().bg(BG_PANEL));
194
206
 
195
207
  let inner = block.inner(area);
196
208
  f.render_widget(block, area);
@@ -206,9 +218,9 @@ fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState)
206
218
  Line::from(vec![
207
219
  Span::styled(
208
220
  if is_mode_focused { "▶ " } else { " " },
209
- Style::default().fg(Color::Yellow)
221
+ Style::default().fg(BRAND_PURPLE).bold()
210
222
  ),
211
- Span::styled("Operation Mode", Style::default().bold()),
223
+ Span::styled("🌐 Operation Mode", Style::default().fg(TEXT_PRIMARY).bold()),
212
224
  ]),
213
225
  Line::from(vec![
214
226
  Span::raw(" "),
@@ -217,14 +229,14 @@ fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState)
217
229
  if settings.mode == OperationMode::Local { "●" } else { " " },
218
230
  if settings.mode == OperationMode::Connected { "●" } else { " " }
219
231
  ),
220
- Style::default().fg(if is_mode_focused { Color::Yellow } else { Color::White })
232
+ Style::default().fg(if is_mode_focused { CYBER_CYAN } else { TEXT_PRIMARY })
221
233
  ),
222
234
  ]),
223
235
  Line::from(vec![
224
236
  Span::raw(" "),
225
237
  Span::styled(
226
238
  settings.mode.description(),
227
- Style::default().fg(Color::DarkGray).italic()
239
+ Style::default().fg(TEXT_DIM).italic()
228
240
  ),
229
241
  ]),
230
242
  Line::from(""),
@@ -233,29 +245,29 @@ fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState)
233
245
  Line::from(vec![
234
246
  Span::styled(
235
247
  if is_provider_focused { "▶ " } else { " " },
236
- Style::default().fg(Color::Yellow)
248
+ Style::default().fg(BRAND_PURPLE).bold()
237
249
  ),
238
- Span::styled("AI Provider", Style::default().bold()),
250
+ Span::styled("🤖 AI Provider", Style::default().fg(TEXT_PRIMARY).bold()),
239
251
  ]),
240
252
  Line::from(vec![
241
- Span::raw(" Provider: "),
253
+ Span::styled(" Provider: ", Style::default().fg(TEXT_DIM)),
242
254
  Span::styled(
243
255
  &settings.ai_provider,
244
- Style::default().fg(if is_provider_focused { Color::Yellow } else { Color::Cyan })
256
+ Style::default().fg(if is_provider_focused { CYBER_CYAN } else { TEXT_PRIMARY }).bold()
245
257
  ),
246
258
  ]),
247
259
  Line::from(vec![
248
- Span::raw(" Model: "),
260
+ Span::styled(" Model: ", Style::default().fg(TEXT_DIM)),
249
261
  Span::styled(
250
262
  &settings.ai_model,
251
- Style::default().fg(if is_provider_focused { Color::Yellow } else { Color::Cyan })
263
+ Style::default().fg(if is_provider_focused { CYBER_CYAN } else { TEXT_PRIMARY })
252
264
  ),
253
265
  ]),
254
266
  Line::from(vec![
255
- Span::raw(" API Key: "),
267
+ Span::styled(" API Key: ", Style::default().fg(TEXT_DIM)),
256
268
  Span::styled(
257
269
  if settings.api_key_set { "Configured ✓" } else { "Not set" },
258
- Style::default().fg(if settings.api_key_set { Color::Green } else { Color::Red })
270
+ Style::default().fg(if settings.api_key_set { NEON_GREEN } else { Color::Rgb(255, 69, 69) })
259
271
  ),
260
272
  ]),
261
273
  Line::from(""),
@@ -264,22 +276,22 @@ fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState)
264
276
  Line::from(vec![
265
277
  Span::styled(
266
278
  if is_gateway_focused { "▶ " } else { " " },
267
- Style::default().fg(Color::Yellow)
279
+ Style::default().fg(BRAND_PURPLE).bold()
268
280
  ),
269
- Span::styled("Gateway Connection", Style::default().bold()),
281
+ Span::styled("🔗 Gateway Connection", Style::default().fg(TEXT_PRIMARY).bold()),
270
282
  ]),
271
283
  Line::from(vec![
272
- Span::raw(" URL: "),
284
+ Span::styled(" URL: ", Style::default().fg(TEXT_DIM)),
273
285
  Span::styled(
274
286
  &settings.gateway_url,
275
- Style::default().fg(if is_gateway_focused { Color::Yellow } else { Color::Cyan })
287
+ Style::default().fg(if is_gateway_focused { CYBER_CYAN } else { TEXT_PRIMARY })
276
288
  ),
277
289
  ]),
278
290
  Line::from(vec![
279
- Span::raw(" Status: "),
291
+ Span::styled(" Status: ", Style::default().fg(TEXT_DIM)),
280
292
  Span::styled(
281
293
  if settings.gateway_connected { "Connected ✓" } else { "Disconnected" },
282
- Style::default().fg(if settings.gateway_connected { Color::Green } else { Color::DarkGray })
294
+ Style::default().fg(if settings.gateway_connected { NEON_GREEN } else { TEXT_MUTED })
283
295
  ),
284
296
  ]),
285
297
  Line::from(""),
@@ -288,9 +300,9 @@ fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState)
288
300
  Line::from(vec![
289
301
  Span::styled(
290
302
  if is_autoupdate_focused { "▶ " } else { " " },
291
- Style::default().fg(Color::Yellow)
303
+ Style::default().fg(BRAND_PURPLE).bold()
292
304
  ),
293
- Span::styled("Auto-Update", Style::default().bold()),
305
+ Span::styled("🔄 Auto-Update", Style::default().fg(TEXT_PRIMARY).bold()),
294
306
  ]),
295
307
  Line::from(vec![
296
308
  Span::raw(" "),
@@ -298,7 +310,7 @@ fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState)
298
310
  format!("[{}] Enable automatic updates",
299
311
  if settings.auto_update { "✓" } else { " " }
300
312
  ),
301
- Style::default().fg(if is_autoupdate_focused { Color::Yellow } else { Color::White })
313
+ Style::default().fg(if is_autoupdate_focused { CYBER_CYAN } else { TEXT_PRIMARY })
302
314
  ),
303
315
  ]),
304
316
  ];
@@ -312,35 +324,38 @@ fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState)
312
324
 
313
325
  fn render_actions(f: &mut Frame, area: Rect, settings: &SettingsState) {
314
326
  let block = Block::default()
315
- .title(" Actions ")
327
+ .title(" ⌨️ Actions ")
316
328
  .borders(Borders::ALL)
317
- .border_style(Style::default().fg(Color::DarkGray));
329
+ .border_style(Style::default().fg(TEXT_DIM))
330
+ .style(Style::default().bg(BG_PANEL));
318
331
 
319
332
  let inner = block.inner(area);
320
333
  f.render_widget(block, area);
321
334
 
322
335
  let text = if settings.has_changes {
323
336
  Line::from(vec![
324
- Span::raw("↑/↓ Navigate | "),
325
- Span::styled("Space", Style::default().fg(Color::Yellow)),
326
- Span::raw(" Toggle | "),
327
- Span::styled("Enter", Style::default().fg(Color::Green)),
328
- Span::raw(" Save | "),
329
- Span::styled("ESC", Style::default().fg(Color::Red)),
330
- Span::raw(" Discard & Close"),
337
+ Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
338
+ Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
339
+ Span::styled("Space", Style::default().fg(AMBER_WARN).bold()),
340
+ Span::styled(" Toggle │ ", Style::default().fg(TEXT_DIM)),
341
+ Span::styled("Enter", Style::default().fg(NEON_GREEN).bold()),
342
+ Span::styled(" Save │ ", Style::default().fg(TEXT_DIM)),
343
+ Span::styled("ESC", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
344
+ Span::styled(" Discard & Close", Style::default().fg(TEXT_DIM)),
331
345
  ])
332
346
  } else {
333
347
  Line::from(vec![
334
- Span::raw("↑/↓ Navigate | "),
335
- Span::styled("Space", Style::default().fg(Color::Yellow)),
336
- Span::raw(" Toggle | "),
337
- Span::styled("ESC", Style::default().fg(Color::Red)),
338
- Span::raw(" Close"),
348
+ Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
349
+ Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
350
+ Span::styled("Space", Style::default().fg(AMBER_WARN).bold()),
351
+ Span::styled(" Toggle │ ", Style::default().fg(TEXT_DIM)),
352
+ Span::styled("ESC", Style::default().fg(BRAND_PURPLE).bold()),
353
+ Span::styled(" Close", Style::default().fg(TEXT_DIM)),
339
354
  ])
340
355
  };
341
356
 
342
357
  let paragraph = Paragraph::new(text)
343
- .style(Style::default().fg(Color::DarkGray))
358
+ .style(Style::default().fg(TEXT_PRIMARY))
344
359
  .alignment(Alignment::Center);
345
360
 
346
361
  f.render_widget(paragraph, inner);
@@ -72,6 +72,9 @@ pub enum WsClientMessage {
72
72
  Response(ResponseMessage),
73
73
  Event(EventMessage),
74
74
  Error(String),
75
+ ConnectionLost,
76
+ Reconnecting,
77
+ ReconnectFailed(String),
75
78
  }
76
79
 
77
80
  pub struct WebSocketClient {
@@ -79,6 +82,7 @@ pub struct WebSocketClient {
79
82
  tx: mpsc::UnboundedSender<WsMessage>,
80
83
  rx: Arc<Mutex<mpsc::UnboundedReceiver<WsClientMessage>>>,
81
84
  connected: Arc<Mutex<bool>>,
85
+ pending_commands: Arc<Mutex<Vec<(String, String, Option<serde_json::Value>)>>>, // (id, command, data)
82
86
  }
83
87
 
84
88
  impl WebSocketClient {
@@ -102,6 +106,7 @@ impl WebSocketClient {
102
106
  tx: msg_tx,
103
107
  rx: Arc::new(Mutex::new(client_rx)),
104
108
  connected,
109
+ pending_commands: Arc::new(Mutex::new(Vec::new())),
105
110
  })
106
111
  }
107
112
 
@@ -146,7 +151,8 @@ impl WebSocketClient {
146
151
  }
147
152
  Err(e) => {
148
153
  *connected.lock().unwrap() = false;
149
- let _ = client_tx.send(WsClientMessage::Error(e.to_string()));
154
+ let _ = client_tx.send(WsClientMessage::ConnectionLost);
155
+ let _ = client_tx.send(WsClientMessage::Error(format!("Connection error: {}", e)));
150
156
  break;
151
157
  }
152
158
  _ => {}
@@ -188,15 +194,54 @@ impl WebSocketClient {
188
194
  timestamp,
189
195
  payload: CommandPayload {
190
196
  command: command.to_string(),
191
- data,
197
+ data: data.clone(),
192
198
  },
193
199
  });
194
200
 
195
201
  let json = serde_json::to_string(&cmd)?;
202
+
203
+ // Track pending command for retry
204
+ if self.is_connected() {
205
+ self.pending_commands.lock().unwrap().push((id.clone(), command.to_string(), data));
206
+ }
207
+
196
208
  self.tx.send(WsMessage::Text(json))?;
197
209
 
198
210
  Ok(id)
199
211
  }
212
+
213
+ /// Retry pending commands after reconnection
214
+ pub fn retry_pending_commands(&self) -> Result<Vec<String>> {
215
+ let mut pending = self.pending_commands.lock().unwrap();
216
+ let mut retried_ids = Vec::new();
217
+
218
+ for (old_id, command, data) in pending.drain(..) {
219
+ // Generate new ID for retry
220
+ let new_id = format!("retry-{}", uuid::Uuid::new_v4());
221
+ let timestamp = chrono::Utc::now().to_rfc3339();
222
+
223
+ let cmd = Message::Command(CommandMessage {
224
+ id: new_id.clone(),
225
+ timestamp,
226
+ payload: CommandPayload {
227
+ command,
228
+ data,
229
+ },
230
+ });
231
+
232
+ let json = serde_json::to_string(&cmd)?;
233
+ self.tx.send(WsMessage::Text(json))?;
234
+
235
+ retried_ids.push(format!("{} -> {}", &old_id[old_id.len().saturating_sub(8)..], &new_id[new_id.len().saturating_sub(8)..]));
236
+ }
237
+
238
+ Ok(retried_ids)
239
+ }
240
+
241
+ /// Clear pending commands (e.g., on explicit disconnect)
242
+ pub fn clear_pending_commands(&self) {
243
+ self.pending_commands.lock().unwrap().clear();
244
+ }
200
245
 
201
246
  pub fn try_recv(&self) -> Option<WsClientMessage> {
202
247
  self.rx.lock().unwrap().try_recv().ok()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.3.8",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.3.5: Fixed boot screen logo rendering (restored original working version), fully functional Agent Builder with input handling and TUI styling. Built with Rust + Ratatui. ⚠️ Pre-MVP / Development Phase",
6
6
  "main": "dist/index.js",