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.
- package/mk3-tui/src/app.rs +86 -8
- package/mk3-tui/src/main.rs +54 -4
- package/mk3-tui/src/storage/cache.rs +213 -0
- package/mk3-tui/src/storage/mod.rs +6 -0
- package/mk3-tui/src/ui/agent_builder.rs +421 -30
- package/mk3-tui/src/ui/boot.rs +1 -1
- package/mk3-tui/src/ui/layout.rs +1 -1
- package/mk3-tui/src/ui/run_manager.rs +346 -71
- package/mk3-tui/src/ui/settings.rs +57 -42
- package/mk3-tui/src/websocket.rs +47 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
192
|
+
AMBER_WARN
|
|
183
193
|
} else {
|
|
184
|
-
|
|
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(
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
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::
|
|
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 {
|
|
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::
|
|
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 {
|
|
263
|
+
Style::default().fg(if is_provider_focused { CYBER_CYAN } else { TEXT_PRIMARY })
|
|
252
264
|
),
|
|
253
265
|
]),
|
|
254
266
|
Line::from(vec![
|
|
255
|
-
Span::
|
|
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 {
|
|
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(
|
|
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::
|
|
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 {
|
|
287
|
+
Style::default().fg(if is_gateway_focused { CYBER_CYAN } else { TEXT_PRIMARY })
|
|
276
288
|
),
|
|
277
289
|
]),
|
|
278
290
|
Line::from(vec![
|
|
279
|
-
Span::
|
|
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 {
|
|
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(
|
|
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 {
|
|
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(
|
|
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::
|
|
325
|
-
Span::styled("
|
|
326
|
-
Span::
|
|
327
|
-
Span::styled("
|
|
328
|
-
Span::
|
|
329
|
-
Span::styled("
|
|
330
|
-
Span::
|
|
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::
|
|
335
|
-
Span::styled("
|
|
336
|
-
Span::
|
|
337
|
-
Span::styled("
|
|
338
|
-
Span::
|
|
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(
|
|
358
|
+
.style(Style::default().fg(TEXT_PRIMARY))
|
|
344
359
|
.alignment(Alignment::Center);
|
|
345
360
|
|
|
346
361
|
f.render_widget(paragraph, inner);
|
package/mk3-tui/src/websocket.rs
CHANGED
|
@@ -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::
|
|
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
|
+
"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",
|