//! 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 {
let available = f32::from(window.viewport_size().width) - SIDEBAR_WIDTH - SHEET_INSET - PAGE_PADDING * 2.;
available.min(max_width) < STACK_BELOW
}
/// 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 ¤t {
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),
}
}