Vectrum
Sign In
890 lines · 31.0 KB · Rust
//! The pieces every settings page is built from, after T3 Code's settings
//! layout: titled sections over raised cards of rows, each row a title and
//! description on the left and its control on the right (stacked below
//! 512px), with a reset button beside the title while the value differs
//! from its default. Plus the controls: switches, dropdowns, steppers, text
//! fields and the model pickers.

use std::{cell::Cell, f32::consts::PI, rc::Rc, time::Duration};

use gpui_kit::component::{
    Icon, IconName, Sizable as _, h_flex,
    input::{Input, InputState},
    menu::{DropdownMenu as _, PopupMenuItem},
    tooltip::Tooltip,
    v_flex,
};
use gpui_kit::prelude::FluentBuilder;
use gpui_kit::*;

use super::SettingsView;
use crate::claude::Effort;
use crate::models::{Catalog, ProbeStatus, Provider, split_key};
use crate::session::Reasoning;
use crate::settings::{self, Settings};
use crate::ui::icons::Lucide;
use crate::ui::metal::{self, MetalButton, MetalStyled as _, Palette};
use crate::ui::pickers;
use crate::ui::workspace::{SHEET_INSET, SIDEBAR_WIDTH};

/// Puts a setting back to its default.
pub(crate) type Reset = Rc<dyn Fn(&mut Window, &mut App)>;

/// The widest a settings page grows (T3 Code's "readable" width).
pub(crate) const READABLE_WIDTH: f32 = 896.;
/// Providers and Connections.
pub(crate) const WIDE_WIDTH: f32 = 1024.;
/// Diagnostics.
pub(crate) const EXPANDED_WIDTH: f32 = 1152.;
/// Rows narrower than this stack their control under the text.
const STACK_BELOW: f32 = 512.;
/// The page's padding on either side.
pub(crate) const PAGE_PADDING: f32 = 24.;

/// The row or section a settings search jumped to: it scrolls into view and
/// pulses twice.
pub(crate) struct Target {
    pub id: &'static str,
    /// Tells one jump's pulse from the next one's.
    pub generation: usize,
    /// Where the target was painted, for scrolling to it.
    pub bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
}

impl Global for Target {}

/// Whether a row on a page of `max_width` stacks its control under its
/// text in this window.
fn stacked(window: &Window, max_width: f32) -> bool {
    content_width(window, max_width) < STACK_BELOW
}

/// How wide a page of `max_width` is in this window.
pub(crate) fn content_width(window: &Window, max_width: f32) -> f32 {
    let available = f32::from(window.viewport_size().width) - SIDEBAR_WIDTH - SHEET_INSET - PAGE_PADDING * 2.;
    available.min(max_width)
}

/// Wraps `element` so a search jump to `id` can find and pulse it.
fn targetable(element: Div, id: Option<&'static str>, radius: f32, cx: &App) -> Div {
    let Some(target) = id.and_then(|id| cx.try_global::<Target>().filter(|target| target.id == id)) else {
        return element;
    };
    let p = metal::palette(cx);
    let bounds = target.bounds.clone();
    element
        .relative()
        .child(
            canvas(move |area, _, _| bounds.set(Some(area)), |_, _, _, _| {})
                .absolute()
                .inset_0(),
        )
        .child(
            div()
                .absolute()
                .inset_0()
                .rounded(px(radius))
                .border_2()
                .border_color(p.green_deep.opacity(0.7))
                .with_animation(
                    ElementId::NamedInteger("settings-target".into(), target.generation as u64),
                    Animation::new(Duration::from_millis(1300)).with_easing(|t| (2. * PI * t).sin().powi(2)),
                    |ring, delta| ring.opacity(delta),
                ),
        )
}

/// One setting: its title, what it does and its status on the left, the
/// control on the right.
#[derive(IntoElement)]
pub(crate) struct SettingRow {
    id: Option<&'static str>,
    title: SharedString,
    icon: Option<AnyElement>,
    title_suffix: Option<AnyElement>,
    description: Option<SharedString>,
    status: Option<AnyElement>,
    control: Option<AnyElement>,
    reset: Option<Reset>,
    below: Option<AnyElement>,
    dimmed: bool,
    max_width: f32,
}

