feat: interactive /settings modal and quick /p persona command with fuzzy prefix matching
Some checks are pending
CI / Test (macos-latest) (push) Waiting to run
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Test (windows-latest) (push) Waiting to run
Some checks are pending
CI / Test (macos-latest) (push) Waiting to run
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Test (windows-latest) (push) Waiting to run
This commit is contained in:
parent
e41a641c94
commit
b1aac41a56
3 changed files with 349 additions and 23 deletions
|
|
@ -41,6 +41,7 @@ pub enum TuiMode {
|
||||||
PipelineSelect,
|
PipelineSelect,
|
||||||
SessionSelect,
|
SessionSelect,
|
||||||
ThemeSelect,
|
ThemeSelect,
|
||||||
|
Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -110,6 +111,10 @@ pub const ALL_COMMANDS: &[CommandSuggestion] = &[
|
||||||
name: "/persona",
|
name: "/persona",
|
||||||
description: "Select active persona or pass name",
|
description: "Select active persona or pass name",
|
||||||
},
|
},
|
||||||
|
CommandSuggestion {
|
||||||
|
name: "/p",
|
||||||
|
description: "Quick switch persona (e.g. /p coder, /p next)",
|
||||||
|
},
|
||||||
CommandSuggestion {
|
CommandSuggestion {
|
||||||
name: "/pipeline",
|
name: "/pipeline",
|
||||||
description: "Select active pipeline or pass name",
|
description: "Select active pipeline or pass name",
|
||||||
|
|
@ -267,6 +272,9 @@ pub struct AppState {
|
||||||
// Options
|
// Options
|
||||||
pub allow_stealth: bool,
|
pub allow_stealth: bool,
|
||||||
pub escalate: bool,
|
pub escalate: bool,
|
||||||
|
pub complexity_threshold: u8,
|
||||||
|
pub tool_execution_enabled: bool,
|
||||||
|
pub selected_setting_idx: usize,
|
||||||
|
|
||||||
// UX & Theme
|
// UX & Theme
|
||||||
pub expand_tool_outputs: bool,
|
pub expand_tool_outputs: bool,
|
||||||
|
|
@ -283,6 +291,7 @@ impl AppState {
|
||||||
daily_limit: usize,
|
daily_limit: usize,
|
||||||
config_dir: PathBuf,
|
config_dir: PathBuf,
|
||||||
initial_theme: String,
|
initial_theme: String,
|
||||||
|
complexity_threshold: u8,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut persona_items = Vec::new();
|
let mut persona_items = Vec::new();
|
||||||
// Item 0 is always the smart Auto-Router
|
// Item 0 is always the smart Auto-Router
|
||||||
|
|
@ -334,6 +343,9 @@ impl AppState {
|
||||||
tokens_out: 0,
|
tokens_out: 0,
|
||||||
allow_stealth: false,
|
allow_stealth: false,
|
||||||
escalate: false,
|
escalate: false,
|
||||||
|
complexity_threshold,
|
||||||
|
tool_execution_enabled: true,
|
||||||
|
selected_setting_idx: 0,
|
||||||
expand_tool_outputs: false,
|
expand_tool_outputs: false,
|
||||||
current_theme: initial_theme,
|
current_theme: initial_theme,
|
||||||
selected_suggestion_idx: 0,
|
selected_suggestion_idx: 0,
|
||||||
|
|
@ -388,6 +400,59 @@ impl AppState {
|
||||||
self.selected_persona_idx == 0
|
self.selected_persona_idx == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cycle_persona(&mut self) {
|
||||||
|
if !self.persona_items.is_empty() {
|
||||||
|
self.selected_persona_idx = (self.selected_persona_idx + 1) % self.persona_items.len();
|
||||||
|
self.active_pipeline_for_prompt = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_persona_by_name(&mut self, query: &str) -> Result<String, String> {
|
||||||
|
let q = query.trim().to_lowercase();
|
||||||
|
if q.is_empty() {
|
||||||
|
return Err("Persona name cannot be empty".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if let Some(pos) = self
|
||||||
|
.persona_items
|
||||||
|
.iter()
|
||||||
|
.position(|p| p.name.to_lowercase() == q)
|
||||||
|
{
|
||||||
|
self.selected_persona_idx = pos;
|
||||||
|
self.active_pipeline_for_prompt = None;
|
||||||
|
return Ok(self.current_persona_name().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix match
|
||||||
|
if let Some(pos) = self
|
||||||
|
.persona_items
|
||||||
|
.iter()
|
||||||
|
.position(|p| p.name.to_lowercase().starts_with(&q))
|
||||||
|
{
|
||||||
|
self.selected_persona_idx = pos;
|
||||||
|
self.active_pipeline_for_prompt = None;
|
||||||
|
return Ok(self.current_persona_name().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substring match
|
||||||
|
if let Some(pos) = self
|
||||||
|
.persona_items
|
||||||
|
.iter()
|
||||||
|
.position(|p| p.name.to_lowercase().contains(&q))
|
||||||
|
{
|
||||||
|
self.selected_persona_idx = pos;
|
||||||
|
self.active_pipeline_for_prompt = None;
|
||||||
|
return Ok(self.current_persona_name().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let names: Vec<String> = self.persona_items.iter().map(|p| p.name.clone()).collect();
|
||||||
|
Err(format!(
|
||||||
|
"No persona matching '{query}'. Available: {}",
|
||||||
|
names.join(", ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn log_working_event(&mut self, text: &str) {
|
pub fn log_working_event(&mut self, text: &str) {
|
||||||
let now = chrono::Local::now().format("%H:%M:%S").to_string();
|
let now = chrono::Local::now().format("%H:%M:%S").to_string();
|
||||||
self.workings.events.push(format!("{now} {text}"));
|
self.workings.events.push(format!("{now} {text}"));
|
||||||
|
|
@ -772,6 +837,7 @@ mod tests {
|
||||||
50,
|
50,
|
||||||
PathBuf::from("/tmp/brokecrew_test"),
|
PathBuf::from("/tmp/brokecrew_test"),
|
||||||
"opencode".to_string(),
|
"opencode".to_string(),
|
||||||
|
7,
|
||||||
);
|
);
|
||||||
assert!(app.is_start_screen());
|
assert!(app.is_start_screen());
|
||||||
|
|
||||||
|
|
@ -798,6 +864,7 @@ mod tests {
|
||||||
50,
|
50,
|
||||||
PathBuf::from("/tmp/brokecrew_test"),
|
PathBuf::from("/tmp/brokecrew_test"),
|
||||||
"opencode".to_string(),
|
"opencode".to_string(),
|
||||||
|
7,
|
||||||
);
|
);
|
||||||
assert_eq!(app.current_theme, "opencode");
|
assert_eq!(app.current_theme, "opencode");
|
||||||
app.cycle_theme();
|
app.cycle_theme();
|
||||||
|
|
@ -824,6 +891,7 @@ mod tests {
|
||||||
50,
|
50,
|
||||||
temp_dir.path().to_path_buf(),
|
temp_dir.path().to_path_buf(),
|
||||||
"opencode".to_string(),
|
"opencode".to_string(),
|
||||||
|
7,
|
||||||
);
|
);
|
||||||
app.add_user_message("Hello from user".to_string());
|
app.add_user_message("Hello from user".to_string());
|
||||||
app.start_assistant_message("coder", "qwen-coder", Tier::Free);
|
app.start_assistant_message("coder", "qwen-coder", Tier::Free);
|
||||||
|
|
@ -838,4 +906,33 @@ mod tests {
|
||||||
let json_path = app.export_active_session_json().expect("export json");
|
let json_path = app.export_active_session_json().expect("export json");
|
||||||
assert!(json_path.exists());
|
assert!(json_path.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_persona_quick_switch() {
|
||||||
|
let personas = vec![
|
||||||
|
("coder".to_string(), "coding assistant".to_string()),
|
||||||
|
("reviewer".to_string(), "review assistant".to_string()),
|
||||||
|
];
|
||||||
|
let mut app = AppState::new(
|
||||||
|
personas,
|
||||||
|
vec![],
|
||||||
|
20,
|
||||||
|
50,
|
||||||
|
PathBuf::from("/tmp/brokecrew_test"),
|
||||||
|
"opencode".to_string(),
|
||||||
|
7,
|
||||||
|
);
|
||||||
|
assert_eq!(app.current_persona_name(), "auto");
|
||||||
|
|
||||||
|
assert_eq!(app.set_persona_by_name("c").unwrap(), "coder");
|
||||||
|
assert_eq!(app.current_persona_name(), "coder");
|
||||||
|
|
||||||
|
assert_eq!(app.set_persona_by_name("rev").unwrap(), "reviewer");
|
||||||
|
assert_eq!(app.current_persona_name(), "reviewer");
|
||||||
|
|
||||||
|
assert_eq!(app.set_persona_by_name("auto").unwrap(), "auto");
|
||||||
|
assert_eq!(app.current_persona_name(), "auto");
|
||||||
|
|
||||||
|
assert!(app.set_persona_by_name("nonexistent").is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
161
src/tui/mod.rs
161
src/tui/mod.rs
|
|
@ -132,6 +132,7 @@ pub async fn run_tui(
|
||||||
daily_limit,
|
daily_limit,
|
||||||
config.config_dir.clone(),
|
config.config_dir.clone(),
|
||||||
config.theme.clone(),
|
config.theme.clone(),
|
||||||
|
config.complexity_threshold,
|
||||||
);
|
);
|
||||||
app.allow_stealth = allow_stealth || config.allow_stealth;
|
app.allow_stealth = allow_stealth || config.allow_stealth;
|
||||||
app.escalate = escalate;
|
app.escalate = escalate;
|
||||||
|
|
@ -561,11 +562,126 @@ async fn handle_key_event(
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
TuiMode::Settings => match key.code {
|
||||||
|
KeyCode::Esc => {
|
||||||
|
app.mode = TuiMode::Normal;
|
||||||
|
}
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
|
app.selected_setting_idx = app.selected_setting_idx.saturating_sub(1);
|
||||||
|
}
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => {
|
||||||
|
if app.selected_setting_idx + 1 < 7 {
|
||||||
|
app.selected_setting_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter | KeyCode::Char(' ') => match app.selected_setting_idx {
|
||||||
|
0 => {
|
||||||
|
app.cycle_theme();
|
||||||
|
let theme_name = app.current_theme.clone();
|
||||||
|
let _ = AppConfig::save_theme(&app.config_dir, &theme_name);
|
||||||
|
app.log_working_event(&format!("[settings] theme switched to {theme_name}"));
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
app.allow_stealth = !app.allow_stealth;
|
||||||
|
app.log_working_event(&format!(
|
||||||
|
"[settings] stealth models: {}",
|
||||||
|
app.allow_stealth
|
||||||
|
));
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
app.escalate = !app.escalate;
|
||||||
|
app.log_working_event(&format!(
|
||||||
|
"[settings] complexity escalate: {}",
|
||||||
|
app.escalate
|
||||||
|
));
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
app.complexity_threshold = if app.complexity_threshold >= 10 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
app.complexity_threshold + 1
|
||||||
|
};
|
||||||
|
app.log_working_event(&format!(
|
||||||
|
"[settings] complexity threshold: {}",
|
||||||
|
app.complexity_threshold
|
||||||
|
));
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
app.tool_execution_enabled = !app.tool_execution_enabled;
|
||||||
|
app.log_working_event(&format!(
|
||||||
|
"[settings] tool execution: {}",
|
||||||
|
app.tool_execution_enabled
|
||||||
|
));
|
||||||
|
}
|
||||||
|
5 => {
|
||||||
|
app.expand_tool_outputs = !app.expand_tool_outputs;
|
||||||
|
app.log_working_event(&format!(
|
||||||
|
"[settings] expand tool output: {}",
|
||||||
|
app.expand_tool_outputs
|
||||||
|
));
|
||||||
|
}
|
||||||
|
6 => {
|
||||||
|
app.cycle_persona();
|
||||||
|
let persona = app.current_persona_name().to_string();
|
||||||
|
app.log_working_event(&format!("[settings] persona set to {persona}"));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
KeyCode::Left | KeyCode::Char('h') => match app.selected_setting_idx {
|
||||||
|
0 => {
|
||||||
|
let current_pos = app::THEMES
|
||||||
|
.iter()
|
||||||
|
.position(|(name, _)| *name == app.current_theme)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let prev_pos = if current_pos == 0 {
|
||||||
|
app::THEMES.len() - 1
|
||||||
|
} else {
|
||||||
|
current_pos - 1
|
||||||
|
};
|
||||||
|
app.current_theme = app::THEMES[prev_pos].0.to_string();
|
||||||
|
app.selected_theme_idx = prev_pos;
|
||||||
|
let theme_name = app.current_theme.clone();
|
||||||
|
let _ = AppConfig::save_theme(&app.config_dir, &theme_name);
|
||||||
|
app.log_working_event(&format!("[settings] theme switched to {theme_name}"));
|
||||||
|
}
|
||||||
|
3 if app.complexity_threshold > 1 => {
|
||||||
|
app.complexity_threshold -= 1;
|
||||||
|
app.log_working_event(&format!(
|
||||||
|
"[settings] complexity threshold: {}",
|
||||||
|
app.complexity_threshold
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
KeyCode::Right | KeyCode::Char('l') => match app.selected_setting_idx {
|
||||||
|
0 => {
|
||||||
|
app.cycle_theme();
|
||||||
|
let theme_name = app.current_theme.clone();
|
||||||
|
let _ = AppConfig::save_theme(&app.config_dir, &theme_name);
|
||||||
|
app.log_working_event(&format!("[settings] theme switched to {theme_name}"));
|
||||||
|
}
|
||||||
|
3 if app.complexity_threshold < 10 => {
|
||||||
|
app.complexity_threshold += 1;
|
||||||
|
app.log_working_event(&format!(
|
||||||
|
"[settings] complexity threshold: {}",
|
||||||
|
app.complexity_threshold
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
|
||||||
TuiMode::Normal => {
|
TuiMode::Normal => {
|
||||||
// Mode switching and shortcuts
|
// Mode switching and shortcuts
|
||||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
app.esc_pressed_at = None;
|
app.esc_pressed_at = None;
|
||||||
match key.code {
|
match key.code {
|
||||||
|
KeyCode::Char(',') => {
|
||||||
|
app.selected_setting_idx = 0;
|
||||||
|
app.mode = TuiMode::Settings;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
KeyCode::Char('p') => {
|
KeyCode::Char('p') => {
|
||||||
app.mode = TuiMode::PipelineSelect;
|
app.mode = TuiMode::PipelineSelect;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -961,13 +1077,8 @@ async fn handle_slash_command(
|
||||||
app.mode = TuiMode::ThemeSelect;
|
app.mode = TuiMode::ThemeSelect;
|
||||||
}
|
}
|
||||||
"/settings" => {
|
"/settings" => {
|
||||||
app.add_system_message(format!(
|
app.selected_setting_idx = 0;
|
||||||
"Runtime Settings:\n Stealth Models: {} (toggle with /stealth)\n Complexity Escalate: {} (toggle with /escalate)\n Active Persona: {}\n Active Pipeline: {}",
|
app.mode = TuiMode::Settings;
|
||||||
if app.allow_stealth { "enabled" } else { "disabled" },
|
|
||||||
if app.escalate { "enabled" } else { "disabled" },
|
|
||||||
app.current_persona_name(),
|
|
||||||
app.active_pipeline_for_prompt.as_deref().unwrap_or("none")
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
"/stealth" => {
|
"/stealth" => {
|
||||||
app.allow_stealth = !app.allow_stealth;
|
app.allow_stealth = !app.allow_stealth;
|
||||||
|
|
@ -990,26 +1101,25 @@ async fn handle_slash_command(
|
||||||
"/clear" => {
|
"/clear" => {
|
||||||
app.clear_chat();
|
app.clear_chat();
|
||||||
}
|
}
|
||||||
"/persona" | "/personas" => {
|
"/p" | "/persona" | "/personas" => {
|
||||||
if let Some(target) = parts.get(1) {
|
if let Some(target) = parts.get(1) {
|
||||||
let target_lower = target.to_lowercase();
|
if target.eq_ignore_ascii_case("next") {
|
||||||
if let Some(pos) = app
|
app.cycle_persona();
|
||||||
.persona_items
|
|
||||||
.iter()
|
|
||||||
.position(|p| p.name.to_lowercase() == target_lower)
|
|
||||||
{
|
|
||||||
app.selected_persona_idx = pos;
|
|
||||||
app.active_pipeline_for_prompt = None;
|
|
||||||
let persona_name = app.current_persona_name().to_string();
|
let persona_name = app.current_persona_name().to_string();
|
||||||
app.add_system_message(format!("Switched active persona to: {persona_name}"));
|
app.add_system_message(format!("Switched active persona to: {persona_name}"));
|
||||||
app.log_working_event(&format!("[persona] switched to {persona_name}"));
|
app.log_working_event(&format!("[persona] switched to {persona_name}"));
|
||||||
} else {
|
} else {
|
||||||
let available: Vec<String> =
|
match app.set_persona_by_name(target) {
|
||||||
app.persona_items.iter().map(|p| p.name.clone()).collect();
|
Ok(persona_name) => {
|
||||||
app.add_system_message(format!(
|
app.add_system_message(format!(
|
||||||
"Persona '{target}' not found. Available personas:\n {}",
|
"Switched active persona to: {persona_name}"
|
||||||
available.join(", ")
|
));
|
||||||
));
|
app.log_working_event(&format!("[persona] switched to {persona_name}"));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
app.add_system_message(format!("! {e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.mode = TuiMode::PersonaSelect;
|
app.mode = TuiMode::PersonaSelect;
|
||||||
|
|
@ -1067,6 +1177,7 @@ fn spawn_dispatch_task(
|
||||||
let direct_persona = app.current_persona_name().to_string();
|
let direct_persona = app.current_persona_name().to_string();
|
||||||
let allow_stealth = app.allow_stealth;
|
let allow_stealth = app.allow_stealth;
|
||||||
let escalate = app.escalate;
|
let escalate = app.escalate;
|
||||||
|
let tool_execution_enabled = app.tool_execution_enabled;
|
||||||
|
|
||||||
let persona_registry = config.persona_registry.clone();
|
let persona_registry = config.persona_registry.clone();
|
||||||
let pipelines = config.pipelines.clone();
|
let pipelines = config.pipelines.clone();
|
||||||
|
|
@ -1319,7 +1430,11 @@ fn spawn_dispatch_task(
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
Ok(disp) => {
|
Ok(disp) => {
|
||||||
let commands = ToolEngine::extract_command_calls(&disp.reply);
|
let commands = if tool_execution_enabled {
|
||||||
|
ToolEngine::extract_command_calls(&disp.reply)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
if !commands.is_empty() {
|
if !commands.is_empty() {
|
||||||
let mut tool_results_summary = String::new();
|
let mut tool_results_summary = String::new();
|
||||||
for cmd in commands {
|
for cmd in commands {
|
||||||
|
|
|
||||||
114
src/tui/ui.rs
114
src/tui/ui.rs
|
|
@ -138,6 +138,7 @@ pub fn render(frame: &mut Frame, app: &mut AppState) {
|
||||||
TuiMode::PipelineSelect => render_pipeline_popup(frame, app, &palette),
|
TuiMode::PipelineSelect => render_pipeline_popup(frame, app, &palette),
|
||||||
TuiMode::SessionSelect => render_session_popup(frame, app, &palette),
|
TuiMode::SessionSelect => render_session_popup(frame, app, &palette),
|
||||||
TuiMode::ThemeSelect => render_theme_popup(frame, app, &palette),
|
TuiMode::ThemeSelect => render_theme_popup(frame, app, &palette),
|
||||||
|
TuiMode::Settings => render_settings_popup(frame, app, &palette),
|
||||||
TuiMode::Normal => {}
|
TuiMode::Normal => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1548,6 +1549,119 @@ fn render_theme_popup(frame: &mut Frame, app: &AppState, palette: &ThemePalette)
|
||||||
frame.render_widget(list, area);
|
frame.render_widget(list, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_settings_popup(frame: &mut Frame, app: &AppState, palette: &ThemePalette) {
|
||||||
|
let area = centered_rect(68, 62, frame.area());
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
|
||||||
|
let setting_items = [
|
||||||
|
(
|
||||||
|
"● Active Theme",
|
||||||
|
format!("{:<14} (enter/left/right cycle)", app.current_theme),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"● Stealth Candidates",
|
||||||
|
if app.allow_stealth {
|
||||||
|
"[ENABLED] (experimental free models included)".to_string()
|
||||||
|
} else {
|
||||||
|
"[DISABLED] (only catalog models)".to_string()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"● Complexity Escalation",
|
||||||
|
if app.escalate {
|
||||||
|
"[ENABLED] (escalate to paid on heavy tasks)".to_string()
|
||||||
|
} else {
|
||||||
|
"[DISABLED] (always prefer free models)".to_string()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("● Complexity Threshold", {
|
||||||
|
let filled = (app.complexity_threshold as usize).min(10);
|
||||||
|
let empty = 10 - filled;
|
||||||
|
format!(
|
||||||
|
"[{}{}] {} / 10 (left/right adjust)",
|
||||||
|
"■".repeat(filled),
|
||||||
|
"□".repeat(empty),
|
||||||
|
app.complexity_threshold
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
(
|
||||||
|
"● Local Tool Execution",
|
||||||
|
if app.tool_execution_enabled {
|
||||||
|
"[AUTO-EXECUTE] (run <run> commands locally)".to_string()
|
||||||
|
} else {
|
||||||
|
"[DISABLED] (commands rendered only)".to_string()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"● Tool Output View",
|
||||||
|
if app.expand_tool_outputs {
|
||||||
|
"[EXPANDED] (full stdout/stderr shown)".to_string()
|
||||||
|
} else {
|
||||||
|
"[COLLAPSED] (compact with ctrl+o expand)".to_string()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"● Active Persona",
|
||||||
|
format!("{:<14} (enter/space to cycle)", app.current_persona_name()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let items: Vec<ListItem> = setting_items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, (name, val))| {
|
||||||
|
let is_selected = idx == app.selected_setting_idx;
|
||||||
|
let prefix = if is_selected { "→ " } else { " " };
|
||||||
|
|
||||||
|
let line = Line::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
prefix,
|
||||||
|
if is_selected {
|
||||||
|
Style::default().fg(palette.accent).bold()
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::DarkGray)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
format!("{:<26} ", name),
|
||||||
|
if is_selected {
|
||||||
|
Style::default().fg(palette.accent).bold()
|
||||||
|
} else {
|
||||||
|
Style::default().fg(palette.text_bright)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
val.clone(),
|
||||||
|
if is_selected {
|
||||||
|
Style::default().fg(palette.text_bright).bold()
|
||||||
|
} else {
|
||||||
|
Style::default().fg(palette.text_muted)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if is_selected {
|
||||||
|
ListItem::new(line).style(Style::default().bg(Color::Rgb(32, 38, 54)))
|
||||||
|
} else {
|
||||||
|
ListItem::new(line)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(items).block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(palette.accent))
|
||||||
|
.title(Span::styled(
|
||||||
|
" settings [up/down navigate, enter/space toggle, left/right adjust, esc close] ",
|
||||||
|
Style::default().fg(palette.accent).bold(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
frame.render_widget(list, area);
|
||||||
|
}
|
||||||
|
|
||||||
fn get_persona_color(name: &str) -> Color {
|
fn get_persona_color(name: &str) -> Color {
|
||||||
match name.to_lowercase().as_str() {
|
match name.to_lowercase().as_str() {
|
||||||
"coder" => Color::LightGreen,
|
"coder" => Color::LightGreen,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue