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

nordisk/dwarves

dwarves-gui/src/tabbed.rs

//! **TabbedView** — a tiny tab shell that turns dwarves-gui into a small TABBED app
//! (**Forge | Funnel | Claude**) without touching any pane.
//!
//! It is itself a [`Facet`] (so the `facett-wasm-demo` harness mounts it exactly like
//! any other panel), holding one `Box<dyn Panel>` per tab. The **Forge** tab is the
//! existing [`ForgeView`](crate::panel::ForgeView); the **Funnel** tab is the LIVE
//! `nornir-funnel-embed` pane (backend (2)); the **Claude** tab is the shared
//! `facett-claude::ClaudePane` (direct-Claude chat) hosted by [`ClaudeTab`] over the
//! shared `nornir-claude` edda leaf's transport (offline stub by default; live HTTP
//! behind the `claude-http` feature) — no dwarves-local Claude client. Additive: no pane is modified, and the demo's
//! `__dwarves_state` hook still returns valid JSON — folded per tab
//! (`{ "active_tab", "tabs", "forge": …, "funnel": …, "claude": … }`).
//!
//! **The tab INTERNALS adopted the scene too.** dwarves' SHELL was already the
//! constellation's reference — a draggable master/detail root, a drawer rail, foldable
//! panes — but every tab BODY was still ONE `Slot` holding one imperative egui flow, so
//! minimise / maximise / drag operated on a whole tab and could do NOTHING inside it: a
//! readout that grew came straight out of the list or canvas it shared the flow with,
//! with no divider to correct it. A pane that implements [`SplitPanel`] is mounted as
//! [`TabPanel::Split`] and its body becomes `Split(axis, ratio, DRAGGABLE)` over
//! `<slug>-main` | `<slug>-inspector` — two chromed, foldable scene panes with a divider
//! between them. See [`body_split`] for the roster and for WHY most tabs are not on it.
//!
//! **And a THIRD shape, for a body that is genuinely more than two things.** A pane that
//! implements [`LeafPanel`] is mounted as [`TabPanel::Leaves`] and its body becomes the
//! whole [`Pane`] subtree its own module declares ([`body_leaves`]) — N named, chromed,
//! foldable leaves over N-1 draggable dividers. ⚒ Build Thing is the one body on it:
//! `build_thing_scene::body()` had declared its leaves over their dividers since it was
//! written and had **no callers**, because the two-way shape above was the ceiling. The
//! opt-in reasoning is unchanged — most tabs stay whole, and a thin wrapper over a shared
//! facett widget still cannot decompose at all.
//!
//! ⚠ **facett patches wanted**, in priority order:
//! 1. ~~`TabView::show` neither wraps nor scrolls~~ — **FIXED UPSTREAM, and this note was
//!    a month out of date.** `facett-app/src/tabview.rs:87` puts the header row in an
//!    `egui::ScrollArea::horizontal`, landed in facett `cdb3cb63` on **2026-07-22**; its
//!    comment names this very host ("dwarves lost its 18th tab exactly this way"). This
//!    paragraph was written on **2026-08-21**, a month AFTER the fix, by describing the
//!    defect from memory instead of opening the file — so a solved problem stayed on a
//!    wanted-list, and the reader below was told to route around something that works.
//!    facett is a PATH dep here, so dwarves has always built against the fixed code.
//!    Kept rather than deleted, because a wanted-list that silently loses entries cannot
//!    be audited: what belongs in the record is that it was wanted, and is done.
//! 2. Most dwarves tabs are thin wrappers whose body is `self.inner.ui(ui)` over a
//!    SHARED facett widget (`facett_bench_view` · `facett_fleet_view` · `facett_saga_view`
//!    · `facett_binview` · `facett_coderoom` · `facett_matrix` · `facett_releaseview` …).
//!    Their master/detail lives INSIDE facett, so they cannot decompose here. Giving those
//!    widgets a `ui_main` / `ui_inspector` pair (the [`SplitPanel`] shape) would let every
//!    host decompose them.
//! 3. `scene::Pane::header_title` is private, so a host cannot audit the AccessKit labels
//!    its own scene publishes — and chrome is default-ON, which makes every pane TITLE an
//!    addressable node where a duplicate makes BOTH unreachable.

use facett_app::scene::{
    self, Action, Axis, Layout, MenuItem, NavState, Pane, Route, Scene, SceneHost, Target,
};
use facett_core::{Facet, Panel};
use serde_json::{json, Value};

use crate::warehouse_select::WarehouseSelector;

// ── the declarative shell, as DATA (facett-app `scene`) ────────────────────────
// The whole tabbed shell is a [`Scene`]: a menubar + a foldable pane tree + routes.
// `TabbedView` is a thin [`SceneHost`] that fills each `Slot` by driving the SAME
// existing `Box<dyn Panel>` (reuse, not rewrite). The three stable pane ids:

/// The root master/detail split pane (rail | workspace). `pub` so the
/// `tests/robot_scene.rs` integration suite can address the shell split by id.
pub const ROOT: &str = "shell";
/// The left rail pane (warehouse selector + About) — a foldable DRAWER. `pub` for the
/// robot suite (drawer / fold assertions address it by id).
pub const RAIL: &str = "rail";
/// The workspace pane — a `Tabs` layout over the existing tab panels. `pub` for the
/// robot suite (tab-switch assertions address it by id).
pub const BODY: &str = "body";

/// The stable SLOT ID for a tab label — the scene pane name, the `state_json` fold key
/// and the deep-link (`app#pane`) path segment, all in one.
///
/// Lowercase, with every non-alphanumeric run collapsed to a single `-` (trimmed at the
/// ends). Plain one-word labels are unchanged (`"Warehouse"` → `"warehouse"`), but a
/// label carrying punctuation can never leak it into an addressable id: `"CI/CD"` →
/// `"ci-cd"`, not `"ci/cd"`. A `/` in a pane name is a latent trap — it is a path
/// separator in the deep-link form — so it is normalised HERE, in the one place every id
/// is derived, rather than by renaming the user-visible label.
pub fn slug(label: &str) -> String {
    let mut out = String::with_capacity(label.len());
    for ch in label.chars() {
        // A Nordic letter keeps its base letter (`Mímir` → `mimir`), never a `-` hole.
        let ch = match ch {
            'á' | 'Á' | 'å' | 'Å' | 'ä' | 'Ä' => 'a',
            'é' | 'É' => 'e',
            'í' | 'Í' => 'i',
            'ó' | 'Ó' | 'ö' | 'Ö' | 'ø' | 'Ø' => 'o',
            'ú' | 'Ú' => 'u',
            other => other,
        };
        if ch.is_ascii_alphanumeric() {
            out.push(ch.to_ascii_lowercase());
        } else if !out.ends_with('-') {
            out.push('-');
        }
    }
    out.trim_matches('-').to_string()
}

/// A stable header glyph per known tab (decorates the foldable tab header).
fn tab_icon(label: &str) -> &'static str {
    match slug(label).as_str() {
        "warehouse" => "🗄",
        "forge" => "🔨",
        "pipeline" => "🪈",
        "builds" => "🧱",
        "build-thing" => "⚒",
        "funnel" => "🫙",
        "claude" => "✳",
        "jera" => "🗂",
        "data-view" => "🏢",
        "tree" => "🌳",
        "status" => "🚦",
        "ci-cd" => "⚙",
        "release" => "🚀",
        "mimir-release" => "🌳",
        "benches" => "📊",
        "fleet" => "🛰",
        "binary" => "🧬",
        "profile-an-app" => "🔬",
        "coderoom" => "📚",
        "saga" => "🎞",
        "settings" => "🎛",
        "profile" => "👤",
        "manual" => "📖",
        _ => "▪",
    }
}

// ── the tab INTERNALS: a body that decomposes into a master/detail pair ────────

/// **A tab body that DECOMPOSES into a master/detail pair of scene panes.**
///
/// dwarves' shell was already the constellation's reference: a draggable master/detail
/// root, a drawer rail, foldable panes. But every tab BODY was still ONE `Slot` holding
/// one imperative egui flow, so minimise / maximise / drag operated on a whole tab and
/// could do NOTHING inside it — a readout that grew (a badge SVG, a poll banner, a step
/// log) simply pushed the list or canvas it shared the flow with off the bottom, with no
/// divider to correct it.
///
/// A pane that implements this is mounted as [`TabPanel::Split`] and gets a
/// `Split(axis, ratio, DRAGGABLE)` body over two named, chromed, foldable scene panes:
/// `<slug>-main` (the list / table / canvas) and `<slug>-inspector` (the detail beside
/// it). It is a DECOMPOSITION, not a rewrite: `Facet::ui` keeps drawing both halves in
/// order, so the same pane still mounts whole in the unit tests that box it alone.
pub trait SplitPanel: Panel {
    /// The MASTER half — the list / table / canvas.
    fn ui_main(&mut self, ui: &mut egui::Ui);
    /// The INSPECTOR half — controls, readouts, detail. Returns its own observable state
    /// (folded into the scene's `render.slots` under `<slug>-inspector`).
    fn ui_inspector(&mut self, ui: &mut egui::Ui) -> Value;
}

/// **A tab body that decomposes into N NAMED LEAVES over a DECLARED pane tree.**
///
/// [`SplitPanel`] can express exactly one shape — a two-way `<slug>-main` |
/// `<slug>-inspector` pair — and that ceiling is the whole reason
/// `build_thing_scene::body()`, which declares THREE leaves over TWO dividers (five over four until the kernel became a node of the build graph and the matrix and settings became the graph and the menu), sat in
/// the tree with **no callers**: the shell could not host it, so the module read as the
/// running layout while describing a shape nothing rendered. This is the
/// generalisation. The tab hands the shell a whole [`Pane`] subtree ([`body_leaves`])
/// and the shell fills each of its leaves BY NAME.
///
/// It keeps every property [`body_split`]'s docs give as the reason most tabs are NOT
/// decomposed — a third shape must not quietly widen the roster:
///
/// * **a decomposition, not a rewrite** — `Facet::ui` still draws every leaf in reading
///   order, so the same pane mounts whole in the unit tests that box it alone, and the
///   `__dwarves_state()["<slug>"]` object keeps its exact shape;
/// * **opt-in per tab** — a body with no internal structure stays one honest `Slot`.
///   Forcing leaves on it buys dividers nobody can use and chrome bars that only steal
///   rows, which is worse than leaving it whole;
/// * **not available to a thin wrapper over a SHARED facett widget** — that widget's
///   master/detail lives inside facett and is not dwarves' to carve, exactly as for
///   [`SplitPanel`]. Nothing here changes that; it changes only what a tab whose body
///   dwarves DOES own is allowed to say about itself.
pub trait LeafPanel: Panel {
    /// Draw ONE leaf, addressed by the pane name the declared tree gives it, and return
    /// that leaf's own observable state (folded into the scene's `render.slots` under
    /// the same name).
    ///
    /// The name is one of [`body_leaf_names`] for this tab's slug — the SAME constants
    /// the declared tree is built from, so a leaf cannot be drawn under a name nothing
    /// declares. An implementation that is handed a name it does not know must SAY SO in
    /// the pane rather than draw nothing: a blank leaf is indistinguishable from a leaf
    /// whose content is empty, which is the defect this whole tab learned twice.
    fn ui_leaf(&mut self, leaf: &str, ui: &mut egui::Ui) -> Value;

    /// **Is this leaf's SUBJECT absent right now** — so the leaf must not EXIST in the
    /// tab this frame: no region, no title, no fold chrome?
    ///
    /// The declared tree ([`body_leaves`]) is the tab's full CAPABILITY; this is the
    /// per-frame fact of which declared leaves have a subject. The shell asks every
    /// frame, before layout, and drives the scene's runtime presence with the answer
    /// (`NavState::set_absent`) — so presence is always a consequence of the panel's own
    /// domain facts and never a stale flag. The ⚒ case that forced it: the kernel
    /// configurator leaf was HARDMOUNTED, so an operator saw a kernel pane frame before
    /// any recipe was resolved — content-gating the leaf (an empty or refusing pane)
    /// still left the frame on screen, and the owner's rule is strict: *the kernel leaf
    /// must not exist until the resolved recipe declares a `[[build.kernel]]`*.
    ///
    /// Defaulted to `false` — "every declared leaf always has a subject" is the honest
    /// answer for a body whose leaves are unconditional, and there is no second story
    /// the default could hide (a leaf wrongly present is on screen to see; a leaf
    /// wrongly absent fails the robot that addresses it).
    fn leaf_absent(&self, leaf: &str) -> bool {
        let _ = leaf;
        false
    }
}

/// One mounted tab: a pane that is honestly WHOLE, one that decomposes into a
/// master/detail pair, or one that decomposes into N named leaves.
pub enum TabPanel {
    /// One pane, one slot — the honest shape for a tab with no master/detail, and the
    /// only shape available for a tab that is a thin wrapper around a SHARED facett
    /// widget (the widget's own internals are not dwarves' to split).
    Whole(Box<dyn Panel>),
    /// A body that decomposes into `<slug>-main` | `<slug>-inspector`.
    Split(Box<dyn SplitPanel>),
    /// A body that decomposes into the N named leaves [`body_leaves`] declares for it.
    Leaves(Box<dyn LeafPanel>),
}

impl TabPanel {
    /// The pane as a plain [`Panel`] (title / state / update are identical either way).
    pub fn panel(&self) -> &dyn Panel {
        match self {
            TabPanel::Whole(p) => &**p,
            TabPanel::Split(p) => &**p,
            TabPanel::Leaves(p) => &**p,
        }
    }
    /// The pane as a mutable [`Panel`].
    pub fn panel_mut(&mut self) -> &mut dyn Panel {
        match self {
            TabPanel::Whole(p) => &mut **p,
            TabPanel::Split(p) => &mut **p,
            TabPanel::Leaves(p) => &mut **p,
        }
    }
    /// The decomposable pane, when this tab is one.
    fn split_mut(&mut self) -> Option<&mut dyn SplitPanel> {
        match self {
            TabPanel::Split(p) => Some(&mut **p),
            TabPanel::Whole(_) | TabPanel::Leaves(_) => None,
        }
    }
    /// The N-leaf pane, when this tab is one.
    fn leaves_mut(&mut self) -> Option<&mut dyn LeafPanel> {
        match self {
            TabPanel::Leaves(p) => Some(&mut **p),
            TabPanel::Whole(_) | TabPanel::Split(_) => None,
        }
    }
    /// The N-leaf pane, immutably — the presence question ([`LeafPanel::leaf_absent`])
    /// is a read of the panel's domain facts, so the frame-path ask never needs `&mut`.
    fn leaves(&self) -> Option<&dyn LeafPanel> {
        match self {
            TabPanel::Leaves(p) => Some(&**p),
            TabPanel::Whole(_) | TabPanel::Split(_) => None,
        }
    }
}

impl From<Box<dyn Panel>> for TabPanel {
    fn from(p: Box<dyn Panel>) -> Self {
        TabPanel::Whole(p)
    }
}

impl From<Box<dyn SplitPanel>> for TabPanel {
    fn from(p: Box<dyn SplitPanel>) -> Self {
        TabPanel::Split(p)
    }
}

impl From<Box<dyn LeafPanel>> for TabPanel {
    fn from(p: Box<dyn LeafPanel>) -> Self {
        TabPanel::Leaves(p)
    }
}

/// **The master/detail decomposition of ONE tab body** — the axis it divides along, the
/// master's seed share, and the two halves' titles + fold icons.
pub struct BodySplit {
    /// Which way the body divides.
    pub axis: Axis,
    /// The MASTER's seed share (what the divider then moves).
    pub ratio: f32,
    /// `(title, fold icon)` of the master half.
    pub main: (&'static str, &'static str),
    /// `(title, fold icon)` of the inspector half.
    pub inspector: (&'static str, &'static str),
}