impl SettingRow {
    pub fn new(title: impl Into<SharedString>) -> Self {
        Self {
            id: None,
            title: title.into(),
            icon: None,
            title_suffix: None,
            description: None,
            status: None,
            control: None,
            reset: None,
            below: None,
            dimmed: false,
            max_width: READABLE_WIDTH,
        }
    }

    /// The anchor a settings search jumps to.
    pub fn id(mut self, id: &'static str) -> Self {
        self.id = Some(id);
        self
    }

    /// An icon before the title.
    pub fn icon(mut self, icon: impl IntoElement) -> Self {
        self.icon = Some(icon.into_any_element());
        self
    }

    /// Something after the title, such as a version.
    pub fn title_suffix(mut self, suffix: impl IntoElement) -> Self {
        self.title_suffix = Some(suffix.into_any_element());
        self
    }

    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
        self.description = Some(description.into()).filter(|text: &SharedString| !text.is_empty());
        self
    }

    /// A line under the description saying how things stand.
    pub fn status(mut self, status: impl Into<SharedString>) -> Self {
        let status: SharedString = status.into();
        if !status.is_empty() {
            self.status = Some(status.into_any_element());
        }
        self
    }

    pub fn status_element(mut self, status: impl IntoElement) -> Self {
        self.status = Some(status.into_any_element());
        self
    }

    pub fn control(mut self, control: impl IntoElement) -> Self {
        self.control = Some(control.into_any_element());
        self
    }

    /// Shows the reset button, which runs `reset`.
    pub fn reset(mut self, reset: Option<Reset>) -> Self {
        self.reset = reset;
        self
    }

    /// Content under the row, like the fields a switch reveals.
    pub fn below(mut self, below: impl IntoElement) -> Self {
        self.below = Some(below.into_any_element());
        self
    }

    /// Fades the row, for settings that don't apply right now.
    pub fn dimmed(mut self, dimmed: bool) -> Self {
        self.dimmed = dimmed;
        self
    }

    /// The width of the page the row is on, for deciding when it stacks.
    pub fn page_width(mut self, width: f32) -> Self {
        self.max_width = width;
        self
    }
}

impl RenderOnce for SettingRow {
    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
        let p = metal::palette(cx);
        let stacked = stacked(window, self.max_width);
        let title = self.title.clone();
        let text = v_flex()
            .flex_1()
            .min_w_0()
            .gap(px(4.))
            .when(self.dimmed, |this| this.opacity(0.64))
            .child(
                h_flex()
                    .min_h(px(20.))
                    .gap(px(6.))
                    .children(self.icon)
                    .when(!self.title.is_empty(), |this| {
                        this.child(div().text_size(px(14.)).font_weight(FontWeight::MEDIUM).text_color(p.ink).child(self.title))
                    })
                    .children(self.title_suffix)
                    .when_some(self.reset, |this, reset| this.child(reset_button(&title, reset, p))),
            )
            .when_some(self.description, |this, description| {
                this.child(
                    div()
                        .max_w(px(576.))
                        .text_size(px(12.))
                        .line_height(px(18.))
                        .text_color(p.ink3)
                        .child(description),
                )
            })
            .when_some(self.status, |this, status| {
                this.child(div().pt(px(2.)).text_size(px(12.)).line_height(px(17.)).text_color(p.ink3).child(status))
            });
        let control = self.control.map(|control| {
            h_flex()
                .gap(px(8.))
                .map(|this| if stacked { this.w_full() } else { this.flex_none().justify_end() })
                .child(control)
        });
        let row = div()
            .px(px(16.))
            .pt(px(12.))
            .pb(px(if self.below.is_some() { 4. } else { 12. }))
            .child(
                div()
                    .flex()
                    .map(|this| {
                        if stacked {
                            this.flex_col().gap(px(12.))
                        } else {
                            this.flex_row().items_center().gap(px(32.))
                        }
                    })
                    .child(text)
                    .children(control),
            )
            .when_some(self.below, |this, below| this.child(div().pt(px(10.)).pb(px(8.)).child(below)));
        targetable(row, self.id, 12., cx)
    }
}

