From 46eca64b7e9214ad3e8ae688b964f9bff7f7e6fc Mon Sep 17 00:00:00 2001 From: curquiza Date: Sun, 3 May 2026 12:55:01 +0200 Subject: [PATCH 1/6] feat: interactive prompts for msc project add When --url is omitted, prompt the user for the URL and API key interactively. An empty API key means no key is stored. Co-Authored-By: Claude Sonnet 4.6 --- src/commands/project.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/commands/project.rs b/src/commands/project.rs index 441dbc4..79da85a 100644 --- a/src/commands/project.rs +++ b/src/commands/project.rs @@ -1,5 +1,6 @@ use anyhow::Result; use clap::Subcommand; +use dialoguer::{Input, Password}; use crate::config::{Config, Project}; @@ -9,7 +10,7 @@ pub enum ProjectCommand { Add { name: String, #[arg(long)] - url: String, + url: Option, #[arg(long)] api_key: Option, }, @@ -26,11 +27,27 @@ pub enum ProjectCommand { pub async fn run(cmd: &ProjectCommand) -> Result<()> { match cmd { ProjectCommand::Add { name, url, api_key } => { + let resolved_url = match url { + Some(u) => u.clone(), + None => Input::::new() + .with_prompt("URL") + .interact_text()?, + }; + let resolved_key = match api_key { + Some(k) => Some(k.clone()), + None => { + let key: String = Password::new() + .with_prompt("API key (leave empty for none)") + .allow_empty_password(true) + .interact()?; + if key.is_empty() { None } else { Some(key) } + } + }; let mut config = Config::load()?; let project = Project { r#type: None, - url: url.clone(), - api_key: api_key.clone(), + url: resolved_url, + api_key: resolved_key, data_dir: None, binary: None, }; From 8abb7e8409bb762296527e5c313ac2d3527ea928 Mon Sep 17 00:00:00 2001 From: curquiza Date: Sun, 3 May 2026 12:57:09 +0200 Subject: [PATCH 2/6] feat: show api key presence in project list Display [api key set] next to projects that have an api key configured, without revealing the key value. Co-Authored-By: Claude Sonnet 4.6 --- src/commands/project.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/project.rs b/src/commands/project.rs index 79da85a..e166427 100644 --- a/src/commands/project.rs +++ b/src/commands/project.rs @@ -67,7 +67,8 @@ pub async fn run(cmd: &ProjectCommand) -> Result<()> { } else { "" }; - println!(" {}{} — {}", name, marker, project.url); + let key_info = if project.api_key.is_some() { " [api key set]" } else { "" }; + println!(" {}{} — {}{}", name, marker, project.url, key_info); } } ProjectCommand::Use { name } => { From 87356322fc0a1b5e57f9128312a7e9cdaf23f3d8 Mon Sep 17 00:00:00 2001 From: curquiza Date: Sun, 3 May 2026 13:00:19 +0200 Subject: [PATCH 3/6] feat: add msc project update command Interactive command to update an existing project's URL and API key. Current URL is shown as hint but input starts empty. Empty API key removes the existing key. Co-Authored-By: Claude Sonnet 4.6 --- src/commands/project.rs | 24 ++++++++++++++++++++++++ src/config.rs | 8 ++++++++ 2 files changed, 32 insertions(+) diff --git a/src/commands/project.rs b/src/commands/project.rs index e166427..f9d0466 100644 --- a/src/commands/project.rs +++ b/src/commands/project.rs @@ -20,6 +20,8 @@ pub enum ProjectCommand { List, /// Set the default project Use { name: String }, + /// Update an existing project + Update { name: String }, /// Show the current default project Current, } @@ -76,6 +78,28 @@ pub async fn run(cmd: &ProjectCommand) -> Result<()> { config.set_default(name)?; println!("Default project set to '{}'.", name); } + ProjectCommand::Update { name } => { + let config = Config::load()?; + let (_, existing) = config.get_project(Some(name))?; + let url = Input::::new() + .with_prompt(format!("URL (current: {})", existing.url)) + .interact_text()?; + let key: String = Password::new() + .with_prompt("API key (leave empty to remove)") + .allow_empty_password(true) + .interact()?; + let api_key = if key.is_empty() { None } else { Some(key) }; + let project = Project { + r#type: existing.r#type.clone(), + url, + api_key, + data_dir: existing.data_dir.clone(), + binary: existing.binary.clone(), + }; + let mut config = Config::load()?; + config.update_project(name, project)?; + println!("Project '{}' updated.", name); + } ProjectCommand::Current => { let config = Config::load()?; let (name, project) = config.get_project(None)?; diff --git a/src/config.rs b/src/config.rs index a6297d5..0338850 100644 --- a/src/config.rs +++ b/src/config.rs @@ -124,6 +124,14 @@ impl Config { self.save() } + pub fn update_project(&mut self, name: &str, project: Project) -> Result<()> { + if !self.projects.contains_key(name) { + bail!("Project '{}' not found.", name); + } + self.projects.insert(name.to_string(), project); + self.save() + } + pub fn remove_project(&mut self, name: &str) -> Result<()> { if name == self.default { bail!( From fe0c532f43a0904aa5fe589a3703b9b8800cd340 Mon Sep 17 00:00:00 2001 From: curquiza Date: Sun, 3 May 2026 13:02:05 +0200 Subject: [PATCH 4/6] style: cargo fmt Co-Authored-By: Claude Sonnet 4.6 --- src/commands/project.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/commands/project.rs b/src/commands/project.rs index f9d0466..dab31ea 100644 --- a/src/commands/project.rs +++ b/src/commands/project.rs @@ -31,9 +31,7 @@ pub async fn run(cmd: &ProjectCommand) -> Result<()> { ProjectCommand::Add { name, url, api_key } => { let resolved_url = match url { Some(u) => u.clone(), - None => Input::::new() - .with_prompt("URL") - .interact_text()?, + None => Input::::new().with_prompt("URL").interact_text()?, }; let resolved_key = match api_key { Some(k) => Some(k.clone()), @@ -69,7 +67,11 @@ pub async fn run(cmd: &ProjectCommand) -> Result<()> { } else { "" }; - let key_info = if project.api_key.is_some() { " [api key set]" } else { "" }; + let key_info = if project.api_key.is_some() { + " [api key set]" + } else { + "" + }; println!(" {}{} — {}{}", name, marker, project.url, key_info); } } From dd792eed777df8572e9a08cd066658a64cc07148 Mon Sep 17 00:00:00 2001 From: curquiza Date: Sun, 3 May 2026 13:10:53 +0200 Subject: [PATCH 5/6] fix: resolve all clippy collapsible_match warnings in TUI files Co-Authored-By: Claude Sonnet 4.6 --- src/tui/chat.rs | 50 ++++++++++++---------- src/tui/search.rs | 14 +++--- src/tui/settings.rs | 101 +++++++++++++++++++++----------------------- 3 files changed, 80 insertions(+), 85 deletions(-) diff --git a/src/tui/chat.rs b/src/tui/chat.rs index dbbaf8c..033f94a 100644 --- a/src/tui/chat.rs +++ b/src/tui/chat.rs @@ -426,8 +426,8 @@ pub async fn run_interactive_chat( state.show_sources = !state.show_sources; } KeyCode::Esc => break, - KeyCode::Enter if !state.loading => { - if !state.input.is_empty() { + KeyCode::Enter if !state.loading && !state.input.is_empty() => { + { // Push to history state.input_history.push(state.input.clone()); state.history_pos = state.input_history.len(); @@ -576,33 +576,37 @@ pub async fn run_interactive_chat( state.auto_scroll = true; } } + KeyCode::Up + if !state.loading + && !state.input_history.is_empty() + && state.history_pos > 0 => + { + // First time pressing up: stash current input + if state.history_pos == state.input_history.len() { + state.input_stash = state.input.clone(); + } + state.history_pos -= 1; + state.input = state.input_history[state.history_pos].clone(); + } + KeyCode::Up if !state.loading => {} + KeyCode::Down + if !state.loading && state.history_pos < state.input_history.len() => + { + state.history_pos += 1; + if state.history_pos == state.input_history.len() { + // Restore stashed input + state.input = state.input_stash.clone(); + } else { + state.input = state.input_history[state.history_pos].clone(); + } + } + KeyCode::Down if !state.loading => {} KeyCode::Char(c) if !state.loading => { state.input.push(c); } KeyCode::Backspace if !state.loading => { state.input.pop(); } - KeyCode::Up if !state.loading => { - if !state.input_history.is_empty() && state.history_pos > 0 { - // First time pressing up: stash current input - if state.history_pos == state.input_history.len() { - state.input_stash = state.input.clone(); - } - state.history_pos -= 1; - state.input = state.input_history[state.history_pos].clone(); - } - } - KeyCode::Down if !state.loading => { - if state.history_pos < state.input_history.len() { - state.history_pos += 1; - if state.history_pos == state.input_history.len() { - // Restore stashed input - state.input = state.input_stash.clone(); - } else { - state.input = state.input_history[state.history_pos].clone(); - } - } - } KeyCode::Up => { state.auto_scroll = false; state.scroll = state.scroll.saturating_sub(1); diff --git a/src/tui/search.rs b/src/tui/search.rs index ae27a38..3f2ed59 100644 --- a/src/tui/search.rs +++ b/src/tui/search.rs @@ -160,16 +160,14 @@ pub async fn run_interactive_search(client: MeiliClient, uid: &str) -> Result<() needs_search = true; state.last_search = Instant::now(); } - KeyCode::Up => { - if state.selected > 0 { - state.selected -= 1; - } + KeyCode::Up if state.selected > 0 => { + state.selected -= 1; } - KeyCode::Down => { - if state.selected + 1 < state.results.len() { - state.selected += 1; - } + KeyCode::Up => {} + KeyCode::Down if state.selected + 1 < state.results.len() => { + state.selected += 1; } + KeyCode::Down => {} KeyCode::Enter => { if state.expanded.is_some() { state.expanded = None; diff --git a/src/tui/settings.rs b/src/tui/settings.rs index d3310ff..0b8299b 100644 --- a/src/tui/settings.rs +++ b/src/tui/settings.rs @@ -903,33 +903,34 @@ fn handle_editor_input(key: event::KeyEvent, state: &mut State) -> Action { KeyCode::Esc => { exit_editor(state); } - KeyCode::Up if shift && *ordered => { - // Reorder: swap with previous checked item - if items[*cursor].checked && *cursor > 0 && items[*cursor - 1].checked { - items.swap(*cursor, *cursor - 1); - *cursor -= 1; - } + KeyCode::Up + if shift && *ordered + && items[*cursor].checked + && *cursor > 0 + && items[*cursor - 1].checked => + { + items.swap(*cursor, *cursor - 1); + *cursor -= 1; } - KeyCode::Down if shift && *ordered => { - // Reorder: swap with next checked item - if items[*cursor].checked + KeyCode::Up if shift && *ordered => {} + KeyCode::Down + if shift && *ordered + && items[*cursor].checked && *cursor + 1 < items.len() - && items[*cursor + 1].checked - { - items.swap(*cursor, *cursor + 1); - *cursor += 1; - } + && items[*cursor + 1].checked => + { + items.swap(*cursor, *cursor + 1); + *cursor += 1; } - KeyCode::Up => { - if *cursor > 0 { - *cursor -= 1; - } + KeyCode::Down if shift && *ordered => {} + KeyCode::Up if *cursor > 0 => { + *cursor -= 1; } - KeyCode::Down => { - if *cursor + 1 < items.len() { - *cursor += 1; - } + KeyCode::Up => {} + KeyCode::Down if *cursor + 1 < items.len() => { + *cursor += 1; } + KeyCode::Down => {} KeyCode::Char(' ') => { if *ordered { let name = items[*cursor].name.clone(); @@ -1014,37 +1015,31 @@ fn handle_editor_input(key: event::KeyEvent, state: &mut State) -> Action { KeyCode::Esc => { exit_editor(state); } - KeyCode::Up if shift => { - if *cursor > 0 { - items.swap(*cursor, *cursor - 1); - *cursor -= 1; - } + KeyCode::Up if shift && *cursor > 0 => { + items.swap(*cursor, *cursor - 1); + *cursor -= 1; } - KeyCode::Down if shift => { - if *cursor + 1 < items.len() { - items.swap(*cursor, *cursor + 1); - *cursor += 1; - } + KeyCode::Up if shift => {} + KeyCode::Down if shift && *cursor + 1 < items.len() => { + items.swap(*cursor, *cursor + 1); + *cursor += 1; } - KeyCode::Up => { - if *cursor > 0 { - *cursor -= 1; - } + KeyCode::Down if shift => {} + KeyCode::Up if *cursor > 0 => { + *cursor -= 1; } - KeyCode::Down => { - if *cursor + 1 < items.len() { - *cursor += 1; - } + KeyCode::Up => {} + KeyCode::Down if *cursor + 1 < items.len() => { + *cursor += 1; } + KeyCode::Down => {} KeyCode::Char('a') => { *adding = true; } - KeyCode::Char('d') | KeyCode::Delete => { - if !items.is_empty() { - items.remove(*cursor); - if *cursor >= items.len() && !items.is_empty() { - *cursor = items.len() - 1; - } + KeyCode::Char('d') | KeyCode::Delete if !items.is_empty() => { + items.remove(*cursor); + if *cursor >= items.len() && !items.is_empty() { + *cursor = items.len() - 1; } } _ => {} @@ -1057,16 +1052,14 @@ fn handle_editor_input(key: event::KeyEvent, state: &mut State) -> Action { KeyCode::Esc => { exit_editor(state); } - KeyCode::Up => { - if *cursor > 0 { - *cursor -= 1; - } + KeyCode::Up if *cursor > 0 => { + *cursor -= 1; } - KeyCode::Down => { - if *cursor + 1 < options.len() { - *cursor += 1; - } + KeyCode::Up => {} + KeyCode::Down if *cursor + 1 < options.len() => { + *cursor += 1; } + KeyCode::Down => {} KeyCode::Enter | KeyCode::Char(' ') => { exit_editor(state); } From b3b3045b5493a334f9a00dcdc875649cfa8e9247 Mon Sep 17 00:00:00 2001 From: curquiza Date: Sun, 3 May 2026 13:14:55 +0200 Subject: [PATCH 6/6] style: cargo fmt Co-Authored-By: Claude Sonnet 4.6 --- src/tui/settings.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tui/settings.rs b/src/tui/settings.rs index 0b8299b..dfa9fec 100644 --- a/src/tui/settings.rs +++ b/src/tui/settings.rs @@ -904,7 +904,8 @@ fn handle_editor_input(key: event::KeyEvent, state: &mut State) -> Action { exit_editor(state); } KeyCode::Up - if shift && *ordered + if shift + && *ordered && items[*cursor].checked && *cursor > 0 && items[*cursor - 1].checked => @@ -914,7 +915,8 @@ fn handle_editor_input(key: event::KeyEvent, state: &mut State) -> Action { } KeyCode::Up if shift && *ordered => {} KeyCode::Down - if shift && *ordered + if shift + && *ordered && items[*cursor].checked && *cursor + 1 < items.len() && items[*cursor + 1].checked =>