Vectrum
Sign In
270 lines · 12.3 KB · Rust
//! The Integrations page: what T3 Code's has that this app supports, the
//! agents' access to the shared browser, devices, plus the editor "Open in" uses.

use gpui_kit::component::h_flex;
use gpui_kit::component::menu::{DropdownMenu as _, PopupMenuItem};
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;

use super::SettingsView;
use super::widgets::{Section, SettingRow, reset, switch};
use crate::browser;
use crate::device::{self, HostStatus, Platform, ToolVersion, toolchain::Tool};
use crate::ui::device::{AGENT_DESCRIPTION, DeviceWatch, HUB_DESCRIPTION, agent_status, background, hub_status, platform_line};
use crate::ui::icons::Lucide;
use crate::ui::metal::{self, MetalButton};

impl SettingsView {
    pub(super) fn render_integrations(&mut self, cx: &mut Context<Self>) -> Vec<AnyElement> {
        let p = metal::palette(cx);
        let view = cx.weak_entity();
        let on = super::scope::shown_settings(cx).agent_browser;
        let installed = browser::find_binary().is_some();

        let browser = Section::new("Browser")
            .id("browser-access")
            .row(
                SettingRow::new("Agent browser access")
                    .id("agent-browser-access")
                    .description("Allow agents to use the shared browser. Applies when the agent session next starts.")
                    .when(!installed, |row| {
                        row.status("agent-browser isn't installed: npm install -g agent-browser && agent-browser install")
                    })
                    .dimmed(!installed)
                    .reset(reset(&view, cx, |s| s.agent_browser, |s, on| s.agent_browser = on))
                    .control(switch("agent-browser", on, &view, p, |settings, on| settings.agent_browser = on)),
            )
            .row(
                SettingRow::new("Auto-show floating preview")
                    .id("browser-auto-show-floating-preview")
                    .description("Show the floating preview when an agent opens a device unless the agent says otherwise.")
                    .reset(reset(&view, cx, |s| s.auto_float_preview, |s, on| s.auto_float_preview = on))
                    .control(switch("auto-float-preview", super::scope::shown_settings(cx).auto_float_preview, &view, p, |settings, on| {
                        settings.auto_float_preview = on
                    })),
            )
            .render(cx);
        let devices = self.render_devices(cx);

        let editor = Section::new("Editor")
            .row(
                SettingRow::new("Open in")
                    .id("open-in")
                    .description("The editor the project panel opens projects in.")
                    .control(self.editor_control(cx)),
            )
            .render(cx);
        vec![browser, devices, editor]
    }

    /// Settings → Integrations → Devices: the device hub, simulator support
    /// and agent access, each with its tool's versions.
    fn render_devices(&mut self, cx: &mut Context<Self>) -> AnyElement {
        let p = metal::palette(cx);
        let state = DeviceWatch::state(cx).clone();
        let enabled = state.host_status != HostStatus::Disabled;
        let busy = state.busy();
        let pending = self.device_pending;
        let local = state.local().cloned();
        let blocked = busy || pending.is_some();

        let hub_toggle = metal::toggle("device-hub", enabled, p, {
            let view = cx.weak_entity();
            move |on, _, cx| {
                let _ = view.update(cx, |this, cx| {
                    if this.device_pending.is_none() && !DeviceWatch::state(cx).busy() {
                        this.configure_devices("hub", Some(on), (!on).then_some(false), cx);
                    }
                });
            }
        })
        .when(blocked, |this| this.opacity(0.5));
        let agent_toggle = metal::toggle("agent-device-access", state.agent_access, p, {
            let view = cx.weak_entity();
            move |on, _, cx| {
                let _ = view.update(cx, |this, cx| {
                    let state = DeviceWatch::state(cx);
                    if this.device_pending.is_none() && !state.busy() && state.host_status != HostStatus::Disabled {
                        this.configure_devices("agent", None, Some(on), cx);
                    }
                });
            }
        })
        .when(blocked || !enabled, |this| this.opacity(0.5));

        let hub_versions = local.as_ref().map(|host| self.versions_button(Tool::Hub, &host.hub, cx));
        let agent_versions = local.as_ref().map(|host| self.versions_button(Tool::Agent, &host.agent, cx));
        let mut section = Section::new("Devices").id("devices").row(
            SettingRow::new("Device hub")
                .id("device-hub")
                .description(HUB_DESCRIPTION)
                .control(
                    h_flex()
                        .gap(px(10.))
                        .children(hub_versions)
                        .children(if pending == Some("hub") { hub_status(&state, true, true, p) } else { None })
                        .child(hub_toggle),
                ),
        );
        if enabled {
            section = section.row(
                SettingRow::new("Simulator support")
                    .id("device-platform-support")
                    .status_element(
                        h_flex()
                            .flex_wrap()
                            .gap(px(20.))
                            .child(platform_line(&state, Platform::Ios, true, p))
                            .child(platform_line(&state, Platform::Android, true, p)),
                    )
                    .control(
                        MetalButton::new("device-platform-refresh")
                            .compact()
                            .label(if pending == Some("check") { "Checking…" } else { "Refresh" })
                            .disabled(blocked)
                            .on_click(cx.listener(|this, _, _, cx| {
                                this.device_pending = Some("check");
                                cx.notify();
                                background(cx, || device::service().list(), |this, _, cx| {
                                    this.device_pending = None;
                                    cx.notify();
                                });
                            })),
                    ),
            );
        }
        section = section.row(
            SettingRow::new("Agent device access")
                .id("agent-device-access")
                .description(AGENT_DESCRIPTION)
                .control(
                    h_flex()
                        .gap(px(10.))
                        .children(agent_versions)
                        .children(if pending == Some("agent") { agent_status(&state, true, true, p) } else { None })
                        .child(agent_toggle),
                ),
        );
        if let Some(error) = self.device_error.clone() {
            section = section.row(div().px(px(16.)).py(px(8.)).text_size(px(11.5)).text_color(p.red).child(error));
        } else if state.host_status == HostStatus::Failed
            && let Some(detail) = state.host_detail.clone()
        {
            section = section.row(div().px(px(16.)).py(px(8.)).text_size(px(11.5)).text_color(p.red).child(detail));
        }
        section.render(cx)
    }

