use super::ShellState;
use super::menus::{BallRef, Target};
use super::start_pane::run_prepare;
use crate::AppModel;
use crate::cli_outbound::Cli;
use crate::nav::menu::Seat;
use crate::projects::join::JoinState;
use crate::start::{BallSpec, Payload, StartInputs};
use std::path::Path;
const START_HINT: &str = "Claim this ball for the focused workspace and open its goal for editing. \
Nothing is sent to a model until you press Send. (s) starts the top ready row, \
and `/prepare ball` says it on one line.";
const ASSIGN_HINT: &str = "Claim this ball for the focused workspace and stop there — no worktree \
is prepared, no conversation starts, no model call is spent. Typed, it is \
`/assign [id]`.";
const CONTINUE_HINT: &str = "Open a fresh start goal on a ball this workspace already holds — a new \
conversation against the same task. Nothing is sent until Send; (s) opens the top \
row, and `/prepare ball` says it on one line.";
pub(super) fn ready_row(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
lernie: &Cli,
inputs: StartInputs,
) {
let ball = ball_ref(&inputs.payload);
let to = model.focused_ws_name();
let label = format!("▶ {}", start_label(&inputs));
let assign_to = ball.as_ref().and(to.clone());
let (start_button, assign_clicked) = super::row::control_last(
ui,
|ui| ui.button(label).on_hover_text(START_HINT),
|ui| {
assign_to.is_some_and(|to| {
ui.button(format!("assign → {to}"))
.on_hover_text(ASSIGN_HINT)
.clicked()
})
},
);
if start_button.clicked() {
run_prepare(model, state, inputs);
return;
}
let Some((project, id, join)) = ball else {
return;
};
if assign_clicked && let Some(to) = &to {
super::ball_bar::assign_ball(model, &project, &id, to);
}
let seat = Seat::BallRow {
state: join,
assign_to: to,
move_to: Vec::new(),
};
let target = Target::Ball(BallRef {
project,
id,
owner: String::new(),
});
super::menus::attach(&start_button, seat, &target, model, state, lernie);
}
pub(super) fn continue_row(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
lernie: &Cli,
inputs: StartInputs,
) {
let ws = model.snap.ws_name(&inputs.workspace);
let rows = super::chrome::balls(model, &ws).value.unwrap_or_default();
let ball =
ball_ref(&inputs.payload).and_then(|(_, id, _)| crate::nav::balls::bound(&rows, &id));
let button = ui
.button(format!("▶ Continue {}", start_label(&inputs)))
.on_hover_text(CONTINUE_HINT);
if button.clicked() {
run_prepare(model, state, inputs);
return;
}
let Some(ball) = ball else {
return;
};
let seat = Seat::BallRow {
state: ball.state,
assign_to: model.focused_ws_name(),
move_to: crate::nav::tabs::move_targets(&super::chrome::ws_rows(model), &ball.owner),
};
let target = Target::Ball(BallRef {
project: ball.project,
id: ball.id,
owner: ball.owner,
});
super::menus::attach(&button, seat, &target, model, state, lernie);
}
pub(super) fn ball_id(payload: &Payload) -> Option<String> {
match payload {
Payload::Ball {
ball: BallSpec::Existing { id, .. },
..
} => Some(id.clone()),
_ => None,
}
}
pub(super) fn ball_ref(payload: &Payload) -> Option<(String, String, JoinState)> {
match payload {
Payload::Ball {
project,
ball: BallSpec::Existing { id, join, .. },
} => Some((project.clone(), id.clone(), *join)),
_ => 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(),
}
}
pub(super) fn new_ball_form(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
project: &Path,
label: &str,
) {
let (mut title, mut body) = state
.start
.new_ball
.get(project)
.cloned()
.unwrap_or_default();
let mut create = false;
egui::CollapsingHeader::new(format!("+ new ball · {label}"))
.id_salt(project)
.show(ui, |ui| {
let hints = crate::actions::new_ball_hints();
ui.add(egui::TextEdit::singleline(&mut title).hint_text(hints.title))
.on_hover_text(
"The new ball's title — the one line it is listed by. Typed, it is \
the words before any flag in `/create <title…>`.",
);
ui.add(egui::TextEdit::multiline(&mut body).hint_text(hints.body))
.on_hover_text(
"The new ball's body — the task written out as the agent will read \
it. Typed, it is `/create <title…> --body <text…>`.",
);
create = ui
.add_enabled(
crate::actions::create_ball_enabled(&title),
egui::Button::new("Create & Start"),
)
.on_hover_text(
"File this ball in the project (`bl create`), claim it for the focused \
workspace, and open its goal. Nothing is sent until Send. Typed, it \
is `/create <title…>` then `/prepare ball`.",
)
.on_disabled_hover_text("give the ball a title first")
.clicked();
})
.header_response
.on_hover_text(format!(
"Fold open a form for filing a brand-new ball in {}. No key of its own: Tab \
reaches it, Space presses it — and `/create <title…>` files one without \
the form.",
project.display()
));
if create {
let inputs = model.new_ball_inputs(project, &title, &body);
state.start.new_ball.remove(project);
run_prepare(model, state, inputs);
} else {
state
.start
.new_ball
.insert(project.to_path_buf(), (title, body));
}
}