use crate::AppModel;
use crate::boundary::reply::Reply;
use crate::boundary::{Action, Query};
use crate::world::marks;
use std::path::{Path, PathBuf};
const BRANCH_HINT: &str = "The balls branch this agent records its tasks on. `balls/tasks` is the \
project's shared board — the branch an agent is pointed at when it is raised to work an \
existing project. Anything else is this agent's own task space, which no other agent's \
churn reaches. Typed, it is `/marks <branch>`.";
pub struct MarksPane {
workspace: Option<PathBuf>,
branch: String,
reading: Option<PathBuf>,
input: String,
act: crate::shell::act::Held,
}
impl MarksPane {
pub fn resolve() -> Self {
Self {
workspace: None,
branch: String::new(),
reading: None,
input: String::new(),
act: crate::shell::act::Held::default(),
}
}
}
pub fn render(ui: &mut egui::Ui, model: &mut AppModel, pane: &mut MarksPane) {
ui.heading(
egui::RichText::new("task branch (this agent's balls space)")
.color(crate::theme::integration_hue("bl")),
);
let Some(workspace) = model.focused_workspace() else {
ui.weak("focus a workspace to read or amend the branch it tracks on");
return;
};
settle(model, pane);
read_marks(model, pane, &workspace);
ui.monospace(workspace.display().to_string());
if pane.workspace.as_ref() == Some(&workspace) && !pane.branch.is_empty() {
ui.label(format!("branch: {}", pane.branch));
}
ui.weak(
"each agent tracks on a branch of its own; subagents inherit the space they were \
dispatched from",
);
crate::shell::row::peers(ui, |ui| {
if ui
.button("Read current")
.on_hover_text(
"Ask which branch this agent's tasks are recorded on right now. \
Changes nothing. Typed, it is `/marks` with no branch.",
)
.clicked()
{
pane.reading = Some(workspace.clone());
}
if ui
.button("Point at the project's board")
.on_hover_text(
"Track on `balls/tasks` — the project's shared board, where a claim this \
agent makes is the same claim `bl list` shows. Typed, it is \
`/marks balls/tasks`.",
)
.clicked()
{
apply_marks(model, pane, &workspace, marks::SHARED_BRANCH);
}
});
crate::shell::row::peers(ui, |ui| {
ui.label("branch:").on_hover_text(BRANCH_HINT);
ui.add(egui::TextEdit::singleline(&mut pane.input).desired_width(f32::INFINITY))
.on_hover_text(BRANCH_HINT);
if ui
.button("Set branch")
.on_hover_text(
"Point this agent's task space at the branch named beside it. Typed, it \
is `/marks <branch>`.",
)
.clicked()
{
let named = pane.input.trim().to_owned();
if marks::lawful(&named) {
apply_marks(model, pane, &workspace, &named);
} else {
pane.act.say(marks::REFUSAL.to_owned());
}
}
});
if !pane.act.quiet() {
ui.weak(pane.act.line());
}
}
fn read_marks(model: &mut AppModel, pane: &mut MarksPane, workspace: &Path) {
if pane.reading.as_deref() != Some(workspace) {
return;
}
let query = Query::Marks {
workspace: model.snap.ws_name(workspace),
};
let landed_branch = crate::shell::wire::ask(model, query, |reply| match reply {
Reply::Marks { branch } => Some(branch),
_ => None,
});
if let Some(said) = landed_branch.refused {
pane.act.say(said);
return;
}
if let Some(branch) = landed_branch.value {
pane.workspace = Some(workspace.to_path_buf());
pane.branch = branch;
}
}
fn apply_marks(model: &mut AppModel, pane: &mut MarksPane, workspace: &Path, branch: &str) {
let action = Action::SetMarks {
workspace: model.snap.ws_name(workspace),
branch: branch.to_owned(),
};
pane.workspace = Some(workspace.to_path_buf());
pane.act
.fire(model, &action, &format!("tracking on {branch}"));
}
fn settle(model: &mut AppModel, pane: &mut MarksPane) {
let Some(landed) = pane.act.landed(model) else {
return;
};
match (crate::shell::act::trouble(&landed), landed) {
(None, Ok(Reply::Marks { branch })) => {
pane.act.say(format!("tracking on {branch}"));
pane.branch = branch;
}
(None, Ok(other)) => pane.act.say(format!("unexpected reply: {other:?}")),
(why, _) => pane.act.say(why.unwrap_or_default()),
}
}