4runr-os 2.10.39 → 2.10.41

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.
Files changed (51) hide show
  1. package/apps/gateway/dist/apps/gateway/src/index.js +14 -4
  2. package/apps/gateway/dist/apps/gateway/src/index.js.map +1 -1
  3. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts +18 -0
  4. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts.map +1 -0
  5. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js +117 -0
  6. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js.map +1 -0
  7. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts +2 -0
  8. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts.map +1 -0
  9. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js +54 -0
  10. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js.map +1 -0
  11. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts +15 -0
  12. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts.map +1 -0
  13. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js +164 -0
  14. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js.map +1 -0
  15. package/apps/gateway/package-lock.json +204 -353
  16. package/apps/gateway/src/index.ts +27 -8
  17. package/apps/gateway/src/metrics/monitoring-detail.ts +162 -0
  18. package/apps/gateway/src/middleware/log-capture.ts +70 -0
  19. package/apps/gateway/src/routes/monitoring.ts +298 -0
  20. package/dist/gateway-client.d.ts +2 -0
  21. package/dist/gateway-client.d.ts.map +1 -1
  22. package/dist/gateway-client.js +22 -0
  23. package/dist/gateway-client.js.map +1 -1
  24. package/dist/tui-handlers.js +498 -0
  25. package/dist/tui-handlers.js.map +1 -1
  26. package/mk3-tui/src/app/render_scheduler.rs +111 -112
  27. package/mk3-tui/src/app.rs +1078 -295
  28. package/mk3-tui/src/debug_log.rs +131 -124
  29. package/mk3-tui/src/io/mod.rs +63 -66
  30. package/mk3-tui/src/io/protocol.rs +14 -15
  31. package/mk3-tui/src/io/stdio.rs +31 -32
  32. package/mk3-tui/src/io/ws.rs +25 -32
  33. package/mk3-tui/src/main.rs +774 -212
  34. package/mk3-tui/src/monitoring/mod.rs +428 -0
  35. package/mk3-tui/src/screens/mod.rs +53 -39
  36. package/mk3-tui/src/storage/cache.rs +221 -224
  37. package/mk3-tui/src/storage/mod.rs +5 -6
  38. package/mk3-tui/src/ui/agent_builder.rs +1148 -922
  39. package/mk3-tui/src/ui/agent_list.rs +344 -295
  40. package/mk3-tui/src/ui/boot.rs +145 -148
  41. package/mk3-tui/src/ui/connection_portal.rs +121 -98
  42. package/mk3-tui/src/ui/help.rs +340 -284
  43. package/mk3-tui/src/ui/layout.rs +966 -803
  44. package/mk3-tui/src/ui/mod.rs +1 -1
  45. package/mk3-tui/src/ui/portal_monitoring.rs +1027 -147
  46. package/mk3-tui/src/ui/run_manager.rs +784 -764
  47. package/mk3-tui/src/ui/safe_viewport.rs +236 -235
  48. package/mk3-tui/src/ui/settings.rs +414 -362
  49. package/mk3-tui/src/ui/setup_portal.rs +158 -101
  50. package/mk3-tui/src/websocket.rs +315 -308
  51. package/package.json +2 -2
