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

+29
View File
@@ -509,6 +509,15 @@ pub fn entry_of(command: &str) -> Option<&'static Capability> {
CATALOGUE.iter().find(|c| c.command() == Some(command)).or_else(|| CATALOGUE.iter().find(|c| c.more.contains(&command)))
}
/// The tab the ribbon follows to when `command` has run from anywhere but the ribbon (a script,
/// the control channel, MCP, the command search, the browser), as a person's click on that tab
/// would: its entry's tab. `None` for commands on the Tools tab (files, history, the model
/// summary: on every workspace and no stage of an analysis) and for unknown commands. A ribbon
/// click runs a command of the tab shown, so it never moves the ribbon (a test checks it).
pub fn stage_tab(command: &str) -> Option<Tab> {
entry_of(command).map(|c| c.tab).filter(|t| *t != Tab::Tools)
}
/// The entries of a tab (one run of the catalogue).
pub fn tab_entries(tab: Tab) -> &'static [Capability] {
let start = CATALOGUE.iter().position(|c| c.tab == tab).unwrap_or(CATALOGUE.len());
@@ -570,6 +579,26 @@ mod tests {
}
}
#[test]
fn the_ribbon_follows_each_command_to_its_stage() {
assert_eq!(stage_tab("mesh.hex.sweep"), Some(Tab::Mesh));
assert_eq!(stage_tab("model.material.create"), Some(Tab::Properties));
assert_eq!(stage_tab("model.material.edit"), Some(Tab::Properties));
assert_eq!(stage_tab("model.load.create"), Some(Tab::Loads));
assert_eq!(stage_tab("solve.static"), Some(Tab::Solve));
assert_eq!(stage_tab("results.contour"), Some(Tab::Results));
assert_eq!(stage_tab("edit.undo"), None);
assert_eq!(stage_tab("file.save"), None);
assert_eq!(stage_tab("model.info"), None);
assert_eq!(stage_tab("no.such"), None);
// A ribbon click (an entry's command, or one of its drop-down's) stays on the tab shown.
for c in CATALOGUE {
for command in c.command().into_iter().chain(c.more.iter().copied()) {
assert!(stage_tab(command).is_none_or(|t| t == c.tab), "{}: `{command}` would move the ribbon from {:?}", c.id, c.tab);
}
}
}
#[test]
fn every_command_is_claimed_and_every_ready_entry_runs_a_command() {
let mut claimed: Vec<&str> = Vec::new();