/// T3 Code's per-row reset: a small ghost button with an undo arrow.
fn reset_button(title: &SharedString, reset: Reset, p: &Palette) -> impl IntoElement {
    div()
        .id(SharedString::from(format!("reset-{title}")))
        .flex()
        .flex_none()
        .items_center()
        .justify_center()
        .size(px(20.))
        .rounded_full()
        .cursor_pointer()
        .text_color(p.ink3)
        .hover(|style| style.bg(p.row_hover).text_color(p.ink))
        .tooltip(|window, cx| Tooltip::new("Reset to default").build(window, cx))
        .on_click(move |_, window, cx| reset(window, cx))
        .child(Icon::new(Lucide::Undo2).size(px(12.)))
}

/// A reset for a setting read with `get`: shown while it differs from the
/// default, and putting the default back with `set`.
pub(crate) fn reset<T: PartialEq + 'static>(
    view: &WeakEntity<SettingsView>,
    get: fn(&Settings) -> T,
    set: fn(&mut Settings, T),
) -> Option<Reset> {
    let differs = settings::get(|settings| get(settings) != get(&Settings::default()));
    let view = view.clone();
    differs.then(|| -> Reset {
        Rc::new(move |window, cx| edit(&view, window, cx, |settings| set(settings, get(&Settings::default()))))
    })
}

/// A titled group of settings on a page.
pub(crate) struct Section {
    id: Option<&'static str>,
    title: SharedString,
    icon: Option<AnyElement>,
    description: Option<SharedString>,
    title_action: Option<AnyElement>,
    header_action: Option<AnyElement>,
    rows: Vec<AnyElement>,
    plain: bool,
    hide_title: bool,
}

impl FluentBuilder for Section {}

impl Section {
    pub fn new(title: impl Into<SharedString>) -> Self {
        Self {
            id: None,
            title: title.into(),
            icon: None,
            description: None,
            title_action: None,
            header_action: None,
            rows: Vec::new(),
            plain: false,
            hide_title: false,
        }
    }

    pub fn id(mut self, id: &'static str) -> Self {
        self.id = Some(id);
        self
    }

    pub fn icon(mut self, icon: impl IntoElement) -> Self {
        self.icon = Some(icon.into_any_element());
        self
    }

    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
        self.description = Some(description.into());
        self
    }

    /// A control right after the title, such as an add button.
    pub fn title_action(mut self, action: impl IntoElement) -> Self {
        self.title_action = Some(action.into_any_element());
        self
    }

    /// A control at the far end of the header.
    pub fn header_action(mut self, action: impl IntoElement) -> Self {
        self.header_action = Some(action.into_any_element());
        self
    }

    pub fn row(mut self, row: impl IntoElement) -> Self {
        self.rows.push(row.into_any_element());
        self
    }

    pub fn rows(mut self, rows: impl IntoIterator<Item = AnyElement>) -> Self {
        self.rows.extend(rows);
        self
    }

    /// No card: the rows sit on the sheet, a little apart.
    pub fn plain(mut self) -> Self {
        self.plain = true;
        self
    }

    /// Leaves the title off, like T3 Code's `hideTitle`.
    pub fn hide_title(mut self) -> Self {
        self.hide_title = true;
        self
    }

    pub fn render(self, cx: &App) -> AnyElement {
        let p = metal::palette(cx);
        if self.hide_title {
            let rows = v_flex().gap(px(4.)).children(self.rows);
            return targetable(rows, self.id, 18., cx).into_any_element();
        }
        let header = h_flex()
            .min_h(px(28.))
            .px(px(16.))
            .gap(px(8.))
            .child(
                h_flex()
                    .flex_1()
                    .min_w_0()
                    .gap(px(8.))
                    .children(self.icon)
                    .child(section_title(self.title, p))
                    .children(self.title_action),
            )
            .children(self.header_action);
        let header = targetable(header, self.id, 10., cx);
        v_flex()
            .gap(px(10.))
            .child(
                v_flex().gap(px(3.)).child(header).when_some(self.description, |this, description| {
                    this.child(div().px(px(16.)).text_size(px(12.)).line_height(px(18.)).text_color(p.ink3).child(description))
                }),
            )
            .map(|this| {
                if self.plain {
                    this.child(v_flex().gap(px(4.)).children(self.rows))
                } else {
                    this.child(card(self.rows, p))
                }
            })
            .into_any_element()
    }
}

pub(crate) fn section_title(title: impl Into<SharedString>, p: &Palette) -> Div {
    div().text_size(px(14.)).font_weight(FontWeight::MEDIUM).text_color(p.ink2).child(title.into())
}

