//! 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()
}
}