mod notes;
use super::PickerState;
use crate::config_edit::brazen::ProviderRow;
use crate::model_pick::ModelRow;
const ADD_PROVIDER: &str = "add a provider…";
const CUSTOM_MODEL: &str = "custom model id…";
const PROVIDER_HINT: &str = "Which of brazen's providers this role is routed through. Choosing one \
asks it for its models and refills the list beside it. Typed, the pick is \
the middle word of `/model <role> <provider> <model-id>`.";
const CUSTOM_ID_HINT: &str = "Type the model id exactly as the provider names it, then press Enter — a \
half-typed id is not a choice, so this one field commits on confirm \
rather than as you type. The whole gesture is one line: \
`/model <role> <provider> <model-id>`.";
const MODEL_HINT: &str = "Which model this role runs on — the ids the chosen provider reports. \
Picking one IS the write: it advances the workspace's config branch at \
once, for the next conversation. Typed, it is the last word of \
`/model <role> <provider> <model-id>`. The whole picker — other roles, a \
custom id, a provider to add — is `m`.";
pub(super) struct PairChoice {
pub(super) provider: String,
pub(super) add_provider: bool,
pub(super) chosen: Option<String>,
pub(super) list_open: bool,
}
pub(super) fn pair_row(
ui: &mut egui::Ui,
picker: &mut PickerState,
row: &ModelRow,
rows: &[ProviderRow],
candidates: &[String],
in_flight: bool,
) -> PairChoice {
let provider = notes::scoped_with_notes(ui, picker, row, rows);
let shown = if picker.custom.is_some() {
CUSTOM_MODEL.to_string()
} else {
picker.model.clone().unwrap_or_else(|| row.model.clone())
};
let mut chosen_provider = provider.clone();
let mut chosen_model = shown.clone();
let list_open = ui
.horizontal(|ui| {
provider_combo(ui, &provider, rows, &mut chosen_provider);
ui.weak("·").on_hover_text(&row.hover);
model_combo(ui, &shown, candidates, in_flight, &mut chosen_model)
})
.inner;
if chosen_provider == ADD_PROVIDER {
return PairChoice {
provider,
add_provider: true,
chosen: None,
list_open,
};
}
if chosen_provider != provider {
picker.provider = Some(chosen_provider.clone());
picker.model = None;
picker.custom = None;
return PairChoice {
provider: chosen_provider,
add_provider: false,
chosen: None,
list_open,
};
}
PairChoice {
provider,
add_provider: false,
chosen: commit(ui, picker, &shown, chosen_model),
list_open,
}
}
fn provider_combo(ui: &mut egui::Ui, selected: &str, rows: &[ProviderRow], chosen: &mut String) {
egui::ComboBox::from_id_salt("model-pick-provider")
.selected_text(selected)
.show_ui(ui, |ui| {
for row in rows {
if let Some(why) = row.tools_blocked() {
ui.weak(format!("{} — {why}", row.name))
.on_hover_text(PROVIDER_HINT);
continue;
}
ui.selectable_value(chosen, row.name.clone(), &row.name)
.on_hover_text(PROVIDER_HINT);
}
ui.selectable_value(chosen, ADD_PROVIDER.to_string(), ADD_PROVIDER)
.on_hover_text(
"Leave for brazen's config.toml editor, where a provider row is \
authored — this list only offers rows that already exist. That \
editor is `/config brazen <text…>`.",
);
})
.response
.on_hover_text(PROVIDER_HINT);
}
fn model_combo(
ui: &mut egui::Ui,
selected: &str,
candidates: &[String],
in_flight: bool,
chosen: &mut String,
) -> bool {
let mut open = false;
egui::ComboBox::from_id_salt("model-pick-model")
.selected_text(selected)
.show_ui(ui, |ui| {
open = true;
if in_flight || candidates.is_empty() {
let time = ui.ctx().input(|i| i.time);
ui.colored_label(
crate::theme::pulse(crate::theme::SPECTRE, time),
"⟳ asking the provider for its models…",
);
ui.ctx()
.request_repaint_after(crate::theme::PULSE_REPAINT_DELAY);
}
for id in candidates {
ui.selectable_value(chosen, id.clone(), id)
.on_hover_text(MODEL_HINT);
}
ui.selectable_value(chosen, CUSTOM_MODEL.to_string(), CUSTOM_MODEL)
.on_hover_text(
"Reveal a field for typing a model id the provider did not \
list — a preview, or a locally served tag. Typed, any id is \
the last word of `/model <role> <provider> <model-id>`.",
);
})
.response
.on_hover_text(MODEL_HINT);
open
}
fn commit(
ui: &mut egui::Ui,
picker: &mut PickerState,
shown: &str,
chosen: String,
) -> Option<String> {
if chosen != shown {
if chosen == CUSTOM_MODEL {
picker.custom = Some(String::new());
return None;
}
picker.model = Some(chosen.clone());
picker.custom = None;
return Some(chosen);
}
custom_entry(ui, picker)
}
fn custom_entry(ui: &mut egui::Ui, picker: &mut PickerState) -> Option<String> {
let typed = picker.custom.as_mut()?;
let response = ui
.horizontal(|ui| {
ui.label("id").on_hover_text(CUSTOM_ID_HINT);
ui.text_edit_singleline(typed).on_hover_text(CUSTOM_ID_HINT)
})
.inner;
let id = typed.trim().to_string();
if !response.lost_focus() || id.is_empty() {
return None;
}
picker.model = Some(id.clone());
picker.custom = None;
Some(id)
}