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:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 00:15:50 -04:00
1 parent 357de3f7a5
commit 34200013a4
3 files changed
+80 -2

No files matched your search

+45 -1
View File
@@ -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());