/// The decomposition for a tab slug, or **`None` for a body that is honestly one pane**.
///
/// Forcing a split on a body with no master/detail is worse than leaving it whole: it
/// buys a divider nobody can use and a chrome bar that only steals rows. Most dwarves
/// tabs are deliberately absent here for a second, structural reason: they are thin
/// wrappers whose body is `self.inner.ui(ui)` over a SHARED facett widget
/// (`facett_bench_view` · `facett_fleet_view` · `facett_saga_view` · `facett_binview` ·
/// `facett_coderoom` · `facett_matrix` …). Their master/detail lives INSIDE facett, so
/// dwarves cannot decompose them without the shared widget exposing its own halves —
/// see the facett patch note in this module's docs.
///
/// **Every slug listed here MUST be mounted as [`TabPanel::Split`]** — asserted in
/// [`TabbedView::new`], so a declared split with no `SplitPanel` behind it is a loud
/// failure rather than two panes reading "no widget for slot".
pub fn body_split(slug: &str) -> Option<BodySplit> {
    match slug {
        // The health BOARD is the master; the expanded badge SVG is the detail. The SVG
        // used to be appended under the board in the same flow, so opening one pushed
        // the board up and (with enough pipelines) off the top of the pane.
        "status" => Some(BodySplit {
            axis: Axis::Horizontal,
            ratio: 0.52,
            main: ("Status · board", "🚦"),
            inspector: ("Status · badge", "🏷"),
        }),
        // The stage DAG is the master; the source selector + stage detail (which runa
        // file, how many stages, where the per-stage light came from, the reload) is the
        // inspector. That header WRAPPED — a long file name or a parse error added rows
        // that came straight out of the graph.
        "pipeline" => Some(BodySplit {
            axis: Axis::Horizontal,
            ratio: 0.70,
            main: ("Pipeline · DAG", "🪈"),
            inspector: ("Pipeline · source", "🔎"),
        }),
        // The three-graph convergence viewer is the master; the shooting-ramp band
        // (Gates · Ramp · Bump plan) is the detail. The band's detail GROWS — a ramp line
        // per member repo, a bump line per crate — and it sat ABOVE the graph, so every
        // line it grew came straight out of the graph's height.
        "release" => Some(BodySplit {
            axis: Axis::Horizontal,
            ratio: 0.62,
            main: ("Release · convergence", "🚀"),
            inspector: ("Release · ramp", "🎯"),
        }),
        // The jobs/zones tree is the master; the live-poll banner (refresh · live/paused ·
        // job count · source) is its control strip. The banner sat ABOVE the tree and
        // every row it grew came straight out of the tree's height.
        "jera" => Some(BodySplit {
            axis: Axis::Horizontal,
            ratio: 0.76,
            main: ("Jera · jobs", "🗂"),
            inspector: ("Jera · feed", "📡"),
        }),
        _ => None,
    }
}

/// **Every tab slug whose body decomposes into N NAMED LEAVES** — the roster
/// [`body_leaves`] answers for, and the one the slot host prefix-matches against so
/// dispatching a leaf costs no allocation on the frame path.
///
/// One entry today, and the list is here rather than inlined so adding a second N-leaf
/// body is one line in one place. **Every slug listed here MUST be mounted as
/// [`TabPanel::Leaves`]** — asserted in [`TabbedView::new`], the same way a declared
/// `body_split` must have a real [`SplitPanel`] behind it.
pub const LEAF_BODIES: [&str; 1] = [crate::build_thing_scene::TAB];

/// The declared pane subtree for a tab whose body is N NAMED LEAVES, or `None` for a
/// body that is one pane or a master/detail pair.
///
/// The tree is the tab's OWN module's — `build_thing_scene::body()` for ⚒ — not a shape
/// spelled here: the leaf names, the axes, the seed ratios and the phone fallback are
/// decisions about that tab's content, and a second copy of them in the shell is a
/// second thing that can disagree with the first. The returned pane's `name` IS the tab
/// slug, so it lands in the workspace `Tabs` as the tab itself.
#[must_use]
pub fn body_leaves(slug: &str) -> Option<Pane> {
    match slug {
        crate::build_thing_scene::TAB => Some(crate::build_thing_scene::body()),
        _ => None,
    }
}

/// The leaf pane names of an N-leaf body, in reading order — walked off the DECLARED
/// tree rather than listed, so a leaf added to the design appears here with no edit.
#[must_use]
pub fn body_leaf_names(slug: &str) -> Vec<String> {
    fn walk(p: &Pane, out: &mut Vec<String>) {
        match &p.content {
            scene::Content::Children(kids) => {
                for k in kids {
                    walk(k, out);
                }
            }
            _ => out.push(p.name.clone()),
        }
    }
    let mut out = Vec::new();
    if let Some(p) = body_leaves(slug) {
        walk(&p, &mut out);
    }
    out
}

/// Which N-leaf body a slot id belongs to, if any — `build-thing-kernel` → `build-thing`.
///
/// A prefix test rather than a roster walk because this runs on the FRAME path, once per
/// leaf per frame: building every declared subtree to compare names would allocate five
/// pane trees a frame to answer a question about a string. Slugs are unique and every
/// leaf is `<slug>-<something>`, so the prefix is decisive. A `<slug>-…` id that is not a
/// declared leaf still reaches the owning panel, which names it — see [`LeafPanel`].
fn leaf_owner(id: &str) -> Option<&'static str> {
    LEAF_BODIES
        .iter()
        .copied()
        .find(|s| id.len() > s.len() && id.starts_with(s) && id.as_bytes()[s.len()] == b'-')
}

/// The pane name of a decomposed body's MASTER half.
pub fn main_pane(slug: &str) -> String {
    format!("{slug}-main")
}

/// The pane name of a decomposed body's INSPECTOR half.
pub fn inspector_pane(slug: &str) -> String {
    format!("{slug}-inspector")
}

/// The number of tabs that can carry a real keyboard accelerator: `Ctrl+1`…`Ctrl+9`.
///
/// **`Ctrl+0` is deliberately NOT offered** — egui reserves it (with `Ctrl++` /
/// `Ctrl+-`) for zoom reset, so a tenth entry advertising it would be a menu row whose
/// key resets the zoom instead of switching the tab. Nine real keys beat ten hints.
pub const GO_ACCELERATORS: usize = 9;

/// The keyboard accelerator advertised — and genuinely BOUND, in [`TabbedView`]'s draw —
/// for the Go-menu entry of the tab at strip index `i`: `Ctrl+1`…`Ctrl+9`, then **`None`**.
///
/// The old formula was `Ctrl+{i + 1}` for every tab, so the shell advertised `Ctrl+13`,
/// `Ctrl+17`, `Ctrl+20` … — keys that exist on no keyboard — and, worse, NOTHING was
/// bound at all: `facett_menu::Command::shortcut` is a display HINT and no key handler
/// ever read it. Twenty entries each promising an accelerator; twenty that did nothing.
///
/// Past the digit row the honest answer is "no accelerator" (the Go menu still lists
/// every tab, one click away, at any window width).
fn go_accelerator(i: usize) -> Option<String> {
    (i < GO_ACCELERATORS).then(|| format!("Ctrl+{}", i + 1))
}

/// The digit key accelerator index `i` maps to (`0..=8` → `Num1`…`Num9`).
fn go_key(i: usize) -> Option<egui::Key> {
    Some(match i {
        0 => egui::Key::Num1,
        1 => egui::Key::Num2,
        2 => egui::Key::Num3,
        3 => egui::Key::Num4,
        4 => egui::Key::Num5,
        5 => egui::Key::Num6,
        6 => egui::Key::Num7,
        7 => egui::Key::Num8,
        8 => egui::Key::Num9,
        _ => return None,
    })
}

/// The first tab's pane id (route target) — falls back to the workspace pane.
fn first_tab_pane(labels: &[String]) -> String {
    labels
        .first()
        .map(|l| slug(l))
        .unwrap_or_else(|| BODY.to_string())
}

/// Build dwarves' tabbed shell as a declarative [`Scene`] from the tab labels — the
/// **Describe** step of the facett-scene adoption recipe. Each tab becomes a FOLDABLE
/// `Slot` leaf (id = the lowercased label, the SAME key the shell folds each pane's
/// state under). The root is a DRAGGABLE horizontal master/detail split of a foldable
/// **drawer** rail (the warehouse selector + About) over a `Tabs` workspace, collapsing
/// to `Tabs` on a phone-class width. A `View`/`Go`/`Help` menubar (shortcut hints) and a
/// few declared `Router` routes complete it.
///
/// ```text
/// shell  ─ Split(H, 0.20, draggable)          [phone → Tabs under 760pt]
/// ├─ rail  (Foldable 🗄, drawer@760) slot:rail — warehouse selector + About; phone drawer
/// └─ body ─ Tabs(active 0)                     [the workspace]
///    ├─ warehouse (Foldable 🗄) slot:warehouse
///    ├─ forge     (Foldable 🔨) slot:forge
///    └─ …one foldable Slot per existing tab panel…
/// ```
/// `pub` so the `tests/robot_scene.rs` integration suite can build the exact shipped
/// scene value and drive it through a real `egui_kittest` render (drag / drawer / fold).
pub fn dwarves_scene(tab_labels: &[String]) -> Scene {
    let tabs: Vec<Pane> = tab_labels
        .iter()
        .map(|label| {
            let id = slug(label);
            let icon = tab_icon(label);
            // A body that decomposes into N NAMED LEAVES brings its OWN declared
            // subtree (axes, seed ratios, per-leaf titles + fold icons, phone
            // fallback) — the shell only re-stamps the tab's roster label and glyph
            // on the root, so the ONE place a tab label lives stays `DWARVES_TABS`.
            if let Some(p) = body_leaves(&id) {
                debug_assert_eq!(
                    p.name, id,
                    "an N-leaf body's root pane IS the tab slug — it is what the \
                     workspace `Tabs` layout and every deep link address"
                );
                return p.title(label.clone()).foldable_icon(true, icon);
            }
            match body_split(&id) {
                // A body that DECOMPOSES: a draggable master/detail split of two named,
                // chromed, foldable halves. The pane keeps the tab's own id + label, so
                // it is still the `TabView` header and the handle every existing fold /
                // deep-link / robot cell addresses.
                Some(b) => Pane::split(
                    id.clone(),
                    b.axis,
                    b.ratio,
                    vec![
                        Pane::slot(main_pane(&id), main_pane(&id))
                            .title(b.main.0)
                            .foldable_icon(true, b.main.1),
                        Pane::slot(inspector_pane(&id), inspector_pane(&id))
                            .title(b.inspector.0)
                            .foldable_icon(true, b.inspector.1),
                    ],
                )
                .title(label.clone())
                .foldable_icon(true, icon),
                None => Pane::slot(id.clone(), id)
                    .title(label.clone())
                    .foldable_icon(true, icon),
            }
        })
        .collect();
    let body = Pane::tabs(BODY, 0, tabs).title("Workspace");

    // The left RAIL: the warehouse selector + About — a foldable pane that also acts as a
    // DRAWER (auto-folds crossing into phone-class, restores above it).
    // NB the title is "Warehouses", PLURAL, and deliberately not "Warehouse": the shell
    // also mounts a *tab* called Warehouse, and both now draw a chrome title bar carrying
    // their label. Two nodes reading `🗄 Warehouse` make BOTH un-addressable to a robot
    // (and ambiguous to a screen reader). The plural is also the truer name — the rail
    // lists every registered warehouse, one of them selected.
    let rail = Pane::slot(RAIL, "rail")
        .title("Warehouses")
        .foldable_icon(true, "🗄")
        .drawer(760.0);

    // The shell root: a DRAGGABLE horizontal master/detail split; phone-class → Tabs.
    let root = Pane::split(ROOT, Axis::Horizontal, 0.20, vec![rail, body])
        .title("dwarves")
        .phone(Layout::Tabs { active: 1 }, 760.0);

    // The menubar: View (fold toggles) · Go (switch tab) · Help (About) — shortcut hints.
    let view_items: Vec<MenuItem> = tab_labels
        .iter()
        .map(|label| {
            let id = slug(label);
            MenuItem::new(format!("view.{id}"), format!("Toggle {label}"))
                .action(Action::Toggle(id))
        })
        .collect();
    // **Go — the width-INDEPENDENT way to every tab.** Originally written as a
    // workaround for a `TabView` strip that neither wrapped nor scrolled; that was fixed
    // upstream in facett `cdb3cb63` (2026-07-22, `tabview.rs:87`) and the strip has
    // scrolled ever since — see the module docs, where the claim outlived the defect by a
    // month.
    //
    // The menu STAYS, and not out of inertia: a scrolling strip keeps every tab
    // reachable but not every tab VISIBLE, so with 24 tabs finding one still means
    // dragging a strip and reading as you go. This lists all of them at once, sorted, one
    // click away, with a real accelerator for the first ten. That is a better affordance
    // than the strip, not a substitute for a broken one — which is why it should not be
    // removed now that its original justification is gone.
    let go_items: Vec<MenuItem> = tab_labels
        .iter()
        .enumerate()
        .map(|(i, label)| {
            let id = slug(label);
            let item = MenuItem::new(format!("go.{id}"), label.clone())
                .action(Action::Command(format!("go:{id}")));
            match go_accelerator(i) {
                Some(keys) => item.shortcut(keys),
                // Past the digit row there is no accelerator to give. Better a menu entry
                // with none than one that lies (see `go_accelerator`).
                None => item,
            }
        })
        .collect();

    // **The Mímir menu — every code-graph / quality operation in ONE group.**
    // The owner: "alla operationer för att hitta dubbletter och liknande skall ligga
    // under Mimir-meny, nu ligger de utspridda." The dedup (SimilarCode), dead-code
    // (Zombie), risk, weak-test, bench-gap, generic-cost, the new purity (Villa) and
    // the Brokkr quality lenses were reachable only by opening the Lenses tab and
    // hunting its in-pane selector. This menu gathers them under one name: each item
    // fires `mimir:<view>`, which jumps to the Lenses tab and selects that lens — the
    // single source is `mimir_lens::MIMIR_MENU`, shared with the in-pane selector so
    // the two surfaces cannot drift.
    let mimir_items: Vec<MenuItem> = crate::mimir_lens::MIMIR_MENU
        .iter()
        .map(|(view, label)| {
            MenuItem::new(format!("mimir.{view}"), (*label).to_string())
                .action(Action::Command(format!("mimir:{view}")))
        })
        .collect();

    Scene::new("dwarves", root)
        .menu(MenuItem::new("view", "View").items(view_items))
        .menu(MenuItem::new("go", "Go").items(go_items))
        .menu(MenuItem::new("mimir", "Mímir").items(mimir_items))
        .menu(MenuItem::new("help", "Help").items(vec![
                MenuItem::new("help.about", "About dwarves")
                    .shortcut("F1")
                    .action(Action::Command("help.about".into())),
            ]))
        // Declared routes over the shared Router (button / shortcut / event triggers) —
        // robot-fireable via `NavState::fire`, observable in `state_json`.
        .route(Route::on_button(
            "focus-warehouse",
            Target::Focus(first_tab_pane(tab_labels)),
        ))
        .route(Route::on_shortcut(
            "Ctrl+Home",
            Target::Show(first_tab_pane(tab_labels)),
        ))
        .route(Route::on_event("toggle-rail", Target::Toggle(RAIL.into())))
}

/// The egui temp-data key a pane's tab request rides on (see [`request_tab`]).
pub const GOTO_TAB: &str = "dwarves.goto_tab";

/// **Ask the shell to open the tab `slug`** (its [`DWARVES_TABS`](crate::DWARVES_TABS)
/// key) — how a pane links to another tab's full view. The shell switches after the
/// frame; an unknown slug does nothing. A link that carries a selection uses
/// [`request_tab_with`].
pub fn request_tab(ctx: &egui::Context, slug: &str) {
    request_tab_with(ctx, slug, Value::Null);
}

/// **Open the tab `slug` ON a selection**: after the switch the shell hands `select`
/// to that tab's own `update_json` — the same door a robot or a user selection takes
/// there (`{"artifact":"tunnr","resolve":true}` on ⚒ Build Thing, `{"run_id":…}` on
/// Forge, …). `Null` = just open it.
pub fn request_tab_with(ctx: &egui::Context, slug: &str, select: Value) {
    let ask = (
        slug.to_string(),
        if select.is_null() {
            String::new()
        } else {
            select.to_string()
        },
    );
    ctx.data_mut(|d| d.insert_temp(egui::Id::new(GOTO_TAB), ask));
}

