feat: implement model management commands and overlays for enhanced user interaction

This commit is contained in:
asepharyana
2026-07-12 02:06:46 +07:00
parent 9f272d5973
commit 662e269f2c
8 changed files with 403 additions and 26 deletions
+110 -4
View File
@@ -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(),
}
}