/// A raised card of rows, with hairlines between them.
pub(crate) fn card(rows: Vec<AnyElement>, p: &Palette) -> Div {
    let count = rows.len();
    v_flex().rounded(px(18.)).raised(p).children(rows.into_iter().enumerate().flat_map(|(ix, row)| {
        let rule = (ix + 1 < count).then(|| div().px(px(16.)).child(metal::rule(p)).into_any_element());
        std::iter::once(row).chain(rule)
    }))
}

/// A titled card of rows: the short form of [`Section`].
pub(crate) fn section(title: &'static str, description: Option<&'static str>, rows: Vec<AnyElement>, p: &Palette) -> AnyElement {
    let _ = p;
    v_flex()
        .gap(px(10.))
        .child(
            v_flex()
                .gap(px(3.))
                .child(h_flex().min_h(px(28.)).px(px(16.)).child(section_title(title, p)))
                .when_some(description, |this, description| {
                    this.child(div().px(px(16.)).text_size(px(12.)).line_height(px(18.)).text_color(p.ink3).child(description))
                }),
        )
        .child(card(rows, p))
        .into_any_element()
}

/// A section that starts closed, opened by its title: T3 Code's folded
/// section, as used for "Legacy features".
pub(crate) fn folded(
    id: &'static str,
    title: &'static str,
    open: bool,
    rows: Vec<AnyElement>,
    view: &WeakEntity<SettingsView>,
    p: &Palette,
) -> AnyElement {
    let view = view.clone();
    v_flex()
        .gap(px(10.))
        .child(
            h_flex()
                .id(SharedString::from(format!("fold-{id}")))
                .min_h(px(28.))
                .px(px(16.))
                .gap(px(6.))
                .cursor_pointer()
                .text_color(p.ink2)
                .hover(|style| style.text_color(p.ink))
                .on_click(move |_, _, cx| {
                    let _ = view.update(cx, |view, cx| {
                        view.toggle_fold(id);
                        cx.notify();
                    });
                })
                .child(div().text_size(px(14.)).font_weight(FontWeight::MEDIUM).child(title))
                .child(
                    Icon::new(IconName::ChevronRight)
                        .size(px(16.))
                        .text_color(p.ink3)
                        .when(open, |icon| icon.rotate(Radians(PI / 2.))),
                ),
        )
        .when(open, |this| this.child(card(rows, p)))
        .into_any_element()
}

/// A muted line inside a card, for empty lists.
pub(crate) fn empty_row(text: impl Into<SharedString>, p: &Palette) -> AnyElement {
    div()
        .px(px(16.))
        .py(px(18.))
        .text_size(px(13.))
        .text_color(p.ink3)
        .child(text.into())
        .into_any_element()
}

/// A short note under a section, like T3 Code's footnotes.
pub(crate) fn note(text: impl Into<SharedString>, p: &Palette) -> Div {
    div().px(px(16.)).text_size(px(12.)).line_height(px(18.)).text_color(p.ink3).child(text.into())
}

// ---- controls ------------------------------------------------------------------

/// Saves an edit, then repaints the view and tells the conversations.
pub(crate) fn edit(view: &WeakEntity<SettingsView>, window: &mut Window, cx: &mut App, change: impl FnOnce(&mut Settings)) {
    settings::update(change);
    let _ = view.update(cx, |view, cx| view.changed(window, cx));
}

pub(crate) fn switch(
    id: &'static str,
    on: bool,
    view: &WeakEntity<SettingsView>,
    p: &Palette,
    set: impl Fn(&mut Settings, bool) + 'static,
) -> Stateful<Div> {
    switch_then(id, on, view, p, set, |_| {})
}

/// A switch that runs `then` after saving.
pub(crate) fn switch_then(
    id: &'static str,
    on: bool,
    view: &WeakEntity<SettingsView>,
    p: &Palette,
    set: impl Fn(&mut Settings, bool) + 'static,
    then: impl Fn(&mut App) + 'static,
) -> Stateful<Div> {
    let view = view.clone();
    metal::toggle(id, on, p, move |on, window, cx| {
        edit(&view, window, cx, |settings| set(settings, on));
        then(cx);
    })
}

