grok-cli-acp 0.1.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.
Files changed (100) hide show
  1. package/.env.example +42 -0
  2. package/.github/workflows/ci.yml +30 -0
  3. package/.github/workflows/rust.yml +22 -0
  4. package/.grok/.env.example +85 -0
  5. package/.grok/COMPLETE_FIX_SUMMARY.md +466 -0
  6. package/.grok/ENV_CONFIG_GUIDE.md +173 -0
  7. package/.grok/QUICK_REFERENCE.md +180 -0
  8. package/.grok/README.md +104 -0
  9. package/.grok/TESTING_GUIDE.md +393 -0
  10. package/CHANGELOG.md +465 -0
  11. package/CODE_REVIEW_SUMMARY.md +414 -0
  12. package/COMPLETE_FIX_SUMMARY.md +415 -0
  13. package/CONFIGURATION.md +489 -0
  14. package/CONTEXT_FILES_GUIDE.md +419 -0
  15. package/CONTRIBUTING.md +55 -0
  16. package/CURSOR_POSITION_FIX.md +206 -0
  17. package/Cargo.toml +88 -0
  18. package/ERROR_HANDLING_REPORT.md +361 -0
  19. package/FINAL_FIX_SUMMARY.md +462 -0
  20. package/FIXES.md +37 -0
  21. package/FIXES_SUMMARY.md +87 -0
  22. package/GROK_API_MIGRATION_SUMMARY.md +111 -0
  23. package/LICENSE +22 -0
  24. package/MIGRATION_TO_GROK_API.md +223 -0
  25. package/README.md +504 -0
  26. package/REVIEW_COMPLETE.md +416 -0
  27. package/REVIEW_QUICK_REFERENCE.md +173 -0
  28. package/SECURITY.md +463 -0
  29. package/SECURITY_AUDIT.md +661 -0
  30. package/SETUP.md +287 -0
  31. package/TESTING_TOOLS.md +88 -0
  32. package/TESTING_TOOL_EXECUTION.md +239 -0
  33. package/TOOL_EXECUTION_FIX.md +491 -0
  34. package/VERIFICATION_CHECKLIST.md +419 -0
  35. package/docs/API.md +74 -0
  36. package/docs/CHAT_LOGGING.md +39 -0
  37. package/docs/CURSOR_FIX_DEMO.md +306 -0
  38. package/docs/ERROR_HANDLING_GUIDE.md +547 -0
  39. package/docs/FILE_OPERATIONS.md +449 -0
  40. package/docs/INTERACTIVE.md +401 -0
  41. package/docs/PROJECT_CREATION_GUIDE.md +570 -0
  42. package/docs/QUICKSTART.md +378 -0
  43. package/docs/QUICK_REFERENCE.md +691 -0
  44. package/docs/RELEASE_NOTES_0.1.2.md +240 -0
  45. package/docs/TOOLS.md +459 -0
  46. package/docs/TOOLS_QUICK_REFERENCE.md +210 -0
  47. package/docs/ZED_INTEGRATION.md +371 -0
  48. package/docs/extensions.md +464 -0
  49. package/docs/settings.md +293 -0
  50. package/examples/extensions/logging-hook/README.md +91 -0
  51. package/examples/extensions/logging-hook/extension.json +22 -0
  52. package/package.json +30 -0
  53. package/scripts/test_acp.py +252 -0
  54. package/scripts/test_acp.sh +143 -0
  55. package/scripts/test_acp_simple.sh +72 -0
  56. package/src/acp/mod.rs +741 -0
  57. package/src/acp/protocol.rs +323 -0
  58. package/src/acp/security.rs +298 -0
  59. package/src/acp/tools.rs +697 -0
  60. package/src/bin/banner_demo.rs +216 -0
  61. package/src/bin/docgen.rs +18 -0
  62. package/src/bin/installer.rs +217 -0
  63. package/src/cli/app.rs +310 -0
  64. package/src/cli/commands/acp.rs +721 -0
  65. package/src/cli/commands/chat.rs +485 -0
  66. package/src/cli/commands/code.rs +513 -0
  67. package/src/cli/commands/config.rs +394 -0
  68. package/src/cli/commands/health.rs +442 -0
  69. package/src/cli/commands/history.rs +421 -0
  70. package/src/cli/commands/mod.rs +14 -0
  71. package/src/cli/commands/settings.rs +1384 -0
  72. package/src/cli/mod.rs +166 -0
  73. package/src/config/mod.rs +2212 -0
  74. package/src/display/ascii_art.rs +139 -0
  75. package/src/display/banner.rs +289 -0
  76. package/src/display/components/input.rs +323 -0
  77. package/src/display/components/mod.rs +2 -0
  78. package/src/display/components/settings_list.rs +306 -0
  79. package/src/display/interactive.rs +1255 -0
  80. package/src/display/mod.rs +62 -0
  81. package/src/display/terminal.rs +42 -0
  82. package/src/display/tips.rs +316 -0
  83. package/src/grok_client_ext.rs +177 -0
  84. package/src/hooks/loader.rs +407 -0
  85. package/src/hooks/mod.rs +158 -0
  86. package/src/lib.rs +174 -0
  87. package/src/main.rs +65 -0
  88. package/src/mcp/client.rs +195 -0
  89. package/src/mcp/config.rs +20 -0
  90. package/src/mcp/mod.rs +6 -0
  91. package/src/mcp/protocol.rs +67 -0
  92. package/src/utils/auth.rs +41 -0
  93. package/src/utils/chat_logger.rs +568 -0
  94. package/src/utils/context.rs +390 -0
  95. package/src/utils/mod.rs +16 -0
  96. package/src/utils/network.rs +320 -0
  97. package/src/utils/rate_limiter.rs +166 -0
  98. package/src/utils/session.rs +73 -0
  99. package/src/utils/shell_permissions.rs +389 -0
  100. package/src/utils/telemetry.rs +41 -0
