Vectrum
Sign In
1449 lines · 66.5 KB · Rust
//! The Providers page in T3 Code's two-pane layout: the agent instances on
//! the left, each with its status and a switch, and the selected one's
//! editor on the right (display name and accent color, account, runtime
//! fields, environment variables and models). "Add provider" opens a
//! three-step wizard. Below: usage hubs and the health check interval.

use std::{path::Path, process::Stdio};

use gpui_kit::component::{
    Icon, IconName, WindowExt as _, h_flex,
    input::{InputEvent, InputState},
    notification::Notification,
    tooltip::Tooltip,
    v_flex,
};
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;

use super::SettingsView;
use super::widgets::{Section, SettingRow, WIDE_WIDTH, ago, code, content_width, focus_later, note, reset, search_field, stepper, text_field};
use crate::claude;
use crate::models::{self, Catalog, InstanceState, ModelEntry, ProbeStatus, Provider};
use crate::settings::{self, CustomModel, EnvVar, ProviderInstance};
use crate::ui::chat::display_path;
use crate::ui::icons::Lucide;
use crate::ui::metal::{self, Led, MetalButton, MetalStyled as _, Palette};
use crate::ui::pickers;

/// The list pane's width.
const LIST_WIDTH: f32 = 272.;

/// One environment variable's fields.
struct EnvField {
    key: Entity<InputState>,
    value: Entity<InputState>,
    sensitive: bool,
    /// The name its secret is stored under, to move it when renamed.
    saved_key: String,
}

/// The fields of the instance open on the page.
struct ProviderForm {
    id: String,
    name: Entity<InputState>,
    binary: Entity<InputState>,
    home: Entity<InputState>,
    auto_compact: Entity<InputState>,
    args: Entity<InputState>,
    accent: Entity<InputState>,
    env: Vec<EnvField>,
    model_filter: Entity<InputState>,
    custom_slug: Entity<InputState>,
    adding_custom: bool,
    custom_error: Option<SharedString>,
    /// The custom model whose display name is being edited, and the field.
    editing_custom: Option<(String, Entity<InputState>)>,
    accent_open: bool,
    auto_compact_error: bool,
    _subscriptions: Vec<Subscription>,
}

/// The "Add provider" wizard.
struct Wizard {
    step: usize,
    kind: Provider,
    label: Entity<InputState>,
    id: Entity<InputState>,
    accent: String,
    binary: Entity<InputState>,
    home: Entity<InputState>,
    args: Entity<InputState>,
    error: Option<SharedString>,
}

#[derive(Default)]
pub(super) struct ProvidersState {
    form: Option<ProviderForm>,
    wizard: Option<Wizard>,
    /// Instances an update is running for.
    updating: Vec<String>,
}

/// T3 Code's status headline and detail for an instance.
fn status_headline(state: &InstanceState) -> (Led, String) {
    match &state.status {
        ProbeStatus::Disabled => (Led::Off, "Disabled".into()),
        ProbeStatus::Checking => (Led::Waiting, "Checking provider status".into()),
        ProbeStatus::Missing => (Led::Failed, "Not found · CLI not detected on PATH.".into()),
        ProbeStatus::Failed(error) => (Led::Failed, format!("Unavailable · {error}")),
        ProbeStatus::Ready => {
            let mut parts = Vec::new();
            match state.kind() {
                Provider::Claude if state.account.is_some() => parts.push("Authenticated".to_string()),
                Provider::Claude => parts.push("Not authenticated".to_string()),
                Provider::Pi | Provider::Jcode if state.models.is_empty() => parts.push("Needs attention · No models configured".into()),
                Provider::Pi | Provider::Jcode => parts.push("Available".to_string()),
            }
            parts.extend(state.plan.clone());
            let led = if parts[0].starts_with("Not") || parts[0].starts_with("Needs") { Led::Waiting } else { Led::Live };
            (led, parts.join(" · "))
        }
    }
}

/// `#3e9bfa` for a color.
fn hex_of(color: Hsla) -> String {
    let rgba = color.to_rgb();
    let channel = |value: f32| (value.clamp(0., 1.) * 255.).round() as u8;
    format!("#{:02x}{:02x}{:02x}", channel(rgba.r), channel(rgba.g), channel(rgba.b))
}

/// A saved accent color, if it parses.
fn accent_color(hex: &str) -> Option<Hsla> {
    let digits = hex.trim().trim_start_matches('#');
    (matches!(digits.len(), 3 | 6) && digits.chars().all(|c| c.is_ascii_hexdigit())).then(|| metal::hex(&format!("#{digits}")))
}

/// The colors the accent picker offers: the theme's tints and accents.
fn accent_choices(p: &Palette) -> Vec<Hsla> {
    let mut colors = p.tints.to_vec();
    colors.extend([p.green_deep, p.amber, p.red]);
    colors
}

/// An instance's icon, with a dot of its accent color.
fn instance_icon(kind: Provider, accent: &str, size: f32, p: &Palette) -> Div {
    div()
        .relative()
        .flex_none()
        .child(pickers::provider_icon(kind, px(size), p))
        .when_some(accent_color(accent), |this, color| {
            this.child(
                div()
                    .absolute()
                    .right(px(-3.))
                    .bottom(px(-3.))
                    .size(px(8.))
                    .rounded_full()
                    .bg(color)
                    .shadow(vec![BoxShadow::new(px(0.), px(0.), p.s_hi).spread_radius(px(1.5))]),
            )
        })
}

fn home_title(kind: Provider) -> &'static str {
    match kind {
        Provider::Claude => "CLAUDE_CONFIG_DIR path",
        Provider::Pi => "PI_CODING_AGENT_DIR path",
        Provider::Jcode => "JCODE_HOME path",
    }
}

fn home_placeholder(kind: Provider) -> &'static str {
    match kind {
        Provider::Claude => "~/.claude",
        Provider::Pi => "~/.pi/agent",
        Provider::Jcode => "~/.jcode",
    }
}

fn custom_placeholder(kind: Provider) -> &'static str {
    match kind {
        Provider::Claude => "claude-sonnet-5",
        Provider::Pi => "anthropic/claude-sonnet-5",
        Provider::Jcode => "claude-oauth:claude-opus-5",
    }
}

impl SettingsView {
    fn instance_input(&self, value: &str, placeholder: &str, window: &mut Window, cx: &mut Context<Self>) -> Entity<InputState> {
        let (value, placeholder) = (value.to_string(), placeholder.to_string());
        cx.new(|cx| InputState::new(window, cx).placeholder(placeholder).default_value(value))
    }