/// A dropdown of fixed choices, at least `width` wide.
pub(crate) fn choice<T: Copy + PartialEq + 'static>(
    id: &'static str,
    current: T,
    options: &'static [(T, &'static str)],
    view: &WeakEntity<SettingsView>,
    set: impl Fn(&mut Settings, T) + 'static,
) -> AnyElement {
    let options: Vec<(T, SharedString)> = options.iter().map(|(value, label)| (*value, SharedString::from(*label))).collect();
    choice_of(id, current, options, None, view, set)
}

/// [`choice`] with labels made at runtime, and an optional label for the
/// button that differs from the selected item's.
pub(crate) fn choice_of<T: Copy + PartialEq + 'static>(
    id: &'static str,
    current: T,
    options: Vec<(T, SharedString)>,
    button_label: Option<SharedString>,
    view: &WeakEntity<SettingsView>,
    set: impl Fn(&mut Settings, T) + 'static,
) -> AnyElement {
    let label = button_label
        .or_else(|| options.iter().find(|(value, _)| *value == current).map(|(_, label)| label.clone()))
        .unwrap_or_default();
    let view = view.clone();
    let set = Rc::new(set);
    MetalButton::new(id)
        .compact()
        .label(label)
        .caret()
        .dropdown_menu_with_anchor(Anchor::TopRight, move |mut menu, _, _| {
            menu = menu.min_w(px(200.));
            for (value, label) in &options {
                let value = *value;
                let view = view.clone();
                let set = set.clone();
                menu = menu.item(PopupMenuItem::new(label.clone()).checked(value == current).on_click(move |_, window, cx| {
                    let set = set.clone();
                    edit(&view, window, cx, move |settings| set(settings, value));
                }));
            }
            menu
        })
        .into_any_element()
}

/// One option of a [`rich_choice`]: an icon, a label and a line about it.
pub(crate) struct RichOption<T> {
    pub value: T,
    pub icon: Option<Icon>,
    pub label: &'static str,
    pub description: &'static str,
}

/// A dropdown whose items explain themselves, like T3 Code's Permissions
/// select.
pub(crate) fn rich_choice<T: Copy + PartialEq + 'static>(
    id: &'static str,
    current: T,
    options: Vec<RichOption<T>>,
    view: &WeakEntity<SettingsView>,
    set: impl Fn(&mut Settings, T) + 'static,
) -> AnyElement {
    let selected = options.iter().find(|option| option.value == current);
    let label = selected.map_or("", |option| option.label);
    let icon = selected.and_then(|option| option.icon.clone());
    let view = view.clone();
    let set = Rc::new(set);
    let options = Rc::new(options);
    MetalButton::new(id)
        .compact()
        .when_some(icon, |this, icon| this.icon(icon))
        .label(label)
        .caret()
        .dropdown_menu_with_anchor(Anchor::TopRight, move |mut menu, _, cx| {
            let p = metal::palette(cx);
            menu = menu.min_w(px(300.));
            for option in options.iter() {
                let value = option.value;
                let view = view.clone();
                let set = set.clone();
                let (label, description) = (option.label, option.description);
                let (ink, ink3) = (p.ink, p.ink3);
                menu = menu.item(
                    PopupMenuItem::element(move |_, _| {
                        v_flex()
                            .py(px(2.))
                            .gap(px(1.))
                            .child(div().text_size(px(13.)).font_weight(FontWeight::SEMIBOLD).text_color(ink).child(label))
                            .child(div().text_size(px(12.)).text_color(ink3).child(description))
                    })
                    .when_some(option.icon.clone(), |item, icon| item.icon(icon))
                    .checked(value == current)
                    .on_click(move |_, window, cx| {
                        let set = set.clone();
                        edit(&view, window, cx, move |settings| set(settings, value));
                    }),
                );
            }
            menu
        })
        .into_any_element()
}

