4runr-os 2.10.42 → 2.10.43

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
  },
@@ -922,6 +922,20 @@ 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
+
925
939
  /// CLI ↔ TUI WebSocket lost: drop Gateway link UI and leave Portal Monitoring / Connection Portal so the session does not appear still "connected".
926
940
  pub fn on_cli_backend_disconnect(&mut self) {
927
941
  use crate::screens::Screen;
@@ -2693,7 +2707,9 @@ impl App {
2693
2707
  key: KeyEvent,
2694
2708
  ws_client: Option<&WebSocketClient>,
2695
2709
  ) -> anyhow::Result<bool> {
2696
- use crate::ui::portal_monitoring::portal_monitoring_scroll_max;
2710
+ use crate::ui::portal_monitoring::{
2711
+ portal_monitoring_scroll_max, portal_monitoring_section_offset,
2712
+ };
2697
2713
  use crossterm::event::KeyModifiers;
2698
2714
  if key.modifiers.contains(KeyModifiers::CONTROL) {
2699
2715
  match key.code {
@@ -2774,12 +2790,24 @@ impl App {
2774
2790
  return Ok(false);
2775
2791
  }
2776
2792
  if key.code == KeyCode::Char('l') || key.code == KeyCode::Char('L') {
2793
+ self.state
2794
+ .advanced_monitoring
2795
+ .monitoring_state
2796
+ .select_section(MonitoringSection::Logs);
2797
+ self.state
2798
+ .advanced_monitoring
2799
+ .monitoring_state
2800
+ .expand_section(MonitoringSection::Logs);
2777
2801
  if let Some(ws) = ws_client {
2778
2802
  self.begin_monitoring_logs_request(ws);
2779
- self.state
2780
- .advanced_monitoring
2781
- .monitoring_state
2782
- .expand_section(MonitoringSection::Logs);
2803
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
2804
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
2805
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
2806
+ &self.state,
2807
+ sw,
2808
+ inner_h,
2809
+ MonitoringSection::Logs,
2810
+ );
2783
2811
  self.request_immediate_render("portal_monitoring_logs");
2784
2812
  } else {
2785
2813
  self.state.portal_monitoring.error =
@@ -2790,7 +2818,23 @@ impl App {
2790
2818
  }
2791
2819
 
2792
2820
  if key.code == KeyCode::Char('h') || key.code == KeyCode::Char('H') {
2821
+ self.state
2822
+ .advanced_monitoring
2823
+ .monitoring_state
2824
+ .select_section(MonitoringSection::Metrics);
2825
+ self.state
2826
+ .advanced_monitoring
2827
+ .monitoring_state
2828
+ .expand_section(MonitoringSection::Metrics);
2793
2829
  self.state.portal_monitoring.trend_view = !self.state.portal_monitoring.trend_view;
2830
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
2831
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
2832
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
2833
+ &self.state,
2834
+ sw,
2835
+ inner_h,
2836
+ MonitoringSection::Metrics,
2837
+ );
2794
2838
  let status = if self.state.portal_monitoring.trend_view {
2795
2839
  "enabled"
2796
2840
  } else {
@@ -2817,103 +2861,94 @@ impl App {
2817
2861
 
2818
2862
  // Phase 3: extended dependency view (GET /api/monitoring/dependencies/detail)
2819
2863
  if key.code == KeyCode::Char('t') || key.code == KeyCode::Char('T') {
2820
- let sections = MonitoringSection::all();
2821
- let idx = self
2822
- .state
2864
+ self.state
2823
2865
  .advanced_monitoring
2824
2866
  .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);
2867
+ .select_section(MonitoringSection::Dependencies);
2868
+ self.state
2869
+ .advanced_monitoring
2870
+ .monitoring_state
2871
+ .expand_section(MonitoringSection::Dependencies);
2872
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
2873
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
2874
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
2875
+ &self.state,
2876
+ sw,
2877
+ inner_h,
2878
+ MonitoringSection::Dependencies,
2879
+ );
2880
+ if let Some(ws) = ws_client {
2881
+ self.begin_dependencies_detail_drill(ws);
2882
+ self.request_immediate_render("portal_deps_drill");
2883
+ } else {
2884
+ self.state.portal_monitoring.error =
2885
+ Some("WebSocket to CLI is not connected.".to_string());
2886
+ self.request_immediate_render("portal_obs_no_ws");
2847
2887
  }
2888
+ return Ok(false);
2848
2889
  }
2849
2890
 
2850
2891
  // Phase 4: diagnostics for the local CLI/TUI bridge + Gateway reachability.
2851
2892
  if key.code == KeyCode::Char('s') || key.code == KeyCode::Char('S') {
2852
- let sections = MonitoringSection::all();
2853
- let idx = self
2854
- .state
2893
+ self.state
2855
2894
  .advanced_monitoring
2856
2895
  .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);
2896
+ .select_section(MonitoringSection::System);
2897
+ self.state
2898
+ .advanced_monitoring
2899
+ .monitoring_state
2900
+ .expand_section(MonitoringSection::System);
2901
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
2902
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
2903
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
2904
+ &self.state,
2905
+ sw,
2906
+ inner_h,
2907
+ MonitoringSection::System,
2908
+ );
2909
+ if let Some(ws) = ws_client {
2910
+ self.begin_system_diagnostics_request(ws);
2911
+ self.request_immediate_render("portal_system_diagnostics");
2912
+ } else {
2913
+ self.state.portal_monitoring.error =
2914
+ Some("WebSocket to CLI is not connected.".to_string());
2915
+ self.request_immediate_render("portal_obs_no_ws");
2879
2916
  }
2917
+ return Ok(false);
2880
2918
  }
2881
2919
 
2882
2920
  if key.code == KeyCode::Right {
2883
- let sections = MonitoringSection::all();
2884
- let idx = self
2885
- .state
2921
+ self.state
2886
2922
  .advanced_monitoring
2887
2923
  .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);
2924
+ .select_section(MonitoringSection::Metrics);
2925
+ self.state
2926
+ .advanced_monitoring
2927
+ .monitoring_state
2928
+ .expand_section(MonitoringSection::Metrics);
2929
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
2930
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
2931
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
2932
+ &self.state,
2933
+ sw,
2934
+ inner_h,
2935
+ MonitoringSection::Metrics,
2936
+ );
2937
+ if let Some(ws) = ws_client {
2938
+ let panel = self
2939
+ .state
2940
+ .portal_monitoring
2941
+ .metrics_drill
2942
+ .map(|p| p.next())
2943
+ .unwrap_or(MetricsDrillPanel::Http);
2944
+ self.begin_monitoring_drill_request(ws, panel);
2945
+ self.request_immediate_render("portal_metrics_drill");
2946
+ } else {
2947
+ self.state.portal_monitoring.error =
2948
+ Some("WebSocket to CLI is not connected.".to_string());
2949
+ self.request_immediate_render("portal_obs_no_ws");
2916
2950
  }
2951
+ return Ok(false);
2917
2952
  }
2918
2953
 
2919
2954
  if key.code == KeyCode::Left {
@@ -2963,20 +2998,46 @@ impl App {
2963
2998
 
2964
2999
  match key.code {
2965
3000
  KeyCode::Up => {
2966
- self.state
3001
+ if self
3002
+ .state
2967
3003
  .advanced_monitoring
2968
3004
  .monitoring_state
2969
- .select_previous_wrapped();
2970
- self.state.portal_monitoring.scroll_offset = 0;
2971
- self.request_immediate_render("portal_monitoring_select");
3005
+ .selected_section
3006
+ == Some(MonitoringSection::Logs)
3007
+ && self.state.portal_monitoring.scroll_offset > 0
3008
+ {
3009
+ self.state.portal_monitoring.scroll_offset =
3010
+ self.state.portal_monitoring.scroll_offset.saturating_sub(1);
3011
+ self.request_immediate_render("portal_obs_scroll");
3012
+ } else {
3013
+ self.state
3014
+ .advanced_monitoring
3015
+ .monitoring_state
3016
+ .select_previous_wrapped();
3017
+ self.state.portal_monitoring.scroll_offset = 0;
3018
+ self.request_immediate_render("portal_monitoring_select");
3019
+ }
2972
3020
  }
2973
3021
  KeyCode::Down => {
2974
- self.state
3022
+ if self
3023
+ .state
2975
3024
  .advanced_monitoring
2976
3025
  .monitoring_state
2977
- .select_next_wrapped();
2978
- self.state.portal_monitoring.scroll_offset = 0;
2979
- self.request_immediate_render("portal_monitoring_select");
3026
+ .selected_section
3027
+ == Some(MonitoringSection::Logs)
3028
+ && self.state.portal_monitoring.scroll_offset < max_scroll
3029
+ {
3030
+ self.state.portal_monitoring.scroll_offset =
3031
+ (self.state.portal_monitoring.scroll_offset + 1).min(max_scroll);
3032
+ self.request_immediate_render("portal_obs_scroll");
3033
+ } else {
3034
+ self.state
3035
+ .advanced_monitoring
3036
+ .monitoring_state
3037
+ .select_next_wrapped();
3038
+ self.state.portal_monitoring.scroll_offset = 0;
3039
+ self.request_immediate_render("portal_monitoring_select");
3040
+ }
2980
3041
  }
2981
3042
  KeyCode::PageUp => {
2982
3043
  let step = inner_h.saturating_sub(3).max(1);
@@ -1548,6 +1548,8 @@ fn main() -> Result<()> {
1548
1548
  // Handle ESC on both Press and Release so we never miss it (e.g. when stuck on "Detecting...")
1549
1549
  if crossterm::event::poll(Duration::from_millis(1))? {
1550
1550
  let on_setup = app.state.navigation.current_screen() == &screens::Screen::SetupPortal;
1551
+ let on_portal_monitoring =
1552
+ app.state.navigation.current_screen() == &screens::Screen::PortalMonitoring;
1551
1553
  let on_standalone_input = matches!(
1552
1554
  app.state.navigation.current_screen(),
1553
1555
  &screens::Screen::ConnectionPortal
@@ -1571,8 +1573,14 @@ fn main() -> Result<()> {
1571
1573
  if on_standalone_input {
1572
1574
  debug_log::log_input("Mouse", &format!("{:?}", mouse));
1573
1575
  }
1574
- // Handle mouse scroll events for Setup Portal
1575
- if on_setup {
1576
+ // Handle mouse scroll events for standalone portals
1577
+ if on_portal_monitoring {
1578
+ match mouse.kind {
1579
+ MouseEventKind::ScrollUp => app.scroll_portal_monitoring_by(-3),
1580
+ MouseEventKind::ScrollDown => app.scroll_portal_monitoring_by(3),
1581
+ _ => {}
1582
+ }
1583
+ } else if on_setup {
1576
1584
  if !app.state.setup_portal.detecting {
1577
1585
  match mouse.kind {
1578
1586
  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`).
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.10.42",
3
+ "version": "2.10.43",
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.43: Makes Portal Monitoring footer shortcuts jump to their sections and fixes log scrolling. v2.10.42: Prevents stale global MK3 binaries from masking packaged TUI fixes.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",