//! The right panel, as T3 Code has it: it shows one surface at a time (the //! subagents, the browser, a device or a terminal), and opens on "Open a surface", a //! short list to pick one from, each with a key of its own. use gpui_kit::component::{Icon, IconName, h_flex, v_flex}; use gpui_kit::prelude::FluentBuilder as _; use gpui_kit::*; use super::Workspace; use crate::ui::icons::Lucide; use crate::ui::metal::{self, MetalStyled as _}; /// What the right panel shows. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(super) enum Surface { /// "Open a surface": the list of the others. Picker, Subagents, Browser, Device, Terminal, } /// The surfaces the picker lists: each with its icon, name and key. const SURFACES: [(Surface, &str, &str); 4] = [ (Surface::Subagents, "Subagents", "S"), (Surface::Browser, "Browser", "B"), (Surface::Device, "Device", "D"), (Surface::Terminal, "Terminal", "T"), ]; /// How wide a terminal in the right panel is. pub(super) const TERMINAL_PANEL_WIDTH: f32 = 520.; impl Workspace { /// What the open thread's right panel shows. pub(super) fn right_panel(&self) -> Option { match &self.active { Some(session) => self.right_panels.get(&session.entity_id()).copied(), None => self.idle_right_panel, } } /// Sets what the open thread's right panel shows. Other threads keep theirs. pub(super) fn set_right_panel(&mut self, surface: Option) { match self.active.as_ref().map(|session| session.entity_id()) { Some(id) => self.set_thread_surface(id, surface), None => self.idle_right_panel = surface, } } /// What a thread's right panel shows, open or not. pub(super) fn thread_surface(&self, thread: EntityId) -> Option { self.right_panels.get(&thread).copied() } /// Sets what a thread's right panel shows, open or not. pub(super) fn set_thread_surface(&mut self, thread: EntityId, surface: Option) { match surface { Some(surface) => { self.right_panels.insert(thread, surface); } None => { self.right_panels.remove(&thread); } } } pub(super) fn surface_open(&self, surface: Surface) -> bool { self.right_panel() == Some(surface) } /// The right panel's button: opens "Open a surface", or closes the panel. pub(super) fn toggle_right_panel(&mut self, window: &mut Window, cx: &mut Context) { if self.right_panel().is_some() { self.set_right_panel(None); } else { self.set_right_panel(Some(Surface::Picker)); self.surface_focus.focus(window, cx); } cx.notify(); } /// Shows a surface in the right panel, or closes the panel if it's /// showing it already. pub(super) fn toggle_surface(&mut self, surface: Surface, window: &mut Window, cx: &mut Context) { if self.right_panel() == Some(surface) { self.set_right_panel(None); cx.notify(); return; } self.open_surface(surface, window, cx); } pub(super) fn open_surface(&mut self, surface: Surface, window: &mut Window, cx: &mut Context) { if surface == Surface::Terminal { if !self.terminal_available(cx) { return; } // The terminals move over from the drawer, if it's open. self.dock_terminal(window, cx); } self.set_right_panel(Some(surface)); if surface == Surface::Picker { self.surface_focus.focus(window, cx); } cx.notify(); } /// The picker's keys: S, B, D and T open their surfaces; Escape closes it. fn surface_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context) { let keystroke = &event.keystroke; if keystroke.modifiers.modified() { return; } if keystroke.key == "escape" { self.set_right_panel(None); cx.notify(); return; } if let Some((surface, _, _)) = SURFACES.iter().find(|(_, _, key)| key.eq_ignore_ascii_case(&keystroke.key)) { cx.stop_propagation(); self.open_surface(*surface, window, cx); } } pub(super) fn render_surface_picker(&self, cx: &mut Context) -> impl IntoElement { let p = metal::palette(cx); let terminal = self.terminal_available(cx); let rows = SURFACES.iter().map(|(surface, name, key)| { let surface = *surface; let enabled = surface != Surface::Terminal || terminal; let icon = match surface { Surface::Subagents => Icon::new(Lucide::Bot), Surface::Browser => Icon::new(IconName::Globe), Surface::Device => Icon::new(Lucide::Smartphone), _ => Icon::new(Lucide::SquareTerminal), }; h_flex() .id(SharedString::from(format!("surface-{name}"))) .h(px(31.)) .px(px(10.)) .gap(px(10.)) .rounded(px(8.)) .text_size(px(13.5)) .text_color(if enabled { p.ink } else { p.ink3 }) .when(enabled, |this| { this.cursor_pointer() .hover(|style| style.bg(p.row_hover)) .on_click(cx.listener(move |this, _, window, cx| this.open_surface(surface, window, cx))) }) .child(icon.size(px(16.)).text_color(if enabled { p.ink2 } else { p.ink3 })) .child(div().flex_1().child(*name)) .child( div() .flex_none() .min_w(px(18.)) .h(px(18.)) .px(px(5.)) .flex() .items_center() .justify_center() .rounded(px(4.)) .bg(p.row_hover) .font_family(metal::MONO) .text_size(px(10.5)) .text_color(p.ink3) .child(*key), ) }); v_flex() .id("surface-picker") .key_context("SurfacePicker") .track_focus(&self.surface_focus) .on_key_down(cx.listener(Self::surface_key)) .w(px(super::PANEL_WIDTH)) .flex_none() .h_full() .p(px(12.)) .pl(px(0.)) .child( v_flex() .flex_1() .justify_center() .px(px(16.)) .rounded(px(18.)) .raised(p) .child( div() .pb(px(14.)) .text_center() .text_size(px(14.)) .font_weight(FontWeight::MEDIUM) .child("Open a surface"), ) .child(v_flex().gap(px(2.)).children(rows)), ) } }