bcode_tui 0.0.1-alpha.0

Terminal user interface for bcode
//! TUI reasoning output settings dialog rendering.

use bmux_tui::component::{Component, Constraints, LayoutCx};
use bmux_tui::geometry::{Insets, Size};
use bmux_tui::paint::PaintCx;
use bmux_tui::prelude::{Line, Span};
use bmux_tui::style::Modifier;
use bmux_tui_components::dialog::{Dialog, DialogComponent};
use bmux_tui_components::modal_frame::{ModalPlacement, ModalSizing};

use super::render::TuiTheme;
use super::thinking_dialog::ThinkingDialogState;

const MIN_DIALOG_WIDTH: u16 = 56;
const MAX_DIALOG_WIDTH: u16 = 96;
const MIN_DIALOG_HEIGHT: u16 = 15;
const MAX_DIALOG_HEIGHT: u16 = 22;

/// Render a reasoning output settings dialog.
///
/// Render effective shortcuts from the same registry used by input.
pub fn render_thinking_dialog_with_keymap(
    state: &ThinkingDialogState,
    frame: &mut PaintCx<'_, '_>,
    theme: TuiTheme,
    keymap: &super::keymap::BmuxKeyMap,
) {
    let body = rows_with_keymap(state, theme, keymap);
    let dialog = Dialog::new(&body, &[], theme.modal_theme())
        .title(" Reasoning output settings ")
        .sizing(dialog_sizing())
        .padding(Insets::new(1, 2, 1, 2))
        .placement(ModalPlacement::UpperThird);
    let actions = std::cell::Cell::new(bmux_tui_components::action_row::ActionRowState::new());
    let component = DialogComponent::new("thinking.dialog", dialog, &actions);
    let layout = component.layout(
        Constraints::tight(bmux_tui::geometry::Size::new(
            frame.area().width,
            frame.area().height,
        )),
        &mut LayoutCx::new(),
    );
    component.paint(&layout, frame);
}

const fn dialog_sizing() -> ModalSizing {
    ModalSizing::new(
        Size::new(MIN_DIALOG_WIDTH, MIN_DIALOG_HEIGHT),
        Size::new(MAX_DIALOG_WIDTH, MAX_DIALOG_HEIGHT),
        Insets::all(4),
    )
}

fn rows_with_keymap(
    state: &ThinkingDialogState,
    theme: TuiTheme,
    keymap: &super::keymap::BmuxKeyMap,
) -> Vec<Line> {
    let mut rows = Vec::new();
    rows.push(Line::from_spans(vec![Span::styled(
        "Control requested reasoning effort and provider-visible reasoning summaries.",
        theme.text,
    )]));
    if !state.supported() {
        rows.push(Line::from_spans(vec![Span::styled(
            "This model does not advertise reasoning support. Add a model metadata override to enable it.",
            theme.selection,
        )]));
    }
    rows.push(modal_blank_line(theme));
    rows.push(setting_row(
        state.focused_row() == 0,
        "Show reasoning output",
        if state.visible() { "shown" } else { "hidden" },
        Some("local display filter; provider must emit reasoning events"),
        theme,
    ));
    rows.push(setting_row(
        state.focused_row() == 1,
        "Displayed reasoning",
        state.mode_label(),
        Some("local display filter"),
        theme,
    ));
    rows.push(setting_row(
        state.focused_row() == 2,
        "Reasoning effort",
        if state.supported() {
            state.effective_effort_label()
        } else {
            "unsupported"
        },
        Some(&values_help(
            state.effort_values(),
            state.effort_values_source(),
        )),
        theme,
    ));
    rows.push(setting_row(
        state.focused_row() == 3,
        "Visible reasoning summary",
        if state.supported() {
            state.effective_summary_label()
        } else {
            "unsupported"
        },
        Some(&values_help(
            state.summary_values(),
            state.summary_values_source(),
        )),
        theme,
    ));
    rows.push(modal_blank_line(theme));
    rows.push(Line::from_spans(vec![
        Span::styled(
            keymap.hint("thinking_dialog", "tui.control.submit"),
            theme.selection.add_modifier(Modifier::BOLD),
        ),
        Span::styled(" apply   ", theme.text),
        Span::styled(
            keymap.hint("thinking_dialog", "tui.control.cancel"),
            theme.selection.add_modifier(Modifier::BOLD),
        ),
        Span::styled(" cancel   ", theme.text),
        Span::styled(
            keymap.hint("thinking_dialog", "tui.control.down"),
            theme.focused.add_modifier(Modifier::BOLD),
        ),
        Span::styled(" move   ", theme.text),
        Span::styled(
            keymap.hint("thinking_dialog", "tui.control.toggle"),
            theme.focused.add_modifier(Modifier::BOLD),
        ),
        Span::styled(" change", theme.text),
    ]));
    rows
}

fn modal_blank_line(theme: TuiTheme) -> Line {
    Line::from_spans(vec![Span::styled("", theme.text)])
}

fn setting_row(
    focused: bool,
    label: &str,
    value: &str,
    help: Option<&str>,
    theme: TuiTheme,
) -> Line {
    let marker = if focused { "›" } else { " " };
    let marker_style = if focused {
        theme.focused.add_modifier(Modifier::BOLD)
    } else {
        theme.muted
    };
    let mut spans = vec![
        Span::styled(marker, marker_style),
        Span::styled(" ", theme.text),
        Span::styled(format!("{label}: "), theme.muted),
        Span::styled(value.to_owned(), theme.focused),
    ];
    if let Some(help) = help {
        spans.push(Span::styled("  ", theme.text));
        spans.push(Span::styled(help.to_owned(), theme.muted));
    }
    Line::from_spans(spans)
}

fn values_help(values: &[String], source: bcode_model::ModelReasoningCapabilitySource) -> String {
    if values.is_empty() {
        return "not supported or unknown".to_owned();
    }
    let values = values.join(", ");
    match source {
        bcode_model::ModelReasoningCapabilitySource::ConfigOverride => {
            format!("config: {values}")
        }
        bcode_model::ModelReasoningCapabilitySource::ProviderMetadata => {
            format!("provider: {values}")
        }
        bcode_model::ModelReasoningCapabilitySource::KnownModelTable => {
            format!("known model: {values}")
        }
        bcode_model::ModelReasoningCapabilitySource::GenericFallback
        | bcode_model::ModelReasoningCapabilitySource::Unknown => {
            format!("common values; provider may reject: {values}")
        }
    }
}