4runr-os 2.10.42 → 2.10.44

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.
@@ -4782,9 +4782,9 @@
4782
4782
  }
4783
4783
  },
4784
4784
  "node_modules/electron-to-chromium": {
4785
- "version": "1.5.353",
4786
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz",
4787
- "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==",
4785
+ "version": "1.5.354",
4786
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.354.tgz",
4787
+ "integrity": "sha512-JaBHwWcfIdmSAfWM5l3uwjGd431j8YEMikZ+K/2nXVuBqJKyZ0f+2h4n4JY5AyNiZmnY9qQr2RU3v9DxDmHMNg==",
4788
4788
  "dev": true,
4789
4789
  "license": "ISC"
4790
4790
  },
@@ -311,7 +311,7 @@ impl Default for PortalMonitoringState {
311
311
  viewport_lines: 18,
312
312
  last_refresh: None,
313
313
  auto_refresh_interval: Duration::from_secs(5),
314
- auto_refresh_enabled: true,
314
+ auto_refresh_enabled: false,
315
315
  last_http_for_delta: None,
316
316
  last_instant_for_delta: None,
317
317
  section_overrides: HashMap::new(),
@@ -922,6 +922,31 @@ impl App {
922
922
  Ok(display)
923
923
  }
924
924
 
925
+ pub fn scroll_portal_monitoring_by(&mut self, delta: isize) {
926
+ use crate::ui::portal_monitoring::portal_monitoring_scroll_max;
927
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
928
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
929
+ let max_scroll = portal_monitoring_scroll_max(&self.state, sw, inner_h);
930
+ let current = self.state.portal_monitoring.scroll_offset;
931
+ self.state.portal_monitoring.scroll_offset = if delta < 0 {
932
+ current.saturating_sub(delta.unsigned_abs())
933
+ } else {
934
+ current.saturating_add(delta as usize).min(max_scroll)
935
+ };
936
+ self.request_immediate_render("portal_obs_scroll");
937
+ }
938
+
939
+ fn cancel_portal_monitoring_requests(&mut self) {
940
+ self.state.pending_gateway_observability_id = None;
941
+ self.state.pending_monitoring_refresh_id = None;
942
+ self.state.pending_monitoring_logs_id = None;
943
+ self.state.pending_monitoring_drill_id = None;
944
+ self.state.portal_monitoring.loading = false;
945
+ self.state.portal_monitoring.section_refresh_loading = None;
946
+ self.state.portal_monitoring.logs_fetch_loading = false;
947
+ self.state.portal_monitoring.metrics_drill_loading = false;
948
+ }
949
+
925
950
  /// CLI ↔ TUI WebSocket lost: drop Gateway link UI and leave Portal Monitoring / Connection Portal so the session does not appear still "connected".
926
951
  pub fn on_cli_backend_disconnect(&mut self) {
927
952
  use crate::screens::Screen;
@@ -2693,7 +2718,9 @@ impl App {
2693
2718
  key: KeyEvent,
2694
2719
  ws_client: Option<&WebSocketClient>,
2695
2720
  ) -> anyhow::Result<bool> {
2696
- use crate::ui::portal_monitoring::portal_monitoring_scroll_max;
2721
+ use crate::ui::portal_monitoring::{
2722
+ portal_monitoring_scroll_max, portal_monitoring_section_offset,
2723
+ };
2697
2724
  use crossterm::event::KeyModifiers;
2698
2725
  if key.modifiers.contains(KeyModifiers::CONTROL) {
2699
2726
  match key.code {
@@ -2734,37 +2761,13 @@ impl App {
2734
2761
  self.request_immediate_render("portal_help_open");
2735
2762
  return Ok(false);
2736
2763
  }
2737
- // R is context-sensitive: Overview full `gateway.observability` snapshot; any other
2738
- // section → `monitoring.refresh` for that section only (cheaper; matches expanded detail).
2764
+ // R is deliberately global: refresh the full snapshot without changing the current view.
2739
2765
  if key.code == KeyCode::Char('r') || key.code == KeyCode::Char('R') {
2740
2766
  if let Some(ws) = ws_client {
2741
- let sections = MonitoringSection::all();
2742
- let idx = self
2743
- .state
2744
- .advanced_monitoring
2745
- .monitoring_state
2746
- .selected_index;
2747
- let current = sections.get(idx).copied();
2748
2767
  self.state.portal_observability_last_poll = None;
2749
- self.state.pending_gateway_observability_id = None;
2750
- self.state.pending_monitoring_refresh_id = None;
2751
- self.state.pending_monitoring_logs_id = None;
2752
- self.state.pending_monitoring_drill_id = None;
2753
- self.state.portal_monitoring.loading = false;
2754
- self.state.portal_monitoring.section_refresh_loading = None;
2755
- self.state.portal_monitoring.logs_fetch_loading = false;
2756
- self.state.portal_monitoring.metrics_drill = None;
2757
- self.state.portal_monitoring.metrics_drill_lines.clear();
2758
- self.state.portal_monitoring.metrics_drill_loading = false;
2768
+ self.cancel_portal_monitoring_requests();
2759
2769
  self.state.portal_monitoring.last_refresh = Some(std::time::Instant::now());
2760
- match current {
2761
- Some(MonitoringSection::Overview) | None => {
2762
- self.begin_portal_observability_request(ws);
2763
- }
2764
- Some(sec) => {
2765
- self.begin_monitoring_refresh_request(ws, sec);
2766
- }
2767
- }
2770
+ self.begin_portal_observability_request(ws);
2768
2771
  self.request_immediate_render("portal_obs_refresh");
2769
2772
  } else {
2770
2773
  self.state.portal_monitoring.error =
@@ -2774,12 +2777,25 @@ impl App {
2774
2777
  return Ok(false);
2775
2778
  }
2776
2779
  if key.code == KeyCode::Char('l') || key.code == KeyCode::Char('L') {
2780
+ self.state
2781
+ .advanced_monitoring
2782
+ .monitoring_state
2783
+ .select_section(MonitoringSection::Logs);
2784
+ self.state
2785
+ .advanced_monitoring
2786
+ .monitoring_state
2787
+ .expand_section(MonitoringSection::Logs);
2777
2788
  if let Some(ws) = ws_client {
2789
+ self.cancel_portal_monitoring_requests();
2778
2790
  self.begin_monitoring_logs_request(ws);
2779
- self.state
2780
- .advanced_monitoring
2781
- .monitoring_state
2782
- .expand_section(MonitoringSection::Logs);
2791
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
2792
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
2793
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
2794
+ &self.state,
2795
+ sw,
2796
+ inner_h,
2797
+ MonitoringSection::Logs,
2798
+ );
2783
2799
  self.request_immediate_render("portal_monitoring_logs");
2784
2800
  } else {
2785
2801
  self.state.portal_monitoring.error =
@@ -2790,7 +2806,23 @@ impl App {
2790
2806
  }
2791
2807
 
2792
2808
  if key.code == KeyCode::Char('h') || key.code == KeyCode::Char('H') {
2809
+ self.state
2810
+ .advanced_monitoring
2811
+ .monitoring_state
2812
+ .select_section(MonitoringSection::Metrics);
2813
+ self.state
2814
+ .advanced_monitoring
2815
+ .monitoring_state
2816
+ .expand_section(MonitoringSection::Metrics);
2793
2817
  self.state.portal_monitoring.trend_view = !self.state.portal_monitoring.trend_view;
2818
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
2819
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
2820
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
2821
+ &self.state,
2822
+ sw,
2823
+ inner_h,
2824
+ MonitoringSection::Metrics,
2825
+ );
2794
2826
  let status = if self.state.portal_monitoring.trend_view {
2795
2827
  "enabled"
2796
2828
  } else {
@@ -2817,103 +2849,97 @@ impl App {
2817
2849
 
2818
2850
  // Phase 3: extended dependency view (GET /api/monitoring/dependencies/detail)
2819
2851
  if key.code == KeyCode::Char('t') || key.code == KeyCode::Char('T') {
2820
- let sections = MonitoringSection::all();
2821
- let idx = self
2822
- .state
2852
+ self.state
2823
2853
  .advanced_monitoring
2824
2854
  .monitoring_state
2825
- .selected_index;
2826
- let current = sections.get(idx).copied();
2827
- let expanded = current
2828
- .and_then(|sec| {
2829
- self.state
2830
- .advanced_monitoring
2831
- .monitoring_state
2832
- .sections
2833
- .get(&sec)
2834
- .map(|s| s.expanded)
2835
- })
2836
- .unwrap_or(false);
2837
- if current == Some(MonitoringSection::Dependencies) && expanded {
2838
- if let Some(ws) = ws_client {
2839
- self.begin_dependencies_detail_drill(ws);
2840
- self.request_immediate_render("portal_deps_drill");
2841
- } else {
2842
- self.state.portal_monitoring.error =
2843
- Some("WebSocket to CLI is not connected.".to_string());
2844
- self.request_immediate_render("portal_obs_no_ws");
2845
- }
2846
- return Ok(false);
2855
+ .select_section(MonitoringSection::Dependencies);
2856
+ self.state
2857
+ .advanced_monitoring
2858
+ .monitoring_state
2859
+ .expand_section(MonitoringSection::Dependencies);
2860
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
2861
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
2862
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
2863
+ &self.state,
2864
+ sw,
2865
+ inner_h,
2866
+ MonitoringSection::Dependencies,
2867
+ );
2868
+ if let Some(ws) = ws_client {
2869
+ self.cancel_portal_monitoring_requests();
2870
+ self.begin_dependencies_detail_drill(ws);
2871
+ self.request_immediate_render("portal_deps_drill");
2872
+ } else {
2873
+ self.state.portal_monitoring.error =
2874
+ Some("WebSocket to CLI is not connected.".to_string());
2875
+ self.request_immediate_render("portal_obs_no_ws");
2847
2876
  }
2877
+ return Ok(false);
2848
2878
  }
2849
2879
 
2850
2880
  // Phase 4: diagnostics for the local CLI/TUI bridge + Gateway reachability.
2851
2881
  if key.code == KeyCode::Char('s') || key.code == KeyCode::Char('S') {
2852
- let sections = MonitoringSection::all();
2853
- let idx = self
2854
- .state
2882
+ self.state
2855
2883
  .advanced_monitoring
2856
2884
  .monitoring_state
2857
- .selected_index;
2858
- let current = sections.get(idx).copied();
2859
- let expanded = current
2860
- .and_then(|sec| {
2861
- self.state
2862
- .advanced_monitoring
2863
- .monitoring_state
2864
- .sections
2865
- .get(&sec)
2866
- .map(|s| s.expanded)
2867
- })
2868
- .unwrap_or(false);
2869
- if current == Some(MonitoringSection::System) && expanded {
2870
- if let Some(ws) = ws_client {
2871
- self.begin_system_diagnostics_request(ws);
2872
- self.request_immediate_render("portal_system_diagnostics");
2873
- } else {
2874
- self.state.portal_monitoring.error =
2875
- Some("WebSocket to CLI is not connected.".to_string());
2876
- self.request_immediate_render("portal_obs_no_ws");
2877
- }
2878
- return Ok(false);
2885
+ .select_section(MonitoringSection::System);
2886
+ self.state
2887
+ .advanced_monitoring
2888
+ .monitoring_state
2889
+ .expand_section(MonitoringSection::System);
2890
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
2891
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
2892
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
2893
+ &self.state,
2894
+ sw,
2895
+ inner_h,
2896
+ MonitoringSection::System,
2897
+ );
2898
+ if let Some(ws) = ws_client {
2899
+ self.cancel_portal_monitoring_requests();
2900
+ self.begin_system_diagnostics_request(ws);
2901
+ self.request_immediate_render("portal_system_diagnostics");
2902
+ } else {
2903
+ self.state.portal_monitoring.error =
2904
+ Some("WebSocket to CLI is not connected.".to_string());
2905
+ self.request_immediate_render("portal_obs_no_ws");
2879
2906
  }
2907
+ return Ok(false);
2880
2908
  }
2881
2909
 
2882
2910
  if key.code == KeyCode::Right {
2883
- let sections = MonitoringSection::all();
2884
- let idx = self
2885
- .state
2911
+ self.state
2886
2912
  .advanced_monitoring
2887
2913
  .monitoring_state
2888
- .selected_index;
2889
- let current = sections.get(idx).copied();
2890
- let expanded = current
2891
- .and_then(|sec| {
2892
- self.state
2893
- .advanced_monitoring
2894
- .monitoring_state
2895
- .sections
2896
- .get(&sec)
2897
- .map(|s| s.expanded)
2898
- })
2899
- .unwrap_or(false);
2900
- if current == Some(MonitoringSection::Metrics) && expanded {
2901
- if let Some(ws) = ws_client {
2902
- let panel = self
2903
- .state
2904
- .portal_monitoring
2905
- .metrics_drill
2906
- .map(|p| p.next())
2907
- .unwrap_or(MetricsDrillPanel::Http);
2908
- self.begin_monitoring_drill_request(ws, panel);
2909
- self.request_immediate_render("portal_metrics_drill");
2910
- } else {
2911
- self.state.portal_monitoring.error =
2912
- Some("WebSocket to CLI is not connected.".to_string());
2913
- self.request_immediate_render("portal_obs_no_ws");
2914
- }
2915
- return Ok(false);
2914
+ .select_section(MonitoringSection::Metrics);
2915
+ self.state
2916
+ .advanced_monitoring
2917
+ .monitoring_state
2918
+ .expand_section(MonitoringSection::Metrics);
2919
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
2920
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
2921
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
2922
+ &self.state,
2923
+ sw,
2924
+ inner_h,
2925
+ MonitoringSection::Metrics,
2926
+ );
2927
+ if let Some(ws) = ws_client {
2928
+ self.cancel_portal_monitoring_requests();
2929
+ let panel = self
2930
+ .state
2931
+ .portal_monitoring
2932
+ .metrics_drill
2933
+ .map(|p| p.next())
2934
+ .unwrap_or(MetricsDrillPanel::Http);
2935
+ self.begin_monitoring_drill_request(ws, panel);
2936
+ self.request_immediate_render("portal_metrics_drill");
2937
+ } else {
2938
+ self.state.portal_monitoring.error =
2939
+ Some("WebSocket to CLI is not connected.".to_string());
2940
+ self.request_immediate_render("portal_obs_no_ws");
2916
2941
  }
2942
+ return Ok(false);
2917
2943
  }
2918
2944
 
2919
2945
  if key.code == KeyCode::Left {
@@ -2963,20 +2989,46 @@ impl App {
2963
2989
 
2964
2990
  match key.code {
2965
2991
  KeyCode::Up => {
2966
- self.state
2992
+ if self
2993
+ .state
2967
2994
  .advanced_monitoring
2968
2995
  .monitoring_state
2969
- .select_previous_wrapped();
2970
- self.state.portal_monitoring.scroll_offset = 0;
2971
- self.request_immediate_render("portal_monitoring_select");
2996
+ .selected_section
2997
+ == Some(MonitoringSection::Logs)
2998
+ && self.state.portal_monitoring.scroll_offset > 0
2999
+ {
3000
+ self.state.portal_monitoring.scroll_offset =
3001
+ self.state.portal_monitoring.scroll_offset.saturating_sub(1);
3002
+ self.request_immediate_render("portal_obs_scroll");
3003
+ } else {
3004
+ self.state
3005
+ .advanced_monitoring
3006
+ .monitoring_state
3007
+ .select_previous_wrapped();
3008
+ self.state.portal_monitoring.scroll_offset = 0;
3009
+ self.request_immediate_render("portal_monitoring_select");
3010
+ }
2972
3011
  }
2973
3012
  KeyCode::Down => {
2974
- self.state
3013
+ if self
3014
+ .state
2975
3015
  .advanced_monitoring
2976
3016
  .monitoring_state
2977
- .select_next_wrapped();
2978
- self.state.portal_monitoring.scroll_offset = 0;
2979
- self.request_immediate_render("portal_monitoring_select");
3017
+ .selected_section
3018
+ == Some(MonitoringSection::Logs)
3019
+ && self.state.portal_monitoring.scroll_offset < max_scroll
3020
+ {
3021
+ self.state.portal_monitoring.scroll_offset =
3022
+ (self.state.portal_monitoring.scroll_offset + 1).min(max_scroll);
3023
+ self.request_immediate_render("portal_obs_scroll");
3024
+ } else {
3025
+ self.state
3026
+ .advanced_monitoring
3027
+ .monitoring_state
3028
+ .select_next_wrapped();
3029
+ self.state.portal_monitoring.scroll_offset = 0;
3030
+ self.request_immediate_render("portal_monitoring_select");
3031
+ }
2980
3032
  }
2981
3033
  KeyCode::PageUp => {
2982
3034
  let step = inner_h.saturating_sub(3).max(1);
@@ -890,19 +890,46 @@ fn main() -> Result<()> {
890
890
  .to_string(),
891
891
  );
892
892
  }
893
+ let preserve_scroll =
894
+ app.state.portal_monitoring.scroll_offset;
895
+ let preserve_metrics_drill =
896
+ app.state.portal_monitoring.metrics_drill;
897
+ let preserve_metrics_drill_lines = app
898
+ .state
899
+ .portal_monitoring
900
+ .metrics_drill_lines
901
+ .clone();
902
+ let preserve_logs = app
903
+ .state
904
+ .portal_monitoring
905
+ .section_overrides
906
+ .get(&crate::monitoring::MonitoringSection::Logs)
907
+ .cloned();
893
908
  app.state.portal_monitoring.last_updated =
894
909
  Some(snapshot);
895
910
  app.state.portal_monitoring.content_lines = lines;
896
911
  app.state.portal_monitoring.section_overrides.clear();
912
+ if let Some(logs) = preserve_logs {
913
+ app.state
914
+ .portal_monitoring
915
+ .section_overrides
916
+ .insert(
917
+ crate::monitoring::MonitoringSection::Logs,
918
+ logs,
919
+ );
920
+ }
897
921
  app.state.portal_monitoring.section_refresh_loading =
898
922
  None;
899
923
  app.state.portal_monitoring.logs_fetch_loading = false;
900
- app.state.portal_monitoring.metrics_drill = None;
901
- app.state.portal_monitoring.metrics_drill_lines.clear();
924
+ app.state.portal_monitoring.metrics_drill =
925
+ preserve_metrics_drill;
926
+ app.state.portal_monitoring.metrics_drill_lines =
927
+ preserve_metrics_drill_lines;
902
928
  app.state.portal_monitoring.metrics_drill_loading =
903
929
  false;
904
930
  app.state.portal_monitoring.error = None;
905
- app.state.portal_monitoring.scroll_offset = 0;
931
+ app.state.portal_monitoring.scroll_offset =
932
+ preserve_scroll;
906
933
  } else {
907
934
  app.state.portal_monitoring.error = Some(
908
935
  "Unexpected observability response.".to_string(),
@@ -1548,6 +1575,8 @@ fn main() -> Result<()> {
1548
1575
  // Handle ESC on both Press and Release so we never miss it (e.g. when stuck on "Detecting...")
1549
1576
  if crossterm::event::poll(Duration::from_millis(1))? {
1550
1577
  let on_setup = app.state.navigation.current_screen() == &screens::Screen::SetupPortal;
1578
+ let on_portal_monitoring =
1579
+ app.state.navigation.current_screen() == &screens::Screen::PortalMonitoring;
1551
1580
  let on_standalone_input = matches!(
1552
1581
  app.state.navigation.current_screen(),
1553
1582
  &screens::Screen::ConnectionPortal
@@ -1571,8 +1600,14 @@ fn main() -> Result<()> {
1571
1600
  if on_standalone_input {
1572
1601
  debug_log::log_input("Mouse", &format!("{:?}", mouse));
1573
1602
  }
1574
- // Handle mouse scroll events for Setup Portal
1575
- if on_setup {
1603
+ // Handle mouse scroll events for standalone portals
1604
+ if on_portal_monitoring {
1605
+ match mouse.kind {
1606
+ MouseEventKind::ScrollUp => app.scroll_portal_monitoring_by(-3),
1607
+ MouseEventKind::ScrollDown => app.scroll_portal_monitoring_by(3),
1608
+ _ => {}
1609
+ }
1610
+ } else if on_setup {
1576
1611
  if !app.state.setup_portal.detecting {
1577
1612
  match mouse.kind {
1578
1613
  MouseEventKind::ScrollUp => {
@@ -294,6 +294,17 @@ impl MonitoringState {
294
294
  }
295
295
  }
296
296
 
297
+ /// Move keyboard focus directly to a specific section.
298
+ pub fn select_section(&mut self, section: MonitoringSection) {
299
+ if let Some(index) = MonitoringSection::all()
300
+ .iter()
301
+ .position(|candidate| *candidate == section)
302
+ {
303
+ self.selected_index = index;
304
+ self.selected_section = Some(section);
305
+ }
306
+ }
307
+
297
308
  /// Collapse a specific section
298
309
  pub fn collapse_section(&mut self, section: MonitoringSection) {
299
310
  if let Some(state) = self.sections.get_mut(&section) {
@@ -468,6 +468,27 @@ pub fn portal_monitoring_scroll_max(
468
468
  total.saturating_sub(content_h.min(total.max(1)))
469
469
  }
470
470
 
471
+ /// Best-effort scroll offset that brings a section header into view.
472
+ pub fn portal_monitoring_section_offset(
473
+ state: &AppState,
474
+ summary_width: usize,
475
+ body_inner_h: usize,
476
+ section: MonitoringSection,
477
+ ) -> usize {
478
+ let body_lines = build_body_lines(state, summary_width);
479
+ let target = section.display_name();
480
+ let raw_offset = body_lines
481
+ .iter()
482
+ .position(|line| {
483
+ line.spans
484
+ .iter()
485
+ .any(|span| span.content.as_ref().contains(target))
486
+ })
487
+ .unwrap_or(0);
488
+ let max_scroll = portal_monitoring_scroll_max(state, summary_width, body_inner_h);
489
+ raw_offset.min(max_scroll)
490
+ }
491
+
471
492
  /// Build scrollable section lines. **Phase 1:** collapsed summaries and ●/⚠/✖ colors are derived only from
472
493
  /// `parse_snapshot` of `content_lines`; `MonitoringState.sections` controls **expand** + **selection** only
473
494
  /// (not `SectionState.status` / `summary`).
@@ -857,7 +878,7 @@ pub fn render(f: &mut Frame, state: &mut AppState) {
857
878
  ]);
858
879
 
859
880
  let keys_hint = Line::from(vec![Span::styled(
860
- "R sect/full · H trends · E export · Metrics + →/← drill · Dep ▼ + T detail · Sys ▼ + S diagnostics · L logs · A auto",
881
+ "R full refresh · L logs · wheel/Pg scroll · H metrics trends · → metrics drill · T dependencies · S system · A live",
861
882
  Style::default().fg(TEXT_DIM),
862
883
  )]);
863
884
 
@@ -962,7 +983,7 @@ pub fn render(f: &mut Frame, state: &mut AppState) {
962
983
  Span::styled("expand ", Style::default().fg(TEXT_DIM)),
963
984
  Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
964
985
  Span::styled("R ", Style::default().fg(CYBER_CYAN).bold()),
965
- Span::styled("full/sect ", Style::default().fg(TEXT_DIM)),
986
+ Span::styled("refresh ", Style::default().fg(TEXT_DIM)),
966
987
  Span::styled("│ ", Style::default().fg(SEPARATOR_COLOR)),
967
988
  Span::styled("L ", Style::default().fg(CYBER_CYAN).bold()),
968
989
  Span::styled("logs ", Style::default().fg(TEXT_DIM)),
@@ -1011,16 +1032,19 @@ pub fn render(f: &mut Frame, state: &mut AppState) {
1011
1032
  )]),
1012
1033
  Line::from(""),
1013
1034
  Line::from("Navigation"),
1014
- Line::from(" ↑/↓ select section Enter/Space expand/collapse PgUp/PgDn scroll"),
1035
+ Line::from(
1036
+ " ↑/↓ select section Enter/Space expand/collapse PgUp/PgDn/mouse scroll",
1037
+ ),
1038
+ Line::from(" When Logs is selected, ↑/↓ scroll log content first."),
1015
1039
  Line::from(" ESC close help / return to Main"),
1016
1040
  Line::from(""),
1017
1041
  Line::from("Actions"),
1018
- Line::from(" R refresh current row (Overview = full snapshot)"),
1019
- Line::from(
1020
- " L fetch Gateway logs H toggle trends E export JSON to current directory",
1021
- ),
1022
- Line::from(" Metrics expanded: cycle HTTP/Runs/Queue/SSE drill, leave drill"),
1023
- Line::from(" Dependencies expanded: T detail System expanded: S diagnostics"),
1042
+ Line::from(" R full snapshot refresh without closing your current section"),
1043
+ Line::from(" L jump to Logs + fetch Gateway logs"),
1044
+ Line::from(" H jump to Metrics + toggle trends Metrics drill ← leave drill"),
1045
+ Line::from(" T jump to Dependencies detail S jump to System diagnostics"),
1046
+ Line::from(" A toggle live auto-refresh (off by default for stable reading)"),
1047
+ Line::from(" E export diagnostics JSON to the current working directory"),
1024
1048
  Line::from(""),
1025
1049
  Line::from("Recovery"),
1026
1050
  Line::from(
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.10.42",
3
+ "version": "2.10.44",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.42: Prevents stale global MK3 binaries from masking packaged TUI fixes. v2.10.41: Stabilizes Portal Monitoring refresh/log shortcuts and log scrolling.",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.44: Stabilizes Portal Monitoring reading mode, global refresh, logs, and shortcut behavior. v2.10.43: Makes Portal Monitoring footer shortcuts jump to their sections and fixes log scrolling.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",