4runr-os 2.10.57 → 2.10.58
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 +48 -31
- package/mk3-tui/src/main.rs +1 -5
- package/mk3-tui/src/ui/run_manager.rs +58 -27
- package/package.json +2 -2
package/mk3-tui/src/app.rs
CHANGED
|
@@ -2226,34 +2226,54 @@ impl App {
|
|
|
2226
2226
|
}
|
|
2227
2227
|
|
|
2228
2228
|
match key.code {
|
|
2229
|
-
// Navigation
|
|
2229
|
+
// Navigation — list only; detail view locks to pinned run
|
|
2230
2230
|
KeyCode::Up => {
|
|
2231
|
-
self.state.run_manager.
|
|
2231
|
+
if self.state.run_manager.is_detail_view() {
|
|
2232
|
+
self.state.run_manager.detail_log_scroll_up();
|
|
2233
|
+
} else {
|
|
2234
|
+
self.state.run_manager.select_previous();
|
|
2235
|
+
}
|
|
2232
2236
|
self.request_render("run_manager_up");
|
|
2233
2237
|
}
|
|
2234
2238
|
KeyCode::Down => {
|
|
2235
|
-
self.state.run_manager.
|
|
2239
|
+
if self.state.run_manager.is_detail_view() {
|
|
2240
|
+
self.state.run_manager.detail_log_scroll_down();
|
|
2241
|
+
} else {
|
|
2242
|
+
self.state.run_manager.select_next();
|
|
2243
|
+
}
|
|
2236
2244
|
self.request_render("run_manager_down");
|
|
2237
2245
|
}
|
|
2238
2246
|
|
|
2239
|
-
// Filter
|
|
2247
|
+
// Filter (list view only)
|
|
2240
2248
|
KeyCode::Char('f') | KeyCode::Char('F') => {
|
|
2241
|
-
self.state.run_manager.
|
|
2242
|
-
|
|
2249
|
+
if !self.state.run_manager.is_detail_view() {
|
|
2250
|
+
self.state.run_manager.next_filter();
|
|
2251
|
+
self.request_render("run_manager_filter");
|
|
2252
|
+
}
|
|
2243
2253
|
}
|
|
2244
2254
|
|
|
2245
|
-
// Sort
|
|
2255
|
+
// Sort (list view only)
|
|
2246
2256
|
KeyCode::Char('s') | KeyCode::Char('S') => {
|
|
2247
|
-
self.state.run_manager.
|
|
2248
|
-
|
|
2257
|
+
if !self.state.run_manager.is_detail_view() {
|
|
2258
|
+
self.state.run_manager.next_sort();
|
|
2259
|
+
self.request_render("run_manager_sort");
|
|
2260
|
+
}
|
|
2249
2261
|
}
|
|
2250
2262
|
|
|
2251
|
-
// Refresh (
|
|
2263
|
+
// Refresh (list or pinned detail)
|
|
2252
2264
|
KeyCode::Char('r') | KeyCode::Char('R') => {
|
|
2253
2265
|
if self.state.operation_mode == OperationMode::Connected {
|
|
2254
2266
|
if let Some(ws) = ws_client {
|
|
2255
|
-
self.
|
|
2256
|
-
|
|
2267
|
+
if self.state.run_manager.is_detail_view() {
|
|
2268
|
+
if let Some(run) = self.state.run_manager.detail_run() {
|
|
2269
|
+
let run_id = run.id.clone();
|
|
2270
|
+
self.begin_run_get_request(ws, &run_id);
|
|
2271
|
+
self.add_log("[RUN] Refreshing run detail...".to_string());
|
|
2272
|
+
}
|
|
2273
|
+
} else {
|
|
2274
|
+
self.begin_run_list_request(ws, false);
|
|
2275
|
+
self.add_log("[RUN] Refreshing run list from Gateway...".to_string());
|
|
2276
|
+
}
|
|
2257
2277
|
} else {
|
|
2258
2278
|
self.add_log("[RUN] WebSocket not connected".to_string());
|
|
2259
2279
|
}
|
|
@@ -2279,34 +2299,31 @@ impl App {
|
|
|
2279
2299
|
// View details / Close detail view
|
|
2280
2300
|
KeyCode::Enter => {
|
|
2281
2301
|
if self.state.run_manager.is_detail_view() {
|
|
2282
|
-
// Close detail view
|
|
2283
2302
|
self.state.run_manager.close_detail_view();
|
|
2284
2303
|
self.add_log("[RUN] Closed detail view".to_string());
|
|
2285
|
-
} else {
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
if self.state.operation_mode == OperationMode::Connected {
|
|
2293
|
-
if let Some(ws) = ws_client {
|
|
2294
|
-
let data = serde_json::json!({ "runId": run_id });
|
|
2295
|
-
if let Ok(id) = ws.send_command("run.get", Some(data)) {
|
|
2296
|
-
self.state.pending_run_get_id = Some(id);
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2304
|
+
} else if let Some(run_id) = self.state.run_manager.open_detail_view() {
|
|
2305
|
+
if let Some(run) = self.state.run_manager.detail_run() {
|
|
2306
|
+
self.add_log(format!("[RUN] Viewing run: {}", run.name));
|
|
2307
|
+
}
|
|
2308
|
+
if self.state.operation_mode == OperationMode::Connected {
|
|
2309
|
+
if let Some(ws) = ws_client {
|
|
2310
|
+
self.begin_run_get_request(ws, &run_id);
|
|
2299
2311
|
}
|
|
2300
|
-
} else {
|
|
2301
|
-
self.add_log("[RUN] No run selected".to_string());
|
|
2302
2312
|
}
|
|
2313
|
+
} else {
|
|
2314
|
+
self.add_log("[RUN] No run selected".to_string());
|
|
2303
2315
|
}
|
|
2304
2316
|
self.request_render("run_manager_view");
|
|
2305
2317
|
}
|
|
2306
2318
|
|
|
2307
|
-
// Cancel run
|
|
2319
|
+
// Cancel run (list selection or pinned detail run)
|
|
2308
2320
|
KeyCode::Char('c') | KeyCode::Char('C') => {
|
|
2309
|
-
|
|
2321
|
+
let run = if self.state.run_manager.is_detail_view() {
|
|
2322
|
+
self.state.run_manager.detail_run()
|
|
2323
|
+
} else {
|
|
2324
|
+
self.state.run_manager.selected_run()
|
|
2325
|
+
};
|
|
2326
|
+
if let Some(run) = run {
|
|
2310
2327
|
if matches!(
|
|
2311
2328
|
run.status,
|
|
2312
2329
|
crate::ui::run_manager::RunStatus::Running
|
package/mk3-tui/src/main.rs
CHANGED
|
@@ -205,11 +205,7 @@ fn main() -> Result<()> {
|
|
|
205
205
|
if !due {
|
|
206
206
|
None
|
|
207
207
|
} else {
|
|
208
|
-
let detail_run_id =
|
|
209
|
-
rm.selected_run().map(|r| r.id.clone())
|
|
210
|
-
} else {
|
|
211
|
-
None
|
|
212
|
-
};
|
|
208
|
+
let detail_run_id = rm.detail_run_id.clone();
|
|
213
209
|
Some(detail_run_id)
|
|
214
210
|
}
|
|
215
211
|
}
|
|
@@ -38,8 +38,11 @@ pub struct RunManagerState {
|
|
|
38
38
|
#[allow(dead_code)]
|
|
39
39
|
pub last_refresh: Option<std::time::Instant>,
|
|
40
40
|
|
|
41
|
-
///
|
|
42
|
-
pub
|
|
41
|
+
/// Full-screen detail view: locked to this run until ESC/Enter.
|
|
42
|
+
pub detail_run_id: Option<String>,
|
|
43
|
+
|
|
44
|
+
/// Log panel scroll offset in detail view.
|
|
45
|
+
pub detail_log_scroll: u16,
|
|
43
46
|
|
|
44
47
|
/// Poll Gateway for run list while Run Manager is open (no manual R).
|
|
45
48
|
pub auto_refresh_enabled: bool,
|
|
@@ -156,7 +159,8 @@ impl Default for RunManagerState {
|
|
|
156
159
|
sort: RunSort::DateDesc,
|
|
157
160
|
loading: false,
|
|
158
161
|
last_refresh: None,
|
|
159
|
-
|
|
162
|
+
detail_run_id: None,
|
|
163
|
+
detail_log_scroll: 0,
|
|
160
164
|
auto_refresh_enabled: true,
|
|
161
165
|
auto_refresh_interval: std::time::Duration::from_secs(3),
|
|
162
166
|
}
|
|
@@ -283,7 +287,8 @@ impl RunManagerState {
|
|
|
283
287
|
pub fn replace_runs(&mut self, runs: Vec<RunInfo>) {
|
|
284
288
|
self.runs = runs;
|
|
285
289
|
self.selected_index = 0;
|
|
286
|
-
self.
|
|
290
|
+
self.detail_run_id = None;
|
|
291
|
+
self.detail_log_scroll = 0;
|
|
287
292
|
self.loading = false;
|
|
288
293
|
self.last_refresh = Some(std::time::Instant::now());
|
|
289
294
|
}
|
|
@@ -291,7 +296,7 @@ impl RunManagerState {
|
|
|
291
296
|
/// Auto-refresh: keep selection and detail view when the selected run still exists.
|
|
292
297
|
pub fn replace_runs_preserve(&mut self, runs: Vec<RunInfo>) {
|
|
293
298
|
let selected_id = self.selected_run().map(|r| r.id.clone());
|
|
294
|
-
let
|
|
299
|
+
let pinned_detail_id = self.detail_run_id.clone();
|
|
295
300
|
self.runs = runs;
|
|
296
301
|
self.loading = false;
|
|
297
302
|
self.last_refresh = Some(std::time::Instant::now());
|
|
@@ -299,7 +304,8 @@ impl RunManagerState {
|
|
|
299
304
|
let filtered = self.filtered_runs();
|
|
300
305
|
if filtered.is_empty() {
|
|
301
306
|
self.selected_index = 0;
|
|
302
|
-
self.
|
|
307
|
+
self.detail_run_id = None;
|
|
308
|
+
self.detail_log_scroll = 0;
|
|
303
309
|
return;
|
|
304
310
|
}
|
|
305
311
|
|
|
@@ -313,15 +319,21 @@ impl RunManagerState {
|
|
|
313
319
|
self.selected_index = filtered.len() - 1;
|
|
314
320
|
}
|
|
315
321
|
|
|
316
|
-
if
|
|
317
|
-
self.
|
|
322
|
+
if let Some(detail_id) = pinned_detail_id {
|
|
323
|
+
if self.runs.iter().any(|r| r.id == detail_id) {
|
|
324
|
+
self.detail_run_id = Some(detail_id);
|
|
325
|
+
} else {
|
|
326
|
+
self.detail_run_id = None;
|
|
327
|
+
self.detail_log_scroll = 0;
|
|
328
|
+
}
|
|
318
329
|
}
|
|
319
330
|
}
|
|
320
331
|
|
|
321
332
|
pub fn clear_runs(&mut self) {
|
|
322
333
|
self.runs.clear();
|
|
323
334
|
self.selected_index = 0;
|
|
324
|
-
self.
|
|
335
|
+
self.detail_run_id = None;
|
|
336
|
+
self.detail_log_scroll = 0;
|
|
325
337
|
self.loading = false;
|
|
326
338
|
self.last_refresh = None;
|
|
327
339
|
}
|
|
@@ -368,28 +380,39 @@ impl RunManagerState {
|
|
|
368
380
|
filtered.get(self.selected_index).copied()
|
|
369
381
|
}
|
|
370
382
|
|
|
371
|
-
///
|
|
372
|
-
pub fn
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
383
|
+
/// Run pinned in full-screen detail view (independent of list selection).
|
|
384
|
+
pub fn detail_run(&self) -> Option<&RunInfo> {
|
|
385
|
+
let id = self.detail_run_id.as_ref()?;
|
|
386
|
+
self.runs.iter().find(|r| &r.id == id)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/// Open full-screen detail for the currently selected run.
|
|
390
|
+
pub fn open_detail_view(&mut self) -> Option<String> {
|
|
391
|
+
let run = self.selected_run()?;
|
|
392
|
+
self.detail_run_id = Some(run.id.clone());
|
|
393
|
+
self.detail_log_scroll = 0;
|
|
394
|
+
Some(run.id.clone())
|
|
380
395
|
}
|
|
381
396
|
|
|
382
|
-
/// Close detail view
|
|
397
|
+
/// Close detail view and return to list (selection unchanged).
|
|
383
398
|
pub fn close_detail_view(&mut self) {
|
|
384
|
-
self.
|
|
399
|
+
self.detail_run_id = None;
|
|
400
|
+
self.detail_log_scroll = 0;
|
|
385
401
|
}
|
|
386
402
|
|
|
387
|
-
/// Check if in detail view
|
|
388
403
|
pub fn is_detail_view(&self) -> bool {
|
|
389
|
-
self.
|
|
404
|
+
self.detail_run_id.is_some()
|
|
390
405
|
}
|
|
391
406
|
|
|
392
|
-
|
|
407
|
+
pub fn detail_log_scroll_up(&mut self) {
|
|
408
|
+
self.detail_log_scroll = self.detail_log_scroll.saturating_sub(1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
pub fn detail_log_scroll_down(&mut self) {
|
|
412
|
+
self.detail_log_scroll = self.detail_log_scroll.saturating_add(1);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/// Move selection up (list view only).
|
|
393
416
|
pub fn select_previous(&mut self) {
|
|
394
417
|
if self.selected_index > 0 {
|
|
395
418
|
self.selected_index -= 1;
|
|
@@ -628,8 +651,8 @@ fn render_detail_view(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
628
651
|
])
|
|
629
652
|
.split(area);
|
|
630
653
|
|
|
631
|
-
// Get the
|
|
632
|
-
let run = match state.
|
|
654
|
+
// Get the pinned run (not list selection — stays fixed until exit)
|
|
655
|
+
let run = match state.detail_run() {
|
|
633
656
|
Some(r) => r,
|
|
634
657
|
None => {
|
|
635
658
|
let block = Block::default()
|
|
@@ -776,7 +799,9 @@ fn render_detail_view(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
776
799
|
.collect()
|
|
777
800
|
};
|
|
778
801
|
|
|
779
|
-
let logs_paragraph = Paragraph::new(log_lines)
|
|
802
|
+
let logs_paragraph = Paragraph::new(log_lines)
|
|
803
|
+
.wrap(Wrap { trim: false })
|
|
804
|
+
.scroll((state.detail_log_scroll, 0));
|
|
780
805
|
f.render_widget(logs_paragraph, logs_inner);
|
|
781
806
|
|
|
782
807
|
// Footer
|
|
@@ -792,8 +817,12 @@ fn render_detail_view(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
792
817
|
let footer_text = Line::from(vec![
|
|
793
818
|
Span::styled("ESC/Enter", Style::default().fg(BRAND_PURPLE).bold()),
|
|
794
819
|
Span::styled(" Back to List │ ", Style::default().fg(TEXT_DIM)),
|
|
820
|
+
Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
|
|
821
|
+
Span::styled(" Scroll logs │ ", Style::default().fg(TEXT_DIM)),
|
|
795
822
|
Span::styled("R", Style::default().fg(NEON_GREEN).bold()),
|
|
796
|
-
Span::styled(" Refresh", Style::default().fg(TEXT_DIM)),
|
|
823
|
+
Span::styled(" Refresh │ ", Style::default().fg(TEXT_DIM)),
|
|
824
|
+
Span::styled("C", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
|
|
825
|
+
Span::styled(" Cancel run", Style::default().fg(TEXT_DIM)),
|
|
797
826
|
]);
|
|
798
827
|
|
|
799
828
|
let footer_paragraph = Paragraph::new(footer_text).alignment(Alignment::Center);
|
|
@@ -813,6 +842,8 @@ fn render_actions(f: &mut Frame, area: Rect) {
|
|
|
813
842
|
let text = Line::from(vec![
|
|
814
843
|
Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
|
|
815
844
|
Span::styled(" Select │ ", Style::default().fg(TEXT_DIM)),
|
|
845
|
+
Span::styled("Enter", Style::default().fg(BRAND_PURPLE).bold()),
|
|
846
|
+
Span::styled(" Details │ ", Style::default().fg(TEXT_DIM)),
|
|
816
847
|
Span::styled("F", Style::default().fg(AMBER_WARN).bold()),
|
|
817
848
|
Span::styled(" Filter │ ", Style::default().fg(TEXT_DIM)),
|
|
818
849
|
Span::styled("S", Style::default().fg(AMBER_WARN).bold()),
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "4runr-os",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.58",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.
|
|
5
|
+
"description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.58: Run Manager detail view locks to selected run; ↑/↓ scroll logs in detail; list nav disabled until ESC.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"4runr": "dist/index.js",
|