yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! The §11 Altitude-2 inspector's interaction glue: the tab strip (digit-key /
//! click tab select), the per-tab controls (the Transcript Raw toggle, the
//! Steps selection + drill-in tab), and the build of the focused agent's
//! view-models handed to the tested [`crate::inspector::render`] dispatch.
//!
//! Coverage-excluded like the rest of `shell/*`: every decision it forwards —
//! the tab select ([`AppModel::select_tab`]), the tab dispatch
//! ([`crate::inspector::render`]), and each view-model build — is tested in its
//! owning module. Only the active tab's VM is built, so a frame pays for one
//! surface's disk/git read, never all five.

use crate::AppModel;
use crate::config_edit::branch::governing_config;
use crate::files_view::{self, FilesView, Preview};
use crate::git_tree::AgentState;
use crate::inboxview::{Deposit, list_inbox};
use crate::inspector::TabData;
use crate::keymap::InspectorTab;
use crate::steps_view::{self, StepDetail, StepTab, StepsView};
use crate::transcript::{self, Transcript};
use std::path::Path;

use super::InspectorState;

/// Render the inspector tab strip and the selected tab's content for the
/// focused agent. No agent selected ⇒ a prompt; the digit keys and the tab
/// strip both target [`AppModel::select_tab`].
pub fn tabs_and_content(
    ui: &mut egui::Ui,
    model: &mut AppModel,
    inspector: &mut InspectorState,
    ws: &Path,
) {
    let active = model.inspector_tab();
    ui.horizontal(|ui| {
        for tab in InspectorTab::all() {
            if ui.selectable_label(tab == active, tab.label()).clicked() {
                model.select_tab(tab);
            }
        }
    });
    let Some(agent) = model.focused_agent() else {
        ui.weak("select an agent to inspect");
        return;
    };
    let agent_id = agent.agent_id.clone();
    let in_flight = agent.state == AgentState::InFlight;
    let state = agent.state;
    let tip = agent.tip_oid.clone();

    // Build only the active tab's view-model (one surface's read per frame).
    let transcript = build_transcript(active, ws, &agent_id, in_flight);
    let steps = build_steps(active, ws, &agent_id, state);
    let detail = build_detail(active, ws, &agent_id, &steps, inspector.step_sel);
    let inbox = build_inbox(active, ws, &agent_id);
    let files = build_files(active, ws, &agent_id);
    let file_preview = build_file_preview(active, ws, &agent_id, &files, inspector.files_sel);
    let governing = (active == InspectorTab::Config)
        .then(|| governing_config(ws, &tip).ok())
        .flatten();

    per_tab_controls(ui, active, inspector, &steps);

    let data = TabData {
        transcript,
        raw: inspector.raw,
        steps,
        step_sel: inspector.step_sel,
        step_detail: detail,
        step_tab: inspector.step_tab,
        inbox,
        files,
        file_preview,
        governing,
    };
    crate::inspector::render(
        ui,
        active,
        &data,
        &mut inspector.json_collapsed,
        &mut inspector.files_sel,
    );
}

fn build_files(tab: InspectorTab, ws: &Path, agent: &str) -> FilesView {
    if tab == InspectorTab::Files {
        files_view::build(ws, agent)
    } else {
        FilesView::default()
    }
}

/// The selected file's preview, when the Files tab is active and the selection
/// points at a file entry (a dir or a stale index yields `None`).
fn build_file_preview(
    tab: InspectorTab,
    ws: &Path,
    agent: &str,
    files: &FilesView,
    sel: Option<usize>,
) -> Option<Preview> {
    if tab != InspectorTab::Files {
        return None;
    }
    let FilesView::Present { entries, .. } = files else {
        return None;
    };
    let entry = entries.get(sel?)?;
    if entry.is_dir {
        return None;
    }
    Some(files_view::preview(
        &files_view::agent_worktree(ws, agent).join(&entry.rel_path),
    ))
}

fn build_transcript(tab: InspectorTab, ws: &Path, agent: &str, in_flight: bool) -> Transcript {
    if tab == InspectorTab::Transcript {
        transcript::build(ws, agent, in_flight)
    } else {
        Transcript::default()
    }
}

fn build_steps(tab: InspectorTab, ws: &Path, agent: &str, state: AgentState) -> StepsView {
    if tab == InspectorTab::Steps {
        steps_view::build(ws, agent, state)
    } else {
        StepsView::default()
    }
}

fn build_detail(
    tab: InspectorTab,
    ws: &Path,
    agent: &str,
    steps: &StepsView,
    sel: Option<usize>,
) -> Option<StepDetail> {
    if tab != InspectorTab::Steps {
        return None;
    }
    let seq = &steps.steps.get(sel?)?.seq;
    Some(steps_view::detail(ws, agent, seq))
}

fn build_inbox(tab: InspectorTab, ws: &Path, agent: &str) -> Vec<Deposit> {
    if tab == InspectorTab::Inbox {
        list_inbox(ws, agent)
    } else {
        Vec::new()
    }
}

/// The active tab's ephemera controls (§5.3): the Transcript Raw toggle and the
/// Steps step-selector + drill-in tab picker. Other tabs need none.
fn per_tab_controls(
    ui: &mut egui::Ui,
    tab: InspectorTab,
    inspector: &mut InspectorState,
    steps: &StepsView,
) {
    match tab {
        InspectorTab::Transcript => {
            ui.checkbox(&mut inspector.raw, "Raw (verbatim bytes)");
        }
        InspectorTab::Steps => step_controls(ui, inspector, steps),
        _ => {}
    }
}

/// The Steps tab's selection controls: a clickable step seq per row (sets the
/// drill-in target), and the drill-in tab picker (§11 meta/request/staging/
/// response/tools). Selection is RAM ephemera the caller owns.
fn step_controls(ui: &mut egui::Ui, inspector: &mut InspectorState, steps: &StepsView) {
    ui.horizontal(|ui| {
        ui.weak("step:");
        for (i, step) in steps.steps.iter().enumerate() {
            if ui
                .selectable_label(inspector.step_sel == Some(i), &step.seq)
                .clicked()
            {
                inspector.step_sel = Some(i);
            }
            // The §8.3 Login affordance beside an auth-shaped step failure: a
            // prompt-time credential failure surfaces here as derived agent state
            // (§13.3). Provider-specific Login lives one click away in the
            // Toolchain pane (which knows the provider rows).
            if step.auth_failed {
                ui.colored_label(crate::theme::ICHOR, "⚠ auth — Login ↙");
            }
        }
    });
    if inspector.step_sel.is_none() {
        return;
    }
    ui.horizontal(|ui| {
        for (which, label) in [
            (StepTab::Meta, "meta"),
            (StepTab::Request, "request"),
            (StepTab::Staging, "staging"),
            (StepTab::Response, "response"),
            (StepTab::Tools, "tools"),
        ] {
            if ui
                .selectable_label(inspector.step_tab == which, label)
                .clicked()
            {
                inspector.step_tab = which;
            }
        }
    });
}