/// A number with − and + caps, like T3 Code's interval field.
#[allow(clippy::too_many_arguments)]
pub(crate) fn stepper(
    id: &'static str,
    value: u64,
    step: u64,
    (min, max): (u64, u64),
    unit: &'static str,
    view: &WeakEntity<SettingsView>,
    p: &Palette,
    set: impl Fn(&mut Settings, u64) + 'static,
) -> Div {
    let set = Rc::new(set);
    let button = |suffix: &'static str, icon: IconName, next: u64, enabled: bool| {
        let view = view.clone();
        let set = set.clone();
        MetalButton::new(SharedString::from(format!("{id}-{suffix}")))
            .ghost()
            .mini()
            .icon(icon)
            .disabled(!enabled)
            .on_click(move |_, window, cx| {
                let set = set.clone();
                edit(&view, window, cx, move |settings| set(settings, next));
            })
    };
    h_flex()
        .gap(px(8.))
        .child(
            h_flex()
                .h(px(30.))
                .px(px(3.))
                .gap(px(2.))
                .rounded(px(12.))
                .well(p)
                .child(button("less", IconName::Minus, value.saturating_sub(step).max(min), value > min))
                .child(
                    div()
                        .min_w(px(48.))
                        .flex()
                        .justify_center()
                        .font_family(metal::MONO)
                        .text_size(px(13.))
                        .text_color(p.ink)
                        .child(value.to_string()),
                )
                .child(button("more", IconName::Plus, (value + step).min(max), value < max)),
        )
        .when(!unit.is_empty(), |this| this.child(div().text_size(px(12.5)).text_color(p.ink3).child(unit)))
}

/// Focuses a field once it has been drawn, for fields a click reveals.
pub(crate) fn focus_later(input: &Entity<InputState>, window: &mut Window) {
    let input = input.clone();
    window.on_next_frame(move |window, cx| input.update(cx, |input, cx| input.focus(window, cx)));
    window.refresh();
}

/// A search field: a well with plain text, unlike the mono value fields.
pub(crate) fn search_field(input: &Entity<InputState>, width: f32, cx: &App) -> Div {
    let p = metal::palette(cx);
    h_flex()
        .w(px(width))
        .h(px(30.))
        .px(px(11.))
        .gap(px(8.))
        .rounded(px(12.))
        .well(p)
        .text_size(px(13.))
        .text_color(p.ink)
        .child(Icon::new(IconName::Search).size(px(14.)).text_color(p.ink3))
        .child(div().flex_1().min_w_0().child(Input::new(input).appearance(false).small()))
}

/// A text field, sunk into a well.
pub(crate) fn text_field(input: &Entity<InputState>, width: f32, _: &mut Window, cx: &App) -> Div {
    let p = metal::palette(cx);
    h_flex()
        .w(px(width))
        .h(px(30.))
        .px(px(11.))
        .rounded(px(12.))
        .well(p)
        .font_family(metal::MONO)
        .text_size(px(12.))
        .text_color(p.ink)
        .child(div().flex_1().min_w_0().child(Input::new(input).appearance(false).small()))
}

/// A model dropdown across every enabled provider, with a first entry for
/// "no model" (`none_label`: automatic, or off). `one_off` leaves out the
/// agents that can't answer a one-off prompt.
pub(crate) fn model_control(
    id: &'static str,
    current: Option<String>,
    none_label: &'static str,
    one_off: bool,
    pick: impl Fn(Option<String>, &mut Window, &mut App) + 'static,
    cx: &App,
) -> AnyElement {
    let catalog = Catalog::global(cx);
    let p = metal::palette(cx);
    let (icon, label): (Option<Provider>, SharedString) = match &current {
        None => (None, none_label.into()),
        Some(key) => match catalog.find_key(key) {
            Some(model) => (Some(model.provider), model.name.clone().into()),
            None => {
                let (instance, model) = split_key(key).unwrap_or((key, key));
                let kind = catalog.instance(instance).map(|state| state.kind()).or_else(|| Provider::from_key(instance));
                (kind, model.to_string().into())
            }
        },
    };
    // One menu section per enabled instance: its label, then its models.
    let groups: Vec<(String, Vec<(String, String)>, String)> = catalog
        .enabled()
        .filter(|state| !one_off || state.kind().answers_one_off())
        .map(|state| {
            let models = state
                .models
                .iter()
                .filter(|model| !catalog.is_hidden(model))
                .map(|model| {
                    let name = match model.provider {
                        Provider::Claude if model.legacy => format!("{} (legacy)", model.name),
                        Provider::Claude => model.name.clone(),
                        Provider::Pi | Provider::Jcode => format!("{} · {}", model.name, model.detail),
                    };
                    (model.key(), name)
                })
                .collect();
            let note = match &state.status {
                ProbeStatus::Checking => "Asking for its models…".to_string(),
                ProbeStatus::Missing => "Not installed".to_string(),
                ProbeStatus::Failed(_) => "Didn't list its models".to_string(),
                _ => "No models".to_string(),
            };
            (state.label(), models, note)
        })
        .collect();
    let empty = groups.is_empty();
    let pick = Rc::new(pick);
    MetalButton::new(id)
        .compact()
        .map(|this| match icon {
            Some(provider) => this.icon(pickers::provider_icon(provider, px(13.), p)),
            None => this,
        })
        .label(label)
        .caret()
        .max_w(px(260.))
        .dropdown_menu_with_anchor(Anchor::TopRight, move |mut menu, _, _| {
            let item = |key: Option<String>| {
                let pick = pick.clone();
                move |_: &ClickEvent, window: &mut Window, cx: &mut App| pick(key.clone(), window, cx)
            };
            menu = menu
                .min_w(px(260.))
                .max_h(px(440.))
                .scrollable(true)
                .item(PopupMenuItem::new(none_label).checked(current.is_none()).on_click(item(None)));
            if empty {
                menu = menu.separator().item(PopupMenuItem::new("No providers available").disabled(true));
            }
            for (label, models, note) in &groups {
                menu = menu.separator().label(label.clone());
                if models.is_empty() {
                    menu = menu.item(PopupMenuItem::new(note.clone()).disabled(true));
                }
                for (key, name) in models {
                    let checked = current.as_deref() == Some(key.as_str());
                    menu = menu.item(PopupMenuItem::new(name.clone()).checked(checked).on_click(item(Some(key.clone()))));
                }
            }
            menu
        })
        .into_any_element()
}