    fn configure_devices(&mut self, kind: &'static str, enabled: Option<bool>, agent: Option<bool>, cx: &mut Context<Self>) {
        self.device_pending = Some(kind);
        self.device_error = None;
        cx.notify();
        background(
            cx,
            move || device::service().configure(enabled, agent, None),
            |this, result, cx| {
                this.device_pending = None;
                if let Err(err) = result {
                    this.device_error = Some(format!("{err:#}").into());
                }
                cx.notify();
            },
        );
    }

    /// A tool's version, opening its details: running, required and
    /// installed versions, a check and an install of the pinned version.
    fn versions_button(&self, tool: Tool, versions: &ToolVersion, cx: &mut Context<Self>) -> AnyElement {
        let label = match versions.shown() {
            Some(version) => format!("v{version}"),
            None => "Not installed".into(),
        };
        let versions = versions.clone();
        let view = cx.weak_entity();
        let busy = self.device_pending.is_some();
        MetalButton::new(match tool {
            Tool::Hub => "device-hub-versions",
            Tool::Agent => "agent-device-versions",
        })
        .ghost()
        .compact()
        .label(label)
        .dropdown_menu_with_anchor(Anchor::TopRight, move |mut menu, _, _| {
            let installed = if versions.installed.is_empty() { "None".to_string() } else { versions.installed.join(", ") };
            menu = menu
                .min_w(px(240.))
                .label(match tool {
                    Tool::Hub => "Device hub",
                    Tool::Agent => "Agent device",
                })
                .item(PopupMenuItem::new(format!("Running: {}", versions.running.as_deref().unwrap_or("Not running"))).disabled(true))
                .item(PopupMenuItem::new(format!("Required: {}", versions.required)).disabled(true))
                .item(PopupMenuItem::new(format!("Installed: {installed}")).disabled(true))
                .separator();
            let check = view.clone();
            menu = menu.item(PopupMenuItem::new("Check versions").disabled(busy).on_click(move |_, _, cx| {
                let _ = check.update(cx, |this, cx| {
                    this.device_pending = Some("versions");
                    cx.notify();
                    background(cx, || device::service().inspect(), |this, _, cx| {
                        this.device_pending = None;
                        cx.notify();
                    });
                });
            }));
            if !versions.installed.contains(&versions.required) {
                let update = view.clone();
                menu = menu.item(PopupMenuItem::new(format!("Install {}", versions.required)).disabled(busy).on_click(move |_, _, cx| {
                    let _ = update.update(cx, |this, cx| {
                        this.device_pending = Some("update");
                        this.device_error = None;
                        cx.notify();
                        background(cx, move || device::service().update_tool(tool), |this, result, cx| {
                            this.device_pending = None;
                            if let Err(err) = result {
                                this.device_error = Some(format!("{err:#}").into());
                            }
                            cx.notify();
                        });
                    });
                }));
            }
            menu.separator().label("Tools update automatically on this machine when needed.")
        })
        .into_any_element()
    }

    fn editor_control(&self, cx: &mut Context<Self>) -> AnyElement {
        let Some(workspace) = self.workspace.upgrade() else {
            return div().into_any_element();
        };
        let (names, current) = {
            let workspace = workspace.read(cx);
            (workspace.editor_names(), workspace.editor())
        };
        let Some(label) = names.get(current).copied() else {
            let p = metal::palette(cx);
            return div().text_size(px(13.)).text_color(p.ink3).child("No editor found").into_any_element();
        };
        let weak = self.workspace.clone();
        let view = cx.weak_entity();
        MetalButton::new("default-editor")
            .compact()
            .icon(Lucide::Code)
            .label(label)
            .caret()
            .dropdown_menu_with_anchor(Anchor::TopRight, move |mut menu, _, _| {
                menu = menu.min_w(px(200.));
                for (ix, name) in names.iter().enumerate() {
                    let weak = weak.clone();
                    let view = view.clone();
                    menu = menu.item(PopupMenuItem::new(*name).checked(ix == current).on_click(move |_, _, cx| {
                        let _ = weak.update(cx, |workspace, cx| workspace.set_editor(ix, cx));
                        let _ = view.update(cx, |_, cx| cx.notify());
                    }));
                }
                menu
            })
            .into_any_element()
    }
}