use crate::AppModel;
use crate::actions::{DraftKey, message_enabled, new_prompt_enabled};
use crate::boundary::line;
use crate::cli_outbound::Cli;
use std::path::PathBuf;
use super::ShellState;
const STOP_HINT: &str = "Kill the driver running this conversation (`lernie stop`). Everything it \
has already committed is kept; you can message it again afterwards (x).";
pub(super) struct VerbCtx {
pub ws: PathBuf,
pub stoppable: bool,
pub stop_children: bool,
pub present: bool,
pub nudgeable: bool,
pub held: Option<crate::control::hold::Held>,
pub key: DraftKey,
pub text: String,
pub conv_name: Option<String>,
pub entered: bool,
pub interrupted: bool,
}
pub(super) fn verb_buttons(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
lernie: &Cli,
bl: &Cli,
ctx: &VerbCtx,
) {
if super::slash::seat(ui, model, state, lernie, bl, ctx) {
return;
}
let selected = state.actions.selected_branch.clone();
let said = line::unescape(&ctx.text);
super::row::peers(ui, |ui| {
if let Some(name) = ctx.conv_name.as_deref() {
ui.weak(format!("→ message {name}"));
}
if let Some(agent) = selected.as_deref() {
let msg_on = message_enabled(ctx.present, &ctx.text);
let send = ui
.add_enabled(msg_on, egui::Button::new("Message"))
.on_hover_text(
"Deposit this text in the selected conversation's inbox and wake its \
driver so it reads it (`lernie message`). Enter sends it; typed \
whole, it is `/message <text…>`.",
)
.on_disabled_hover_text("type something first — an empty message sends nothing")
.clicked();
if msg_on && (send || ctx.entered) {
super::focus::request(state);
super::dispatch::message(model, state, &ctx.key, &ctx.ws, agent, &said);
}
interrupt_control(ui, model, state, ctx, agent, &said, msg_on);
nudge_control(ui, model, ctx, agent);
hold_controls(ui, model, ctx, agent);
stop_controls(ui, model, state, ctx);
} else {
let on = new_prompt_enabled(&ctx.text, &state.actions.path_dir);
let send = ui
.add_enabled(on, egui::Button::new("New prompt"))
.on_hover_text(
"Start a new conversation in this workspace from this text — a \
detached `lernie prompt` that keeps running whatever yog does. \
Enter starts it; typed whole, it is `/prompt <goal…>`.",
)
.on_disabled_hover_text("type something first — an empty prompt starts nothing")
.clicked();
if on && (send || ctx.entered) {
super::focus::request(state);
super::fire::fire_start(model, state, &ctx.key, &said);
}
}
});
}
fn interrupt_control(
ui: &mut egui::Ui,
model: &mut AppModel,
state: &mut ShellState,
ctx: &VerbCtx,
agent: &str,
said: &str,
armed: bool,
) {
let clicked = ui
.add_enabled(armed, egui::Button::new("Interrupt"))
.on_hover_text(
"Cut this conversation off mid-work and send it this text instead: it stops what is \
running and then deposits the message (`lernie stop`, then `lernie message`), and \
the deposit is what starts it going again. Work already committed is kept, and a \
tool call cut off mid-flight is reported to the model as having produced no result. \
Ctrl+Enter does the same; typed whole, it is `/interrupt <text…>`.",
)
.on_disabled_hover_text("type something first — an interrupt with nothing to say is a stop")
.clicked();
if armed && (clicked || ctx.interrupted) {
super::focus::request(state);
super::dispatch::interrupt(model, state, &ctx.key, &ctx.ws, agent, said);
}
}
fn nudge_control(ui: &mut egui::Ui, model: &mut AppModel, ctx: &VerbCtx, agent: &str) {
let on = ctx.nudgeable;
if ui
.add_enabled(on, egui::Button::new("Nudge"))
.on_hover_text(
"Run the model on this conversation as it already stands — no new message, no \
goal retyped (`lernie advance`). It is the re-dispatch for a first turn that \
died before it reached the model: sign in, press this, and the same \
conversation carries on. Typed, it is `/nudge`.",
)
.on_disabled_hover_text(
"something is already running this conversation — a nudge would find the \
driver's lease taken and do nothing",
)
.clicked()
{
super::dispatch::nudge(model, &ctx.ws, agent);
}
}
fn hold_controls(ui: &mut egui::Ui, model: &mut AppModel, ctx: &VerbCtx, agent: &str) {
use crate::control::judge::Ruling;
let Some(held) = ctx.held.as_ref() else {
return;
};
ui.weak("⏸ held").on_hover_text(held.reason.clone());
if ui
.button("Approve")
.on_hover_text(
"Let this one parked tool call through and drive the conversation on. \
The approval is scoped to this exact call — the next one is judged afresh. \
Typed, it is `/answer pass`.",
)
.clicked()
{
super::dispatch::answer_hold(model, &ctx.ws, agent, Ruling::Pass);
}
if ui
.button("Decline")
.on_hover_text(
"Refuse this one parked tool call and drive the conversation on. The model \
is told why and carries on from there — nothing is stopped or killed. \
Typed, it is `/answer refuse`.",
)
.clicked()
{
super::dispatch::answer_hold(model, &ctx.ws, agent, Ruling::Refuse);
}
}
fn stop_controls(ui: &mut egui::Ui, model: &mut AppModel, state: &mut ShellState, ctx: &VerbCtx) {
if ctx.stop_children {
ui.checkbox(&mut state.actions.stop_children, "children")
.on_hover_text(
"Stop the agents this conversation spawned too, not only the one at \
its root. Typed, it is `/stop children`.",
);
}
if ui
.add_enabled(ctx.stoppable, egui::Button::new("Stop"))
.on_hover_text(STOP_HINT)
.on_disabled_hover_text(
"nothing is running on this conversation — there is no driver to kill",
)
.clicked()
{
super::dispatch::stop_selected(model, state);
}
}