use super::composer::Composer;
use super::{Attempt, Choices, ForkPoint};
use crate::model_pick::grammar::RoleModel;
use crate::theme;
const HEAD: &str = "Fork from here";
const HEAD_HOVER: &str = "Try this conversation again from the mark you pinned. The fork inherits everything the \
conversation had read by then and nothing it said afterwards. Typed, the whole \
composer is one `/fork` line.";
const GOAL_HINT: &str = "what the fork should do instead";
const GOAL_HOVER: &str = "What every candidate is asked to do. It reaches the model exactly as typed — the same \
verbatim rule the ordinary composer keeps; `/fork --goal <the goal…>` says it on \
one line.";
const MORE: &str = "+";
const FEWER: &str = "−";
const TIMES_HOVER: &str = "How many candidates to fire. Each gets its own controls; they group themselves on the rail \
because they were born at the same mark — which is also what firing `/fork` more \
than once from one mark does.";
const FROM_HOVER: &str = "Where this candidate starts. `here` inherits this conversation's history up to the pinned \
mark; a config branch starts clean from that policy. It is `/fork --from <ref>`.";
const ROLE_HOVER: &str = "The role decides the model: it is read from the config governing the fork point, which is \
the same file the run itself resolves against. It is `/fork --role <role>`.";
const SKILL_HOVER: &str = "Pin this skill's instructions into the candidate's context — `/fork --skills a,b` \
names them on the line.";
const FIRE: &str = "Fire";
const FIRE_HOVER: &str = "Dispatch every candidate above. Each is an ordinary fork; nothing records that they belong \
together — the rail groups them by where they were born. One candidate is one \
`/fork` line.";
pub fn render(ui: &mut egui::Ui, composer: &mut Composer, choices: &Choices) -> bool {
ui.horizontal(|ui| {
ui.colored_label(theme::BRAZEN, HEAD)
.on_hover_text(HEAD_HOVER);
times(ui, composer);
});
ui.add(
egui::TextEdit::multiline(&mut composer.goal)
.hint_text(GOAL_HINT)
.desired_rows(2)
.desired_width(f32::INFINITY),
)
.on_hover_text(GOAL_HOVER);
let mut edits: Vec<(usize, String)> = Vec::new();
for (index, attempt) in composer.attempts.iter_mut().enumerate() {
candidate(ui, index, attempt, choices, &mut edits);
}
for (index, skill) in edits {
composer.toggle_skill(index, &skill);
}
ui.add_enabled(composer.ready(), egui::Button::new(FIRE))
.on_hover_text(FIRE_HOVER)
.clicked()
}
fn times(ui: &mut egui::Ui, composer: &mut Composer) {
let n = composer.attempts.len();
if ui.small_button(FEWER).on_hover_text(TIMES_HOVER).clicked() {
composer.resize(n.saturating_sub(1));
}
ui.weak(format!("×{n}")).on_hover_text(TIMES_HOVER);
if ui.small_button(MORE).on_hover_text(TIMES_HOVER).clicked() {
composer.resize(n + 1);
}
}
fn candidate(
ui: &mut egui::Ui,
index: usize,
attempt: &mut Attempt,
choices: &Choices,
edits: &mut Vec<(usize, String)>,
) {
egui::Frame::group(ui.style()).show(ui, |ui| {
ui.horizontal_wrapped(|ui| {
ui.weak(format!("#{}", index + 1));
for point in &choices.points {
if ui
.selectable_label(attempt.from == point.refspec, &point.label)
.on_hover_text(FROM_HOVER)
.clicked()
{
attempt.from.clone_from(&point.refspec);
attempt.role = first_role(point);
}
}
});
ui.horizontal_wrapped(|ui| {
for row in &roles_of(attempt, choices) {
if ui
.selectable_label(attempt.role == row.role, role_line(row))
.on_hover_text(ROLE_HOVER)
.clicked()
{
attempt.role.clone_from(&row.role);
}
}
});
ui.horizontal_wrapped(|ui| {
for skill in &choices.skills {
let on = attempt.skills.iter().any(|s| s == skill);
if ui
.selectable_label(on, skill)
.on_hover_text(SKILL_HOVER)
.clicked()
{
edits.push((index, skill.clone()));
}
}
});
});
}
fn roles_of(attempt: &Attempt, choices: &Choices) -> Vec<RoleModel> {
choices
.point(&attempt.from)
.map(|p| p.roles)
.unwrap_or_default()
}
fn role_line(row: &RoleModel) -> String {
format!("{} — {}/{}", row.role, row.provider, row.model)
}
fn first_role(point: &ForkPoint) -> String {
point
.roles
.first()
.map(|r| r.role.clone())
.unwrap_or_default()
}