/// A minimal tabbed host over N `Panel`s. Renders a selectable-label tab bar, then
/// the active pane. State + input route to the active pane (or the whole shell).
///
/// Optionally carries a **warehouse selector** (nornir parity: many registered, ONE
/// selected — [`with_warehouse_selector`](Self::with_warehouse_selector)) rendered in
/// a header above the tabs; changing the selection re-binds EVERY tab (broadcasts
/// `{"set_workspace":…}`), so the funnel + warehouse-bound panes fetch the SELECTED
/// warehouse. And an **About popup** ([`with_about`](Self::with_about)) — the shared
/// `facett-about` card behind an ℹ button.
pub struct TabbedView {
    title: String,
    tabs: Vec<(String, TabPanel)>,
    /// The warehouse selector (rail widget); `None` = no selector (bare shell/tests).
    selector: Option<WarehouseSelector>,
    /// The About identity (data for the shared `facett-about` card); `None` = no About.
    about_id: Option<facett_about::Identity>,
    /// Whether the About popup is open.
    about_open: bool,
    /// The decoded About card (logo texture), built lazily on the first frame (needs
    /// an egui `Context`), so `new` stays host-agnostic + headless-constructible.
    about_card: Option<facett_about::AboutCard>,
    /// The declarative shell (menubar + foldable pane tree + routes) — the DATA the
    /// renderer walks. Built from the tab labels in [`new`](Self::new).
    scene: Scene,
    /// The scene's runtime nav state (per-pane fold / active-tab / split-ratio + Router).
    nav: NavState,
    /// Per N-leaf tab: `(slug, its declared leaf names)` — cached at mount because the
    /// presence sync runs every frame, and walking [`body_leaves`]'s freshly built pane
    /// tree per frame would allocate a tree to answer a question about a string (the
    /// same reasoning as [`leaf_owner`]'s prefix test).
    leaf_roster: Vec<(String, Vec<String>)>,
    /// The last `scene::render`'s observable facts (device class, slot states, pane rects),
    /// folded into [`state_json`](Facet::state_json) — the **Observe** step.
    last_render: Value,
    /// The menu AREAS ([`with_areas`](Self::with_areas)): the rail lists them and the
    /// tab strip shows only the active area's tabs. Empty = one flat strip.
    areas: Vec<Area>,
    /// Per area, the tab index it was last on — clicking an area returns there.
    area_last: Vec<usize>,
}

/// One menu AREA: its rail label and the slugs of the tabs it holds.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Area {
    pub label: String,
    pub tabs: Vec<String>,
}

/// The area holding tab `slug`, if any.
fn area_of(areas: &[Area], slug: &str) -> Option<usize> {
    areas.iter().position(|a| a.tabs.iter().any(|t| t == slug))
}

impl TabbedView {
    /// Build the shell from `(label, panel)` pairs; the first tab is active. No
    /// selector / About until [`with_warehouse_selector`](Self::with_warehouse_selector)
    /// / [`with_about`](Self::with_about) is chained. The declarative [`Scene`] is derived
    /// from the tab labels (one foldable `Slot` per panel) + its [`NavState`] seeded.
    pub fn new<P: Into<TabPanel>>(
        title: impl Into<String>,
        tabs: impl IntoIterator<Item = (String, P)>,
    ) -> Self {
        let tabs: Vec<(String, TabPanel)> = tabs.into_iter().map(|(n, p)| (n, p.into())).collect();
        let labels: Vec<String> = tabs.iter().map(|(n, _)| n.clone()).collect();
        let scene = dwarves_scene(&labels);
        // Fail loudly in debug if the addressability invariant is ever broken.
        debug_assert!(
            scene.validate().is_ok(),
            "scene names must be unique: {:?}",
            scene.validate()
        );
        // **A declared split MUST have a `SplitPanel` behind it.** Otherwise the scene
        // grows two panes whose slots no host can fill and the tab silently renders "no
        // widget for slot" twice — a decomposition that looks declared and draws nothing.
        for (label, panel) in &tabs {
            let id = slug(label);
            assert!(
                body_split(&id).is_none() || matches!(panel, TabPanel::Split(_)),
                "tab `{label}` declares a master/detail body_split but is mounted as a \
                 whole Panel — mount it as `Box<dyn SplitPanel>`"
            );
            // …and the same for the THIRD shape. `build_thing_scene::body()` spent its
            // whole life with no caller because nothing asserted this: the tree was
            // declared, the tab was mounted `Whole`, and the two facts never met.
            assert!(
                body_leaves(&id).is_none() || matches!(panel, TabPanel::Leaves(_)),
                "tab `{label}` declares an N-leaf body but is not mounted as one — mount \
                 it as `Box<dyn LeafPanel>`"
            );
            // ONE SHAPE PER BODY. A tab claiming both would grow `<slug>-main` /
            // `<slug>-inspector` panes beside its leaves, and the two would fight over
            // the same tab slug.
            assert!(
                body_leaves(&id).is_none() || body_split(&id).is_none(),
                "tab `{label}` declares BOTH a master/detail body_split and an N-leaf \
                 body — a body has exactly one shape"
            );
            assert!(
                !LEAF_BODIES.contains(&id.as_str()) || body_leaves(&id).is_some(),
                "tab `{label}` is listed in LEAF_BODIES but `body_leaves` gives it no tree"
            );
        }
        let nav = NavState::new(&scene);
        let leaf_roster: Vec<(String, Vec<String>)> = tabs
            .iter()
            .filter_map(|(label, panel)| {
                let id = slug(label);
                (matches!(panel, TabPanel::Leaves(_)) && body_leaves(&id).is_some())
                    .then(|| (id.clone(), body_leaf_names(&id)))
            })
            .collect();
        Self {
            title: title.into(),
            tabs,
            selector: None,
            about_id: None,
            about_open: false,
            about_card: None,
            scene,
            nav,
            leaf_roster,
            last_render: Value::Null,
            areas: Vec::new(),
            area_last: Vec::new(),
        }
    }

    /// Group the tabs into AREAS: `(rail label, tab slugs)`. The rail lists the areas;
    /// the strip shows the active area's tabs; the active area is always the one
    /// holding the active tab, so every way of reaching a tab (Go menu, Ctrl+N,
    /// `set_tab`) brings its area along. Every tab must be in exactly one area.
    pub fn with_areas(mut self, areas: impl IntoIterator<Item = (String, Vec<String>)>) -> Self {
        let areas: Vec<Area> = areas
            .into_iter()
            .map(|(label, tabs)| Area { label, tabs })
            .collect();
        for (n, _) in &self.tabs {
            let id = slug(n);
            let holders = areas.iter().filter(|a| a.tabs.contains(&id)).count();
            assert_eq!(
                holders, 1,
                "tab `{n}` must sit in exactly one area, not {holders}"
            );
        }
        let known: Vec<String> = self.tabs.iter().map(|(n, _)| slug(n)).collect();
        for a in &areas {
            assert!(!a.tabs.is_empty(), "area `{}` holds no tab", a.label);
            for t in &a.tabs {
                assert!(
                    known.contains(t),
                    "area `{}` names `{t}`, which is no tab",
                    a.label
                );
            }
        }
        self.area_last = areas
            .iter()
            .map(|a| known.iter().position(|k| *k == a.tabs[0]).unwrap_or(0))
            .collect();
        self.areas = areas;
        self
    }

    /// The active area's label (`""` without areas).
    pub fn active_area(&self) -> &str {
        let active = self
            .tabs
            .get(self.active_index())
            .map(|(n, _)| slug(n))
            .unwrap_or_default();
        area_of(&self.areas, &active)
            .map(|i| self.areas[i].label.as_str())
            .unwrap_or("")
    }

    /// Switch to `area` (its label, or the label without its icon, any case): back to
    /// the tab it was last on. `false` if there is no such area.
    pub fn select_area(&mut self, area: &str) -> bool {
        let want = area.trim().to_lowercase();
        let Some(i) = self.areas.iter().position(|a| {
            let l = a.label.to_lowercase();
            l == want || l.split_once(' ').is_some_and(|(_, name)| name == want)
        }) else {
            return false;
        };
        self.nav.set_active_tab(BODY, self.area_last[i]);
        true
    }

    /// The active tab index (the `body` Tabs pane's live active child, clamped).
    fn active_index(&self) -> usize {
        self.nav
            .active_tab(BODY)
            .min(self.tabs.len().saturating_sub(1))
    }

    /// The scene, as canonical (byte-stable) RON — the on-disk authoring form.
    /// (Robot/test-facing surface; unreferenced by the non-test binary.)
    #[allow(dead_code)]
    pub fn to_ron(&self) -> String {
        self.scene.to_ron()
    }

    /// Every FOLDABLE pane name (the rail + each content tab). Delegates to facett-app's
    /// own [`Scene::foldable_panes`] — this used to re-walk the tree here, which is the
    /// twin L5 forbids: the shared crate is the one place that knows what "foldable"
    /// means.
    pub fn foldable_panes(&self) -> Vec<String> {
        self.scene.foldable_panes()
    }

    /// Every DRAWER pane name (auto-folds into a drawer on phone-class width).
    pub fn drawer_panes(&self) -> Vec<String> {
        self.scene
            .pane_names()
            .into_iter()
            .filter(|n| {
                self.scene
                    .pane(n)
                    .map(|p| p.responsive.drawer)
                    .unwrap_or(false)
            })
            .collect()
    }

    /// Every DRAGGABLE (resizable) split pane name — delegated to facett-app's own
    /// [`Scene::draggable_splits`] (L5).
    pub fn draggable_splits(&self) -> Vec<String> {
        self.scene.draggable_splits()
    }

    /// Every pane that draws a **window-chrome title bar** (fold · minimise · maximise).
    /// facett-app puts chrome on every pane but the scene root by default, so this is the
    /// whole shell minus `shell` — the affordance roster a robot drives.
    pub fn chrome_panes(&self) -> Vec<String> {
        self.scene.chrome_panes()
    }

    /// Fold / unfold a pane by name (the robot-addressable toggle). Returns the new state.
    /// (Also reachable over the JSON seam via `{"toggle_fold":"<pane>"}`.)
    pub fn toggle_fold(&mut self, pane: &str) -> bool {
        self.nav.toggle_fold(pane)
    }

    /// Whether `pane` is currently folded.
    #[allow(dead_code)]
    pub fn is_folded(&self, pane: &str) -> bool {
        self.nav.is_folded(pane)
    }

    /// **Minimise / un-minimise** a pane — collapse it to its chrome title bar and hand
    /// its space back to its siblings. The same toggle the bar's `−` control runs.
    /// Returns the new state. (JSON seam: `{"toggle_minimize":"<pane>"}`.)
    pub fn toggle_minimize(&mut self, pane: &str) -> bool {
        self.nav.toggle_minimize(pane)
    }

    /// **Maximise / un-maximise** a pane — give it the whole shell area, shrinking every
    /// non-ancestor sibling to zero. At most one pane holds it. Returns the new state.
    /// (JSON seam: `{"toggle_maximize":"<pane>"}`.)
    pub fn toggle_maximize(&mut self, pane: &str) -> bool {
        self.nav.toggle_maximize(pane)
    }

    /// **Restore** a pane: drop its minimise AND its maximise. The split ratios were never
    /// overwritten, so the layout returns exactly as it was.
    /// (JSON seam: `{"restore":"<pane>"}`.)
    pub fn restore_pane(&mut self, pane: &str) -> bool {
        self.nav.restore(pane)
    }

    /// **Reset the layout** — clear every minimise and the maximise in one call.
    /// (JSON seam: `{"restore_all":true}`.)
    pub fn restore_layout(&mut self) -> bool {
        self.nav.restore_all()
    }

    /// Attach a warehouse selector. If it already has a selection, EVERY tab is bound
    /// to it immediately (the funnel fetches that warehouse from the first frame).
    pub fn with_warehouse_selector(mut self, selector: WarehouseSelector) -> Self {
        if selector.has_selection() {
            let ws = selector.selected().to_string();
            self.broadcast_set_workspace(&ws);
        }
        self.selector = Some(selector);
        self
    }

    /// Attach the About popup identity (the shared `facett-about` card renders it).
    pub fn with_about(mut self, id: facett_about::Identity) -> Self {
        self.about_id = Some(id);
        self
    }

    /// Attach the About popup AND open it on the first frame (task #31 — dwarves
    /// About-at-startup). Same shared `facett-about` card as [`with_about`], just shown
    /// immediately (the user dismisses it with ✕ / Close, or re-opens via the ℹ button).
    pub fn with_about_at_startup(mut self, id: facett_about::Identity) -> Self {
        self.about_id = Some(id);
        self.about_open = true;
        self
    }

    /// The active tab label (for tests / introspection) — the `body` Tabs pane's live
    /// active child, read from [`NavState`].
    pub fn active_tab(&self) -> &str {
        self.tabs
            .get(self.active_index())
            .map(|(n, _)| n.as_str())
            .unwrap_or("")
    }

    /// Each body-tab header's last-drawn on-screen rect `[x, y, w, h]`, keyed by the tab's
    /// VISIBLE label (e.g. `"Warehouse"`, `"CI/CD"`) — the geometry a **visible-window**
    /// robot travels to so it can CLICK a tab by name. Read from facett-app's scene render
    /// (`tab_rects`, keyed by pane slug) and mapped back to the label via [`slug`]. Empty
    /// until the shell has drawn at least once (`last_render` is seeded on the first frame).
    pub fn tab_label_rects(&self) -> std::collections::BTreeMap<String, [f32; 4]> {
        let mut out = std::collections::BTreeMap::new();
        let Some(rects) = self
            .last_render
            .get("tab_rects")
            .and_then(|v| v.as_object())
        else {
            return out;
        };
        for (label, _) in &self.tabs {
            if let Some(arr) = rects.get(&slug(label)).and_then(|v| v.as_array()) {
                if arr.len() == 4 {
                    let f = |i: usize| arr[i].as_f64().unwrap_or(0.0) as f32;
                    out.insert(label.clone(), [f(0), f(1), f(2), f(3)]);
                }
            }
        }
        out
    }

    /// The selected warehouse (`""` = none / no selector) — the active-warehouse the
    /// funnel + release bind to.
    pub fn selected_warehouse(&self) -> &str {
        self.selector.as_ref().map(|s| s.selected()).unwrap_or("")
    }

    /// **Re-bind every tab to `workspace`** — the broadcast nornir's `switch_workspace`
    /// does (it calls `set_workspace` on every tab). Each pane's `update_json` picks up
    /// `{"set_workspace":…}` (the funnel re-fetches; panes that don't understand it
    /// ignore it — never a panic).
    fn broadcast_set_workspace(&mut self, workspace: &str) {
        let msg = serde_json::json!({ "set_workspace": workspace }).to_string();
        for (_, panel) in self.tabs.iter_mut() {
            panel.panel_mut().update_json(&msg);
        }
    }
}