@@ -0,0 +1,323 @@
1
+ use anyhow::Result;
2
+ use crossterm::cursor::{self, MoveTo, MoveToColumn, MoveToNextLine, MoveToPreviousLine};
3
+ use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
4
+ use crossterm::style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor};
5
+ use crossterm::terminal::{self, Clear, ClearType};
6
+ use crossterm::{ExecutableCommand, QueueableCommand};
7
+ use regex::Regex;
8
+ use std::io::{stdout, Write};
9
+
10
+ pub struct Suggestion {
11
+ pub text: String,
12
+ pub description: String,
13
+ }
14
+
15
+ fn strip_ansi(s: &str) -> String {
16
+ let re = Regex::new(r"\x1b\[[0-9;]*m").unwrap();
17
+ re.replace_all(s, "").to_string()
18
+ }
19
+
20
+ pub fn read_input_with_suggestions(prompt: &str, suggestions: &[Suggestion]) -> Result<String> {
21
+ let mut buffer = String::new();
22
+ let mut cursor_pos = 0;
23
+ let mut suggestion_index: Option<usize> = None;
24
+ let mut scroll_offset = 0;
25
+ let mut horizontal_scroll = 0; // For scrolling long input text
26
+ let mut stdout = stdout();
27
+
28
+ // Box drawing characters
29
+ let box_top_left = '╭';
30
+ let box_top_right = '╮';
31
+ let box_bottom_left = '╰';
32
+ let box_bottom_right = '╯';
33
+ let box_horizontal = '─';
34
+ let box_vertical = '│';
35
+
36
+ terminal::enable_raw_mode()?;
37
+
38
+ let mut is_first_render = true;
39
+
40
+ loop {
41
+ // 0. Preparation
42
+ let (cols, _) = terminal::size()?;
43
+ let cols = cols as usize;
44
+
45
+ // Calculate visual widths
46
+ let prompt_stripped = strip_ansi(prompt);
47
+ let prompt_width = prompt_stripped.chars().count();
48
+ // Box content: "│ " + prompt + buffer + " │"
49
+ // Inner width needed: 1 (space) + prompt + buffer + 1 (space)
50
+ // But we want the box to extend to the right reasonably
51
+ let content_width = prompt_width + buffer.len() + 2;
52
+ let min_box_width = 60;
53
+ let box_width = (content_width + 2) // +2 for borders
54
+ .max(min_box_width)
55
+ .min(cols);
56
+
57
+ // 1. Clear Previous Frame
58
+ if !is_first_render {
59
+ // We assume cursor is at the input line (middle of box)
60
+ stdout.execute(MoveToPreviousLine(1))?;
61
+ stdout.execute(MoveToColumn(0))?;
62
+ stdout.execute(Clear(ClearType::FromCursorDown))?;
63
+ }
64
+ is_first_render = false;
65
+
66
+ // Capture start position (Top of box)
67
+ let (_, start_row) = cursor::position()?;
68
+
69
+ // 2. Render Box
70
+ // Top Border
71
+ let top_border_len = box_width.saturating_sub(2);
72
+ let top_border = format!(
73
+ "{}{}{}",
74
+ box_top_left,
75
+ box_horizontal.to_string().repeat(top_border_len),
76
+ box_top_right
77
+ );
78
+ stdout.execute(Print(top_border))?;
79
+ stdout.execute(MoveToNextLine(1))?;
80
+ stdout.execute(MoveToColumn(0))?;
81
+
82
+ // Middle Line (Prompt + Input)
83
+ // Calculate available space for input text
84
+ let available_width = box_width
85
+ .saturating_sub(4) // 2 for borders + 2 for spaces around content
86
+ .saturating_sub(prompt_width);
87
+
88
+ // Calculate horizontal scroll to keep cursor visible
89
+ if cursor_pos < horizontal_scroll {
90
+ horizontal_scroll = cursor_pos;
91
+ } else if cursor_pos >= horizontal_scroll + available_width {
92
+ horizontal_scroll = cursor_pos.saturating_sub(available_width) + 1;
93
+ }
94
+
95
+ // Get the visible portion of the buffer
96
+ let visible_buffer: String = buffer
97
+ .chars()
98
+ .skip(horizontal_scroll)
99
+ .take(available_width)
100
+ .collect();
101
+
102
+ stdout.execute(Print(format!("{} ", box_vertical)))?;
103
+ stdout.execute(Print(prompt))?;
104
+ stdout.execute(Print(&visible_buffer))?;
105
+
106
+ // Fill remaining space with spaces
107
+ let current_inner_len = 1 + prompt_width + visible_buffer.len(); // " " + prompt + visible_buffer
108
+ let remaining_space = box_width
109
+ .saturating_sub(2)
110
+ .saturating_sub(current_inner_len);
111
+ if remaining_space > 0 {
112
+ stdout.execute(Print(" ".repeat(remaining_space)))?;
113
+ }
114
+ stdout.execute(Print(format!("{}", box_vertical)))?;
115
+
116
+ stdout.execute(MoveToNextLine(1))?;
117
+ stdout.execute(MoveToColumn(0))?;
118
+
119
+ // Bottom Border
120
+ let bottom_border = format!(
121
+ "{}{}{}",
122
+ box_bottom_left,
123
+ box_horizontal.to_string().repeat(top_border_len),
124
+ box_bottom_right
125
+ );
126
+ stdout.execute(Print(bottom_border))?;
127
+ stdout.execute(MoveToNextLine(1))?;
128
+ stdout.execute(MoveToColumn(0))?;
129
+
130
+ // 3. Render Suggestions
131
+ let filtered_suggestions: Vec<&Suggestion> = if let Some(search) = buffer.strip_prefix('/')
132
+ {
133
+ suggestions
134
+ .iter()
135
+ .filter(|s| s.text.starts_with('/') && s.text[1..].starts_with(search))
136
+ .collect()
137
+ } else {
138
+ Vec::new()
139
+ };
140
+
141
+ if !filtered_suggestions.is_empty() {
142
+ // Update selection index wrap-around
143
+ if let Some(idx) = suggestion_index {
144
+ if idx >= filtered_suggestions.len() {
145
+ suggestion_index = Some(0);
146
+ }
147
+ } else {
148
+ suggestion_index = Some(0);
149
+ }
150
+
151
+ let idx = suggestion_index.unwrap();
152
+ let list_height = 8; // Max visible suggestions
153
+
154
+ // Adjust scroll
155
+ if idx < scroll_offset {
156
+ scroll_offset = idx;
157
+ } else if idx >= scroll_offset + list_height {
158
+ scroll_offset = idx - list_height + 1;
159
+ }
160
+ // Ensure scroll is valid
161
+ if scroll_offset + list_height > filtered_suggestions.len() {
162
+ scroll_offset = filtered_suggestions.len().saturating_sub(list_height);
163
+ }
164
+
165
+ // Render Up Arrow
166
+ if scroll_offset > 0 {
167
+ stdout.queue(MoveToColumn(2))?;
168
+ stdout.queue(SetForegroundColor(Color::DarkGrey))?;
169
+ stdout.queue(Print("▲"))?;
170
+ stdout.queue(ResetColor)?;
171
+ stdout.queue(MoveToNextLine(1))?;
172
+ }
173
+
174
+ // Render Items
175
+ let _end_index = (scroll_offset + list_height).min(filtered_suggestions.len());
176
+ for (i, suggestion) in filtered_suggestions
177
+ .iter()
178
+ .enumerate()
179
+ .skip(scroll_offset)
180
+ .take(list_height)
181
+ {
182
+ stdout.queue(MoveToColumn(2))?; // Indent
183
+ if Some(i) == suggestion_index {
184
+ stdout.queue(SetForegroundColor(Color::Black))?;
185
+ stdout.queue(SetBackgroundColor(Color::White))?;
186
+ } else {
187
+ stdout.queue(SetForegroundColor(Color::Cyan))?;
188
+ stdout.queue(SetBackgroundColor(Color::Reset))?;
189
+ }
190
+
191
+ let text = format!("{} - {}", suggestion.text, suggestion.description);
192
+ // Truncate if too long (simple check)
193
+ let max_text_len = cols.saturating_sub(4);
194
+ let text_preview = if text.len() > max_text_len {
195
+ format!("{}...", &text[..max_text_len.saturating_sub(3)])
196
+ } else {
197
+ text
198
+ };
199
+
200
+ stdout.queue(Print(text_preview))?;
201
+ stdout.queue(ResetColor)?;
202
+ stdout.queue(MoveToNextLine(1))?;
203
+ }
204
+
205
+ // Render Down Arrow
206
+ if scroll_offset + list_height < filtered_suggestions.len() {
207
+ stdout.queue(MoveToColumn(2))?;
208
+ stdout.queue(SetForegroundColor(Color::DarkGrey))?;
209
+ stdout.queue(Print("▼"))?;
210
+ stdout.queue(ResetColor)?;
211
+ }
212
+ } else {
213
+ suggestion_index = None;
214
+ scroll_offset = 0;
215
+ }
216
+
217
+ // 4. Position Cursor
218
+ // We want cursor at Middle Line of box.
219
+ // Start row was Top Line.
220
+ // Middle Line is start_row + 1.
221
+ let cursor_row = start_row + 1;
222
+ // Col: "│ " (2 chars) + prompt_width + (cursor_pos - horizontal_scroll)
223
+ let visible_cursor_pos = cursor_pos.saturating_sub(horizontal_scroll);
224
+ let cursor_col = 2 + prompt_width + visible_cursor_pos;
225
+
226
+ stdout.execute(MoveTo(cursor_col as u16, cursor_row))?;
227
+ stdout.flush()?;
228
+
229
+ // 5. Handle Input
230
+ if let Event::Key(KeyEvent {
231
+ code,
232
+ modifiers,
233
+ kind,
234
+ ..
235
+ }) = read()?
236
+ && kind == KeyEventKind::Press {
237
+ match code {
238
+ KeyCode::Enter => {
239
+ if let Some(idx) = suggestion_index
240
+ && !filtered_suggestions.is_empty() {
241
+ buffer = filtered_suggestions[idx].text.clone();
242
+ // Select and break
243
+ break;
244
+ }
245
+ if !buffer.is_empty() {
246
+ break;
247
+ }
248
+ }
249
+ KeyCode::Char(c) => {
250
+ if modifiers == KeyModifiers::CONTROL && c == 'c' {
251
+ buffer.clear();
252
+ break;
253
+ }
254
+ if cursor_pos < buffer.len() {
255
+ buffer.insert(cursor_pos, c);
256
+ } else {
257
+ buffer.push(c);
258
+ }
259
+ cursor_pos += 1;
260
+ }
261
+ KeyCode::Backspace => {
262
+ if cursor_pos > 0 {
263
+ buffer.remove(cursor_pos - 1);
264
+ cursor_pos -= 1;
265
+ }
266
+ }
267
+ KeyCode::Left => {
268
+ cursor_pos = cursor_pos.saturating_sub(1);
269
+ }
270
+ KeyCode::Right => {
271
+ if cursor_pos < buffer.len() {
272
+ cursor_pos += 1;
273
+ }
274
+ }
275
+ KeyCode::Up => {
276
+ if let Some(idx) = suggestion_index {
277
+ if idx > 0 {
278
+ suggestion_index = Some(idx - 1);
279
+ }
280
+ } else if !filtered_suggestions.is_empty() {
281
+ // If no selection but list exists, select last?
282
+ suggestion_index = Some(filtered_suggestions.len() - 1);
283
+ }
284
+ }
285
+ KeyCode::Down => {
286
+ if let Some(idx) = suggestion_index {
287
+ if idx < filtered_suggestions.len().saturating_sub(1) {
288
+ suggestion_index = Some(idx + 1);
289
+ }
290
+ } else if !filtered_suggestions.is_empty() {
291
+ suggestion_index = Some(0);
292
+ }
293
+ }
294
+ KeyCode::Tab => {
295
+ if let Some(idx) = suggestion_index
296
+ && !filtered_suggestions.is_empty() {
297
+ buffer = filtered_suggestions[idx].text.clone();
298
+ cursor_pos = buffer.len();
299
+ // Don't break, just fill
300
+ }
301
+ }
302
+ KeyCode::Esc => {
303
+ suggestion_index = None;
304
+ }
305
+ _ => {}
306
+ }
307
+ }
308
+ }
309
+
310
+ // Cleanup
311
+ // Move to Top of box and clear down to leave a clean state
312
+ if !is_first_render {
313
+ stdout.execute(MoveToPreviousLine(1))?;
314
+ stdout.execute(MoveToColumn(0))?;
315
+ stdout.execute(Clear(ClearType::FromCursorDown))?;
316
+ }
317
+
318
+ // Print final committed line (simple text, no box, for history)
319
+ stdout.execute(Print(format!("{}{}{}", prompt, buffer, '\n')))?;
320
+
321
+ terminal::disable_raw_mode()?;
322
+ Ok(buffer)
323
+ }
@@ -0,0 +1,2 @@
1
+ pub mod input;
2
+ pub mod settings_list;
@@ -0,0 +1,306 @@
1
+ use crate::cli::commands::settings::{SettingDefinition, SettingType};
2
+ use crate::config::Config;
3
+ use crate::display::terminal::{init_tui, restore_tui, Tui};
4
+ use anyhow::Result;
5
+ use crossterm::event::{self, Event, KeyCode, KeyEventKind};
6
+ use ratatui::{
7
+ layout::{Alignment, Constraint, Direction, Layout, Rect},
8
+ style::{Color, Modifier, Style},
9
+ text::{Line, Span},
10
+ widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph},
11
+ };
12
+ use std::time::Duration;
13
+
14
+ pub async fn run_settings_tui(config: &Config, settings: Vec<SettingDefinition>) -> Result<()> {
15
+ let mut terminal = init_tui()?;
16
+ let mut app = SettingsApp::new(config.clone(), settings);
17
+ let res = run_app(&mut terminal, &mut app).await;
18
+ restore_tui()?;
19
+ res
20
+ }
21
+
22
+ #[derive(PartialEq)]
23
+ enum EditMode {
24
+ View,
25
+ Input,
26
+ }
27
+
28
+ struct SettingsApp {
29
+ config: Config,
30
+ settings: Vec<SettingDefinition>,
31
+ state: ListState,
32
+ should_quit: bool,
33
+ mode: EditMode,
34
+ input_buffer: String,
35
+ status_message: Option<(String, Color)>,
36
+ }
37
+
38
+ impl SettingsApp {
39
+ fn new(config: Config, settings: Vec<SettingDefinition>) -> Self {
40
+ let mut state = ListState::default();
41
+ if !settings.is_empty() {
42
+ state.select(Some(0));
43
+ }
44
+ Self {
45
+ config,
46
+ settings,
47
+ state,
48
+ should_quit: false,
49
+ mode: EditMode::View,
50
+ input_buffer: String::new(),
51
+ status_message: None,
52
+ }
53
+ }
54
+
55
+ fn next(&mut self) {
56
+ let i = match self.state.selected() {
57
+ Some(i) => {
58
+ if i >= self.settings.len() - 1 {
59
+ 0
60
+ } else {
61
+ i + 1
62
+ }
63
+ }
64
+ None => 0,
65
+ };
66
+ self.state.select(Some(i));
67
+ }
68
+
69
+ fn previous(&mut self) {
70
+ let i = match self.state.selected() {
71
+ Some(i) => {
72
+ if i == 0 {
73
+ self.settings.len() - 1
74
+ } else {
75
+ i - 1
76
+ }
77
+ }
78
+ None => 0,
79
+ };
80
+ self.state.select(Some(i));
81
+ }
82
+
83
+ async fn handle_selection(&mut self) -> Result<()> {
84
+ if let Some(index) = self.state.selected() {
85
+ let setting = self.settings[index].clone();
86
+
87
+ match setting.setting_type {
88
+ SettingType::Boolean => {
89
+ let current_bool = setting.current_value == "true";
90
+ let new_value = (!current_bool).to_string();
91
+ self.update_setting(&setting.key, &new_value).await?;
92
+ }
93
+ SettingType::String | SettingType::Number => {
94
+ self.mode = EditMode::Input;
95
+ self.input_buffer = setting.current_value.clone();
96
+ }
97
+ _ => {
98
+ self.set_status("Editing this type is not supported in TUI", Color::Red);
99
+ }
100
+ }
101
+ }
102
+ Ok(())
103
+ }
104
+
105
+ async fn update_setting(&mut self, key: &str, value: &str) -> Result<()> {
106
+ let mut updated_config = self.config.clone();
107
+ if let Err(e) = updated_config.set_value(key, value) {
108
+ self.set_status(&format!("Error: {}", e), Color::Red);
109
+ return Ok(());
110
+ }
111
+
112
+ if let Err(e) = updated_config.validate() {
113
+ self.set_status(&format!("Validation Error: {}", e), Color::Red);
114
+ return Ok(());
115
+ }
116
+
117
+ if let Err(e) = updated_config.save(None).await {
118
+ self.set_status(&format!("Save Error: {}", e), Color::Red);
119
+ return Ok(());
120
+ }
121
+
122
+ self.config = updated_config;
123
+ self.settings = crate::cli::commands::settings::get_all_settings(&self.config);
124
+ self.set_status("Setting updated successfully", Color::Green);
125
+ Ok(())
126
+ }
127
+
128
+ fn set_status(&mut self, msg: &str, color: Color) {
129
+ self.status_message = Some((msg.to_string(), color));
130
+ }
131
+
132
+ async fn submit_input(&mut self) -> Result<()> {
133
+ if let Some(index) = self.state.selected() {
134
+ let setting = self.settings[index].clone();
135
+
136
+ // Validate number if needed
137
+ if let SettingType::Number = setting.setting_type
138
+ && self.input_buffer.parse::<f64>().is_err() {
139
+ self.set_status("Invalid number format", Color::Red);
140
+ return Ok(());
141
+ }
142
+
143
+ let value = self.input_buffer.clone();
144
+ self.update_setting(&setting.key, &value).await?;
145
+ }
146
+ self.mode = EditMode::View;
147
+ Ok(())
148
+ }
149
+ }
150
+
151
+ async fn run_app(terminal: &mut Tui, app: &mut SettingsApp) -> Result<()> {
152
+ loop {
153
+ terminal.draw(|f| ui(f, app))?;
154
+
155
+ if event::poll(Duration::from_millis(100))?
156
+ && let Event::Key(key) = event::read()?
157
+ && key.kind == KeyEventKind::Press {
158
+ match app.mode {
159
+ EditMode::View => match key.code {
160
+ KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
161
+ KeyCode::Down | KeyCode::Char('j') => app.next(),
162
+ KeyCode::Up | KeyCode::Char('k') => app.previous(),
163
+ KeyCode::Enter | KeyCode::Char(' ') => app.handle_selection().await?,
164
+ _ => {}
165
+ },
166
+ EditMode::Input => match key.code {
167
+ KeyCode::Enter => app.submit_input().await?,
168
+ KeyCode::Esc => {
169
+ app.mode = EditMode::View;
170
+ app.set_status("Cancelled", Color::Yellow);
171
+ }
172
+ KeyCode::Backspace => {
173
+ app.input_buffer.pop();
174
+ }
175
+ KeyCode::Char(c) => {
176
+ app.input_buffer.push(c);
177
+ }
178
+ _ => {}
179
+ },
180
+ }
181
+ }
182
+ if app.should_quit {
183
+ return Ok(());
184
+ }
185
+ }
186
+ }
187
+
188
+ fn ui(frame: &mut ratatui::Frame, app: &mut SettingsApp) {
189
+ let vertical = Layout::default()
190
+ .direction(Direction::Vertical)
191
+ .constraints([
192
+ Constraint::Length(3), // Header
193
+ Constraint::Min(0), // List
194
+ Constraint::Length(3), // Description
195
+ Constraint::Length(1), // Status/Footer
196
+ ])
197
+ .split(frame.area());
198
+
199
+ // Header
200
+ let header = Paragraph::new("Grok CLI Settings (TUI)")
201
+ .style(
202
+ Style::default()
203
+ .fg(Color::Cyan)
204
+ .add_modifier(Modifier::BOLD),
205
+ )
206
+ .block(Block::default().borders(Borders::ALL));
207
+ frame.render_widget(header, vertical[0]);
208
+
209
+ // List
210
+ let items: Vec<ListItem> = app
211
+ .settings
212
+ .iter()
213
+ .map(|setting| {
214
+ let value_style = if setting.current_value == "true" {
215
+ Style::default().fg(Color::Green)
216
+ } else if setting.current_value == "false" {
217
+ Style::default().fg(Color::Red)
218
+ } else {
219
+ Style::default().fg(Color::Yellow)
220
+ };
221
+
222
+ let content = Line::from(vec![
223
+ Span::styled(
224
+ format!("{:<30}", setting.label),
225
+ Style::default().add_modifier(Modifier::BOLD),
226
+ ),
227
+ Span::raw(" │ "),
228
+ Span::styled(setting.current_value.to_string(), value_style),
229
+ ]);
230
+
231
+ ListItem::new(content)
232
+ })
233
+ .collect();
234
+
235
+ let list = List::new(items)
236
+ .block(Block::default().borders(Borders::ALL).title("Settings"))
237
+ .highlight_style(
238
+ Style::default()
239
+ .bg(Color::DarkGray)
240
+ .add_modifier(Modifier::BOLD),
241
+ )
242
+ .highlight_symbol("> ");
243
+
244
+ frame.render_stateful_widget(list, vertical[1], &mut app.state);
245
+
246
+ // Description
247
+ let description = if let Some(index) = app.state.selected() {
248
+ &app.settings[index].description
249
+ } else {
250
+ "Select a setting to edit"
251
+ };
252
+
253
+ let desc_widget = Paragraph::new(description.to_string())
254
+ .style(Style::default().fg(Color::Gray))
255
+ .block(Block::default().borders(Borders::ALL).title("Description"));
256
+ frame.render_widget(desc_widget, vertical[2]);
257
+
258
+ // Status/Help
259
+ let status_text = if let Some((msg, color)) = &app.status_message {
260
+ Span::styled(msg, Style::default().fg(*color))
261
+ } else {
262
+ Span::styled(
263
+ "Press 'q' to quit, Enter to edit/toggle",
264
+ Style::default().fg(Color::DarkGray),
265
+ )
266
+ };
267
+ frame.render_widget(Paragraph::new(status_text), vertical[3]);
268
+
269
+ // Input Popup
270
+ if app.mode == EditMode::Input {
271
+ let block = Block::default()
272
+ .title("Edit Value")
273
+ .borders(Borders::ALL)
274
+ .style(Style::default().bg(Color::Blue));
275
+
276
+ let area = centered_rect(60, 20, frame.area());
277
+ frame.render_widget(Clear, area); // Clear background
278
+
279
+ let input = Paragraph::new(app.input_buffer.clone())
280
+ .style(Style::default().fg(Color::White))
281
+ .block(block);
282
+
283
+ frame.render_widget(input, area);
284
+ }
285
+ }
286
+
287
+ /// Helper function to center a rect
288
+ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
289
+ let popup_layout = Layout::default()
290
+ .direction(Direction::Vertical)
291
+ .constraints([
292
+ Constraint::Percentage((100 - percent_y) / 2),
293
+ Constraint::Percentage(percent_y),
294
+ Constraint::Percentage((100 - percent_y) / 2),
295
+ ])
296
+ .split(r);
297
+
298
+ Layout::default()
299
+ .direction(Direction::Horizontal)
300
+ .constraints([
301
+ Constraint::Percentage((100 - percent_x) / 2),
302
+ Constraint::Percentage(percent_x),
303
+ Constraint::Percentage((100 - percent_x) / 2),
304
+ ])
305
+ .split(popup_layout[1])[1]
306
+ }