use super::StartState;
use crate::AppModel;
use crate::actions::verbs;
use crate::cli_outbound::Cli;
use crate::names::SplitMix64;
use crate::start::{self, BallSpec, Deps, Payload, StartInputs};
use std::path::{Path, PathBuf};
pub fn startable(
ui: &mut egui::Ui,
model: &mut AppModel,
start: &mut StartState,
lernie: &Cli,
bl: &Cli,
) {
if let Some(failure) = &start.last_failure {
super::failure_banner(ui, failure);
}
if let Some(hint) = model.empty_project_hint() {
ui.weak(format!("No projects yet — add one with: {hint}"));
}
let target = model.focused_ws_name();
for inputs in model.startable() {
ready_row(ui, model, start, lernie, bl, inputs, target.as_deref());
}
for inputs in model.resumable() {
if ui
.button(format!("▶ Continue {}", start_label(&inputs)))
.clicked()
{
run_prepare(model, start, lernie, bl, inputs);
}
}
ui.separator();
for project in model.project_paths() {
new_ball_form(ui, model, start, lernie, bl, &project);
}
}
fn ready_row(
ui: &mut egui::Ui,
model: &mut AppModel,
start: &mut StartState,
lernie: &Cli,
bl: &Cli,
inputs: StartInputs,
target: Option<&str>,
) {
let assign =
target.and_then(|to| ball_ref(&inputs.payload).map(|(p, id)| (p, id, to.to_owned())));
ui.horizontal(|ui| {
if ui.button(format!("▶ {}", start_label(&inputs))).clicked() {
run_prepare(model, start, lernie, bl, inputs);
} else if let Some((project, id, to)) = assign
&& ui.button(format!("assign → {to}")).clicked()
{
let gate = model.toolchain().clone();
let sr = model.state_root().to_path_buf();
let _ = verbs::assign(&gate, bl, &sr, &super::now_ts(), &project, &id, &to);
model.after_bl_verb(&project);
start.last_failure = model.last_failure();
}
});
}
fn ball_ref(payload: &Payload) -> Option<(PathBuf, String)> {
match payload {
Payload::Ball {
project,
ball: BallSpec::Existing { id, .. },
} => Some((project.clone(), id.clone())),
_ => None,
}
}
fn start_label(inputs: &StartInputs) -> String {
match &inputs.payload {
Payload::Ball {
ball: BallSpec::Existing { id, title, .. },
..
} => format!("{id}: {title}"),
Payload::Ball {
ball: BallSpec::New { title, .. },
..
} => title.clone(),
Payload::Bare => "(new conversation)".to_owned(),
Payload::Path { dir } => dir.display().to_string(),
}
}
fn new_ball_form(
ui: &mut egui::Ui,
model: &mut AppModel,
start: &mut StartState,
lernie: &Cli,
bl: &Cli,
project: &Path,
) {
let (mut title, mut body) = start.new_ball.get(project).cloned().unwrap_or_default();
let mut create = false;
ui.collapsing(format!("+ new ball · {}", project.display()), |ui| {
ui.text_edit_singleline(&mut title);
ui.text_edit_multiline(&mut body);
create = ui
.add_enabled(
crate::actions::create_ball_enabled(&title),
egui::Button::new("Create & Start"),
)
.clicked();
});
if create {
let inputs = model.new_ball_inputs(project, &title, &body);
start.new_ball.remove(project);
run_prepare(model, start, lernie, bl, inputs);
} else {
start.new_ball.insert(project.to_path_buf(), (title, body));
}
}
pub(super) fn run_prepare(
model: &mut AppModel,
start: &mut StartState,
lernie: &Cli,
bl: &Cli,
inputs: StartInputs,
) {
let project = payload_project(&inputs.payload);
let ts = super::now_ts();
let mut rng = SplitMix64::from_seed(start.mint_seed);
let result = {
let deps = Deps {
bl: bl.clone(),
lernie: lernie.clone(),
state_root: model.state_root().to_path_buf(),
gate: model.toolchain().clone(),
};
start::prepare(&deps, &inputs, &mut rng, &ts)
};
if let Ok(prepared) = result {
start.pending = Some(prepared);
}
match project {
Some(p) => model.after_bl_verb(&p),
None => model.after_lernie_verb(),
}
start.last_failure = model.last_failure();
}
fn payload_project(payload: &Payload) -> Option<PathBuf> {
match payload {
Payload::Ball { project, .. } => Some(project.clone()),
Payload::Bare | Payload::Path { .. } => None,
}
}
pub fn composer(ui: &mut egui::Ui, model: &mut AppModel, start: &mut StartState, lernie: &Cli) {
let Some(pending) = start.pending.as_mut() else {
return;
};
let (mut send, mut cancel) = (false, false);
ui.weak(start::identity_preview(&pending.name));
ui.label(format!("Start goal → {}", pending.workspace.display()));
egui::ScrollArea::vertical()
.max_height(160.0)
.show(ui, |ui| ui.text_edit_multiline(&mut pending.goal));
ui.horizontal(|ui| {
send = ui.button("Send (detached prompt)").clicked();
cancel = ui.button("Cancel").clicked();
});
if send && let Some(p) = start.pending.take() {
let launched = start::execute_prompt(
lernie,
model.state_root(),
&super::now_ts(),
&p.name,
&p.cwd,
&p.workspace,
&p.goal,
)
.is_ok();
model.after_lernie_verb();
start.last_failure = model.last_failure();
if !launched {
start.pending = Some(p);
}
} else if cancel {
start.pending = None;
}
}