impl Facet for TabbedView {
    fn title(&self) -> &str {
        &self.title
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        // ── RENDER the declarative scene through facett-app's own primitives ──────────
        // Disjoint field borrows so the host holds the slot widgets (tabs / selector /
        // About) while `render` reads the scene + mutates the nav state.
        let Self {
            title: _,
            tabs,
            selector,
            about_id,
            about_open,
            about_card,
            scene,
            nav,
            leaf_roster,
            last_render,
            areas,
            area_last,
        } = self;

        // ── AREAS: the strip shows the active tab's area, nothing else ───────────────
        // An absent tab has no header and no body (facett-app's Tabs layout), and the
        // active index is never rewritten, so leaving an area and coming back is exact.
        let active_area = if areas.is_empty() || tabs.is_empty() {
            None
        } else {
            let active = nav.active_tab(BODY).min(tabs.len() - 1);
            let area = area_of(areas, &slug(&tabs[active].0));
            if let Some(a) = area {
                area_last[a] = active;
                for (n, _) in tabs.iter() {
                    let id = slug(n);
                    nav.set_absent(&id, !areas[a].tabs.contains(&id));
                }
            }
            area
        };
        let mut picked_area: Option<usize> = None;

        // ── PRESENCE, before layout: a leaf whose subject is absent must not EXIST ────
        // Asked of the panel every frame (its own domain facts — for ⚒, whether the
        // resolved recipe declares a `[[build.kernel]]`), and asserted into the scene's
        // runtime presence, so the region — title, fold chrome, divider — never renders
        // for it. This is the strict form of the kernel-leaf gate: content-gating left a
        // hardmounted pane frame on screen before any recipe was resolved.
        for (id, leaves) in leaf_roster.iter() {
            if let Some(lp) = tabs
                .iter()
                .find(|(n, _)| &slug(n) == id)
                .and_then(|(_, p)| p.leaves())
            {
                for leaf in leaves {
                    nav.set_absent(leaf, lp.leaf_absent(leaf));
                }
            }
        }

        let mut rebind: Option<String> = None;
        let mut pending: Option<String> = None;
        {
            let mut host = Host {
                tabs: &mut *tabs,
                selector: &mut *selector,
                about_id: &*about_id,
                about_open: &mut *about_open,
                about_card: &mut *about_card,
                rebind: &mut rebind,
                pending: &mut pending,
                areas: &*areas,
                active_area,
                picked_area: &mut picked_area,
            };
            *last_render = scene::render(&*scene, &mut *nav, ui, &mut host);
        }
        // An area clicked on the rail: back to the tab that area was last on.
        if let Some(a) = picked_area {
            nav.set_active_tab(BODY, area_last[a]);
        }

        // **The advertised accelerators, actually BOUND.** `Ctrl+1`…`Ctrl+9` select the
        // first nine tabs without touching the strip — the keyboard path that works no
        // matter how far past the pane's right edge the headers laid out.
        //
        // …but NOT while a text widget owns the keyboard. `consume_key` takes the event
        // away from every widget below it, so an unconditional loop here steals
        // `Ctrl+1`…`Ctrl+9` out from under a focused `TextEdit` (stinky `kopiera-3`) —
        // and the shell just gained a lot more of them, because every log/console block
        // is now a real `CopyableBlock` text widget the operator can click into. A shell
        // accelerator that fires while someone is typing is the shell reaching past the
        // pane that has focus.
        //
        // `egui_wants_keyboard_input()` is egui's own answer to "does a text widget have
        // focus" — the same predicate its built-in shortcuts defer to.
        let n = tabs.len();
        if !ui.ctx().egui_wants_keyboard_input() {
            for i in 0..n.min(GO_ACCELERATORS) {
                let Some(key) = go_key(i) else { continue };
                if ui.input_mut(|inp| inp.consume_key(egui::Modifiers::COMMAND, key)) {
                    nav.set_active_tab(BODY, i);
                }
            }
        }

        // A pane asked to open another tab ([`request_tab`]: the 🧭 Explorer's links to
        // the full views) — the same switch a `go:<tab>` menu command makes.
        if let Some((t, select)) = ui
            .ctx()
            .data_mut(|d| d.remove_temp::<(String, String)>(egui::Id::new(GOTO_TAB)))
        {
            if let Some(i) = tabs.iter().position(|(n, _)| slug(n) == t) {
                nav.set_active_tab(BODY, i);
                if !select.is_empty() {
                    tabs[i].1.panel_mut().update_json(&select);
                }
            }
        }
        // A `go:<tab>` menu command switches the visible tab (the Router doesn't drive the
        // Tabs active index — the host does, after the render).
        if let Some(cmd) = pending.as_deref() {
            if let Some(t) = cmd.strip_prefix("go:") {
                if let Some(i) = tabs.iter().position(|(n, _)| slug(n) == t) {
                    nav.set_active_tab(BODY, i);
                }
            }
            // A `mimir:<view>` menu command jumps to the Lenses tab AND selects that lens
            // — the Mímir menu is a single door onto every code-graph/quality operation.
            if let Some(view) = cmd.strip_prefix("mimir:") {
                if let Some(i) = tabs.iter().position(|(n, _)| slug(n) == "lenses") {
                    nav.set_active_tab(BODY, i);
                    let msg = json!({ "view": view }).to_string();
                    tabs[i].1.panel_mut().update_json(&msg);
                }
            }
        }
        // A warehouse selection changed in the rail → re-bind EVERY tab to it (the same
        // broadcast nornir's `switch_workspace` does).
        if let Some(ws) = rebind {
            let msg = json!({ "set_workspace": ws }).to_string();
            for (_, panel) in tabs.iter_mut() {
                panel.panel_mut().update_json(&msg);
            }
        }
    }

    fn state_json(&self) -> serde_json::Value {
        let mut obj = serde_json::Map::new();
        obj.insert("active_tab".into(), serde_json::json!(self.active_tab()));
        if !self.areas.is_empty() {
            obj.insert("active_area".into(), serde_json::json!(self.active_area()));
            obj.insert(
                "areas".into(),
                serde_json::json!(self
                    .areas
                    .iter()
                    .map(|a| json!({ "label": a.label, "tabs": a.tabs }))
                    .collect::<Vec<_>>()),
            );
        }
        obj.insert(
            "tabs".into(),
            serde_json::json!(self.tabs.iter().map(|(n, _)| n.clone()).collect::<Vec<_>>()),
        );
        // ── the declarative shell as observable data (the Observe step) ──────────────
        obj.insert("scene".into(), serde_json::json!(self.scene.name));
        obj.insert("panes".into(), serde_json::json!(self.scene.pane_names()));
        obj.insert("foldable".into(), serde_json::json!(self.foldable_panes()));
        obj.insert("drawers".into(), serde_json::json!(self.drawer_panes()));
        obj.insert(
            "draggable_splits".into(),
            serde_json::json!(self.draggable_splits()),
        );
        // Every pane that draws a window title bar, plus facett-app's own adoption audit
        // (the one blob naming every affordance this shell ships — asserting against IT
        // is what makes "the chrome silently disappeared" a red test, not a shrug).
        obj.insert("chrome".into(), serde_json::json!(self.chrome_panes()));
        obj.insert("affordances".into(), self.scene.affordances());
        obj.insert("folded".into(), serde_json::json!(self.nav.folded));
        // The live WINDOW state: which panes are collapsed to their title bar, which one
        // (at most) holds the whole shell, and the last chrome control that fired.
        obj.insert("minimized".into(), serde_json::json!(self.nav.minimized));
        obj.insert("maximized".into(), serde_json::json!(self.nav.maximized));
        // The panes whose SUBJECT is absent this frame — they do not exist on screen
        // (no region, no chrome, no divider), however much the declared tree names
        // them. For ⚒ this is the kernel leaf whenever the resolved recipe declares no
        // `[[build.kernel]]` — including the opening state, where nothing is resolved.
        // Computed LIVE from the panels (the same ask the per-frame sync makes), not
        // read back from `nav`, so the oracle is honest before the first render too.
        let absent: Vec<&String> = self
            .leaf_roster
            .iter()
            .filter_map(|(id, leaves)| {
                let lp = self.tabs.iter().find(|(n, _)| &slug(n) == id)?.1.leaves()?;
                Some(leaves.iter().filter(move |l| lp.leaf_absent(l)))
            })
            .flatten()
            .collect();
        obj.insert("absent".into(), serde_json::json!(absent));
        obj.insert(
            "last_chrome".into(),
            serde_json::json!(self.nav.last_chrome),
        );
        obj.insert(
            "split_ratio".into(),
            serde_json::json!(self.nav.split_ratio),
        );
        // **The tab INTERNALS, as data.** Which tab bodies DECOMPOSED into a draggable
        // master/detail split of `<slug>-main` | `<slug>-inspector`, which decomposed
        // into N NAMED LEAVES, and which are honestly ONE pane. A robot reads this
        // instead of hard-coding the decomposition, so a body split (or leaved, or
        // un-split) later is swept without editing the suite.
        //
        // The three rosters PARTITION the strip — every tab is in exactly one, which is
        // what `TabbedView::new`'s one-shape-per-body assertion buys.
        let (mut split_bodies, mut leaf_bodies, mut single_bodies) =
            (Vec::new(), Vec::new(), Vec::new());
        let mut leaves = serde_json::Map::new();
        for (n, _) in &self.tabs {
            let id = slug(n);
            if body_split(&id).is_some() {
                split_bodies.push(id);
            } else if body_leaves(&id).is_some() {
                leaves.insert(id.clone(), serde_json::json!(body_leaf_names(&id)));
                leaf_bodies.push(id);
            } else {
                single_bodies.push(id);
            }
        }
        obj.insert("split_bodies".into(), serde_json::json!(split_bodies));
        obj.insert("leaf_bodies".into(), serde_json::json!(leaf_bodies));
        // …and, per leaved body, its leaf roster in reading order — the names a driver
        // addresses, published rather than copied into every suite that walks them.
        obj.insert("body_leaves".into(), Value::Object(leaves));
        obj.insert(
            "single_pane_bodies".into(),
            serde_json::json!(single_bodies),
        );
        obj.insert("menus".into(), serde_json::json!(self.scene.menubar.len()));
        obj.insert("routes".into(), serde_json::json!(self.scene.routes.len()));
        obj.insert("render".into(), self.last_render.clone());
        // The SELECTED warehouse (nornir parity) — top-level so a reader of
        // `__dwarves_state()` finds the active warehouse the funnel + release bind to.
        if let Some(sel) = &self.selector {
            obj.insert(
                "selected_warehouse".into(),
                serde_json::json!(sel.selected()),
            );
            obj.insert("warehouses".into(), serde_json::json!(sel.warehouses()));
            obj.insert("warehouse_select".into(), sel.state_json());
        }
        // The About popup oracle (shared facett-about identity + open flag).
        if let Some(id) = &self.about_id {
            obj.insert(
                "about".into(),
                facett_about::state_json(id, self.about_open),
            );
        }
        // Fold each pane's own state under its (lower-cased) label — additive, so a
        // reader of `__dwarves_state()["forge"]` finds the untouched Forge shape.
        for (name, panel) in &self.tabs {
            obj.insert(slug(name), panel.panel().state_json());
        }
        serde_json::Value::Object(obj)
    }

    fn update_json(&mut self, msg_json: &str) {
        // `{"set_tab":"Funnel"}` switches tabs (case-insensitive, drives the scene's
        //     `body` Tabs active index) — its area comes along;
        // `{"set_area":"Build"}` switches to an area, on the tab it was last on;
        // `{"toggle_fold":"forge"}` folds/unfolds a pane by name (the scene fold seam);
        // `{"toggle_minimize":"rail"}` / `{"toggle_maximize":"body"}` / `{"restore":"rail"}`
        //     / `{"restore_all":true}` drive the per-pane WINDOW CHROME headlessly — the
        //     exact state the title bar's controls mutate;
        // `{"select_warehouse":"…"}` selects the active warehouse + re-binds every tab;
        // `{"register_warehouse":"…"}` adds a warehouse to the registry;
        // `{"about": true|false}` toggles the About popup;
        // anything else is forwarded to the active pane's own JSON seam.
        if let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) {
            if let Some(want) = v.get("set_area").and_then(|t| t.as_str()) {
                if self.select_area(want) {
                    return;
                }
            }
            if let Some(want) = v.get("set_tab").and_then(|t| t.as_str()) {
                if let Some(i) = self
                    .tabs
                    .iter()
                    .position(|(n, _)| slug(n) == want || n.eq_ignore_ascii_case(want))
                {
                    self.nav.set_active_tab(BODY, i);
                    return;
                }
            }
            // ONE WRITER PER VERB (L5). These five used to call `self.nav.*` DIRECTLY,
            // in parallel with the identically-named `pub fn` wrappers below — so the
            // shell carried two independent paths to the same state, the wrappers had
            // ZERO callers (five `#[allow(dead_code)]` claims that were true), and any
            // behaviour added to a wrapper would silently NOT reach the seam a robot
            // drives. The seam now goes THROUGH the wrapper, which is the only code that
            // touches `self.nav` for these verbs, and the `#[allow(dead_code)]` came off
            // with the last caller that made it necessary.
            //
            // What holds it is `the_json_seam_and_the_shell_methods_are_one_writer` in
            // this file's tests, NOT the compiler: `dead_code` is only a warning here (no
            // `deny`, nothing in .cargo/config.toml — checked 2026-08-22), so reverting
            // one of these five lines to `self.nav.*` would compile and warn about
            // exactly nothing. The test is the thing that can go red.
            if let Some(pane) = v.get("toggle_fold").and_then(|t| t.as_str()) {
                self.toggle_fold(pane);
                return;
            }
            // ── window chrome, over the same JSON seam the fold already uses ──────────
            if let Some(pane) = v.get("toggle_minimize").and_then(|t| t.as_str()) {
                self.toggle_minimize(pane);
                return;
            }
            if let Some(pane) = v.get("toggle_maximize").and_then(|t| t.as_str()) {
                self.toggle_maximize(pane);
                return;
            }
            if let Some(pane) = v.get("restore").and_then(|t| t.as_str()) {
                self.restore_pane(pane);
                return;
            }
            if v.get("restore_all")
                .and_then(|t| t.as_bool())
                .unwrap_or(false)
            {
                self.restore_layout();
                return;
            }
            // Warehouse selection (nornir's switch_workspace, mirrored): select + broadcast.
            if let Some(ws) = v.get("select_warehouse").and_then(|t| t.as_str()) {
                let changed = self
                    .selector
                    .as_mut()
                    .map(|s| s.select(ws))
                    .unwrap_or(false);
                if changed {
                    let selected = self.selected_warehouse().to_string();
                    self.broadcast_set_workspace(&selected);
                }
                return;
            }
            if let Some(ws) = v.get("register_warehouse").and_then(|t| t.as_str()) {
                if let Some(s) = self.selector.as_mut() {
                    s.register(ws);
                }
                return;
            }
            if let Some(open) = v.get("about").and_then(|t| t.as_bool()) {
                self.about_open = open;
                return;
            }
        }
        let active = self.active_index();
        if let Some((_, panel)) = self.tabs.get_mut(active) {
            panel.panel_mut().update_json(msg_json);
        }
    }
}

// ── the slot host: fills each `Content::Slot` by DRIVING an existing widget ─────
//
// `TabbedView`'s slots map to the SAME `Box<dyn Panel>` tabs (+ the rail's warehouse
// selector / About) — the host draws each and returns its `state_json`, folded into the
// scene's observable state. Nothing is forked: the scene DRIVES the existing components.
struct Host<'a> {
    tabs: &'a mut Vec<(String, TabPanel)>,
    selector: &'a mut Option<WarehouseSelector>,
    about_id: &'a Option<facett_about::Identity>,
    about_open: &'a mut bool,
    about_card: &'a mut Option<facett_about::AboutCard>,
    /// A warehouse selection made in the rail this frame (broadcast after the render).
    rebind: &'a mut Option<String>,
    /// A menu command fired this frame (applied after the render — e.g. `go:<tab>`).
    pending: &'a mut Option<String>,
    /// The menu areas the rail lists, and which one holds the active tab.
    areas: &'a [Area],
    active_area: Option<usize>,
    /// An area clicked on the rail this frame (applied after the render).
    picked_area: &'a mut Option<usize>,
}

/// Which half of a decomposed body a `<slug>-<half>` slot id addresses.
enum Half {
    Main,
    Inspector,
}

