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.
@@ -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.select_previous();
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.select_next();
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.next_filter();
2242
- self.request_render("run_manager_filter");
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.next_sort();
2248
- self.request_render("run_manager_sort");
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 (manual)
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.begin_run_list_request(ws, false);
2256
- self.add_log("[RUN] Refreshing run list from Gateway...".to_string());
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
- // Open detail view
2287
- if let Some(run) = self.state.run_manager.selected_run() {
2288
- let run_id = run.id.clone();
2289
- let run_name = run.name.clone();
2290
- self.add_log(format!("[RUN] Viewing run: {}", run_name));
2291
- self.state.run_manager.toggle_detail_view();
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
- if let Some(run) = self.state.run_manager.selected_run() {
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
@@ -205,11 +205,7 @@ fn main() -> Result<()> {
205
205
  if !due {
206
206
  None
207
207
  } else {
208
- let detail_run_id = if rm.is_detail_view() {
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
- /// Detailed view state (None = list view, Some(index) = detail view)
42
- pub detail_view: Option<usize>,
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
- detail_view: None,
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.detail_view = None;
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 in_detail = self.detail_view.is_some();
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.detail_view = None;
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 in_detail && self.selected_run().is_none() {
317
- self.detail_view = None;
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.detail_view = None;
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
- /// Toggle detail view for selected run
372
- pub fn toggle_detail_view(&mut self) {
373
- if self.detail_view.is_some() {
374
- // Close detail view
375
- self.detail_view = None;
376
- } else {
377
- // Open detail view for selected run
378
- self.detail_view = Some(self.selected_index);
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.detail_view = None;
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.detail_view.is_some()
404
+ self.detail_run_id.is_some()
390
405
  }
391
406
 
392
- /// Move selection up
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 selected run
632
- let run = match state.selected_run() {
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).wrap(Wrap { trim: false });
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.57",
3
+ "version": "2.10.58",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.57: Fix mk3-tui compile (Run Manager live refresh borrow); DDoS loopback exempt; run retention + 4Runr Tools.",
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",