    /// Opens an instance's fields on the page.
    fn select_provider(&mut self, id: &str, window: &mut Window, cx: &mut Context<Self>) {
        let Some(instance) = settings::get(|settings| settings.instance(id).cloned()) else {
            self.providers.form = None;
            cx.notify();
            return;
        };
        let kind = instance.kind;
        let name = self.instance_input(&instance.name, kind.label(), window, cx);
        let binary = self.instance_input(&instance.binary, kind.key(), window, cx);
        let home = self.instance_input(&instance.home, home_placeholder(kind), window, cx);
        let auto_compact = self.instance_input(&instance.auto_compact, "e.g. 300000", window, cx);
        let args = self.instance_input(&instance.args, if kind == Provider::Claude { "e.g. --chrome" } else { "" }, window, cx);
        let accent = self.instance_input(&instance.accent, "#2563eb", window, cx);
        let env: Vec<EnvField> = instance
            .env
            .iter()
            .map(|var| {
                let has_secret = var.sensitive && settings::has_secret(&instance.id, &var.key);
                let placeholder = if has_secret { "Stored secret, enter a new value to replace" } else { "value" };
                let value = cx.new(|cx| {
                    InputState::new(window, cx).placeholder(placeholder).default_value(var.value.clone()).masked(var.sensitive)
                });
                EnvField {
                    key: self.instance_input(&var.key, "VARIABLE_NAME", window, cx),
                    value,
                    sensitive: var.sensitive,
                    saved_key: var.key.clone(),
                }
            })
            .collect();
        let model_filter = self.instance_input("", "Filter models", window, cx);
        let custom_slug = self.instance_input("", custom_placeholder(kind), window, cx);
        let mut subscriptions = Vec::new();
        let fields = [&name, &binary, &home, &auto_compact, &args, &accent]
            .into_iter()
            .chain(env.iter().flat_map(|field| [&field.key, &field.value]));
        for field in fields {
            subscriptions.push(cx.subscribe_in(field, window, |this, _, event: &InputEvent, window, cx| {
                if matches!(event, InputEvent::Blur | InputEvent::PressEnter { .. }) {
                    this.save_provider_form(window, cx);
                }
            }));
        }
        subscriptions.push(cx.subscribe_in(&model_filter, window, |_, _, event: &InputEvent, _, cx| {
            if matches!(event, InputEvent::Change) {
                cx.notify();
            }
        }));
        subscriptions.push(cx.subscribe_in(&custom_slug, window, |this, _, event: &InputEvent, window, cx| {
            if matches!(event, InputEvent::PressEnter { .. }) {
                this.add_custom_model(window, cx);
            }
        }));
        self.providers.form = Some(ProviderForm {
            id: instance.id,
            name,
            binary,
            home,
            auto_compact,
            args,
            accent,
            env,
            model_filter,
            custom_slug,
            adding_custom: false,
            custom_error: None,
            editing_custom: None,
            accent_open: false,
            auto_compact_error: false,
            _subscriptions: subscriptions,
        });
        cx.notify();
    }

    /// Rebuilds the open instance's fields from the saved settings.
    pub(super) fn reload_provider_form(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(id) = self.providers.form.as_ref().map(|form| form.id.clone()) {
            self.providers.form = None;
            self.select_provider(&id, window, cx);
        }
    }

    /// Saves the open instance's fields, if they changed.
    fn save_provider_form(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        let Some(form) = self.providers.form.as_mut() else { return };
        let value = |input: &Entity<InputState>, cx: &App| input.read(cx).value().trim().to_string();
        let auto_compact = value(&form.auto_compact, cx);
        form.auto_compact_error = !auto_compact.is_empty() && claude::auto_compact_window(&auto_compact).is_none();
        let accent = value(&form.accent, cx);
        let accent = if accent.is_empty() || accent_color(&accent).is_some() { accent } else { String::new() };
        let id = form.id.clone();
        let (name, binary, home, args) =
            (value(&form.name, cx), value(&form.binary, cx), value(&form.home, cx), value(&form.args, cx));
        // Sensitive values go to the secrets file, not the settings.
        let mut env = Vec::new();
        for field in &mut form.env {
            let key = value(&field.key, cx);
            let typed = field.value.read(cx).value().to_string();
            if field.sensitive {
                if key != field.saved_key {
                    settings::rename_secret(&id, &field.saved_key, &key);
                }
                if !typed.is_empty() {
                    settings::set_secret(&id, &key, Some(&typed));
                    field.value.update(cx, |input, cx| {
                        input.set_value("", window, cx);
                        input.set_placeholder("Stored secret, enter a new value to replace", window, cx);
                    });
                }
                field.saved_key = key.clone();
                env.push(EnvVar { key, value: String::new(), sensitive: true });
            } else {
                env.push(EnvVar { key, value: typed, sensitive: false });
            }
        }
        let unchanged = settings::get(|settings| {
            settings.instance(&id).is_some_and(|instance| {
                instance.name == name
                    && instance.binary == binary
                    && instance.home == home
                    && instance.args == args
                    && instance.env == env
                    && instance.auto_compact == auto_compact
                    && instance.accent == accent
            })
        });
        if unchanged {
            cx.notify();
            return;
        }
        settings::update(|settings| {
            if let Some(instance) = settings.providers.iter_mut().find(|instance| instance.id == id) {
                instance.name = name;
                instance.binary = binary;
                instance.home = home;
                instance.args = args;
                instance.env = env;
                instance.auto_compact = auto_compact;
                instance.accent = accent;
            }
        });
        Catalog::sync_instances(cx);
        self.changed(window, cx);
    }

    fn edit_instance(&mut self, id: &str, window: &mut Window, cx: &mut Context<Self>, change: impl FnOnce(&mut ProviderInstance)) {
        settings::update(|settings| {
            if let Some(instance) = settings.providers.iter_mut().find(|instance| instance.id == id) {
                change(instance);
            }
        });
        Catalog::sync_instances(cx);
        self.changed(window, cx);
    }

    fn remove_provider(&mut self, id: &str, window: &mut Window, cx: &mut Context<Self>) {
        let id = id.to_string();
        settings::update(|settings| settings.providers.retain(|instance| instance.id != id));
        Catalog::sync_instances(cx);
        self.providers.form = None;
        if let Some(first) = settings::instances().first() {
            self.select_provider(&first.id.clone(), window, cx);
        }
        self.changed(window, cx);
    }

    fn add_env_var(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        let Some(id) = self.providers.form.as_ref().map(|form| form.id.clone()) else { return };
        self.save_provider_form(window, cx);
        self.edit_instance(&id, window, cx, |instance| instance.env.push(EnvVar::default()));
        self.select_provider(&id, window, cx);
    }

    fn remove_env_var(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
        let Some(id) = self.providers.form.as_ref().map(|form| form.id.clone()) else { return };
        self.save_provider_form(window, cx);
        self.edit_instance(&id, window, cx, |instance| {
            if ix < instance.env.len() {
                let var = instance.env.remove(ix);
                if var.sensitive {
                    settings::set_secret(&instance.id, &var.key, None);
                }
            }
        });
        self.select_provider(&id, window, cx);
    }

    /// Flips a variable between plain and sensitive. A value turning
    /// sensitive moves to the secrets file; a secret turning plain is
    /// forgotten, to be typed in again.
    fn toggle_sensitive(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
        let Some(form) = self.providers.form.as_mut() else { return };
        let Some(field) = form.env.get_mut(ix) else { return };
        let id = form.id.clone();
        let key = field.key.read(cx).value().trim().to_string();
        let typed = field.value.read(cx).value().to_string();
        let sensitive = !field.sensitive;
        self.edit_instance(&id, window, cx, |instance| {
            if let Some(var) = instance.env.get_mut(ix) {
                var.key = key.clone();
                var.sensitive = sensitive;
                if sensitive {
                    if !typed.is_empty() {
                        settings::set_secret(&instance.id, &key, Some(&typed));
                    }
                    var.value.clear();
                } else {
                    settings::set_secret(&instance.id, &key, None);
                    var.value.clear();
                }
            }
        });
        self.select_provider(&id, window, cx);
    }

