4runr-os 2.4.0 → 2.4.2
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.
- package/mk3-tui/src/app.rs +111 -47
- package/mk3-tui/src/main.rs +42 -7
- package/mk3-tui/src/screens/mod.rs +3 -1
- package/mk3-tui/src/storage/cache.rs +213 -213
- package/mk3-tui/src/storage/mod.rs +6 -6
- package/mk3-tui/src/ui/agent_builder.rs +921 -921
- package/mk3-tui/src/ui/agent_list.rs +247 -0
- package/mk3-tui/src/ui/help.rs +353 -0
- package/mk3-tui/src/ui/layout.rs +3 -3
- package/mk3-tui/src/ui/mod.rs +2 -0
- package/mk3-tui/src/ui/run_manager.rs +676 -676
- package/mk3-tui/src/ui/settings.rs +362 -362
- package/mk3-tui/src/websocket.rs +303 -303
- package/package.json +2 -1
|
@@ -1,362 +1,362 @@
|
|
|
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
|
+
/// 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
|
+
}
|