|
| 1 | +// ABOUTME: Implements the /experiment slash command for toggling experimental features |
| 2 | +// ABOUTME: Provides interactive selection interface similar to /model command |
| 3 | + |
| 4 | +use clap::Args; |
| 5 | +use crossterm::style::{ |
| 6 | + self, |
| 7 | + Color, |
| 8 | +}; |
| 9 | +use crossterm::{ |
| 10 | + execute, |
| 11 | + queue, |
| 12 | +}; |
| 13 | +use dialoguer::Select; |
| 14 | + |
| 15 | +use crate::cli::chat::{ |
| 16 | + ChatError, |
| 17 | + ChatSession, |
| 18 | + ChatState, |
| 19 | +}; |
| 20 | +use crate::database::settings::Setting; |
| 21 | +use crate::os::Os; |
| 22 | + |
| 23 | +/// Represents an experimental feature that can be toggled |
| 24 | +#[derive(Debug, Clone)] |
| 25 | +struct Experiment { |
| 26 | + name: &'static str, |
| 27 | + description: &'static str, |
| 28 | + setting_key: Setting, |
| 29 | +} |
| 30 | + |
| 31 | +static AVAILABLE_EXPERIMENTS: &[Experiment] = &[ |
| 32 | + Experiment { |
| 33 | + name: "Knowledge", |
| 34 | + description: "Enables persistent context storage and retrieval across chat sessions (/knowledge)", |
| 35 | + setting_key: Setting::EnabledKnowledge, |
| 36 | + }, |
| 37 | + Experiment { |
| 38 | + name: "Thinking", |
| 39 | + description: "Enables complex reasoning with step-by-step thought processes", |
| 40 | + setting_key: Setting::EnabledThinking, |
| 41 | + }, |
| 42 | +]; |
| 43 | + |
| 44 | +#[derive(Debug, PartialEq, Args)] |
| 45 | +pub struct ExperimentArgs; |
| 46 | +impl ExperimentArgs { |
| 47 | + pub async fn execute(self, os: &mut Os, session: &mut ChatSession) -> Result<ChatState, ChatError> { |
| 48 | + Ok(select_experiment(os, session).await?.unwrap_or(ChatState::PromptUser { |
| 49 | + skip_printing_tools: false, |
| 50 | + })) |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +async fn select_experiment(os: &mut Os, session: &mut ChatSession) -> Result<Option<ChatState>, ChatError> { |
| 55 | + // Get current experiment status |
| 56 | + let mut experiment_labels = Vec::new(); |
| 57 | + let mut current_states = Vec::new(); |
| 58 | + |
| 59 | + for experiment in AVAILABLE_EXPERIMENTS { |
| 60 | + let is_enabled = os.database.settings.get_bool(experiment.setting_key).unwrap_or(false); |
| 61 | + |
| 62 | + current_states.push(is_enabled); |
| 63 | + // Create clean single-line format: "Knowledge [ON] - Description" |
| 64 | + let status_indicator = if is_enabled { |
| 65 | + style::Stylize::green("[ON] ") |
| 66 | + } else { |
| 67 | + style::Stylize::grey("[OFF]") |
| 68 | + }; |
| 69 | + let label = format!( |
| 70 | + "{:<18} {} - {}", |
| 71 | + experiment.name, |
| 72 | + status_indicator, |
| 73 | + style::Stylize::dark_grey(experiment.description) |
| 74 | + ); |
| 75 | + experiment_labels.push(label); |
| 76 | + } |
| 77 | + |
| 78 | + experiment_labels.push(String::new()); |
| 79 | + experiment_labels.push(format!( |
| 80 | + "{}", |
| 81 | + style::Stylize::white("⚠ Experimental features may be changed or removed at any time") |
| 82 | + )); |
| 83 | + |
| 84 | + let selection: Option<_> = match Select::with_theme(&crate::util::dialoguer_theme()) |
| 85 | + .with_prompt("Select an experiment to toggle") |
| 86 | + .items(&experiment_labels) |
| 87 | + .default(0) |
| 88 | + .interact_on_opt(&dialoguer::console::Term::stdout()) |
| 89 | + { |
| 90 | + Ok(sel) => { |
| 91 | + let _ = crossterm::execute!( |
| 92 | + std::io::stdout(), |
| 93 | + crossterm::style::SetForegroundColor(crossterm::style::Color::Magenta) |
| 94 | + ); |
| 95 | + sel |
| 96 | + }, |
| 97 | + // Ctrl‑C -> Err(Interrupted) |
| 98 | + Err(dialoguer::Error::IO(ref e)) if e.kind() == std::io::ErrorKind::Interrupted => return Ok(None), |
| 99 | + Err(e) => return Err(ChatError::Custom(format!("Failed to choose experiment: {e}").into())), |
| 100 | + }; |
| 101 | + |
| 102 | + queue!(session.stderr, style::ResetColor)?; |
| 103 | + |
| 104 | + if let Some(index) = selection { |
| 105 | + // Clear the dialoguer selection line to avoid showing old status |
| 106 | + queue!( |
| 107 | + session.stderr, |
| 108 | + crossterm::cursor::MoveUp(1), |
| 109 | + crossterm::terminal::Clear(crossterm::terminal::ClearType::CurrentLine), |
| 110 | + )?; |
| 111 | + |
| 112 | + // Skip if user selected disclaimer or empty line |
| 113 | + if index >= AVAILABLE_EXPERIMENTS.len() { |
| 114 | + return Ok(Some(ChatState::PromptUser { |
| 115 | + skip_printing_tools: false, |
| 116 | + })); |
| 117 | + } |
| 118 | + |
| 119 | + let experiment = &AVAILABLE_EXPERIMENTS[index]; |
| 120 | + let current_state = current_states[index]; |
| 121 | + let new_state = !current_state; |
| 122 | + |
| 123 | + // Update the setting |
| 124 | + os.database |
| 125 | + .settings |
| 126 | + .set(experiment.setting_key, new_state) |
| 127 | + .await |
| 128 | + .map_err(|e| ChatError::Custom(format!("Failed to update experiment setting: {e}").into()))?; |
| 129 | + |
| 130 | + // Reload tools to reflect the experiment change |
| 131 | + let _ = session |
| 132 | + .conversation |
| 133 | + .tool_manager |
| 134 | + .load_tools(os, &mut session.stderr) |
| 135 | + .await; |
| 136 | + |
| 137 | + let status_text = if new_state { "enabled" } else { "disabled" }; |
| 138 | + |
| 139 | + queue!( |
| 140 | + session.stderr, |
| 141 | + style::Print("\n"), |
| 142 | + style::SetForegroundColor(Color::Green), |
| 143 | + style::Print(format!(" {} experiment {}\n\n", experiment.name, status_text)), |
| 144 | + style::ResetColor, |
| 145 | + style::SetForegroundColor(Color::Reset), |
| 146 | + style::SetBackgroundColor(Color::Reset), |
| 147 | + )?; |
| 148 | + } |
| 149 | + |
| 150 | + execute!(session.stderr, style::ResetColor)?; |
| 151 | + |
| 152 | + Ok(Some(ChatState::PromptUser { |
| 153 | + skip_printing_tools: false, |
| 154 | + })) |
| 155 | +} |
0 commit comments