    /// Runs the agent's own updater in the background, then checks it again.
    fn update_provider(&mut self, id: &str, window: &mut Window, cx: &mut Context<Self>) {
        let Some(config) = settings::get(|settings| settings.instance(id).cloned()) else { return };
        let Some((mut command, description)) = models::update_command(&config) else { return };
        self.providers.updating.push(id.to_string());
        cx.notify();
        let task = cx.background_executor().spawn(async move {
            command.stdin(Stdio::null()).output().map_err(|err| err.to_string()).and_then(|output| {
                if output.status.success() {
                    Ok(())
                } else {
                    let stderr = String::from_utf8_lossy(&output.stderr);
                    Err(stderr.lines().rev().find(|line| !line.trim().is_empty()).unwrap_or("failed").trim().to_string())
                }
            })
        });
        let id = id.to_string();
        cx.spawn_in(window, async move |this, cx| {
            let result = task.await;
            let _ = this.update_in(cx, |this, window, cx| {
                this.providers.updating.retain(|updating| *updating != id);
                let note = match result {
                    Ok(()) => Notification::success(format!("Updated with {description}.")),
                    Err(err) => Notification::error(format!("{description} failed: {err}")),
                };
                window.push_notification(note, cx);
                Catalog::refresh(&id, cx);
                cx.notify();
            });
        })
        .detach();
    }

    // ---- models --------------------------------------------------------

