From c12b41855ffc417e8029f8dc5d7355e544f1cad6 Mon Sep 17 00:00:00 2001 From: Abhishek Pandya Date: Tue, 28 Apr 2026 23:05:23 -0400 Subject: [PATCH 1/3] Slash command menu sidecar. --- app/src/search/data_source.rs | 6 + app/src/search/item.rs | 27 ++ .../slash_commands/cloud_mode_v2_view.rs | 285 +++++++++++++++--- .../input/slash_commands/search_item.rs | 11 + 4 files changed, 295 insertions(+), 34 deletions(-) diff --git a/app/src/search/data_source.rs b/app/src/search/data_source.rs index 832c93e7..6c001759 100644 --- a/app/src/search/data_source.rs +++ b/app/src/search/data_source.rs @@ -458,6 +458,12 @@ impl QueryResult { self.item.accessibility_help_message() } + /// Forwards `SearchItem::detail_data` so callers can read sidecar + /// metadata without holding the underlying `Arc`. + pub fn detail_data(&self) -> Option { + self.item.detail_data() + } + /// Returns an optional deduplication key for this item from the [`SearchItem`]. pub fn dedup_key(&self) -> Option { self.item.dedup_key() diff --git a/app/src/search/item.rs b/app/src/search/item.rs index 3bdf3064..bafab768 100644 --- a/app/src/search/item.rs +++ b/app/src/search/item.rs @@ -1,11 +1,30 @@ use ordered_float::OrderedFloat; use warp_core::ui::theme::Fill; +use warpui::fonts::FamilyId; use warpui::{Action, AppContext, Element}; use crate::appearance::Appearance; use super::result_renderer::ItemHighlightState; +/// Compact, type-erased metadata used by detail/sidecar views (e.g. the +/// V2 cloud-mode slash command menu sidecar) to render an expanded preview +/// of a search item without re-running the row layout. Items that don't +/// have a meaningful title/description pair (e.g. action shortcuts, static +/// separators) should return `None` from `SearchItem::detail_data`. +#[derive(Clone)] +pub struct SearchItemDetail { + /// Display title shown at the top of the sidecar. + pub title: String, + /// Optional description body shown beneath the title. `None` for items + /// like saved prompts that only have a title. + pub description: Option, + /// Font family used to render the title. Typically matches the row's + /// title font (monospace for slash commands and skills, UI font for + /// saved prompts). + pub title_font_family: FamilyId, +} + /// Location where icon should be rendered relative to the [`SearchItem`]. pub enum IconLocation { /// Icon should be centered within the element. @@ -109,4 +128,12 @@ pub trait SearchItem: Send + Sync { fn tooltip(&self) -> Option { None } + + /// Optional structured metadata used by detail/sidecar views to render a + /// richer preview of this item. Default returns `None`; items that + /// participate in sidecar rendering (currently only the V2 cloud-mode + /// slash command menu) override this. + fn detail_data(&self) -> Option { + None + } } diff --git a/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs b/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs index 5db6654f..466d8c82 100644 --- a/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs +++ b/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs @@ -1,13 +1,15 @@ use std::collections::{HashMap, HashSet}; use std::sync::LazyLock; +use pathfinder_geometry::vector::vec2f; use warp_core::ui::appearance::Appearance; use warp_core::ui::theme::Fill; use warpui::elements::{ - Border, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, Container, CornerRadius, - CrossAxisAlignment, DispatchEventResult, DropShadow, EventHandler, Flex, Hoverable, - MainAxisSize, MouseInBehavior, MouseStateHandle, ParentElement, Radius, SavePosition, - ScrollTarget, ScrollToPositionMode, ScrollbarWidth, Text, + Border, ChildAnchor, Clipped, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, + Container, CornerRadius, CrossAxisAlignment, DispatchEventResult, DropShadow, EventHandler, + Flex, Hoverable, MainAxisSize, MouseInBehavior, MouseStateHandle, OffsetPositioning, + ParentElement, PositionedElementAnchor, PositionedElementOffsetBounds, Radius, SavePosition, + ScrollTarget, ScrollToPositionMode, ScrollbarWidth, Stack, Text, }; use warpui::platform::Cursor; use warpui::{ @@ -15,6 +17,7 @@ use warpui::{ }; use crate::search::data_source::QueryFilter; +use crate::search::item::SearchItemDetail; use crate::search::mixer::{AddAsyncSourceOptions, SearchMixer, SearchMixerEvent}; use crate::search::result_renderer::{QueryResultRenderer, QueryResultRendererStyles}; use crate::terminal::input::buffer_model::{InputBufferModel, InputBufferUpdateEvent}; @@ -55,10 +58,65 @@ const DIVIDER_HEIGHT: f32 = 1.; const DIVIDER_VERTICAL_PADDING: f32 = 0.; +/// Width of the detail sidecar panel that appears to the right of the menu +/// when the selected row's content is truncated. Matches the menu's width +/// so the two panels share a visual rhythm. +const SIDECAR_WIDTH: f32 = MENU_WIDTH; + +/// Maximum height of the sidecar panel. Description text wraps within the +/// panel up to this height; longer descriptions are clipped at the bottom. +/// Keeps the sidecar visually balanced even for very verbose items. +const SIDECAR_MAX_HEIGHT: f32 = 240.; + +/// Horizontal gap between the menu's right edge and the sidecar's left edge. +const SIDECAR_GAP: f32 = 2.; + +/// Description font size inside the sidecar. Intentionally smaller than the +/// 14px row description to match the Figma frame and pack more text into +/// the panel. +const SIDECAR_DESCRIPTION_FONT_SIZE: f32 = 12.; + +/// Vertical gap between the sidecar's title and its description body. +const SIDECAR_TITLE_TO_DESCRIPTION_GAP: f32 = 4.; + +/// Horizontal gap between a row's name and description in the compact V2 +/// layout. Mirrors the `Container::with_margin_right(8.)` in +/// `search_item.rs` and is used by `item_is_truncated_in_row` to estimate +/// the row's available width. +const NAME_DESCRIPTION_GAP_PX: f32 = 8.; + fn row_position_id(visible_idx: usize) -> String { format!("cloud_mode_v2_slash_row_{visible_idx}") } +/// Returns `true` if rendering `detail` in a single menu row would cause +/// the row content to be truncated by `…`. For items with a description +/// (commands/skills) this measures `name + 8 + description`; for items +/// without a description (saved prompts) this measures `name` alone, since +/// the row collapses to just the title in that case. +/// +/// Estimation pattern mirrors `search_item::inline_width_for_name_column`: +/// font em-width × character count. Coarse but good enough to gate sidecar +/// rendering — short rows that fit don't get a sidecar, long rows do. +fn item_is_truncated_in_row(detail: &SearchItemDetail, app: &AppContext) -> bool { + let appearance = Appearance::as_ref(app); + let font_size = inline_styles::font_size(appearance); + let font_cache = app.font_cache(); + let name_em = font_cache.em_width(detail.title_font_family, font_size); + let name_px = name_em * detail.title.chars().count() as f32; + // 16px horizontal padding on each side + icon (16) + icon margin (8). + let row_chrome_px = MENU_HORIZONTAL_PADDING * 2. + ICON_SIZE + inline_styles::ICON_MARGIN; + let available = MENU_WIDTH - row_chrome_px; + match &detail.description { + Some(description) => { + let description_em = font_cache.em_width(appearance.ui_font_family(), font_size); + let description_px = description_em * description.chars().count() as f32; + (name_px + NAME_DESCRIPTION_GAP_PX + description_px) > available + } + None => name_px > available, + } +} + static QUERY_RESULT_RENDERER_STYLES: LazyLock = LazyLock::new(|| QueryResultRendererStyles { result_item_height_fn: |appearance| appearance.monospace_font_size() + 8., @@ -822,46 +880,140 @@ impl CloudModeV2SlashCommandView { .with_vertical_padding(ROW_VERTICAL_PADDING) .finish() } -} - -impl Entity for CloudModeV2SlashCommandView { - type Event = SlashCommandsEvent; -} -impl TypedActionView for CloudModeV2SlashCommandView { - type Action = CloudModeV2SlashCommandAction; - - fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { - match action { - CloudModeV2SlashCommandAction::Accept { - item, - cmd_or_ctrl_enter, + /// Returns the `SearchItemDetail` for the currently selected row, if + /// the row is selectable and its underlying `SearchItem` provides + /// detail data. Used to drive sidecar rendering. + fn selected_detail_data(&self) -> Option { + match &self.menu_state { + MenuState::NoSearchActive { + sections, + expanded_sections, + selected_idx, + .. } => { - self.emit_selection(item, *cmd_or_ctrl_enter, ctx); - } - CloudModeV2SlashCommandAction::HoverIdx(idx) => { - let idx = *idx; - match &self.menu_state { - MenuState::NoSearchActive { .. } => self.set_browsing_selection(idx, ctx), - MenuState::SearchActive { .. } => self.set_search_selection(idx, ctx), + let idx = (*selected_idx)?; + let row = browsing_rows(sections, expanded_sections).get(idx).copied()?; + match row { + NoSearchActiveRow::Item { section, item_idx } => { + let rendered = sections.iter().find(|s| s.section == section)?; + let renderer = rendered.items.get(item_idx)?; + renderer.search_result.detail_data() + } + _ => None, } } - CloudModeV2SlashCommandAction::ToggleSection(section) => { - self.toggle_section(*section, ctx); - } - CloudModeV2SlashCommandAction::Dismiss => { - self.dismiss(ctx); + MenuState::SearchActive { + results, + selected_idx, + } => { + let idx = (*selected_idx)?; + let renderer = results.get(idx)?; + renderer.search_result.detail_data() } } } -} -impl View for CloudModeV2SlashCommandView { - fn ui_name() -> &'static str { - "CloudModeV2SlashCommandView" + /// Returns the visible-row index of the currently selected row, used + /// as the position id key for `SavePosition`-anchored sidecar + /// positioning. `None` if nothing is selected. + fn selected_visible_idx(&self) -> Option { + match &self.menu_state { + MenuState::NoSearchActive { selected_idx, .. } => *selected_idx, + MenuState::SearchActive { selected_idx, .. } => *selected_idx, + } } - fn render(&self, app: &AppContext) -> Box { + /// Builds the optional sidecar element together with the + /// `SavePosition` id of the row it should anchor to. Returns `None` + /// if no row is selected, the selected row's `SearchItem` doesn't + /// expose detail data, or its content fits within the menu row. + fn render_sidecar_if_eligible( + &self, + app: &AppContext, + ) -> Option<(String, Box)> { + let detail = self.selected_detail_data()?; + if !item_is_truncated_in_row(&detail, app) { + return None; + } + let visible_idx = self.selected_visible_idx()?; + Some((row_position_id(visible_idx), self.render_sidecar_panel(&detail, app))) + } + + /// Renders the sidecar panel chrome (border, background, drop shadow, + /// padding) wrapping the title + optional description. The panel is + /// styled to match the menu so the two visually rhyme. + fn render_sidecar_panel(&self, detail: &SearchItemDetail, app: &AppContext) -> Box { + let appearance = Appearance::as_ref(app); + let theme = appearance.theme(); + let menu_bg = inline_styles::menu_background_color(app); + let primary = inline_styles::primary_text_color(theme, menu_bg.into()); + let secondary = inline_styles::secondary_text_color(theme, menu_bg.into()); + + // Title uses the same family as the source row (monospace for + // commands/skills, UI font for saved prompts) so the sidecar + // visually echoes the row that triggered it. + let title = Text::new_inline( + detail.title.clone(), + detail.title_font_family, + inline_styles::font_size(appearance), + ) + .with_color(primary.into()) + .finish(); + + let mut column = Flex::column() + .with_cross_axis_alignment(CrossAxisAlignment::Start) + .with_main_axis_size(MainAxisSize::Min) + .with_child(title); + + // Saved prompts have no description; only commands and skills + // append a description block beneath the title. + if let Some(description_text) = detail.description.clone() { + let description = Text::new( + description_text, + appearance.ui_font_family(), + SIDECAR_DESCRIPTION_FONT_SIZE, + ) + .with_color(secondary.into()) + .finish(); + column = column.with_child( + Container::new(description) + .with_margin_top(SIDECAR_TITLE_TO_DESCRIPTION_GAP) + .finish(), + ); + } + + // `Clipped` ensures wrapped description text that exceeds + // `SIDECAR_MAX_HEIGHT` is cut off at the panel boundary instead of + // bleeding through the rounded border. The outer ConstrainedBox + // sets the max width and the max height of the entire panel. + Container::new( + ConstrainedBox::new( + Clipped::new( + Container::new(column.finish()) + .with_horizontal_padding(MENU_HORIZONTAL_PADDING) + .with_vertical_padding(ROW_VERTICAL_PADDING) + .finish(), + ) + .finish(), + ) + .with_max_width(SIDECAR_WIDTH) + .with_max_height(SIDECAR_MAX_HEIGHT) + .finish(), + ) + .with_background(Fill::Solid(menu_bg)) + .with_border(Border::all(1.).with_border_fill(Fill::Solid(theme.outline().into_solid()))) + .with_corner_radius(CornerRadius::with_all(Radius::Pixels(MENU_CORNER_RADIUS))) + .with_padding_top(MENU_VERTICAL_PADDING) + .with_padding_bottom(MENU_VERTICAL_PADDING) + .with_drop_shadow(DropShadow::default()) + .finish() + } + + /// Wraps the menu's `ClippedScrollable` content in the menu's chrome + /// (border, background, corner radius, padding, drop shadow). Pulled + /// out so `View::render` can compose it next to the sidecar. + fn render_menu_panel(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); let menu_bg = inline_styles::menu_background_color(app); @@ -925,6 +1077,71 @@ impl View for CloudModeV2SlashCommandView { } } +impl Entity for CloudModeV2SlashCommandView { + type Event = SlashCommandsEvent; +} + +impl TypedActionView for CloudModeV2SlashCommandView { + type Action = CloudModeV2SlashCommandAction; + + fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { + match action { + CloudModeV2SlashCommandAction::Accept { + item, + cmd_or_ctrl_enter, + } => { + self.emit_selection(item, *cmd_or_ctrl_enter, ctx); + } + CloudModeV2SlashCommandAction::HoverIdx(idx) => { + let idx = *idx; + match &self.menu_state { + MenuState::NoSearchActive { .. } => self.set_browsing_selection(idx, ctx), + MenuState::SearchActive { .. } => self.set_search_selection(idx, ctx), + } + } + CloudModeV2SlashCommandAction::ToggleSection(section) => { + self.toggle_section(*section, ctx); + } + CloudModeV2SlashCommandAction::Dismiss => { + self.dismiss(ctx); + } + } + } +} + +impl View for CloudModeV2SlashCommandView { + fn ui_name() -> &'static str { + "CloudModeV2SlashCommandView" + } + + fn render(&self, app: &AppContext) -> Box { + let menu_panel = self.render_menu_panel(app); + let Some((row_position_id, sidecar)) = self.render_sidecar_if_eligible(app) else { + return menu_panel; + }; + // Anchor the sidecar's bottom-left to the selected row's + // bottom-right via the row's `SavePosition` id, mirroring the + // Figma frame. Using `Stack` + a positioned overlay child means + // the sidecar's vertical alignment falls out of the row's + // painted bounds in the position cache — no need for analytical + // row-height math, and scrolling the menu naturally moves the + // sidecar with the row. + let mut stack = Stack::new(); + stack.add_child(menu_panel); + stack.add_positioned_overlay_child( + sidecar, + OffsetPositioning::offset_from_save_position_element( + row_position_id, + vec2f(SIDECAR_GAP, 0.), + PositionedElementOffsetBounds::Unbounded, + PositionedElementAnchor::BottomRight, + ChildAnchor::BottomLeft, + ), + ); + stack.finish() + } +} + fn render_section_header(section: Section, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); diff --git a/app/src/terminal/input/slash_commands/search_item.rs b/app/src/terminal/input/slash_commands/search_item.rs index 5bddad1c..7c9f45f6 100644 --- a/app/src/terminal/input/slash_commands/search_item.rs +++ b/app/src/terminal/input/slash_commands/search_item.rs @@ -7,6 +7,7 @@ use warpui::prelude::{ConstrainedBox, Container, CrossAxisAlignment, Empty, Flex use warpui::{AppContext, Element, SingletonEntity}; use crate::ai::blocklist::agent_view::shortcuts::render_keystroke_with_color_overrides; +use crate::search::item::SearchItemDetail; use crate::search::slash_command_menu::static_commands::commands::COMMAND_REGISTRY; use crate::search::{ItemHighlightState, SearchItem}; use crate::terminal::input::inline_menu::styles as inline_styles; @@ -177,4 +178,14 @@ impl SearchItem for InlineItem { fn accessibility_label(&self) -> String { format!("{:?}", self.action) } + + fn detail_data(&self) -> Option { + // Mirrors the row's title/description split so the V2 sidecar can + // re-render the same fields without going through `render_item`. + Some(SearchItemDetail { + title: self.name.clone(), + description: self.description.clone(), + title_font_family: self.font_family, + }) + } } From efd3653234b707ad7869f551cc247ad2b437b42c Mon Sep 17 00:00:00 2001 From: Abhishek Pandya Date: Tue, 28 Apr 2026 23:06:14 -0400 Subject: [PATCH 2/3] remove comments --- app/src/search/data_source.rs | 2 - app/src/search/item.rs | 15 ---- .../slash_commands/cloud_mode_v2_view.rs | 77 ++++--------------- .../input/slash_commands/search_item.rs | 2 - 4 files changed, 13 insertions(+), 83 deletions(-) diff --git a/app/src/search/data_source.rs b/app/src/search/data_source.rs index 6c001759..2cfe968e 100644 --- a/app/src/search/data_source.rs +++ b/app/src/search/data_source.rs @@ -458,8 +458,6 @@ impl QueryResult { self.item.accessibility_help_message() } - /// Forwards `SearchItem::detail_data` so callers can read sidecar - /// metadata without holding the underlying `Arc`. pub fn detail_data(&self) -> Option { self.item.detail_data() } diff --git a/app/src/search/item.rs b/app/src/search/item.rs index bafab768..0525638d 100644 --- a/app/src/search/item.rs +++ b/app/src/search/item.rs @@ -7,21 +7,10 @@ use crate::appearance::Appearance; use super::result_renderer::ItemHighlightState; -/// Compact, type-erased metadata used by detail/sidecar views (e.g. the -/// V2 cloud-mode slash command menu sidecar) to render an expanded preview -/// of a search item without re-running the row layout. Items that don't -/// have a meaningful title/description pair (e.g. action shortcuts, static -/// separators) should return `None` from `SearchItem::detail_data`. #[derive(Clone)] pub struct SearchItemDetail { - /// Display title shown at the top of the sidecar. pub title: String, - /// Optional description body shown beneath the title. `None` for items - /// like saved prompts that only have a title. pub description: Option, - /// Font family used to render the title. Typically matches the row's - /// title font (monospace for slash commands and skills, UI font for - /// saved prompts). pub title_font_family: FamilyId, } @@ -129,10 +118,6 @@ pub trait SearchItem: Send + Sync { None } - /// Optional structured metadata used by detail/sidecar views to render a - /// richer preview of this item. Default returns `None`; items that - /// participate in sidecar rendering (currently only the V2 cloud-mode - /// slash command menu) override this. fn detail_data(&self) -> Option { None } diff --git a/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs b/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs index 466d8c82..9274bece 100644 --- a/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs +++ b/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs @@ -58,53 +58,28 @@ const DIVIDER_HEIGHT: f32 = 1.; const DIVIDER_VERTICAL_PADDING: f32 = 0.; -/// Width of the detail sidecar panel that appears to the right of the menu -/// when the selected row's content is truncated. Matches the menu's width -/// so the two panels share a visual rhythm. const SIDECAR_WIDTH: f32 = MENU_WIDTH; -/// Maximum height of the sidecar panel. Description text wraps within the -/// panel up to this height; longer descriptions are clipped at the bottom. -/// Keeps the sidecar visually balanced even for very verbose items. const SIDECAR_MAX_HEIGHT: f32 = 240.; -/// Horizontal gap between the menu's right edge and the sidecar's left edge. const SIDECAR_GAP: f32 = 2.; -/// Description font size inside the sidecar. Intentionally smaller than the -/// 14px row description to match the Figma frame and pack more text into -/// the panel. const SIDECAR_DESCRIPTION_FONT_SIZE: f32 = 12.; -/// Vertical gap between the sidecar's title and its description body. const SIDECAR_TITLE_TO_DESCRIPTION_GAP: f32 = 4.; -/// Horizontal gap between a row's name and description in the compact V2 -/// layout. Mirrors the `Container::with_margin_right(8.)` in -/// `search_item.rs` and is used by `item_is_truncated_in_row` to estimate -/// the row's available width. const NAME_DESCRIPTION_GAP_PX: f32 = 8.; fn row_position_id(visible_idx: usize) -> String { format!("cloud_mode_v2_slash_row_{visible_idx}") } -/// Returns `true` if rendering `detail` in a single menu row would cause -/// the row content to be truncated by `…`. For items with a description -/// (commands/skills) this measures `name + 8 + description`; for items -/// without a description (saved prompts) this measures `name` alone, since -/// the row collapses to just the title in that case. -/// -/// Estimation pattern mirrors `search_item::inline_width_for_name_column`: -/// font em-width × character count. Coarse but good enough to gate sidecar -/// rendering — short rows that fit don't get a sidecar, long rows do. fn item_is_truncated_in_row(detail: &SearchItemDetail, app: &AppContext) -> bool { let appearance = Appearance::as_ref(app); let font_size = inline_styles::font_size(appearance); let font_cache = app.font_cache(); let name_em = font_cache.em_width(detail.title_font_family, font_size); let name_px = name_em * detail.title.chars().count() as f32; - // 16px horizontal padding on each side + icon (16) + icon margin (8). let row_chrome_px = MENU_HORIZONTAL_PADDING * 2. + ICON_SIZE + inline_styles::ICON_MARGIN; let available = MENU_WIDTH - row_chrome_px; match &detail.description { @@ -881,9 +856,6 @@ impl CloudModeV2SlashCommandView { .finish() } - /// Returns the `SearchItemDetail` for the currently selected row, if - /// the row is selectable and its underlying `SearchItem` provides - /// detail data. Used to drive sidecar rendering. fn selected_detail_data(&self) -> Option { match &self.menu_state { MenuState::NoSearchActive { @@ -893,7 +865,9 @@ impl CloudModeV2SlashCommandView { .. } => { let idx = (*selected_idx)?; - let row = browsing_rows(sections, expanded_sections).get(idx).copied()?; + let row = browsing_rows(sections, expanded_sections) + .get(idx) + .copied()?; match row { NoSearchActiveRow::Item { section, item_idx } => { let rendered = sections.iter().find(|s| s.section == section)?; @@ -914,9 +888,6 @@ impl CloudModeV2SlashCommandView { } } - /// Returns the visible-row index of the currently selected row, used - /// as the position id key for `SavePosition`-anchored sidecar - /// positioning. `None` if nothing is selected. fn selected_visible_idx(&self) -> Option { match &self.menu_state { MenuState::NoSearchActive { selected_idx, .. } => *selected_idx, @@ -924,35 +895,29 @@ impl CloudModeV2SlashCommandView { } } - /// Builds the optional sidecar element together with the - /// `SavePosition` id of the row it should anchor to. Returns `None` - /// if no row is selected, the selected row's `SearchItem` doesn't - /// expose detail data, or its content fits within the menu row. - fn render_sidecar_if_eligible( - &self, - app: &AppContext, - ) -> Option<(String, Box)> { + fn render_sidecar_if_eligible(&self, app: &AppContext) -> Option<(String, Box)> { let detail = self.selected_detail_data()?; if !item_is_truncated_in_row(&detail, app) { return None; } let visible_idx = self.selected_visible_idx()?; - Some((row_position_id(visible_idx), self.render_sidecar_panel(&detail, app))) + Some(( + row_position_id(visible_idx), + self.render_sidecar_panel(&detail, app), + )) } - /// Renders the sidecar panel chrome (border, background, drop shadow, - /// padding) wrapping the title + optional description. The panel is - /// styled to match the menu so the two visually rhyme. - fn render_sidecar_panel(&self, detail: &SearchItemDetail, app: &AppContext) -> Box { + fn render_sidecar_panel( + &self, + detail: &SearchItemDetail, + app: &AppContext, + ) -> Box { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); let menu_bg = inline_styles::menu_background_color(app); let primary = inline_styles::primary_text_color(theme, menu_bg.into()); let secondary = inline_styles::secondary_text_color(theme, menu_bg.into()); - // Title uses the same family as the source row (monospace for - // commands/skills, UI font for saved prompts) so the sidecar - // visually echoes the row that triggered it. let title = Text::new_inline( detail.title.clone(), detail.title_font_family, @@ -966,8 +931,6 @@ impl CloudModeV2SlashCommandView { .with_main_axis_size(MainAxisSize::Min) .with_child(title); - // Saved prompts have no description; only commands and skills - // append a description block beneath the title. if let Some(description_text) = detail.description.clone() { let description = Text::new( description_text, @@ -983,10 +946,6 @@ impl CloudModeV2SlashCommandView { ); } - // `Clipped` ensures wrapped description text that exceeds - // `SIDECAR_MAX_HEIGHT` is cut off at the panel boundary instead of - // bleeding through the rounded border. The outer ConstrainedBox - // sets the max width and the max height of the entire panel. Container::new( ConstrainedBox::new( Clipped::new( @@ -1010,9 +969,6 @@ impl CloudModeV2SlashCommandView { .finish() } - /// Wraps the menu's `ClippedScrollable` content in the menu's chrome - /// (border, background, corner radius, padding, drop shadow). Pulled - /// out so `View::render` can compose it next to the sidecar. fn render_menu_panel(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); @@ -1119,13 +1075,6 @@ impl View for CloudModeV2SlashCommandView { let Some((row_position_id, sidecar)) = self.render_sidecar_if_eligible(app) else { return menu_panel; }; - // Anchor the sidecar's bottom-left to the selected row's - // bottom-right via the row's `SavePosition` id, mirroring the - // Figma frame. Using `Stack` + a positioned overlay child means - // the sidecar's vertical alignment falls out of the row's - // painted bounds in the position cache — no need for analytical - // row-height math, and scrolling the menu naturally moves the - // sidecar with the row. let mut stack = Stack::new(); stack.add_child(menu_panel); stack.add_positioned_overlay_child( diff --git a/app/src/terminal/input/slash_commands/search_item.rs b/app/src/terminal/input/slash_commands/search_item.rs index 7c9f45f6..f5850dad 100644 --- a/app/src/terminal/input/slash_commands/search_item.rs +++ b/app/src/terminal/input/slash_commands/search_item.rs @@ -180,8 +180,6 @@ impl SearchItem for InlineItem { } fn detail_data(&self) -> Option { - // Mirrors the row's title/description split so the V2 sidecar can - // re-render the same fields without going through `render_item`. Some(SearchItemDetail { title: self.name.clone(), description: self.description.clone(), From 9187f640e27022dc5015c72ebcf7af255381860b Mon Sep 17 00:00:00 2001 From: Abhishek Pandya Date: Tue, 28 Apr 2026 23:21:02 -0400 Subject: [PATCH 3/3] robot feedback --- app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs b/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs index 9274bece..ec37f3b7 100644 --- a/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs +++ b/app/src/terminal/input/slash_commands/cloud_mode_v2_view.rs @@ -1082,7 +1082,7 @@ impl View for CloudModeV2SlashCommandView { OffsetPositioning::offset_from_save_position_element( row_position_id, vec2f(SIDECAR_GAP, 0.), - PositionedElementOffsetBounds::Unbounded, + PositionedElementOffsetBounds::WindowByPosition, PositionedElementAnchor::BottomRight, ChildAnchor::BottomLeft, ),