//! 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) -> Vec { 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) -> 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, agent: Option, cx: &mut Context) { 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) -> 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) -> 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() } }