impl SceneHost for Host<'_> {
    fn slot(&mut self, id: &str, ui: &mut egui::Ui) -> Value {
        if id == "rail" {
            // The rail: the menu AREAS, then the warehouse selector (nornir parity) +
            // the About affordance.
            let mut st = serde_json::Map::new();
            if !self.areas.is_empty() {
                for (i, a) in self.areas.iter().enumerate() {
                    let on = self.active_area == Some(i);
                    let label = egui::RichText::new(&a.label).size(15.0).strong();
                    if ui
                        .selectable_label(on, label)
                        .on_hover_text(format!("{} tab(s)", a.tabs.len()))
                        .clicked()
                    {
                        *self.picked_area = Some(i);
                    }
                }
                ui.separator();
                st.insert(
                    "areas".into(),
                    json!(self
                        .areas
                        .iter()
                        .map(|a| a.label.clone())
                        .collect::<Vec<_>>()),
                );
            }
            if let Some(sel) = self.selector.as_mut() {
                if let Some(new_ws) = sel.ui(ui) {
                    *self.rebind = Some(new_ws);
                }
                st.insert("warehouse_select".into(), sel.state_json());
            }
            if let Some(idm) = self.about_id {
                ui.separator();
                if ui
                    .button("ℹ About")
                    .on_hover_text("about dwarves")
                    .clicked()
                {
                    *self.about_open = true;
                }
                // Lazily decode the logo once (needs the egui Context — only here at render).
                if self.about_card.is_none() {
                    *self.about_card = Some(facett_about::AboutCard::new(ui.ctx(), idm));
                }
                if let Some(card) = self.about_card.as_ref() {
                    card.show(ui.ctx(), self.about_open, idm);
                }
                st.insert(
                    "about".into(),
                    facett_about::state_json(idm, *self.about_open),
                );
            }
            if st.is_empty() {
                Value::Null
            } else {
                Value::Object(st)
            }
        } else if let Some((_, panel)) = self.tabs.iter_mut().find(|(n, _)| slug(n) == id) {
            // A content tab: draw the EXISTING panel, return its untouched state.
            panel.panel_mut().ui(ui);
            panel.panel().state_json()
        } else if let Some(owner) = leaf_owner(id) {
            // ONE LEAF of a body that decomposes into N named leaves. The panel draws
            // that leaf and returns its own observable state under the leaf's name; the
            // tab's whole `state_json` is still folded under the tab slug by
            // `TabbedView::state_json`, so every existing oracle reads the same object.
            match self.tabs.iter_mut().find(|(n, _)| slug(n) == owner) {
                Some((_, p)) => match p.leaves_mut() {
                    Some(lp) => lp.ui_leaf(id, ui),
                    None => {
                        ui.weak(format!("no leaf widget for slot: {id}"));
                        Value::Null
                    }
                },
                None => {
                    ui.weak(format!("no widget for slot: {id}"));
                    Value::Null
                }
            }
        } else if let Some((base, half)) = id
            .strip_suffix("-main")
            .map(|b| (b, Half::Main))
            .or_else(|| id.strip_suffix("-inspector").map(|b| (b, Half::Inspector)))
        {
            // One HALF of a decomposed body. The master returns the pane's own untouched
            // `state_json` (so `__dwarves_state()["<slug>"]` keeps its exact shape and
            // every existing oracle reads the same object); the inspector returns its own.
            match self.tabs.iter_mut().find(|(n, _)| slug(n) == base) {
                Some((_, p)) => match half {
                    Half::Main => match p.split_mut() {
                        Some(sp) => {
                            sp.ui_main(ui);
                            p.panel().state_json()
                        }
                        None => {
                            ui.weak(format!("no split widget for slot: {id}"));
                            Value::Null
                        }
                    },
                    Half::Inspector => match p.split_mut() {
                        Some(sp) => sp.ui_inspector(ui),
                        None => {
                            ui.weak(format!("no split widget for slot: {id}"));
                            Value::Null
                        }
                    },
                },
                None => {
                    ui.weak(format!("no widget for slot: {id}"));
                    Value::Null
                }
            }
        } else {
            ui.weak(format!("no widget for slot: {id}"));
            Value::Null
        }
    }

    fn command(&mut self, id: &str) {
        match id {
            "help.about" => *self.about_open = true,
            _ if id.starts_with("wh:") => {
                let name = &id[3..];
                if let Some(s) = self.selector.as_mut() {
                    if s.select(name) {
                        *self.rebind = Some(s.selected().to_string());
                    }
                }
            }
            // Everything else (notably `go:<tab>`) is applied by the shell after render.
            _ => *self.pending = Some(id.to_string()),
        }
    }
}

// ── the Claude tab (host wrapper over facett-claude::ClaudePane) ──────────────
//
// `ClaudePane` is a pure facett-Elm leaf: its `Send` emits a `ClaudeEffect::Send`
// (data) that a HOST must run through a `ClaudeTransport`. `ClaudeTab` is that host
// — it owns the pane + an injected transport and pumps the effect loop, so the pane
// is fully drivable (Send actually produces a reply) as a plain `Box<dyn Panel>`.
//
// L5 (REUSE): the transport is NOT hand-rolled here. dwarves CONSUMES the shared
// `nornir-claude` edda leaf — the ONE Anthropic Messages-API client (offline stub +
// live HTTP transport + credential resolution). [`EddaBackend`] is the ~3-line newtype
// adapter documented in edda `.nornir/guide.md` §10: it maps `facett_claude::WireMsg`
// → `nornir_claude::WireMsg` and forwards to the leaf's byte-identical `send_wire`.
// (Previously dwarves carried a bespoke `OfflineClaudeTransport` twin — retired.)

use std::sync::Arc;

/// The offline stub's canned reply — carries the `offline demo` label the robot
/// scenario asserts on, and names the shared leaf it now rides.
/// (`allow(dead_code)`: unreferenced by the non-test binary when `claude-http` picks
/// the live path in `main`; still used by the default build + every robot test.)
#[allow(dead_code)]
const OFFLINE_DEMO_REPLY: &str =
    "(offline demo — no live Claude wired) — via the shared nornir-claude leaf";

/// Adapter bridging dwarves' facett `ClaudeTransport` seam onto the shared
/// `nornir-claude` leaf transport. The pane speaks `facett_claude`; the leaf owns the
/// real send (offline stub or live HTTP + credential resolution) — this newtype is the
/// only glue (the guide §10 pattern). Both `WireMsg`s are structurally identical, so it
/// is a field-copy `.map` + a forward to the leaf's `send_wire`.
struct EddaBackend(Arc<dyn nornir_claude::ClaudeTransport>);

impl facett_claude::ClaudeTransport for EddaBackend {
    fn send(&self, model: &str, messages: &[facett_claude::WireMsg]) -> Result<String, String> {
        let wire: Vec<nornir_claude::WireMsg> = messages
            .iter()
            .map(|w| nornir_claude::WireMsg {
                role: w.role.clone(),
                content: w.content.clone(),
            })
            .collect();
        self.0.send_wire(model, &wire)
    }
}

/// Hosts a [`facett_claude::ClaudePane`] with an injected transport, running the
/// pane's `Send`-effect loop so it drives end-to-end (view→update→deliver→reply).
pub struct ClaudeTab {
    pane: facett_claude::ClaudePane,
    transport: Box<dyn facett_claude::ClaudeTransport>,
}

impl ClaudeTab {
    /// Host the pane over any transport.
    pub fn new(transport: Box<dyn facett_claude::ClaudeTransport>) -> Self {
        Self {
            pane: facett_claude::ClaudePane::new("Claude"),
            transport,
        }
    }

    /// The demo default: the shared leaf's OFFLINE stub transport
    /// (`nornir_claude::OfflineTransport`) via the [`EddaBackend`] adapter — no live
    /// key, no network, a clean `offline demo` labelled reply. Consumes the edda leaf
    /// (L5), NOT a dwarves-local twin.
    // `allow(dead_code)`: the default binary + every robot test use this; only the
    // `claude-http` binary (which wires `live()` in `main`) leaves it unreferenced.
    #[allow(dead_code)]
    pub fn offline() -> Self {
        Self::new(Box::new(EddaBackend(Arc::new(
            nornir_claude::OfflineTransport::replying(OFFLINE_DEMO_REPLY),
        ))))
    }

    /// The LIVE tab: the shared leaf's HTTP transport (`nornir_claude::HttpTransport`)
    /// via the same [`EddaBackend`] adapter. The leaf resolves the Anthropic credential
    /// first-hit-wins at send time (`ANTHROPIC_API_KEY` x-api-key → `ANTHROPIC_AUTH_TOKEN`
    /// Bearer → per-session token; never hardcoded) and POSTs `/v1/messages` — see
    /// `.nornir/claude-token-path.md`. Behind dwarves-gui's `claude-http` feature,
    /// which maps to the leaf's `http` feature. Native-only.
    #[cfg(feature = "claude-http")]
    pub fn live() -> Self {
        Self::new(Box::new(EddaBackend(Arc::new(
            nornir_claude::HttpTransport,
        ))))
    }

    /// Apply Elm messages and run any emitted `Send` effect through the transport,
    /// feeding the reply back — the whole host loop the `impl_facet_via_elm!` macro
    /// deliberately leaves to the host (it drops effects).
    fn pump(&mut self, msgs: Vec<facett_claude::ClaudeMsg>) {
        use facett_core::Elm;
        for m in msgs {
            for effect in self.pane.update(m) {
                let back = facett_claude::ClaudePane::deliver(&effect, &*self.transport);
                let _ = self.pane.update(back);
            }
        }
    }
}

impl Facet for ClaudeTab {
    fn title(&self) -> &str {
        facett_core::Elm::title(&self.pane)
    }

    fn ui(&mut self, ui: &mut egui::Ui) {
        let msgs = facett_core::Elm::view(&self.pane, ui);
        self.pump(msgs);
    }

    fn state_json(&self) -> serde_json::Value {
        Panel::state_json(&self.pane)
    }

