forked from 1b-agent/fieldcraft-upstream
The ribbon follows each command to its stage
A command run by a script, the control channel, MCP, the command search or the browser left the ribbon on whatever tab it showed, so an agent's session never showed the stage it was at. `catalogue::stage_tab` is the rule, from the catalogue: a command's entry tab, none for the Tools tab (files, history, the model summary). The app follows it after a command that changed the model or the results display (queries leave the ribbon alone); a script follows its last command with a stage. A ribbon click runs a command of the tab shown, so it never moves the ribbon: a test checks every entry and drop-down command against its tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
357de3f7a5
commit
34200013a4
3 files changed
+80
-2
No files matched your search
@@ -369,6 +369,9 @@ impl ModelApp {
|
||||
}
|
||||
// The results display commands reply with what is shown, which the viewport shows.
|
||||
let display = (id.starts_with("results.") && id != "results.reactions") || id == "solve.run";
|
||||
if r.is_ok() && (changed || display) {
|
||||
self.follow_stage(id);
|
||||
}
|
||||
match &r {
|
||||
Ok(v) if v.is_null() || changed || display => self.set_status(format!("{label} done"), false),
|
||||
Ok(v) => self.set_status(format!("{label}: {v}"), false),
|
||||
@@ -377,10 +380,17 @@ impl ModelApp {
|
||||
r
|
||||
}
|
||||
|
||||
/// Run a script (a JSON array of `{command, params}`), stopping at the first error.
|
||||
/// Run a script (a JSON array of `{command, params}`), stopping at the first error. The
|
||||
/// ribbon follows its last command that has a stage.
|
||||
pub fn run_script(&mut self, script: &Value) -> Result<(), String> {
|
||||
let r = self.session.run_script(script).map(|_| ()).map_err(|e| e.to_string());
|
||||
self.model_changed();
|
||||
if r.is_ok() {
|
||||
let commands = script.as_array().into_iter().flatten().filter_map(|step| step.get("command").and_then(Value::as_str));
|
||||
if let Some(id) = commands.rev().find(|id| catalogue::stage_tab(id).is_some()) {
|
||||
self.follow_stage(id);
|
||||
}
|
||||
}
|
||||
match &r {
|
||||
Ok(()) => self.set_status("Script done", false),
|
||||
Err(e) => self.set_status(format!("Script: {e}"), true),
|
||||
@@ -388,6 +398,16 @@ impl ModelApp {
|
||||
r
|
||||
}
|
||||
|
||||
/// The ribbon turns to the tab where `id` lives ([`catalogue::stage_tab`]), as a person's click
|
||||
/// there would, so commands run by scripts, the control channel and MCP show their stage.
|
||||
/// Called after a command changed the model or the results display; queries leave the ribbon
|
||||
/// alone, and a ribbon click never moves it.
|
||||
fn follow_stage(&mut self, id: &str) {
|
||||
if let Some(tab) = catalogue::stage_tab(id) {
|
||||
self.ui.select_tab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
/// What a click on a toolbar entry, a menu row or a palette result does: a command without
|
||||
/// parameters runs, one with parameters opens its form, and the file entries (open, save,
|
||||
/// write a deck, recover) go through the host's file pickers. `id` is a command or a
|
||||
@@ -872,6 +892,30 @@ mod tests {
|
||||
assert!(app.status.as_ref().is_some_and(|s| s.text.contains("\"nodes\":1")));
|
||||
}
|
||||
|
||||
/// Commands run from outside the ribbon bring their tab; queries and Tools commands leave it.
|
||||
#[test]
|
||||
fn the_ribbon_follows_commands_to_their_stage() {
|
||||
let mut app = ModelApp::new(Session::default());
|
||||
app.ui.select_tab(Tab::Results);
|
||||
app.run("mesh.node.add", json!({"label": 1, "xyz": [0, 0, 0]})).unwrap();
|
||||
assert_eq!((app.ui.workspace, app.ui.tab), (Workspace::Prepare, Tab::Mesh));
|
||||
app.run("model.material.create", json!({"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3}})).unwrap();
|
||||
assert_eq!((app.ui.workspace, app.ui.tab), (Workspace::Model, Tab::Properties));
|
||||
// A query and a failed command leave it; so does undo (Tools).
|
||||
app.run("mesh.quality", json!({})).ok();
|
||||
let _ = app.run("model.load.create", json!({"name": "bad"}));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.ui.tab, Tab::Properties);
|
||||
// A script follows its last command with a stage.
|
||||
app.run_script(&json!([{"command": "mesh.node.add", "params": {"label": 2, "xyz": [1, 0, 0]}}, {"command": "model.info", "params": {}}]))
|
||||
.unwrap();
|
||||
assert_eq!(app.ui.tab, Tab::Mesh);
|
||||
// The ribbon's own entries never move it.
|
||||
app.ui.select_tab(Tab::Properties);
|
||||
app.start("model.units").unwrap();
|
||||
assert_eq!(app.ui.tab, Tab::Properties);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn starting_a_command_opens_its_form_and_running_it_closes_the_form() {
|
||||
let mut app = ModelApp::new(Session::default());
|
||||
|
||||
Reference in new issue
Block a user