4runr-os 2.9.134 → 2.9.136

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.
@@ -113,6 +113,8 @@ pub struct ConnectionPortalState {
113
113
  pub username: String,
114
114
  pub focused_field: PortalField,
115
115
  pub connecting: bool,
116
+ /// When `connecting` became true (for elapsed timer in the portal UI).
117
+ pub connecting_started: Option<Instant>,
116
118
  pub connection_success: bool,
117
119
  pub error: Option<String>,
118
120
  pub last_successful_url: Option<String>,
@@ -127,6 +129,7 @@ impl Default for ConnectionPortalState {
127
129
  username: String::new(),
128
130
  focused_field: PortalField::GatewayUrl,
129
131
  connecting: false,
132
+ connecting_started: None,
130
133
  connection_success: false,
131
134
  error: None,
132
135
  last_successful_url: None,
@@ -141,10 +144,21 @@ impl ConnectionPortalState {
141
144
  self.username = String::new();
142
145
  self.focused_field = PortalField::GatewayUrl;
143
146
  self.connecting = false;
147
+ self.connecting_started = None;
144
148
  self.connection_success = false;
145
149
  self.error = None;
146
150
  self.activity_log.clear();
147
151
  }
152
+
153
+ pub fn begin_connecting(&mut self) {
154
+ self.connecting = true;
155
+ self.connecting_started = Some(Instant::now());
156
+ }
157
+
158
+ pub fn finish_connecting(&mut self) {
159
+ self.connecting = false;
160
+ self.connecting_started = None;
161
+ }
148
162
 
149
163
  pub fn toggle_field(&mut self) {
150
164
  self.focused_field = match self.focused_field {
@@ -160,8 +174,9 @@ impl ConnectionPortalState {
160
174
  message,
161
175
  });
162
176
 
163
- // Keep only last 20 entries to prevent memory bloat
164
- if self.activity_log.len() > 20 {
177
+ // Keep last N lines so the portal can show a full connect trace on small terminals
178
+ const MAX_ACTIVITY: usize = 80;
179
+ if self.activity_log.len() > MAX_ACTIVITY {
165
180
  self.activity_log.remove(0);
166
181
  }
167
182
  }
@@ -1924,7 +1939,7 @@ impl App {
1924
1939
  KeyCode::Esc => {
1925
1940
  // Drop stale connect attempt + UI state so the next open is not "pre-broken".
1926
1941
  self.state.pending_gateway_connect_id = None;
1927
- self.state.connection_portal.connecting = false;
1942
+ self.state.connection_portal.finish_connecting();
1928
1943
  self.state.connection_portal.error = None;
1929
1944
  self.state.connection_portal.activity_log.clear();
1930
1945
  self.state.navigation.navigate_to_base(Screen::Main);
@@ -1953,7 +1968,7 @@ impl App {
1953
1968
  self.state.gateway_url = None;
1954
1969
  self.state.connected = false;
1955
1970
  self.state.gateway_healthy = false;
1956
- self.state.connection_portal.connecting = false;
1971
+ self.state.connection_portal.finish_connecting();
1957
1972
  self.state.connection_portal.connection_success = false;
1958
1973
  self.state.connection_portal.error = None;
1959
1974
  self.state.pending_gateway_connect_id = None;
@@ -2009,8 +2024,8 @@ impl App {
2009
2024
  }
2010
2025
 
2011
2026
  // Connect to Gateway
2012
- self.state.connection_portal.connecting = true;
2013
2027
  self.state.connection_portal.error = None;
2028
+ self.state.connection_portal.begin_connecting();
2014
2029
 
2015
2030
  if let Some(ws) = ws_client {
2016
2031
  let connect_data = serde_json::json!({
@@ -2025,13 +2040,13 @@ impl App {
2025
2040
  self.state.pending_gateway_connect_id = Some(id);
2026
2041
  }
2027
2042
  Err(e) => {
2028
- self.state.connection_portal.connecting = false;
2043
+ self.state.connection_portal.finish_connecting();
2029
2044
  self.state.connection_portal.error = Some(format!("Failed to connect: {}", e));
2030
2045
  self.add_log(format!("[ERROR] Gateway connection failed: {}", e));
2031
2046
  }
2032
2047
  }
2033
2048
  } else {
2034
- self.state.connection_portal.connecting = false;
2049
+ self.state.connection_portal.finish_connecting();
2035
2050
  self.state.connection_portal.error = Some("WebSocket not connected".to_string());
2036
2051
  self.add_log("[ERROR] WebSocket not connected".to_string());
2037
2052
  }
@@ -2154,7 +2169,7 @@ impl App {
2154
2169
  self.state.connection_portal.username.clear();
2155
2170
  self.state.connection_portal.focused_field = PortalField::GatewayUrl;
2156
2171
  self.state.connection_portal.connection_success = false;
2157
- self.state.connection_portal.connecting = false;
2172
+ self.state.connection_portal.finish_connecting();
2158
2173
 
2159
2174
  if let Some(ref det) = self.state.setup_portal.detection_result {
2160
2175
  let now = wall_clock_hms();
@@ -82,6 +82,7 @@ fn main() -> Result<()> {
82
82
  // Track real time for uptime (not from tick counter)
83
83
  let start_time = Instant::now();
84
84
  let mut last_tick = Instant::now();
85
+ let mut portal_connect_ui_tick = Instant::now();
85
86
 
86
87
  // Track previous screen to detect portal navigation
87
88
  let mut previous_screen: Option<crate::screens::Screen> = None;
@@ -101,7 +102,7 @@ fn main() -> Result<()> {
101
102
  // Check if portal is active - if so, skip base screen state updates
102
103
  // This prevents base screen from affecting portal performance
103
104
  use crate::screens::Screen;
104
- let current_screen = app.state.navigation.current_screen();
105
+ let current_screen = app.state.navigation.current_screen().clone();
105
106
  let is_portal_active = matches!(
106
107
  current_screen,
107
108
  Screen::ConnectionPortal | Screen::SetupPortal | Screen::PortalMonitoring
@@ -135,6 +136,14 @@ fn main() -> Result<()> {
135
136
  app.request_render("animation_tick"); // Request render for animation update
136
137
  }
137
138
  }
139
+
140
+ // Connection Portal: redraw while waiting on gateway.connect so the elapsed timer advances
141
+ if matches!(current_screen, Screen::ConnectionPortal) && app.state.connection_portal.connecting {
142
+ if portal_connect_ui_tick.elapsed() >= Duration::from_millis(500) {
143
+ portal_connect_ui_tick = Instant::now();
144
+ app.request_immediate_render("portal_connecting_tick");
145
+ }
146
+ }
138
147
 
139
148
  // ┌─────────────────────────────────────────────────────────┐
140
149
  // │ STEP 2: CHECK WEBSOCKET MESSAGES │
@@ -465,7 +474,7 @@ fn main() -> Result<()> {
465
474
 
466
475
  app.state.gateway_url = Some(url.to_string());
467
476
  app.state.connection_portal.last_successful_url = Some(url.to_string());
468
- app.state.connection_portal.connecting = false;
477
+ app.state.connection_portal.finish_connecting();
469
478
  app.state.connection_portal.connection_success = true; // Phase 1.3: Set success flag
470
479
  // Dashboard "Demo Mode / NET" used `connected`, which was never set — align with Gateway link
471
480
  app.state.connected = true;
@@ -596,7 +605,7 @@ fn main() -> Result<()> {
596
605
 
597
606
  if is_healthy {
598
607
  app.state.gateway_healthy = true;
599
- app.state.connection_portal.connecting = false;
608
+ app.state.connection_portal.finish_connecting();
600
609
  let suffix = match deps_ready {
601
610
  Some(true) => " — dependencies ready",
602
611
  Some(false) => " — Gateway up; /ready reports deps degraded",
@@ -608,7 +617,7 @@ fn main() -> Result<()> {
608
617
  ));
609
618
  } else {
610
619
  app.state.gateway_healthy = false;
611
- app.state.connection_portal.connecting = false;
620
+ app.state.connection_portal.finish_connecting();
612
621
  let error = obj.get("error").and_then(|v| v.as_str()).unwrap_or("Unknown error");
613
622
  app.add_log(format!("⚠ [{}] Gateway health check failed: {} ({}ms)", short_id, error, latency));
614
623
  }
@@ -660,7 +669,7 @@ fn main() -> Result<()> {
660
669
  // Check if this is a response to gateway.connect command (Step 7.5)
661
670
  else if Some(&resp.id) == app.state.pending_gateway_connect_id.as_ref() {
662
671
  app.state.pending_gateway_connect_id = None;
663
- app.state.connection_portal.connecting = false;
672
+ app.state.connection_portal.finish_connecting();
664
673
  app.state.connection_portal.error = Some(error_msg.clone());
665
674
 
666
675
  // Parse activity log from error response if present
@@ -1,6 +1,8 @@
1
1
  /// Portal Connection Screen (formerly Gateway Connection)
2
2
  /// Clean, professional interface for connecting to Gateway servers
3
3
 
4
+ use std::time::Instant;
5
+
4
6
  use ratatui::prelude::*;
5
7
  use ratatui::widgets::{Block, Borders, Paragraph, Wrap, Clear};
6
8
  use ratatui::layout::Alignment;
@@ -371,12 +373,16 @@ fn render_content_area(f: &mut Frame, area: Rect, state: &AppState) {
371
373
  if portal.error.is_some() {
372
374
  let err_msg = portal.error.as_ref().unwrap();
373
375
  if !portal.activity_log.is_empty() {
374
- // Show error message first so user sees "port in use, try 3002", then activity log below
376
+ // Compact status/error strip; most vertical space for the activity log (CLI trace).
375
377
  use ratatui::layout::{Constraint, Direction, Layout};
376
- // Give the error panel enough height so wrapped text does not visually collide with the log.
378
+ let total_h = area.height;
379
+ let log_min: u16 = 8;
380
+ let err_h = (total_h / 3)
381
+ .max(10)
382
+ .min(total_h.saturating_sub(log_min));
377
383
  let chunks = Layout::default()
378
384
  .direction(Direction::Vertical)
379
- .constraints([Constraint::Min(12), Constraint::Min(6)])
385
+ .constraints([Constraint::Length(err_h), Constraint::Min(log_min)])
380
386
  .split(area);
381
387
  render_error(f, chunks[0], err_msg);
382
388
  render_activity_log(f, chunks[1], state);
@@ -386,7 +392,18 @@ fn render_content_area(f: &mut Frame, area: Rect, state: &AppState) {
386
392
  } else if session_linked(state) {
387
393
  render_success(f, area, state);
388
394
  } else if portal.connecting {
389
- render_connecting(f, area, state);
395
+ if portal.activity_log.is_empty() {
396
+ render_connecting(f, area, state);
397
+ } else {
398
+ use ratatui::layout::{Constraint, Direction, Layout};
399
+ let strip = area.height.min(9).max(6);
400
+ let chunks = Layout::default()
401
+ .direction(Direction::Vertical)
402
+ .constraints([Constraint::Length(strip), Constraint::Min(4)])
403
+ .split(area);
404
+ render_connecting(f, chunks[0], state);
405
+ render_activity_log(f, chunks[1], state);
406
+ }
390
407
  } else if !portal.activity_log.is_empty() {
391
408
  // Show activity log with instructions when there's no error
392
409
  render_activity_log(f, area, state);
@@ -404,24 +421,68 @@ fn render_connecting(f: &mut Frame, area: Rect, state: &AppState) {
404
421
  } else {
405
422
  url_one.as_str()
406
423
  };
424
+ let elapsed_secs = portal
425
+ .connecting_started
426
+ .map(|t: Instant| t.elapsed().as_secs())
427
+ .unwrap_or(0);
407
428
  let block = Block::default()
408
- .title(" ⏳ CONNECTING ")
429
+ .title(format!(" ⏳ CONNECTING — {}s ", elapsed_secs))
409
430
  .borders(Borders::ALL)
410
431
  .border_style(Style::default().fg(AMBER_WARN).bold())
411
432
  .style(Style::default().bg(BG_PANEL));
412
433
  let inner = block.inner(area);
413
434
  f.render_widget(block, area);
414
- let lines = vec![
435
+
436
+ // Simple “pulse” bar: fill grows and wraps so the UI keeps moving while we wait on Node.
437
+ let bar_w = (inner.width.saturating_sub(4) as usize).clamp(8, 48);
438
+ let fill = (elapsed_secs as usize) % (bar_w + 1);
439
+ let bar: String = (0..bar_w)
440
+ .map(|i| if i < fill { '█' } else { '░' })
441
+ .collect();
442
+
443
+ let mut lines: Vec<Line> = vec![
415
444
  Line::from(""),
416
- Line::from(Span::styled(
445
+ Line::from(vec![Span::styled(
417
446
  format!("Connecting to {} …", url),
418
- Style::default().fg(AMBER_WARN).bold()
419
- )).alignment(Alignment::Center),
420
- Line::from(""),
421
- Line::from(Span::styled("Please wait", Style::default().fg(TEXT_DIM))).alignment(Alignment::Center),
422
- Line::from(""),
447
+ Style::default().fg(AMBER_WARN).bold(),
448
+ )])
449
+ .alignment(Alignment::Center),
450
+ Line::from(vec![Span::styled(
451
+ format!(
452
+ "Elapsed {}s — waiting on CLI (gateway autostart can take a few minutes)",
453
+ elapsed_secs
454
+ ),
455
+ Style::default().fg(CYBER_CYAN),
456
+ )])
457
+ .alignment(Alignment::Center),
423
458
  ];
424
- let paragraph = Paragraph::new(lines).alignment(Alignment::Center);
459
+ if inner.height > 5 {
460
+ lines.push(Line::from(vec![Span::styled(
461
+ bar,
462
+ Style::default().fg(TEXT_DIM),
463
+ )])
464
+ .alignment(Alignment::Center));
465
+ lines.push(Line::from(""));
466
+ lines.push(
467
+ Line::from(Span::styled(
468
+ "See Connection Activity Log for live steps from the Gateway starter",
469
+ Style::default().fg(TEXT_DIM),
470
+ ))
471
+ .alignment(Alignment::Center),
472
+ );
473
+ }
474
+
475
+ let target_h = inner.height as usize;
476
+ while lines.len() < target_h {
477
+ lines.push(Line::from(Span::styled("", Style::default().bg(BG_PANEL))));
478
+ }
479
+ if lines.len() > target_h {
480
+ lines.truncate(target_h);
481
+ }
482
+
483
+ let paragraph = Paragraph::new(lines)
484
+ .alignment(Alignment::Center)
485
+ .style(Style::default().bg(BG_PANEL));
425
486
  f.render_widget(paragraph, inner);
426
487
  }
427
488
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.9.134",
3
+ "version": "2.9.136",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.9.134: CRITICALcopy-gateway no longer drops memory-logger.js (bad *.log regex). Gateway @4runr/shared bundle complete. Prior: 2.9.133 auto-update verify, TUI stderr logs. ⚠️ Pre-MVP / Development Phase",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.9.136: Gateway autostart diagnostics port conflict check, bundle verification, spawn/stderr capture, HTTP detail on /health fails. Prior: 2.9.135 longer wait + timer. ⚠️ Pre-MVP / Development Phase",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",