@@ -0,0 +1,428 @@
1
+ // Phase 0: Portal Monitoring State Management
2
+ // Structures for section-based, interactive monitoring
3
+
4
+ use serde::{Deserialize, Serialize};
5
+ use std::collections::HashMap;
6
+ use std::time::{Duration, Instant};
7
+
8
+ /// Main sections of the monitoring view
9
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10
+ pub enum MonitoringSection {
11
+ Overview,
12
+ Health,
13
+ Dependencies,
14
+ Metrics,
15
+ Queue,
16
+ Logs,
17
+ System,
18
+ }
19
+
20
+ impl MonitoringSection {
21
+ pub fn all() -> Vec<Self> {
22
+ vec![
23
+ Self::Overview,
24
+ Self::Health,
25
+ Self::Dependencies,
26
+ Self::Metrics,
27
+ Self::Queue,
28
+ Self::Logs,
29
+ Self::System,
30
+ ]
31
+ }
32
+
33
+ pub fn display_name(&self) -> &'static str {
34
+ match self {
35
+ Self::Overview => "Overview",
36
+ Self::Health => "Health",
37
+ Self::Dependencies => "Dependencies",
38
+ Self::Metrics => "Metrics",
39
+ Self::Queue => "Queue",
40
+ Self::Logs => "Logs",
41
+ Self::System => "System",
42
+ }
43
+ }
44
+
45
+ /// Lowercase slug sent on `monitoring.refresh` (`section` field).
46
+ pub fn cli_slug(&self) -> &'static str {
47
+ match self {
48
+ Self::Overview => "overview",
49
+ Self::Health => "health",
50
+ Self::Dependencies => "dependencies",
51
+ Self::Metrics => "metrics",
52
+ Self::Queue => "queue",
53
+ Self::Logs => "logs",
54
+ Self::System => "system",
55
+ }
56
+ }
57
+
58
+ pub fn from_cli_slug(s: &str) -> Option<Self> {
59
+ let t = s.trim().to_lowercase();
60
+ match t.as_str() {
61
+ "overview" => Some(Self::Overview),
62
+ "health" => Some(Self::Health),
63
+ "dependencies" => Some(Self::Dependencies),
64
+ "metrics" => Some(Self::Metrics),
65
+ "queue" => Some(Self::Queue),
66
+ "logs" => Some(Self::Logs),
67
+ "system" => Some(Self::System),
68
+ _ => None,
69
+ }
70
+ }
71
+ }
72
+
73
+ /// Phase 3: Metrics section drill targets (`monitoring.drill` `target` field).
74
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75
+ pub enum MetricsDrillPanel {
76
+ Http,
77
+ Runs,
78
+ Queue,
79
+ Sse,
80
+ }
81
+
82
+ impl MetricsDrillPanel {
83
+ pub fn next(self) -> Self {
84
+ match self {
85
+ Self::Http => Self::Runs,
86
+ Self::Runs => Self::Queue,
87
+ Self::Queue => Self::Sse,
88
+ Self::Sse => Self::Http,
89
+ }
90
+ }
91
+
92
+ pub fn cli_target(&self) -> &'static str {
93
+ match self {
94
+ Self::Http => "metrics_http",
95
+ Self::Runs => "metrics_runs",
96
+ Self::Queue => "metrics_queue",
97
+ Self::Sse => "metrics_sse",
98
+ }
99
+ }
100
+
101
+ pub fn label(&self) -> &'static str {
102
+ match self {
103
+ Self::Http => "HTTP",
104
+ Self::Runs => "Runs",
105
+ Self::Queue => "Queue",
106
+ Self::Sse => "SSE",
107
+ }
108
+ }
109
+
110
+ pub fn from_cli_target(s: &str) -> Option<Self> {
111
+ match s.trim() {
112
+ "metrics_http" => Some(Self::Http),
113
+ "metrics_runs" => Some(Self::Runs),
114
+ "metrics_queue" => Some(Self::Queue),
115
+ "metrics_sse" => Some(Self::Sse),
116
+ _ => None,
117
+ }
118
+ }
119
+ }
120
+
121
+ /// Status indicator for sections (determines color)
122
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123
+ pub enum SectionStatus {
124
+ Healthy, // Green
125
+ Degraded, // Yellow
126
+ Unhealthy, // Red
127
+ Unknown, // Gray
128
+ Loading, // Blue/Cyan
129
+ }
130
+
131
+ impl SectionStatus {
132
+ pub fn symbol(&self) -> &'static str {
133
+ match self {
134
+ Self::Healthy => "●",
135
+ Self::Degraded => "⚠",
136
+ Self::Unhealthy => "✖",
137
+ Self::Unknown => "○",
138
+ Self::Loading => "◉",
139
+ }
140
+ }
141
+ }
142
+
143
+ /// State for each individual section.
144
+ ///
145
+ /// **Phase 1 Portal Monitoring:** only `expanded` is authoritative for the UI. Status/summary strings
146
+ /// here are not driven by `portal_monitoring` render (that path recomputes from the CLI snapshot each
147
+ /// frame). Future phases may use `update_section` to populate these; until then do not assume headers
148
+ /// reflect `SectionState::status` / `summary`.
149
+ #[derive(Debug, Clone)]
150
+ pub struct SectionState {
151
+ /// Whether section is expanded (shows detail) or collapsed (shows summary)
152
+ pub expanded: bool,
153
+ /// Current status (for color coding header)
154
+ pub status: SectionStatus,
155
+ /// One-line summary shown when collapsed
156
+ pub summary: String,
157
+ /// When this section was last refreshed
158
+ pub last_refresh: Option<Instant>,
159
+ /// Whether section is currently loading new data
160
+ pub loading: bool,
161
+ /// Error message if last refresh failed
162
+ pub error: Option<String>,
163
+ /// Arbitrary data payload (parsed from Gateway response)
164
+ pub data: Option<serde_json::Value>,
165
+ }
166
+
167
+ impl Default for SectionState {
168
+ fn default() -> Self {
169
+ Self {
170
+ expanded: false,
171
+ status: SectionStatus::Unknown,
172
+ summary: String::new(),
173
+ last_refresh: None,
174
+ loading: false,
175
+ error: None,
176
+ data: None,
177
+ }
178
+ }
179
+ }
180
+
181
+ /// Main monitoring state (Phase 0 foundation).
182
+ ///
183
+ /// **Phase 1:** `selected_index` / `selected_section` and `sections[..].expanded` drive the TUI;
184
+ /// per-section `SectionState::status` / `summary` are not used for drawing (see `build_body_lines` in
185
+ /// `ui/portal_monitoring.rs` — status and one-line summaries come from `parse_snapshot` only).
186
+ #[derive(Debug, Clone)]
187
+ pub struct MonitoringState {
188
+ /// Currently selected section (for keyboard navigation)
189
+ pub selected_section: Option<MonitoringSection>,
190
+ /// Index in the section list (0 = Overview, 1 = Health, etc.)
191
+ pub selected_index: usize,
192
+ /// Per-section state
193
+ pub sections: HashMap<MonitoringSection, SectionState>,
194
+ /// Auto-refresh enabled/disabled
195
+ pub live_mode: bool,
196
+ /// Auto-refresh interval (default 5s)
197
+ pub refresh_interval: Duration,
198
+ /// When auto-refresh last triggered
199
+ pub last_auto_refresh: Option<Instant>,
200
+ /// Gateway URL we're monitoring
201
+ pub gateway_url: Option<String>,
202
+ }
203
+
204
+ impl Default for MonitoringState {
205
+ fn default() -> Self {
206
+ let mut sections = HashMap::new();
207
+ for section in MonitoringSection::all() {
208
+ sections.insert(section, SectionState::default());
209
+ }
210
+
211
+ Self {
212
+ selected_section: Some(MonitoringSection::Overview),
213
+ selected_index: 0,
214
+ sections,
215
+ live_mode: false, // Start with auto-refresh off
216
+ refresh_interval: Duration::from_secs(5),
217
+ last_auto_refresh: None,
218
+ gateway_url: None,
219
+ }
220
+ }
221
+ }
222
+
223
+ impl MonitoringState {
224
+ pub fn new(gateway_url: Option<String>) -> Self {
225
+ let mut state = Self::default();
226
+ state.gateway_url = gateway_url;
227
+ state
228
+ }
229
+
230
+ /// Get the currently selected section
231
+ pub fn current_section(&self) -> Option<MonitoringSection> {
232
+ self.selected_section
233
+ }
234
+
235
+ /// Move selection up (previous section)
236
+ pub fn select_previous(&mut self) {
237
+ if self.selected_index > 0 {
238
+ self.selected_index -= 1;
239
+ self.selected_section = Some(MonitoringSection::all()[self.selected_index]);
240
+ }
241
+ }
242
+
243
+ /// Move selection down (next section)
244
+ pub fn select_next(&mut self) {
245
+ let sections = MonitoringSection::all();
246
+ if self.selected_index < sections.len() - 1 {
247
+ self.selected_index += 1;
248
+ self.selected_section = Some(sections[self.selected_index]);
249
+ }
250
+ }
251
+
252
+ /// Toggle expand/collapse for currently selected section
253
+ pub fn toggle_current_section(&mut self) {
254
+ if let Some(section) = self.selected_section {
255
+ if let Some(state) = self.sections.get_mut(&section) {
256
+ state.expanded = !state.expanded;
257
+ }
258
+ }
259
+ }
260
+
261
+ /// Toggle expand/collapse for the section at `index` (Phase 1 Portal Monitoring).
262
+ pub fn toggle_section_at_index(&mut self, index: usize) {
263
+ let sections = MonitoringSection::all();
264
+ if let Some(&section) = sections.get(index) {
265
+ if let Some(state) = self.sections.get_mut(&section) {
266
+ state.expanded = !state.expanded;
267
+ }
268
+ }
269
+ }
270
+
271
+ /// Move selection with wrap (Phase 1).
272
+ pub fn select_previous_wrapped(&mut self) {
273
+ let n = MonitoringSection::all().len();
274
+ if n == 0 {
275
+ return;
276
+ }
277
+ self.selected_index = (self.selected_index + n - 1) % n;
278
+ self.selected_section = MonitoringSection::all().get(self.selected_index).copied();
279
+ }
280
+
281
+ pub fn select_next_wrapped(&mut self) {
282
+ let n = MonitoringSection::all().len();
283
+ if n == 0 {
284
+ return;
285
+ }
286
+ self.selected_index = (self.selected_index + 1) % n;
287
+ self.selected_section = MonitoringSection::all().get(self.selected_index).copied();
288
+ }
289
+
290
+ /// Expand a specific section
291
+ pub fn expand_section(&mut self, section: MonitoringSection) {
292
+ if let Some(state) = self.sections.get_mut(&section) {
293
+ state.expanded = true;
294
+ }
295
+ }
296
+
297
+ /// Collapse a specific section
298
+ pub fn collapse_section(&mut self, section: MonitoringSection) {
299
+ if let Some(state) = self.sections.get_mut(&section) {
300
+ state.expanded = false;
301
+ }
302
+ }
303
+
304
+ /// Collapse all sections
305
+ pub fn collapse_all(&mut self) {
306
+ for state in self.sections.values_mut() {
307
+ state.expanded = false;
308
+ }
309
+ }
310
+
311
+ /// Toggle auto-refresh (live mode)
312
+ pub fn toggle_live_mode(&mut self) {
313
+ self.live_mode = !self.live_mode;
314
+ if self.live_mode {
315
+ self.last_auto_refresh = Some(Instant::now());
316
+ }
317
+ }
318
+
319
+ /// Check if it's time for auto-refresh
320
+ pub fn should_auto_refresh(&self) -> bool {
321
+ if !self.live_mode {
322
+ return false;
323
+ }
324
+ match self.last_auto_refresh {
325
+ Some(last) => last.elapsed() >= self.refresh_interval,
326
+ None => true,
327
+ }
328
+ }
329
+
330
+ /// Mark that auto-refresh just happened
331
+ pub fn mark_auto_refresh(&mut self) {
332
+ self.last_auto_refresh = Some(Instant::now());
333
+ }
334
+
335
+ /// Set loading state for a section
336
+ pub fn set_loading(&mut self, section: MonitoringSection, loading: bool) {
337
+ if let Some(state) = self.sections.get_mut(&section) {
338
+ state.loading = loading;
339
+ if loading {
340
+ state.status = SectionStatus::Loading;
341
+ state.error = None;
342
+ }
343
+ }
344
+ }
345
+
346
+ /// Update section data after successful refresh
347
+ pub fn update_section(
348
+ &mut self,
349
+ section: MonitoringSection,
350
+ status: SectionStatus,
351
+ summary: String,
352
+ data: Option<serde_json::Value>,
353
+ ) {
354
+ if let Some(state) = self.sections.get_mut(&section) {
355
+ state.status = status;
356
+ state.summary = summary;
357
+ state.data = data;
358
+ state.last_refresh = Some(Instant::now());
359
+ state.loading = false;
360
+ state.error = None;
361
+ }
362
+ }
363
+
364
+ /// Set error state for a section
365
+ pub fn set_error(&mut self, section: MonitoringSection, error: String) {
366
+ if let Some(state) = self.sections.get_mut(&section) {
367
+ state.status = SectionStatus::Unhealthy;
368
+ state.error = Some(error);
369
+ state.loading = false;
370
+ }
371
+ }
372
+
373
+ /// Get section state (immutable)
374
+ pub fn get_section(&self, section: MonitoringSection) -> Option<&SectionState> {
375
+ self.sections.get(&section)
376
+ }
377
+
378
+ /// Get time since last refresh for a section (for display)
379
+ pub fn time_since_refresh(&self, section: MonitoringSection) -> Option<Duration> {
380
+ self.sections
381
+ .get(&section)
382
+ .and_then(|s| s.last_refresh)
383
+ .map(|t| t.elapsed())
384
+ }
385
+ }
386
+
387
+ #[cfg(test)]
388
+ mod tests {
389
+ use super::*;
390
+
391
+ #[test]
392
+ fn test_section_navigation() {
393
+ let mut state = MonitoringState::default();
394
+ assert_eq!(state.selected_index, 0);
395
+ assert_eq!(state.current_section(), Some(MonitoringSection::Overview));
396
+
397
+ state.select_next();
398
+ assert_eq!(state.selected_index, 1);
399
+ assert_eq!(state.current_section(), Some(MonitoringSection::Health));
400
+
401
+ state.select_previous();
402
+ assert_eq!(state.selected_index, 0);
403
+ assert_eq!(state.current_section(), Some(MonitoringSection::Overview));
404
+ }
405
+
406
+ #[test]
407
+ fn test_toggle_expansion() {
408
+ let mut state = MonitoringState::default();
409
+ state.selected_section = Some(MonitoringSection::Health);
410
+
411
+ let health = state.get_section(MonitoringSection::Health).unwrap();
412
+ assert!(!health.expanded);
413
+
414
+ state.toggle_current_section();
415
+ let health = state.get_section(MonitoringSection::Health).unwrap();
416
+ assert!(health.expanded);
417
+ }
418
+
419
+ #[test]
420
+ fn test_live_mode() {
421
+ let mut state = MonitoringState::default();
422
+ assert!(!state.live_mode);
423
+
424
+ state.toggle_live_mode();
425
+ assert!(state.live_mode);
426
+ assert!(state.last_auto_refresh.is_some());
427
+ }
428
+ }
@@ -1,11 +1,10 @@
1
1
  /// Screen types and navigation system
