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
@@ -1,362 +1,414 @@
1
- /// Settings Screen
2
- /// Configure system settings, AI providers, and mode
3
-
4
- use ratatui::prelude::*;
5
- use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
6
- use crate::app::AppState;
7
-
8
- // === 4RUNR BRAND COLORS (matching layout.rs) ===
9
- const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
10
- const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
11
- const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
12
- const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
13
- const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
14
- const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
15
- const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
16
- const BG_PANEL: Color = Color::Rgb(18, 18, 25);
17
-
18
- /// Settings state
19
- #[derive(Debug, Clone)]
20
- pub struct SettingsState {
21
- /// Current focused section
22
- pub focused_section: SettingsSection,
23
-
24
- /// Mode: Local or Connected
25
- pub mode: OperationMode,
26
-
27
- /// AI Provider settings
28
- pub ai_provider: String, // "openai", "anthropic", "local"
29
- pub ai_model: String, // "gpt-4", "claude-3", etc.
30
- pub api_key_set: bool, // Whether API key is configured
31
-
32
- /// Gateway settings
33
- pub gateway_url: String,
34
- pub gateway_connected: bool,
35
-
36
- /// Auto-update setting
37
- pub auto_update: bool,
38
-
39
- /// Unsaved changes
40
- pub has_changes: bool,
41
- }
42
-
43
- #[derive(Debug, Clone, PartialEq)]
44
- pub enum SettingsSection {
45
- Mode,
46
- AIProvider,
47
- Gateway,
48
- AutoUpdate,
49
- }
50
-
51
- #[derive(Debug, Clone, PartialEq)]
52
- pub enum OperationMode {
53
- Local, // Local-only, no gateway
54
- Connected, // Connected to gateway
55
- }
56
-
57
- impl OperationMode {
58
- pub fn as_str(&self) -> &str {
59
- match self {
60
- OperationMode::Local => "Local",
61
- OperationMode::Connected => "Connected",
62
- }
63
- }
64
-
65
- pub fn description(&self) -> &str {
66
- match self {
67
- OperationMode::Local => "Offline mode - local tools and models only",
68
- OperationMode::Connected => "Full mode - gateway access, server tools, Shield, Sentinel",
69
- }
70
- }
71
- }
72
-
73
- impl Default for SettingsState {
74
- fn default() -> Self {
75
- Self {
76
- focused_section: SettingsSection::Mode,
77
- mode: OperationMode::Local,
78
- ai_provider: "openai".to_string(),
79
- ai_model: "gpt-4".to_string(),
80
- api_key_set: false,
81
- gateway_url: "http://localhost:3001".to_string(),
82
- gateway_connected: false,
83
- auto_update: true,
84
- has_changes: false,
85
- }
86
- }
87
- }
88
-
89
- impl SettingsState {
90
- /// Toggle mode
91
- pub fn toggle_mode(&mut self) {
92
- self.mode = match self.mode {
93
- OperationMode::Local => OperationMode::Connected,
94
- OperationMode::Connected => OperationMode::Local,
95
- };
96
- self.has_changes = true;
97
- }
98
-
99
- /// Cycle to next AI provider
100
- pub fn next_provider(&mut self) {
101
- self.ai_provider = match self.ai_provider.as_str() {
102
- "openai" => "anthropic".to_string(),
103
- "anthropic" => "local".to_string(),
104
- _ => "openai".to_string(),
105
- };
106
-
107
- // Update default model for provider
108
- self.ai_model = match self.ai_provider.as_str() {
109
- "openai" => "gpt-4".to_string(),
110
- "anthropic" => "claude-3-opus".to_string(),
111
- "local" => "llama2".to_string(),
112
- _ => "gpt-4".to_string(),
113
- };
114
-
115
- self.has_changes = true;
116
- }
117
-
118
- /// Toggle auto-update
119
- pub fn toggle_auto_update(&mut self) {
120
- self.auto_update = !self.auto_update;
121
- self.has_changes = true;
122
- }
123
-
124
- /// Move to next section
125
- pub fn next_section(&mut self) {
126
- self.focused_section = match self.focused_section {
127
- SettingsSection::Mode => SettingsSection::AIProvider,
128
- SettingsSection::AIProvider => SettingsSection::Gateway,
129
- SettingsSection::Gateway => SettingsSection::AutoUpdate,
130
- SettingsSection::AutoUpdate => SettingsSection::Mode,
131
- };
132
- }
133
-
134
- /// Move to previous section
135
- pub fn prev_section(&mut self) {
136
- self.focused_section = match self.focused_section {
137
- SettingsSection::Mode => SettingsSection::AutoUpdate,
138
- SettingsSection::AIProvider => SettingsSection::Mode,
139
- SettingsSection::Gateway => SettingsSection::AIProvider,
140
- SettingsSection::AutoUpdate => SettingsSection::Gateway,
141
- };
142
- }
143
-
144
- /// Save settings
145
- pub fn save(&mut self) {
146
- self.has_changes = false;
147
- // TODO: Persist settings to config file
148
- }
149
-
150
- /// Discard changes
151
- pub fn discard(&mut self) {
152
- // TODO: Reload from config file
153
- self.has_changes = false;
154
- }
155
- }
156
-
157
- /// Render the Settings screen
158
- pub fn render(f: &mut Frame, state: &AppState) {
159
- let area = f.size();
160
-
161
- // Get settings state from AppState
162
- let settings = &state.settings;
163
-
164
- // Split screen into sections
165
- use ratatui::layout::{Constraint, Direction, Layout};
166
-
167
- let chunks = Layout::default()
168
- .direction(Direction::Vertical)
169
- .constraints([
170
- Constraint::Length(3), // Header
171
- Constraint::Min(15), // Settings content
172
- Constraint::Length(3), // Actions
173
- ])
174
- .split(area);
175
-
176
- render_header(f, chunks[0], settings);
177
- render_settings_content(f, chunks[1], settings);
178
- render_actions(f, chunks[2], settings);
179
- }
180
-
181
- fn render_header(f: &mut Frame, area: Rect, settings: &SettingsState) {
182
- let title = if settings.has_changes {
183
- " ⚙️ Settings (Unsaved Changes ●) "
184
- } else {
185
- " ⚙️ Settings "
186
- };
187
-
188
- let block = Block::default()
189
- .title(title)
190
- .borders(Borders::ALL)
191
- .border_style(Style::default().fg(if settings.has_changes {
192
- AMBER_WARN
193
- } else {
194
- BRAND_PURPLE
195
- }))
196
- .style(Style::default().bg(BG_PANEL));
197
-
198
- f.render_widget(block, area);
199
- }
200
-
201
- fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState) {
202
- let block = Block::default()
203
- .borders(Borders::ALL)
204
- .border_style(Style::default().fg(CYBER_CYAN))
205
- .style(Style::default().bg(BG_PANEL));
206
-
207
- let inner = block.inner(area);
208
- f.render_widget(block, area);
209
-
210
- let is_mode_focused = settings.focused_section == SettingsSection::Mode;
211
- let is_provider_focused = settings.focused_section == SettingsSection::AIProvider;
212
- let is_gateway_focused = settings.focused_section == SettingsSection::Gateway;
213
- let is_autoupdate_focused = settings.focused_section == SettingsSection::AutoUpdate;
214
-
215
- let text = vec![
216
- Line::from(""),
217
- // Mode Section
218
- Line::from(vec![
219
- Span::styled(
220
- if is_mode_focused { "▶ " } else { " " },
221
- Style::default().fg(BRAND_PURPLE).bold()
222
- ),
223
- Span::styled("🌐 Operation Mode", Style::default().fg(TEXT_PRIMARY).bold()),
224
- ]),
225
- Line::from(vec![
226
- Span::raw(" "),
227
- Span::styled(
228
- format!("[{}] Local [{}] Connected",
229
- if settings.mode == OperationMode::Local { "●" } else { " " },
230
- if settings.mode == OperationMode::Connected { "" } else { " " }
231
- ),
232
- Style::default().fg(if is_mode_focused { CYBER_CYAN } else { TEXT_PRIMARY })
233
- ),
234
- ]),
235
- Line::from(vec![
236
- Span::raw(" "),
237
- Span::styled(
238
- settings.mode.description(),
239
- Style::default().fg(TEXT_DIM).italic()
240
- ),
241
- ]),
242
- Line::from(""),
243
-
244
- // AI Provider Section
245
- Line::from(vec![
246
- Span::styled(
247
- if is_provider_focused { "▶ " } else { " " },
248
- Style::default().fg(BRAND_PURPLE).bold()
249
- ),
250
- Span::styled("🤖 AI Provider", Style::default().fg(TEXT_PRIMARY).bold()),
251
- ]),
252
- Line::from(vec![
253
- Span::styled(" Provider: ", Style::default().fg(TEXT_DIM)),
254
- Span::styled(
255
- &settings.ai_provider,
256
- Style::default().fg(if is_provider_focused { CYBER_CYAN } else { TEXT_PRIMARY }).bold()
257
- ),
258
- ]),
259
- Line::from(vec![
260
- Span::styled(" Model: ", Style::default().fg(TEXT_DIM)),
261
- Span::styled(
262
- &settings.ai_model,
263
- Style::default().fg(if is_provider_focused { CYBER_CYAN } else { TEXT_PRIMARY })
264
- ),
265
- ]),
266
- Line::from(vec![
267
- Span::styled(" API Key: ", Style::default().fg(TEXT_DIM)),
268
- Span::styled(
269
- if settings.api_key_set { "Configured " } else { "Not set" },
270
- Style::default().fg(if settings.api_key_set { NEON_GREEN } else { Color::Rgb(255, 69, 69) })
271
- ),
272
- ]),
273
- Line::from(""),
274
-
275
- // Gateway Section
276
- Line::from(vec![
277
- Span::styled(
278
- if is_gateway_focused { "▶ " } else { " " },
279
- Style::default().fg(BRAND_PURPLE).bold()
280
- ),
281
- Span::styled("🔗 Gateway Connection", Style::default().fg(TEXT_PRIMARY).bold()),
282
- ]),
283
- Line::from(vec![
284
- Span::styled(" URL: ", Style::default().fg(TEXT_DIM)),
285
- Span::styled(
286
- &settings.gateway_url,
287
- Style::default().fg(if is_gateway_focused { CYBER_CYAN } else { TEXT_PRIMARY })
288
- ),
289
- ]),
290
- Line::from(vec![
291
- Span::styled(" Status: ", Style::default().fg(TEXT_DIM)),
292
- Span::styled(
293
- if settings.gateway_connected { "Connected ✓" } else { "Disconnected" },
294
- Style::default().fg(if settings.gateway_connected { NEON_GREEN } else { TEXT_MUTED })
295
- ),
296
- ]),
297
- Line::from(""),
298
-
299
- // Auto-update Section
300
- Line::from(vec![
301
- Span::styled(
302
- if is_autoupdate_focused { "▶ " } else { " " },
303
- Style::default().fg(BRAND_PURPLE).bold()
304
- ),
305
- Span::styled("🔄 Auto-Update", Style::default().fg(TEXT_PRIMARY).bold()),
306
- ]),
307
- Line::from(vec![
308
- Span::raw(" "),
309
- Span::styled(
310
- format!("[{}] Enable automatic updates",
311
- if settings.auto_update { "" } else { " " }
312
- ),
313
- Style::default().fg(if is_autoupdate_focused { CYBER_CYAN } else { TEXT_PRIMARY })
314
- ),
315
- ]),
316
- ];
317
-
318
- let paragraph = Paragraph::new(text)
319
- .wrap(Wrap { trim: false })
320
- .alignment(Alignment::Left);
321
-
322
- f.render_widget(paragraph, inner);
323
- }
324
-
325
- fn render_actions(f: &mut Frame, area: Rect, settings: &SettingsState) {
326
- let block = Block::default()
327
- .title(" ⌨️ Actions ")
328
- .borders(Borders::ALL)
329
- .border_style(Style::default().fg(TEXT_DIM))
330
- .style(Style::default().bg(BG_PANEL));
331
-
332
- let inner = block.inner(area);
333
- f.render_widget(block, area);
334
-
335
- let text = if settings.has_changes {
336
- Line::from(vec![
337
- Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
338
- Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
339
- Span::styled("Space", Style::default().fg(AMBER_WARN).bold()),
340
- Span::styled(" Toggle │ ", Style::default().fg(TEXT_DIM)),
341
- Span::styled("Enter", Style::default().fg(NEON_GREEN).bold()),
342
- Span::styled(" Save │ ", Style::default().fg(TEXT_DIM)),
343
- Span::styled("ESC", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
344
- Span::styled(" Discard & Close", Style::default().fg(TEXT_DIM)),
345
- ])
346
- } else {
347
- Line::from(vec![
348
- Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
349
- Span::styled(" Navigate ", Style::default().fg(TEXT_DIM)),
350
- Span::styled("Space", Style::default().fg(AMBER_WARN).bold()),
351
- Span::styled(" Toggle │ ", Style::default().fg(TEXT_DIM)),
352
- Span::styled("ESC", Style::default().fg(BRAND_PURPLE).bold()),
353
- Span::styled(" Close", Style::default().fg(TEXT_DIM)),
354
- ])
355
- };
356
-
357
- let paragraph = Paragraph::new(text)
358
- .style(Style::default().fg(TEXT_PRIMARY))
359
- .alignment(Alignment::Center);
360
-
361
- f.render_widget(paragraph, inner);
362
- }
1
+ use crate::app::AppState;
2
+ /// Settings Screen
3
+ /// Configure system settings, AI providers, and mode
4
+ use ratatui::prelude::*;
5
+ use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
6
+
7
+ // === 4RUNR BRAND COLORS (matching layout.rs) ===
8
+ const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
9
+ const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
10
+ const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
11
+ const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
12
+ const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
13
+ const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
14
+ const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
15
+ const BG_PANEL: Color = Color::Rgb(18, 18, 25);
16
+
17
+ /// Settings state
18
+ #[derive(Debug, Clone)]
19
+ pub struct SettingsState {
20
+ /// Current focused section
21
+ pub focused_section: SettingsSection,
22
+
23
+ /// Mode: Local or Connected
24
+ pub mode: OperationMode,
25
+
26
+ /// AI Provider settings
27
+ pub ai_provider: String, // "openai", "anthropic", "local"
28
+ pub ai_model: String, // "gpt-4", "claude-3", etc.
29
+ pub api_key_set: bool, // Whether API key is configured
30
+
31
+ /// Gateway settings
32
+ pub gateway_url: String,
33
+ pub gateway_connected: bool,
34
+
35
+ /// Auto-update setting
36
+ pub auto_update: bool,
37
+
38
+ /// Unsaved changes
39
+ pub has_changes: bool,
40
+ }
41
+
42
+ #[derive(Debug, Clone, PartialEq)]
43
+ pub enum SettingsSection {
44
+ Mode,
45
+ AIProvider,
46
+ Gateway,
47
+ AutoUpdate,
48
+ }
49
+
50
+ #[derive(Debug, Clone, PartialEq)]
51
+ pub enum OperationMode {
52
+ Local, // Local-only, no gateway
53
+ Connected, // Connected to gateway
54
+ }
55
+
56
+ impl OperationMode {
57
+ pub fn as_str(&self) -> &str {
58
+ match self {
59
+ OperationMode::Local => "Local",
60
+ OperationMode::Connected => "Connected",
61
+ }
62
+ }
63
+
64
+ pub fn description(&self) -> &str {
65
+ match self {
66
+ OperationMode::Local => "Offline mode - local tools and models only",
67
+ OperationMode::Connected => {
68
+ "Full mode - gateway access, server tools, Shield, Sentinel"
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ impl Default for SettingsState {
75
+ fn default() -> Self {
76
+ Self {
77
+ focused_section: SettingsSection::Mode,
78
+ mode: OperationMode::Local,
79
+ ai_provider: "openai".to_string(),
80
+ ai_model: "gpt-4".to_string(),
81
+ api_key_set: false,
82
+ gateway_url: "http://localhost:3001".to_string(),
83
+ gateway_connected: false,
84
+ auto_update: true,
85
+ has_changes: false,
86
+ }
87
+ }
88
+ }
89
+
90
+ impl SettingsState {
91
+ /// Toggle mode
92
+ pub fn toggle_mode(&mut self) {
93
+ self.mode = match self.mode {
94
+ OperationMode::Local => OperationMode::Connected,
95
+ OperationMode::Connected => OperationMode::Local,
96
+ };
97
+ self.has_changes = true;
98
+ }
99
+
100
+ /// Cycle to next AI provider
101
+ pub fn next_provider(&mut self) {
102
+ self.ai_provider = match self.ai_provider.as_str() {
103
+ "openai" => "anthropic".to_string(),
104
+ "anthropic" => "local".to_string(),
105
+ _ => "openai".to_string(),
106
+ };
107
+
108
+ // Update default model for provider
109
+ self.ai_model = match self.ai_provider.as_str() {
110
+ "openai" => "gpt-4".to_string(),
111
+ "anthropic" => "claude-3-opus".to_string(),
112
+ "local" => "llama2".to_string(),
113
+ _ => "gpt-4".to_string(),
114
+ };
115
+
116
+ self.has_changes = true;
117
+ }
118
+
119
+ /// Toggle auto-update
120
+ pub fn toggle_auto_update(&mut self) {
121
+ self.auto_update = !self.auto_update;
122
+ self.has_changes = true;
123
+ }
124
+
125
+ /// Move to next section
126
+ pub fn next_section(&mut self) {
127
+ self.focused_section = match self.focused_section {
128
+ SettingsSection::Mode => SettingsSection::AIProvider,
129
+ SettingsSection::AIProvider => SettingsSection::Gateway,
130
+ SettingsSection::Gateway => SettingsSection::AutoUpdate,
131
+ SettingsSection::AutoUpdate => SettingsSection::Mode,
132
+ };
133
+ }
134
+
135
+ /// Move to previous section
136
+ pub fn prev_section(&mut self) {
137
+ self.focused_section = match self.focused_section {
138
+ SettingsSection::Mode => SettingsSection::AutoUpdate,
139
+ SettingsSection::AIProvider => SettingsSection::Mode,
140
+ SettingsSection::Gateway => SettingsSection::AIProvider,
141
+ SettingsSection::AutoUpdate => SettingsSection::Gateway,
142
+ };
143
+ }
144
+
145
+ /// Save settings
146
+ pub fn save(&mut self) {
147
+ self.has_changes = false;
148
+ // TODO: Persist settings to config file
149
+ }
150
+
151
+ /// Discard changes
152
+ pub fn discard(&mut self) {
153
+ // TODO: Reload from config file
154
+ self.has_changes = false;
155
+ }
156
+ }
157
+
158
+ /// Render the Settings screen
159
+ pub fn render(f: &mut Frame, state: &AppState) {
160
+ let area = f.size();
161
+
162
+ // Get settings state from AppState
163
+ let settings = &state.settings;
164
+
165
+ // Split screen into sections
166
+ use ratatui::layout::{Constraint, Direction, Layout};
167
+
168
+ let chunks = Layout::default()
169
+ .direction(Direction::Vertical)
170
+ .constraints([
171
+ Constraint::Length(3), // Header
172
+ Constraint::Min(15), // Settings content
173
+ Constraint::Length(3), // Actions
174
+ ])
175
+ .split(area);
176
+
177
+ render_header(f, chunks[0], settings);
178
+ render_settings_content(f, chunks[1], settings);
179
+ render_actions(f, chunks[2], settings);
180
+ }
181
+
182
+ fn render_header(f: &mut Frame, area: Rect, settings: &SettingsState) {
183
+ let title = if settings.has_changes {
184
+ " ⚙️ Settings (Unsaved Changes ●) "
185
+ } else {
186
+ " ⚙️ Settings "
187
+ };
188
+
189
+ let block = Block::default()
190
+ .title(title)
191
+ .borders(Borders::ALL)
192
+ .border_style(Style::default().fg(if settings.has_changes {
193
+ AMBER_WARN
194
+ } else {
195
+ BRAND_PURPLE
196
+ }))
197
+ .style(Style::default().bg(BG_PANEL));
198
+
199
+ f.render_widget(block, area);
200
+ }
201
+
202
+ fn render_settings_content(f: &mut Frame, area: Rect, settings: &SettingsState) {
203
+ let block = Block::default()
204
+ .borders(Borders::ALL)
205
+ .border_style(Style::default().fg(CYBER_CYAN))
206
+ .style(Style::default().bg(BG_PANEL));
207
+
208
+ let inner = block.inner(area);
209
+ f.render_widget(block, area);
210
+
211
+ let is_mode_focused = settings.focused_section == SettingsSection::Mode;
212
+ let is_provider_focused = settings.focused_section == SettingsSection::AIProvider;
213
+ let is_gateway_focused = settings.focused_section == SettingsSection::Gateway;
214
+ let is_autoupdate_focused = settings.focused_section == SettingsSection::AutoUpdate;
215
+
216
+ let text = vec![
217
+ Line::from(""),
218
+ // Mode Section
219
+ Line::from(vec![
220
+ Span::styled(
221
+ if is_mode_focused { "▶ " } else { " " },
222
+ Style::default().fg(BRAND_PURPLE).bold(),
223
+ ),
224
+ Span::styled(
225
+ "🌐 Operation Mode",
226
+ Style::default().fg(TEXT_PRIMARY).bold(),
227
+ ),
228
+ ]),
229
+ Line::from(vec![
230
+ Span::raw(" "),
231
+ Span::styled(
232
+ format!(
233
+ "[{}] Local [{}] Connected",
234
+ if settings.mode == OperationMode::Local {
235
+ "●"
236
+ } else {
237
+ " "
238
+ },
239
+ if settings.mode == OperationMode::Connected {
240
+ "●"
241
+ } else {
242
+ " "
243
+ }
244
+ ),
245
+ Style::default().fg(if is_mode_focused {
246
+ CYBER_CYAN
247
+ } else {
248
+ TEXT_PRIMARY
249
+ }),
250
+ ),
251
+ ]),
252
+ Line::from(vec![
253
+ Span::raw(" "),
254
+ Span::styled(
255
+ settings.mode.description(),
256
+ Style::default().fg(TEXT_DIM).italic(),
257
+ ),
258
+ ]),
259
+ Line::from(""),
260
+ // AI Provider Section
261
+ Line::from(vec![
262
+ Span::styled(
263
+ if is_provider_focused { "▶ " } else { " " },
264
+ Style::default().fg(BRAND_PURPLE).bold(),
265
+ ),
266
+ Span::styled("🤖 AI Provider", Style::default().fg(TEXT_PRIMARY).bold()),
267
+ ]),
268
+ Line::from(vec![
269
+ Span::styled(" Provider: ", Style::default().fg(TEXT_DIM)),
270
+ Span::styled(
271
+ &settings.ai_provider,
272
+ Style::default()
273
+ .fg(if is_provider_focused {
274
+ CYBER_CYAN
275
+ } else {
276
+ TEXT_PRIMARY
277
+ })
278
+ .bold(),
279
+ ),
280
+ ]),
281
+ Line::from(vec![
282
+ Span::styled(" Model: ", Style::default().fg(TEXT_DIM)),
283
+ Span::styled(
284
+ &settings.ai_model,
285
+ Style::default().fg(if is_provider_focused {
286
+ CYBER_CYAN
287
+ } else {
288
+ TEXT_PRIMARY
289
+ }),
290
+ ),
291
+ ]),
292
+ Line::from(vec![
293
+ Span::styled(" API Key: ", Style::default().fg(TEXT_DIM)),
294
+ Span::styled(
295
+ if settings.api_key_set {
296
+ "Configured ✓"
297
+ } else {
298
+ "Not set"
299
+ },
300
+ Style::default().fg(if settings.api_key_set {
301
+ NEON_GREEN
302
+ } else {
303
+ Color::Rgb(255, 69, 69)
304
+ }),
305
+ ),
306
+ ]),
307
+ Line::from(""),
308
+ // Gateway Section
309
+ Line::from(vec![
310
+ Span::styled(
311
+ if is_gateway_focused { "" } else { " " },
312
+ Style::default().fg(BRAND_PURPLE).bold(),
313
+ ),
314
+ Span::styled(
315
+ "🔗 Gateway Connection",
316
+ Style::default().fg(TEXT_PRIMARY).bold(),
317
+ ),
318
+ ]),
319
+ Line::from(vec![
320
+ Span::styled(" URL: ", Style::default().fg(TEXT_DIM)),
321
+ Span::styled(
322
+ &settings.gateway_url,
323
+ Style::default().fg(if is_gateway_focused {
324
+ CYBER_CYAN
325
+ } else {
326
+ TEXT_PRIMARY
327
+ }),
328
+ ),
329
+ ]),
330
+ Line::from(vec![
331
+ Span::styled(" Status: ", Style::default().fg(TEXT_DIM)),
332
+ Span::styled(
333
+ if settings.gateway_connected {
334
+ "Connected ✓"
335
+ } else {
336
+ "Disconnected"
337
+ },
338
+ Style::default().fg(if settings.gateway_connected {
339
+ NEON_GREEN
340
+ } else {
341
+ TEXT_MUTED
342
+ }),
343
+ ),
344
+ ]),
345
+ Line::from(""),
346
+ // Auto-update Section
347
+ Line::from(vec![
348
+ Span::styled(
349
+ if is_autoupdate_focused { " " } else { " " },
350
+ Style::default().fg(BRAND_PURPLE).bold(),
351
+ ),
352
+ Span::styled("🔄 Auto-Update", Style::default().fg(TEXT_PRIMARY).bold()),
353
+ ]),
354
+ Line::from(vec![
355
+ Span::raw(" "),
356
+ Span::styled(
357
+ format!(
358
+ "[{}] Enable automatic updates",
359
+ if settings.auto_update { "✓" } else { " " }
360
+ ),
361
+ Style::default().fg(if is_autoupdate_focused {
362
+ CYBER_CYAN
363
+ } else {
364
+ TEXT_PRIMARY
365
+ }),
366
+ ),
367
+ ]),
368
+ ];
369
+
370
+ let paragraph = Paragraph::new(text)
371
+ .wrap(Wrap { trim: false })
372
+ .alignment(Alignment::Left);
373
+
374
+ f.render_widget(paragraph, inner);
375
+ }
376
+
377
+ fn render_actions(f: &mut Frame, area: Rect, settings: &SettingsState) {
378
+ let block = Block::default()
379
+ .title(" ⌨️ Actions ")
380
+ .borders(Borders::ALL)
381
+ .border_style(Style::default().fg(TEXT_DIM))
382
+ .style(Style::default().bg(BG_PANEL));
383
+
384
+ let inner = block.inner(area);
385
+ f.render_widget(block, area);
386
+
387
+ let text = if settings.has_changes {
388
+ Line::from(vec![
389
+ Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
390
+ Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
391
+ Span::styled("Space", Style::default().fg(AMBER_WARN).bold()),
392
+ Span::styled(" Toggle │ ", Style::default().fg(TEXT_DIM)),
393
+ Span::styled("Enter", Style::default().fg(NEON_GREEN).bold()),
394
+ Span::styled(" Save │ ", Style::default().fg(TEXT_DIM)),
395
+ Span::styled("ESC", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
396
+ Span::styled(" Discard & Close", Style::default().fg(TEXT_DIM)),
397
+ ])
398
+ } else {
399
+ Line::from(vec![
400
+ Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
401
+ Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
402
+ Span::styled("Space", Style::default().fg(AMBER_WARN).bold()),
403
+ Span::styled(" Toggle │ ", Style::default().fg(TEXT_DIM)),
404
+ Span::styled("ESC", Style::default().fg(BRAND_PURPLE).bold()),
405
+ Span::styled(" Close", Style::default().fg(TEXT_DIM)),
406
+ ])
407
+ };
408
+
409
+ let paragraph = Paragraph::new(text)
410
+ .style(Style::default().fg(TEXT_PRIMARY))
411
+ .alignment(Alignment::Center);
412
+
413
+ f.render_widget(paragraph, inner);
414
+ }