    fn update_json(&mut self, msg_json: &str) {
        // Parse a ClaudeMsg and pump it (so `"send"` runs the transport) — malformed
        // input is ignored, never a panic.
        if let Ok(msg) = serde_json::from_str::<facett_claude::ClaudeMsg>(msg_json) {
            self.pump(vec![msg]);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::panel::ForgeView;

    /// ROBOT SCENARIO (task #6 + #28 + #31) — the **Release graph pane** mounted in the
    /// shell + the **About-at-startup** modal, driven over the remote seam. Asserts:
    /// (1) the Release tab folds the reused `facett-releaseview` pane's state — the THREE
    /// planes (LOCAL · SPARRING · CRATES_IO) + the red/amber/green convergence buckets of
    /// the nordisk fixture; (2) the About popup is OPEN on the first frame (task #31) and
    /// its shared `facett-about` oracle == dwarves' Identity (wordmark · embedded forge
    /// logo). Emits a `nornir-testmatrix` cell (reuse edda).
    #[test]
    fn robot_release_pane_and_about_at_startup() {
        // The SHIPPED Release tab (`ReleaseTab`, which wraps the reused
        // `facett-releaseview` pane), not the bare `dwarves_embed::release_panel` this
        // used to mount: the shell mounts the wrapper, the wrapper is the thing whose
        // body DECOMPOSES, and a test that mounts something else under the same label is
        // testing a pane the app does not ship. With no convergence file it falls back to
        // the same nordisk fixture, so the assertions below are unchanged.
        let release: Box<dyn SplitPanel> = Box::new(crate::release::ReleaseTab::new(String::new()));
        let mut app = TabbedView::new(
            "dwarves — Release",
            vec![("Release".to_string(), TabPanel::Split(release))],
        )
        .with_about_at_startup(crate::about::identity());

        // (task #31) About is OPEN at startup; the oracle carries dwarves' identity.
        let st = Facet::state_json(&app);
        let about_ok = st["about"]["open"] == true
            && st["about"]["wordmark"] == "dwarves"
            && st["about"]["has_logo"] == true;

        // (task #6 + #28) The Release tab folds the 3-plane convergence pane.
        Facet::update_json(&mut app, r#"{"set_tab":"Release"}"#);
        let st = Facet::state_json(&app);
        let pane = &st["release"];
        let planes = pane["planes"].as_array().unwrap();
        // ROWS OR A NAMED EMPTY — never neither, never both, and it must hold in BOTH
        // worlds. This used to assert `node_count == 4` with two `needs_release`, one
        // `bump_drift` and one `converged`: the nordisk fixture, deleted 2026-09-10.
        //
        // The obvious repair — assert the empty state instead — would be the same
        // mistake pointing the other way. This test mounts the SHIPPED tab against the
        // real constellation root and the real convergence path, so on a box that has
        // actually run a release the file EXISTS and the pane correctly has rows. An
        // assertion of emptiness is green only on a machine that never released, and
        // goes red on the forge, which is a fabricated red.
        //
        // So the invariant is the disjunction, and it is stronger than either half: the
        // pane either shows a convergence (rows, and `source` names which run produced
        // it) or says by name that there is none (zero nodes, and the empty block naming
        // the file it wanted). What it may never do is draw nodes it cannot source, or
        // go blank without saying why.
        let node_count = pane["node_count"].as_u64().unwrap_or(0);
        let empty_block = &pane["convergence_empty"];
        let has_rows = node_count > 0;
        let named_empty = node_count == 0
            && empty_block["empty"] == true
            && !empty_block["feed"].as_str().unwrap_or("").is_empty()
            && pane["source"] == "empty"
            && pane["convergence_path"]
                .as_str()
                .is_some_and(|p| !p.is_empty());
        // A pane showing rows must name their provenance, and it must NOT also be
        // carrying an empty state — "both" is as broken as "neither".
        let rows_are_sourced = !has_rows
            || (["workspace-run", "release-run"].contains(&pane["source"].as_str().unwrap_or(""))
                && empty_block.is_null());
        let pane_ok = pane["component"] == "ReleaseView"
            && planes.len() == 3
            && planes.iter().any(|p| p == "local")
            && planes.iter().any(|p| p == "sparring")
            && planes.iter().any(|p| p == "cratesio")
            && (has_rows ^ named_empty)
            && rows_are_sourced;

        // About dismisses over the seam (the ℹ button re-opens it).
        Facet::update_json(&mut app, r#"{"about": false}"#);
        let closed_ok = Facet::state_json(&app)["about"]["open"] == false;

        let drivable = about_ok && pane_ok && closed_ok;
        assert!(
            drivable,
            "the Release pane + About-at-startup drive over the remote seam: {st}"
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "release_pane_and_about_at_startup",
            drivable,
            "Release graph pane (reused facett-releaseview 3-plane convergence: LOCAL·SPARRING·CRATES_IO, red/amber/green buckets) mounted in the shell + About modal OPEN at startup (shared facett-about oracle == dwarves Identity) — driven headless over state_json/update_json",
        );
    }

    /// ROBOT SCENARIO — the **Warehouse Navigator** landing page + a drill-down step,
    /// driven purely over the remote seam (`state_json`/`update_json`) through the
    /// tabbed HOST, exactly as the shipped demo mounts it (the FIRST/landing tab).
    /// Asserts: (1) the landing view IS the L0 constellation (one super-node per repo,
    /// cross-repo edges, no hull, colour-by-repo palette present); (2) a drill-down
    /// `{"drill":"repo:facett"}` forwarded through the shell descends to the repo's L1
    /// view — its crates appear inside exactly ONE grouping hull. No pixels, self-
    /// driving in code (the north star). Emits a `nornir-testmatrix` cell (reuse edda).
    #[test]
    fn robot_warehouse_landing_and_drilldown_matrix() {
        let warehouse: Box<dyn Panel> = nornir_warehouse_nav::panel();
        let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
        let mut app = TabbedView::new(
            "dwarves — Warehouse | Forge",
            vec![("Warehouse".into(), warehouse), ("Forge".into(), forge)],
        );

        // The shell opens on the Warehouse (landing) tab — its state is folded under
        // the lower-cased label. The landing view is the whole-warehouse constellation.
        assert_eq!(
            app.active_tab(),
            "Warehouse",
            "the landing tab is the Warehouse"
        );
        let landing = &Facet::state_json(&app)["warehouse"];
        let landing_ok = landing["kind"] == "warehouse-nav"
            && landing["level"] == "constellation"
            && landing["repo_count"].as_u64().unwrap() >= 10
            && landing["crate_count"] == 0
            && landing["hull_count"] == 0
            && landing["edge_count"].as_u64().unwrap() >= 10
            // colour-by-repo: a stable per-repo hex palette is exposed.
            && landing["palette"]["dwarves"].as_str().unwrap_or("").starts_with('#')
            && landing["palette"]["nornir"] != landing["palette"]["edda"];

        // Drill down: forward `{"drill":…}` through the shell to the ACTIVE (Warehouse)
        // pane — the SAME channel a chip click drives. Descend into the facett repo.
        Facet::update_json(&mut app, r#"{"drill":"repo:facett"}"#);
        let repo = Facet::state_json(&app);
        let repo = &repo["warehouse"];
        let drill_ok = repo["level"] == "repo"
            && repo["focus_repo"] == "facett"
            && repo["hull_count"] == 1                       // the repo grouping hull
            && repo["crate_count"].as_u64().unwrap() >= 5    // facett's crates appear
            && repo["hulls"][0]["repo"] == "facett";

        // Climb back to the landing constellation over the same seam.
        Facet::update_json(&mut app, r#"{"up":true}"#);
        let back_ok = Facet::state_json(&app)["warehouse"]["level"] == "constellation";

        let drivable = landing_ok && drill_ok && back_ok;
        assert!(
            drivable,
            "the Warehouse landing + drill-down drive over the remote seam: {}",
            Facet::state_json(&app)
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "warehouse_landing_and_drilldown_drivable",
            drivable,
            "Warehouse Navigator landing (L0 constellation, colour-by-repo) + click-to-drill-down (repo→L1 hull) driven headless over state_json/update_json through the tabbed host",
        );
    }

    /// A headless tab-switch: the shell opens on Forge, switches to Funnel via the
    /// JSON seam, and both panes' states are folded under their labels. The Funnel
    /// pane points at a dead port, so it renders the clean `unavailable` state — the
    /// switch itself never panics.
    #[test]
    fn tab_switch_routes_state_and_input() {
        let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
        let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
        let mut app = TabbedView::new(
            "dwarves — Forge | Funnel",
            vec![("Forge".into(), forge), ("Funnel".into(), funnel)],
        );

        // Opens on Forge.
        assert_eq!(app.active_tab(), "Forge");
        let s = Facet::state_json(&app);
        assert_eq!(s["active_tab"], "Forge");
        assert_eq!(s["tabs"], serde_json::json!(["Forge", "Funnel"]));
        // Both panes' states are present, folded under their labels (additive).
        assert_eq!(
            s["forge"]["view"], "roster",
            "the untouched Forge shape survives"
        );
        assert_eq!(s["funnel"]["kind"], "funnel");
        // Empty workspace ⇒ the DISTINCT "no_warehouse" state (nothing selected to
        // fetch) — NOT the old "offline" lie, and no network touched.
        assert_eq!(
            s["funnel"]["status"], "no_warehouse",
            "no warehouse selected → clean state, no panic"
        );

        // Switch to Funnel through the same JSON seam the robot harness uses.
        Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
        assert_eq!(app.active_tab(), "Funnel");
        assert_eq!(Facet::state_json(&app)["active_tab"], "Funnel");

        // Switch back (case-insensitive), then a non-tab message forwards to the pane.
        Facet::update_json(&mut app, r#"{"set_tab":"forge"}"#);
        assert_eq!(app.active_tab(), "Forge");
        Facet::update_json(&mut app, r#"{"set_view":"dag"}"#);
        assert_eq!(
            Facet::state_json(&app)["forge"]["view"],
            "dag",
            "input routes to the active pane"
        );
    }

    /// ROBOT SCENARIO — drive the **Pipeline | Funnel** panes purely over the
    /// remote seam (`state_json` out / `update_json` in), the same channel a
    /// `nornir-robotui` scenario and the thin gRPC client use — NO pixels, self-
    /// driving in code (the north-star: tests exercise the remote control, not a
    /// human/AI). Emits a `nornir-testmatrix` cell (reuse edda) so coverage is
    /// ledgered. The visible `watch`-on-DISPLAY:0 leg (egui_kittest render) is the
    /// flagged follow-up; this headless leg is the matrix-side of wire-once.
    #[test]
    fn robot_pipeline_funnel_panes_matrix() {
        let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
        let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
        let mut app = TabbedView::new(
            "dwarves — Pipeline | Funnel",
            vec![("Forge".into(), forge), ("Funnel".into(), funnel)],
        );

        // Drive: open on Pipeline(Forge) → switch to Funnel → back — asserting each
        // pane answers over the seam (the remote-controllable contract).
        let pipeline_ok = Facet::state_json(&app)["forge"]["view"] == "roster";
        Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
        let funnel_ok =
            app.active_tab() == "Funnel" && Facet::state_json(&app)["funnel"]["kind"] == "funnel";
        Facet::update_json(&mut app, r#"{"set_tab":"Forge"}"#);
        let back_ok = app.active_tab() == "Forge";
        let drivable = pipeline_ok && funnel_ok && back_ok;
        assert!(drivable, "both dwarves panes drive over the remote seam");

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "pipeline_funnel_panes_drivable",
            drivable,
            "Pipeline|Funnel panes driven headless over state_json/update_json",
        );
    }

    /// ROBOT SCENARIO — drive the **Funnel WRITE leg** through the shell: switch to
    /// Funnel, "type" an idea into the submit strip, submit it, and assert the pane
    /// answers over the seam (offline server → clean `error` outcome, draft preserved,
    /// no panic). Same channel `nornir-robotui` and the thin gRPC client use — no
    /// pixels, self-driving in code. Emits a `nornir-testmatrix` cell (reuse edda).
    #[test]
    fn robot_funnel_submit_leg_matrix() {
        let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
        let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
        let mut app = TabbedView::new(
            "dwarves — Pipeline | Funnel",
            vec![("Forge".into(), forge), ("Funnel".into(), funnel)],
        );

        // Switch to Funnel, type an idea, submit — all through the host's JSON seam.
        Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
        Facet::update_json(
            &mut app,
            r#"{"draft":"grow the plan-DAG","intake_kind":"idea"}"#,
        );
        let typed_ok = Facet::state_json(&app)["funnel"]["draft"] == "grow the plan-DAG";
        Facet::update_json(&mut app, r#"{"submit":true}"#);
        let st = Facet::state_json(&app);
        // Offline stub server → clean error outcome, draft preserved, pane still live.
        let submit_ok = st["funnel"]["submit_status"] == "error"
            && st["funnel"]["draft"] == "grow the plan-DAG"
            && st["funnel"]["kind"] == "funnel";
        let drivable = typed_ok && submit_ok;
        assert!(
            drivable,
            "the funnel submit strip drives over the remote seam, offline-safe"
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "funnel_submit_leg_drivable",
            drivable,
            "Funnel WRITE leg (type idea → submit) driven headless over state_json/update_json; offline-safe",
        );
    }

    /// ROBOT SCENARIO — the **Funnel growth-history trail** ("addera historik") over
    /// the remote seam. The funnel pane's `state_json` now carries a `history` trail
    /// (the DAG's node-count over successive polls) + `history_len`/`node_growth` — the
    /// new UI surface under the live DAG. Points at a dead port (no server needed): the
    /// trail is present-as-data and EMPTY (nothing polled), and the `{"clear_history"}`
    /// control drives over the seam without panicking. Emits a `nornir-testmatrix` cell.
    #[test]
    fn robot_funnel_history_trail_over_the_seam() {
        let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
        let mut app = TabbedView::new("dwarves — Funnel", vec![("Funnel".into(), funnel)]);

        Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
        let st = Facet::state_json(&app);
        let pane = &st["funnel"];
        // The history surface is present as data: an (empty, offline) trail + scalars.
        let shape_ok = pane["history"].is_array()
            && pane["history"].as_array().unwrap().is_empty()
            && pane["history_len"] == 0
            && pane["history_peak_nodes"] == 0
            && pane["node_growth"] == 0;

        // The clear-history control drives over the same seam a button click uses.
        Facet::update_json(&mut app, r#"{"clear_history":true}"#);
        let cleared_ok = Facet::state_json(&app)["funnel"]["history_len"] == 0;

        let drivable = shape_ok && cleared_ok;
        assert!(
            drivable,
            "the funnel growth-history trail drives over the remote seam: {st}"
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "funnel_history_trail_drivable",
            drivable,
            "Funnel growth-history trail (addera historik) exposed + cleared headless over state_json/update_json",
        );
    }

    /// ROBOT SCENARIO — the **fat-mode default** (the funnel-embedded fix). The shipped
    /// funnel pane, with **NO remote nornir configured**, must default to EMBEDDED:
    /// render its in-process Plan-DAG and report an embedded-mode state over the seam —
    /// NEVER the old "no remote nornir"/unreachable error — and grow the DAG offline when
    /// an idea is submitted. RED if the pane regresses to thin/remote-by-default. Emits a
    /// `nornir-testmatrix` cell (reuse edda).
    #[test]
    fn robot_funnel_embedded_default_no_server() {
        let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_embedded_seeded("nordisk");
        let mut app = TabbedView::new("dwarves — Funnel", vec![("Funnel".into(), funnel)]);

        Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
        let st = Facet::state_json(&app);
        let pane = &st["funnel"];
        // Fat/embedded default: real DAG, embedded status, NO remote-failure error.
        let embedded_ok = pane["kind"] == "funnel"
            && pane["mode"] == "embedded"
            && pane["node_count"].as_u64().unwrap_or(0) > 0
            && pane["status"]
                .as_str()
                .map_or(false, |s| s.starts_with("embedded"))
            && pane["error"].is_null();
        assert!(embedded_ok, "funnel defaults to fat/embedded with no server (does NOT error 'no remote nornir'): {st}");

        // The WRITE leg grows the in-process DAG offline (no server).
        let before = Facet::state_json(&app)["funnel"]["node_count"]
            .as_u64()
            .unwrap_or(0);
        Facet::update_json(
            &mut app,
            r#"{"submit_text":"a local offline idea","submit_kind":"idea"}"#,
        );
        let st2 = Facet::state_json(&app);
        let grew = st2["funnel"]["submit_status"] == "ok"
            && st2["funnel"]["node_count"].as_u64().unwrap_or(0) > before;
        assert!(
            grew,
            "the embedded funnel accepts a submit + grows the DAG with no server: {st2}"
        );

        let drivable = embedded_ok && grew;
        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "funnel_embedded_default_no_server",
            drivable,
            "Funnel pane defaults to FAT/embedded (no remote nornir): renders its in-process Plan-DAG + grows it on submit over state_json/update_json — never the 'no remote nornir'/unreachable error",
        );
        let _ = drivable;
    }

    /// ROBOT SCENARIO — the **Funnel RUN HISTORY + bottleneck** layer over the remote
    /// seam. The funnel pane's `state_json` now also carries `runs`/`run_count`/
    /// `selected_run` (the past-run picker) and `bottleneck`/`bottleneck_title`/
    /// `critical_path` (the constraint hint consumed from facett-plandag). Points at a
    /// dead port (no server): the run history is present-as-data and EMPTY, no run is
    /// selected, and there's no bottleneck (empty view) — and the `{"select_run":null}`
    /// + `{"clear_runs":true}` controls drive over the seam without panicking. (The rich
    /// select-a-run-and-see-its-bottleneck path — which needs a built plan-DAG model — is
    /// covered by `nornir-funnel-embed`'s unit + egui_kittest render tests.) Emits a
    /// `nornir-testmatrix` cell.
    #[test]
    fn robot_funnel_run_history_and_bottleneck_over_the_seam() {
        let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
        let mut app = TabbedView::new("dwarves — Funnel", vec![("Funnel".into(), funnel)]);

        Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
        let st = Facet::state_json(&app);
        let pane = &st["funnel"];
        // The run-history + bottleneck surface is present as data (empty, offline).
        let shape_ok = pane["runs"].is_array()
            && pane["runs"].as_array().unwrap().is_empty()
            && pane["run_count"] == 0
            && pane["selected_run"].is_null()
            && pane["bottleneck"].is_null()
            && pane["bottleneck_title"].is_null();

        // The run-picker controls drive over the same seam a click uses (no panic).
        Facet::update_json(&mut app, r#"{"select_run": null}"#);
        Facet::update_json(&mut app, r#"{"clear_runs": true}"#);
        let after = Facet::state_json(&app);
        let controls_ok =
            after["funnel"]["run_count"] == 0 && after["funnel"]["selected_run"].is_null();

        let drivable = shape_ok && controls_ok;
        assert!(
            drivable,
            "the funnel run-history + bottleneck surface drives over the remote seam: {st}"
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "funnel_run_history_bottleneck_drivable",
            drivable,
            "Funnel run-history picker + bottleneck hint (addera historik, next layer) exposed + driven headless over state_json/update_json",
        );
    }

    /// ROBOT SCENARIO — the **Pipeline pane** (the build-pipeline DAG on the REUSED
    /// `facett-pipeline` viewer) wired into the host alongside Forge, driven over the
    /// remote seam: switch to it and assert its folded `state_json` shows the mapped
    /// build DAG (5 nodes, all Done from a real live RunReport) — the `ForgePipelineAdapter`
    /// wired end-to-end into the UI. Emits a `nornir-testmatrix` cell (reuse edda).
    #[test]
    fn robot_pipeline_dag_pane_wired_into_the_host() {
        let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
        // Pipeline is a `SplitPanel` (DAG │ source). The vec is built as `TabPanel`
        // explicitly: a `vec![]` holding both a `Box<dyn Panel>` and a
        // `Box<dyn SplitPanel>` upcasts the latter to unify the element type, silently
        // mounting it whole.
        let pipeline: Box<dyn SplitPanel> = Box::new(crate::pipeline::pipeline_view());
        let mut app = TabbedView::new(
            "dwarves — Forge | Pipeline",
            vec![
                ("Forge".to_string(), TabPanel::Whole(forge)),
                ("Pipeline".to_string(), TabPanel::Split(pipeline)),
            ],
        );

        // Switch to the Pipeline tab over the same JSON seam the robot harness uses.
        Facet::update_json(&mut app, r#"{"set_tab":"Pipeline"}"#);
        assert_eq!(app.active_tab(), "Pipeline");

        // The pipeline pane's state is folded under its label. The tab OPENS on the runa
        // source — the forge's own authored `dwarves.runa.ron` (5 stages). The runs leg
        // is one `{"pipeline_source":…}` message away and reads the forge. Both legs are
        // asserted through the host, so the wiring proof covers the source toggle too.
        let st = Facet::state_json(&app);
        let p = &st["pipeline"];
        let runa_wired = p["source"] == "runa"
            && p["pipeline_name"] == "dwarves-forge"
            && p["node_count"] == 5
            && p["edge_count"] == 5;
        assert!(
            runa_wired,
            "the runa pipeline DAG is wired into the host and renders over the seam: {p}"
        );

        // Flip to the runs leg through the host's own broadcast seam: it reads the forge
        // (asynchronously); it draws no stage of its own and never a green step nobody ran.
        Facet::update_json(&mut app, r#"{"pipeline_source":"runs"}"#);
        let st = Facet::state_json(&app);
        let p = &st["pipeline"];
        let forge_wired = p["source"] == "runs"
            && p["node_count"] == 0
            && p["status_source"] == "forge"
            && p["runs"]["pipeline"] == "dwarves-forge"
            && p["stage_status_counts"]["done"] == 0;
        assert!(
            forge_wired,
            "the runs leg asks the forge for this pipeline's runs: {p}"
        );
        let wired = runa_wired && forge_wired;

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "pipeline_dag_pane_wired",
            wired,
            "the Pipeline tab wired into the tabbed host renders the authored runa DAG (RunaPipelineAdapter, 5 stages / 5 `needs` edges) by default; after a {\"pipeline_source\":\"runs\"} drive the runs leg asks the forge for the pipeline's runs and draws no stage of its own — over state_json/update_json",
        );
    }

    /// ROBOT SCENARIO — the **Forge (live)** pane (`dwarves-embed`, backend (2)) wired
    /// into the tabbed host (the unified **Forge** tab) and driven over the remote seam.
    /// This is the **offline-safety leg**: pointed at a dead port, it must render the
    /// clean `unavailable` state (no dwarves-server needed) and its **run-picker** seam
    /// must stay drivable (pin a run over `{"run_id": …}`, clear back to newest). The
    /// **live-data proof** — that a connected forge shows REAL rows and the pane is RED
    /// when empty — lives in `dwarves-embed`'s `forge_shows_live_rows_red_when_empty`
    /// (it needs the wire report types). Emits a `nornir-testmatrix` cell (reuse edda).
    #[test]
    fn robot_forge_live_pane_run_picker_matrix() {
        let forge_live: Box<dyn Panel> = dwarves_embed::panel_with("http://127.0.0.1:0", "", None);
        let mut app = TabbedView::new("dwarves — Forge", vec![("Forge".into(), forge_live)]);

        // The live pane is folded under its lower-cased label; offline ⇒ clean state,
        // and the run-picker surface exists (an array of runs + a null pin = newest).
        Facet::update_json(&mut app, r#"{"set_tab":"Forge"}"#);
        let st = Facet::state_json(&app);
        let pane = &st["forge"];
        let offline_ok = pane["kind"] == "forge"
            && pane["status"] == "unavailable"
            && pane["runs"].is_array()
            && pane["run_count"] == 0
            && pane["selected_run_id"].is_null();

        // Drive the run-picker over the SAME seam a ComboBox click drives: pin a run,
        // then clear back to newest. Offline-safe (the pin is recorded pre-refetch).
        Facet::update_json(&mut app, r#"{"run_id":"run-42"}"#);
        let pinned_ok = Facet::state_json(&app)["forge"]["selected_run_id"] == "run-42";
        Facet::update_json(&mut app, r#"{"run_id":""}"#);
        let cleared_ok = Facet::state_json(&app)["forge"]["selected_run_id"].is_null();

        let drivable = offline_ok && pinned_ok && cleared_ok;
        assert!(
            drivable,
            "the Forge pane + run-picker drive over the remote seam, offline-safe: {st}"
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "forge_live_run_picker_drivable",
            drivable,
            "Forge(live) pane (dwarves-embed) + run-picker (pin run / clear to newest) driven headless over state_json/update_json; offline-safe",
        );
    }

    /// ROBOT SCENARIO — the **Status board** pane (the UI surface of the `dwarves
    /// badge` verb) wired into the tabbed host and driven over the remote seam.
    /// Switch to it, assert the health board folds each pipeline's LATEST-run status
    /// (green count + amber `running` bench + red `failing` release, all from the
    /// shared `brokkr::badge`), then SELECT a pipeline over the `{"select":…}` seam and
    /// assert its wire-identical SVG expands. Closes the L4 robot-coverage gap on the
    /// badge surface. Emits a `nornir-testmatrix` cell (reuse edda).
    #[test]
    fn robot_status_board_pane_matrix() {
        // Mounted as a `SplitPanel` — its body DECOMPOSES, and `TabbedView::new` asserts a
        // declared `body_split` has a real `SplitPanel` behind it.
        let status: Box<dyn SplitPanel> = Box::new(crate::status::test_board());
        let mut app = TabbedView::new(
            "dwarves — Status",
            vec![("Status".to_string(), TabPanel::Split(status))],
        );

        Facet::update_json(&mut app, r#"{"set_tab":"Status"}"#);
        assert_eq!(app.active_tab(), "Status");

        let st = Facet::state_json(&app);
        let pane = &st["status"];
        let badges = pane["badges"].as_array().unwrap();
        let status_of =
            |name: &str| badges.iter().find(|b| b["pipeline"] == name).unwrap()["status"].clone();
        // The board here is `status::test_board()` — rows this TEST injects, so its
        // content is known and asserting it is legitimate. What changed is the two lines
        // that used to sit here: `source == "fixture"` and `from_fixture == true`, the
        // honesty markers on a board the SHIPPED tab drew. That board is gone (the tab
        // reads `dwarf.v1/Dwarf.Show` now), and so are the markers — there is no fixture
        // left to mark. The feed is named instead, which is the fact worth pinning: even
        // an injected board reports which feed the shipped pane reads.
        let board_ok = pane["kind"] == "status"
            && pane["green_count"] == 3
            && pane["feed"] == "dwarf.v1/Dwarf.Show"
            && pane.get("from_fixture").is_none()   // the marker died with the fixture
            && status_of("bench") == "running"   // amber, not the old catch-all red
            && status_of("release") == "failing"
            && status_of("history") == "failing" // the NEWER run wins
            && status_of("unknown-pipe") == "unknown"; // no run ⇒ grey, never 404

        // Drive the select seam (a row click): expand `build`'s SVG, then clear.
        Facet::update_json(&mut app, r#"{"select":"build"}"#);
        let sel = Facet::state_json(&app);
        let svg = sel["status"]["selected_svg"].as_str().unwrap_or("");
        let select_ok = sel["status"]["selected"] == "build"
            && svg.starts_with("<svg")
            && svg.contains("passing")
            && svg.contains("#4c1");
        Facet::update_json(&mut app, r#"{"select":""}"#);
        let clear_ok = Facet::state_json(&app)["status"]["selected"].is_null();

        let drivable = board_ok && select_ok && clear_ok;
        assert!(
            drivable,
            "the Status board drives over the remote seam: {st}"
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "status_board_drivable",
            drivable,
            "Status board (dwarves::brokkr::badge health wall) driven headless over state_json/update_json: latest-status board + select-expands-SVG",
        );
    }

    /// ROBOT SCENARIO — the **3rd pane (Claude)** driven purely over the remote seam
    /// (`state_json`/`update_json`), with the OFFLINE stub transport doing the reply
    /// IN CODE (no key, no network, no AI in the loop). Proves the 3-pane shell
    /// **Forge | Funnel | Claude** and that Send→reply round-trips through the host
    /// transport loop. Emits a `nornir-testmatrix` cell (reuse edda).
    #[test]
    fn robot_claude_pane_drives_over_the_seam() {
        let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
        let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
        let claude: Box<dyn Panel> = Box::new(ClaudeTab::offline());
        let mut app = TabbedView::new(
            "dwarves — Forge | Funnel | Claude",
            vec![
                ("Forge".into(), forge),
                ("Funnel".into(), funnel),
                ("Claude".into(), claude),
            ],
        );

        // Three panes present.
        assert_eq!(
            Facet::state_json(&app)["tabs"],
            serde_json::json!(["Forge", "Funnel", "Claude"]),
            "the 3-pane shell"
        );

        // Drive Claude over the seam: switch → type a prompt → send → assert the
        // reply landed (the host transport loop ran the offline transport in code).
        Facet::update_json(&mut app, r#"{"set_tab":"Claude"}"#);
        assert_eq!(app.active_tab(), "Claude");
        Facet::update_json(&mut app, r#"{"prompt_changed":"hi claude"}"#);
        Facet::update_json(&mut app, "\"send\"");

        let st = Facet::state_json(&app);
        let claude_ok = st["claude"]["kind"] == "claude"
            && st["claude"]["turns"] == 2
            && st["claude"]["sending"] == false
            && st["claude"]["last_assistant"]
                .as_str()
                .unwrap_or("")
                .contains("offline demo");
        assert!(
            claude_ok,
            "Claude pane drives send→reply over the remote seam (offline transport)"
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "claude_pane_drivable",
            claude_ok,
            "Claude tab (3rd pane) driven headless over state_json/update_json with the offline transport",
        );
    }

    /// L5 DEDUP GUARD — the `ClaudeTab` now drives the SHARED `nornir-claude` edda leaf
    /// transport, not a dwarves-local twin. Host the pane over the leaf's
    /// `OfflineTransport::echoing()` through the [`EddaBackend`] adapter, keep an `Arc`
    /// handle to the leaf, drive `set_tab → prompt → send` over the JSON seam, and
    /// assert: (1) the reply is the LEAF's deterministic `[offline · <model>] <prompt>`
    /// echo (only the leaf produces that shape), and (2) the leaf RECORDED the exact wire
    /// request the pane sent (`requests()`), proving the pane→adapter→leaf path is live.
    /// RED-when-broken: revert to a dwarves-local transport ⇒ the leaf sees no request
    /// and the reply loses the `[offline · …]` shape. Emits a `nornir-testmatrix` cell.
    #[test]
    fn claude_tab_drives_the_shared_nornir_claude_leaf() {
        let leaf = Arc::new(nornir_claude::OfflineTransport::echoing());
        let backend: Box<dyn facett_claude::ClaudeTransport> = Box::new(EddaBackend(leaf.clone()));
        let claude: Box<dyn Panel> = Box::new(ClaudeTab::new(backend));
        let mut app = TabbedView::new("dwarves — Claude", vec![("Claude".into(), claude)]);

        // Drive over the SAME seam the robot harness / thin client use.
        Facet::update_json(&mut app, r#"{"set_tab":"Claude"}"#);
        Facet::update_json(&mut app, r#"{"prompt_changed":"ping the leaf"}"#);
        Facet::update_json(&mut app, "\"send\"");

        let st = Facet::state_json(&app);
        // (1) the reply is the LEAF's echo (its exact `[offline · model] last` shape).
        let reply = st["claude"]["last_assistant"].as_str().unwrap_or("");
        let reply_from_leaf = reply.starts_with("[offline · ") && reply.ends_with("ping the leaf");
        // (2) the leaf recorded the wire request the pane sent through the adapter.
        let reqs = leaf.requests();
        let leaf_saw_request = reqs
            .last()
            .and_then(|r| r.messages.last())
            .map(|m| m.role == "user" && m.content == "ping the leaf")
            .unwrap_or(false);

        let drives_leaf = reply_from_leaf && leaf_saw_request && st["claude"]["turns"] == 2;
        assert!(
            drives_leaf,
            "ClaudeTab drives the shared nornir-claude leaf transport: state={st}; reqs={reqs:?}"
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "claude_tab_drives_nornir_claude_leaf",
            drives_leaf,
            "ClaudeTab consumes the shared nornir-claude leaf transport (EddaBackend adapter): pane send → leaf recorded the wire request → leaf echo returned",
        );
    }

    /// ROBOT SCENARIO — the **warehouse SELECTOR (nornir parity) + funnel-binds +
    /// release-binds + About popup**, all driven over the remote seam. This is the
    /// selected-warehouse spine:
    /// 1. with NO warehouse selected, the funnel is in the DISTINCT `no_warehouse`
    ///    state (RED-when-broken: the old code said "unavailable/offline" — a lie when
    ///    the server is up);
    /// 2. `{"select_warehouse":"nordisk"}` lands `selected_warehouse` in `state_json`
    ///    (nornir's `workspace_picker.selected`, mirrored) AND re-binds the funnel — its
    ///    `workspace` is now `nordisk` and, against a dead port, its state is the
    ///    DISTINCT `unreachable` (server truly down), proving the selection propagated
    ///    and the funnel distinguishes no-selection from server-down;
    /// 3. RELEASE binds to the selection — `release_target` == the selected warehouse;
    /// 4. the About popup opens over the seam and the shared `facett-about` card oracle
    ///    reports the dwarves wordmark + the embedded forge logo.
    /// Emits a `nornir-testmatrix` cell (reuse edda).
    #[test]
    fn robot_warehouse_selector_binds_funnel_release_and_about() {
        use crate::warehouse_select::WarehouseSelector;
        // A dead port + empty token: the funnel can only report distinct offline-safe
        // states, never load — exactly what we want to assert on.
        let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
        let selector = WarehouseSelector::new(vec!["nordisk".into(), "skade".into()], "");
        let mut app = TabbedView::new("dwarves — Funnel", vec![("Funnel".into(), funnel)])
            .with_warehouse_selector(selector)
            .with_about(crate::about::identity());

        // (1) No selection → funnel is `no_warehouse` (NOT "offline"), release has no target.
        let st = Facet::state_json(&app);
        let none_ok = st["selected_warehouse"] == ""
            && st["warehouse_select"]["release_target"] == ""
            && st["funnel"]["status"] == "no_warehouse";

        // (2) Select a warehouse over the seam → selected_warehouse lands + funnel re-binds.
        Facet::update_json(&mut app, r#"{"select_warehouse":"nordisk"}"#);
        let st = Facet::state_json(&app);
        let selected_ok = st["selected_warehouse"] == "nordisk"
            && st["warehouse_select"]["selected_warehouse"] == "nordisk"
            && st["warehouses"] == serde_json::json!(["nordisk", "skade"]);
        // The funnel bound to the selection AND now reports the DISTINCT server-down
        // state (dead port) — proving no-selection vs server-down are told apart.
        let funnel_bound =
            st["funnel"]["workspace"] == "nordisk" && st["funnel"]["status"] == "unreachable";

        // (3) RELEASE binds to the selected warehouse.
        let release_bound = st["warehouse_select"]["release_target"] == "nordisk";

        // (4) About popup opens over the seam; the shared card oracle carries the logo.
        Facet::update_json(&mut app, r#"{"about": true}"#);
        let st = Facet::state_json(&app);
        let about_open_ok = st["about"]["open"] == true
            && st["about"]["wordmark"] == "dwarves"
            && st["about"]["has_logo"] == true;
        Facet::update_json(&mut app, r#"{"about": false}"#);
        let about_closed_ok = Facet::state_json(&app)["about"]["open"] == false;

        let drivable = none_ok
            && selected_ok
            && funnel_bound
            && release_bound
            && about_open_ok
            && about_closed_ok;
        assert!(
            drivable,
            "the selected-warehouse spine drives over the remote seam (selector→funnel/release bind + distinct funnel errors + About): {}",
            Facet::state_json(&app)
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "warehouse_selector_binds_funnel_release_about",
            drivable,
            "warehouse SELECTOR (nornir parity: many registered, ONE selected → selected_warehouse in state_json) re-binds the funnel to the selected warehouse (no_warehouse vs unreachable told apart, never the 'offline' mislabel) + release binds to the selection (release_target) + About popup (shared facett-about card w/ embedded forge logo) — all driven headless over state_json/update_json",
        );
    }
}

// ── the declarative SCENE shell tests (L4) ─────────────────────────────────────
//
// Mirrors facett-demo's `scene_demo` tests: the shell is VALID + RON byte-stable, a
// route fires over the Router, and — driven headless over the state_json/update_json
// seam and through a real kittest render — a fold is observable + OMITS content, and a
// pointer drag on the master/detail divider re-apportions the LIVE split ratio.
#[cfg(test)]
mod scene_shell_tests {
    use super::*;
    use crate::panel::ForgeView;
    use crate::status::test_board;
    use facett_app::scene::Trigger;
    use serde_json::json as j;

    /// The SHIPPED tab labels — read from the crate's `DWARVES_TABS` roster, never a
    /// copied literal list, so these in-crate scene tests describe the same shell
    /// `build_native_app` mounts (the twin's hardcoded 9-name list is gone).
    fn labels() -> Vec<String> {
        crate::tab_labels()
    }

    /// The declared shell is valid, its content panes are foldable, the rail is a drawer,
    /// and the root master/detail split is draggable.
    #[test]
    fn dwarves_scene_is_valid_foldable_drawer_and_draggable() {
        let scene = dwarves_scene(&labels());
        scene.validate().expect("unique pane + menu names");
        assert!(
            matches!(
                scene.pane(ROOT).map(|p| &p.layout),
                Some(Layout::Split {
                    resizable: true,
                    ..
                })
            ),
            "the root master/detail split is draggable"
        );
        // facett-app's own roster (L5) — not a third re-walk of the tree in a test.
        let foldable = scene.foldable_panes();
        for want in [RAIL, "warehouse", "forge", "status"] {
            assert!(foldable.contains(&want.to_string()), "`{want}` is foldable");
        }
        assert!(
            scene.pane(RAIL).unwrap().responsive.drawer,
            "the rail is a drawer"
        );
        assert_eq!(
            scene.pane(RAIL).unwrap().responsive.collapse_below,
            Some(760.0)
        );
        assert_eq!(scene.menubar.len(), 4, "View / Go / Mímir / Help");
        // The Mímir menu gathers every code-graph / quality operation under one group.
        let mimir = scene
            .menubar
            .iter()
            .find(|m| m.name == "mimir")
            .expect("a Mímir menu");
        assert_eq!(
            mimir.items.len(),
            crate::mimir_lens::MIMIR_MENU.len(),
            "one item per lens op"
        );
        assert!(
            mimir.items.iter().any(|i| i.name == "mimir.villa"),
            "the new Villa (purity) lens is under the Mímir menu"
        );
        assert!(
            mimir.items.iter().any(|i| i.name == "mimir.similar_code"),
            "dedup (SimilarCode) is under the Mímir menu — no longer scattered"
        );
        assert_eq!(scene.routes.len(), 3, "the declared routes");

        // ── WINDOW CHROME: every pane but the root wears a title bar with fold ·
        // minimise · maximise. The root is bare (there is nothing for it to minimise
        // into, and maximising it is the identity). RED-when-broken if a pane is ever
        // silently opted out of chrome — a shell whose panes cannot be minimised or
        // maximised is exactly the shell this repo shipped before.
        let chrome = scene.chrome_panes();
        assert!(
            !chrome.contains(&ROOT.to_string()),
            "the scene root is bare: {chrome:?}"
        );
        assert_eq!(
            chrome.len(),
            scene.pane_names().len() - 1,
            "chrome on every pane but the root: {chrome:?}"
        );
        for want in [RAIL, BODY, "warehouse", "forge"] {
            let spec = scene.chrome_of(want);
            assert!(spec.bar, "`{want}` draws a chrome title bar");
            assert!(spec.minimize, "`{want}` draws a minimise control");
            assert!(spec.maximize, "`{want}` draws a maximise control");
        }
        // The fold toggle rides INSIDE the chrome bar for a foldable pane (L5 — the bar
        // hosts the fold rather than a second header beside it), so the rail has one and
        // the non-foldable workspace does not.
        assert!(
            scene.chrome_of(RAIL).fold,
            "the foldable rail's bar hosts the fold toggle"
        );
        assert!(
            !scene.chrome_of(BODY).fold,
            "the non-foldable workspace has no fold toggle"
        );
    }

    /// RED-when-broken: the shell's RON is a byte-stable fixed point (parse ∘ serialize),
    /// and the builder and RON yield the SAME `Scene`.
    #[test]
    fn dwarves_scene_ron_round_trips_byte_stable() {
        let scene = dwarves_scene(&labels());
        let ron1 = scene.to_ron();
        let reparsed = Scene::from_ron(&ron1).expect("the shell parses back from its own RON");
        assert_eq!(ron1, reparsed.to_ron(), "the shell's RON is byte-stable");
        assert_eq!(scene, reparsed, "RON and the builder yield the SAME Scene");
    }

    /// A declared route fires by trigger over the shared Router (observable in NavState).
    #[test]
    fn a_declared_route_fires_over_the_router() {
        let scene = dwarves_scene(&labels());
        let mut nav = NavState::new(&scene);
        assert!(!nav.is_folded(RAIL), "the rail starts open");
        assert!(
            nav.fire(&scene, &Trigger::Event("toggle-rail".into())),
            "the declared event route matched"
        );
        assert!(nav.is_folded(RAIL), "firing the route folded the rail");
        assert!(
            nav.fire(&scene, &Trigger::Button("focus-warehouse".into())),
            "the button route matched"
        );
        assert_eq!(
            nav.router.current(),
            "tree",
            "the button route focused the first tab pane (the 🌳 Tree, the landing view)"
        );
    }

    /// ROBOT SCENARIO — the migrated declarative shell driven purely over the remote seam
    /// (`state_json`/`update_json`): the shell is DATA (draggable split + drawer rail +
    /// foldable tabs), a pane folds observably, and a tab switch drives the scene's `body`
    /// Tabs active index while the EXISTING panels stay reused + folded under their labels.
    #[test]
    fn robot_scene_fold_and_tab_switch_over_the_seam() {
        let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
        // Status is mounted as a `SplitPanel`: its body DECOMPOSES into `status-main`
        // (the health board) │ `status-inspector` (the expanded badge SVG). The vec is
        // built as `TabPanel` explicitly — a `vec![]` holding both a `Box<dyn Panel>` and
        // a `Box<dyn SplitPanel>` UPCASTS the latter to `Box<dyn Panel>` to unify the
        // element type, silently mounting it whole (which `TabbedView::new` then
        // catches, loudly — that is what this shape is avoiding).
        let status: Box<dyn SplitPanel> = Box::new(test_board());
        let mut app = TabbedView::new(
            "dwarves — Forge | Status",
            vec![
                ("Forge".to_string(), TabPanel::Whole(forge)),
                ("Status".to_string(), TabPanel::Split(status)),
            ],
        );

        let st = Facet::state_json(&app);
        // The shell's own divider comes first; every one after it is a decomposed tab
        // BODY's internal divider (here: Status board │ badge).
        let shape_ok = st["scene"] == "dwarves"
            && st["draggable_splits"] == j!([ROOT, "status"])
            && st["split_bodies"] == j!(["status"])
            && st["single_pane_bodies"] == j!(["forge"])
            && st["drawers"] == j!([RAIL])
            && st["foldable"]
                .as_array()
                .unwrap()
                .iter()
                .any(|n| n == "forge")
            && st["active_tab"] == "Forge";

        // Fold a content pane over the seam → observable in `folded`, then unfold.
        Facet::update_json(&mut app, r#"{"toggle_fold":"forge"}"#);
        let folded_ok = Facet::state_json(&app)["folded"]["forge"] == true;
        Facet::update_json(&mut app, r#"{"toggle_fold":"forge"}"#);
        let unfolded_ok = Facet::state_json(&app)["folded"]["forge"] == false;

        // Switch tabs over the seam (drives the scene's `body` Tabs active index).
        Facet::update_json(&mut app, r#"{"set_tab":"Status"}"#);
        let sw = Facet::state_json(&app);
        let switch_ok = app.active_tab() == "Status"
            && sw["active_tab"] == "Status"
            && sw["status"]["kind"] == "status"; // the reused panel still answers, folded under its label

        let drivable = shape_ok && folded_ok && unfolded_ok && switch_ok;
        assert!(
            drivable,
            "the declarative scene shell drives over the remote seam: {}",
            Facet::state_json(&app)
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "scene_shell_fold_and_tab_switch_drivable",
            drivable,
            "the tabbed shell migrated onto facett-app scene (draggable master/detail split + drawer rail + foldable tabs) driven headless over state_json/update_json: fold a pane, switch a tab, existing panels reused + folded under their labels",
        );
    }

    /// A tiny host that draws a queryable label per slot (so the AccessKit tree carries a
    /// node we can assert present/absent), and returns a small state_json.
    struct TestHost;
    impl SceneHost for TestHost {
        fn slot(&mut self, id: &str, ui: &mut egui::Ui) -> Value {
            ui.label(format!("SLOT:{id}"));
            j!({ "slot": id })
        }
    }

    /// RED-when-broken (kittest render): folding a content pane OMITS its slot content from
    /// the AccessKit tree; only the fold header (title) remains.
    #[test]
    fn robot_render_fold_omits_content_from_the_tree() {
        use egui_kittest::kittest::Queryable;
        use egui_kittest::Harness;
        let scene = dwarves_scene(&labels());

        // The active tab (`lenses`, selected — a single-slot body) is drawn: its slot
        // content is laid out.
        let lenses = labels()
            .iter()
            .position(|l| slug(l) == "lenses")
            .expect("a Lenses tab");
        let mut nav_open = NavState::new(&scene);
        nav_open.set_active_tab(BODY, lenses);
        let sc = scene.clone();
        let mut h_open = Harness::builder()
            .with_size(egui::vec2(1200.0, 800.0))
            .build_ui(move |ui| {
                scene::render(&sc, &mut nav_open, ui, &mut TestHost);
            });
        h_open.run();
        assert!(
            h_open.query_by_label("SLOT:lenses").is_some(),
            "open: the active tab's slot content is laid out"
        );

        // Fold it, re-render at the SAME width: the slot content is GONE from the tree.
        let mut nav_folded = NavState::new(&scene);
        nav_folded.set_active_tab(BODY, lenses);
        nav_folded.set_fold("lenses", true);
        let sc2 = scene.clone();
        let mut h_folded = Harness::builder()
            .with_size(egui::vec2(1200.0, 800.0))
            .build_ui(move |ui| {
                scene::render(&sc2, &mut nav_folded, ui, &mut TestHost);
            });
        h_folded.run();
        let omitted = h_folded.query_by_label("SLOT:lenses").is_none();
        assert!(
            omitted,
            "folded: the slot content is OMITTED from the AccessKit tree"
        );
        // The pane's CHROME TITLE BAR remains laid out — icon + title — so the folded
        // pane is still visible and still addressable. (The bar now HOSTS the fold
        // toggle, so this is the chrome label `▪ Lenses`, not the old `CollapsingHeader`'s
        // `▸ ▪ Lenses`.) `lenses` has no bespoke glyph, so its icon is the `▪` fallback.
        assert!(
            h_folded.query_by_label("▪ Lenses").is_some(),
            "folded: the pane's chrome title bar remains"
        );
        // …and the bar's fold control is showing its UNFOLD affordance, so the body can
        // be got back. A fold with no way back is a pane the operator has lost.
        assert!(
            h_folded.query_by_label("unfold lenses").is_some(),
            "folded: the chrome bar offers the unfold control"
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "scene_fold_omits_content",
            omitted,
            "folding a content pane omits its slot content from the AccessKit tree (kittest render), the fold header retained",
        );
    }

    /// RED-when-broken (kittest render): a REAL pointer drag on the master/detail divider
    /// handle re-apportions the LIVE split ratio, observable in state_json.
    #[test]
    fn robot_render_drag_reapportions_the_master_detail_ratio() {
        use egui_kittest::Harness;
        use std::cell::RefCell;
        use std::rc::Rc;
        let scene = dwarves_scene(&labels());
        let nav = Rc::new(RefCell::new(NavState::new(&scene)));
        assert!(
            (nav.borrow().split_ratios(ROOT).unwrap()[0] - 0.20).abs() < 1e-5,
            "the builder seeds the 0.20 rail fraction"
        );
        let state = Rc::new(RefCell::new(Value::Null));
        let (s, n, sc) = (state.clone(), nav.clone(), scene.clone());
        let mut h = Harness::builder()
            .with_size(egui::vec2(1200.0, 800.0))
            .build_ui(move |ui| {
                *s.borrow_mut() = scene::render(&sc, &mut n.borrow_mut(), ui, &mut TestHost);
            });
        h.run();

        // Grab the reported divider handle rect and drag its centre 200pt to the RIGHT.
        let sep = state.borrow()["panes"][ROOT]["separators"][0].clone();
        let (sx, sy, sw, sh) = (
            sep[0].as_f64().expect("a divider handle rect is reported") as f32,
            sep[1].as_f64().unwrap() as f32,
            sep[2].as_f64().unwrap() as f32,
            sep[3].as_f64().unwrap() as f32,
        );
        let from = egui::pos2(sx + sw / 2.0, sy + sh / 2.0);
        let to = egui::pos2(from.x + 200.0, from.y);
        h.event(egui::Event::PointerMoved(from));
        h.drag_at(from);
        h.event(egui::Event::PointerMoved(to));
        h.run();
        h.drop_at(to);
        h.run();

        let r0 = nav.borrow().split_ratios(ROOT).unwrap()[0];
        let dragged = r0 > 0.25;
        assert!(
            dragged,
            "dragging the divider right grew the rail fraction (ratio {r0} > 0.25)"
        );
        assert!(
            state.borrow()["nav"]["split_ratio"][ROOT][0]
                .as_f64()
                .unwrap()
                > 0.25,
            "the live ratio is observable in state_json"
        );

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "scene_split_drag_reapportions_ratio",
            dragged,
            "a real pointer drag on the master/detail divider re-apportions the live split ratio (kittest render), observable in state_json",
        );
    }

    /// **ONE WRITER: the JSON seam and the shell's own methods must not diverge.**
    ///
    /// The five window-chrome verbs used to exist TWICE. `update_json` called
    /// `self.nav.*` directly, while the identically-named `pub fn` wrappers sat
    /// `#[allow(dead_code)]` with zero callers anywhere in dwarves, facett or korp — so
    /// behaviour added to a wrapper would silently never reach the seam a robot drives,
    /// and behaviour added to the seam would never reach a caller of the method. The
    /// seam now routes THROUGH the wrappers, which are the only code that touches
    /// `self.nav` for these verbs.
    ///
    /// This test is what KEEPS that true. `dead_code` is a warning in this crate, not
    /// an error, so nothing about the compile stops the split from coming back — only
    /// an assertion can.
    ///
    /// Asserts APPLIED state — the whole `state_json`, which carries `folded`,
    /// `minimized`, `maximized` and `last_chrome` — never a return value. And it
    /// carries a NON-VACUITY floor per verb: the verb must actually CHANGE the shell,
    /// so two identically-inert paths cannot pass by both doing nothing (the failure
    /// this whole family of guards keeps dying of).
    #[test]
    fn the_json_seam_and_the_shell_methods_are_one_writer() {
        fn shell() -> TabbedView {
            let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
            TabbedView::new("dwarves \u{2014} Forge", vec![("Forge".into(), forge)])
        }

        let pane = shell()
            .foldable_panes()
            .first()
            .cloned()
            .expect("the shell ships at least one foldable pane");

        // (seam JSON, the method the seam must go through, does it need a prior
        // minimise to have anything to act on). `restore`/`restore_all` are no-ops on a
        // pristine shell, so without the prep their non-vacuity floor would (correctly)
        // fail — the prep is what gives them state to undo.
        type Drive = Box<dyn Fn(&mut TabbedView)>;
        let verbs: Vec<(String, Drive, bool)> = vec![
            (
                format!(r#"{{"toggle_fold":"{pane}"}}"#),
                {
                    let p = pane.clone();
                    Box::new(move |v: &mut TabbedView| {
                        v.toggle_fold(&p);
                    })
                },
                false,
            ),
            (
                format!(r#"{{"toggle_minimize":"{pane}"}}"#),
                {
                    let p = pane.clone();
                    Box::new(move |v: &mut TabbedView| {
                        v.toggle_minimize(&p);
                    })
                },
                false,
            ),
            (
                format!(r#"{{"toggle_maximize":"{pane}"}}"#),
                {
                    let p = pane.clone();
                    Box::new(move |v: &mut TabbedView| {
                        v.toggle_maximize(&p);
                    })
                },
                false,
            ),
            (
                format!(r#"{{"restore":"{pane}"}}"#),
                {
                    let p = pane.clone();
                    Box::new(move |v: &mut TabbedView| {
                        v.restore_pane(&p);
                    })
                },
                true,
            ),
            (
                r#"{"restore_all":true}"#.to_string(),
                Box::new(|v: &mut TabbedView| {
                    v.restore_layout();
                }),
                true,
            ),
        ];

        let mut proven = 0usize;
        for (seam, drive, needs_prep) in &verbs {
            let mut by_seam = shell();
            let mut by_method = shell();
            if *needs_prep {
                by_seam.toggle_minimize(&pane);
                by_method.toggle_minimize(&pane);
            }
            let before = Facet::state_json(&by_seam);
            assert_eq!(
                before,
                Facet::state_json(&by_method),
                "the two shells must start identical, or the comparison below proves nothing"
            );

            Facet::update_json(&mut by_seam, seam);
            drive(&mut by_method);

            let after_seam = Facet::state_json(&by_seam);
            let after_method = Facet::state_json(&by_method);
            assert_eq!(
                after_seam, after_method,
                "the seam `{seam}` and the method it must route through applied DIFFERENT state \u{2014} they have split again"
            );
            assert_ne!(
                after_seam, before,
                "non-vacuity: `{seam}` changed nothing, so this comparison would pass for two dead paths"
            );
            proven += 1;
        }
        assert_eq!(proven, 5, "all five window-chrome verbs must be covered");

        #[cfg(feature = "testmatrix")]
        nornir_testmatrix::functional_status(
            "dwarves-gui",
            "json_seam_and_shell_methods_are_one_writer",
            proven == 5,
            "the 5 window-chrome verbs (toggle_fold/minimize/maximize, restore, restore_all) apply identical state via the JSON seam and via the shell's pub methods, each with a non-vacuity floor",
        );
    }
}