/// A reasoning dropdown, with a first entry for "no level" (`none_label`).
pub(crate) fn reasoning_control(
    id: &'static str,
    current: Option<Reasoning>,
    none_label: &'static str,
    pick: impl Fn(Option<Reasoning>, &mut Window, &mut App) + 'static,
) -> AnyElement {
    let mut choices = vec![Reasoning::Default];
    choices.extend([Effort::Low, Effort::Medium, Effort::High, Effort::XHigh, Effort::Max].map(Reasoning::Level));
    choices.extend([Reasoning::Ultracode, Reasoning::Ultrathink]);
    let pick = Rc::new(pick);
    MetalButton::new(id)
        .compact()
        .icon(Lucide::Brain)
        .label(current.map(Reasoning::label).unwrap_or(none_label))
        .caret()
        .dropdown_menu_with_anchor(Anchor::TopRight, move |mut menu, _, _| {
            let item = |reasoning: Option<Reasoning>| {
                let pick = pick.clone();
                move |_: &ClickEvent, window: &mut Window, cx: &mut App| pick(reasoning, window, cx)
            };
            menu = menu
                .min_w(px(220.))
                .item(PopupMenuItem::new(none_label).checked(current.is_none()).on_click(item(None)))
                .separator();
            for reasoning in choices.clone() {
                if reasoning == Reasoning::Default && none_label == "Default" {
                    continue;
                }
                if reasoning == Reasoning::Ultracode {
                    menu = menu.separator().label("Claude only");
                }
                menu = menu.item(
                    PopupMenuItem::new(reasoning.label()).checked(current == Some(reasoning)).on_click(item(Some(reasoning))),
                );
            }
            menu
        })
        .into_any_element()
}

/// A radio dot: a well, with a raised green bead when picked.
pub(crate) fn radio(selected: bool, p: &Palette) -> Div {
    div()
        .flex()
        .flex_none()
        .items_center()
        .justify_center()
        .size(px(16.))
        .rounded_full()
        .well(p)
        .when(selected, |this| this.child(div().size(px(8.)).rounded_full().bg(p.green_deep)))
}

/// Inline code, like T3 Code's version labels: `v2.1.0`.
pub(crate) fn code(text: impl Into<SharedString>, p: &Palette) -> Div {
    div()
        .flex_none()
        .px(px(5.))
        .rounded(px(5.))
        .bg(p.ink.opacity(0.06))
        .font_family(metal::MONO)
        .text_size(px(11.))
        .text_color(p.ink2)
        .child(text.into())
}

/// `2m ago`, `just now`.
pub(crate) fn ago(time: std::time::SystemTime) -> String {
    let secs = time.elapsed().unwrap_or_default().as_secs();
    match secs {
        0..60 => "just now".into(),
        60..3_600 => format!("{}m ago", secs / 60),
        3_600..86_400 => format!("{}h ago", secs / 3_600),
        _ => format!("{}d ago", secs / 86_400),
    }
}