use crossterm::event::KeyEvent;
use rho_providers::model::favorites::{CycleDirection, CycleOutcome};
use super::{
catalog, favorites, model_picker, App, ComposerMode, Entry, InteractiveModelSelection,
InteractiveRuntime, UiPicker,
};
enum CycleTarget {
NotComposing,
NoPins,
Unchanged,
Failed(String),
Switch(Box<InteractiveModelSelection>),
}
impl App {
pub(super) fn resolved_model_picker_scope(&self) -> model_picker::ModelPickerScope {
model_picker::effective_model_picker_scope(
self.model_picker_scope_override
.unwrap_or(model_picker::ModelPickerScope::Pinned),
&self.info.runtime.favorite_models,
&self.available_auths,
)
}
pub(super) fn conversation_model_picker(&mut self) -> UiPicker {
self.refresh_available_auths();
let scope = self.resolved_model_picker_scope();
model_picker::model_picker(&self.info.runtime, &self.available_auths, scope)
}
pub(super) fn conversation_model_picker_during_run(&mut self) -> UiPicker {
self.refresh_available_auths();
let scope = self.resolved_model_picker_scope();
model_picker::model_picker_during_run(
&self.info.runtime,
self.pending_model_selection
.as_ref()
.map(|pending| &pending.selection),
&self.available_auths,
scope,
)
}
fn rebuilt_model_picker(&mut self, selected_value: &str, filter: String) -> Option<UiPicker> {
let mut picker = match self.input_ui.composer() {
ComposerMode::Picker(picker) if picker.is_conversation_model() => {
if self.is_provider_turn_ui() {
self.conversation_model_picker_during_run()
} else {
self.conversation_model_picker()
}
}
ComposerMode::Picker(picker) if picker.is_internal_agent_model() => {
let target = self.internal_agent_model_target.clone()?;
self.internal_agent_model_picker(&target.id, target.origin)
}
_ => return None,
};
let parent = match self.input_ui.composer_mut() {
ComposerMode::Picker(picker) => picker.take_parent(),
_ => None,
};
if let Some(parent) = parent {
picker = picker.with_parent(parent);
}
Self::restore_picker_position(&mut picker, selected_value, filter);
Some(picker)
}
pub(super) fn rebuild_open_model_picker(&mut self, selected_value: &str, filter: String) {
if let Some(picker) = self.rebuilt_model_picker(selected_value, filter) {
self.input_ui.set_composer(ComposerMode::Picker(picker));
}
}
pub(super) fn toggle_model_picker_scope(&mut self) -> anyhow::Result<()> {
let (value, filter) = match self.input_ui.composer() {
ComposerMode::Picker(picker) if picker.is_model_list() => (
picker
.selected_item()
.map(|item| item.value.clone())
.unwrap_or_default(),
picker.filter.clone(),
),
_ => return Ok(()),
};
if value.is_empty() {
return Ok(());
}
self.refresh_available_auths();
let current = self.resolved_model_picker_scope();
let next = current.other();
if model_picker::effective_model_picker_scope(
next,
&self.info.runtime.favorite_models,
&self.available_auths,
) != next
{
self.set_status("no pinned models");
return Ok(());
}
let previous = self.model_picker_scope_override;
self.model_picker_scope_override = Some(next);
match self.rebuilt_model_picker(&value, filter) {
Some(picker) => {
self.input_ui.set_composer(ComposerMode::Picker(picker));
self.set_status(format!("showing {}", next.status_label()));
}
None => self.model_picker_scope_override = previous,
}
Ok(())
}
fn next_pinned_selection(&mut self, direction: CycleDirection) -> CycleTarget {
if !matches!(self.input_ui.composer(), ComposerMode::Input) {
return CycleTarget::NotComposing;
}
self.refresh_available_auths();
let favorites = favorites::normalized_favorite_models(&self.info.runtime.favorite_models);
let available = catalog::available_models_for_auths(&self.available_auths);
let next = match favorites::cycle_favorite(
&favorites,
&available,
&self.info.runtime.provider,
&self.info.runtime.model,
direction,
) {
CycleOutcome::NoPins => return CycleTarget::NoPins,
CycleOutcome::Unchanged => return CycleTarget::Unchanged,
CycleOutcome::Switch(favorite) => favorite.value(),
};
match self.resolve_model_selection(
&next,
&self.info.runtime.provider,
&self.info.runtime.auth,
) {
Ok(selection) => CycleTarget::Switch(Box::new(selection)),
Err(err) => CycleTarget::Failed(err.to_string()),
}
}
fn report_cycle_target(&mut self, target: &CycleTarget) -> bool {
match target {
CycleTarget::NotComposing => return false,
CycleTarget::NoPins => self.notify_status("no pinned models"),
CycleTarget::Unchanged => {}
CycleTarget::Failed(message) => {
self.insert_entry(&Entry::Error(message.clone()));
self.set_status("model switch failed");
}
CycleTarget::Switch(_) => {}
}
self.clear_transient_key_state();
true
}
fn favorite_cycle_direction(&self, key: KeyEvent) -> Option<CycleDirection> {
let keybindings = &self.info.runtime.keybindings;
if keybindings.cycle_pinned_model.matches(key) {
Some(CycleDirection::Forward)
} else if keybindings.cycle_pinned_model_back.matches(key) {
Some(CycleDirection::Backward)
} else {
None
}
}
pub(super) async fn handle_favorite_cycle_key(
&mut self,
key: KeyEvent,
agent: &mut InteractiveRuntime,
) -> anyhow::Result<bool> {
let Some(direction) = self.favorite_cycle_direction(key) else {
return Ok(false);
};
let target = self.next_pinned_selection(direction);
if !self.report_cycle_target(&target) {
return Ok(false);
}
if let CycleTarget::Switch(selection) = target {
if self.is_provider_turn_ui() {
self.queue_model_selection(*selection)?;
} else {
self.request_model_selection(*selection, agent).await?;
}
}
Ok(true)
}
pub(super) fn handle_running_favorite_cycle_key(
&mut self,
key: KeyEvent,
) -> anyhow::Result<bool> {
let Some(direction) = self.favorite_cycle_direction(key) else {
return Ok(false);
};
let target = self.next_pinned_selection(direction);
if !self.report_cycle_target(&target) {
return Ok(false);
}
if let CycleTarget::Switch(selection) = target {
self.queue_model_selection(*selection)?;
}
Ok(true)
}
}
#[cfg(test)]
#[path = "model_cycle_tests.rs"]
mod tests;