2
- ///
2
+ ///
3
3
  /// This module defines the screen/mode system for the TUI.
4
4
  /// Screens can be:
5
5
  /// - Base screens (Boot, Main) - always visible
6
6
  /// - Overlay screens (Agent Builder, Run Manager, Settings) - full-screen modals
7
7
  /// - Popup screens (Confirmations, Alerts) - small overlays
8
-
9
8
  use serde::{Deserialize, Serialize};
10
9
 
11
10
  /// Screen enum - represents all possible screens in the TUI
@@ -14,7 +13,7 @@ pub enum Screen {
14
13
  // Base screens
15
14
  Boot,
16
15
  Main,
17
-
16
+
18
17
  // Overlay screens (full-screen modals)
19
18
  AgentBuilder,
20
19
  RunManager,
@@ -24,10 +23,15 @@ pub enum Screen {
24
23
  SetupPortal,
25
24
  /// Gateway traffic / server-style observability (scaffold; live stream TBD)
26
25
  PortalMonitoring,
27
-
26
+
28
27
  // Popup screens (small overlays)
29
- Confirmation { message: String, action: String },
30
- Alert { message: String },
28
+ Confirmation {
29
+ message: String,
30
+ action: String,
31
+ },
32
+ Alert {
33
+ message: String,
34
+ },
31
35
  }
32
36
 
33
37
  impl Screen {
@@ -38,23 +42,24 @@ impl Screen {
38
42
  Screen::AgentBuilder | Screen::RunManager | Screen::Settings | Screen::AgentList
39
43
  )
40
44
  }
41
-
45
+
42
46
  /// Returns true if this screen is a popup (small overlay)
43
47
  pub fn is_popup(&self) -> bool {
44
- matches!(
45
- self,
46
- Screen::Confirmation { .. } | Screen::Alert { .. }
47
- )
48
+ matches!(self, Screen::Confirmation { .. } | Screen::Alert { .. })
48
49
  }
49
-
50
+
50
51
  /// Returns true if this screen is a base screen (full-screen standalone)
51
52
  pub fn is_base(&self) -> bool {
52
53
  matches!(
53
54
  self,
54
- Screen::Boot | Screen::Main | Screen::ConnectionPortal | Screen::SetupPortal | Screen::PortalMonitoring
55
+ Screen::Boot
56
+ | Screen::Main
57
+ | Screen::ConnectionPortal
58
+ | Screen::SetupPortal
59
+ | Screen::PortalMonitoring
55
60
  )
56
61
  }
57
-
62
+
58
63
  /// Returns the screen name for display
59
64
  #[allow(dead_code)]
60
65
  pub fn name(&self) -> &str {
@@ -79,11 +84,11 @@ impl Screen {
79
84
  pub struct NavigationState {
80
85
  /// Current base screen (Boot or Main)
81
86
  pub base_screen: Screen,
82
-
87
+
83
88
  /// Overlay stack (full-screen modals)
84
89
  /// Last item is the currently visible overlay
85
90
  pub overlay_stack: Vec<Screen>,
86
-
91
+
87
92
  /// Popup stack (small overlays on top of everything)
88
93
  /// Last item is the currently visible popup
89
94
  pub popup_stack: Vec<Screen>,
@@ -104,7 +109,7 @@ impl NavigationState {
104
109
  pub fn new() -> Self {
105
110
  Self::default()
106
111
  }
107
-
112
+
108
113
  /// Get the currently visible screen (topmost in hierarchy)
109
114
  pub fn current_screen(&self) -> &Screen {
110
115
  // Priority: Popup > Overlay > Base
@@ -116,7 +121,7 @@ impl NavigationState {
116
121
  &self.base_screen
117
122
  }
118
123
  }
119
-
124
+
120
125
  /// Navigate to a base screen (Boot or Main)
121
126
  pub fn navigate_to_base(&mut self, screen: Screen) {
122
127
  if screen.is_base() {
@@ -126,49 +131,49 @@ impl NavigationState {
126
131
  self.popup_stack.clear();
127
132
  }
128
133
  }
129
-
134
+
130
135
  /// Push an overlay screen (Agent Builder, Run Manager, Settings)
131
136
  pub fn push_overlay(&mut self, screen: Screen) {
132
137
  if screen.is_overlay() {
133
138
  self.overlay_stack.push(screen);
134
139
  }
135
140
  }
136
-
141
+
137
142
  /// Pop the current overlay and return to previous screen
138
143
  pub fn pop_overlay(&mut self) -> Option<Screen> {
139
144
  self.overlay_stack.pop()
140
145
  }
141
-
146
+
142
147
  /// Push a popup screen (Confirmation, Alert, Help)
143
148
  pub fn push_popup(&mut self, screen: Screen) {
144
149
  if screen.is_popup() {
145
150
  self.popup_stack.push(screen);
146
151
  }
147
152
  }
148
-
153
+
149
154
  /// Pop the current popup
150
155
  pub fn pop_popup(&mut self) -> Option<Screen> {
151
156
  self.popup_stack.pop()
152
157
  }
153
-
158
+
154
159
  /// Close all overlays and popups (return to base screen)
155
160
  #[allow(dead_code)]
156
161
  pub fn close_all(&mut self) {
157
162
  self.overlay_stack.clear();
158
163
  self.popup_stack.clear();
159
164
  }
160
-
165
+
161
166
  /// Check if we're currently on the base screen (no overlays/popups)
162
167
  #[allow(dead_code)]
163
168
  pub fn is_on_base(&self) -> bool {
164
169
  self.overlay_stack.is_empty() && self.popup_stack.is_empty()
165
170
  }
166
-
171
+
167
172
  /// Check if we have any overlays open
168
173
  pub fn has_overlay(&self) -> bool {
169
174
  !self.overlay_stack.is_empty()
170
175
  }
171
-
176
+
172
177
  /// Check if we have any popups open
173
178
  pub fn has_popup(&self) -> bool {
174
179
  !self.popup_stack.is_empty()
@@ -178,7 +183,7 @@ impl NavigationState {
178
183
  #[cfg(test)]
179
184
  mod tests {
180
185
  use super::*;
181
-
186
+
182
187
  #[test]
183
188
  fn test_screen_classification() {
184
189
  assert!(Screen::Boot.is_base());
@@ -188,47 +193,56 @@ mod tests {
188
193
  assert!(Screen::Settings.is_overlay());
189
194
  assert!(Screen::Help.is_popup());
190
195
  }
191
-
196
+
192
197
  #[test]
193
198
  fn test_navigation_state() {
194
199
  let mut nav = NavigationState::new();
195
-
200
+
196
201
  // Start at Boot
197
202
  assert_eq!(nav.current_screen(), &Screen::Boot);
198
203
  assert!(nav.is_on_base());
199
-
204
+
200
205
  // Navigate to Main
201
206
  nav.navigate_to_base(Screen::Main);
202
207
  assert_eq!(nav.current_screen(), &Screen::Main);
203
-
208
+
204
209
  // Push overlay
205
210
  nav.push_overlay(Screen::AgentBuilder);
206
211
  assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
207
212
  assert!(nav.has_overlay());
208
213
  assert!(!nav.is_on_base());
209
-
214
+
210
215
  // Push popup
211
- nav.push_popup(Screen::Alert { message: "Test".to_string() });
212
- assert_eq!(nav.current_screen(), &Screen::Alert { message: "Test".to_string() });
216
+ nav.push_popup(Screen::Alert {
217
+ message: "Test".to_string(),
218
+ });
219
+ assert_eq!(
220
+ nav.current_screen(),
221
+ &Screen::Alert {
222
+ message: "Test".to_string()
223
+ }
224
+ );
213
225
  assert!(nav.has_popup());
214
-
226
+
215
227
  // Pop popup
216
228
  nav.pop_popup();
217
229
  assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
218
-
230
+
219
231
  // Pop overlay
220
232
  nav.pop_overlay();
221
233
  assert_eq!(nav.current_screen(), &Screen::Main);
222
234
  assert!(nav.is_on_base());
223
235
  }
224
-
236
+
225
237
  #[test]
226
238
  fn test_close_all() {
227
239
  let mut nav = NavigationState::new();
228
240
  nav.navigate_to_base(Screen::Main);
229
241
  nav.push_overlay(Screen::AgentBuilder);
230
- nav.push_popup(Screen::Alert { message: "Test".to_string() });
231
-
242
+ nav.push_popup(Screen::Alert {
243
+ message: "Test".to_string(),
244
+ });
245
+
232
246
  nav.close_all();
233
247
  assert!(nav.is_on_base());
234
248
  assert_eq!(nav.current_screen(), &Screen::Main);