Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

.nornir/scene-design.md

dwarves-gui shell — the declarative facett-scene migration — design

Class: design · Function axis: f0 (the tabbed shell as data) · f1 the dwarves_scene() builder · f2 TabbedView as a SceneHost (reuse, not rewrite) · f3 render wiring + the state seam · f4 interactions (draggable split · drawer · foldable tabs · responsive · menubar) · f5 tests. Strict: this file states only the shape we run. The shell lives in dwarves-gui/src/tabbed.rs; the headless release engine crates/dwarves is UNTOUCHED. Reference: ../../facett/.nornir/scene-design.md (the facett-app scene LAW doc).

f0 — the tabbed shell as DATA

dwarves-gui's native window was a hand-rolled TabbedView (a manual tab-bar strip + a warehouse-selector/About header). It is now DESCRIBED as one facett_app::scene::Scene value — a menubar + a foldable pane tree + routes — and rendered through facett-app's OWN primitives (Router / TabView / facett-menu) by scene::render. Nothing was forked: the scene DRIVES the SAME existing Box<dyn Panel> tabs and the SAME warehouse selector / About widgets.

The builder is dwarves_scene(tab_labels) (dwarves-gui/src/tabbed.rs), derived from whatever tab labels the shell is constructed with. Those labels come from ONE place — dwarves_gui::DWARVES_TABS, the (slot-id, label) roster in dwarves-gui/src/lib.rs (korp's proven scene_shell::KORP_TABS shape). The shipped shell, the scene, the menubar and the whole robot suite all read that const, so there is no second hardcoded tab list anywhere and "no tab dropped" is an oracle the compiler and the tests enforce, not a comment (see f5).

shell  ─ Split(H, 0.20, DRAGGABLE)              [tabbed.rs:64 · phone → Tabs under 760pt]
├─ rail  (Foldable 🗄, DRAWER@760) slot:rail     — warehouse selector + About; phone drawer
└─ body ─ Tabs(active 0)                         — the workspace (tabbed.rs:60)
   ├─ warehouse (Foldable 🗄) slot:warehouse     ─┐
   ├─ forge     (Foldable 🔨) slot:forge          │  one FOLDABLE Slot per existing
   ├─ pipeline  (Foldable 🪈) slot:pipeline       │  tab panel; the Slot id = the
   ├─ builds    (Foldable 🧱) slot:builds         │  lowercased label, the SAME key
   ├─ funnel    (Foldable 🫙) slot:funnel         │  the shell folds each pane's
   ├─ claude    (Foldable ✳) slot:claude          │  state under.
   ├─ jera      (Foldable 🗂) slot:jera            │
   ├─ tools     (Foldable 🧰) slot:tools          │
   ├─ status    (Foldable 🚦) slot:status         │
   ├─ ci-cd     (Foldable ⚙) slot:ci-cd           │  …the roster is 18 tabs; the
   ├─ release   (Foldable 🚀) slot:release        │  order here is the DWARVES_TABS
   ├─ benches   (Foldable 📊) slot:benches        │  order (warehouse · forge ·
   ├─ fleet     (Foldable 🛰) slot:fleet          │  pipeline · ci/cd · builds ·
   ├─ binary    (Foldable 🧬) slot:binary         │  funnel · claude · jera · tools ·
   ├─ coderoom  (Foldable 📚) slot:coderoom       │  status · release · benches ·
   ├─ saga      (Foldable 🎞) slot:saga           │  fleet · binary · coderoom ·
   ├─ settings  (Foldable 🎛) slot:settings       │  saga · settings · profile).
   └─ profile   (Foldable 👤) slot:profile       ─┘
menubar: View (fold toggles) · Go (switch tab, Ctrl+1..N) · Help (About, F1)
routes : on_button focus-warehouse→Focus · on_shortcut Ctrl+Home→Show · on_event toggle-rail→Toggle(rail)

The three stable pane ids are ROOT/RAIL/BODY (tabbed.rs:28-32). Scene::validate (the addressability invariant — unique pane + menu names) is asserted in TabbedView::new (tabbed.rs:166).

f1 — the dwarves_scene() builder (Describe)

dwarves_scene (tabbed.rs:71) uses the Rust builder form: Pane::slot/.title/ .foldable_icon per tab, Pane::tabs for the workspace, Pane::split (DRAGGABLE by default) for the master/detail root, .drawer(760) on the rail, .phone(Layout::Tabs, 760) on the root, and Scene::new(...).menu(...).route(...). The identical value round-trips through canonical RON (Scene::to_ron/from_ron, byte-stable — the RED-when-broken test below).

f2 — TabbedView is a thin SceneHost (Host — reuse, not rewrite)

TabbedView (tabbed.rs:135) now carries the scene + its nav: NavState + last_render, and it is impl Facet so it still drops into the facett-wasm-demo FacetApp harness unchanged (lib.rs build_native_app). The slot host is Host (tabbed.rs:406, impl SceneHost at :418):

f3 — render wiring + the observable seam (Render + Observe)

TabbedView::ui (tabbed.rs:279) splits the struct into disjoint borrows, builds Host over the slot widgets, and calls scene::render(&scene, &mut nav, ui, &mut host) (:296) — replacing the old manual tab-bar draw. A go:<tab> command then drives the body Tabs active index; a rail warehouse change re-binds every tab (the same broadcast nornir's switch_workspace does).

Facet::state_json (tabbed.rs:317) folds the render Value in under render, and adds the shell-as-data facts (scene, panes, foldable, drawers, draggable_splits, folded, split_ratio, menus, routes) ALONGSIDE the pre-existing keys (active_tab, tabs, selected_warehouse, per-tab folded state) — additive, so every existing robot oracle still resolves. Facet::update_json gains {"toggle_fold":"<pane>"} and drives {"set_tab":…} through the scene's body Tabs active index; unknown messages still forward to the active pane.

f4 — interactions (all declarative, all observable)

f5 — Tests (L4, recorded matrix rows)

In dwarves-gui/src/tabbed.rs mod scene_shell_tests (:1156), all green in the worktree (cargo test -p dwarves-gui, 32 passed incl. the pre-existing robot suite — no regressions):

The roster oracle + the dead lib/bin twin

dwarves-gui used to be a lib/bin TWIN, not a lib/bin split: main.rs re-declared every module lib.rs declared, so each file compiled TWICE into two separate crates — and the two copies then silently DRIFTED. The lib's build_native_app had an always-thin endpoint: String funnel and 9 tabs; the bin's had the FAT/embedded nornir_server: Option<String> default and 18. The compiler could not see the divergence, and tests/robot_scene.rs — which linked the LIB — was therefore driving a 9-tab shell while asserting it was "the EXACT shipped native shell".

That is fixed:

Because the suite now reads the roster instead of a copied 9-name list, all 7 tests/robot_scene.rs scene tests cover 18 panes rather than 9 — the tab switch + slot state, fold/unfold, menubar View/Go items, and deep-link-by-pane-name checks all scale with the shell. The shipped app is additionally exercised end-to-end by app_tests::robot_zone_framework_rollout_no_duality_no_demo_button (now in lib.rs).