feat: implement model management commands and overlays for enhanced user interaction
This commit is contained in:
@@ -12,12 +12,21 @@ pub enum Command {
|
||||
LessonList,
|
||||
Mode(ModeKind),
|
||||
Clear,
|
||||
ClearConfirm,
|
||||
Login { provider: String },
|
||||
Edit(String),
|
||||
McpAdd {
|
||||
name: String,
|
||||
command: String,
|
||||
},
|
||||
ModelList,
|
||||
ModelUse {
|
||||
provider: String,
|
||||
},
|
||||
ModelAdd {
|
||||
name: String,
|
||||
base_url: String,
|
||||
},
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
@@ -33,7 +42,9 @@ pub fn parse_command(text: &str) -> Command {
|
||||
match cmd {
|
||||
"/help" => Command::Help,
|
||||
"/quit" => Command::Quit,
|
||||
"/clear" if arg1.is_empty() => Command::ClearConfirm,
|
||||
"/clear" => Command::Clear,
|
||||
"/mode" if arg1.is_empty() => Command::Help, // open help to show available modes
|
||||
"/mode" => {
|
||||
let mode = match arg1 {
|
||||
"chat" | "c" => ModeKind::Chat,
|
||||
@@ -53,25 +64,36 @@ pub fn parse_command(text: &str) -> Command {
|
||||
"/lesson" if arg1 == "reject" && !arg2.is_empty() => Command::LessonReject(arg2.to_string()),
|
||||
"/lesson" if !arg1.is_empty() => Command::LessonCreate(arg1.to_string()),
|
||||
"/lesson" => Command::LessonList,
|
||||
"/login" if arg1.is_empty() => Command::LessonList,
|
||||
"/login" if !arg1.is_empty() => Command::Login { provider: arg1.to_string() },
|
||||
"/login" => Command::Login { provider: "zen".to_string() },
|
||||
"/edit" if !arg1.is_empty() => Command::Edit(arg1.to_string()),
|
||||
"/edit" => Command::Edit(".".to_string()),
|
||||
"/mcp" if arg1.is_empty() => {
|
||||
Command::Mode(ModeKind::Mcp)
|
||||
}
|
||||
"/mcp" if arg1 == "add" && !arg2.is_empty() => {
|
||||
// /mcp add <name> <command> [args...]
|
||||
// name is the first word of arg2, the rest is the command
|
||||
let rest = arg2.trim();
|
||||
if let Some(space) = rest.find(' ') {
|
||||
let name = rest[..space].to_string();
|
||||
let command = rest[space + 1..].trim().to_string();
|
||||
Command::McpAdd { name, command }
|
||||
} else {
|
||||
Command::McpAdd {
|
||||
name: rest.to_string(),
|
||||
command: String::new(),
|
||||
}
|
||||
Command::McpAdd { name: rest.to_string(), command: String::new() }
|
||||
}
|
||||
}
|
||||
"/model" if arg1 == "ls" || arg1 == "list" => Command::ModelList,
|
||||
"/model" if arg1 == "add" && !arg2.is_empty() => {
|
||||
let rest = arg2.trim();
|
||||
if let Some(space) = rest.find(' ') {
|
||||
let name = rest[..space].to_string();
|
||||
let base_url = rest[space + 1..].trim().to_string();
|
||||
Command::ModelAdd { name, base_url }
|
||||
} else {
|
||||
Command::ModelAdd { name: rest.to_string(), base_url: String::new() }
|
||||
}
|
||||
}
|
||||
"/model" if arg1.is_empty() => Command::ModelList,
|
||||
"/model" => Command::ModelUse { provider: arg1.to_string() },
|
||||
_ => Command::Unknown(cmd.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
+110
-4
@@ -63,6 +63,11 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
|
||||
vec![Action::CloseOverlay]
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if state.input.autocomplete_visible {
|
||||
state.input.select_autocomplete();
|
||||
state.dirty = true;
|
||||
return Vec::new();
|
||||
}
|
||||
if state.misc.overlay.is_active() {
|
||||
return handle_overlay_enter(state);
|
||||
}
|
||||
@@ -73,9 +78,19 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
|
||||
vec![Action::SubmitInput(text)]
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
if state.input.autocomplete_visible {
|
||||
state.input.close_autocomplete();
|
||||
state.dirty = true;
|
||||
return Vec::new();
|
||||
}
|
||||
vec![Action::DeleteChar]
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
if state.input.autocomplete_visible {
|
||||
state.input.close_autocomplete();
|
||||
state.dirty = true;
|
||||
return Vec::new();
|
||||
}
|
||||
vec![Action::DeleteCharRight]
|
||||
}
|
||||
KeyCode::Left => {
|
||||
@@ -85,7 +100,11 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
|
||||
vec![Action::CursorRight]
|
||||
}
|
||||
KeyCode::Up => {
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
if state.input.autocomplete_visible {
|
||||
state.input.cycle_autocomplete(false);
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
} else if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
vec![Action::HistoryUp]
|
||||
} else if state.misc.overlay == Overlay::Effort {
|
||||
mode::effort::cycle_effort(state);
|
||||
@@ -100,12 +119,30 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
|
||||
state.misc.selected_index = if state.misc.selected_index == 0 { n.saturating_sub(1) } else { state.misc.selected_index - 1 };
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
} else if state.misc.overlay == Overlay::ModelSelector {
|
||||
let n = state.app_config.providers.len();
|
||||
state.misc.selected_index = if state.misc.selected_index == 0 { n.saturating_sub(1) } else { state.misc.selected_index - 1 };
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
} else if state.misc.overlay == Overlay::ModePicker {
|
||||
state.misc.selected_index = if state.misc.selected_index == 0 { 3 } else { state.misc.selected_index - 1 };
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
} else if state.misc.overlay == Overlay::LoginPicker {
|
||||
let n = crate::app::mode::onboard_provider::PROVIDERS.len();
|
||||
state.misc.selected_index = if state.misc.selected_index == 0 { n.saturating_sub(1) } else { state.misc.selected_index - 1 };
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![Action::ScrollUp]
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
if state.input.autocomplete_visible {
|
||||
state.input.cycle_autocomplete(true);
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
} else if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
vec![Action::HistoryDown]
|
||||
} else if state.misc.overlay == Overlay::Effort {
|
||||
mode::effort::cycle_effort(state);
|
||||
@@ -120,6 +157,20 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
|
||||
state.misc.selected_index = if n == 0 { 0 } else { (state.misc.selected_index + 1) % n };
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
} else if state.misc.overlay == Overlay::ModelSelector {
|
||||
let n = state.app_config.providers.len();
|
||||
state.misc.selected_index = if n == 0 { 0 } else { (state.misc.selected_index + 1) % n };
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
} else if state.misc.overlay == Overlay::ModePicker {
|
||||
state.misc.selected_index = (state.misc.selected_index + 1) % 4;
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
} else if state.misc.overlay == Overlay::LoginPicker {
|
||||
let n = crate::app::mode::onboard_provider::PROVIDERS.len();
|
||||
state.misc.selected_index = (state.misc.selected_index + 1) % n;
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![Action::ScrollDown]
|
||||
}
|
||||
@@ -158,7 +209,11 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
|
||||
vec![Action::OpenOverlay(Overlay::KeyInput)]
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
if state.misc.overlay.is_active() {
|
||||
if state.input.autocomplete_visible {
|
||||
state.input.close_autocomplete();
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
} else if state.misc.overlay.is_active() {
|
||||
vec![Action::CloseOverlay]
|
||||
} else {
|
||||
Vec::new()
|
||||
@@ -166,7 +221,11 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
if state.input.buffer.starts_with('/') {
|
||||
state.input.tab_complete();
|
||||
if state.input.autocomplete_visible {
|
||||
state.input.cycle_autocomplete(true);
|
||||
} else {
|
||||
state.input.tab_complete();
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Vec::new()
|
||||
@@ -178,6 +237,10 @@ pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
|
||||
vec![Action::OpenOverlay(Overlay::Usage)]
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
if state.input.autocomplete_visible {
|
||||
state.input.close_autocomplete();
|
||||
state.dirty = true;
|
||||
}
|
||||
vec![Action::InsertChar(c)]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
@@ -251,6 +314,49 @@ fn handle_overlay_enter(state: &mut AppStateRest) -> Vec<Action> {
|
||||
mode::rewind::rewind_to(state, idx);
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::ModelSelector => {
|
||||
let providers: Vec<String> = state.app_config.providers.keys().cloned().collect();
|
||||
if let Some(provider) = providers.get(state.misc.selected_index) {
|
||||
if let Some(cfg) = state.app_config.providers.get(provider) {
|
||||
let model = cfg.default_model.clone().unwrap_or_else(|| "claude-opus-4-8".to_string());
|
||||
state.settings.provider = provider.clone();
|
||||
state.settings.model = model.clone();
|
||||
let _ = state.settings.save();
|
||||
state.push_toast(crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Success,
|
||||
format!("Switched to {} / {}", provider, model),
|
||||
));
|
||||
}
|
||||
}
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::ClearConfirm => {
|
||||
state.push_toast(crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Info,
|
||||
"Transcript cleared".to_string(),
|
||||
));
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::ModePicker => {
|
||||
let mode = state.mode.cycle();
|
||||
state.mode = mode;
|
||||
state.push_toast(crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Info,
|
||||
format!("Mode: {}", mode.name()),
|
||||
));
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::LoginPicker => {
|
||||
state.misc.overlay = Overlay::OnboardProvider;
|
||||
state.dirty = true;
|
||||
Vec::new()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user