forked from 1b-agent/fieldcraft-upstream
ui: jobs with progress, refusals listed, datum preview, form filters and defaults
- A solve or a mesher runs as a job with one progress mechanism: the overlay shows the step, stage, count and elapsed time with a bar when bounded, OK waits while it runs, and the status sums it up (elements, smallest angle, time) (#322, #357). - Over the control channel such a command is a job too: answered when it ends however long it runs, or at once with wait: false, then job.status, job.wait, job.cancel; ui.inspect gives job; MCP waits for it (#367). - A command that cannot run yet lists what it misses in the context panel, each with a button to the command that provides it (#323). - A cancelled or refused solve stays in its workspace (#324, test). - A mesher form starts from the parameters of the mesh it would replace (#343). - Seed Edges and edge/vertex/face datum points switch the selection filter while open and restore it (#341). - Datum forms draw the datum live, the Partition form shows its datum, and new datums and partitions get the next free name (#347). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
3eaca26246
commit
4ee855bb27
11 files changed
+447
-28
No files matched your search
@@ -33,8 +33,16 @@ impl ControlRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// The key of the interim reply to a request that waits for a background job
|
||||
/// ([`Outcome::Pending`]): the server then waits for the final reply however long the job takes,
|
||||
/// since the job always answers (its result, its error, or that it was cancelled).
|
||||
pub const PENDING: &str = "pending";
|
||||
|
||||
pub enum Outcome {
|
||||
Done(Value),
|
||||
/// The reply comes when the background job finishes: the request's sender waits in
|
||||
/// [`crate::files::Loading::waiters`].
|
||||
Pending,
|
||||
/// A window screenshot, answered once a frame is presented.
|
||||
Screenshot {
|
||||
path: Option<String>,
|
||||
@@ -170,6 +178,17 @@ fn privacy_json(app: &ModelApp) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
/// Answer `req` when the running job finishes; with none running, at once.
|
||||
fn wait_job(app: &mut ModelApp, req: &ControlRequest) -> Outcome {
|
||||
match app.loading.as_mut() {
|
||||
Some(l) => {
|
||||
l.waiters.push(req.reply.clone());
|
||||
Outcome::Pending
|
||||
}
|
||||
None => ok(json!({"job": null})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome {
|
||||
let p = &req.params;
|
||||
let s = |k: &str| p.get(k).and_then(Value::as_str);
|
||||
@@ -182,8 +201,25 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
"engine.execute" => {
|
||||
let Some(id) = s("command") else { return err("missing `command`") };
|
||||
let params = p.get("params").cloned().filter(|v| !v.is_null()).unwrap_or(json!({}));
|
||||
// A solve or a mesher runs as a job: the window keeps drawing its progress, and the
|
||||
// reply comes when it is done however long that takes (or at once with `"wait":
|
||||
// false`, then `job.status` / `job.wait` / `job.cancel`).
|
||||
if crate::files::is_long(id) {
|
||||
return match app.start_job(id, params) {
|
||||
Err(e) => err(e),
|
||||
Ok(job) if p.get("wait").and_then(Value::as_bool) == Some(false) => ok(json!({"job": job})),
|
||||
Ok(_) => wait_job(app, req),
|
||||
};
|
||||
}
|
||||
wrap(app.run(id, params))
|
||||
}
|
||||
"job.status" => ok(json!({"job": app.loading.as_ref().map(|l| l.to_json(app.now))})),
|
||||
"job.wait" => wait_job(app, req),
|
||||
"job.cancel" => {
|
||||
let job = app.loading.as_ref().map(|l| l.to_json(app.now));
|
||||
app.cancel_loading();
|
||||
ok(json!({"cancelled": job}))
|
||||
}
|
||||
"engine.script" => {
|
||||
let Some(steps) = p.get("commands") else { return err("missing `commands` (a list of {command, params})") };
|
||||
wrap(app.run_script(steps).map(|()| json!({"journal": app.session.journal.len()})))
|
||||
@@ -201,6 +237,7 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
"results": app.session.results_report(),
|
||||
"probe": app.probe,
|
||||
"busy": app.busy(),
|
||||
"job": app.loading.as_ref().map(|l| l.to_json(app.now)),
|
||||
"solve": solve_json(app),
|
||||
"selected": app.selected.as_ref().map(|(k, n)| json!({"kind": k, "name": n})).or_else(|| app.selected_seed.as_ref().map(|e| json!({"kind": "seed", "edge": e}))),
|
||||
"browser": crate::browser::describe(app),
|
||||
@@ -634,10 +671,15 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn call(app: &mut ModelApp, method: &str, params: Value) -> Value {
|
||||
let (req, _rx) = ControlRequest::new(method, params);
|
||||
let (req, rx) = ControlRequest::new(method, params);
|
||||
match handle(app, &egui::Context::default(), &req) {
|
||||
Outcome::Done(v) => v,
|
||||
Outcome::Screenshot { .. } => json!({"screenshot": true}),
|
||||
// A job's reply comes when it ends.
|
||||
Outcome::Pending => {
|
||||
app.wait_loaded();
|
||||
rx.try_recv().unwrap_or(Value::Null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,10 +734,15 @@ mod tests {
|
||||
}
|
||||
|
||||
fn call_in(app: &mut ModelApp, ctx: &egui::Context, method: &str, params: Value) -> Value {
|
||||
let (req, _rx) = ControlRequest::new(method, params);
|
||||
let (req, rx) = ControlRequest::new(method, params);
|
||||
match handle(app, ctx, &req) {
|
||||
Outcome::Done(v) => v,
|
||||
Outcome::Screenshot { .. } => json!({"screenshot": true}),
|
||||
// A job's reply comes when it ends.
|
||||
Outcome::Pending => {
|
||||
app.wait_loaded();
|
||||
rx.try_recv().unwrap_or(Value::Null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user