    fn add_custom_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        let Some(form) = self.providers.form.as_mut() else { return };
        let slug = form.custom_slug.read(cx).value().trim().to_string();
        let id = form.id.clone();
        let error = if slug.is_empty() {
            Some("Enter a model slug.")
        } else if Catalog::global(cx).instance(&id).is_some_and(|state| {
            state.models.iter().any(|model| model.id == slug && !state.is_custom(model))
        }) {
            Some("That model is already built in.")
        } else if settings::get(|settings| {
            settings.instance(&id).is_some_and(|instance| instance.custom_models.iter().any(|custom| custom.slug == slug))
        }) {
            Some("That custom model is already saved.")
        } else {
            None
        };
        if let Some(error) = error {
            form.custom_error = Some(error.into());
            cx.notify();
            return;
        }
        form.adding_custom = false;
        form.custom_error = None;
        form.custom_slug.update(cx, |input, cx| input.set_value("", window, cx));
        self.edit_instance(&id, window, cx, |instance| instance.custom_models.push(CustomModel { slug, name: String::new() }));
    }

    /// Opens the display name field under a custom model.
    fn edit_custom_model(&mut self, slug: String, window: &mut Window, cx: &mut Context<Self>) {
        let Some(id) = self.providers.form.as_ref().map(|form| form.id.clone()) else { return };
        let name = settings::get(|settings| {
            settings
                .instance(&id)
                .and_then(|instance| instance.custom_models.iter().find(|custom| custom.slug == slug))
                .map(|custom| custom.name.clone())
        })
        .unwrap_or_default();
        let input = self.instance_input(&name, &slug, window, cx);
        let subscription = cx.subscribe_in(&input, window, |this, _, event: &InputEvent, window, cx| {
            if matches!(event, InputEvent::PressEnter { .. }) {
                this.save_custom_name(window, cx);
            }
        });
        focus_later(&input, window);
        if let Some(form) = self.providers.form.as_mut() {
            form.editing_custom = Some((slug, input));
            form._subscriptions.push(subscription);
        }
        cx.notify();
    }

    fn save_custom_name(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        let Some(form) = self.providers.form.as_mut() else { return };
        let Some((slug, input)) = form.editing_custom.take() else { return };
        let name = input.read(cx).value().trim().to_string();
        let id = form.id.clone();
        self.edit_instance(&id, window, cx, |instance| {
            if let Some(custom) = instance.custom_models.iter_mut().find(|custom| custom.slug == slug) {
                custom.name = name;
            }
        });
    }

    fn remove_custom_model(&mut self, slug: String, window: &mut Window, cx: &mut Context<Self>) {
        let Some(id) = self.providers.form.as_ref().map(|form| form.id.clone()) else { return };
        self.edit_instance(&id, window, cx, |instance| instance.custom_models.retain(|custom| custom.slug != slug));
    }

    /// Moves a model up (-1) or down (+1) among its instance's models.
    fn move_model(&mut self, keys: Vec<String>, ix: usize, by: isize, window: &mut Window, cx: &mut Context<Self>) {
        let target = ix as isize + by;
        if target < 0 || target as usize >= keys.len() {
            return;
        }
        let mut keys = keys;
        keys.swap(ix, target as usize);
        settings::update(|settings| {
            settings.model_order.retain(|key| !keys.contains(key));
            settings.model_order.extend(keys);
        });
        Catalog::rearrange(cx);
        self.changed(window, cx);
    }

    fn set_hidden(&mut self, keys: Vec<String>, hidden: bool, window: &mut Window, cx: &mut Context<Self>) {
        settings::update(|settings| {
            settings.hidden_models.retain(|key| !keys.contains(key));
            if hidden {
                settings.hidden_models.extend(keys);
            }
        });
        self.changed(window, cx);
    }

    // ---- page ----------------------------------------------------------

    pub(super) fn render_providers(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
        let p = metal::palette(cx);
        let catalog = Catalog::global(cx);
        let instances: Vec<InstanceState> = catalog.instances.clone();
        let selected = self
            .providers
            .form
            .as_ref()
            .map(|form| form.id.clone())
            .filter(|id| instances.iter().any(|state| state.id() == id))
            .or_else(|| instances.first().map(|state| state.id().to_string()));
        let checked = instances.iter().filter_map(|state| state.checked).max();
        let checking = instances.iter().any(|state| state.checking);
        let view = cx.weak_entity();

        // Below 768px the list sits over the editor, as in T3 Code.
        let two_panes = content_width(window, WIDE_WIDTH) >= 768.;
        let list = v_flex()
            .map(|this| if two_panes { this.w(px(LIST_WIDTH)).self_stretch().rounded_l(px(18.)) } else { this.w_full().rounded_t(px(18.)) })
            .flex_none()
            .region(p)
            .children(instances.iter().map(|state| {
                let id = state.id().to_string();
                let is_selected = selected.as_deref() == Some(id.as_str());
                let enabled = state.config.enabled;
                let (led, headline) = status_headline(state);
                let (toggle_id, select_id) = (id.clone(), id.clone());
                let toggle_view = view.clone();
                h_flex()
                    .id(SharedString::from(format!("provider-{id}")))
                    .min_h(px(72.))
                    .px(px(16.))
                    .py(px(12.))
                    .gap(px(12.))
                    .cursor_pointer()
                    .when(!enabled, |this| this.opacity(0.6))
                    .map(|this| if is_selected { this.raised(p) } else { this.hover(|style| style.bg(p.row_hover)) })
                    .on_click(cx.listener(move |this, _, window, cx| this.select_provider(&select_id, window, cx)))
                    .child(instance_icon(state.kind(), &state.config.accent, 20., p))
                    .child(
                        v_flex()
                            .flex_1()
                            .min_w_0()
                            .gap(px(3.))
                            .child(
                                h_flex()
                                    .gap(px(6.))
                                    .child(div().truncate().text_size(px(14.)).font_weight(FontWeight::MEDIUM).text_color(p.ink).child(state.label()))
                                    .when_some(state.version.clone(), |this, version| this.child(code(version, p).max_w(px(96.)).truncate()))
                                    .when(state.update_available(), |this| {
                                        this.child(Icon::new(IconName::ArrowUp).size(px(12.)).text_color(p.amber_ink))
                                    }),
                            )
                            .child(
                                h_flex()
                                    .gap(px(6.))
                                    .child(metal::led(SharedString::from(format!("provider-led-{id}")), led, state.checking, p))
                                    .child(div().truncate().text_size(px(12.)).text_color(p.ink3).child(headline)),
                            ),
                    )
                    .child(metal::toggle(SharedString::from(format!("provider-enabled-{id}")), enabled, p, move |on, window, cx| {
                        let id = toggle_id.clone();
                        let _ = toggle_view.update(cx, |this, cx| this.edit_instance(&id, window, cx, |instance| instance.enabled = on));
                    }))
                    .into_any_element()
            }))
            .child(
                h_flex()
                    .id("add-provider-row")
                    .min_h(px(48.))
                    .px(px(16.))
                    .gap(px(8.))
                    .cursor_pointer()
                    .text_size(px(13.5))
                    .text_color(p.ink2)
                    .hover(|style| style.bg(p.row_hover).text_color(p.ink))
                    .on_click(cx.listener(|this, _, window, cx| this.open_wizard(window, cx)))
                    .child(Icon::new(Lucide::Plus).size(px(14.)))
                    .child("Add provider"),
            );

        let detail = match (selected.as_deref().and_then(|id| catalog.instance(id)).cloned(), &self.providers.form) {
            (Some(state), Some(form)) if form.id == state.id() => self.render_provider_editor(&state, window, cx),
            (Some(state), _) => {
                // The form is built on first open; until then show nothing.
                let id = state.id().to_string();
                let view = view.clone();
                window.defer(cx, move |window, cx| {
                    let _ = view.update(cx, |this, cx| this.select_provider(&id, window, cx));
                });
                div().into_any_element()
            }
            (None, _) => div().p(px(18.)).text_size(px(13.)).text_color(p.ink3).child("No providers.").into_any_element(),
        };

        let refresh = MetalButton::new("refresh-providers")
            .ghost()
            .compact()
            .icon(IconName::RefreshCw)
            .label(match (checking, checked) {
                (true, _) => "Refreshing providers".to_string(),
                (false, Some(checked)) => format!("Checked {}", ago(checked)),
                (false, None) => "Check now".to_string(),
            })
            .disabled(checking)
            .tooltip("Refresh provider status")
            .on_click(|_, _, cx| Catalog::refresh_all(cx));
        let providers = Section::new("Providers")
            .id("providers")
            .title_action(
                MetalButton::new("add-provider-header")
                    .ghost()
                    .mini()
                    .icon(Lucide::Plus)
                    .tooltip("Add provider")
                    .on_click(cx.listener(|this, _, window, cx| this.open_wizard(window, cx))),
            )
            .header_action(refresh)
            .row(
                div()
                    .flex()
                    .map(|this| if two_panes { this.flex_row().items_start().min_h(px(512.)) } else { this.flex_col() })
                    .rounded(px(18.))
                    .raised(p)
                    .child(list)
                    .child(if two_panes { div().w(px(1.)).self_stretch().bg(p.rule) } else { div().h(px(1.)).w_full().bg(p.rule) })
                    .child(div().flex_1().min_w_0().child(detail)),
            )
            .plain()
            .render(cx);

        let health = settings::get(|settings| settings.health_check_secs);
        let advanced = Section::new("Advanced")
            .row(
                SettingRow::new("Health check interval")
                    .id("provider-health-check-interval")
                    .page_width(WIDE_WIDTH)
                    .description("Refresh provider status, versions, and models in the background. Set to 0 to disable.")
                    .reset(reset(&view, |s| s.health_check_secs, |s, secs| s.health_check_secs = secs))
                    .control(stepper("health-interval", health, 30, (0, 86_400), "seconds", &view, p, |settings, secs| {
                        settings.health_check_secs = secs
                    })),
            )
            .render(cx);

        vec![providers, div().id("usage-providers").child(self.hubs.clone()).into_any_element(), advanced]
    }

    fn render_provider_editor(&self, state: &InstanceState, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
        let p = metal::palette(cx);
        let Some(form) = self.providers.form.as_ref() else { return div().into_any_element() };
        let id = state.id().to_string();
        let kind = state.kind();
        let config = state.config.clone();
        let default_instance = id == kind.key();
        let updating = self.providers.updating.contains(&id);
        // The editor's own width decides when its rows stack.
        let content = content_width(window, WIDE_WIDTH);
        let editor_width = if content >= 768. { content - LIST_WIDTH - 33. } else { content - 32. };
        let row = |title: &'static str| SettingRow::new(title).page_width(editor_width);
        let (_, headline) = status_headline(state);

        // ---- the instance's header ----
        let modified = {
            let mut fresh = ProviderInstance::new(kind, &id);
            fresh.enabled = config.enabled;
            fresh.custom_models = config.custom_models.clone();
            fresh != config
        };
        let (reset_id, remove_id, update_id) = (id.clone(), id.clone(), id.clone());
        let header_action = h_flex()
            .gap(px(4.))
            .when_some(state.version.clone(), |this, version| this.child(code(version, p)))
            .when(state.update_available(), |this| {
                let latest = state.latest.clone().unwrap_or_default();
                this.child(
                    MetalButton::new("update-provider")
                        .compact()
                        .icon(IconName::ArrowUp)
                        .label(if updating { "Updating".to_string() } else { format!("Install {latest}") })
                        .disabled(updating)
                        .tooltip("Update now")
                        .on_click(cx.listener(move |this, _, window, cx| this.update_provider(&update_id, window, cx))),
                )
            })
            .when(default_instance && modified, |this| {
                this.child(
                    MetalButton::new("reset-provider")
                        .ghost()
                        .mini()
                        .icon(Lucide::Undo2)
                        .tooltip(format!("Reset {} provider settings", state.label()))
                        .on_click(cx.listener(move |this, _, window, cx| {
                            this.edit_instance(&reset_id, window, cx, |instance| {
                                let mut fresh = ProviderInstance::new(instance.kind, &instance.id);
                                fresh.enabled = instance.enabled;
                                fresh.custom_models = std::mem::take(&mut instance.custom_models);
                                *instance = fresh;
                            });
                            let id = reset_id.clone();
                            this.select_provider(&id, window, cx);
                        })),
                )
            })
            .when(!default_instance, |this| {
                this.child(
                    MetalButton::new("remove-provider")
                        .ghost()
                        .mini()
                        .icon(Lucide::Trash)
                        .tooltip(format!("Delete instance {id}"))
                        .on_click(cx.listener(move |this, _, window, cx| this.remove_provider(&remove_id, window, cx))),
                )
            });

        // ---- display name and accent ----
        let accent = accent_color(&config.accent);
        let accent_button = div()
            .id("accent-swatch")
            .flex()
            .flex_none()
            .items_center()
            .justify_center()
            .size(px(28.))
            .rounded_full()
            .cursor_pointer()
            .map(|this| match accent {
                Some(color) => this.bg(color).shadow(p.raise_sm.clone()),
                None => this.well(p).child(Icon::new(Lucide::Plus).size(px(12.)).text_color(p.ink3)),
            })
            .tooltip(|window, cx| Tooltip::new("Accent color").build(window, cx))
            .on_click(cx.listener(|this, _, _, cx| {
                if let Some(form) = this.providers.form.as_mut() {
                    form.accent_open = !form.accent_open;
                }
                cx.notify();
            }));
        let accent_picker = form.accent_open.then(|| {
            let swatches = accent_choices(p).into_iter().enumerate().map(|(ix, color)| {
                let hex = hex_of(color);
                let chosen = config.accent.eq_ignore_ascii_case(&hex);
                let set_id = id.clone();
                div()
                    .id(("accent-choice", ix))
                    .size(px(22.))
                    .rounded_full()
                    .cursor_pointer()
                    .bg(color)
                    .shadow(if chosen {
                        vec![BoxShadow::new(px(0.), px(0.), p.ink).spread_radius(px(2.))]
                    } else {
                        p.raise_sm.clone()
                    })
                    .on_click(cx.listener(move |this, _, window, cx| {
                        let hex = hex.clone();
                        this.edit_instance(&set_id, window, cx, |instance| instance.accent = hex.clone());
                        if let Some(form) = this.providers.form.as_ref() {
                            form.accent.update(cx, |input, cx| input.set_value(hex, window, cx));
                        }
                    }))
            });
            let clear_id = id.clone();
            h_flex()
                .gap(px(8.))
                .flex_wrap()
                .children(swatches)
                .child(text_field(&form.accent, 96., window, cx))
                .child(MetalButton::new("clear-accent").ghost().compact().label("Clear color").on_click(cx.listener(
                    move |this, _, window, cx| {
                        this.edit_instance(&clear_id, window, cx, |instance| instance.accent.clear());
                        if let Some(form) = this.providers.form.as_ref() {
                            form.accent.update(cx, |input, cx| input.set_value("", window, cx));
                        }
                    },
                )))
        });
        let display = row("Display name")
            .status(match (&state.account, kind) {
                (Some(account), Provider::Claude) => match &state.plan {
                    Some(plan) => format!("Authenticated as {account} · {plan}"),
                    None => format!("Authenticated as {account}"),
                },
                _ => headline.clone(),
            })
            .control(h_flex().gap(px(8.)).child(accent_button).child(text_field(&form.name, 224., window, cx)))
            .when_some(accent_picker, |row, picker| row.below(picker));
        let identity = Section::new(state.label())
            .icon(instance_icon(kind, &config.accent, 16., p))
            .header_action(header_action)
            .row(display)
            .render(cx);

        // ---- setup ----
        let account = match (kind, &state.account, &state.status) {
            (_, _, ProbeStatus::Disabled) => "Turn the provider on to check its account.".to_string(),
            (Provider::Claude, Some(account), _) => format!("Signed in as {account}"),
            (Provider::Claude, None, _) => "Not signed in. Run claude in a terminal and log in, then refresh.".into(),
            (Provider::Pi, _, _) => "Pi signs in to model providers itself: run pi in a terminal and use /login.".into(),
            (Provider::Jcode, _, _) => "jcode signs in itself: run jcode login in a terminal.".into(),
        };
        let setup = Section::new("Setup").row(row("Account").description(account)).render(cx);

        // ---- runtime ----
        let found = claude::find_binary(&config);
        let binary_status = binary_status(found.as_deref(), &config);
        let mut runtime = Section::new("Runtime").row(
            row("Binary path")
                .description(match kind {
                    Provider::Claude => "Path to the Claude binary used by this instance.",
                    Provider::Pi => "Path to the Pi coding agent binary.",
                    Provider::Jcode => "Path to the jcode binary used by this instance.",
                })
                .status(binary_status)
                .control(text_field(&form.binary, 224., window, cx)),
        );
        runtime = runtime.row(
            row(home_title(kind))
                .description(match kind {
                    Provider::Claude => "Custom Claude home and config directory. Keeps .claude.json and .claude separate.",
                    Provider::Pi => "Custom Pi agent directory, so this instance keeps its own logins, settings and sessions.",
                    Provider::Jcode => "Custom jcode home, so this instance keeps its own logins and sessions.",
                })
                .control(text_field(&form.home, 224., window, cx)),
        );
        if kind == Provider::Claude {
            runtime = runtime.row(
                row("Auto-compact after")
                    .description("Compact after 100,000 to 1,000,000 tokens. Leave empty to use Claude's default.")
                    .when(form.auto_compact_error, |row| row.status("Enter a number from 100,000 to 1,000,000."))
                    .control(text_field(&form.auto_compact, 224., window, cx)),
            );
        }
        runtime = runtime.row(
            row("Launch arguments")
                .description(match kind {
                    Provider::Claude => "Additional CLI arguments passed on session start.",
                    Provider::Pi => "Additional CLI arguments passed to pi --mode rpc on session start.",
                    Provider::Jcode => "Additional CLI arguments passed to jcode ahead of api --stdio on session start.",
                })
                .control(text_field(&form.args, 224., window, cx)),
        );
        let runtime = runtime.render(cx);

        // ---- environment ----
        let mut environment = Section::new("Environment").row(
            row("Variables").description("API keys, base URLs, and other per-instance CLI settings.").control(
                MetalButton::new("add-env")
                    .compact()
                    .icon(Lucide::Plus)
                    .label("Add variable")
                    .on_click(cx.listener(|this, _, window, cx| this.add_env_var(window, cx))),
            ),
        );
        for (ix, field) in form.env.iter().enumerate() {
            let sensitive = field.sensitive;
            environment = environment.row(
                h_flex()
                    .px(px(16.))
                    .py(px(8.))
                    .gap(px(8.))
                    .child(text_field(&field.key, 180., window, cx))
                    .child(div().text_color(p.ink3).child("="))
                    .child(div().flex_1().min_w_0().child(text_field(&field.value, 0., window, cx).w_full()))
                    .child(
                        MetalButton::new(("env-sensitive", ix))
                            .ghost()
                            .mini()
                            .icon(if sensitive { Lucide::Lock } else { Lucide::LockOpen })
                            .tooltip(if sensitive { "Sensitive, stored separately" } else { "Plain text" })
                            .on_click(cx.listener(move |this, _, window, cx| this.toggle_sensitive(ix, window, cx))),
                    )
                    .child(
                        MetalButton::new(("remove-env", ix))
                            .ghost()
                            .mini()
                            .icon(Lucide::Trash)
                            .tooltip("Remove")
                            .on_click(cx.listener(move |this, _, window, cx| this.remove_env_var(ix, window, cx))),
                    ),
            );
        }
        let environment = v_flex()
            .gap(px(8.))
            .child(environment.render(cx))
            .child(note("Sensitive values are stored separately and never returned to the app.", p))
            .into_any_element();

        let models = self.render_models(state, window, cx);

        v_flex()
            .p(px(16.))
            .gap(px(24.))
            .child(identity)
            .child(setup)
            .child(runtime)
            .child(environment)
            .child(models)
            .into_any_element()
    }

    fn render_models(&self, state: &InstanceState, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
        let p = metal::palette(cx);
        let Some(form) = self.providers.form.as_ref() else { return div().into_any_element() };
        let models: Vec<ModelEntry> = state.models.clone();
        let keys: Vec<String> = models.iter().map(ModelEntry::key).collect();
        let favorite_keys: Vec<String> = settings::get(|settings| settings.favorites.clone());
        let is_favorite = |model: &ModelEntry| favorite_keys.contains(&model.key());
        let hidden: Vec<String> = settings::get(|settings| settings.hidden_models.clone());
        let filter = form.model_filter.read(cx).value().trim().to_lowercase();
        let favorites = models.iter().filter(|model| is_favorite(model)).count();
        let hidden_count = models.iter().filter(|model| hidden.contains(&model.key())).count();
        let shown = |model: &ModelEntry| {
            filter.is_empty() || model.name.to_lowercase().contains(&filter) || model.id.to_lowercase().contains(&filter)
        };

        let model_row = |model: &ModelEntry, ix: usize, cx: &mut Context<Self>| -> AnyElement {
            let key = model.key();
            let custom = state.is_custom(model);
            let favorite = is_favorite(model);
            let is_hidden = hidden.contains(&key);
            let mut capabilities = Vec::new();
            if model.fast_mode {
                capabilities.push("Fast mode");
            }
            if !model.efforts.is_empty() {
                capabilities.push("Thinking");
            }
            if model.legacy {
                capabilities.push("Legacy");
            }
            let star_model = model.clone();
            let (up_keys, down_keys, hide_key) = (keys.clone(), keys.clone(), key.clone());
            let remove_slug = model.id.clone();
            h_flex()
                .h(px(32.))
                .px(px(12.))
                .gap(px(8.))
                .rounded(px(10.))
                .hover(|style| style.bg(p.row_hover))
                .child(
                    MetalButton::new(SharedString::from(format!("favorite-{key}")))
                        .ghost()
                        .mini()
                        .icon(Icon::new(Lucide::Star).text_color(if favorite { p.amber } else { p.ink3 }))
                        .tooltip(if favorite { "Remove from favorites" } else { "Add to favorites" })
                        .on_click(move |_, _, cx| Catalog::toggle_favorite(&star_model, cx)),
                )
                .child(
                    h_flex()
                        .flex_1()
                        .min_w_0()
                        .gap(px(6.))
                        .child(div().truncate().text_size(px(13.)).text_color(if is_hidden { p.ink3 } else { p.ink }).child(model.name.clone()))
                        .when(model.name != model.id, |this| this.child(code(model.id.clone(), p).max_w(px(160.)).truncate()))
                        .when(custom, |this| this.child(metal::tag("custom", p))),
                )
                .child(div().text_size(px(11.5)).text_color(p.ink3).child(capabilities.join(" · ")))
                .child(
                    MetalButton::new(SharedString::from(format!("model-up-{key}")))
                        .ghost()
                        .mini()
                        .icon(IconName::ArrowUp)
                        .tooltip("Move up")
                        .disabled(ix == 0)
                        .on_click(cx.listener(move |this, _, window, cx| this.move_model(up_keys.clone(), ix, -1, window, cx))),
                )
                .child(
                    MetalButton::new(SharedString::from(format!("model-down-{key}")))
                        .ghost()
                        .mini()
                        .icon(IconName::ArrowDown)
                        .tooltip("Move down")
                        .disabled(ix + 1 >= keys.len())
                        .on_click(cx.listener(move |this, _, window, cx| this.move_model(down_keys.clone(), ix, 1, window, cx))),
                )
                .when(custom, |this| {
                    let edit_slug = model.id.clone();
                    this.child(
                        MetalButton::new(SharedString::from(format!("model-edit-{key}")))
                            .ghost()
                            .mini()
                            .icon(Lucide::Pencil)
                            .tooltip("Edit name")
                            .on_click(cx.listener(move |this, _, window, cx| this.edit_custom_model(edit_slug.clone(), window, cx))),
                    )
                    .child(
                        MetalButton::new(SharedString::from(format!("model-remove-{key}")))
                            .ghost()
                            .mini()
                            .icon(Lucide::Trash)
                            .tooltip("Remove custom model")
                            .on_click(cx.listener(move |this, _, window, cx| this.remove_custom_model(remove_slug.clone(), window, cx))),
                    )
                })
                .child(
                    div()
                        .id(SharedString::from(format!("model-visible-{key}")))
                        .when(custom, |this| this.opacity(0.5))
                        .tooltip(move |window, cx| {
                            Tooltip::new(if custom {
                                "Custom models are always shown in the picker"
                            } else if is_hidden {
                                "Hidden from picker"
                            } else {
                                "Shown in picker"
                            })
                            .build(window, cx)
                        })
                        .child(metal::toggle(SharedString::from(format!("model-toggle-{key}")), !is_hidden || custom, p, {
                            let view = cx.weak_entity();
                            move |on, window, cx| {
                                if custom {
                                    return;
                                }
                                let key = hide_key.clone();
                                let _ = view.update(cx, |this, cx| this.set_hidden(vec![key], !on, window, cx));
                            }
                        })),
                )
                .into_any_element()
        };

        let group = |title: &'static str, rows: Vec<AnyElement>| {
            (!rows.is_empty()).then(|| {
                v_flex()
                    .gap(px(2.))
                    .child(div().px(px(12.)).pt(px(8.)).pb(px(4.)).text_size(px(11.5)).font_weight(FontWeight::MEDIUM).text_color(p.ink3).child(title))
                    .children(rows)
            })
        };
        let mut favorite_rows = Vec::new();
        let mut all_rows = Vec::new();
        let mut hidden_rows = Vec::new();
        for (ix, model) in models.iter().enumerate() {
            if !shown(model) {
                continue;
            }
            let mut rows = vec![model_row(model, ix, cx)];
            if let Some((slug, input)) = &form.editing_custom
                && *slug == model.id
            {
                rows.push(
                    h_flex()
                        .px(px(12.))
                        .py(px(6.))
                        .gap(px(8.))
                        .child(div().text_size(px(12.5)).text_color(p.ink2).child("Display name"))
                        .child(text_field(input, 0., window, cx).flex_1())
                        .child(
                            MetalButton::new("custom-name-save")
                                .primary()
                                .compact()
                                .label("Save")
                                .on_click(cx.listener(|this, _, window, cx| this.save_custom_name(window, cx))),
                        )
                        .child(MetalButton::new("custom-name-cancel").ghost().compact().label("Cancel").on_click(cx.listener(
                            |this, _, _, cx| {
                                if let Some(form) = this.providers.form.as_mut() {
                                    form.editing_custom = None;
                                }
                                cx.notify();
                            },
                        )))
                        .into_any_element(),
                );
            }
            if hidden.contains(&model.key()) && !state.is_custom(model) {
                hidden_rows.extend(rows);
            } else if is_favorite(model) {
                favorite_rows.extend(rows);
            } else {
                all_rows.extend(rows);
            }
        }

        let all_keys: Vec<String> = models.iter().filter(|model| !state.is_custom(model)).map(ModelEntry::key).collect();
        let any_hidden = hidden_count > 0;
        let toolbar = h_flex()
            .px(px(12.))
            .py(px(10.))
            .gap(px(8.))
            .when(models.len() > 8, |this| this.child(search_field(&form.model_filter, 180., cx)))
            .child(
                MetalButton::new("models-visibility")
                    .ghost()
                    .compact()
                    .label(if any_hidden { "Enable all" } else { "Disable all" })
                    .disabled(models.is_empty())
                    .on_click(cx.listener(move |this, _, window, cx| this.set_hidden(all_keys.clone(), !any_hidden, window, cx))),
            )
            .child(
                div()
                    .flex_1()
                    .text_size(px(12.))
                    .text_color(p.ink3)
                    .child(format!("{} models · {favorites} favorites · {hidden_count} hidden", models.len())),
            )
            .child(
                MetalButton::new("add-custom-model")
                    .compact()
                    .icon(Lucide::Plus)
                    .label("Add custom model")
                    .disabled(form.adding_custom)
                    .on_click(cx.listener(|this, _, window, cx| {
                        if let Some(form) = this.providers.form.as_mut() {
                            form.adding_custom = true;
                            form.custom_error = None;
                            focus_later(&form.custom_slug, window);
                        }
                        cx.notify();
                    })),
            );
        let adding = form.adding_custom.then(|| {
            v_flex()
                .px(px(12.))
                .py(px(8.))
                .gap(px(6.))
                .child(
                    h_flex()
                        .gap(px(8.))
                        .child(text_field(&form.custom_slug, 0., window, cx).flex_1())
                        .child(
                            MetalButton::new("custom-model-add")
                                .primary()
                                .compact()
                                .label("Add")
                                .on_click(cx.listener(|this, _, window, cx| this.add_custom_model(window, cx))),
                        )
                        .child(MetalButton::new("custom-model-cancel").ghost().compact().label("Cancel").on_click(cx.listener(
                            |this, _, _, cx| {
                                if let Some(form) = this.providers.form.as_mut() {
                                    form.adding_custom = false;
                                    form.custom_error = None;
                                }
                                cx.notify();
                            },
                        ))),
                )
                .when_some(form.custom_error.clone(), |this, error| {
                    this.child(div().text_size(px(12.)).text_color(p.red).child(error))
                })
        });
        let body = v_flex()
            .pb(px(8.))
            .child(toolbar)
            .children(adding)
            .children(group("Favorites", favorite_rows))
            .children(group("All", all_rows))
            .children(group("Hidden from picker", hidden_rows))
            .when(models.is_empty(), |this| {
                this.child(div().px(px(12.)).py(px(10.)).text_size(px(12.5)).text_color(p.ink3).child(match &state.status {
                    ProbeStatus::Checking => "Asking for its models…",
                    ProbeStatus::Disabled => "Turn the provider on to list its models.",
                    _ => "No models reported.",
                }))
            });
        v_flex()
            .gap(px(8.))
            .child(Section::new("Models").row(body).render(cx))
            .child(note(
                "Favorites, visibility, and ordering are saved on this device. Custom models are saved with the instance.",
                p,
            ))
            .into_any_element()
    }

    // ---- the Add provider wizard ---------------------------------------

    fn open_wizard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        let input = |placeholder: &str, window: &mut Window, cx: &mut Context<Self>| {
            let placeholder = placeholder.to_string();
            cx.new(|cx| InputState::new(window, cx).placeholder(placeholder))
        };
        self.providers.wizard = Some(Wizard {
            step: 0,
            kind: Provider::Claude,
            label: input("e.g. Work", window, cx),
            id: input("claude_work", window, cx),
            accent: String::new(),
            binary: input("claude", window, cx),
            home: input("~/.claude", window, cx),
            args: input("", window, cx),
            error: None,
        });
        cx.notify();
    }

    fn wizard_next(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        let Some(wizard) = self.providers.wizard.as_mut() else { return };
        match wizard.step {
            0 => {
                let kind = wizard.kind;
                let placeholder = format!("{}_work", kind.key());
                wizard.id.update(cx, |input, cx| input.set_placeholder(placeholder, window, cx));
                wizard.binary.update(cx, |input, cx| input.set_placeholder(kind.key(), window, cx));
                wizard.home.update(cx, |input, cx| input.set_placeholder(home_placeholder(kind), window, cx));
                wizard.step = 1;
            }
            1 => {
                let id = wizard.id.read(cx).value().trim().to_string();
                let label = wizard.label.read(cx).value().trim().to_string();
                let id = if id.is_empty() && !label.is_empty() {
                    format!("{}_{}", wizard.kind.key(), label.to_lowercase().replace(|c: char| !c.is_ascii_alphanumeric(), "_"))
                } else {
                    id
                };
                wizard.error = if id.is_empty() {
                    Some("Enter an instance ID.".into())
                } else if !id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
                    Some("Instance IDs use letters, digits, '-', or '_'.".into())
                } else if settings::get(|settings| settings.instance(&id).is_some()) {
                    Some(format!("An instance called {id} already exists.").into())
                } else {
                    None
                };
                if wizard.error.is_none() {
                    wizard.id.update(cx, |input, cx| input.set_value(id, window, cx));
                    wizard.step = 2;
                }
            }
            _ => self.wizard_finish(window, cx),
        }
        cx.notify();
    }

    fn wizard_finish(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        let Some(wizard) = self.providers.wizard.take() else { return };
        let value = |input: &Entity<InputState>| input.read(cx).value().trim().to_string();
        let id = value(&wizard.id);
        let mut instance = ProviderInstance::new(wizard.kind, &id);
        instance.name = value(&wizard.label);
        instance.accent = wizard.accent.clone();
        instance.binary = value(&wizard.binary);
        instance.home = value(&wizard.home);
        instance.args = value(&wizard.args);
        settings::update(|settings| settings.providers.push(instance));
        Catalog::sync_instances(cx);
        window.push_notification(Notification::success("Provider instance added"), cx);
        self.select_provider(&id, window, cx);
        self.changed(window, cx);
    }

    /// The wizard, floating over the settings.
    pub(super) fn render_add_provider(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<AnyElement> {
        let wizard = self.providers.wizard.as_ref()?;
        let p = metal::palette(cx);
        let steps = ["Provider", "Identity", "Config"];
        let field = |title: &'static str, hint: &'static str, input: &Entity<InputState>, window: &mut Window, cx: &mut Context<Self>| {
            v_flex()
                .gap(px(6.))
                .child(div().text_size(px(13.)).font_weight(FontWeight::MEDIUM).text_color(p.ink).child(title))
                .child(text_field(input, 0., window, cx).w_full())
                .when(!hint.is_empty(), |this| this.child(div().text_size(px(12.)).text_color(p.ink3).child(hint)))
        };
        let body: AnyElement = match wizard.step {
            0 => div()
                .grid()
                .grid_cols(2)
                .gap(px(8.))
                .children(Provider::ALL.into_iter().map(|kind| {
                    let selected = kind == wizard.kind;
                    h_flex()
                        .id(SharedString::from(format!("wizard-kind-{}", kind.key())))
                        .h(px(56.))
                        .px(px(14.))
                        .gap(px(10.))
                        .rounded(px(14.))
                        .cursor_pointer()
                        .map(|this| if selected { this.raised(p) } else { this.well(p) })
                        .on_click(cx.listener(move |this, _, _, cx| {
                            if let Some(wizard) = this.providers.wizard.as_mut() {
                                wizard.kind = kind;
                            }
                            cx.notify();
                        }))
                        .child(super::radio(selected, p))
                        .child(pickers::provider_icon(kind, px(18.), p))
                        .child(div().text_size(px(13.5)).font_weight(FontWeight::MEDIUM).text_color(p.ink).child(kind.label()))
                }))
                .into_any_element(),
            1 => {
                let swatches = accent_choices(p).into_iter().enumerate().map(|(ix, color)| {
                    let hex = hex_of(color);
                    let chosen = wizard.accent == hex;
                    div()
                        .id(("wizard-accent", ix))
                        .size(px(22.))
                        .rounded_full()
                        .cursor_pointer()
                        .bg(color)
                        .shadow(if chosen { vec![BoxShadow::new(px(0.), px(0.), p.ink).spread_radius(px(2.))] } else { p.raise_sm.clone() })
                        .on_click(cx.listener(move |this, _, _, cx| {
                            if let Some(wizard) = this.providers.wizard.as_mut() {
                                wizard.accent = if wizard.accent == hex { String::new() } else { hex.clone() };
                            }
                            cx.notify();
                        }))
                }).collect::<Vec<_>>();
                v_flex()
                    .gap(px(14.))
                    .child(field("Label", "Shown in the provider list.", &wizard.label, window, cx))
                    .child(field("Instance ID", "Letters, digits, '-', or '_'.", &wizard.id, window, cx))
                    .child(
                        v_flex()
                            .gap(px(6.))
                            .child(div().text_size(px(13.)).font_weight(FontWeight::MEDIUM).text_color(p.ink).child("Accent color"))
                            .child(h_flex().gap(px(8.)).children(swatches))
                            .child(div().text_size(px(12.)).text_color(p.ink3).child("Optional marker shown in the picker.")),
                    )
                    .when_some(wizard.error.clone(), |this, error| {
                        this.child(
                            div().px(px(12.)).py(px(8.)).rounded(px(12.)).bg(p.red.opacity(0.08)).text_size(px(12.5)).text_color(p.red).child(error),
                        )
                    })
                    .into_any_element()
            }
            _ => v_flex()
                .gap(px(14.))
                .child(field("Binary path", "Leave empty to look it up on PATH.", &wizard.binary, window, cx))
                .child(field(home_title(wizard.kind), "Leave empty for the default.", &wizard.home, window, cx))
                .child(field("Launch arguments", "Additional CLI arguments passed on session start.", &wizard.args, window, cx))
                .into_any_element(),
        };
        let step = wizard.step;
        let dialog = v_flex()
            .id("add-provider-dialog")
            .w(px(520.))
            .p(px(20.))
            .gap(px(16.))
            .rounded(px(22.))
            .plate(p)
            .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
            .child(
                v_flex()
                    .gap(px(4.))
                    .child(div().text_size(px(16.)).font_weight(FontWeight::SEMIBOLD).text_color(p.ink).child("Add provider"))
                    .child(div().text_size(px(12.5)).text_color(p.ink3).child("Add an account or configure a provider on this machine.")),
            )
            .child(h_flex().gap(px(6.)).children(steps.iter().enumerate().map(|(ix, label)| {
                h_flex()
                    .gap(px(6.))
                    .child(
                        div()
                            .flex()
                            .items_center()
                            .justify_center()
                            .size(px(18.))
                            .rounded_full()
                            .text_size(px(10.5))
                            .font_weight(FontWeight::SEMIBOLD)
                            .map(|this| {
                                if ix <= step {
                                    this.bg(metal::vertical(p.primary)).text_color(p.primary_ink)
                                } else {
                                    this.well(p).text_color(p.ink3)
                                }
                            })
                            .child((ix + 1).to_string()),
                    )
                    .child(div().text_size(px(12.5)).text_color(if ix == step { p.ink } else { p.ink3 }).child(*label))
                    .when(ix + 1 < steps.len(), |this| this.child(div().w(px(20.)).h(px(1.)).bg(p.rule)))
            })))
            .child(body)
            .child(
                h_flex()
                    .gap(px(8.))
                    .child(div().flex_1())
                    .child(
                        MetalButton::new("wizard-back")
                            .ghost()
                            .compact()
                            .label(if step == 0 { "Cancel" } else { "Back" })
                            .on_click(cx.listener(|this, _, _, cx| {
                                match this.providers.wizard.as_mut() {
                                    Some(wizard) if wizard.step > 0 => {
                                        wizard.step -= 1;
                                        wizard.error = None;
                                    }
                                    _ => this.providers.wizard = None,
                                }
                                cx.notify();
                            })),
                    )
                    .child(
                        MetalButton::new("wizard-next")
                            .primary()
                            .compact()
                            .label(if step == 2 { "Add instance" } else { "Next" })
                            .on_click(cx.listener(|this, _, window, cx| this.wizard_next(window, cx))),
                    ),
            );
        Some(
            div()
                .absolute()
                .inset_0()
                .flex()
                .items_center()
                .justify_center()
                .bg(black().opacity(if p.dark { 0.4 } else { 0.16 }))
                .occlude()
                .on_mouse_down(
                    MouseButton::Left,
                    cx.listener(|this, _, _, cx| {
                        this.providers.wizard = None;
                        cx.notify();
                    }),
                )
                .child(dialog)
                .into_any_element(),
        )
    }
}

/// Where a binary was found, and what overrides the field.
fn binary_status(found: Option<&Path>, instance: &ProviderInstance) -> String {
    let found_text = match found {
        Some(path) if claude::is_executable(path) => format!("Using {}.", display_path(path)),
        Some(path) => format!("{} isn't an executable file.", display_path(path)),
        None => "Not found on PATH or in the usual install folders.".to_string(),
    };
    let var = claude::binary_var(instance.kind);
    if instance.id == instance.kind.key() && std::env::var_os(var).is_some() {
        format!("{found_text} Set by {var}, which wins over this field.")
    } else {
        found_text
    }
}